连接数据库并修改页面

This commit is contained in:
not-like-juvenile
2026-02-02 18:20:22 +08:00
parent 5f856a96c9
commit 8efe6d5e89
22 changed files with 1630 additions and 992 deletions

View File

@@ -0,0 +1,205 @@
## 测试数据生成与验证记录
本文档记录了在本项目中为配送端页面生成并验证测试数据库数据的全过程,包含关键表结构要点、执行的幂等 SQL、遇到的问题及解决办法以及前端验证步骤便于回溯和复现。
---
**概述**
- 目标:让配送端页面 `pages/mall/delivery/index.uvue` 能读取真实 DB 中的配送任务(`ml_delivery_tasks`)并在附近订单 / 当前任务中展示;为此需要在 DB 中创建或复用 `ak_users``ml_delivery_drivers``ml_orders``ml_delivery_tasks` 的测试数据。
- 环境Supabase(Postgres)。建议在 Supabase SQL Editor 中以 `Role = postgres` 执行 SQL 以避免 RLS/触发器引起的权限问题。
**关键表与约束摘要**
- `public.ak_users`:项目用户表,`id` 为主键,`auth_id` 对应 `auth.users.id`
- `public.ml_delivery_drivers`:配送员表,重要列:`id``user_id` (引用 `ak_users.id`UNIQUE NOT NULL)、`real_name``id_card`。注意:此表没有 `phone` 列(以前曾误以为存在)。
- `public.ml_orders`:订单表,重要列:`id``order_no`(UNIQUE NOT NULL)、`user_id``merchant_id``shipping_address` (JSONB NOT NULL)、`order_status`
- `public.ml_delivery_tasks`:配送任务表,重要列:`id``order_id` (UNIQUE NOT NULL引用 `ml_orders.id`)、`driver_id` (引用 `ml_delivery_drivers.id`)、`pickup_address`/`delivery_address` (JSONB)、`status` (CHECK in (1..6))。注意:联系人信息应内嵌到地址 JSON 中(没有单独 `pickup_contact` 字段)。
---
**我执行的主要步骤(已实现的幂等脚本)**
1. 查找或创建 `ak_users`(按 `auth_id` 唯一):避免重复插入,若已存在则复用其 `id`
2. 查找或创建 `ml_delivery_drivers`(以 `ak_users.id``user_id`):注意去掉对不存在列的引用(例如 `phone`)。
3. 插入两个测试订单(使用唯一 `order_no` 标识,若已存在则跳过),`shipping_address` 使用 JSONB 格式且包含 `contact`/`phone`/`province`/`city`/`detail` 等字段。
4. 为每个订单创建对应的 `ml_delivery_tasks`:一个保持 `status = 1`(可接单,`driver_id = NULL`),另一个设置 `status = 4` 并分配上步骤创建或找到的 `driver_id`
下面是我实际使用的幂等 SQL已去除不存在列并兼容 schema。在 Supabase SQL Editor 中以 `Role = postgres` 运行整段脚本即可。
```sql
-- 替换 auth_id 为你的 auth.users.id示例值可替换
WITH
found_user AS (
SELECT id FROM public.ak_users WHERE auth_id = 'dae9f45b-3955-43ae-992f-a3e24beaa520'
),
ins_user AS (
INSERT INTO public.ak_users (id, auth_id, email, username, created_at)
SELECT uuid_generate_v4(), 'dae9f45b-3955-43ae-992f-a3e24beaa520', 'test+delivery@example.com', 'test_delivery_user', NOW()
WHERE NOT EXISTS (SELECT 1 FROM found_user)
RETURNING id
),
user_id AS (
SELECT id FROM ins_user
UNION ALL
SELECT id FROM found_user
LIMIT 1
),
found_driver AS (
SELECT id, user_id FROM public.ml_delivery_drivers WHERE user_id = (SELECT id FROM user_id) LIMIT 1
),
ins_driver AS (
INSERT INTO public.ml_delivery_drivers (id, user_id, real_name, id_card, created_at)
SELECT uuid_generate_v4(), (SELECT id FROM user_id), '张师傅', 'ID-TEST-0001', NOW()
WHERE NOT EXISTS (SELECT 1 FROM found_driver)
RETURNING id, user_id
),
driver_row AS (
SELECT id, user_id FROM ins_driver
UNION ALL
SELECT id, user_id FROM found_driver
LIMIT 1
),
ins_order_1 AS (
INSERT INTO public.ml_orders (
order_no, user_id, merchant_id,
product_amount, shipping_fee, total_amount,
shipping_address, created_at, updated_at
)
SELECT
'TEST-DELIV-20260202-001',
(SELECT id FROM user_id),
(SELECT id FROM user_id),
88.00, 12.00, 100.00,
('{"contact":"李小明","phone":"13800000002","province":"上海市","city":"上海市","district":"浦东新区","street":"测试路100号","detail":"5楼502室","lat":31.2000,"lng":121.5000}')::jsonb,
NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM public.ml_orders WHERE order_no = 'TEST-DELIV-20260202-001')
RETURNING id, order_no, order_status
),
sel_order_1 AS (
SELECT id, order_no FROM ins_order_1
UNION ALL
SELECT id, order_no FROM public.ml_orders WHERE order_no = 'TEST-DELIV-20260202-001'
LIMIT 1
),
ins_task_1 AS (
INSERT INTO public.ml_delivery_tasks (
order_id, pickup_address, delivery_address, delivery_fee, status, created_at, updated_at
)
SELECT
(SELECT id FROM sel_order_1),
('{"contact":"商家小二","phone":"13900000002","street":"商家路1號","city":"上海市","detail":"商家门店A"}')::jsonb,
('{"contact":"李小明","phone":"13800000002","street":"测试路100号","city":"上海市","detail":"5楼502室"}')::jsonb,
12.00,
1,
NOW(), NOW()
WHERE NOT EXISTS (
SELECT 1 FROM public.ml_delivery_tasks dt WHERE dt.order_id = (SELECT id FROM sel_order_1)
)
RETURNING id, order_id, status, driver_id
),
ins_order_2 AS (
INSERT INTO public.ml_orders (
order_no, user_id, merchant_id,
product_amount, shipping_fee, total_amount,
shipping_address, created_at, updated_at
)
SELECT
'TEST-DELIV-20260202-002',
(SELECT id FROM user_id),
(SELECT id FROM user_id),
59.00, 8.00, 67.00,
('{"contact":"王小红","phone":"13800000003","province":"上海市","city":"上海市","district":"闵行区","street":"示例街20号","detail":"2幢101室","lat":31.1000,"lng":121.4000}')::jsonb,
NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM public.ml_orders WHERE order_no = 'TEST-DELIV-20260202-002')
RETURNING id, order_no, order_status
),
sel_order_2 AS (
SELECT id, order_no FROM ins_order_2
UNION ALL
SELECT id, order_no FROM public.ml_orders WHERE order_no = 'TEST-DELIV-20260202-002'
LIMIT 1
),
ins_task_2 AS (
INSERT INTO public.ml_delivery_tasks (
order_id, driver_id, pickup_address, delivery_address,
delivery_fee, status, assigned_at, picked_at, created_at, updated_at
)
SELECT
(SELECT id FROM sel_order_2),
(SELECT id FROM driver_row),
('{"contact":"商家B","phone":"13900000003","street":"商家街2號","city":"上海市","detail":"商家门店B"}')::jsonb,
('{"contact":"王小红","phone":"13800000003","street":"示例街20号","city":"上海市","detail":"2幢101室"}')::jsonb,
8.00,
4,
NOW(), NOW(), NOW(), NOW()
WHERE NOT EXISTS (
SELECT 1 FROM public.ml_delivery_tasks dt WHERE dt.order_id = (SELECT id FROM sel_order_2)
)
RETURNING id, order_id, status, driver_id
)
SELECT
(SELECT id FROM user_id) AS ak_user_id,
(SELECT id FROM driver_row) AS delivery_driver_id,
(SELECT id FROM sel_order_1) AS order1_id,
(SELECT order_no FROM sel_order_1) AS order1_no,
(SELECT id FROM sel_order_2) AS order2_id,
(SELECT order_no FROM sel_order_2) AS order2_no,
(SELECT id FROM ins_task_1) AS task1_id,
(SELECT id FROM ins_task_2) AS task2_id;
```
---
**常见错误与处理**
- 错误:列不存在(例如 `address` / `phone` / `pickup_contact`)。处理:核对 `complete_mall_database.sql` 中的表结构,确认应使用 `shipping_address`、并将联系人内嵌到 JSON 中。
- 错误:插入触发 FK 或 NOT NULL 违规23503 / 23502。处理按顺序插入依赖表或使用已存在的 FK 值(例如先查找 `ak_users` 再插入 driver
- 错误唯一约束冲突23505例如 `ml_delivery_tasks.order_id` 唯一)。处理:脚本使用 `WHERE NOT EXISTS``ORDER_NO` 检查以避免重复插入。
- 无行返回UPDATE/SELECT 返回 “Success. No rows returned”可能因为复制 id 时包含不可见字符,或 WHERE 条件不匹配。建议先用 `SELECT``order_no` 查找 task再用 `UPDATE ... FROM public.ml_orders WHERE order_no = '...' RETURNING ...` 更新并检查返回值。
---
**前端验证(我在 `pages/mall/delivery/index.uvue` 已加入日志)**
1. 刷新配送端页面(或重新打开),观察控制台日志:
- `loadDriverInfo` 日志:应输出 `driverInfo`,例如打印 `loadDriverInfo: try user_id=... res=...`
- `loadCurrentTask` 日志:输出查询结果 `loadCurrentTask: driverId=... res=...`,若 driver 有未完成任务status < 5`currentTask` 会被设置。
- `loadAvailableOrders` 日志:输出 `loadAvailableOrders: query result=...`,用于验证 `status = 1``driver_id IS NULL` 的任务是否列出。
2. 若需要把某条 `ml_delivery_tasks``status` 改为 4分配给司机可运行
```sql
UPDATE public.ml_delivery_tasks dt
SET status = 4,
driver_id = '<DRIVER_ID>',
assigned_at = NOW(),
updated_at = NOW()
FROM public.ml_orders o
WHERE dt.order_id = o.id
AND o.order_no = 'TEST-DELIV-20260202-001'
RETURNING dt.id, dt.order_id, dt.status, dt.driver_id, dt.assigned_at, dt.updated_at;
```
执行后刷新前端并贴回 `loadCurrentTask` / `loadAvailableOrders` 的日志,以便确认页面行为。
---
**附:运行建议与注意事项**
- 优先在 Supabase SQL Editor 中以 `Role = postgres` 执行插入与更新脚本,能避免 RLS 导致的“无行返回”。
- 若需要创建 `ml_order_items`,需先确保 `ml_products` 存在或用最小 mock 产品插入,否则会触发 FK 约束错误。为减少依赖,我先仅创建 `ml_orders``ml_delivery_tasks`
- 保持 `order_no` 的唯一性(测试用的 `order_no` 推荐加时间戳或前缀 `TEST-`)。
如需我把脚本扩展为同时创建 `ml_products` / `ml_order_items`(以便测试完整订单页),或希望我直接在 repo 中加入一个可执行的 SQL 脚本文件,请回复我想要的范围,我来补充。
---
作者:自动化助手(配合用户执行并记录)
日期2026-02-02

