1327 lines
36 KiB
Plaintext
1327 lines
36 KiB
Plaintext
<!-- 配送端首页 - 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>
|
||
<view class="section-header-actions">
|
||
<text class="refresh-btn" @click="refreshOrders">🔄 刷新</text>
|
||
</view>
|
||
</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, index) in availableOrders" :key="order.id">
|
||
<view v-if="index < 5" 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 || order.pickup_address.detail }}</text>
|
||
</view>
|
||
<text class="route-arrow">→</text>
|
||
<view class="route-item">
|
||
<text class="route-icon">🏠</text>
|
||
<text class="route-text">{{ order.delivery_address.area || order.delivery_address.detail }}</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>
|
||
|
||
<!-- 超过5个展示订单就有个加载更多 -->
|
||
<view v-if="availableOrders.length > 5" class="view-all-footer" @click="goToAllOrders">
|
||
<text class="view-all-text">查看全部订单 (共 {{ availableOrders.length }} 个待接订单) ➜</text>
|
||
</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'
|
||
|
||
import supa, { supaReady } from '@/components/supadb/aksupainstance.uts'
|
||
import { getCurrentUserId, getCurrentUser } from '@/utils/store.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,
|
||
// 防抖:记录上次刷新时间,避免 onShow 导致的频繁 refresh
|
||
lastRefreshAt: 0,
|
||
// 控制是否启用自动刷新(onShow)——默认关闭,避免频繁或意外刷新
|
||
enableAutoRefresh: false,
|
||
|
||
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>
|
||
}
|
||
},
|
||
|
||
async onLoad() {
|
||
// 确保 userProfile 已加载,以便 getCurrentUserId 能返回正确值
|
||
try {
|
||
await getCurrentUser()
|
||
} catch (e) {
|
||
console.warn('getCurrentUser failed on onLoad', e)
|
||
}
|
||
await this.loadDriverInfo()
|
||
await this.loadTodayStats()
|
||
await this.loadCurrentTask()
|
||
await this.loadAvailableOrders()
|
||
},
|
||
|
||
async onShow() {
|
||
// 自动刷新已被禁用(enableAutoRefresh = false)以避免页面抖动。
|
||
// 如需临时启用,可在控制台设置 `this.enableAutoRefresh = true`。
|
||
if (!this.enableAutoRefresh) {
|
||
console.log('onShow: auto refresh disabled')
|
||
return
|
||
}
|
||
const now = Date.now()
|
||
if (this.lastRefreshAt && (now - this.lastRefreshAt < 5000)) {
|
||
console.log('onShow: skipped refresh (debounced)')
|
||
return
|
||
}
|
||
this.lastRefreshAt = now
|
||
await this.refreshData()
|
||
},
|
||
|
||
methods: {
|
||
// 加载配送员信息
|
||
async loadDriverInfo() {
|
||
try {
|
||
const ready = await Promise.race([supaReady, new Promise(resolve => setTimeout(() => resolve(false), 1500))])
|
||
if (!ready) console.warn('supaReady timeout/failed in loadDriverInfo - proceeding')
|
||
const userId = getCurrentUserId()
|
||
if (!userId) return
|
||
// 先按 user_id 查询(userId 可能是 ak_users.id 或 auth.users.id)
|
||
let res = await supa.from('ml_delivery_drivers').select('*').eq('user_id', userId).limit(1).execute()
|
||
console.log('loadDriverInfo: try user_id=', userId, 'res=', res)
|
||
if (!(res && (res.data instanceof Array) && res.data.length > 0)) {
|
||
// 回退:尝试从 ak_users 表根据 auth_id 查出 ak_users.id
|
||
const akRes = await supa.from('ak_users').select('id').eq('auth_id', userId).limit(1).execute()
|
||
console.log('loadDriverInfo: ak_users lookup by auth_id=', userId, 'akRes=', akRes)
|
||
let akId = ''
|
||
if (akRes && Array.isArray(akRes.data) && akRes.data.length > 0) {
|
||
akId = (akRes.data[0] as any).id
|
||
}
|
||
if (akId) {
|
||
res = await supa.from('ml_delivery_drivers').select('*').eq('user_id', akId).limit(1).execute()
|
||
console.log('loadDriverInfo: retry user_id with akId=', akId, 'res=', res)
|
||
}
|
||
}
|
||
if (res && (res.data instanceof Array) && res.data.length > 0) {
|
||
const data = res.data[0] as DeliveryDriverType
|
||
this.driverInfo = Object.assign(this.driverInfo, data)
|
||
// 同步工作状态到本地变量
|
||
this.isOnline = (this.driverInfo.work_status === 1)
|
||
}
|
||
} catch (e) {
|
||
console.error('loadDriverInfo error', e)
|
||
}
|
||
},
|
||
|
||
async loadTodayStats() {
|
||
try {
|
||
const ready = await Promise.race([supaReady, new Promise(resolve => setTimeout(() => resolve(false), 1500))])
|
||
if (!ready) console.warn('supaReady timeout/failed in loadTodayStats - proceeding')
|
||
const driverId = this.driverInfo.id || null
|
||
if (!driverId) return
|
||
const start = new Date()
|
||
start.setHours(0,0,0,0)
|
||
const end = new Date()
|
||
end.setHours(23,59,59,999)
|
||
const res = await supa.from('ml_delivery_tasks')
|
||
.select('id,delivery_fee,distance,created_at,status')
|
||
.eq('driver_id', driverId)
|
||
.gte('created_at', start.toISOString())
|
||
.lte('created_at', end.toISOString())
|
||
.execute()
|
||
if (res && res.data) {
|
||
const rows = res.data as Array<any>
|
||
const completed = rows.filter(r => r.status >= 5).length
|
||
const earning = rows.reduce((s, r) => s + (Number(r.delivery_fee) || 0), 0)
|
||
const distance = rows.reduce((s, r) => s + (Number(r.distance) || 0), 0)
|
||
this.todayStats = {
|
||
completed_orders: completed,
|
||
total_earning: earning.toFixed(2),
|
||
total_distance: Number(distance.toFixed(2)),
|
||
avg_rating: this.driverInfo.rating || 0
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('loadTodayStats error', e)
|
||
}
|
||
},
|
||
|
||
async loadCurrentTask() {
|
||
try {
|
||
const ready = await Promise.race([supaReady, new Promise(resolve => setTimeout(() => resolve(false), 1500))])
|
||
if (!ready) console.warn('supaReady timeout/failed in loadCurrentTask - proceeding')
|
||
const driverId = this.driverInfo.id || null
|
||
if (!driverId) {
|
||
this.currentTask = null
|
||
return
|
||
}
|
||
const res = await supa.from('ml_delivery_tasks')
|
||
.select('*')
|
||
.eq('driver_id', driverId)
|
||
.lt('status', 5)
|
||
.order('created_at', { ascending: false })
|
||
.limit(1)
|
||
.execute()
|
||
console.log('loadCurrentTask: driverId=', driverId, 'res=', res)
|
||
if (res && Array.isArray(res.data) && res.data.length > 0) {
|
||
this.currentTask = this._transformTask(res.data[0])
|
||
} else {
|
||
this.currentTask = null
|
||
}
|
||
} catch (e) {
|
||
console.error('loadCurrentTask error', e)
|
||
}
|
||
},
|
||
|
||
async loadAvailableOrders() {
|
||
// 如果当前不在线或已有任务,直接清空并返回
|
||
if (!this.isOnline || this.currentTask) {
|
||
this.availableOrders = []
|
||
return
|
||
}
|
||
// 在加载过程中先清空,避免显示过期或闪现的数据
|
||
this.availableOrders = []
|
||
try {
|
||
const ready = await Promise.race([supaReady, new Promise(resolve => setTimeout(() => resolve(false), 1500))])
|
||
if (!ready) console.warn('supaReady timeout/failed in loadAvailableOrders - proceeding')
|
||
console.log('loadAvailableOrders: supa session=', supa.getSession && supa.getSession())
|
||
console.log('loadAvailableOrders: getCurrentUserId=', getCurrentUserId())
|
||
const res = await supa.from('ml_delivery_tasks')
|
||
.select('*')
|
||
.is('driver_id', 'null')
|
||
.eq('status', 1)
|
||
.range(0, 19)
|
||
.execute()
|
||
console.log('loadAvailableOrders: query result=', res)
|
||
if (res && Array.isArray(res.data)) {
|
||
const fetched = (res.data as Array<any>).map((r:any) => this._transformTask(r))
|
||
// 再次检查 currentTask,避免并发情况下短暂展示可接单
|
||
if (this.currentTask) {
|
||
this.availableOrders = []
|
||
} else {
|
||
this.availableOrders = fetched
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('loadAvailableOrders error', e)
|
||
this.availableOrders = []
|
||
}
|
||
},
|
||
|
||
// 将 DB 行转换为页面期望的结构
|
||
_transformTask(task: any) {
|
||
const parseAddress = (a: any) => {
|
||
if (!a) return { detail: '', area: '' }
|
||
let obj = a
|
||
if (typeof a === 'string') {
|
||
try { obj = JSON.parse(a) } catch (e) { obj = { detail: a } }
|
||
}
|
||
const detail = obj.detail || obj.address || obj.full_address || obj.address_detail || obj.name || ''
|
||
const area = (obj.city || obj.district || obj.area || '')
|
||
return { detail, area }
|
||
}
|
||
|
||
const parseContact = (c: any) => {
|
||
if (!c) return { name: '', phone: '' }
|
||
let obj = c
|
||
if (typeof c === 'string') {
|
||
try { obj = JSON.parse(c) } catch (e) { obj = { name: c } }
|
||
}
|
||
return { name: obj.name || obj.contact_name || obj.receiver_name || '', phone: obj.phone || obj.mobile || obj.contact_phone || '' }
|
||
}
|
||
|
||
return {
|
||
id: task.id,
|
||
order_id: task.order_id || task.orderId || task.orderId || '',
|
||
order_no: task.order_no || task.orderNo || task.trade_no || '',
|
||
status: Number(task.status) || 1,
|
||
pickup_address: parseAddress(task.pickup_address),
|
||
delivery_address: parseAddress(task.delivery_address),
|
||
pickup_contact: parseContact(task.pickup_contact),
|
||
delivery_contact: parseContact(task.delivery_contact),
|
||
delivery_fee: Number(task.delivery_fee) || 0,
|
||
distance: Number(task.distance) || 0,
|
||
estimated_time: Number(task.estimated_time) || 0,
|
||
created_at: task.created_at || task.createdAt || ''
|
||
}
|
||
},
|
||
|
||
// 刷新数据
|
||
async refreshData() {
|
||
await this.loadTodayStats()
|
||
await this.loadCurrentTask()
|
||
await this.loadAvailableOrders()
|
||
},
|
||
|
||
// 刷新订单列表
|
||
refreshOrders() {
|
||
this.loadAvailableOrders()
|
||
uni.showToast({
|
||
title: '刷新成功',
|
||
icon: 'success'
|
||
})
|
||
},
|
||
|
||
// 切换工作状态
|
||
toggleWorkStatus(event: UniSwitchChangeEvent) {
|
||
const targetStatus = event.detail.value
|
||
|
||
// 检查是否有当前任务,不允许离线
|
||
if (!targetStatus && this.currentTask != null) {
|
||
// 1. 先同步 UI 状态为 false (由于用户已经拨动了开关)
|
||
this.isOnline = false
|
||
|
||
// 2. 弹出警告
|
||
uni.showModal({
|
||
title: '无法下线',
|
||
content: '您当前有正在进行的任务,请完成后再下线。',
|
||
showCancel: false,
|
||
success: (_) => {
|
||
// 3. 用户点击确定后或立即强制回弹开关为 true
|
||
this.$nextTick(() => {
|
||
this.isOnline = true
|
||
})
|
||
}
|
||
})
|
||
|
||
// 4. 冗余保障:如果 Modal 没及时回弹,延时强制重置
|
||
setTimeout(() => {
|
||
this.isOnline = true
|
||
}, 300)
|
||
return
|
||
}
|
||
|
||
this.isOnline = targetStatus
|
||
if (this.isOnline) {
|
||
this.startWork()
|
||
} else {
|
||
this.stopWork()
|
||
}
|
||
},
|
||
|
||
// 开始工作
|
||
async startWork() {
|
||
const driverId = this.driverInfo.id
|
||
if (driverId != '') {
|
||
try {
|
||
await supa.from('ml_delivery_drivers').update({ work_status: 1 } as any).eq('id', driverId).execute()
|
||
} catch (e) {
|
||
console.error('startWork update failed', e)
|
||
}
|
||
}
|
||
this.loadAvailableOrders()
|
||
uni.showToast({
|
||
title: '已上线接单',
|
||
icon: 'success'
|
||
})
|
||
},
|
||
|
||
// 停止工作
|
||
async stopWork() {
|
||
const driverId = this.driverInfo.id
|
||
if (driverId != '') {
|
||
try {
|
||
await supa.from('ml_delivery_drivers').update({ work_status: 0 } as any).eq('id', driverId).execute()
|
||
} catch (e) {
|
||
console.error('stopWork update failed', e)
|
||
}
|
||
}
|
||
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)}小时前`
|
||
}
|
||
},
|
||
|
||
// 任务操作方法
|
||
async acceptTask() {
|
||
if (!this.currentTask) return
|
||
try {
|
||
const ready = await Promise.race([supaReady, new Promise(resolve => setTimeout(() => resolve(false), 1500))])
|
||
if (!ready) console.warn('supaReady timeout/failed in acceptTask - proceeding')
|
||
const driverId = this.driverInfo.id || null
|
||
if (!driverId) throw new Error('无配送员ID')
|
||
const res = await supa.from('ml_delivery_tasks').update({ driver_id: driverId, status: 2 }).eq('id', this.currentTask.id).execute()
|
||
if (res && !res.error) {
|
||
this.currentTask.status = 2
|
||
uni.showToast({ title: '任务已接受', icon: 'success' })
|
||
}
|
||
} catch (e) {
|
||
console.error('acceptTask error', e)
|
||
uni.showToast({ title: '接受任务失败', icon: 'none' })
|
||
}
|
||
},
|
||
|
||
async startPickup() {
|
||
if (!this.currentTask) return
|
||
try {
|
||
const ready = await Promise.race([supaReady, new Promise(resolve => setTimeout(() => resolve(false), 1500))])
|
||
if (!ready) console.warn('supaReady timeout/failed in startPickup - proceeding')
|
||
const res = await supa.from('ml_delivery_tasks').update({ status: 3 }).eq('id', this.currentTask.id).execute()
|
||
if (res && !res.error) {
|
||
this.currentTask.status = 3
|
||
uni.showToast({ title: '开始取货', icon: 'success' })
|
||
}
|
||
} catch (e) {
|
||
console.error('startPickup error', e)
|
||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||
}
|
||
},
|
||
|
||
async confirmPickup() {
|
||
if (!this.currentTask) return
|
||
try {
|
||
const ready = await Promise.race([supaReady, new Promise(resolve => setTimeout(() => resolve(false), 1500))])
|
||
if (!ready) console.warn('supaReady timeout/failed in confirmPickup - proceeding')
|
||
const res = await supa.from('ml_delivery_tasks').update({ status: 4, pickup_time: new Date().toISOString() }).eq('id', this.currentTask.id).execute()
|
||
if (res && !res.error) {
|
||
this.currentTask.status = 4
|
||
uni.showToast({ title: '取货完成', icon: 'success' })
|
||
}
|
||
} catch (e) {
|
||
console.error('confirmPickup error', e)
|
||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||
}
|
||
},
|
||
|
||
async startDelivery() {
|
||
if (!this.currentTask) return
|
||
try {
|
||
const ready = await Promise.race([supaReady, new Promise(resolve => setTimeout(() => resolve(false), 1500))])
|
||
if (!ready) console.warn('supaReady timeout/failed in startDelivery - proceeding')
|
||
const res = await supa.from('ml_delivery_tasks').update({ status: 5 }).eq('id', this.currentTask.id).execute()
|
||
if (res && !res.error) {
|
||
this.currentTask.status = 5
|
||
uni.showToast({ title: '开始配送', icon: 'success' })
|
||
}
|
||
} catch (e) {
|
||
console.error('startDelivery error', e)
|
||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||
}
|
||
},
|
||
|
||
// 显示确认送达弹框
|
||
showConfirmDeliveryDialog() {
|
||
uni.showModal({
|
||
title: '确认送达',
|
||
content: '确认商品已送到顾客手中?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
this.confirmDelivery()
|
||
}
|
||
}
|
||
})
|
||
},
|
||
|
||
async confirmDelivery() {
|
||
if (!this.currentTask) return
|
||
try {
|
||
const ready = await Promise.race([supaReady, new Promise(resolve => setTimeout(() => resolve(false), 1500))])
|
||
if (!ready) console.warn('supaReady timeout/failed in confirmDelivery - proceeding')
|
||
const res = await supa.from('ml_delivery_tasks').update({ status: 6, delivered_time: new Date().toISOString() }).eq('id', this.currentTask.id).execute()
|
||
if (res && !res.error) {
|
||
const completedOrder = { ...this.currentTask }
|
||
uni.setStorageSync('completed_order_for_history', completedOrder)
|
||
// 同步更新 ml_orders 的状态,确保两个表状态一致
|
||
try {
|
||
// 尝试使用 currentTask.order_id(由 _transformTask 提供)
|
||
const orderId = (this.currentTask as any).order_id || ''
|
||
if (orderId) {
|
||
const upRes: any = await supa.from('ml_orders').update({ order_status: 5 }).eq('id', orderId).execute()
|
||
console.log('confirmDelivery: ml_orders update res=', upRes)
|
||
if (!upRes || upRes.error) console.warn('confirmDelivery: ml_orders update failed', upRes)
|
||
} else {
|
||
// 如无 order_id,回退读取任务行以查找 order_id
|
||
const tRes: any = await supa.from('ml_delivery_tasks').select('order_id').eq('id', this.currentTask.id).limit(1).execute()
|
||
if (tRes && Array.isArray(tRes.data) && tRes.data.length > 0) {
|
||
const oid = tRes.data[0].order_id
|
||
if (oid) {
|
||
const upRes2: any = await supa.from('ml_orders').update({ order_status: 5 }).eq('id', oid).execute()
|
||
console.log('confirmDelivery: ml_orders update (fallback) res=', upRes2)
|
||
if (!upRes2 || upRes2.error) console.warn('confirmDelivery: ml_orders update (fallback) failed', upRes2)
|
||
}
|
||
}
|
||
}
|
||
} catch (syncErr) {
|
||
console.warn('confirmDelivery: failed to sync ml_orders status', syncErr)
|
||
}
|
||
uni.showToast({ title: '配送完成', icon: 'success' })
|
||
this.currentTask = null
|
||
this.loadAvailableOrders()
|
||
}
|
||
} catch (e) {
|
||
console.error('confirmDelivery error', e)
|
||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||
}
|
||
},
|
||
|
||
contactCustomer() {
|
||
if (this.currentTask) {
|
||
uni.makePhoneCall({
|
||
phoneNumber: this.currentTask.delivery_contact.phone
|
||
})
|
||
}
|
||
},
|
||
|
||
viewNavigation() {
|
||
// TODO: 打开地图导航
|
||
uni.showToast({
|
||
title: '打开导航',
|
||
icon: 'none'
|
||
})
|
||
},
|
||
|
||
// 查看订单详情(跳转到 order-detail 页面)
|
||
viewOrderDetail(orderId?: string,status?:number) {
|
||
if (orderId && status) {
|
||
uni.navigateTo({
|
||
url: `/pages/mall/delivery/order-detail?id=${orderId}&status=${status}` // ✅ 强制为 1
|
||
})
|
||
} else if (this.currentTask) {
|
||
uni.navigateTo({
|
||
url: `/pages/mall/delivery/order-detail?id=${this.currentTask.id}&status=${this.currentTask.status}`
|
||
})
|
||
}else{
|
||
uni.navigateTo({
|
||
url: `/pages/mall/delivery/order-detail?id=${orderId}&status=1` // ✅ 强制为 1
|
||
})
|
||
}
|
||
},
|
||
|
||
// 订单操作方法
|
||
async acceptOrder(orderId: string) {
|
||
try {
|
||
const ready = await Promise.race([supaReady, new Promise(resolve => setTimeout(() => resolve(false), 1500))])
|
||
if (!ready) console.warn('supaReady timeout/failed in acceptOrder - proceeding')
|
||
const driverId = this.driverInfo.id || null
|
||
if (!driverId) throw new Error('无配送员ID')
|
||
const res = await supa.from('ml_delivery_tasks').update({ driver_id: driverId, status: 2 }).eq('id', orderId).execute()
|
||
if (res && !res.error) {
|
||
uni.showToast({ title: '订单已接受', icon: 'success' })
|
||
// 同步更新 ml_orders 状态为已接取(2)
|
||
try {
|
||
// orderId 这里是 ml_delivery_tasks.id(task id),需要先获取 order_id
|
||
const tRes: any = await supa.from('ml_delivery_tasks').select('order_id').eq('id', orderId).limit(1).execute()
|
||
if (tRes && Array.isArray(tRes.data) && tRes.data.length > 0) {
|
||
const oid = tRes.data[0].order_id
|
||
if (oid) {
|
||
const upRes: any = await supa.from('ml_orders').update({ order_status: 2 }).eq('id', oid).execute()
|
||
console.log('acceptOrder: ml_orders update res=', upRes)
|
||
if (!upRes || upRes.error) console.warn('acceptOrder: ml_orders update failed', upRes)
|
||
}
|
||
}
|
||
} catch (syncErr) {
|
||
console.warn('acceptOrder: failed to sync ml_orders status', syncErr)
|
||
}
|
||
await this.loadCurrentTask()
|
||
await this.loadAvailableOrders()
|
||
}
|
||
} catch (e) {
|
||
console.error('acceptOrder error', e)
|
||
uni.showToast({ title: '接受订单失败', icon: 'none' })
|
||
}
|
||
},
|
||
|
||
// 导航方法
|
||
goToOrderHistory() {
|
||
uni.navigateTo({
|
||
url: '/pages/mall/delivery/order-history'
|
||
})
|
||
},
|
||
|
||
// 跳转到“全部可接订单”页面
|
||
goToAllOrders() {
|
||
uni.navigateTo({
|
||
url: '/pages/mall/delivery/all'
|
||
})
|
||
},
|
||
|
||
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 lang="scss">
|
||
/* ... 保持原有 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;
|
||
}
|
||
|
||
.section-header-actions {
|
||
display: flex;
|
||
gap: 12rpx;
|
||
align-items: center;
|
||
}
|
||
|
||
.more-btn {
|
||
color: #1976d2;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* 加载更多订单入口样式 */
|
||
.view-all-footer {
|
||
background-color: #ffffff;
|
||
padding: 24rpx;
|
||
border-radius: 12rpx;
|
||
margin: 10rpx 0 30rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
border: 1rpx dashed #4CAF50;
|
||
box-shadow: 0 4rpx 12rpx rgba(76, 175, 80, 0.1);
|
||
}
|
||
|
||
.view-all-text {
|
||
font-size: 26rpx;
|
||
color: #4CAF50;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* 历史记录快捷入口 */
|
||
.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> |