Files
delivery-uniapp/pages/index/index.vue

581 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 接单页将原首页替换为接单页面含订单卡片顶部 tab底部批量操作栏 -->
<template>
<view class="receive-page">
<!-- 顶部区域状态 + tabs -->
<view class="top-area" :style="headerStyle">
<view class="top-bg"></view>
<view class="top-inner">
<view class="user-info">
<image class="user-avatar" @tap="sheep.$router.go('/pages/index/user')"
:src="driverInfo.avatar || defaultAvatar" mode="cover" />
<view class="user-meta">
<!-- <text class="user-status" @click="toggleOnline">{{ driverInfo.isOnline ? '在线中' : '离线' }}</text> -->
<text class="user-status">
{{ driverInfo.onlineStatus == 0 ? '离线' : (driverInfo.onlineStatus == 1 ? '在线' : '待审核') }}
</text>
</view>
</view>
<view class="tabs">
<view :class="['tab', activeTab === 'pickup' ? 'active' : '']" @click="switchTab('pickup')">
<text>待取货</text>
<text class="count">({{ pickupCount }})</text>
</view>
<view :class="['tab', activeTab === 'delivering' ? 'active' : '']" @click="switchTab('delivering')">
<text>配送中</text>
<text class="count">({{ deliveringCount }})</text>
</view>
</view>
</view>
</view>
<!-- 输入用户单编码 弹窗 -->
<order-code-popup :show="orderPopupShow" @close="orderPopupShow = false" @confirm="onConfirmCode" />
<!-- 订单列表 -->
<scroll-view class="order-list" scroll-y="true" :style="{ height: listHeight + 'px' }">
<view v-for="order in filteredOrders" :key="order.id" class="order-card">
<view @click="toDetail(order.id)">
<!-- 头部编号 -->
<view class="order-header">
<view class="order-badge">{{ order.type === 'pickup' ? '取' : '送' }}</view>
<view class="order-title">
<text class="shop-name">{{ order.shopName }}</text>
<text class="order-id">#{{ order.id }}</text>
</view>
<view class="order-status">{{ order.statusText }}</view>
</view>
<!-- 地址信息 -->
<view class="order-info">
<view class="address-row">
<view class="icon pickup"></view>
<view class="address-content">
<text class="address-title">{{ order.shopAddress }}</text>
<text class="address-sub">商家 · {{ order.shopPhone || '' }}</text>
</view>
<view class="nav-icon" @click="openMap(order.shopLat, order.shopLng, order.shopAddress)">导航</view>
</view>
<view class="address-row">
<view class="icon deliver"></view>
<view class="address-content">
<text class="address-title">{{ order.deliveryAddress }}</text>
<text class="address-sub">收货人{{ order.receiverName }} {{ order.receiverPhone ? ('尾号' + (order.receiverPhone + '').slice(-4)) : ''}}</text>
</view>
<view class="nav-icon" @click="openMap(order.deliveryLat, order.deliveryLng, order.deliveryAddress)">导航</view>
</view>
</view>
</view>
<!-- 操作区 -->
<view class="order-actions">
<view class="contact" @click="callPhone(order.receiverPhone)">
<text>联系</text>
</view>
<view class="confirm" @click="confirmArrive(order.id)" v-if="order.deliveryStatus == 2">
<text>确认到店</text>
</view>
<view class="confirm" @click="confirmPickup(order.id)" v-if="order.deliveryStatus == 3">
<text>确认取餐</text>
</view>
</view>
</view>
</scroll-view>
<!-- 底部批量操作栏 -->
<view class="bottom-bar">
<view class="batch-item" @click="scanQr">扫一扫取单</view>
<view class="batch-item" @click="openManualInput">输入用户单编码</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import sheep from '@/sheep';
import { onShow } from '@dcloudio/uni-app';
import OrderCodePopup from './components/order-code-popup.vue';
import DeliveryOrderApi from '@/sheep/api/member/deliveryOrder';
// 驿站/骑手信息(从 store 获取或 mock
const driverInfo = ref({
isOnline: true,
nickName: '骑手张三',
avatar: ''
});
const defaultAvatar = 'https://huichibao.oss-cn-guangzhou.aliyuncs.com/1/material/348b8223-8d03-46aa-8836-6757e8beebd2.png';
// 页面状态
const activeTab = ref('pickup'); // 'pickup' | 'delivering'
const listHeight = ref(600);
const loading = ref(false);
const noMore = ref(false);
// 配送单列表数据
const orders = ref([]);
const pagination = ref({
pageNo: 1,
pageSize: 10,
total: 0
});
// deliveryStatus 到页面 type 的映射
const statusToTypeMap = {
'3': 'pickup', // 骑手待取货 -> 待取货
'4': 'delivering', // 配送中待送达交接点 -> 配送中
'5': 'delivering', // 配送中送达交接点待分配 -> 配送中
'6': 'delivering' // 配送中待送达顾客 -> 配送中
};
// deliveryStatus 状态文本映射
const deliveryStatusTextMap = {
'-1': '配送异常',
'0': '已取消',
'1': '待接单',
'2': '骑手待到店',
'3': '待取货',
'4': '待送达交接点',
'5': '送达交接点待分配',
'6': '待送达顾客',
'7': '已完成'
};
// 计算各 tab 数量
const pickupCount = computed(() => orders.value.filter(o => o.type === 'pickup').length);
const deliveringCount = computed(() => orders.value.filter(o => o.type === 'delivering').length);
const filteredOrders = computed(() => orders.value);
// 加载订单列表数据
async function loadOrders(isLoadMore = false) {
if (loading.value) return;
loading.value = true;
// 根据当前 tab 确定接口参数 status
const status = activeTab.value === 'pickup' ? 1 : 2;
try {
const res = await DeliveryOrderApi.getPageByDeliveryManId({
pageNo: pagination.value.pageNo,
pageSize: pagination.value.pageSize,
status: status
});
if (res.code === 0 && res.data) {
const records = res.data.records || [];
// 转换接口数据为页面所需格式
const transformedOrders = records.map(item => {
const deliveryStatus = String(item.deliveryStatus);
return {
id: item.id,
type: statusToTypeMap[deliveryStatus] || 'pickup',
statusText: deliveryStatusTextMap[deliveryStatus] || '未知状态',
shopName: item.shopName || '',
shopAddress: item.shopAddress || '',
shopLat: item.shopLatitude || null,
shopLng: item.shopLongitude || null,
shopPhone: item.shopPhone || '',
shopShipmentStatus: item.shopShipmentStatus,
deliveryAddress: item.receiverAddress || '',
deliveryLat: item.receiverLatitude || null,
deliveryLng: item.receiverLongitude || null,
receiverName: item.receiverName || '',
receiverPhone: item.receiverPhone || '',
deliveryStatus: item.deliveryStatus
};
});
if (isLoadMore) {
orders.value = [...orders.value, ...transformedOrders];
} else {
orders.value = transformedOrders;
}
// 更新分页信息
pagination.value.total = res.data.total || 0;
pagination.value.pageNo = res.data.current || 1;
// 判断是否还有更多数据
noMore.value = orders.value.length >= pagination.value.total;
} else {
sheep.$helper.toast(res.msg || '加载失败');
}
} catch (error) {
console.error('加载订单列表异常:', error);
sheep.$helper.toast('加载失败,请重试');
} finally {
loading.value = false;
}
}
// 刷新列表
function refreshOrders() {
pagination.value.pageNo = 1;
noMore.value = false;
loadOrders(false);
}
// 切换 tab
function switchTab(tab) {
if (activeTab.value === tab) return;
activeTab.value = tab;
refreshOrders();
}
// 切换上线/下线(简单 UI 切换,建议接入后端)
function toggleOnline() {
driverInfo.value.isOnline = !driverInfo.value.isOnline;
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast(driverInfo.value.isOnline ? '已上线' : '已下线');
}
// 确认到店
async function confirmArrive(orderId) {
const order = orders.value.find(o => o.id === orderId);
if (!order) return;
if (order.type !== 'pickup') return;
try {
const res = await DeliveryOrderApi.riderConfirmArrival(orderId);
if (res.code === 0 && res.data === true) {
// 接口返回成功,更新本地订单状态
order.type = 'delivering';
order.statusText = '配送中';
order.deliveryStatus = 4; // 状态更新为待送达交接点
sheep.$helper.toast('已确认到店,开始配送');
} else {
sheep.$helper.toast(res.msg || '确认到店失败');
}
} catch (error) {
console.error('确认到店异常:', error);
sheep.$helper.toast('操作异常,请重试');
}
}
// 确认取餐
async function confirmPickup(orderId) {
const order = orders.value.find(o => o.id === orderId);
if (!order) return;
try {
const res = await DeliveryOrderApi.riderConfirmPickup(orderId);
if (res.code === 0 && res.data === true) {
// 接口返回成功,更新本地订单状态为配送中
order.type = 'delivering';
order.statusText = '配送中';
order.deliveryStatus = 4; // 状态更新为待送达交接点
sheep.$helper.toast('已确认取餐,开始配送');
} else {
sheep.$helper.toast(res.msg || '确认取餐失败');
}
} catch (error) {
console.error('确认取餐异常:', error);
sheep.$helper.toast('操作异常,请重试');
}
}
// 拨打电话
function callPhone(phone) {
if (!phone) {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('未找到联系电话');
return;
}
uni.makePhoneCall({
phoneNumber: phone
});
}
// 打开地图导航(使用 openLocation 打开经纬度或直接跳转小程序地图)
function openMap(lat, lng, name) {
if (!lat || !lng) {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('无法获取坐标');
return;
}
uni.openLocation({
latitude: Number(lat),
longitude: Number(lng),
name: name || '',
scale: 18
});
}
// 底部操作(扫码、手动输入)
function scanQr() {
uni.scanCode({
onlyFromCamera: false,
success(res) {
const code = res.result || res.path || '';
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('已识别:' + code);
},
fail() {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('扫码失败');
}
});
}
const orderPopupShow = ref(false);
function openManualInput() {
// 打开手动输入弹窗(使用 uView Plus 的 up-popup
orderPopupShow.value = true;
}
function onConfirmCode(payload) {
// payload 包含 code, result, remark, images
// 这里简单展示提示,实际应调用后端或触发下一步逻辑
console.log('confirmed code:', payload);
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('交接已确认');
}
const headerStyle = ref({});
function setHeaderSafeArea() {
try {
const sys = uni.getSystemInfoSync();
const statusBarHeightPx = sys?.statusBarHeight || 0;
const windowWidth = sys?.windowWidth || 375;
// 将 px 转为 rpx rpx = px / windowWidth * 750
const statusBarHeightRpx = Math.round((statusBarHeightPx / windowWidth) * 750);
headerStyle.value = {
paddingTop: statusBarHeightPx + 'px',
'--statusbar': statusBarHeightRpx + 'rpx'
};
} catch (e) {
// ignore
}
}
//跳转订单详情
function toDetail(id) {
uni.navigateTo({
url: `/pages/order/detail?orderId=${id}`
})
}
onMounted(() => {
// setHeaderSafeArea();
});
onShow(() => {
// 每次页面显示时重新计算(兼容热更或状态变化)
setHeaderSafeArea();
// 加载订单列表
refreshOrders();
});
</script>
<style scoped>
.receive-page {
background: #fff;
min-height: 100vh;
position: relative;
}
.top-area {
position: relative;
/* 兼容刘海屏安全区处理 */
padding-top: constant(safe-area-inset-top);
padding-top: env(safe-area-inset-top);
height: 220rpx;
}
.top-bg {
position: absolute;
left: 0;
right: 0;
top: 0;
/* 背景高度需要包含安全区高度 */
height: 270rpx;
height: calc(270rpx + constant(safe-area-inset-top));
height: calc(270rpx + env(safe-area-inset-top));
height: calc(270rpx + var(--statusbar, 0rpx));
background: #c292ee;
border-bottom-left-radius: 12rpx;
border-bottom-right-radius: 12rpx;
z-index: 0;
}
.top-inner {
position: absolute;
left: 0;
right: 0;
top: calc(var(--statusbar, 0rpx) + 30rpx);
z-index: 1;
padding: 0 30rpx;
display: flex;
flex-direction: column;
}
.user-info {
display: flex;
flex-direction: row;
align-items: center;
}
.user-avatar {
width: 110rpx;
height: 110rpx;
border-radius: 55rpx;
border: 4rpx solid rgba(255,255,255,0.6);
}
.user-meta {
margin-left: 20rpx;
}
.user-name {
font-size: 32rpx;
color: #fff;
font-weight: 700;
}
.user-status {
margin-top: 8rpx;
color: rgba(255,255,255,0.9);
font-size: 26rpx;
}
.tabs {
margin-top: 18rpx;
display: flex;
flex-direction: row;
gap: 20rpx;
}
.tab {
padding: 10rpx 20rpx;
background: rgba(255,255,255,0.12);
border-radius: 40rpx;
color: #fff;
display: flex;
align-items: center;
}
.tab.active {
background: #fff;
color: #6b3aa6;
}
.tab .count {
margin-left: 8rpx;
}
.order-list {
padding: 20rpx;
padding-right: 20rpx;
box-sizing: border-box;
background: #f7f7f7;
}
.order-card {
background: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 18rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
overflow: hidden;
box-sizing: border-box;
max-width: 100%;
}
.order-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.order-badge {
width: 54rpx;
height: 54rpx;
border-radius: 27rpx;
background: #e6f7ff;
color: #1890ff;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
}
.order-title {
flex: 1;
margin-left: 12rpx;
}
.shop-name {
font-size: 28rpx;
font-weight: 700;
}
.order-id {
color: #999;
margin-left: 8rpx;
}
.order-status {
color: #ff7a45;
}
.order-info {
margin-top: 8rpx;
}
.address-row {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 10rpx;
}
.icon {
width: 38rpx;
height: 38rpx;
border-radius: 19rpx;
background: #f2f2f2;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10rpx;
font-weight: 700;
}
.icon.pickup { background: #87d6ff; color: #fff; }
.icon.deliver { background: #ffd591; color: #fff; }
.address-content { flex: 1; min-width: 0; }
.address-title {
display: block;
font-size: 26rpx;
font-weight: 600;
/* 支持长地址换行,防止撑开布局 */
white-space: normal;
word-break: break-word;
}
.address-sub { display: block; font-size: 22rpx; color: #888; margin-top: 6rpx; }
.nav-icon { color: #1e9fff; padding: 6rpx 10rpx; }
.order-note { background: #f6f6f6; padding: 12rpx; border-radius: 8rpx; margin-top: 12rpx; color: #666; }
.order-actions {
display: flex;
flex-direction: row;
margin-top: 12rpx;
gap: 12rpx;
}
.contact {
flex: 1;
background: #fff;
border: 1rpx solid #ddd;
padding: 14rpx;
text-align: center;
border-radius: 8rpx;
color: #333;
}
.confirm {
flex: 2;
background: #1e9fff;
padding: 14rpx;
text-align: center;
border-radius: 8rpx;
color: #fff;
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 110rpx;
background: rgba(255,255,255,0.98);
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
border-top: 1rpx solid #eee;
}
.batch-item {
background: #fff;
padding: 14rpx 20rpx;
border-radius: 40rpx;
border: 1rpx solid #ddd;
}
</style>