View File

@@ -0,0 +1,170 @@
# Delivery 模块 — 总体架构
## 概要
本文件给出配送delivery模块的总体架构说明覆盖前端视图组件、后端数据表与 API 交互、关键业务流、并发/事务策略与建议改进方向,便于开发者与运维快速理解与修改。
## 架构概览
- 前端Uni-app / .uvue 组件):负责页面渲染、用户交互与轻量校验,主要文件位于 `pages/mall/delivery/`
- 后端Supabase/Postgres + RPC负责数据持久化、原子操作、权限校验与并发控制。建议将关键并发操作例如“接单”放在后端 RPC/函数中执行。
- 数据层Postgres 表):主表包括 `ml_orders``ml_order_items``ml_shops``ml_delivery_tasks``ml_delivery_drivers` 等。
- 通信:前端通过 Supabase 客户端或后端 HTTP RPC 调用进行读写;推荐对写操作使用后端受限 RPC减小竞态风险。
## 前端组件映射(关键文件)
- 入口与导航:`pages/mall/delivery/index.uvue`(模块入口)
- 任务列表 / 历史:`pages/mall/delivery/tasks.uvue``pages/mall/delivery/order-history.uvue`
- 任务详情 / 订单详情:`pages/mall/delivery/task-detail.uvue``pages/mall/delivery/order-detail.uvue`
- 配送员信息:`pages/mall/delivery/profile.uvue` / `profile-edit.uvue`
- 收益 / 统计:`pages/mall/delivery/earnings.uvue`
- 车辆管理:`pages/mall/delivery/vehicle.uvue``vehicle-add.uvue``vehicle-edit.uvue`
- 帮助/反馈/测试:`help-center.uvue``feedback.uvue``test.uvue`
对应文档(开发说明)位于 `pages/mall/delivery/doc/`,每个视图对应一个 `.md` 说明文件(例如 `order-detail.md``tasks.md` 等)。
## 数据模型(常用字段 / 表)
- `ml_orders`: id, cid, order_no, user_id, merchant_id, order_status / status, total_amount, shipping_fee, paid_amount, shipping_address, remark, created_at
- `ml_order_items`: id, order_id, product_id, product_name, quantity, price, image_url
- `ml_shops`: id, merchant_id, shop_name, contact_name, contact_phone, address
- `ml_delivery_tasks`: id, order_id, driver_id, status, distance, estimated_time, pickup_time, delivered_time, created_at
- `ml_delivery_drivers`: id, user_id, vehicle_id, status
字段命名在代码中可能存在兼容写法(如 `order_status` vs `status``shipping_fee` vs `delivery_fee`),建议在前端统一使用一个转换函数 `normalizeOrderRow(row)` 做映射。
## 关键业务流(简要)
- 接单Accept
- 不要在前端直接做盲写;推荐后端 RPC 原子化操作:
- SQL 示例(后端执行):
```sql
UPDATE ml_delivery_tasks
SET driver_id = $1, status = 2
WHERE order_id = $2 AND (driver_id IS NULL OR driver_id = '')
RETURNING *;
```
- 前端调用 RPC根据返回行数判断是否接单成功若返回 0 行则提示“已被其他配送员接单”。
- 确认取货 / 确认送达
- 后端更新 `ml_delivery_tasks` 的时间戳字段(`pickup_time` / `delivered_time`)并更新 `ml_orders.order_status`,前端在成功回调后同步 UI 状态并展示时间戳。
- 读取详情页order-detail
- 并行查询 `ml_orders`, `ml_order_items`, `ml_shops`, `ml_delivery_tasks` 并合并结果,务必对空值做兼容处理(地址可能是字符串或对象)。
## 并发与事务策略
- 将所有会改变接单归属或状态的操作放在数据库事务或后端函数中执行,前端仅调用并根据返回结果更新 UI。
- 使用行级条件(`WHERE driver_id IS NULL` / `WHERE order_status = expected`)与 `RETURNING` 判断是否成功,避免乐观锁冲突。
- 在前端增加防重loading 标志位、按钮禁用)和超时重试策略,并记录幂等请求 ID如必要
## 错误处理与监控
- 前端对所有写操作展示用户友好错误(网络错误 / 已被接单 / 权限不足)。
- 后端应记录关键操作日志(接单、拒单、确认取货/送达),并导出指标(接单失败率、并发冲突数)。
## 权限与安全
- 接单/确认类操作必须在后端校验当前用户是合法配送员(基于 `ml_delivery_drivers.user_id`),并确保 RPC 仅对认证用户开放。
## 开发与测试建议
- 在 `pages/mall/delivery/test/create_test_orders.sql` 中准备自动化测试数据,用于本地/CI 环境的集成测试。
- 在前端加入单元测试/集成测试用例,模拟并发接单场景,验证后端 RPC 行为。
## 建议改进(优先级)
- 高:把接单逻辑迁移为后端 RPC前端只调用并根据返回结果更新 UI。
- 高:在所有变更接口返回受影响行数与最新状态,便于前端做幂等与回退。
- 中:统一并导出状态常量(例如 `ORDER_STATUS`)供前端与文档引用,避免魔法数字。
- 中:为详情页加入字段规范示例(在 `order-detail.md` 中补充),并同步到 API 文档。
---
(如需将 `order-detail.uvue` 的接单逻辑改为调用 RPC 并添加 loading 状态,我可以继续提交补丁。)
# Delivery 模块文档
本文档为 `pages/mall/delivery` 模块的开发与运维手册,已按与 `analytics` 模块相近的结构补充:概览、文件清单、技术特性、页面架构、实现要点、部署与测试建议、下一步计划与代码质量指引。
## 概览
配送端Delivery面向配送员覆盖司机状态管理、任务与订单流转、配送记录/历史、收入统计、个人资料与车辆管理、帮助与反馈。页面既包含前端聚合逻辑也与后端Supabase / RPC交互实现关键业务。
## 文件清单(快速索引)
- [about.uvue](pages/mall/delivery/about.uvue) — 关于我们(静态)
- [index.uvue](pages/mall/delivery/index.uvue) — 配送端首页(司机信息、今日统计、可接订单)
- [tasks.uvue](pages/mall/delivery/tasks.uvue) — 任务列表(筛选、分页)
- [task-detail.uvue](pages/mall/delivery/task-detail.uvue) — 任务详情(联系、取货、完成)
- [order-detail.uvue](pages/mall/delivery/order-detail.uvue) — 订单详情(多表聚合、状态流转)
- [order-history.uvue](pages/mall/delivery/order-history.uvue) — 历史订单(本地合并刚完成订单显示)
- [earnings.uvue](pages/mall/delivery/earnings.uvue) — 收入明细(按 `order_no` 聚合)
- [profile.uvue](pages/mall/delivery/profile.uvue) — 个人中心与入口
- [profile-edit.uvue](pages/mall/delivery/profile-edit.uvue) — 编辑资料(头像、服务区域)
- [vehicle.uvue](pages/mall/delivery/vehicle.uvue) — 车辆管理(列表/新增/编辑)
- [vehicle-add.uvue](pages/mall/delivery/vehicle-add.uvue) — 添加车辆表单
- [vehicle-edit.uvue](pages/mall/delivery/vehicle-edit.uvue) — 编辑车辆
- [feedback.uvue](pages/mall/delivery/feedback.uvue) — 意见反馈(图片、关联订单)
- [help-center.uvue](pages/mall/delivery/help-center.uvue) — 帮助中心/FAQ
- [ratings.uvue](pages/mall/delivery/ratings.uvue) — 评价记录(分页/筛选)
- [settings.uvue](pages/mall/delivery/settings.uvue) — 设置页
- [test.uvue](pages/mall/delivery/test.uvue) — Supabase 连接/权限诊断
## 技术特性
- UTS / Uni-app 页面实现(`.uvue`),兼容 UTS Android 规范类型定义、UTSJSONObject 使用等)。
- 使用 `supa`(项目内 Supabase 封装进行数据读取、RPC 调用与认证。示例:`supa.from('ml_delivery_tasks')`、`supa.rpc('rpc_analytics_top_products', params)`analytics 模式的参考)。
- 本模块使用本地存储做页面间临时数据传递(`uni.setStorageSync`),并在部分页面(如 `vehicle`、`order-history`)合并本地临时项以支持无后端或原型场景。
- 对接建议:关键写操作(接单/状态变更)必须依赖后端原子性保护(事务或 WHERE driver_id IS NULL 的更新语句)。
## 页面架构与实现要点
- 首页(`index.uvue`):聚合 `driver`、`todayStats`、`currentTask` 与 `availableOrders`;包含 `_transformTask` 用于兼容多种地址/联系人字段格式。
- 订单详情(`order-detail.uvue`):按 id 格式选择查询字段(`id`/`cid`/`order_no`),并联查 `ml_order_items`, `ml_shops`, `ml_delivery_tasks`;提供接单/拒单/确认取货/确认送达等操作。
- 任务流(`tasks` / `task-detail`):任务筛选(待接单/进行中/已完成)与详情页的状态变更操作;完成时应记录时间戳与凭证以便对账。
- 收入(`earnings`):前端按 `order_no` 聚合并补齐来源类型,建议后端提供已聚合接口以提升性能与准确性。
- 个人/车辆页:使用图片上传、表单校验与本地缓存作为临时方案;生产应对接车辆管理 API 并保存到服务端。
## 典型 DB / RPC 示例
Supabase (supa) 示例:
```
// 查询司机信息
await supa.from('ml_delivery_drivers').select('*').eq('user_id', userId).limit(1).execute()
// 查询可接任务(示例)
await supa.from('ml_delivery_tasks').select('*').is('driver_id', null).eq('status', 1).order('created_at', { ascending: false }).range(offset, offset+size-1).execute()
// 接单(需后端并发保护)
await supa.from('ml_delivery_tasks').update({ driver_id: driverId, status: 2 }).eq('id', taskId).eq('driver_id', null).execute()
```
## 样式与 UI 设计
- 推荐采用与 `analytics` 模块相同的卡片式布局与 KPI 卡片样式(便于统一风格);关键组件(任务卡、订单明细卡、统计卡)应抽象成可复用组件以减少重复代码。
## 部署与配置
- 环境变量:确认 `SUPA_URL`/`SUPA_KEY` 在 `ak/config.uts` 或环境配置中正确配置。
- 本地测试:使用 `pages/mall/delivery/test.uvue` 验证 Supabase 连接与权限(避免在仓库暴露明文 key
## 测试建议
- 功能测试:接单并发测试、状态流转(取货/送达)、图片上传与表单校验、分页与历史数据展示。
- 兼容性Android/不同屏幕尺寸测试UTS Android 兼容性须验证)。
- 性能:大量任务/历史记录分页测试,收入聚合在后端做性能验证。
## 下一步计划
1. 抽取并复用 UI 组件(任务卡、订单详情卡、统计 KPI 卡)。
2. 将前端聚合逻辑(如 `earnings`)迁移或补充后端聚合接口以提升性能。
3. 用后端事务或乐观锁增强接单并发保护,并在前端实现失败回退提示。
4. 添加自动化测试用例覆盖关键流程(接单/确认/拒单/结算)。
---
Generated on: 2026-02-02
## 快速参考(已合并页摘要)
- `about.uvue`:关于我们(静态信息,运营可从 CMS 拉取版本/更新日志)。
- `help-center.uvue`:帮助中心 / FAQ本地数组或后端搜索支持分类与全文检索
- `settings.uvue`:设置页(通知、语言、隐私、登出),存储建议使用 `uni.setStorageSync` + 后端同步。
- `feedback.uvue`:意见反馈(文本 + 图片 + 关联订单),图片需先上传再提交 URL。
- `ratings.uvue`:评价记录列表(分页筛选,可能需要后端审核/屏蔽)。
- `profile.uvue` / `profile-edit.uvue`:个人中心与编辑(头像上传、服务区域、车辆信息),建议在保存后同步刷新缓存。
- `vehicle*.uvue`:车辆管理相关(列表/新增/编辑),当前以本地存储为演示,生产应对接 REST 接口并做权限校验。
(已把上述 per-page md 移动到 `pages/mall/delivery/doc/archive/`,保留历史版本以便追溯。)
---
Generated on: 2026-02-02
---
Generated on: 2026-02-02

