Files
medical-mall/pages/mall/delivery/index.uvue
2026-01-26 17:12:37 +08:00

1056 lines
23 KiB
Plaintext
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.
<!-- 配送端首页 - UTS Android 兼容 -->
<template>
<view class="delivery-container">
<!-- 头部状态栏 -->
<view class="header">
<view class="driver-info">
<image :src="driverInfo.avatar_url || '/static/default-avatar.png'" class="avatar" mode="aspectFit" />
<view class="driver-details">
<text class="driver-name">{{ driverInfo.real_name }}</text>
<text class="work-status" :class="getWorkStatusClass()">{{ getWorkStatusText() }}</text>
</view>
</view>
<view class="status-switch">
<switch :checked="isOnline" @change="toggleWorkStatus" color="#4CAF50" />
<text class="switch-label">{{ isOnline ? '在线接单' : '离线休息' }}</text>
</view>
</view>
<!-- 今日统计 -->
<view class="stats-section">
<text class="section-title">今日数据</text>
<view class="stats-grid">
<view class="stat-item">
<text class="stat-value">{{ todayStats.completed_orders }}</text>
<text class="stat-label">完成订单</text>
</view>
<view class="stat-item">
<text class="stat-value">¥{{ todayStats.total_earning }}</text>
<text class="stat-label">总收入</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ todayStats.total_distance }}km</text>
<text class="stat-label">配送距离</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ todayStats.avg_rating }}</text>
<text class="stat-label">平均评分</text>
</view>
</view>
</view>
<!-- 当前配送任务 -->
<view v-if="currentTask" class="current-task-section">
<text class="section-title">当前任务</text>
<view class="task-card">
<view class="task-header">
<text class="task-id">订单号: {{ currentTask.order_no }}</text>
<text class="task-status" :class="getTaskStatusClass(currentTask.status)">{{ getTaskStatusText(currentTask.status) }}</text>
</view>
<view class="task-addresses">
<view class="address-item">
<text class="address-icon">📍</text>
<view class="address-info">
<text class="address-label">取货地址</text>
<text class="address-text">{{ currentTask.pickup_address.detail }}</text>
<text class="contact-info">联系人: {{ currentTask.pickup_contact.name }} {{ currentTask.pickup_contact.phone }}</text>
</view>
</view>
<view class="address-line"></view>
<view class="address-item">
<text class="address-icon">🏠</text>
<view class="address-info">
<text class="address-label">收货地址</text>
<text class="address-text">{{ currentTask.delivery_address.detail }}</text>
<text class="contact-info">联系人: {{ currentTask.delivery_contact.name }} {{ currentTask.delivery_contact.phone }}</text>
</view>
</view>
</view>
<view class="task-details">
<text class="task-info">配送费: ¥{{ currentTask.delivery_fee }}</text>
<text class="task-info">预计距离: {{ currentTask.distance }}km</text>
<text class="task-info">预计时间: {{ currentTask.estimated_time }}分钟</text>
</view>
<view class="task-actions">
<!-- 根据状态显示不同的操作按钮 -->
<button v-if="currentTask.status === 1" class="action-btn primary" @click="acceptTask">接受任务</button>
<button v-if="currentTask.status === 2" class="action-btn primary" @click="startPickup">开始取货</button>
<button v-if="currentTask.status === 3" class="action-btn primary" @click="confirmPickup">确认取货</button>
<button v-if="currentTask.status === 4" class="action-btn primary" @click="startDelivery">开始配送</button>
<button v-if="currentTask.status === 5" class="action-btn primary" @click="showConfirmDeliveryDialog">确认送达</button>
<button class="action-btn secondary" @click="contactCustomer">联系客户</button>
<button class="action-btn secondary" @click="viewNavigation">查看导航</button>
<button class="action-btn secondary" @click="viewOrderDetail">查看详情</button>
</view>
</view>
</view>
<!-- 可接取订单 -->
<view v-if="!currentTask && isOnline" class="available-orders-section">
<view class="section-header">
<text class="section-title">附近订单</text>
<text class="refresh-btn" @click="refreshOrders">🔄 刷新</text>
</view>
<view v-if="availableOrders.length === 0" class="empty-orders">
<text class="empty-text">暂无可接取订单</text>
<text class="empty-subtitle">请保持在线状态,有新订单会自动推送</text>
</view>
<view v-for="order in availableOrders" :key="order.id" class="order-card">
<view class="order-header">
<text class="order-id">{{ order.order_no }}</text>
<text class="order-fee">¥{{ order.delivery_fee }}</text>
</view>
<view class="order-route">
<view class="route-item">
<text class="route-icon">📍</text>
<text class="route-text">{{ order.pickup_address.area }}</text>
</view>
<text class="route-arrow">→</text>
<view class="route-item">
<text class="route-icon">🏠</text>
<text class="route-text">{{ order.delivery_address.area }}</text>
</view>
</view>
<view class="order-info">
<text class="info-item">距离: {{ order.distance }}km</text>
<text class="info-item">预计: {{ order.estimated_time }}分钟</text>
<text class="info-item">下单: {{ formatTime(order.created_at) }}</text>
</view>
<view class="order-actions">
<button class="order-btn accept" @click="acceptOrder(order.id)">接受订单</button>
<button class="order-btn detail" @click="viewOrderDetail(order.id)">查看详情</button>
</view>
</view>
</view>
<!-- 历史记录快捷入口 -->
<view class="quick-actions-section">
<text class="section-title">快捷功能</text>
<view class="actions-grid">
<view class="action-item" @click="goToOrderHistory">
<text class="action-icon">📋</text>
<text class="action-text">历史订单</text>
</view>
<view class="action-item" @click="goToEarnings">
<text class="action-icon">💰</text>
<text class="action-text">收入明细</text>
</view>
<view class="action-item" @click="goToProfile">
<text class="action-icon">👤</text>
<text class="action-text">个人资料</text>
</view>
<view class="action-item" @click="goToSettings">
<text class="action-icon">⚙️</text>
<text class="action-text">设置</text>
</view>
</view>
</view>
</view>
</template>
<script lang="uts">
import type {
DeliveryDriverType,
DeliveryTaskType
} from '@/types/mall-types.uts'
type TodayStatsType = {
completed_orders: number
total_earning: string
total_distance: number
avg_rating: number
}
type AddressInfoType = {
detail: string
area: string
}
type ContactInfoType = {
name: string
phone: string
}
type CurrentTaskType = {
id: string
order_no: string
status: number
pickup_address: AddressInfoType
delivery_address: AddressInfoType
pickup_contact: ContactInfoType
delivery_contact: ContactInfoType
delivery_fee: number
distance: number
estimated_time: number
created_at: string
}
type AvailableOrderType = {
id: string
order_no: string
pickup_address: AddressInfoType
delivery_address: AddressInfoType
delivery_fee: number
distance: number
estimated_time: number
created_at: string
}
export default {
data() {
return {
isOnline: true,
driverInfo: {
id: '',
user_id: '',
real_name: '配送员',
id_card: '',
driver_license: '',
vehicle_type: 1,
vehicle_number: '',
work_status: 1,
current_location: null,
service_areas: [],
rating: 5.0,
total_orders: 0,
auth_status: 1,
created_at: '',
updated_at: ''
} as DeliveryDriverType,
todayStats: {
completed_orders: 0,
total_earning: '0.00',
total_distance: 0,
avg_rating: 5.0
} as TodayStatsType,
currentTask: null as CurrentTaskType | null,
availableOrders: [] as Array<AvailableOrderType>
}
},
onLoad() {
this.loadDriverInfo()
this.loadTodayStats()
this.loadCurrentTask()
this.loadAvailableOrders()
},
onShow() {
// 页面显示时刷新数据
this.refreshData()
},
methods: {
// 加载配送员信息
loadDriverInfo() {
// TODO: 调用API获取配送员信息
this.driverInfo.real_name = '张师傅'
this.driverInfo.rating = 4.8
this.driverInfo.total_orders = 1250
},
// 加载今日统计
loadTodayStats() {
// TODO: 调用API获取今日统计
this.todayStats = {
completed_orders: 8,
total_earning: '245.60',
total_distance: 45,
avg_rating: 4.9
}
},
// 加载当前任务
loadCurrentTask() {
// TODO: 调用API获取当前任务
this.currentTask = {
id: '1',
order_no: 'D202501081234',
status: 2, // 👈 设置为“已接取”,以便测试“开始取货”按钮
pickup_address: {
detail: '华强北商业区华强电子世界2楼A205',
area: '华强北'
},
delivery_address: {
detail: '南山区科技园深南大道9999号',
area: '科技园'
},
pickup_contact: {
name: '商家联系人',
phone: '138****5678'
},
delivery_contact: {
name: '张先生',
phone: '139****1234'
},
delivery_fee: 8.5,
distance: 12.5,
estimated_time: 35,
created_at: '2025-01-08T14:30:00Z'
}
},
// 加载可接取订单
loadAvailableOrders() {
if (!this.isOnline || this.currentTask) {
this.availableOrders = []
return
}
// TODO: 调用API获取附近订单
this.availableOrders = [
{
id: '2',
order_no: 'D202501081235',
pickup_address: {
detail: '福田区购物公园',
area: '购物公园'
},
delivery_address: {
detail: '南山区海岸城',
area: '海岸城'
},
delivery_fee: 12.0,
distance: 8.2,
estimated_time: 25,
created_at: '2025-01-08T15:00:00Z'
}
]
},
// 刷新数据
refreshData() {
this.loadTodayStats()
this.loadCurrentTask()
this.loadAvailableOrders()
},
// 刷新订单列表
refreshOrders() {
this.loadAvailableOrders()
uni.showToast({
title: '刷新成功',
icon: 'success'
})
},
// 切换工作状态
toggleWorkStatus(event: UniSwitchChangeEvent) {
this.isOnline = event.detail.value
if (this.isOnline) {
this.startWork()
} else {
this.stopWork()
}
},
// 开始工作
startWork() {
// TODO: 调用API开始工作上传位置
this.loadAvailableOrders()
uni.showToast({
title: '已上线接单',
icon: 'success'
})
},
// 停止工作
stopWork() {
// TODO: 调用API停止工作
this.availableOrders = []
uni.showToast({
title: '已下线休息',
icon: 'none'
})
},
// 获取工作状态样式
getWorkStatusClass(): string {
return this.isOnline ? 'status-online' : 'status-offline'
},
// 获取工作状态文本
getWorkStatusText(): string {
return this.isOnline ? '在线中' : '已离线'
},
// 获取任务状态样式
getTaskStatusClass(status: number): string {
switch (status) {
case 1: return 'task-pending'
case 2: return 'task-accepted'
case 3: return 'task-picking'
case 4: return 'task-picked'
case 5: return 'task-delivering'
default: return 'task-default'
}
},
// 获取任务状态文本
getTaskStatusText(status: number): string {
switch (status) {
case 1: return '待接取'
case 2: return '已接取'
case 3: return '取货中'
case 4: return '已取货'
case 5: return '配送中'
default: return '未知状态'
}
},
// 格式化时间
formatTime(timeStr: string): string {
const date = new Date(timeStr)
const now = new Date()
const diff = now.getTime() - date.getTime()
const minutes = Math.floor(diff / (1000 * 60))
if (minutes < 60) {
return `${minutes}分钟前`
} else {
return `${Math.floor(minutes / 60)}小时前`
}
},
// 任务操作方法
acceptTask() {
// TODO: 调用API接受任务
if (this.currentTask) {
this.currentTask.status = 2 // 更新状态为“已接取”
}
uni.showToast({
title: '任务已接受',
icon: 'success'
})
},
startPickup() {
// TODO: 调用API开始取货
if (this.currentTask) {
this.currentTask.status = 3 // 更新状态为“取货中”
}
uni.showToast({
title: '开始取货',
icon: 'success'
})
},
confirmPickup() {
// TODO: 调用API确认取货
if (this.currentTask) {
this.currentTask.status = 4 // 更新状态为“已取货”
}
uni.showToast({
title: '取货完成',
icon: 'success'
})
},
startDelivery() {
// TODO: 调用API开始配送
if (this.currentTask) {
this.currentTask.status = 5 // 更新状态为“配送中”
}
uni.showToast({
title: '开始配送',
icon: 'success'
})
},
// 显示确认送达弹框
showConfirmDeliveryDialog() {
uni.showModal({
title: '确认送达',
content: '确认商品已送到顾客手中?',
success: (res) => {
if (res.confirm) {
this.confirmDelivery()
}
}
})
},
// 确认送达
confirmDelivery() {
// TODO: 调用API确认送达
if (this.currentTask) {
// 1. 将订单状态更新为“已完成” (假设5表示已完成)
this.currentTask.status = 5;
// 2. 将已完成的任务保存到本地存储,以便历史订单页面可以读取
const completedOrder = {...this.currentTask}; // 创建副本,避免引用问题
uni.setStorageSync('completed_order_for_history', completedOrder);
}
uni.showToast({
title: '配送完成',
icon: 'success'
})
this.currentTask = null
this.loadAvailableOrders()
},
contactCustomer() {
if (this.currentTask) {
uni.makePhoneCall({
phoneNumber: this.currentTask.delivery_contact.phone
})
}
},
viewNavigation() {
// TODO: 打开地图导航
uni.showToast({
title: '打开导航',
icon: 'none'
})
},
// 查看订单详情(跳转到 order-detail 页面)
viewOrderDetail(orderId?: string) {
if (orderId) {
// 跳转到附近订单的详情页
uni.navigateTo({
url: `/pages/mall/delivery/order-detail?id=${orderId}`
})
} else if (this.currentTask) {
// 跳转到当前任务的详情页,并传递 status
uni.navigateTo({
url: `/pages/mall/delivery/order-detail?id=${this.currentTask.id}&status=${this.currentTask.status}`
})
}
},
// 订单操作方法
acceptOrder(orderId: string) {
// TODO: 调用API接受订单
uni.showToast({
title: '订单已接受',
icon: 'success'
})
this.loadCurrentTask()
this.loadAvailableOrders()
},
// 导航方法
goToOrderHistory() {
uni.navigateTo({
url: '/pages/mall/delivery/order-history'
})
},
goToEarnings() {
uni.navigateTo({
url: '/pages/mall/delivery/earnings'
})
},
goToProfile() {
uni.navigateTo({
url: '/pages/mall/delivery/profile'
})
},
goToSettings() {
uni.navigateTo({
url: '/pages/mall/delivery/settings'
})
}
}
}
</script>
<style>
/* ... 保持原有 style 部分不变 ... */
.delivery-container {
background-color: #f8f9fa;
min-height: 100vh;
padding-bottom: 40rpx;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.header {
background-color: #fff;
padding: 20rpx 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1rpx solid #e9ecef;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.driver-info {
display: flex;
align-items: center;
}
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
margin-right: 20rpx;
border: 2rpx solid #dee2e6;
}
.driver-details {
display: flex;
flex-direction: column;
justify-content: center;
}
.driver-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.work-status {
font-size: 24rpx;
padding: 6rpx 12rpx;
border-radius: 12rpx;
font-weight: 500;
}
.status-online {
background-color: #E8F5E8;
color: #4CAF50;
}
.status-offline {
background-color: #FFF3E0;
color: #FF9800;
}
.status-switch {
display: flex;
align-items: center;
gap: 10rpx;
}
.switch-label {
font-size: 22rpx;
color: #666;
}
/* 今日统计 */
.stats-section {
background-color: #fff;
margin: 20rpx;
padding: 20rpx 30rpx;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
text-align: center;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15rpx;
justify-items: center;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 15rpx;
background-color: #f8f9fa;
border-radius: 12rpx;
min-width: 120rpx;
}
.stat-value {
font-size: 36rpx;
font-weight: bold;
color: #4CAF50;
margin-bottom: 10rpx;
line-height: 1.2;
}
.stat-label {
font-size: 24rpx;
color: #666;
text-align: center;
}
/* 当前任务 */
.current-task-section {
background-color: #fff;
margin: 20rpx;
padding: 20rpx 30rpx;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
text-align: center;
}
.task-card {
border: 1rpx solid #e9ecef;
border-radius: 12rpx;
padding: 20rpx;
background-color: #ffffff;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
padding-bottom: 15rpx;
border-bottom: 1rpx solid #f8f9fa;
}
.task-id {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.task-status {
font-size: 24rpx;
padding: 6rpx 12rpx;
border-radius: 12rpx;
font-weight: 500;
}
.task-accepted {
background-color: #E3F2FD;
color: #1976D2;
}
.task-picking {
background-color: #FFF3E0;
color: #F57C00;
}
.task-delivering {
background-color: #E8F5E8;
color: #388E3C;
}
.task-addresses {
margin-bottom: 20rpx;
}
.address-item {
display: flex;
align-items: flex-start;
margin-bottom: 15rpx;
padding: 10rpx 0;
border-bottom: 1rpx dashed #e9ecef;
}
.address-icon {
font-size: 28rpx;
margin-right: 15rpx;
margin-top: 5rpx;
color: #666;
}
.address-info {
display: flex;
flex-direction: column;
flex: 1;
}
.address-label {
font-size: 24rpx;
color: #666;
margin-bottom: 8rpx;
font-weight: 500;
}
.address-text {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
word-break: break-all;
}
.contact-info {
font-size: 24rpx;
color: #666;
font-weight: 500;
}
.address-line {
width: 2rpx;
height: 30rpx;
background-color: #ddd;
margin: 10rpx 0 10rpx 14rpx;
}
.task-details {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
padding: 15rpx;
background-color: #f8f9fa;
border-radius: 8rpx;
font-size: 24rpx;
color: #666;
}
.task-info {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
font-size: 24rpx;
color: #666;
margin: 0 5rpx;
}
.task-actions {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
margin-top: 10rpx;
}
.action-btn {
flex: 1;
height: 80rpx;
border-radius: 8rpx;
font-size: 28rpx;
border: none;
font-weight: 500;
padding: 0 10rpx;
box-sizing: border-box;
}
.primary {
background-color: #4CAF50;
color: #fff;
}
.secondary {
background-color: #f0f0f0;
color: #333;
border: 1rpx solid #ddd;
}
/* 可接取订单 */
.available-orders-section {
margin: 20rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.refresh-btn {
font-size: 26rpx;
color: #4CAF50;
padding: 8rpx 16rpx;
background-color: #e8f5e8;
border-radius: 12rpx;
font-weight: 500;
}
.empty-orders {
background-color: #fff;
padding: 40rpx 30rpx;
border-radius: 16rpx;
text-align: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.empty-text {
font-size: 32rpx;
color: #999;
margin-bottom: 15rpx;
}
.empty-subtitle {
font-size: 24rpx;
color: #ccc;
}
.order-card {
background-color: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 15rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
border: 1rpx solid #e9ecef;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
padding-bottom: 15rpx;
border-bottom: 1rpx solid #f8f9fa;
}
.order-id {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.order-fee {
font-size: 32rpx;
color: #4CAF50;
font-weight: bold;
}
.order-route {
display: flex;
align-items: center;
margin-bottom: 15rpx;
padding: 10rpx 0;
border-bottom: 1rpx solid #f8f9fa;
}
.route-item {
display: flex;
align-items: center;
flex: 1;
}
.route-icon {
font-size: 24rpx;
margin-right: 8rpx;
color: #666;
}
.route-text {
font-size: 26rpx;
color: #333;
word-break: break-all;
}
.route-arrow {
font-size: 24rpx;
color: #999;
margin: 0 15rpx;
}
.order-info {
display: flex;
justify-content: space-between;
margin-bottom: 15rpx;
padding: 10rpx 0;
border-bottom: 1rpx solid #f8f9fa;
font-size: 22rpx;
color: #666;
}
.info-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
font-size: 22rpx;
color: #666;
margin: 0 5rpx;
}
.order-actions {
display: flex;
gap: 10rpx;
margin-top: 10rpx;
}
.order-btn {
flex: 1;
height: 70rpx;
border-radius: 8rpx;
font-size: 26rpx;
border: none;
font-weight: 500;
padding: 0 10rpx;
box-sizing: border-box;
}
.accept {
background-color: #4CAF50;
color: #fff;
}
.detail {
background-color: #f0f0f0;
color: #333;
border: 1rpx solid #ddd;
}
/* 历史记录快捷入口 */
.quick-actions-section {
background-color: #fff;
margin: 20rpx;
padding: 20rpx 30rpx;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.actions-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
justify-items: center;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
background-color: #f8f9fa;
border-radius: 12rpx;
min-width: 120rpx;
cursor: pointer;
transition: background-color 0.2s;
}
.action-item:hover {
background-color: #e8f5e8;
}
.action-icon {
font-size: 48rpx;
margin-bottom: 15rpx;
color: #666;
}
.action-text {
font-size: 24rpx;
color: #333;
text-align: center;
font-weight: 500;
}
</style>