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

832 lines
22 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" @tap="sheep.$router.go('/pages/index/user')">
<image class="user-avatar" :src="driverInfo.avatar || defaultAvatar" mode="cover" />
<view class="user-meta">
<!-- <text class="user-status" @click="toggleOnline">{{ driverInfo.isOnline ? '在线中' : '离线' }}</text> -->
<text class="user-status" v-if="driverInfo.nickname">
{{ driverInfo.onlineStatus == 0 ? '离线' : (driverInfo.onlineStatus == 1 ? '在线' : '待审核') }}
</text>
<text class="user-status" v-else>
请登录
</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.stop="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.stop="openMap(order.deliveryLat, order.deliveryLng, order.deliveryAddress)">导航</view>
</view>
</view>
</view>
<!-- 操作区 -->
<view class="order-actions">
<view class="contact" @click="openRemindPopupWithOrder(order)">
<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 class="confirm" style="background: #ffaa00;" v-if="order.deliveryStatus == 4">
<text>送达交接点</text>
</view>
<view class="confirm" v-if="order.deliveryStatus == 5" @click="openDeliveryPopup(order)">
<text>确认送达顾客</text>
</view>
</view>
</view>
</scroll-view>
<!-- 催单弹框 -->
<up-popup v-model:show="showRemind" mode="bottom" :closeable="false" border-radius="12">
<view class="remind-popup">
<view class="remind-row">
<text class="remind-title">联系商家</text>
<view class="remind-btn" @click.stop="callShopPhone">拨打电话</view>
</view>
<view class="remind-row">
<text class="remind-title">联系顾客</text>
<view class="remind-btn" @click.stop="callCustomerPhone">拨打电话</view>
</view>
</view>
</up-popup>
<!-- 底部批量操作栏 -->
<view class="bottom-bar">
<view v-if="activeTab == 'delivering'" class="batch-operation" @click="openBatchOperation">批量操作</view>
<view class="batch-item" @click="scanQr">扫一扫取单</view>
<view class="batch-item" @click="openManualInput">输入用户单编码</view>
</view>
<!-- 批量操作模态弹框 -->
<u-modal :show="batchModalShow" :title="batchModalTitle" :content="batchModalContent" confirmText="确认送达" cancelText="取消"
:showCancelButton="hasDeliveryOrders" @confirm="onBatchConfirm" @cancel="batchModalShow = false" @close="batchModalShow = false"
:closeOnClickOverlay="false" :zoom="true" confirmColor="#1e9fff" />
<!-- 批量送达交接点异步操作弹框 -->
<u-modal :show="batchLoadingModalShow" :title="batchLoadingModalTitle" :content="batchLoadingModalContent" confirmText="确认送达"
:showCancelButton="false" :asyncClose="true" @confirm="onBatchLoadingConfirm" ref="batchLoadingModalRef"
confirmColor="#1e9fff" />
<!-- 确认送达顾客弹框 -->
<DeliveryPopup
:show="showDeliveryPopup"
:receiverPhone="currentOrderForDelivery?.receiverPhone || ''"
@update:show="showDeliveryPopup = $event"
@submit="handleDeliveryConfirm"
@close="showDeliveryPopup = false"
/>
</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';
import DeliveryPopup from './components/delivery-popup.vue';
//骑手信息
const driverInfo = computed(() => sheep.$store('user').userInfo);
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;
if (driverInfo.value.auditStatus != 2) 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 {
// 清空列表,防止旧数据残留
if (!isLoadMore) {
orders.value = [];
}
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('操作异常,请重试');
}
}
// 催单弹框控制
const showRemind = ref(false);
const currentOrder = ref(null);
// 批量操作模态弹框控制
const batchModalShow = ref(false);
const batchModalTitle = ref('当前送达交接点订单数');
const batchModalContent = ref('');
const hasDeliveryOrders = ref(false);
// 批量送达异步操作弹框控制
const batchLoadingModalShow = ref(false);
const batchLoadingModalTitle = ref('当前送达交接点订单数');
const batchLoadingModalContent = ref('');
const batchOrderCount = ref(0);
// 确认送达顾客弹框控制
const showDeliveryPopup = ref(false);
const currentOrderForDelivery = ref(null);
function openRemindPopupWithOrder(order) {
currentOrder.value = order;
showRemind.value = true;
}
function openRemindPopup() {
showRemind.value = true;
}
function closeRemindPopup() {
showRemind.value = false;
}
function callShopPhone(phone) {
//订单获取商家电话
const orderPhone = currentOrder.value?.shopPhone || '';
if (!orderPhone) {
sheep.$helper.toast('未找到商家电话');
return;
}
callPhone(orderPhone);
closeRemindPopup();
}
function callCustomerPhone(phone) {
//从订单获取顾客电话
const orderPhone = currentOrder.value?.receiverPhone || '';
if (!orderPhone) {
sheep.$helper.toast('未找到顾客电话');
return;
}
callPhone(orderPhone);
closeRemindPopup();
}
// 拨打电话
function callPhone(phone) {
console.log("电话:", phone)
if (!phone) {
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,
success: function () {
console.log('success');
},
error: function (err) {
console.log("错误信息:", err);
}
});
}
// 底部操作(扫码、手动输入)
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 openBatchOperation() {
// 筛选出配送中状态为待送达交接点(ddeliveryStatus=4)的订单
const deliveryOrders = orders.value.filter(o => o.type === 'delivering' && o.deliveryStatus === 4);
hasDeliveryOrders.value = deliveryOrders.length > 0;
if (hasDeliveryOrders.value) {
// 有待送达交接点的订单
batchModalTitle.value = '当前送达交接点订单数';
batchModalContent.value = `${deliveryOrders.length}`;
batchModalShow.value = true;
} else {
// 没有待送达交接点的订单
batchModalTitle.value = '';
batchModalContent.value = '暂无待送达交接点的订单';
batchModalShow.value = true;
}
}
// 批量送达交接点确认
function onBatchConfirm() {
// 筛选出配送中状态为待送达交接点的订单
const deliveryOrders = orders.value.filter(o => o.type === 'delivering' && o.deliveryStatus === 4);
batchOrderCount.value = deliveryOrders.length;
if (deliveryOrders.length > 0) {
// 关闭第一个弹框,打开异步操作弹框
batchModalShow.value = false;
batchLoadingModalTitle.value = '当前送达交接点订单数';
batchLoadingModalContent.value = `${deliveryOrders.length}`;
batchLoadingModalShow.value = true;
} else {
// 没有订单,直接关闭
batchModalShow.value = false;
}
}
// 批量送达交接点异步确认(真正执行批量操作)
async function onBatchLoadingConfirm() {
// 筛选出配送中状态为待送达交接点的订单ID
const deliveryOrderIds = orders.value
.filter(o => o.type === 'delivering' && o.deliveryStatus === 4)
.map(o => o.id);
if (deliveryOrderIds.length === 0) {
batchLoadingModalShow.value = false;
return;
}
try {
// 调用批量送达交接点接口ids 为逗号分隔的订单ID字符串
const res = await DeliveryOrderApi.riderDeliveryHandoverBatch(deliveryOrderIds.join(','));
if (res.code === 0 && res.data === true) {
// 关闭异步弹框
batchLoadingModalShow.value = false;
sheep.$helper.toast('批量送达交接点操作成功');
// 刷新订单列表
refreshOrders();
} else {
batchLoadingModalShow.value = false;
sheep.$helper.toast(res.msg || '批量送达交接点失败');
}
} catch (error) {
console.error('批量送达交接点异常:', error);
batchLoadingModalShow.value = false;
sheep.$helper.toast('批量送达交接点失败,请重试');
}
}
function onConfirmCode(payload) {
// payload 包含 code, result, remark, images
// 这里简单展示提示,实际应调用后端或触发下一步逻辑
console.log('confirmed code:', payload);
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('交接已确认');
}
// 确认送达顾客
async function handleDeliveryConfirm(imageUrl) {
console.log('送达照片URL:', imageUrl);
if (!currentOrderForDelivery.value?.id) {
sheep.$helper.toast('订单信息异常');
return;
}
try {
const res = await DeliveryOrderApi.riderConfirmDelivery({
deliveryOrderId: currentOrderForDelivery.value.id,
imageUrl: imageUrl
});
if (res.code === 0 && res.data === true) {
sheep.$helper.toast('已提交送达照片');
showDeliveryPopup.value = false;
// 刷新订单列表
refreshOrders();
} else {
sheep.$helper.toast(res.msg || '提交失败');
}
} catch (error) {
console.error('确认送达异常:', error);
sheep.$helper.toast('提交失败,请重试');
}
}
// 打开确认送达顾客弹框
function openDeliveryPopup(order) {
currentOrderForDelivery.value = order;
showDeliveryPopup.value = true;
}
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;
}
.batch-operation {
background: #1e9fff;
color: #fff;
padding: 14rpx 20rpx;
border-radius: 40rpx;
font-weight: 500;
}
/* 催单弹框样式 */
.remind-popup {
padding: 20rpx 0;
}
.remind-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx;
background: #f6f6f6;
margin: 10rpx 16rpx;
border-radius: 8rpx;
}
.remind-title {
font-size: 28rpx;
font-weight: 700;
color: #333;
}
.remind-btn {
background: #1e9fff;
color: #fff;
padding: 10rpx 18rpx;
border-radius: 8rpx;
font-weight: 700;
}
.remind-cancel {
text-align: center;
padding: 18rpx 0 70rpx;
color: #666;
font-size: 26rpx;
background: #fff;
margin-top: 10rpx;
}
</style>