View File

@@ -0,0 +1,89 @@
# earnings.uvue — 收入明细
## 概要
`order_no` 聚合展示配送员收入明细(配送费、用户打赏、商家打赏),支持分页加载与按时间区间统计。
## 数据结构
- `EarningItem`
- `id: string`
- `order_no: string`
- `date: string`
- `amount: number`
- `source: 'delivery_fee' | 'user_tip' | 'merchant_tip' | string`
- `distance?: number`
- `OrderEarning`
- `order_no: string`
- `date: string`
- `distance?: number`
- `totalAmount: number`
- `details: EarningItem[]`
## 关键方法
- `loadAllEarnings()`
- 从后端或本地数据源取原始 `EarningItem[]`,按 `order_no` 聚合成 `OrderEarning[]`
- 对每个 `OrderEarning.details` 补齐三类来源占位项amount=0以保证前端渲染顺序稳定。
- `calculateTotalStats()`
- 计算 `totalEarnings`, `totalUserTips`, `totalMerchantTips`
- `loadPage()` / `loadMore()`
- 基于 `pageSize` 控制分页渲染,避免一次性载入全部历史记录。
## 聚合示例(核心)
```
const orderMap = new Map()
raw.forEach(item => {
let order = orderMap.get(item.order_no)
if (!order) {
order = { order_no: item.order_no, date: item.date, distance: item.distance, totalAmount: 0, details: [] }
orderMap.set(item.order_no, order)
}
order.totalAmount += Number(item.amount || 0)
order.details.push(item)
})
// 补齐三类来源
orderMap.forEach(order => {
const has = type => order.details.some(d => d.source === type)
;['delivery_fee','merchant_tip','user_tip'].forEach(t => { if (!has(t)) order.details.push({ id: `${order.order_no}_${t}`, order_no: order.order_no, amount: 0, source: t }) })
})
const result = Array.from(orderMap.values())
```
## 接口 / DB 建议
- 推荐后端提供按订单聚合的接口:
- GET `/api/driver/{id}/earnings?start=&end=&page=&size=`
- 返回 `OrderEarning[]` 已聚合的数据以减轻前端压力
## 数据源更新包含已接订单Accepted
- 需求说明为了与“历史订单”页面保持一致收入统计应包含配送员已接取accepted/assigned的订单不仅限于已完成订单。也就是说只要 `ml_delivery_tasks``driver_id` = 当前司机且 `status >= 2`(已接取或进行中),其对应的订单都应计入收入统计范围。
- 推荐 Supabase 查询示例(后端或前端按需实现):
```
-- 查询分配给当前司机且已接取的任务对应的订单ID
SELECT DISTINCT order_id FROM ml_delivery_tasks WHERE driver_id = :driverId AND status >= 2;
-- 基于 order_id 查询订单及收入项示例ml_orders + ml_delivery_tips
SELECT o.id,o.order_no,o.created_at,o.distance,o.delivery_fee, t.amount as tip_amount, 'user_tip' as tip_source
FROM ml_orders o
LEFT JOIN ml_delivery_tips t ON t.order_id = o.id
WHERE o.id IN (<order_id_list>)
ORDER BY o.created_at DESC
LIMIT :size OFFSET :offset;
```
- 若在前端直接使用 `supabase-js`
```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)
const ordersRes = await supa.from('ml_orders').select('*,ml_delivery_tips(*)').in('id', orderIds).order('created_at',{ascending:false}).limit(size).execute()
```
注意:若订单很多,务必在后端做分页/聚合以减轻客户端开销。
## 注意事项
- 在金额计算中使用整数 cents 或 BigInt 或固定小数处理,避免浮点误差。
- 大量数据应后端分页/聚合并支持过滤(按时间、按来源)。

View File

@@ -0,0 +1,63 @@
# index.uvue — 配送端首页
## 概要
配送员主界面,包含司机信息、在线/离线切换、今日统计、当前配送任务、附近可接订单与快捷入口。
## 数据结构
- DeliveryDriverType (来自 `ml_delivery_drivers`)
- id, user_id, real_name, avatar_url, work_status, vehicle_type, vehicle_number, rating, service_areas, ...
- CurrentTaskType / AvailableOrderType
- id, order_no, status, pickup_address, delivery_address, pickup_contact, delivery_contact, delivery_fee, distance, estimated_time, created_at
## 关键方法
- `onLoad()`
- 调用 `getCurrentUser()` 确保用户上下文可用
- 执行:`loadDriverInfo()`, `loadTodayStats()`, `loadCurrentTask()`, `loadAvailableOrders()`
- `loadDriverInfo()`
-`ml_delivery_drivers` 表按 `user_id` 查询并更新 `driverInfo`
- `loadTodayStats()`
- 查询 `ml_delivery_tasks` 在当天范围内的记录,计算完成订单数、总收入、配送里程等。
- `loadCurrentTask()`
- 查询 `ml_delivery_tasks` 中 driver_id 的未完成任务status < 5并取最新一条作为当前任务。
- `loadAvailableOrders()`
-`isOnline` 且无 `currentTask`,从 `ml_delivery_tasks` 查询 `driver_id IS NULL` 且 status=1 的订单列表(可按地理位置筛选)。
- `_transformTask(task)`
- 兼容处理 `pickup_address` / `delivery_address``pickup_contact` 的多种格式(对象或 JSON 字符串),输出页面期望结构。
## 关键 DB 查询 示例
```
await supa.from('ml_delivery_drivers').select('*').eq('user_id', userId).limit(1).execute()
await supa.from('ml_delivery_tasks')
.select('id,delivery_fee,distance,created_at,status')
.eq('driver_id', driverId)
.gte('created_at', start)
.lte('created_at', end)
.execute()
```
## 交互与状态流
- `toggleWorkStatus()`:切换 `isOnline` 并调用 `startWork()` / `stopWork()`。上线时会刷新可接订单列表。
- 接单/开始取货/确认取货/开始配送/确认送达等均通过对 `ml_delivery_tasks``update` 操作变更 `status`,并在成功后更新本地 `currentTask`
## 注意事项
- 高并发接单场景需后端保证原子性(乐观锁或 DB 事务)以防止竞单冲突。
- `loadAvailableOrders()` 最好按司机服务区域与距离筛选,并使用分页/实时推送代替频繁轮询。
- 日志console.log语句便于调试但生产环境建议使用集中化日志系统。
# index.uvue — 配送端首页
页面目的:配送员主界面,展示司机资料、工作状态、今日统计、当前任务与可接订单。
关键点:
- 数据加载:`onLoad()` 调用 `getCurrentUser()` 后依次调用 `loadDriverInfo()`, `loadTodayStats()`, `loadCurrentTask()`, `loadAvailableOrders()`
- Supabase 交互:通过 `supa.from(...).select(...).eq(...).execute()` 查询 `ml_delivery_drivers``ml_delivery_tasks` 等表。
- 兼容解析:`_transformTask(task)` 将 DB 返回的地址/contact 字段兼容解析为页面所需结构(处理字符串或对象)。
- 操作:接单/开始取货/确认取货/开始配送/确认送达等,会调用 supa 更新 `ml_delivery_tasks` 状态并同步页面状态。
注意:对 supa 操作有大量 try/catch页面对无用户 ID 情况做了保护,适合直接对接真实后端。

View File

