diff --git a/pages.json b/pages.json
index 6261795f..813e91fe 100644
--- a/pages.json
+++ b/pages.json
@@ -268,6 +268,13 @@
"navigationStyle": "custom"
}
},
+ {
+ "path": "all",
+ "style": {
+ "navigationBarTitleText": "待接取任务",
+ "navigationStyle": "custom"
+ }
+ },
{
"path": "earnings",
"style": {
diff --git a/pages/mall/delivery/all.uvue b/pages/mall/delivery/all.uvue
new file mode 100644
index 00000000..bbdb9b58
--- /dev/null
+++ b/pages/mall/delivery/all.uvue
@@ -0,0 +1,360 @@
+
+
+
+
+
+
+
+
+
+
+ 附近暂时没有待接订单
+
+
+
+
+ ¥{{ order.delivery_fee }}
+
+
+
+
+ 📍
+ {{ order.pickup_address.detail || order.pickup_address.area }}
+
+ ↓
+
+ 🏠
+ {{ order.delivery_address.detail || order.delivery_address.area }}
+
+
+
+
+ 距离: {{ order.distance }}km
+ 预计: {{ order.estimated_time }}分钟
+ 下单: {{ order.created_at }}
+
+
+
+
+
+
+
+
+
+ 已加载全部订单
+
+
+
+
+
+
+
+
diff --git a/pages/mall/delivery/doc/db-data-generation.md b/pages/mall/delivery/db/db-data-generation.md
similarity index 100%
rename from pages/mall/delivery/doc/db-data-generation.md
rename to pages/mall/delivery/db/db-data-generation.md
diff --git a/pages/mall/delivery/doc/all.md b/pages/mall/delivery/doc/all.md
new file mode 100644
index 00000000..fcf50ed0
--- /dev/null
+++ b/pages/mall/delivery/doc/all.md
@@ -0,0 +1,51 @@
+
+## 1. 功能概述
+`all.uvue` 是为配送端设计的全量订单抓取页面。当首页待接订单超过 5 个时,用户可通过此页面查看并抢单。
+
+## 2. 核心设计说明
+根据最新 UI 指标,该页面采用了**现代垂直流布局**:
+- **中心费用展示**:订单金额在卡片顶部居中加粗显示,强化利益点。
+- **垂直路径流**:采用垂直排布的取货(📍)与送达(🏠)地址,中间以箭头连接,更符合手机屏阅读习惯。
+- **全宽操作按钮**:底部采用 100% 宽度的按钮,提高抢单操作的触达率。
+- **实时空态处理**:当订单被他人抢先接走时,列表会自动更新。
+
+## 3. 技术实现要点
+
+### 3.1 数据安全与并发控制
+在执行 `acceptOrder`(抢单)时,通过数据库约束确保操作的原子性:
+```uts
+// 增加 driver_id 为空的前提条件,防止已被他人接单
+const res = await supa.from('ml_delivery_tasks')
+ .update({
+ driver_id: driverId,
+ status: 2,
+ accepted_at: new Date().toISOString()
+ })
+ .eq('id', taskId)
+ .is('driver_id', 'null') // 关键:确保单子还没被接
+ .execute();
+```
+
+### 3.2 数据结构转换 (`_transformTask`)
+为了兼容数据库存储的 JSON 字符串格式与 UI 组件所需的 Object 格式,页面内置了转换逻辑:
+- 自动解析 `pickup_address` 和 `delivery_address` JSON 字符串。
+- 格式化 `delivery_fee` 为保留两位小数。
+- 映射状态码到对应的 UI 标签。
+
+### 3.3 路由与交互
+- **路由路径**:`pages/mall/delivery/all`
+- **导航栏**:配置了 `navigationBarTitleText: "待接订单"`,并开启了原生的回退功能。
+- **动态跳转**:支持从详情页返回后自动刷新列表(通过 `onShow` 触发)。
+
+## 4. 样式规范
+- 布局:Flexbox (Column)
+- 配色:
+ - 取货点:Pink (`#ff4d94`)
+ - 送达点:Blue (`#2196F3`)
+ - 主按钮:Green (`#4CAF50`)
+- 间距:标准 30rpx 外边距,卡片内 40rpx 内边距。
+
+## 5. 开发历史
+- **2026-02-03**: 页面初版创建。
+- **2026-02-03**: 完成从水平卡片到垂直流卡片的 UI 重构。
+- **2026-02-03**: 接入 Supabase 实现秒级抢单逻辑与状态同步。
diff --git a/pages/mall/delivery/doc/earnings.md b/pages/mall/delivery/doc/earnings.md
index c8b8a825..147f2b3b 100644
--- a/pages/mall/delivery/doc/earnings.md
+++ b/pages/mall/delivery/doc/earnings.md
@@ -58,6 +58,8 @@ const result = Array.from(orderMap.values())
## 数据源更新:包含已接订单(Accepted)
- 需求说明:为了与“历史订单”页面保持一致,收入统计应包含配送员已接取(accepted/assigned)的订单,不仅限于已完成订单。也就是说,只要 `ml_delivery_tasks` 中 `driver_id` = 当前司机且 `status >= 2`(已接取或进行中),其对应的订单都应计入收入统计范围。
+- 需求说明(已采纳):收入统计以 `ml_delivery_tasks` 为数据源之一:只要任务记录显示 `driver_id` = 当前司机且 `status >= 2`(已接取或进行中),其对应订单即应计入统计范围。注意:部分 `ml_delivery_tasks` 可能没有可匹配的 `ml_orders`(`order_id` 为空或在 `ml_orders` 中找不到),页面/后端应对缺失 `order_no` 做回退显示并记录供核查。
+
- 推荐 Supabase 查询示例(后端或前端按需实现):
```
@@ -78,6 +80,7 @@ LIMIT :size OFFSET :offset;
```js
const taskRes = await supa.from('ml_delivery_tasks').select('order_id').eq('driver_id', uid).gte('status', 2).execute()
const orderIds = taskRes.data.map(r => r.order_id)
+// 对于 order_id 为空或找不到的情况,server 端应记录这些 id 以便排查
const ordersRes = await supa.from('ml_orders').select('*,ml_delivery_tips(*)').in('id', orderIds).order('created_at',{ascending:false}).limit(size).execute()
```
diff --git a/pages/mall/delivery/doc/index.md b/pages/mall/delivery/doc/index.md
index dbea39f3..b3ef5702 100644
--- a/pages/mall/delivery/doc/index.md
+++ b/pages/mall/delivery/doc/index.md
@@ -46,6 +46,16 @@ await supa.from('ml_delivery_tasks')
- `toggleWorkStatus()`:切换 `isOnline` 并调用 `startWork()` / `stopWork()`。上线时会刷新可接订单列表。
- 接单/开始取货/确认取货/开始配送/确认送达等均通过对 `ml_delivery_tasks` 的 `update` 操作变更 `status`,并在成功后更新本地 `currentTask`。
+## UI 行为变动(已生效)
+- 当页面检测到存在 `currentTask`(来自 `ml_delivery_tasks`)时,页面不会再弹出“附近可接订单”列表。即:配送端以 `ml_delivery_tasks` 为状态真源,主页面的订单展示不再依赖或回退展示 `ml_orders.order_status`,以避免两表不同步导致的显示冲突。
+- 为了减少页面闪烁与重复刷新,`enableAutoRefresh` 在默认实现中已可被关闭(`false`),且 `loadAvailableOrders()` 在检测到 `currentTask` 时会跳过可接订单的渲染。
+
+## 会话与加载保护
+- `supaReady` 的会话恢复在某些环境中会较慢,页面中已对其使用 `Promise.race(..., 1500)` 超时包装:超时后页面会打警告并继续渲染以避免长时间阻塞用户界面。依赖用户 id 的查询在超时情况下可能为空,请参照 `order-history.md` 的“已实现的防护”部分进行排查。
+
+## 前端同步尝试(临时)
+- 在接单/确认送达流程中前端会尝试向 `ml_orders` 发送更新(将 `order_status` 同步到任务的状态)并记录返回结果用于诊断。但该同步并不保证在所有权限或网络错误下成功,因此更稳妥的方案仍是后端触发器同步或可信服务端接口。
+
## 注意事项
- 高并发接单场景需后端保证原子性(乐观锁或 DB 事务)以防止竞单冲突。
- `loadAvailableOrders()` 最好按司机服务区域与距离筛选,并使用分页/实时推送代替频繁轮询。
diff --git a/pages/mall/delivery/doc/order-detail.md b/pages/mall/delivery/doc/order-detail.md
index 31408d88..6b683279 100644
--- a/pages/mall/delivery/doc/order-detail.md
+++ b/pages/mall/delivery/doc/order-detail.md
@@ -22,30 +22,26 @@
- 解析 `options.id` 与 `options.status`,调用 `loadOrderDetail(id)`。
- `loadOrderDetail(id)`
- - 判断 ID 类型(UUID / 数字 / 非数字)以决定查询字段(`id`、`cid`、`order_no`)。
- - 并行查询 `ml_orders`, `ml_order_items`, `ml_shops`, `ml_delivery_tasks` 并合并到页面状态。
+ - **加载保护**:对 `supaReady` 采用 1.5s 超时策略包装,防止会话刷新阻塞页面加载。
+ - **智能 ID 回查**:优先从 `ml_orders` (UUID/cid/order_no) 查找。若未找到,则尝试从未分配任务表 `ml_delivery_tasks` 中根据 ID 查找,再反查关联订单。
+ - **降级机制(Fallback)**:当 `ml_orders` 行缺失时,自动回退到从 `ml_delivery_tasks` 提取地址、手机号、配送费及距离等基础信息进行展示,并在 UI 上显示回退加载提示。
+ - **清理加载状态**:在 `finally` 块中统一切除加载动画 (`uni.hideLoading`),防止界面挂起。
-- `acceptOrder()` / `rejectOrder(reason)`
- - accept: 尝试对 `ml_delivery_tasks` 执行 `update driver_id` 操作并设置 `status=2`(处理中),需要后端并发保护。
- - reject: 增加拒单原因到 `ml_delivery_tasks` 或 `order_notes` 并回滚本地 UI 状态。
+## 交互与样式优化(2026-02-03 更新)
+- **联系人信息解析**:地址栏(取货/送货)现在仅在“联系人姓名”存在时显示分隔点 `·`。若无姓名,则仅显示手机号,避免显示为 `. 手机号`。
+- **联系方式布局优化**:为了防止手机号在不同屏幕宽度下被图标遮挡,联系人区域采用**垂直居中布局**(图标在上,姓名电话在下),显著拉高了边框高度 (`min-height: 180rpx`) 并增加了垂直间距。
+- **文本显示优化**:
+ - 商品区域的“**订单号**”增加了加粗显示 (`bold`),提升核对便利性。
+ - 展示给配送员的联系手机号调大了字号并加粗,确保清晰可见。
+ - 修复了在 Uni-app x 下由于类型推断导致的手机号无法通过点语法访问的问题(改用索引访问)。
-- `confirmPickup()` / `confirmDelivery()`
- - 根据 `task.id` 更新相应时间戳字段(`picked_at`/`delivered_at`)并设置状态(例如 `status=3/4`)。
-
-## 示例:按 id 类型查询(伪代码)
-```
-let q = supa.from('ml_orders').select('*')
-if (isUUID(id)) q = q.eq('id', id)
-else if (isNumeric(id)) q = q.eq('cid', id)
-else q = q.eq('order_no', id)
-const { data: order } = await q.limit(1).execute()
+## 示例:地址兼容解析逻辑
+```typescript
+// 兼容 JSON 字符串及对象格式的地址字段
+let shipping = {}
+if (typeof raw == 'string') {
+ try { shipping = JSON.parse(raw) } catch (e) { shipping = { detail: raw } }
+} else { shipping = raw || {} }
+// 访问方式:(address as UTSJSONObject)['phone']
```
-## 事务与并发注意
-- 接单场景应使用后端原子性检查(数据库事务或行级乐观锁)以避免多司机同时接单。
-- 前端接单流程:先尝试 update(带 where driver_id IS NULL),若返回 0 row affected 则提示已被接单。
-
-## 错误处理与回退
-- 捕获所有 supa 调用错误并将友好错误展示给用户(例如:'网络错误,请稍后重试')。
-- 对可能缺失的字段(地址为字符串或对象)使用 `_transformAddress()` 做兼容处理。
-
diff --git a/pages/mall/delivery/doc/order-history.md b/pages/mall/delivery/doc/order-history.md
index b46ea431..9095fd79 100644
--- a/pages/mall/delivery/doc/order-history.md
+++ b/pages/mall/delivery/doc/order-history.md
@@ -1,3 +1,78 @@
+# 历史订单 页面说明(order-history.uvue)
+
+## 概要
+`order-history.uvue` 用于配送员查看历史订单与近期任务。页面会展示:
+- 以 `ml_delivery_tasks` 为配送端“状态真源”的任务记录(只要 `driver_id = 当前司机` 且 `status >= 2`,均会包含在统计/列表中);
+- 页面会批量回填对应 `ml_orders.order_no` 以补全显示(若 `order_no` 缺失会显示回退文本),避免直接以 `ml_orders.order_status` 作为展示依据而导致与配送端不一致。
+
+页面关键点:
+- 首次加载时通过 `loadOrderHistory()` 拉取数据;页面每次显示时会检查本地存储 `completed_order_for_history`,并把刚完成订单插入列表表头。
+- 使用 Supabase 客户端 `supa` 读取 `ml_delivery_tasks` 与 `ml_orders` 表,并通过 `getCurrentUser()` / `getCurrentUserId()` 获取当前用户/司机 id。
+
+## 行为细节
+- 当前实现优先以 `ml_delivery_tasks`(status >= 2)作为数据源,页面会:
+ - 查询 `ml_delivery_tasks` 中与 `driver_id` 相关的任务,按时间排序并映射为页面项;
+ - 对获取到的 `order_id` 列表做一次批量查询 `ml_orders` 以回填 `order_no` 和订单详情;
+ - 对没有匹配到 `ml_orders` 的 `order_id`,页面会用短 id 回退显示并在控制台打印缺失 id 列表,便于后台核查数据不一致的原因。
+- 为避免重复展示,页面在将“当前任务对应订单”插入顶部时,会先检查 `orderList` 是否已有相同 `id`。
+
+## 依赖 & 相关文件
+- 页面文件:`pages/mall/delivery/order-history.uvue`(当前)
+- Supabase 实例:`components/supadb/aksupainstance.uts`(导出 `supa` 与 `supaReady`)
+- 用户/会话工具:`utils/store.uts`(`getCurrentUser()`、`getCurrentUserId()`)
+- 相关文档:
+ - `pages/mall/delivery/doc/earnings.md`(收入聚合与 DB 建议)
+ - `pages/mall/delivery/doc/test-user-1_at_123.com.md`(测试用户与 SQL 示例)
+
+## 已实现的防护与诊断信息
+- `supaReady` 在会话恢复时可能会进行网络刷新(refreshSession),该步骤可能较慢。为避免页面长时间阻塞,页面中对 `supaReady` 使用了 `Promise.race` 的 1.5s 超时包装:如果超时会打印警告并继续执行(某些依赖用户 id 的查询可能因此为空)。
+- 如果 `getCurrentUserId()` 返回空,页面会尝试从 `supa.getSession()` 获取 auth id 并在 `ak_users` 表中查找对应的 `ak_users.id` 作为回退,这能修复 `driver_id` 在数据库中为 `ak_users.id` 的常见映射问题。
+
+## 常见不一致现象说明
+- 我们观察到的常见情况:`ml_delivery_tasks` 中的任务显示为“已完成”(配送端),但对应 `ml_orders.order_status` 仍为“已取消”或其它状态,导致不同页面显示冲突。原因通常为:
+ - `ml_delivery_tasks.order_id` 为空或格式不一致(UUID vs string);
+ - `ml_orders` 没有相应行(数据尚未同步或被删除);
+ - RLS/权限导致前端不能读取或更新 `ml_orders`。
+
+建议排查 SQL(例):
+```
+SELECT t.id AS task_id, t.order_id, t.status AS task_status, o.order_status
+FROM public.ml_delivery_tasks t
+LEFT JOIN public.ml_orders o ON o.id = t.order_id
+WHERE t.status >= 2
+ORDER BY t.created_at DESC
+LIMIT 200;
+```
+
+如需我为你生成触发器或前端重试队列示例,我可以继续实现。
+
+## 常见问题与排查步骤
+1. 问题:页面没有显示当前已接订单(即使首页显示有当前任务)。
+ - 检查控制台日志:页面会打印 `loadOrderHistory: currentUserId=`、`loadOrderHistory: session id fallback=`、`loadOrderHistory: delivery_tasks dtRes=`、`loadOrderHistory: ordersRes=`。把这些日志逐项核对:
+ - `currentUserId` 应为 `ak_users.id`(或系统实际使用的 driver id)。
+ - `dtRes`(delivery_tasks 查询)应包含对象数组,且数组项含 `order_id`。
+ - `ordersRes` 应包含对应的 `ml_orders` 行。
+ - 若 `dtRes` 为空且 `session id fallback` 有值,说明 `ak_users` 表中可能没有把 auth id 映射到 `ak_users.id`,需要把 `ak_users.auth_id` 填入或同步。
+ - 若 `ordersRes` 为空,但 `dtRes` 非空,请检查 `ml_orders.id` 与 `ml_delivery_tasks.order_id` 的数据类型(例如 UUID vs string)以及 RLS 策略。
+
+2. 问题:页面加载慢或时不时刷新。
+ - 原因:多个页面在 `onShow`/`onLoad` 时发起多次 supa 查询,且 `supaReady` 恢复会话有时较慢,导致累积延迟。已在 `index.uvue` 增加了防抖与 `enableAutoRefresh` 开关来禁止自动刷新。
+ - 排查:查看控制台是否有 `supaReady timeout/failed` 警告(若有,则说明会话恢复慢或失败)。
+
+## 性能与安全建议
+- 若数据量大,请在后端做分页与聚合(只返回必要字段);避免一次性查询大量 `ml_orders` 字段。参见 `earnings.md` 中的后端接口建议。
+- 长期建议:修改 `components/supadb/aksupainstance.uts` 中会话恢复逻辑,让刷新在后台异步进行或提供可配置的超时策略,避免阻塞页面加载。
+
+## 测试步骤(快速)
+1. 使用测试用户(参见 `test-user-1_at_123.com.md`)创建一个 `ml_delivery_tasks` 记录,`driver_id` 对应当前司机,且 `status >= 2`。
+2. 在首页确认当前任务显示;然后打开“历史订单”页面,观察顶部是否显示该订单。若未显示,贴上控制台中 `loadOrderHistory:` 的相关日志给开发者排查。
+
+## 变更历史
+- 2026-02-02:添加回退 mapping(session -> ak_users.id)、supaReady 超时保护的说明、调试日志建议及性能建议。
+
+---
+
+如需我把文档翻译为英文或生成 README 风格的一页说明,我可以继续补充。
# order-history.uvue — 历史订单
## 概要
diff --git a/pages/mall/delivery/index.uvue b/pages/mall/delivery/index.uvue
index cd916250..de149c9b 100644
--- a/pages/mall/delivery/index.uvue
+++ b/pages/mall/delivery/index.uvue
@@ -97,8 +97,6 @@
附近订单
@@ -107,34 +105,41 @@
请保持在线状态,有新订单会自动推送
-
-
-
-
-
- 📍
- {{ order.pickup_address.area }}
+
+
+
- →
-
- 🏠
- {{ order.delivery_address.area }}
+
+
+
+ 📍
+ {{ order.pickup_address.area || order.pickup_address.detail }}
+
+ →
+
+ 🏠
+ {{ order.delivery_address.area || order.delivery_address.detail }}
+
+
+
+
+ 距离: {{ order.distance }}km
+ 预计: {{ order.estimated_time }}分钟
+ 下单: {{ formatTime(order.created_at) }}
+
+
+
+
+
-
-
- 距离: {{ order.distance }}km
- 预计: {{ order.estimated_time }}分钟
- 下单: {{ formatTime(order.created_at) }}
-
-
-
-
-
-
+
+
+
+
@@ -308,7 +313,10 @@ import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
}
}
if (res && (res.data instanceof Array) && res.data.length > 0) {
- this.driverInfo = Object.assign(this.driverInfo, res.data[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)
@@ -434,6 +442,7 @@ import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
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),
@@ -465,8 +474,34 @@ import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
// 切换工作状态
toggleWorkStatus(event: UniSwitchChangeEvent) {
- this.isOnline = event.detail.value
+ 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 {
@@ -475,8 +510,15 @@ import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
},
// 开始工作
- startWork() {
- // TODO: 调用API开始工作,上传位置
+ 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: '已上线接单',
@@ -485,8 +527,15 @@ import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
},
// 停止工作
- stopWork() {
- // TODO: 调用API停止工作
+ 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: '已下线休息',
@@ -631,6 +680,29 @@ import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
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()
@@ -684,6 +756,21 @@ import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
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()
}
@@ -1174,6 +1261,25 @@ import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
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;
diff --git a/pages/mall/delivery/order-detail.uvue b/pages/mall/delivery/order-detail.uvue
index d58fb0e4..d516ccd1 100644
--- a/pages/mall/delivery/order-detail.uvue
+++ b/pages/mall/delivery/order-detail.uvue
@@ -1,5 +1,6 @@
+
+
+
@@ -249,30 +252,78 @@ export default {
async loadOrderDetail(orderId: string) {
const originalStatus = this.order.status
try {
- await supaReady
- console.log('loadOrderDetail called', { orderId })
+ // 使用 1.5s 超时策略包装 supaReady,防止 session 刷新卡死页面
+ const readyPromise = supaReady
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('supaReady timeout')), 1500))
+ try {
+ await Promise.race([readyPromise, timeoutPromise])
+ } catch (e) {
+ console.warn('loadOrderDetail: supaReady timeout, proceeding with current session', e)
+ }
- // 获取订单主表:按 id(UUID) / cid(数字) / order_no 三种可能匹配
+ console.log('loadOrderDetail: start loading', { orderId, originalStatus })
+ uni.showLoading({ title: '加载中...' })
+
+ // 配送端详情页逻辑:
+ // 1. 先尝试直接查 ml_orders (传入的是真实订单ID时)
+ // 2. 如果没查到,尝试从 ml_delivery_tasks 查找 (传入的是任务ID时)
+ // 3. 如果从任务表查到了,再拿其关联的 order_id 回查 ml_orders
+
+ let targetOrderId = orderId
let orderRes: any = null
+ let taskData: any = null
+
const isUuid = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(orderId)
const isNumber = /^\d+$/.test(orderId)
- console.log('loadOrderDetail: supa session=', supa.getSession && supa.getSession())
+
console.log('loadOrderDetail: detect id format', { isUuid, isNumber })
+
+ // 步骤 A: 查 ml_orders
if (isUuid) {
- console.log('loadOrderDetail: querying by id')
orderRes = await supa.from('ml_orders').select('*').eq('id', orderId).limit(1).execute()
} else if (isNumber) {
- console.log('loadOrderDetail: querying by cid')
orderRes = await supa.from('ml_orders').select('*').eq('cid', Number(orderId)).limit(1).execute()
} else {
- console.log('loadOrderDetail: querying by order_no')
orderRes = await supa.from('ml_orders').select('*').eq('order_no', orderId).limit(1).execute()
}
- console.log('loadOrderDetail: orderRes=', orderRes)
+
+ // 步骤 B: 如果订单没查到,尝试从任务表查
+ if (!(orderRes && Array.isArray(orderRes.data) && orderRes.data.length > 0)) {
+ console.log('loadOrderDetail: order not found directly, checking ml_delivery_tasks')
+ const taskRes: any = await supa.from('ml_delivery_tasks').select('*').eq('id', orderId).limit(1).execute()
+ if (taskRes && Array.isArray(taskRes.data) && taskRes.data.length > 0) {
+ taskData = taskRes.data[0]
+ console.log('loadOrderDetail: found task record', taskData)
+ if (taskData.order_id) {
+ targetOrderId = taskData.order_id
+ console.log('loadOrderDetail: found linked order_id in task', targetOrderId)
+ orderRes = await supa.from('ml_orders').select('*').eq('id', targetOrderId).limit(1).execute()
+ }
+ }
+ }
+
+ console.log('loadOrderDetail: final orderRes data length=', orderRes?.data?.length)
+
if (orderRes && Array.isArray(orderRes.data) && orderRes.data.length > 0) {
const row = orderRes.data[0]
-
- const shipping = row.shipping_address || {}
+ console.log('loadOrderDetail: discovered order details', row)
+
+ // 如果没有预先获取 taskData,在这里查一次
+ if (!taskData) {
+ const dtQuery: any = await supa.from('ml_delivery_tasks').select('*').eq('order_id', row.id).limit(1).execute()
+ if (dtQuery && Array.isArray(dtQuery.data) && dtQuery.data.length > 0) taskData = dtQuery.data[0]
+ }
+ let shipping: UTSJSONObject = {} as UTSJSONObject
+ try {
+ const rawShipping = row.shipping_address
+ if (typeof rawShipping == 'string' && (rawShipping as string).startsWith('{')) {
+ shipping = JSON.parse(rawShipping as string) as UTSJSONObject
+ } else if (rawShipping != null && typeof rawShipping == 'object') {
+ shipping = rawShipping as UTSJSONObject
+ }
+ } catch (e) {
+ console.warn('loadOrderDetail: parse shipping_address failed', e)
+ }
this.order = Object.assign(this.order, {
id: row.id,
@@ -287,9 +338,9 @@ export default {
payment_method: row.payment_method || 0,
payment_status: row.payment_status || 0,
delivery_address: {
- name: shipping.name || shipping.recipient || '',
- phone: shipping.phone || shipping.mobile || '',
- detail: shipping.detail || shipping.address || JSON.stringify(shipping)
+ name: shipping['name'] || shipping['recipient'] || '',
+ phone: shipping['phone'] || shipping['mobile'] || '',
+ detail: shipping['detail'] || shipping['address'] || JSON.stringify(shipping)
},
created_at: row.created_at || ''
})
@@ -341,19 +392,63 @@ export default {
}
// deliveryInfo 从 ml_delivery_tasks 中读取(如果存在)
- const dtRes: any = await supa.from('ml_delivery_tasks').select('*').eq('order_id', realOrderId).limit(1).execute()
- console.log('loadOrderDetail: dtRes=', dtRes)
- if (dtRes && Array.isArray(dtRes.data) && dtRes.data.length > 0) {
- const dt = dtRes.data[0]
- this.deliveryInfo.distance = Number(dt.distance) || this.deliveryInfo.distance
- this.deliveryInfo.estimated_time = Number(dt.estimated_time) || this.deliveryInfo.estimated_time
- this.deliveryInfo.courier_id = dt.driver_id || ''
- this.deliveryInfo.pickup_time = dt.pickup_time || ''
- this.deliveryInfo.delivery_time = dt.delivered_time || ''
+ if (taskData) {
+ this.deliveryInfo.distance = Number(taskData.distance) || this.deliveryInfo.distance
+ this.deliveryInfo.estimated_time = Number(taskData.estimated_time) || this.deliveryInfo.estimated_time
+ this.deliveryInfo.courier_id = taskData.driver_id || ''
+ this.deliveryInfo.pickup_time = taskData.pickup_time || ''
+ this.deliveryInfo.delivery_time = taskData.delivered_time || taskData.delivered_at || ''
}
+ } else if (taskData) {
+ // ⚠️ 数据不一致兜底:订单主表查不到,但任务表查到了
+ console.warn('loadOrderDetail: order missing but task found. Using fallback.')
+
+ const parseF = (v: any) => {
+ if (!v) return { detail: '', name: '', phone: '' }
+ let o: any = v
+ if (typeof v === 'string') {
+ try { o = JSON.parse(v) } catch (e) { o = { detail: v } }
+ }
+ return {
+ detail: o.detail || o.address || o.full_address || '',
+ name: o.name || o.contact_name || o.recipient_name || '',
+ phone: o.phone || o.mobile || o.contact_phone || ''
+ }
+ }
+
+ const pickup = parseF(taskData.pickup_address)
+ const delivery = parseF(taskData.delivery_address)
+ const pContact = parseF(taskData.pickup_contact)
+ const dContact = parseF(taskData.delivery_contact)
+
+ this.order = Object.assign(this.order, {
+ id: taskData.order_id || taskData.id,
+ order_no: taskData.order_no || taskData.id.substring(0, 8),
+ status: Number(taskData.status) || originalStatus,
+ delivery_address: {
+ name: dContact.name || delivery.name,
+ phone: dContact.phone || delivery.phone,
+ detail: delivery.detail
+ }
+ })
+
+ this.merchant.contact_name = pContact.name || pickup.name
+ this.merchant.contact_phone = pContact.phone || pickup.phone
+ this.pickupAddress = pickup.detail
+
+ this.deliveryInfo.distance = Number(taskData.distance) || 0
+ this.deliveryInfo.estimated_time = Number(taskData.estimated_time) || 0
+
+ uni.showToast({ title: '已回退从任务记录显示', icon: 'none' })
+ } else {
+ console.warn('loadOrderDetail: no order found for id', orderId)
+ uni.showToast({ title: '未找到订单或任务信息', icon: 'none' })
}
} catch (e) {
console.error('loadOrderDetail db error', e)
+ uni.showToast({ title: '加载订单失败', icon: 'none' })
+ } finally {
+ uni.hideLoading()
}
},
@@ -530,7 +625,15 @@ export default {
.delivery-order-detail {
background-color: #f5f5f5;
min-height: 100vh;
- padding-bottom: 160rpx;
+ display: flex;
+ flex-direction: column;
+ padding-bottom: 0; /* padding moved to .page-content to avoid double spacing */
+}
+
+.page-content {
+ flex: 1;
+ overflow: auto;
+ padding-bottom: 300rpx; /* 留出底部操作区域高度,确保联系商家等内容可滚动到最下 */
}
/* ... 其余样式保持原样 ... */
@@ -738,7 +841,8 @@ export default {
.order-no {
font-size: 24rpx;
- color: #666;
+ color: #333;
+ font-weight: bold;
}
.product-item {
@@ -865,30 +969,38 @@ export default {
.contact-item {
flex: 1;
display: flex;
+ flex-direction: column; /* 改为垂直排列避免空间不足 */
align-items: center;
- padding: 25rpx;
+ justify-content: center;
+ padding: 20rpx;
background-color: #f8f9fa;
border-radius: 10rpx;
+ min-height: 120rpx;
}
.contact-icon {
- font-size: 32rpx;
- margin-right: 15rpx;
+ font-size: 36rpx;
+ margin-bottom: 10rpx;
}
.contact-info {
flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
}
.contact-name {
- font-size: 26rpx;
- color: #333;
+ font-size: 24rpx;
+ color: #666;
margin-bottom: 5rpx;
}
.contact-phone {
- font-size: 24rpx;
+ font-size: 26rpx;
color: #007aff;
+ font-weight: bold;
}
.bottom-actions {
diff --git a/pages/mall/delivery/order-history.uvue b/pages/mall/delivery/order-history.uvue
index 137a0879..8064b2bc 100644
--- a/pages/mall/delivery/order-history.uvue
+++ b/pages/mall/delivery/order-history.uvue
@@ -53,7 +53,7 @@
-
+
@@ -93,11 +93,15 @@ export default {
checkForNewCompletedOrder() {
const completedOrderFromStorage = uni.getStorageSync('completed_order_for_history')
if (completedOrderFromStorage) {
- // 如果有,将其添加到订单列表的开头
- // 检查是否已经存在于列表中,避免重复添加
- const exists = this.orderList.some(order => order.id === completedOrderFromStorage.id)
- if (!exists) {
- this.orderList.unshift(completedOrderFromStorage)
+ // 仅在本地存储条目标记为已完成(status >= 4)时才合并到历史
+ const storedStatus = Number(completedOrderFromStorage && (completedOrderFromStorage.status ?? 0))
+ if (storedStatus >= 4) {
+ const exists = this.orderList.some(order => order.id === completedOrderFromStorage.id)
+ if (!exists) {
+ this.orderList.unshift(completedOrderFromStorage)
+ }
+ } else {
+ console.warn('checkForNewCompletedOrder: ignoring stored completed_order_for_history with non-completed status', completedOrderFromStorage)
}
// 清除本地存储,防止下次进入页面时重复添加
uni.removeStorageSync('completed_order_for_history')
@@ -107,122 +111,114 @@ export default {
// 加载历史订单(从数据库读取)
async loadOrderHistory() {
try {
- const ready = await Promise.race([supaReady, new Promise(resolve => setTimeout(() => resolve(false), 1500))])
- if (!ready) console.warn('supaReady timeout/failed in loadOrderHistory - proceeding')
+ await supaReady
} catch (e) {
console.warn('supaReady failed', e)
}
- // Ensure profile is loaded so getCurrentUserId returns ak_users.id when available
- try {
- await getCurrentUser()
- } catch (e) {
- console.warn('getCurrentUser failed in loadOrderHistory', e)
- }
-
const uid = getCurrentUserId()
console.log('loadOrderHistory: currentUserId=', uid)
- // 首先查 ml_delivery_tasks 中分配给当前用户的任务(仅包含已接/分配的 status >= 2)
- let dtRes: any = { data: [] }
+ // 首先尝试解析出 ml_delivery_drivers 的 id(driver_id),避免直接使用 auth user id 导致不匹配
+ let driverId: string | null = null
try {
- let queryUid = uid
- // 如果 uid 为空,尝试从 supa session 获取 auth id 并映射到 ak_users.id
- if (!queryUid || queryUid === '') {
- try {
- const sess = supa.getSession && supa.getSession()
- const sessId = sess && sess.user && sess.user.getString && sess.user.getString('id')
- console.log('loadOrderHistory: session id fallback=', sessId)
- if (sessId) {
- const akRes = await supa.from('ak_users').select('id').eq('auth_id', sessId).limit(1).execute()
- if (akRes && Array.isArray(akRes.data) && akRes.data.length > 0) {
- queryUid = (akRes.data[0] as any).id
- console.log('loadOrderHistory: mapped ak_users.id=', queryUid)
+ if (uid && uid !== '') {
+ // 尝试直接按 user_id 查找 driver
+ const drvRes: any = await supa.from('ml_delivery_drivers').select('id').eq('user_id', uid).limit(1).execute()
+ if (drvRes && Array.isArray(drvRes.data) && drvRes.data.length > 0) {
+ driverId = drvRes.data[0].id
+ } else {
+ // 回退:尝试 ak_users 表根据 auth_id 查出 ak_users.id,再查 ml_delivery_drivers
+ const akRes: any = await supa.from('ak_users').select('id').eq('auth_id', uid).limit(1).execute()
+ if (akRes && Array.isArray(akRes.data) && akRes.data.length > 0) {
+ const akId = akRes.data[0].id
+ const drvRes2: any = await supa.from('ml_delivery_drivers').select('id').eq('user_id', akId).limit(1).execute()
+ if (drvRes2 && Array.isArray(drvRes2.data) && drvRes2.data.length > 0) {
+ driverId = drvRes2.data[0].id
}
}
- } catch (mapErr) {
- console.warn('loadOrderHistory: ak_users mapping failed', mapErr)
}
}
+ } catch (err) {
+ console.error('loadOrderHistory: driver lookup failed', err)
+ }
- if (queryUid && queryUid !== '') {
- dtRes = await supa.from('ml_delivery_tasks')
- .select('order_id,status')
- .eq('driver_id', queryUid)
+ // 直接以 ml_delivery_tasks 作为数据源(配送端真源)
+ let tasksRes: any = { data: [] }
+ try {
+ if (driverId) {
+ tasksRes = await supa.from('ml_delivery_tasks')
+ .select('*')
+ .eq('driver_id', driverId)
.gte('status', 2)
.order('created_at', { ascending: false })
.limit(200)
.execute()
} else {
- dtRes = { data: [] }
+ tasksRes = { data: [] }
}
- console.log('loadOrderHistory: delivery_tasks dtRes=', dtRes)
} catch (err) {
- console.error('loadOrderHistory: delivery_tasks query failed', err)
- dtRes = { data: [] }
+ console.error('loadOrderHistory: ml_delivery_tasks query failed', err)
+ tasksRes = { data: [] }
}
- const orderIds = (dtRes && Array.isArray(dtRes.data)) ? dtRes.data.map((r: any) => r.order_id) : []
+ console.log('loadOrderHistory: tasksRes=', tasksRes)
- // 如果没有通过 delivery_tasks 找到订单,改为直接读取最近完成/已取货订单(兼容测试环境)
- let ordersRes
+ // 如果任务包含 order_id,则从 ml_orders 查询 order_no 用于显示
+ const orderIdsFromTasks = (tasksRes && Array.isArray(tasksRes.data)) ? tasksRes.data.map((r: any) => r.order_id).filter(Boolean) : []
+ let ordersRes: any = { data: [] }
try {
- if (orderIds.length > 0) {
- ordersRes = await supa.from('ml_orders').select('*').in('id', orderIds).order('created_at', { ascending: false }).limit(200).execute()
- } else {
- ordersRes = await supa.from('ml_orders').select('*').in('order_status', [4,5]).order('created_at', { ascending: false }).limit(200).execute()
+ if (orderIdsFromTasks.length > 0) {
+ ordersRes = await supa.from('ml_orders').select('id,order_no').in('id', orderIdsFromTasks).execute()
}
} catch (err) {
- console.error('loadOrderHistory: ml_orders query failed', err)
+ console.warn('loadOrderHistory: ml_orders lookup failed', err)
ordersRes = { data: [] }
}
-
console.log('loadOrderHistory: ordersRes=', ordersRes)
- const mapOrder = (r: any) => ({
- id: r.id,
- order_no: r.order_no || String(r.cid || ''),
- status: r.order_status ?? r.status ?? 0,
- pickup_address: r.pickup_address || r.shipping_address || { detail: '', area: '' },
- delivery_address: r.delivery_address || r.shipping_address || { detail: '', area: '' },
- pickup_contact: r.pickup_contact || r.shipping_contact || { name: '', phone: '' },
- delivery_contact: r.delivery_contact || r.shipping_contact || { name: '', phone: '' },
- delivery_fee: r.delivery_fee || r.delivery_fees || 0,
- distance: r.distance || 0,
- estimated_time: r.estimated_time || 0,
- created_at: r.created_at
+ const orderNoMap: Record = {}
+ if (ordersRes && Array.isArray(ordersRes.data)) {
+ ordersRes.data.forEach((o: any) => { if (o && o.id) orderNoMap[o.id] = o.order_no })
+ }
+
+ 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 || '' }
+ }
+
+ const mapTaskToOrder = (t: any) => ({
+ id: t.id,
+ // 优先使用任务自身的 order_no 字段(若存在),否则使用从 ml_orders 查询到的 order_no
+ order_no: t.order_no || t.orderNo || t.trade_no || orderNoMap[t.order_id] || '',
+ status: Number(t.status) || 0,
+ pickup_address: parseAddress(t.pickup_address),
+ delivery_address: parseAddress(t.delivery_address),
+ pickup_contact: parseContact(t.pickup_contact),
+ delivery_contact: parseContact(t.delivery_contact),
+ delivery_fee: Number(t.delivery_fee) || 0,
+ distance: Number(t.distance) || 0,
+ estimated_time: Number(t.estimated_time) || 0,
+ created_at: t.created_at,
+ order_id: t.order_id || ''
})
- this.orderList = (ordersRes && ordersRes.data) ? ordersRes.data.map(mapOrder) : []
-
- // 额外:把当前分配给本司机但尚未完成的任务对应订单也展示在列表顶部(便于查看当前任务)
- try {
- if (uid && uid !== '') {
- // 仅获取已经被接取/分配给本司机的任务(status >= 2),包括进行中和已取货但未完成的
- const taskRes: any = await supa.from('ml_delivery_tasks')
- .select('order_id,status')
- .eq('driver_id', uid)
- .gte('status', 2)
- .order('created_at', { ascending: false })
- .limit(50)
- .execute()
- if (taskRes && Array.isArray(taskRes.data) && taskRes.data.length > 0) {
- const taskOrderIds = taskRes.data.map((t: any) => t.order_id).filter((id: any) => id)
- if (taskOrderIds.length > 0) {
- const curOrdersRes: any = await supa.from('ml_orders').select('*').in('id', taskOrderIds).execute()
- const curMapped = (curOrdersRes && Array.isArray(curOrdersRes.data)) ? curOrdersRes.data.map(mapOrder) : []
- // 把当前任务对应订单插入到列表最前面(避免重复)
- curMapped.forEach((o: any) => {
- if (!this.orderList.some((ex: any) => ex.id === o.id)) {
- this.orderList.unshift(o)
- }
- })
- }
- }
- }
- } catch (err) {
- console.error('loadOrderHistory: fetch current tasks failed', err)
- }
+ this.orderList = (tasksRes && Array.isArray(tasksRes.data)) ? tasksRes.data.map(mapTaskToOrder) : []
// 检查是否有新完成的订单(在加载初始数据后)
this.checkForNewCompletedOrder()
@@ -253,9 +249,11 @@ export default {
},
// 查看订单详情
- viewOrderDetail(orderId: string, status: number) {
+ viewOrderDetail(order: any) {
+ // 优先传递 ml_orders.id,但如果订单表对应行确实缺失,详情页逻辑现在支持用 Task ID 回退显示
+ const targetId = order.order_id || order.id
uni.navigateTo({
- url: `/pages/mall/delivery/order-detail?id=${orderId}&status=${status}`
+ url: `/pages/mall/delivery/order-detail?id=${targetId}&status=${order.status}`
})
},
diff --git a/pages/mall/delivery/profile.uvue b/pages/mall/delivery/profile.uvue
index b35cba79..1592bcab 100644
--- a/pages/mall/delivery/profile.uvue
+++ b/pages/mall/delivery/profile.uvue
@@ -209,6 +209,11 @@ function backToIndex() {
uni.navigateBack({ url: '/pages/mall/delivery/index' })
}
+/* 兼容模板中的 goBack 绑定,保持与其它页面一致 */
+function goBack() {
+ uni.navigateBack()
+}
+
/* ----------------- 数据 & 状态 ----------------- */
const driverInfo = ref({ id: '', real_name: '配送员', avatar_url: '', rating: 4.9, total_orders: 0, work_status: 1 })
const workStatus = ref(1)