@@ -0,0 +1,118 @@
## 目的
当“附近订单”过多时,在主页面展示前 N 条(当前为 20 条),需要提供“更多”入口,跳转到单独页面展示所有可接取的订单并支持翻页/上拉加载。
## 路由与文件
- 主入口:在 `pages/mall/delivery/index.uvue` 的“附近订单”区块新增“更多”按钮,导航到新页面。
- 新页面文件:`pages/mall/delivery/all.uvue`(建议路径)
- 文档说明文件:`pages/mall/delivery/doc/more-orders.md`(本文件)
## 用户流程
1. 用户在 `index.uvue` 看到“附近订单”列表(最多 20 条)。
2. 当可接取订单数量 >= 20 时,显示“更多”按钮。
3. 点击“更多”跳转到 `all.uvue`,展示可接取订单的完整列表(分页或无限滚动)。
4. 用户可在新页面接受订单或刷新列表。
## 后端 / 查询设计示例Supabase 客户端)
- 建议分页参数:`pageSize = 50`(可配置)。
- 前端通过 offset/pagination 请求:
示例(首次加载 page=0, size=50
```ts
const page = 0
const size = 50
const res = await supa.from('ml_delivery_tasks')
.select('*')
.is('driver_id', 'null')
.eq('status', 1)
.order('created_at', { ascending: false })
.range(page*size, page*size + size - 1)
.execute()
```
- 翻页时将 `page` 增加;上拉加载时继续请求下一段 `range`
- 后端/数据库建议对 `ml_delivery_tasks` 建立索引status, driver_id, created_at以加速查询。
## 前端实现建议
- `index.uvue`:在 `availableOrders` 列表下方加入:
- 条件显示:`availableOrders.length >= 20` 时显示“更多”按钮。
- 点击处理函数:`uni.navigateTo({ url: '/pages/mall/delivery/all' })`
- `all.uvue` 页面职责:
- 使用与 `index.uvue` 相同的 `supa` 客户端和 `_transformTask` 方法复用数据格式化逻辑。
- 支持分页page + pageSize或无限滚动onReachBottom 加载下一页)。
- 显示空状态、加载中状态和错误提示。
示例数据加载片段(伪代码):
```ts
data() { return { page: 0, pageSize: 50, items: [], loading: false, finished: false } }
async loadPage() {
if (this.loading || this.finished) return
this.loading = true
const res = await supa.from('ml_delivery_tasks')
.select('*')
.is('driver_id', 'null')
.eq('status', 1)
.order('created_at', { ascending: false })
.range(this.page * this.pageSize, (this.page+1)*this.pageSize - 1)
.execute()
const rows = Array.isArray(res.data) ? res.data : []
if (rows.length < this.pageSize) this.finished = true
this.items.push(...rows.map(r => this._transformTask(r)))
this.page += 1
this.loading = false
}
```
## 并发与安全
- 接单时应在服务器端/数据库层做原子性判断,避免竞态:
- 示例(伪 SQL
UPDATE ml_delivery_tasks
SET driver_id = :driverId, status = 2
WHERE id = :taskId AND driver_id IS NULL AND status = 1
RETURNING id
- 前端在发起“接受订单”请求后,基于返回结果确认是否成功;若失败需提示用户订单已被接取。
## 接受标准/验收条件
-`index.uvue` 点击“更多”能跳转到 `all.uvue`
- `all.uvue` 能正确加载 >20 条可接订单并支持继续加载直到无更多数据。
- 接单操作的竞态由后端或事务性查询处理,前端能正确反馈成功/失败。
## 测试数据与验证
- 使用现有文档 `doc_mall/database/realistic_mock_data.sql` 创建若干测试订单;也可循环插入多条 `ml_delivery_tasks`status=1, driver_id=NULL用于分页测试。
示例快速生成Postgres 伪 SQL
```sql
INSERT INTO public.ml_delivery_tasks (order_id, pickup_address, delivery_address, delivery_fee, status, created_at, updated_at)
SELECT uuid_generate_v4(), '{"detail":"店铺"}'::jsonb, '{"detail":"用户地址"}'::jsonb, 5.0, 1, NOW() - (i || ' minutes')::interval, NOW()
FROM generate_series(1,200) as s(i);
```
## 开发任务清单(简要)
1.`pages/mall/delivery/index.uvue` 增加“更多”按钮並导航。
2. 新建 `pages/mall/delivery/all.uvue`,实现分页/上拉加载并复用 `_transformTask`
3. 后端或 DB 层确保接单操作的原子性。
4. 编写 E2E 或手动测试脚本验证加载与接单行为。
## 是否需要我实现?
如果你需要,我可以:
- 直接在 `index.uvue` 添加“更多”按钮示例(小改动)。
- 新增 `pages/mall/delivery/all.uvue` 的样板实现并提交 PR 样例。
---
文档已创建:`pages/mall/delivery/doc/more-orders.md`

View File

@@ -0,0 +1,51 @@
# order-detail.uvue — 订单详情
## 概要
展示单笔订单全部信息并提供状态流转(接单、确认取货、确认送达、拒单等)。页面会联合查询 `ml_orders``ml_order_items``ml_shops``ml_delivery_tasks`
## 数据结构(常用字段)
- `Order`
- `id: string | number`
- `order_no: string`
- `cid?: number` (兼容旧字段)
- `shipping_address | pickup_address`: object/string
- `shipping_fee`, `total_amount`, `status`
- `OrderItem`
- `id`, `order_id`, `product_name`, `qty`, `price`
- `DeliveryTask`
- `id`, `order_id`, `driver_id`, `status`, `accepted_at`, `picked_at`, `delivered_at`
## 关键方法
- `onLoad(options)`
- 解析 `options.id``options.status`,调用 `loadOrderDetail(id)`
- `loadOrderDetail(id)`
- 判断 ID 类型UUID / 数字 / 非数字)以决定查询字段(`id``cid``order_no`)。
- 并行查询 `ml_orders`, `ml_order_items`, `ml_shops`, `ml_delivery_tasks` 并合并到页面状态。
- `acceptOrder()` / `rejectOrder(reason)`
- accept: 尝试对 `ml_delivery_tasks` 执行 `update driver_id` 操作并设置 `status=2`(处理中),需要后端并发保护。
- reject: 增加拒单原因到 `ml_delivery_tasks``order_notes` 并回滚本地 UI 状态。
- `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()
```
## 事务与并发注意
- 接单场景应使用后端原子性检查(数据库事务或行级乐观锁)以避免多司机同时接单。
- 前端接单流程:先尝试 update带 where driver_id IS NULL若返回 0 row affected 则提示已被接单。
## 错误处理与回退
- 捕获所有 supa 调用错误并将友好错误展示给用户(例如:'网络错误,请稍后重试')。
- 对可能缺失的字段(地址为字符串或对象)使用 `_transformAddress()` 做兼容处理。

View File

@@ -0,0 +1,31 @@
# order-history.uvue — 历史订单
## 概要
显示配送员历史订单(已完成/已取消等),并支持按时间范围过滤、分页和插入刚完成订单以做到“即时显示”。
## 数据结构
- `HistoryOrder`
- `order_no`, `id`, `status`, `delivered_at`, `total_amount`, `shop_name`
## 关键方法
- `loadOrderHistory({start,end,page,size})`
- 优先按 `ml_delivery_tasks` 中与 `driver_id` 相关的 `order_id` 拉取任务记录,再批量查询 `ml_orders` 获取详情。
- 若后端支持直接按 `driver_id` 返回已完成订单则调用后端聚合接口更高效。
- `checkForNewCompletedOrder()`
-`uni.getStorageSync('completed_order_for_history')` 读取并合并到 `orderHistory` 顶部,随后清除本地缓存键。
## DB 查询示例(伪 SQL / supa
```
const tasks = await supa.from('ml_delivery_tasks').select('order_id,delivered_at').eq('driver_id', driverId).eq('status', 4).order('delivered_at', { ascending: false }).limit(100).execute()
const orders = await supa.from('ml_orders').select('*').in('id', tasks.map(t=>t.order_id)).execute()
```
## 分页与筛选
- 使用后端分页(`page/size`),前端仅负责渲染和“加载更多”。
- 支持按日期区间和商家名模糊搜索。
## 注意事项
- 当从本地合并刚完成订单时,去重逻辑必不可少(按 `order_no``id`)。
- 对于大量历史数据,应依赖后端支持归档与按需加载。错误/异常应有兜底 UI空状态/重试按钮)。

View File

@@ -0,0 +1,25 @@
# profile-edit.uvue — 编辑资料
## 概要
编辑配送员个人资料(姓名、电话、头像、服务区域、车辆信息等),并将修改提交到后端保存。
## 数据结构
- `ProfileForm`
- `real_name`, `phone`, `avatar_url`, `service_areas[]`, `vehicle`{
`type`, `model`, `plate_no`
}
## 关键方法
- `chooseAvatar()``uni.chooseImage` 获取本地图片并调用 `uploadAvatar()` 上传。
- `uploadAvatar(file)`:上传到文件存储并返回 URL更新 `form.avatar_url`
- `saveProfile()`:表单校验后调用后端 API 更新 `ml_delivery_drivers``users` 表。
## 接口示例
```
await api.put('/driver/profile', { ...form })
```
## 注意事项
- 上传图片应限制大小并在客户端压缩以减少带宽。
- 服务区域通常是数组或 GeoJSON保存时与后端约定格式以支持范围匹配。

View File

@@ -0,0 +1,26 @@
# profile.uvue — 个人中心
## 概要
配送员个人中心,展示司机基本信息、当前工作状态、统计摘要(今日订单/收入)、常用入口(车辆管理/收入/设置/帮助)以及最近任务预览。
## 数据结构
- `DriverInfo`
- `id`, `user_id`, `real_name`, `avatar_url`, `work_status`, `rating`, `vehicle`(简要信息)
## 关键方法
- `loadDriverInfo()`:从 `ml_delivery_drivers``users` 表加载司机信息并设置 `driverInfo`
- `toggleWorkStatus()`:切换上线/下线并触发 `loadAvailableOrders()`(上线时)。
- `relocate()`:请求定位权限并更新司机服务位置(可调用后端保存)。
## 交互示例
```
async toggleWorkStatus() {
await supa.from('ml_delivery_drivers').update({ work_status: newStatus }).eq('id', driverInfo.id)
this.driverInfo.work_status = newStatus
}
```
## 注意事项
- 工作状态变更应通知后端并尽量在服务端发出状态变更事件WebSocket/推送)。
- 个人信息编辑应跳转到 `profile-edit`,并在保存后同步更新本页缓存。

View File

@@ -95,7 +95,11 @@
<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 class="section-header-actions">
<text class="refresh-btn" @click="refreshOrders">🔄 刷新</text>
<!-- 当可接取订单达到上限时显示更多入口 -->
<text v-if="availableOrders && availableOrders.length >= 20" class="more-btn" @click="goToAllOrders">更多 ➜</text>
</view>
</view>
<view v-if="availableOrders.length === 0" class="empty-orders">
@@ -164,6 +168,9 @@
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
@@ -211,6 +218,10 @@
data() {
return {
isOnline: true,
// 防抖:记录上次刷新时间,避免 onShow 导致的频繁 refresh
lastRefreshAt: 0,
// 控制是否启用自动刷新onShow——默认关闭避免频繁或意外刷新
enableAutoRefresh: false,
driverInfo: {
id: '',
@@ -243,101 +254,204 @@
}
},
onLoad() {
this.loadDriverInfo()
this.loadTodayStats()
this.loadCurrentTask()
this.loadAvailableOrders()
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()
},
onShow() {
// 页面显示时刷新数据
this.refreshData()
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: {
// 加载配送员信息
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
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) {
this.driverInfo = Object.assign(this.driverInfo, res.data[0])
}
} catch (e) {
console.error('loadDriverInfo error', e)
}
},
// 加载当前任务
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'
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)
}
},
// 加载可接取订单
loadAvailableOrders() {
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
}
// 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'
// 在加载过程中先清空,避免显示过期或闪现的数据
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_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 || ''
}
},
// 刷新数据
refreshData() {
this.loadTodayStats()
this.loadCurrentTask()
this.loadAvailableOrders()
async refreshData() {
await this.loadTodayStats()
await this.loadCurrentTask()
await this.loadAvailableOrders()
},
// 刷新订单列表
@@ -429,49 +543,71 @@
},
// 任务操作方法
acceptTask() {
// TODO: 调用API接受任务
if (this.currentTask) {
this.currentTask.status = 2 // 更新状态为“已接取”
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' })
}
uni.showToast({
title: '任务已接受',
icon: 'success'
})
},
startPickup() {
// TODO: 调用API开始取货
if (this.currentTask) {
this.currentTask.status = 3 // 更新状态为“取货中”
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' })
}
uni.showToast({
title: '开始取货',
icon: 'success'
})
},
confirmPickup() {
// TODO: 调用API确认取货
if (this.currentTask) {
this.currentTask.status = 5 // 更新状态为“已取货”
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' })
}
uni.showToast({
title: '取货完成',
icon: 'success'
})
},
// startDelivery() {
// // TODO: 调用API开始配送
// if (this.currentTask) {
// this.currentTask.status = 5 // 更新状态为“配送中”
// }
// uni.showToast({
// title: '开始配送',
// icon: 'success'
// })
// },
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() {
@@ -486,22 +622,23 @@
})
},
// 确认送达
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()
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)
uni.showToast({ title: '配送完成', icon: 'success' })
this.currentTask = null
this.loadAvailableOrders()
}
} catch (e) {
console.error('confirmDelivery error', e)
uni.showToast({ title: '操作失败', icon: 'none' })
}
},
contactCustomer() {
@@ -538,14 +675,22 @@
},
// 订单操作方法
acceptOrder(orderId: string) {
// TODO: 调用API接受订单
uni.showToast({
title: '订单已接受',
icon: 'success'
})
this.loadCurrentTask()
this.loadAvailableOrders()
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' })
await this.loadCurrentTask()
await this.loadAvailableOrders()
}
} catch (e) {
console.error('acceptOrder error', e)
uni.showToast({ title: '接受订单失败', icon: 'none' })
}
},
// 导航方法
@@ -554,6 +699,13 @@
url: '/pages/mall/delivery/order-history'
})
},
// 跳转到“全部可接订单”页面
goToAllOrders() {
uni.navigateTo({
url: '/pages/mall/delivery/all'
})
},
goToEarnings() {
uni.navigateTo({
@@ -666,6 +818,17 @@
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);

View File

@@ -179,6 +179,9 @@
</template>
<script lang="uts">
import supa, { supaReady } from '@/components/supadb/aksupainstance.uts'
import { getCurrentUserId } from '@/utils/store.uts'
export default {
data() {
return {
@@ -243,76 +246,114 @@ export default {
uni.navigateBack()
},
loadOrderDetail(orderId: string) {
// ✅ 保留从 URL 获取的状态
async loadOrderDetail(orderId: string) {
const originalStatus = this.order.status
// 模拟加载订单详情数据
const mockOrder = {
id: orderId,
order_no: 'ORD202401150001',
user_id: 'user_001',
merchant_id: 'merchant_001',
// ✅ 使用传入的 status而不是硬编码
status: originalStatus,
total_amount: 299.98,
discount_amount: 30.00,
delivery_fee: 8.00,
actual_amount: 277.98,
payment_method: 1,
payment_status: 1,
delivery_address: {
name: '张三',
phone: '13800138000',
detail: '北京市朝阳区某某街道某某小区1号楼101室'
},
created_at: '2024-01-15 14:30:00'
}
// ✅ 合并数据,保留传入的 status
Object.assign(this.order, mockOrder)
this.orderItems = [
{
id: 'item_001',
order_id: orderId,
product_id: 'product_001',
sku_id: 'sku_001',
product_name: '精选好物商品',
sku_specifications: { color: '红色', size: 'M' },
price: 199.99,
quantity: 1,
total_amount: 199.99,
product_image: '/static/product1.jpg'
try {
await supaReady
console.log('loadOrderDetail called', { orderId })
// 获取订单主表:按 id(UUID) / cid(数字) / order_no 三种可能匹配
let orderRes: 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 })
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()
}
]
this.merchant = {
id: 'merchant_001',
user_id: 'user_001',
shop_name: '优质好店',
shop_logo: '/static/shop-logo.png',
shop_banner: '/static/shop-banner.png',
shop_description: '专注品质生活',
contact_name: '店主小王',
contact_phone: '13800138000',
shop_status: 1,
rating: 4.8,
total_sales: 15680,
created_at: '2023-06-01'
}
this.pickupAddress = '北京市朝阳区商家街道123号'
this.customerNote = '请送到门口,谢谢'
this.merchantNote = '商品易碎,小心搬运'
this.deliveryNote = '已按时送达,顾客签收'
this.deliveryInfo = {
distance: 3.2,
estimated_time: 25,
courier_id: 'courier_001',
pickup_time: '2024-01-15 15:00:00',
delivery_time: '2024-01-15 15:25:00'
console.log('loadOrderDetail: orderRes=', orderRes)
if (orderRes && Array.isArray(orderRes.data) && orderRes.data.length > 0) {
const row = orderRes.data[0]
const shipping = row.shipping_address || {}
this.order = Object.assign(this.order, {
id: row.id,
order_no: row.order_no || '',
user_id: row.user_id || '',
merchant_id: row.merchant_id || '',
status: Number(row.order_status) || originalStatus,
total_amount: Number(row.total_amount || row.product_amount || 0),
discount_amount: Number(row.discount_amount || 0),
delivery_fee: Number(row.shipping_fee || row.shipping_fee || 0),
actual_amount: Number(row.paid_amount || row.actual_amount || 0),
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)
},
created_at: row.created_at || ''
})
this.customerNote = row.remark || ''
this.merchantNote = row.merchant_memo || ''
// 读取订单商品 - 使用从主表获得的真实 order id
const realOrderId = row.id
const itemsRes: any = await supa.from('ml_order_items').select('*').eq('order_id', realOrderId).execute()
console.log('loadOrderDetail: itemsRes=', itemsRes)
if (itemsRes && Array.isArray(itemsRes.data)) {
this.orderItems = itemsRes.data.map((it:any) => ({
id: it.id,
order_id: it.order_id,
product_id: it.product_id,
sku_id: it.sku_id,
product_name: it.product_name,
sku_specifications: it.specifications || it.sku_name || null,
price: Number(it.price) || 0,
quantity: Number(it.quantity) || 0,
total_amount: Number(it.total_amount) || 0,
product_image: it.image_url || it.product_image || ''
}))
}
// 读取商家店铺信息ml_shops
if (row.merchant_id) {
const shopRes: any = await supa.from('ml_shops').select('*').eq('merchant_id', row.merchant_id).limit(1).execute()
console.log('loadOrderDetail: shopRes=', shopRes)
if (shopRes && Array.isArray(shopRes.data) && shopRes.data.length > 0) {
const s = shopRes.data[0]
this.merchant = Object.assign(this.merchant, {
id: s.id || '',
user_id: s.merchant_id || '',
shop_name: s.shop_name || s.name || '',
shop_logo: s.shop_logo || '',
shop_banner: s.shop_banner || '',
shop_description: s.description || s.shop_description || '',
contact_name: s.contact_name || '',
contact_phone: s.contact_phone || '',
shop_status: s.status || 0,
rating: s.rating || 0,
total_sales: s.total_sales || 0,
created_at: s.created_at || ''
})
this.pickupAddress = s.address || ''
}
}
// 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 || ''
}
}
} catch (e) {
console.error('loadOrderDetail db error', e)
}
},
@@ -346,13 +387,20 @@ export default {
uni.showModal({
title: '确认取货',
content: '确认已从商家处取到商品?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
this.order.status = 4 // 更新为已取货
uni.showToast({
title: '取货确认成功',
icon: 'success'
})
try {
await supaReady
// 更新 delivery task
const dtRes: any = await supa.from('ml_delivery_tasks').update({ status: 4, pickup_time: new Date().toISOString() }).eq('order_id', this.order.id).execute()
// 同步订单状态
await supa.from('ml_orders').update({ order_status: 4 }).eq('id', this.order.id).execute()
this.order.status = 4
uni.showToast({ title: '取货确认成功', icon: 'success' })
} catch (e) {
console.error('confirmPickup db error', e)
uni.showToast({ title: '取货确认失败', icon: 'none' })
}
}
}
})
@@ -364,13 +412,20 @@ export default {
uni.showModal({
title: '确认送达',
content: '确认商品已送达到顾客手中?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
this.order.status = 5 // 更新为已完成
uni.showToast({
title: '送达确认成功',
icon: 'success'
})
try {
await supaReady
// 更新 delivery task
await supa.from('ml_delivery_tasks').update({ status: 6, delivered_time: new Date().toISOString() }).eq('order_id', this.order.id).execute()
// 更新订单状态为已送达
await supa.from('ml_orders').update({ order_status: 5 }).eq('id', this.order.id).execute()
this.order.status = 5
uni.showToast({ title: '送达确认成功', icon: 'success' })
} catch (e) {
console.error('confirmDelivery db error', e)
uni.showToast({ title: '送达确认失败', icon: 'none' })
}
}
}
})
@@ -382,13 +437,31 @@ export default {
uni.showModal({
title: '接受订单',
content: '确定接受这个配送订单吗?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
this.order.status = 2 // 更新为已接单
uni.showToast({
title: '订单接受成功',
icon: 'success'
})
try {
await supaReady
const userId = getCurrentUserId()
let driverId = null
if (userId) {
const dRes: any = await supa.from('ml_delivery_drivers').select('id').eq('user_id', userId).limit(1).execute()
if (dRes && Array.isArray(dRes.data) && dRes.data.length > 0) driverId = dRes.data[0].id
}
// 尝试更新已有 delivery task
const updateRes: any = await supa.from('ml_delivery_tasks').update({ driver_id: driverId, status: 2 }).eq('order_id', this.order.id).execute()
if (!(updateRes && Array.isArray(updateRes.data) && updateRes.data.length > 0)) {
// 若不存在,则插入新的 task
await supa.from('ml_delivery_tasks').insert({ order_id: this.order.id, driver_id: driverId, status: 2, created_at: new Date().toISOString() }).execute()
}
// 更新订单状态
await supa.from('ml_orders').update({ order_status: 2 }).eq('id', this.order.id).execute()
this.order.status = 2
uni.showToast({ title: '订单接受成功', icon: 'success' })
} catch (e) {
console.error('acceptOrder db error', e)
uni.showToast({ title: '接受订单失败', icon: 'none' })
}
}
}
})
@@ -400,13 +473,18 @@ export default {
uni.showModal({
title: '拒绝订单',
content: '确定拒绝这个配送订单吗?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
uni.showToast({
title: '订单已拒绝',
icon: 'success'
})
uni.navigateBack()
try {
await supaReady
// 标记订单为已拒绝order_status = 7
await supa.from('ml_orders').update({ order_status: 7 }).eq('id', this.order.id).execute()
uni.showToast({ title: '订单已拒绝', icon: 'success' })
uni.navigateBack()
} catch (e) {
console.error('rejectOrder db error', e)
uni.showToast({ title: '拒绝订单失败', icon: 'none' })
}
}
}
})

View File

@@ -68,6 +68,8 @@
<script lang="uts">
import type { DeliveryTaskType } from '@/types/mall-types.uts'
import supa, { supaReady } from '@/components/supadb/aksupainstance.uts'
import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
export default {
data() {
@@ -102,86 +104,125 @@ export default {
}
},
// 加载历史订单
loadOrderHistory() {
// TODO: 调用API获取历史订单列表
this.orderList = [
{
id: '1',
order_no: 'D202501081234',
status: 4, // 已接取
pickup_address: {
detail: '深圳公司',
area: '购物公园'
},
delivery_address: {
detail: '梅州',
area: '海岸城'
},
pickup_contact: {
name: '商家联系人',
phone: '138****6567'
},
delivery_contact: {
name: '张先生',
phone: '139****9786'
},
delivery_fee: 12.0,
distance: 8.2,
estimated_time: 25,
created_at: '2025-01-08T15:00:00Z'
},
{
id: '2',
order_no: 'D202501081235',
status: 5, // 已完成
pickup_address: {
detail: '福田区购物公园',
area: '购物公园'
},
delivery_address: {
detail: '南山区海岸城',
area: '海岸城'
},
pickup_contact: {
name: '商家联系人',
phone: '138****5678'
},
delivery_contact: {
name: '张先生',
phone: '139****1234'
},
delivery_fee: 12.0,
distance: 8.2,
estimated_time: 25,
created_at: '2025-01-08T15:00:00Z'
},
{
id: '3',
order_no: 'D202501081236',
status: 1, // 取货中
pickup_address: {
detail: '罗湖区东门步行街',
area: '罗湖'
},
delivery_address: {
detail: '福田区市民中心',
area: '福田'
},
pickup_contact: {
name: '商家联系人',
phone: '138****5678'
},
delivery_contact: {
name: '李先生',
phone: '139****5678'
},
delivery_fee: 10.0,
distance: 5.0,
estimated_time: 15,
created_at: '2025-01-08T16:00:00Z'
// 加载历史订单(从数据库读取)
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')
} 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: [] }
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)
}
}
} catch (mapErr) {
console.warn('loadOrderHistory: ak_users mapping failed', mapErr)
}
}
]
if (queryUid && queryUid !== '') {
dtRes = await supa.from('ml_delivery_tasks')
.select('order_id,status')
.eq('driver_id', queryUid)
.gte('status', 2)
.order('created_at', { ascending: false })
.limit(200)
.execute()
} else {
dtRes = { data: [] }
}
console.log('loadOrderHistory: delivery_tasks dtRes=', dtRes)
} catch (err) {
console.error('loadOrderHistory: delivery_tasks query failed', err)
dtRes = { data: [] }
}
const orderIds = (dtRes && Array.isArray(dtRes.data)) ? dtRes.data.map((r: any) => r.order_id) : []
// 如果没有通过 delivery_tasks 找到订单,改为直接读取最近完成/已取货订单(兼容测试环境)
let ordersRes
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()
}
} catch (err) {
console.error('loadOrderHistory: ml_orders query 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
})
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.checkForNewCompletedOrder()

View File

@@ -200,142 +200,134 @@
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import type { DeliveryDriverType, DeliveryTaskType, ApiResponseType } from '@/types/mall-types'
import supa, { supaReady } from '@/components/supadb/aksupainstance.uts'
import { getCurrentUserId } from '@/utils/store.uts'
import type { DeliveryDriverType, DeliveryTaskType } from '@/types/mall-types'
/* ----------------- 返回按钮 ----------------- */
function backToIndex() {
uni.navigateBack({ url: '/pages/mall/delivery/index' })
}
/* ----------------- 数据 ----------------- */
const driverInfo = ref({
id: '',
real_name: '配送员',
avatar_url: '',
rating: 4.9,
total_orders: 368,
work_status: 1
})
const workStatus = ref(1) // 1 工作中 0 休息中
const currentLocation = ref('朝阳区建国门附近') // 默认位置
/* ----------------- 数据 & 状态 ----------------- */
const driverInfo = ref({ id: '', real_name: '配送员', avatar_url: '', rating: 4.9, total_orders: 0, work_status: 1 })
const workStatus = ref(1)
const currentLocation = ref('定位中...')
const taskCounts = ref({ total: 0, pending: 0, ongoing: 0, completed: 0 })
const todayStats = ref({
deliveries: 12,
earnings: '156.80',
distance: 45.6,
efficiency: 96.5
})
const todayStats = ref({ deliveries: 0, earnings: '0.00', distance: 0, efficiency: 0 })
const currentTask = ref<DeliveryTaskType | null>(null)
const recentTasks = ref<DeliveryTaskType[]>([])
const weeklyEarnings = ref([
{ day: '周一', amount: 120 },
{ day: '周二', amount: 156 },
{ day: '周三', amount: 189 },
{ day: '周四', amount: 145 },
{ day: '周五', amount: 203 },
{ day: '周六', amount: 245 },
{ day: '周日', amount: 198 }
])
const weeklyEarnings = ref([{ day: '周一', amount: 0 }, { day: '周二', amount: 0 }, { day: '周三', amount: 0 }, { day: '周四', amount: 0 }, { day: '周五', amount: 0 }, { day: '周六', amount: 0 }, { day: '周日', amount: 0 }])
const maxEarnings = computed(() => Math.max(...weeklyEarnings.value.map(i => i.amount)))
const isLoading = ref(false)
/* ----------------- 生命周期 ----------------- */
onMounted(() => {
loadDriverInfo()
loadTaskCounts()
loadCurrentTask()
loadRecentTasks()
onMounted(async () => {
await loadDriverInfo()
await Promise.all([loadTaskCounts(), loadTodayStats(), loadCurrentTask(), loadRecentTasks()])
})
/* ----------------- 方法 ----------------- */
function loadDriverInfo() {
driverInfo.value = {
id: 'driver001',
user_id: 'user001',
real_name: '李师傅',
id_card: '110101199001011234',
driver_license: 'C1',
vehicle_type: 1,
vehicle_number: '京A12345',
work_status: 1,
current_location: { lat: 39.9042, lng: 116.4074 },
service_areas: ['朝阳区', '东城区'],
rating: 4.9,
total_orders: 368,
auth_status: 1,
created_at: '2024-01-01',
updated_at: '2024-12-01'
}
}
function loadTaskCounts() {
taskCounts.value = { total: 25, pending: 3, ongoing: 1, completed: 21 }
}
function loadCurrentTask() {
currentTask.value = {
id: 'task001',
order_id: 'order001',
driver_id: 'driver001',
pickup_address: { address: '朝阳区建国门大街1号', lat: 39.9042, lng: 116.4074 },
delivery_address: { address: '朝阳区CBD核心区2号', lat: 39.9142, lng: 116.4174 },
distance: 2.5,
estimated_time: 15,
delivery_fee: 8.0,
status: 2,
pickup_time: null,
delivered_time: null,
delivery_code: 'DEL123',
remark: '联系电话: 13888888888',
created_at: '2024-12-01 14:30:00',
updated_at: '2024-12-01 14:30:00'
}
}
function loadRecentTasks() {
recentTasks.value = [
{
id: 'task002',
order_id: 'order002',
driver_id: 'driver001',
pickup_address: { address: '朝阳区国贸中心' },
delivery_address: { address: '朝阳区望京SOHO' },
distance: 12.5,
estimated_time: 35,
delivery_fee: 12.0,
status: 4,
pickup_time: '2024-12-01 13:00:00',
delivered_time: '2024-12-01 13:30:00',
delivery_code: 'DEL122',
remark: '',
created_at: '2024-12-01 12:45:00',
updated_at: '2024-12-01 13:30:00'
},
{
id: 'task003',
order_id: 'order003',
driver_id: 'driver001',
pickup_address: { address: '朝阳区三里屯太古里' },
delivery_address: { address: '朝阳区工体北路' },
distance: 1.8,
estimated_time: 8,
delivery_fee: 6.0,
status: 4,
pickup_time: '2024-12-01 11:30:00',
delivered_time: '2024-12-01 11:45:00',
delivery_code: 'DEL121',
remark: '',
created_at: '2024-12-01 11:15:00',
updated_at: '2024-12-01 11:45:00'
/* ----------------- 后端数据加载(使用 supa ----------------- */
async function 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
let res = await supa.from('ml_delivery_drivers').select('*').eq('user_id', userId).limit(1).execute()
if (!(res && Array.isArray(res.data) && res.data.length > 0)) {
const akRes = await supa.from('ak_users').select('id').eq('auth_id', userId).limit(1).execute()
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()
}
]
if (res && Array.isArray(res.data) && res.data.length > 0) {
driverInfo.value = Object.assign(driverInfo.value, res.data[0])
workStatus.value = driverInfo.value.work_status ?? workStatus.value
}
} catch (e) {
console.error('loadDriverInfo error', e)
}
}
async function loadTaskCounts() {
try {
const ready = await Promise.race([supaReady, new Promise(resolve => setTimeout(() => resolve(false), 1500))])
if (!ready) console.warn('supaReady timeout/failed in loadTaskCounts - proceeding')
const driverId = driverInfo.value.id || getCurrentUserId()
if (!driverId) return
const res = await supa.from('ml_delivery_tasks').select('id,status').eq('driver_id', driverId).execute()
if (res && Array.isArray(res.data)) {
const rows = res.data as Array<any>
const total = rows.length
const pending = rows.filter(r => Number(r.status) === 1).length
const ongoing = rows.filter(r => Number(r.status) > 1 && Number(r.status) < 5).length
const completed = rows.filter(r => Number(r.status) >= 5).length
taskCounts.value = { total, pending, ongoing, completed }
}
} catch (e) {
console.error('loadTaskCounts error', e)
}
}
async function 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 = driverInfo.value.id || getCurrentUserId()
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('delivery_fee,distance,status,created_at').eq('driver_id', driverId).gte('created_at', start.toISOString()).lte('created_at', end.toISOString()).execute()
if (res && Array.isArray(res.data)) {
const rows = res.data as Array<any>
const deliveries = rows.filter(r => Number(r.status) >= 5).length
const earnings = rows.reduce((s, r) => s + (Number(r.delivery_fee) || 0), 0)
const distance = rows.reduce((s, r) => s + (Number(r.distance) || 0), 0)
todayStats.value = { deliveries, earnings: earnings.toFixed(2), distance: Number(distance.toFixed(2)), efficiency: deliveries === 0 ? 0 : Math.round((deliveries / Math.max(1, rows.length)) * 10000)/100 }
}
} catch (e) {
console.error('loadTodayStats error', e)
}
}
async function 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 = driverInfo.value.id || getCurrentUserId()
if (!driverId) { currentTask.value = 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()
if (res && Array.isArray(res.data) && res.data.length > 0) {
currentTask.value = res.data[0]
} else {
currentTask.value = null
}
} catch (e) {
console.error('loadCurrentTask error', e)
}
}
async function loadRecentTasks() {
try {
const ready = await Promise.race([supaReady, new Promise(resolve => setTimeout(() => resolve(false), 1500))])
if (!ready) console.warn('supaReady timeout/failed in loadRecentTasks - proceeding')
const driverId = driverInfo.value.id || getCurrentUserId()
if (!driverId) { recentTasks.value = []; return }
const res = await supa.from('ml_delivery_tasks').select('*').eq('driver_id', driverId).order('created_at', { ascending: false }).range(0, 9).execute()
if (res && Array.isArray(res.data)) {
recentTasks.value = res.data as Array<any>
} else {
recentTasks.value = []
}
} catch (e) {
console.error('loadRecentTasks error', e)
recentTasks.value = []
}
}
/* ----------------- 小工具 & 交互 ----------------- */
function getWorkStatus(): string {
const m: Record<number, string> = { 0: '休息中', 1: '工作中', 2: '忙碌中' }
return m[driverInfo.value.work_status] || '未知状态'
@@ -347,10 +339,11 @@ function getTaskStatusText(status: number): string {
}
function getAddressText(address: UTSJSONObject): string {
return (address['address'] as string) || '地址信息'
return (address && (address['address'] as string)) || (address && (address['detail'] as string)) || '地址信息'
}
function formatTime(dateStr: string): string {
if (!dateStr) return ''
const diff = Date.now() - new Date(dateStr).getTime()
const hours = Math.floor(diff / 36e5)
if (hours < 1) return '刚刚'
@@ -358,58 +351,27 @@ function formatTime(dateStr: string): string {
return `${Math.floor(hours / 24)}天前`
}
/* ----------------- 交互 ----------------- */
function toggleWorkStatus() {
workStatus.value = workStatus.value === 1 ? 0 : 1
driverInfo.value.work_status = workStatus.value
uni.showToast({
title: workStatus.value === 1 ? '已开始工作' : '已停止工作',
icon: 'success'
})
uni.showToast({ title: workStatus.value === 1 ? '已开始工作' : '已停止工作', icon: 'success' })
}
// --- 新增:重新定位逻辑 ---
function showRelocateConfirm() {
uni.showModal({
title: '重新定位',
content: '确定要更新当前位置吗?',
confirmText: '立即定位',
success: (res) => {
if (res.confirm) {
relocate()
}
}
})
uni.showModal({ title: '重新定位', content: '确定要更新当前位置吗?', confirmText: '立即定位', success: (res) => { if (res.confirm) relocate() } })
}
function relocate() {
uni.showLoading({
title: '获取位置中...',
mask: true
})
// 模拟定位耗时
uni.showLoading({ title: '获取位置中...', mask: true })
setTimeout(() => {
uni.hideLoading()
// 模拟定位成功更新位置实际项目中应调用API获取真实位置
currentLocation.value = '朝阳区建国门外大街附近'
uni.showToast({
title: '定位成功',
icon: 'success',
duration: 1500
})
uni.showToast({ title: '定位成功', icon: 'success', duration: 1500 })
}, 1200)
}
function contactCustomer() {
uni.showActionSheet({
itemList: ['拨打电话', '发送短信'],
success: res => {
if (res.tapIndex === 0) uni.makePhoneCall({ phoneNumber: '13888888888' })
}
})
uni.showActionSheet({ itemList: ['拨打电话', '发送短信'], success: res => { if (res.tapIndex === 0) uni.makePhoneCall({ phoneNumber: '13888888888' }) } })
}
function viewTaskDetail(taskId = '') {
@@ -417,31 +379,14 @@ function viewTaskDetail(taskId = '') {
uni.navigateTo({ url: `/pages/mall/delivery/task-detail?id=${id}` })
}
/* ----------------- 导航 ----------------- */
function editProfile() {
uni.navigateTo({ url: '/pages/mall/delivery/profile-edit' })
}
function goToSettings() {
uni.navigateTo({ url: '/pages/mall/delivery/settings' })
}
function goToTasks(type: string) {
uni.navigateTo({ url: `/pages/mall/delivery/tasks?type=${type}` })
}
function goToEarnings() {
uni.navigateTo({ url: '/pages/mall/delivery/earnings' })
}
function goToVehicle() {
uni.navigateTo({ url: '/pages/mall/delivery/vehicle' })
}
function goToRatings() {
uni.navigateTo({ url: '/pages/mall/delivery/ratings' })
}
function goToHelp() {
uni.navigateTo({ url: '/pages/mall/delivery/help-center' })
}
function goToFeedback() {
uni.navigateTo({ url: '/pages/mall/delivery/feedback' })
}
function editProfile() { uni.navigateTo({ url: '/pages/mall/delivery/profile-edit' }) }
function goToSettings() { uni.navigateTo({ url: '/pages/mall/delivery/settings' }) }
function goToTasks(type: string) { uni.navigateTo({ url: `/pages/mall/delivery/tasks?type=${type}` }) }
function goToEarnings() { uni.navigateTo({ url: '/pages/mall/delivery/earnings' }) }
function goToVehicle() { uni.navigateTo({ url: '/pages/mall/delivery/vehicle' }) }
function goToRatings() { uni.navigateTo({ url: '/pages/mall/delivery/ratings' }) }
function goToHelp() { uni.navigateTo({ url: '/pages/mall/delivery/help-center' }) }
function goToFeedback() { uni.navigateTo({ url: '/pages/mall/delivery/feedback' }) }
</script>
<style scoped>

View File

@@ -1,271 +0,0 @@
- 创建正确的触发器和函数
CREATE OR REPLACE FUNCTION ensure_ak_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.ak_users (user_id, email, nickname)
VALUES (NEW.id, NEW.email, COALESCE(NEW.email, '新用户'))
ON CONFLICT (user_id) DO UPDATE SET
email = EXCLUDED.email,
nickname = EXCLUDED.nickname;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_ensure_ak_user
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION ensure_ak_user();
-- =============================================
-- 0. 创建 ak_users 表(你的核心用户表)
-- =============================================
CREATE TABLE IF NOT EXISTS public.ak_users (
id BIGSERIAL PRIMARY KEY,
user_id UUID UNIQUE NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
email VARCHAR(255) UNIQUE,
phone VARCHAR(20),
nickname VARCHAR(100),
avatar_url TEXT,
gender CHAR(1) DEFAULT 'U',
birthday DATE,
address TEXT,
user_type INTEGER DEFAULT 1, -- 1:普通用户, 2:配送员
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- =============================================
-- 1. 配送员资料表 (delivery_drivers)
-- =============================================
CREATE TABLE IF NOT EXISTS public.delivery_drivers (
id BIGSERIAL PRIMARY KEY,
user_id UUID UNIQUE NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
real_name VARCHAR(100) NOT NULL,
id_card VARCHAR(18),
driver_license VARCHAR(50),
vehicle_type INTEGER DEFAULT 1, -- 1:电动车, 2:摩托车, 3:汽车
vehicle_number VARCHAR(20),
work_status INTEGER DEFAULT 1, -- 1:空闲, 2:忙碌, 3:休息
current_location JSONB, -- {lat: 22.5431, lng: 114.0579, address: "深圳市南山区"}
service_areas TEXT[], -- 服务区域数组 ["南山", "福田", "罗湖"]
rating DECIMAL(3,2) DEFAULT 5.00, -- 平均评分
total_orders INTEGER DEFAULT 0, -- 总完成订单数
auth_status INTEGER DEFAULT 1, -- 1:未认证, 2:审核中, 3:已认证
online_status BOOLEAN DEFAULT FALSE, -- 是否在线
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- =============================================
-- 2. 订单表 (orders)
-- =============================================
CREATE TABLE IF NOT EXISTS public.orders (
id BIGSERIAL PRIMARY KEY,
order_no VARCHAR(50) UNIQUE NOT NULL, -- 订单号,如 D202501081234
user_id UUID NOT NULL REFERENCES auth.users(id), -- 下单用户
status INTEGER DEFAULT 1, -- 1:待支付, 2:待接单, 3:配送中, 4:已完成, 5:已取消
total_amount DECIMAL(10,2) NOT NULL, -- 订单总金额
delivery_fee DECIMAL(10,2) NOT NULL, -- 配送费
payable_amount DECIMAL(10,2) NOT NULL, -- 实付金额
pickup_address JSONB NOT NULL, -- 取货地址
delivery_address JSONB NOT NULL, -- 配送地址
pickup_contact JSONB NOT NULL, -- 取货联系人
delivery_contact JSONB NOT NULL, -- 配送联系人
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- =============================================
-- 3. 配送任务表 (delivery_tasks)
-- =============================================
CREATE TABLE IF NOT EXISTS public.delivery_tasks (
id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(id),
driver_id BIGINT NOT NULL REFERENCES delivery_drivers(id),
task_status INTEGER DEFAULT 1, -- 1:待接取, 2:已接取, 3:取货中, 4:已取货, 5:配送中, 6:已完成
pickup_address JSONB NOT NULL, -- 取货地址 {detail: "", area: "", lat: 0, lng: 0}
delivery_address JSONB NOT NULL, -- 配送地址 {detail: "", area: "", lat: 0, lng: 0}
pickup_contact JSONB NOT NULL, -- 取货联系人 {name: "", phone: ""}
delivery_contact JSONB NOT NULL, -- 配送联系人 {name: "", phone: ""}
delivery_fee DECIMAL(10,2) NOT NULL, -- 配送费
distance DECIMAL(8,2) DEFAULT 0, -- 配送距离(km)
estimated_time INTEGER DEFAULT 0, -- 预计配送时间(分钟)
pickup_time TIMESTAMP WITH TIME ZONE, -- 实际取货时间
delivery_time TIMESTAMP WITH TIME ZONE, -- 实际配送时间
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- =============================================
-- 4. 今日统计表 (daily_stats)
-- =============================================
CREATE TABLE IF NOT EXISTS public.daily_stats (
id BIGSERIAL PRIMARY KEY,
driver_id BIGINT NOT NULL REFERENCES delivery_drivers(id),
stat_date DATE NOT NULL,
completed_orders INTEGER DEFAULT 0,
total_earning DECIMAL(10,2) DEFAULT 0.00,
total_distance DECIMAL(8,2) DEFAULT 0.00,
avg_rating DECIMAL(3,2) DEFAULT 5.00,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(driver_id, stat_date) -- 每个司机每天只有一条统计记录
);
-- =============================================
-- 5. 启用 RLS
-- =============================================
ALTER TABLE public.ak_users ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.delivery_drivers ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.delivery_tasks ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.daily_stats ENABLE ROW LEVEL SECURITY;
-- =============================================
-- 6. 创建 RLS 策略
-- =============================================
CREATE POLICY "Users can view own profile" ON public.ak_users
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Drivers can view own profile" ON public.delivery_drivers
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can view own orders" ON public.orders
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Drivers can view own tasks" ON public.delivery_tasks
FOR SELECT USING (EXISTS (
SELECT 1 FROM delivery_drivers dd
WHERE dd.id = delivery_tasks.driver_id AND dd.user_id = auth.uid()
));
CREATE POLICY "Drivers can view own stats" ON public.daily_stats
FOR SELECT USING (EXISTS (
SELECT 1 FROM delivery_drivers dd
WHERE dd.id = daily_stats.driver_id AND dd.user_id = auth.uid()
));
-- =============================================
-- 7. 创建索引
-- =============================================
CREATE INDEX IF NOT EXISTS idx_ak_users_user_id ON public.ak_users(user_id);
CREATE INDEX IF NOT EXISTS idx_ak_users_email ON public.ak_users(email);
CREATE INDEX IF NOT EXISTS idx_delivery_drivers_user_id ON public.delivery_drivers(user_id);
CREATE INDEX IF NOT EXISTS idx_orders_user_id ON public.orders(user_id);
CREATE INDEX IF NOT EXISTS idx_delivery_tasks_driver_id ON public.delivery_tasks(driver_id);
CREATE INDEX IF NOT EXISTS idx_daily_stats_driver_date ON public.daily_stats(driver_id, stat_date);
-- =============================================
-- 8. 创建触发器:当新用户注册时自动创建 ak_users 记录
-- =============================================
-- 先删除已存在的(防止重复创建错误)
DROP TRIGGER IF EXISTS trigger_ensure_ak_user ON auth.users;
DROP FUNCTION IF EXISTS ensure_ak_user();
-- 创建新的触发器和函数
CREATE OR REPLACE FUNCTION ensure_ak_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.ak_users (user_id, email, nickname, user_type)
VALUES (NEW.id, NEW.email, COALESCE(NEW.email, '新用户'), 1)
ON CONFLICT (user_id) DO UPDATE SET
email = EXCLUDED.email,
nickname = EXCLUDED.nickname;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_ensure_ak_user
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION ensure_ak_user();
-- =============================================
-- 9. 创建测试数据
-- =============================================
DO $$
DECLARE
test_user_id UUID;
test_driver_id UUID;
test_driver_record_id BIGINT;
test_order_id BIGINT;
BEGIN
-- 创建配送员用户
INSERT INTO auth.users (
instance_id, id, aud, role, email, encrypted_password,
email_confirmed_at, created_at, updated_at
) VALUES (
'00000000-0000-0000-0000-000000000000',
gen_random_uuid(),
'authenticated',
'authenticated',
'123@example.com',
crypt('Test123456', gen_salt('bf')),
NOW(),
NOW(),
NOW()
) RETURNING id INTO test_driver_id;
-- 创建配送员资料
INSERT INTO public.delivery_drivers (
user_id, real_name, vehicle_number, online_status, rating, total_orders
) VALUES (
test_driver_id,
'张师傅',
'粤B12345',
TRUE,
4.8,
1250
) RETURNING id INTO test_driver_record_id;
-- 创建今日统计数据
INSERT INTO public.daily_stats (
driver_id, stat_date, completed_orders, total_earning, total_distance, avg_rating
) VALUES (
test_driver_record_id,
CURRENT_DATE,
8,
245.60,
45.0,
4.9
);
-- 创建测试订单
INSERT INTO public.orders (
order_no, user_id, status, total_amount, delivery_fee, payable_amount,
pickup_address, delivery_address, pickup_contact, delivery_contact
) VALUES (
'D202501081234',
test_driver_id, -- 为了测试,暂时让司机自己下单
2, -- 已接单
125.50,
8.50,
134.00,
'{"detail": "华强北商业区华强电子世界2楼A205", "area": "华强北", "lat": 22.5431, "lng": 114.0579}',
'{"detail": "南山区科技园深南大道9999号", "area": "科技园", "lat": 22.5350, "lng": 113.9490}',
'{"name": "商家联系人", "phone": "138****5678"}',
'{"name": "张先生", "phone": "139****1234"}'
) RETURNING id INTO test_order_id;
-- 创建配送任务
INSERT INTO public.delivery_tasks (
order_id, driver_id, task_status, pickup_address, delivery_address,
pickup_contact, delivery_contact, delivery_fee, distance, estimated_time
) VALUES (
test_order_id,
test_driver_record_id,
2, -- 已接取
'{"detail": "华强北商业区华强电子世界2楼A205", "area": "华强北", "lat": 22.5431, "lng": 114.0579}',
'{"detail": "南山区科技园深南大道9999号", "area": "科技园", "lat": 22.5350, "lng": 113.9490}',
'{"name": "商家联系人", "phone": "138****5678"}',
'{"name": "张先生", "phone": "139****1234"}',
8.50,
12.5,
35
);
RAISE NOTICE '✅ 配送系统数据库创建完成!';
END $$;

View File

@@ -1,183 +0,0 @@
# 🚚 配送端模块项目结构说明(`/pages/mall/delivery/`
> 本模块为配送员使用的移动端应用核心功能区,涵盖个人中心、任务管理、收入统计、车辆管理等核心功能。
---
## 📁 目录结构概览
```
delivery/
├── index.uvue # 配送端首页(工作台)
├── profile.uvue # 个人中心
├── profile-edit.uvue # 编辑个人信息
├── settings.uvue # 设置页面
├── tasks.uvue # 配送任务列表(按状态筛选)
├── task-detail.uvue # 单个任务详情页
├── order-history.uvue # 历史订单列表
├── order-detail.uvue # 订单详情页(从历史或当前跳转)
├── earnings.uvue # 收入明细
├── ratings.uvue # 评价记录
├── vehicle.uvue # 车辆管理主页
├── vehicle-add.uvue # 添加车辆
├── vehicle-edit.uvue # 编辑车辆信息
```
---
## 🧭 各页面功能说明
### 1. `index.uvue` —— **配送端首页 / 工作台**
- ✅ 核心入口页面
- 📍 展示当前任务、今日数据、快捷操作入口
- 🔄 可跳转到“个人中心”、“任务列表”、“收入明细”等
---
### 2. `profile.uvue` —— **个人中心**
- 👤 显示配送员基本信息(头像、姓名、评分、总单数)
- 📊 展示今日配送数据(完成单数、收入、里程、准时率)
- 📈 收入统计图表最近7天
- 🚗 功能菜单:收入明细、车辆管理、评价记录、帮助中心、意见反馈
---
### 3. `profile-edit.uvue` —— **编辑个人信息**
- 🖋️ 修改头像、姓名、身份证号、驾驶证、车辆信息、服务区域等
- 📱 界面包含表单输入 + 保存按钮
- ⬅️ 左上角返回按钮(箭头+文字垂直排列)
- 💾 数据本地模拟或调用API更新
---
### 4. `settings.uvue` —— **设置页面**
- ⚙️ 通用设置项(如通知、隐私、退出登录等)
- 🔐 安全相关设置(修改密码、绑定手机等)
- 📲 通常由 `profile.uvue` 中的“⚙️”图标进入
---
### 5. `tasks.uvue` —— **配送任务列表**
- 📋 按状态分类展示任务:
- 全部任务
- 待接单
- 配送中
- 已完成
- 🚀 点击任一任务 → 跳转至 `task-detail.uvue`
- 📈 页面顶部有“工作状态”切换开关(工作中/休息中)
---
### 6. `task-detail.uvue` —— **任务详情页**
- 📍 显示取货地址、送达地址、距离、预计时间
- 📞 “联系客户”按钮
- 📝 查看任务详情(可选)
- ✅ 适用于“当前任务”或“待接单”的操作场景
---
### 7. `order-history.uvue` —— **历史订单列表**
- 📜 展示已完成、已接受、配送中的历史订单
- 📌 包含订单号、状态、取送货地址、配送费、距离、时间
- 🔍 点击“查看详情” → 跳转至 `order-detail.uvue`,并携带参数 `?from=history`
- 📅 支持查看“已完成”的订单(仅显示“联系客服”按钮)
---
### 8. `order-detail.uvue` —— **订单详情页(多来源)**
- 🔄 从 `tasks.uvue``order-history.uvue` 进入
- 🎯 **关键逻辑**
- 若来自历史订单(`from=history`)且状态为“已完成” → 只显示“联系客服”
- 若来自历史订单且状态为“进行中” → 显示“接受/拒绝/导航/完成”等操作按钮
- 若非历史来源 → 显示完整操作按钮
- 📞 包含联系顾客、联系商家、联系客服三个联系方式
---
### 9. `earnings.uvue` —— **收入明细**
- 💰 展示总收入、用户打赏、商家打赏、总订单数
- 📊 按订单聚合的收入数据列表
- 📈 图表展示最近7天收入趋势
- 加载更多按钮
---
### 10. `ratings.uvue` —— **评价记录**
- ⭐ 展示用户对配送员的评价
- 📝 包含评分、评价内容、订单号、时间
- 📊 统计平均分、好评率等
---
### 11. `vehicle.uvue` —— **车辆管理主页**
- 🚗 列出当前绑定的所有车辆
- “添加车辆”按钮
- 🖋️ 点击车辆 → 跳转至 `vehicle-edit.uvue`
- 🗑️ 支持删除、设为主用车等操作
---
### 12. `vehicle-add.uvue` —— **添加车辆**
- 📝 表单填写:车牌号、车型、行驶证照片、车辆类型等
- ✅ 提交后绑定到当前账户
- ⬅️ 返回车辆管理页
---
### 13. `vehicle-edit.uvue` —— **编辑车辆信息**
- 🖋️ 修改已有车辆信息(车牌、车型、照片等)
- 📸 支持重新上传行驶证照片
- ✅ 保存后更新车辆信息
---
## 🔄 页面跳转关系图(简化版)
```
index.uvue
├──→ profile.uvue → profile-edit.uvue
├──→ tasks.uvue → task-detail.uvue
├──→ order-history.uvue → order-detail.uvue (from=history)
├──→ earnings.uvue
├──→ ratings.uvue
└──→ vehicle.uvue → vehicle-add.uvue / vehicle-edit.uvue
```
---
## 🎯 设计原则
-**一致性**:所有页面使用相同导航栏样式(左上角返回按钮 + 居中标题)
-**响应式**:适配 H5、APP、小程序等平台
-**状态驱动**:根据订单状态动态显示不同操作按钮
-**用户体验优先**:历史订单跳转后只显示必要操作,避免冗余按钮
---
## 📌 建议优化点
| 问题 | 建议 |
|------|------|
| 页面间参数传递 | 使用 `uni.navigateTo({ url: '?param=value' })` 并在 `onLoad` 中接收 |
| UTS 语法兼容性 | 如遇问题,可临时改用标准 TS 语法调试 |
---
## ✅ 总结
这个 `delivery` 模块是一个完整的配送员工作系统,涵盖了:
- **个人管理**(资料、设置)
- **任务处理**(接单、配送、完成)
- **数据统计**(收入、评价、历史)
- **车辆管理**
结构清晰、功能完整,适合用于实际配送平台开发。
---