Merge branch 'ysj-delivery' into main

This commit is contained in:
comlibmb
2026-02-02 21:41:53 +08:00
26 changed files with 3402 additions and 547 deletions

View File

@@ -1,11 +1,14 @@
// Supabase 配置
// 内网环境 - 本地部署的 Supabase
// IP: 192.168.1.63
// IP: 192.168.1.62
// Kong HTTP Port: 8000
//export const SUPA_URL: string = 'http://192.168.1.62:18000'
//export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
export const SUPA_URL: string = 'http://192.168.1.63:18000'
export const SUPA_KEY: string = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJyb2xlIjogImFub24iLCAiaXNzIjogInN1cGFiYXNlIiwgImlhdCI6IDE3Njk4NDczMzQsICJleHAiOiAyMDg1MjA3MzM0fQ.js-2CS5_cUmf4iVv8aCmmx9iyFsQvLNDbt8YYOngeLU'
// WebSocket 实时连接(内网使用 ws:// 而非 wss://
//export const WS_URL: string = 'ws://192.168.1.62:18000/realtime/v1/websocket'
export const WS_URL: string = 'ws://192.168.1.63:18000/realtime/v1/websocket'
// 备用配置(已注释,如需切换可取消注释)

View File

@@ -0,0 +1,41 @@
**Supabase 会话恢复说明supa session recovery**
- **目的**: 说明为什么刷新后 `session` / `user` 变为 `null`,以及我在项目中做了哪些修改来改善会话恢复。
- **文件变更**:
- **`components/supadb/aksupainstance.uts`**: 统一 Supabase 实例导出,新增 `supaReady` 初始化流程。在初始化时尝试从持久化 token`AkReq` 存储)读取 `access_token` / `refresh_token`,将 refresh token 注入到 `supa.session` 并调用 `supa.refreshSession()` 以恢复会话;保留 `ensureSupabaseReady()` 作为兼容接口。
- **`utils/store.uts`**: 将 `import` 改为 `import supa, { supaReady }`,并在获取 session 前 `await supaReady`,确保恢复逻辑已经运行完毕。
- **`utils/sapi.uts`**、**`pages/sense/senseDataService.uts`** 及若干页面文件: 替换对旧 `ensureSupabaseReady` 的调用,改为 `await supaReady`,并使用默认导出的 `supa` 实例。
- **问题根本原因**:
1. 系统原先将会话信息保持在 `AkSupa.session` / `AkSupa.user`(内存)中,页面刷新或应用重启会清空内存,导致 `supa.getSession()` 返回 `null`
2. 虽然 `AkReq.setToken``access_token` / `refresh_token` 持久化到 `uni` storage`AkSupa` 在启动时没有读取这些持久化 token 并执行恢复/刷新流程(或未将 refresh token 注入到 `AkSupa`),因此无法重建会话。
3. 若 refresh token 过期或没有被正确持久化,也会导致恢复失败。
- **我做的修改说明(要点)**:
-`aksupainstance.uts` 添加 `supaReady`:这是一个 Promise模块初始化时会尝试读取 `AkReq.getRefreshToken()` / `AkReq.getToken()`;若找到 refresh token则把其临时赋到 `supa.session`,再调用 `supa.refreshSession()` 以更新内存中的 session 和 user。
- 在所有依赖会话的模块中,在执行数据库请求前 `await supaReady`,确保恢复尝试已完成,避免 race condition页面刷新后立即调用 supa API 导致 401/空 session
- 保留向后兼容接口 `ensureSupabaseReady()`(内部直接返回 `supaReady`)。
- **如何验证(开发环境)**:
1. 登录并确认本地存储中有 token通过控制台或在 uni 环境运行):
- `uni.getStorageSync('akreq_access_token')`
- `uni.getStorageSync('akreq_refresh_token')`
2. 刷新页面或重启应用,打开控制台查找初始化日志或错误(`Supabase instance init failed` 等)。
3. 在页面中(例如 `pages/mall/delivery/index.uvue`查看已有的调试输出search `supa session=` 的 console 日志,应显示非 null session若 refresh 成功)。
4. 若恢复失败,请检查后端 refresh 接口是否返回 200以及 refresh token 是否已过期。
- **后续建议(可选)**:
-`AkSupa.signIn` / `refreshSession` 成功时,将完整 session或至少 `refresh_token`)写入 `uni.setStorageSync`(持久化),并在 `signOut` 时清除,这样可以进一步减少恢复失败的情况。
-`supa.session` 的 JSON 快照也写入 storage作为额外冗余并在 init 时尝试直接恢复(注意安全和敏感信息保护)。
- 增加更详细的日志(成功/失败原因),并在 UI 层对“登录过期”做更友好的提示或自动跳转到登录页。
- **注意事项**:
- 切勿在代码中硬编码匿名 key 或生产密钥;请在 `ak/config.uts` 中正确配置 `SUPA_URL``SUPA_KEY`
- refresh token 本身也是敏感凭证,应妥善保管;如需长期保持登录,建议使用 refresh 流程并合理设置过期与刷新策略。
如果你需要,我可以继续:
-`AkSupa` 中实现“登录后持久化 session JSON”的补丁并在 `signOut` 时清理;
- 或添加更详细的调试输出帮助定位某次具体恢复失败的 HTTP 请求和响应。
文件路径components/supadb/SESSION_RECOVERY.md

View File

@@ -1,17 +1,52 @@
// /components/supadb/aksupainstance.uts
import AkSupa from './aksupa.uts'
import { SUPA_URL, SUPA_KEY } from '@/ak/config.uts'
import { AkReq } from '@/uni_modules/ak-req/index.uts'
// 创建 AkSupa 实例(在全局复用)
const supa = new AkSupa(SUPA_URL, SUPA_KEY)
// Do not perform hard-coded auto sign-in during page preload (development mode may preload pages).
// Instead, mark supa as ready if an existing session is present; otherwise defer sign-in to explicit user action.
/**
* supaReady: 初始化时尝试从持久化 token 恢复会话。
* - 若内存中已有 session 则直接返回 true
* - 否则尝试读取 AkReq 中持久化的 refresh token 并调用 `refreshSession()` 恢复
*/
const supaReady: Promise<boolean> = (async () => {
try {
const sess = supa.getSession()
if (sess != null && sess.session != null) {
const cur = supa.getSession()
if (cur && cur.session != null && cur.session.access_token) {
return true
}
// 从持久化 storage 读取 token
const access = AkReq.getToken()
const refresh = AkReq.getRefreshToken()
const expiresAt = AkReq.getExpiresAt() ?? 0
if (refresh && refresh !== '') {
// 临时注入 session以便 refreshSession 使用 refresh_token
try {
supa.session = {
access_token: access ?? '',
refresh_token: refresh,
expires_at: expiresAt,
user: null,
token_type: '',
expires_in: 0,
raw: new UTSJSONObject({})
}
} catch (e) {
// 在某些环境 UTSJSONObject 构造可能不可用,忽略并继续
try { (supa as any).session = { access_token: access ?? '', refresh_token: refresh, expires_at: expiresAt } } catch (_) {}
}
const ok = await supa.refreshSession()
if (ok) return true
// 刷新失败,清空内存 session
try { supa.session = null; supa.user = null } catch (_) {}
}
return true
} catch (err) {
console.error('Supabase instance init failed', err)
@@ -19,10 +54,8 @@ const supaReady: Promise<boolean> = (async () => {
}
})()
// 兼容具名导出(部分代码使用 import { supabase, ensureSupabaseReady }
export const supabase = supa
export async function ensureSupabaseReady(): Promise<boolean> {
// 向后兼容:保留 ensureSupabaseReady 接口
export async function ensureSupabaseReady() {
return await supaReady
}

View File

@@ -92,6 +92,43 @@ const config = {
nickname: '快递小哥2'
}
}
,
{
email: 'merchant.real@example.com',
password: 'Test123456!',
user_metadata: {
name: '测试商家',
role: 'merchant',
nickname: '测试商家'
}
},
{
email: 'zhang.san@example.com',
password: 'Test123456!',
user_metadata: {
name: '张三',
role: 'customer',
nickname: '张三'
}
},
{
email: 'li.si@example.com',
password: 'Test123456!',
user_metadata: {
name: '李四',
role: 'customer',
nickname: '李四'
}
},
{
email: 'wang.wu@example.com',
password: 'Test123456!',
user_metadata: {
name: '王五',
role: 'customer',
nickname: '王五'
}
}
]
};

1
package-lock.json generated
View File

@@ -4,6 +4,7 @@
"requires": true,
"packages": {
"": {
"name": "mall",
"dependencies": {
"echarts": "^6.0.0"
},

View File

@@ -324,6 +324,34 @@
"navigationStyle": "custom"
}
},
{
"path": "help-center",
"style": {
"navigationBarTitleText": "帮助中心",
"navigationStyle": "custom"
}
},
{
"path": "about",
"style": {
"navigationBarTitleText": "关于我们",
"navigationStyle": "custom"
}
},
{
"path": "feedback",
"style": {
"navigationBarTitleText": "意见反馈",
"navigationStyle": "custom"
}
},
{
"path": "test",
"style": {
"navigationBarTitleText": "test",
"navigationStyle": "custom"
}
},
{
"path": "settings",
"style": {

View File

@@ -0,0 +1,293 @@
<template>
<view class="about-container">
<!-- 顶部导航栏 -->
<view class="header-bar">
<!-- 左侧:返回按钮 -->
<view class="nav-left" @click="goBack">
<text class="nav-icon">←</text>
</view>
<!-- 中部:页面标题 -->
<text class="page-title">关于我们</text>
<!-- 右侧:留空 -->
<view class="nav-right"></view>
</view>
<!-- 主要内容 -->
<view class="content-wrapper">
<!-- Logo 区域 -->
<view class="logo-section">
<image src="/static/logo.png" class="app-logo" mode="aspectFit" />
</view>
<!-- 版本信息 -->
<view class="version-info">
<text class="version-text">版本: v2.1.5</text>
</view>
<!-- 应用介绍 -->
<view class="app-intro">
<text class="intro-title">快递配送助手</text>
<text class="intro-desc">专为配送员打造的一站式智能配送平台,提升效率,优化服务体验</text>
</view>
<!-- 功能亮点 -->
<view class="feature-section">
<text class="section-header">核心功能</text>
<view class="features">
<view class="feature-item">
<text class="feature-icon">📍</text>
<text class="feature-text">智能路线规划</text>
</view>
<view class="feature-item">
<text class="feature-icon">📱</text>
<text class="feature-text">实时订单管理</text>
</view>
<view class="feature-item">
<text class="feature-icon">💰</text>
<text class="feature-text">在线结算系统</text>
</view>
<view class="feature-item">
<text class="feature-icon">⭐</text>
<text class="feature-text">客户评价反馈</text>
</view>
</view>
</view>
<!-- 公司信息 -->
<view class="company-info">
<text class="section-header">公司信息</text>
<text class="info-item">公司名称: 快递科技有限公司</text>
<text class="info-item">成立时间: 2020年</text>
<text class="info-item">总部地点: 深圳市南山区科技园</text>
<text class="info-item">联系电话: 400-123-4567</text>
<text class="info-item">邮箱: support@delivery.com</text>
</view>
<!-- 更新日志 -->
<view class="update-log">
<text class="section-header">近期更新</text>
<view class="log-item">
<text class="log-version">v2.1.5 (2026-01-27)</text>
<text class="log-desc">• 修复了地图定位精度问题\n• 优化了任务分配算法\n• 提升了语音播报稳定性</text>
</view>
<view class="log-item">
<text class="log-version">v2.1.4 (2026-01-15)</text>
<text class="log-desc">• 新增智能语音助手功能\n• 改进了路线导航算法\n• 修复了部分UI显示异常</text>
</view>
<view class="log-item">
<text class="log-version">v2.1.3 (2025-12-28)</text>
<text class="log-desc">• 新增客户满意度评价\n• 优化了离线地图功能\n• 增强了数据同步机制</text>
</view>
</view>
<!-- 版权信息 -->
<view class="copyright">
<text class="copyright-text">© 2020-2026 快递科技有限公司 版权所有</text>
</view>
</view>
</view>
</template>
<script lang="uts">
export default {
methods: {
// 返回上一页
goBack() {
uni.navigateBack()
}
}
}
</script>
<style scoped>
.about-container {
background-color: #f8f9fa;
min-height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.header-bar {
background-color: #fff;
padding: 20rpx 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1rpx solid #e9ecef;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.nav-left {
display: flex;
align-items: center;
cursor: pointer;
padding: 10rpx;
border-radius: 8rpx;
transition: background-color 0.2s ease;
}
.nav-left:hover {
background-color: #f0f0f0;
}
.nav-left:active {
background-color: #e0e0e0;
}
.nav-icon {
font-size: 36rpx;
color: #333;
}
.page-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
text-align: center;
flex: 1; /* 让标题占据中间剩余空间 */
}
.nav-right {
width: 60rpx; /* 与左侧箭头区域宽度一致 */
height: 1rpx;
}
.content-wrapper {
flex: 1;
display: flex;
flex-direction: column;
padding: 40rpx 30rpx;
gap: 40rpx;
}
.logo-section {
display: flex;
justify-content: center;
margin-top: 20rpx;
}
.app-logo {
width: 150rpx;
height: 150rpx;
border-radius: 20rpx;
background-color: #e3f2fd;
padding: 20rpx;
}
.version-info {
text-align: center;
}
.version-text {
font-size: 28rpx;
color: #666;
font-weight: 500;
background-color: #e8f4fd;
padding: 10rpx 20rpx;
border-radius: 15rpx;
}
.app-intro {
text-align: center;
padding: 30rpx 0;
}
.intro-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 15rpx;
display: block;
}
.intro-desc {
font-size: 26rpx;
color: #666;
line-height: 1.6;
}
.feature-section, .company-info, .update-log {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.section-header {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
display: block;
}
.features {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.feature-item {
display: flex;
align-items: center;
}
.feature-icon {
font-size: 32rpx;
margin-right: 15rpx;
width: 40rpx;
}
.feature-text {
font-size: 26rpx;
color: #555;
}
.company-info .info-item {
font-size: 26rpx;
color: #666;
margin-bottom: 15rpx;
line-height: 1.5;
}
.update-log {
display: flex;
flex-direction: column;
gap: 25rpx;
}
.log-item {
border-bottom: 1rpx solid #f0f0f0;
padding-bottom: 20rpx;
}
.log-item:last-child {
border-bottom: none;
}
.log-version {
font-size: 26rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 10rpx;
}
.log-desc {
font-size: 24rpx;
color: #666;
line-height: 1.5;
white-space: pre-line;
}
.copyright {
text-align: center;
padding: 30rpx 0;
}
.copyright-text {
font-size: 22rpx;
color: #999;
}
</style>

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

@@ -0,0 +1,451 @@
<template>
<view class="feedback-container">
<!-- 顶部导航栏 -->
<view class="header-bar">
<view class="nav-left" @click="goBack">
<text class="nav-icon">←</text>
</view>
<text class="page-title">意见反馈</text>
<view class="nav-right"></view>
</view>
<!-- 表单区域 -->
<view class="form-wrapper">
<!-- 问题类型 -->
<view class="form-item">
<text class="label">问题类型</text>
<picker mode="selector" :range="feedbackTypes" @change="onTypeChange" :value="selectedTypeIndex">
<view class="picker-input">{{ feedbackTypes[selectedTypeIndex] }}</view>
</picker>
</view>
<!-- 标题 -->
<view class="form-item">
<text class="label">标题</text>
<input
type="text"
placeholder="请简要描述问题"
v-model="title"
class="input-field"
maxlength="50"
/>
</view>
<!-- 内容 -->
<view class="form-item">
<text class="label">详细内容</text>
<textarea
placeholder="请描述具体问题、发生时间、订单号等信息至少10字"
v-model="content"
class="textarea-field"
maxlength="500"
auto-height
/>
</view>
<!-- 上传截图(可选) -->
<view class="form-item">
<text class="label">上传截图(可选)</text>
<view class="upload-area" @click="chooseImage">
<text class="upload-icon">🖼️</text>
<text class="upload-text">点击上传截图</text>
<text class="upload-tip">支持 JPG/PNG最多3张</text>
</view>
<!-- 已上传图片预览 -->
<view class="preview-list" v-if="previewImages.length > 0">
<view class="preview-item" v-for="(img, index) in previewImages" :key="index">
<image :src="img" class="preview-img" mode="aspectFill" />
<text class="remove-btn" @click="removeImage(index)">×</text>
</view>
</view>
</view>
<!-- 关联订单号(下拉选择) -->
<view class="form-item">
<text class="label">关联订单号(可选)</text>
<picker mode="selector" :range="orderOptions" @change="onOrderChange" :value="selectedOrderIndex">
<view class="picker-input">
{{ selectedOrderIndex === -1 ? '请选择订单' : orderOptions[selectedOrderIndex] }}
</view>
</picker>
</view>
<!-- 提交按钮 -->
<view class="submit-section">
<button
class="submit-btn"
:disabled="!isValid || isSubmitting"
@click="submitFeedback"
>
{{ isSubmitting ? '提交中...' : '提交反馈' }}
</button>
</view>
</view>
<!-- 底部提示 -->
<view class="footer-tip">
<text>您的反馈将被优先处理我们将在24小时内回复</text>
</view>
</view>
</template>
<script lang="uts">
export default {
data() {
return {
// 表单数据
title: '',
content: '',
selectedTypeIndex: 0,
selectedOrderIndex: -1, // -1 表示未选择
previewImages: [] as string[],
// 配置
feedbackTypes: [
'订单问题',
'配送异常',
'系统故障',
'账号/认证问题',
'车辆/设备问题',
'收入结算问题',
'其他'
],
// 模拟历史订单(实际项目中应从接口获取)
orders: [
{ id: 'ORD20250122001', status: '已完成', created_at: '2025-01-22 14:30' },
{ id: 'ORD20250122002', status: '配送中', created_at: '2025-01-22 13:15' },
{ id: 'ORD20250122003', status: '已取消', created_at: '2025-01-22 10:45' },
{ id: 'ORD20250122004', status: '待接单', created_at: '2025-01-22 09:20' }
],
isSubmitting: false
}
},
computed: {
// 订单选项:格式为 "ORDxxx - 状态 - 时间"
orderOptions() {
if (this.orders.length === 0) return ['无可用订单']
return this.orders.map(order =>
`${order.id} - ${order.status} - ${order.created_at.split(' ')[0]}`
)
},
isValid() {
return this.title.trim().length >= 2 &&
this.content.trim().length >= 10 &&
this.selectedTypeIndex >= 0
}
},
methods: {
goBack() {
uni.navigateBack()
},
onTypeChange(e: UniEvent<HTMLInputElement>) {
this.selectedTypeIndex = e.detail.value as number
},
onOrderChange(e: UniEvent<HTMLInputElement>) {
const index = e.detail.value as number
this.selectedOrderIndex = index
// 如果选择了有效订单,自动填充 orderId 字段(可选)
if (index >= 0 && index < this.orders.length) {
// 实际业务中可绑定 order.id 到某个字段,此处仅作示意
} else {
// 未选择时清空
}
},
chooseImage() {
if (this.previewImages.length >= 3) {
uni.showToast({
title: '最多上传3张图片',
icon: 'none'
})
return
}
uni.chooseImage({
count: 3 - this.previewImages.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePaths = res.tempFilePaths
const newImages = [...this.previewImages, ...tempFilePaths]
if (newImages.length > 3) {
this.previewImages = newImages.slice(0, 3)
} else {
this.previewImages = newImages
}
}
})
},
removeImage(index: number) {
this.previewImages.splice(index, 1)
},
submitFeedback() {
if (!this.isValid) {
let errorMsg = ''
if (this.title.trim().length < 2) {
errorMsg = '请填写标题至少2字'
} else if (this.content.trim().length < 10) {
errorMsg = '请填写详细内容至少10字'
} else if (this.selectedTypeIndex < 0) {
errorMsg = '请选择问题类型'
}
uni.showToast({
title: errorMsg,
icon: 'none'
})
return
}
if (this.isSubmitting) return
this.isSubmitting = true
// 模拟提交
uni.showLoading({ title: '提交中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '反馈已提交成功!',
icon: 'success'
})
// 1.5秒后返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
}, 1000)
}
}
}
</script>
<style scoped>
.feedback-container {
background-color: #f8f9fa;
min-height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* 顶部导航 */
.header-bar {
background-color: #fff;
padding: 20rpx 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1rpx solid #e9ecef;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.nav-left {
display: flex;
align-items: center;
cursor: pointer;
padding: 10rpx;
border-radius: 8rpx;
transition: background-color 0.2s ease;
}
.nav-left:hover {
background-color: #f0f0f0;
}
.nav-left:active {
background-color: #e0e0e0;
}
.nav-icon {
font-size: 36rpx;
color: #333;
}
.page-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
text-align: center;
flex: 1;
}
.nav-right {
width: 60rpx;
height: 1rpx;
}
/* 表单区域 */
.form-wrapper {
padding: 30rpx;
background-color: #fff;
}
.form-item {
margin-bottom: 30rpx;
}
.label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
font-weight: 500;
}
/* 输入框统一样式 */
.input-field,
.textarea-field,
.picker-input {
width: 100%;
padding: 20rpx 24rpx;
border: 1rpx solid #e0e0e0;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
background-color: #fafafa;
box-sizing: border-box;
}
.textarea-field {
min-height: 180rpx;
height: auto;
padding: 24rpx;
line-height: 1.6;
}
.picker-input {
background-color: #fff;
text-align: right;
color: #666;
padding-right: 40rpx;
position: relative;
}
.picker-input::after {
content: '▼';
position: absolute;
right: 24rpx;
top: 50%;
transform: translateY(-50%);
font-size: 24rpx;
color: #999;
}
/* 上传区域 */
.upload-area {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40rpx 20rpx;
border: 2rpx dashed #e0e0e0;
border-radius: 16rpx;
background-color: #f8f9fa;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover {
border-color: #74b9ff;
background-color: #e8f4fd;
}
.upload-icon {
font-size: 60rpx;
color: #999;
margin-bottom: 16rpx;
}
.upload-text {
font-size: 28rpx;
color: #666;
margin-bottom: 10rpx;
}
.upload-tip {
font-size: 24rpx;
color: #999;
}
/* 图片预览 */
.preview-list {
display: flex;
gap: 20rpx;
margin-top: 20rpx;
flex-wrap: wrap;
}
.preview-item {
position: relative;
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
overflow: hidden;
}
.preview-img {
width: 100%;
height: 100%;
}
.remove-btn {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 32rpx;
height: 32rpx;
background-color: rgba(0, 0, 0, 0.7);
color: white;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
z-index: 1;
}
/* 提交按钮 */
.submit-section {
margin-top: 40rpx;
}
.submit-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
color: white;
font-size: 32rpx;
font-weight: bold;
border-radius: 44rpx;
border: none;
box-shadow: 0 8rpx 20rpx rgba(116, 185, 255, 0.3);
transition: all 0.3s;
}
.submit-btn:disabled {
background: #cccccc; /* 禁用时灰色背景 */
color: #999;
cursor: not-allowed; /* 禁用时显示禁止光标 */
}
.submit-btn:active {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(116, 185, 255, 0.2);
}
/* 底部提示 */
.footer-tip {
text-align: center;
padding: 30rpx;
color: #999;
font-size: 24rpx;
background-color: #fff;
border-top: 1rpx solid #e9ecef;
}
</style>

View File

@@ -0,0 +1,524 @@
<template>
<view class="help-center-container">
<!-- 顶部导航栏 -->
<view class="header-bar">
<view class="nav-left" @click="goBack">
<text class="nav-icon">←</text>
</view>
<text class="page-title">帮助中心</text>
<view class="nav-right"></view>
</view>
<!-- 搜索框 -->
<view class="search-bar">
<view class="search-input-wrapper">
<text class="search-icon">🔍</text>
<input
type="text"
placeholder="搜索常见问题..."
v-model="searchQuery"
@input="onSearch"
class="search-input"
/>
</view>
</view>
<!-- 分类导航 -->
<view class="category-tabs">
<scroll-view scroll-x="true" class="tabs-scroll">
<view
v-for="cat in categories"
:key="cat.id"
class="tab-item"
:class="{ active: activeCategory === cat.id }"
@click="switchCategory(cat.id)"
>
{{ cat.name }}
</view>
</scroll-view>
</view>
<!-- 内容区域 -->
<view class="content-wrapper">
<!-- 筛选标签(仅在搜索时显示) -->
<view v-if="searchQuery" class="filter-tags">
<text class="tag">搜索结果:{{ searchResults.length }} 条</text>
<text class="clear-btn" @click="clearSearch">清除</text>
</view>
<!-- 常见问题列表 -->
<view v-if="currentQuestions.length > 0">
<view v-for="item in currentQuestions" :key="item.id" class="question-item">
<view class="question-header" @click="toggleExpand(item)">
<text class="question-text">{{ item.title }}</text>
<text class="expand-icon">{{ item.expanded ? '▲' : '▼' }}</text>
</view>
<view v-if="item.expanded" class="answer-content">
<text class="answer-text">{{ item.content }}</text>
</view>
</view>
</view>
<!-- 无结果提示 -->
<view v-else class="no-result">
<text class="no-result-icon">❓</text>
<text class="no-result-title">暂无匹配结果</text>
<text class="no-result-desc">请尝试更换关键词,或查看以下热门问题</text>
<view class="hot-questions">
<text class="hot-tag">热门问题:</text>
<text class="hot-item" @click="jumpToQuestion('q1')">如何接单?</text>
<text class="hot-item" @click="jumpToQuestion('q2')">配送超时怎么办?</text>
<text class="hot-item" @click="jumpToQuestion('q3')">如何联系客户?</text>
</view>
</view>
</view>
<!-- 底部联系客服 -->
<view class="contact-footer">
<view class="contact-card">
<text class="contact-title">需要人工帮助?</text>
<text class="contact-desc">7×24小时在线客服为您解决配送中遇到的任何问题</text>
<button class="contact-btn" @click="contactCustomerService">联系客服</button>
</view>
</view>
</view>
</template>
<script lang="uts">
export default {
data() {
return {
searchQuery: '',
activeCategory: 'all',
categories: [
{ id: 'all', name: '全部' },
{ id: 'order', name: '订单相关' },
{ id: 'delivery', name: '配送操作' },
{ id: 'account', name: '账号与认证' },
{ id: 'payment', name: '收入与结算' },
{ id: 'vehicle', name: '车辆管理' }
],
questions: [
{
id: 'q1',
title: '如何接单?',
content: '1. 进入【任务列表】页面\n2. 查看“待接单”任务\n3. 点击任务卡片 → 点击【接受订单】按钮\n4. 系统将自动分配该订单给您,状态变为“已接单”\n⚠ 注意同一时间只能接1个订单接单后需在15分钟内取货。',
category: ['order', 'delivery'],
expanded: false
},
{
id: 'q2',
title: '配送超时怎么办?',
content: '若因交通堵塞、客户联系不上等不可抗力导致超时,请在送达后点击【异常上报】→ 选择原因并上传凭证如截图、通话记录系统将审核后减免处罚。建议提前10分钟联系客户确认收货时间。',
category: ['delivery'],
expanded: false
},
{
id: 'q3',
title: '如何联系客户?',
content: '在【任务详情】页点击【联系客户】→ 选择【拨打电话】或【发送短信】。系统会隐藏真实号码,保护双方隐私。首次联系建议使用语音电话,确保沟通效率。',
category: ['delivery', 'order'],
expanded: false
},
{
id: 'q4',
title: '如何修改个人信息?',
content: '进入【个人中心】→ 点击头像 → 【编辑资料】→ 修改姓名/电话/身份证号等信息 → 提交后等待审核通常2小时内完成。注意身份证信息需上传清晰照片。',
category: ['account'],
expanded: false
},
{
id: 'q5',
title: '配送费如何计算?',
content: '基础配送费 = 距离费¥1.5/km + 时间费¥0.5/分钟) + 服务费¥2~5根据时段浮动\n例如3km + 15分钟 = 3×1.5 + 15×0.5 + 3 = ¥15\n实际金额以订单详情页为准。',
category: ['payment'],
expanded: false
},
{
id: 'q6',
title: '车辆信息如何绑定?',
content: '【个人中心】→ 【车辆管理】→ 【添加车辆】→ 输入车牌号、车型、行驶证照片 → 提交审核。审核通过后,系统将自动关联您的配送任务。未绑定车辆无法接单。',
category: ['vehicle'],
expanded: false
},
{
id: 'q7',
title: '如何查看收入明细?',
content: '【个人中心】→ 【收入明细】→ 可按日/周/月筛选 → 查看每笔订单的配送费、奖励、扣款及到账时间。支持导出Excel报表需企业版权限。',
category: ['payment'],
expanded: false
},
{
id: 'q8',
title: '任务被取消了怎么办?',
content: '若客户取消订单,您将收到系统通知。已完成取货的订单,可申请补偿(路径:任务详情 → 【申请补偿】未取货的订单无补偿。每月最多3次无责取消不计入考核。',
category: ['order'],
expanded: false
},
{
id: 'q9',
title: '导航功能不准确?',
content: '请检查:① 手机GPS权限是否开启② 是否使用最新版APP③ 在【设置】→ 【地图偏好】中切换高德/百度地图。仍无效请反馈至客服,并提供截图+坐标。',
category: ['delivery'],
expanded: false
},
{
id: 'q10',
title: '如何提升评分?',
content: '评分由客户评价70%+ 系统考核30%)组成。建议:① 准时送达(≤预计时间);② 礼貌沟通;③ 主动拍照上传(取货/送达);④ 避免频繁拒单。当前平均分4.8/5.0',
category: ['delivery', 'account'],
expanded: false
}
]
}
},
computed: {
currentQuestions() {
if (this.searchQuery) {
return this.searchResults
}
return this.questions.filter(q =>
this.activeCategory === 'all' || q.category.includes(this.activeCategory)
)
},
searchResults() {
const query = this.searchQuery.toLowerCase()
return this.questions.filter(q =>
q.title.toLowerCase().includes(query) ||
q.content.toLowerCase().includes(query)
)
}
},
methods: {
goBack() {
uni.navigateBack()
},
onSearch() {
// 实时搜索(可加防抖)
if (!this.searchQuery) {
this.activeCategory = 'all'
}
},
clearSearch() {
this.searchQuery = ''
this.activeCategory = 'all'
},
switchCategory(categoryId) {
this.activeCategory = categoryId
this.searchQuery = ''
},
toggleExpand(item) {
item.expanded = !item.expanded
// 关闭其他展开项(可选)
// this.questions.forEach(q => { if (q !== item) q.expanded = false })
},
jumpToQuestion(id) {
const item = this.questions.find(q => q.id === id)
if (item) {
item.expanded = true
this.activeCategory = 'all'
this.searchQuery = ''
// 滚动到该问题(简化版)
uni.pageScrollTo({ scrollTop: 300, duration: 300 })
}
},
contactCustomerService() {
uni.showActionSheet({
itemList: ['在线客服', '电话咨询', '提交工单'],
success: (res) => {
if (res.tapIndex === 0) {
uni.navigateTo({ url: '/pages/mall/common/customer-service' })
} else if (res.tapIndex === 1) {
uni.makePhoneCall({ phoneNumber: '400-123-4567' })
} else {
uni.navigateTo({ url: '/pages/mall/common/feedback?from=help' })
}
}
})
}
}
}
</script>
<style scoped>
.help-center-container {
background-color: #f8f9fa;
min-height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* 顶部导航 */
.header-bar {
background-color: #fff;
padding: 20rpx 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1rpx solid #e9ecef;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.nav-left {
display: flex;
align-items: center;
cursor: pointer;
padding: 10rpx;
border-radius: 8rpx;
transition: background-color 0.2s ease;
}
.nav-left:hover {
background-color: #f0f0f0;
}
.nav-left:active {
background-color: #e0e0e0;
}
.nav-icon {
font-size: 36rpx;
color: #333;
}
.page-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
text-align: center;
flex: 1;
}
.nav-right {
width: 60rpx;
height: 1rpx;
}
/* 搜索框 —— 修复放大镜与输入框垂直对齐 */
.search-bar {
padding: 20rpx 30rpx;
background-color: #fff;
}
.search-input-wrapper {
display: flex;
align-items: center; /* ✅ 垂直居中核心 */
background-color: #f5f5f5;
border-radius: 40rpx;
padding: 12rpx 20rpx;
}
.search-icon {
font-size: 28rpx;
color: #999;
margin-right: 12rpx;
line-height: 1;
display: inline-flex;
align-items: center;
height: 48rpx; /* 与 input 高度一致 */
}
.search-input {
flex: 1;
font-size: 26rpx;
background: transparent;
outline: none;
color: #333;
height: 48rpx;
padding: 0;
}
/* 分类导航 */
.category-tabs {
background-color: #fff;
padding: 0 30rpx 20rpx;
}
.tabs-scroll {
white-space: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.tab-item {
display: inline-block;
padding: 12rpx 24rpx;
margin-right: 16rpx;
background-color: #f8f9fa;
border-radius: 30rpx;
font-size: 26rpx;
color: #666;
transition: all 0.3s;
}
.tab-item.active {
background-color: #74b9ff;
color: white;
font-weight: 500;
}
/* 内容区域 */
.content-wrapper {
padding: 0 30rpx;
flex: 1;
}
.filter-tags {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15rpx 0;
background-color: #e8f4fd;
border-radius: 12rpx;
margin-bottom: 20rpx;
}
.tag {
font-size: 24rpx;
color: #333;
}
.clear-btn {
font-size: 24rpx;
color: #74b9ff;
padding: 4rpx 12rpx;
border-radius: 8rpx;
}
.question-item {
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
}
.question-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 20rpx;
cursor: pointer;
border-bottom: 1rpx solid #f0f0f0;
}
.question-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
flex: 1;
}
.expand-icon {
font-size: 32rpx;
color: #999;
width: 36rpx;
text-align: center;
}
.answer-content {
padding: 0 20rpx 20rpx;
background-color: #f8f9fa;
}
.answer-text {
font-size: 26rpx;
color: #666;
line-height: 1.6;
white-space: pre-line;
}
/* 无结果提示 */
.no-result {
text-align: center;
padding: 80rpx 30rpx;
background-color: #fff;
border-radius: 16rpx;
margin-top: 20rpx;
}
.no-result-icon {
font-size: 80rpx;
color: #e0e0e0;
margin-bottom: 20rpx;
}
.no-result-title {
font-size: 32rpx;
color: #333;
font-weight: bold;
margin-bottom: 15rpx;
}
.no-result-desc {
font-size: 26rpx;
color: #666;
margin-bottom: 25rpx;
}
.hot-questions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12rpx;
}
.hot-tag {
font-size: 24rpx;
color: #999;
margin-right: 12rpx;
}
.hot-item {
font-size: 24rpx;
color: #74b9ff;
padding: 4rpx 12rpx;
border-radius: 8rpx;
background-color: #e8f4fd;
}
/* 底部联系客服 */
.contact-footer {
padding: 30rpx;
background-color: #fff;
border-top: 1rpx solid #e9ecef;
}
.contact-card {
background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
border-radius: 20rpx;
padding: 30rpx;
color: white;
text-align: center;
}
.contact-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 12rpx;
}
.contact-desc {
font-size: 26rpx;
opacity: 0.9;
line-height: 1.5;
margin-bottom: 20rpx;
}
.contact-btn {
width: 100%;
height: 80rpx;
background-color: white;
color: #74b9ff;
font-size: 30rpx;
font-weight: bold;
border-radius: 40rpx;
border: none;
box-shadow: 0 6rpx 15rpx rgba(0, 0, 0, 0.1);
}
</style>

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,48 +543,70 @@
},
// 任务操作方法
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 = 4 // 更新状态为“已取货”
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 // 更新状态为“配送中”
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' })
}
uni.showToast({
title: '开始配送',
icon: 'success'
})
},
// 显示确认送达弹框
@@ -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

@@ -1,4 +1,3 @@
<!-- 配送端 - 订单详情页 -->
<template>
<view class="delivery-order-detail">
<!-- 返回按钮 -->
@@ -180,15 +179,8 @@
</template>
<script lang="uts">
import type { OrderType, OrderItemType, MerchantType } from '@/types/mall-types.uts'
type DeliveryInfoType = {
distance: number
estimated_time: number
courier_id: string
pickup_time: string
delivery_time: string
}
import supa, { supaReady } from '@/components/supadb/aksupainstance.uts'
import { getCurrentUserId } from '@/utils/store.uts'
export default {
data() {
@@ -207,8 +199,8 @@ export default {
payment_status: 0,
delivery_address: {},
created_at: ''
} as OrderType,
orderItems: [] as Array<OrderItemType & { product_image: string }>,
},
orderItems: [],
merchant: {
id: '',
user_id: '',
@@ -222,109 +214,150 @@ export default {
rating: 0,
total_sales: 0,
created_at: ''
} as MerchantType,
},
deliveryInfo: {
distance: 0,
estimated_time: 0,
courier_id: '',
pickup_time: '',
delivery_time: ''
} as DeliveryInfoType,
},
pickupAddress: '',
customerNote: '',
merchantNote: '',
deliveryNote: '', // 配送备注
deliveryNote: '',
}
},
onLoad(options: any) {
const orderId = options.id as string
// 👇 从 URL 参数获取 status
// 从 URL 参数获取 status
const status = parseInt(options.status as string) || 0
if (orderId) {
// 将从 URL 获取的状态赋值给 order 对象
this.order.id = orderId
this.order.status = status // 👈 设置状态
this.order.status = status // 设置传入的状态
this.loadOrderDetail(orderId)
}
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack()
},
loadOrderDetail(orderId: string) {
// 模拟加载订单详情数据
// 注意:现在 status 的值在 onLoad 时已经从 URL 获取并设置,这里可以依据它来调整数据加载逻辑(如果需要)
// 为了演示,我们保持模拟数据不变,但实际应用中可以根据 status 加载不同的数据
this.order = {
...this.order, // 保留从 URL 获取的 id 和 status
order_no: 'ORD202401150001',
user_id: 'user_001',
merchant_id: 'merchant_001',
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'
}
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'
async loadOrderDetail(orderId: string) {
const originalStatus = this.order.status
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)
}
},
getStatusDesc(): string {
// 根据 order.status 动态返回描述
if (this.order.status >= 5) {
return '订单已送达完成'
} else if (this.order.status === 4) {
@@ -349,111 +382,116 @@ export default {
return Object.keys(specifications).map(key => `${key}: ${specifications[key]}`).join(', ')
},
// ✅ 新增:点击“正在取货”按钮
confirmArrivedAtPickup() {
// 仅在订单未完成时执行
if (this.order.status < 5) {
uni.showModal({
title: '正在取货',
content: '确认已到达商家,开始取货?',
success: (res) => {
if (res.confirm) {
this.order.status = 3 // 更新状态为取货中
uni.showToast({
title: '已进入取货流程',
icon: 'success'
})
}
}
})
}
},
// ✅ 保留:点击“确认取货”按钮
confirmPickup() {
// 仅在订单未完成时执行
if (this.order.status < 5) {
uni.showModal({
title: '确认取货',
content: '确认已从商家处取到商品?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
this.order.status = 4 // 更新状态为已取货
this.deliveryInfo.pickup_time = new Date().toISOString()
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' })
}
}
}
})
}
},
// ✅ 保留:点击“确认送达”按钮
confirmDelivery() {
// 仅在订单未完成时执行
if (this.order.status < 5) {
uni.showModal({
title: '确认送达',
content: '确认商品已送达到顾客手中?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
this.order.status = 5 // 更新状态为已完成
this.deliveryInfo.delivery_time = new Date().toISOString()
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' })
}
}
}
})
}
},
// ✅ 保留:接受订单
acceptOrder() {
// 仅在订单未完成时执行
if (this.order.status < 5) {
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' })
}
}
}
})
}
},
// ✅ 保留:拒绝订单
rejectOrder() {
// 仅在订单未完成时执行
if (this.order.status < 5) {
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' })
}
}
}
})
}
},
// ✅ 保留:启动导航(用于“前往取货”按钮)
startNavigation() {
// 仅在订单未完成时执行
if (this.order.status < 5) {
uni.showToast({
title: '正在启动导航',
@@ -471,15 +509,11 @@ export default {
callCustomer() {
const phone = this.getDeliveryAddress().phone
uni.makePhoneCall({
phoneNumber: phone
})
uni.makePhoneCall({ phoneNumber: phone })
},
callMerchant() {
uni.makePhoneCall({
phoneNumber: this.merchant.contact_phone
})
uni.makePhoneCall({ phoneNumber: this.merchant.contact_phone })
},
contactService() {
@@ -492,13 +526,14 @@ export default {
</script>
<style>
/* ... 保持原有 style 部分不变 ... */
/* 保持原有样式不变 */
.delivery-order-detail {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 160rpx;
}
/* ... 其余样式保持原样 ... */
/* 返回按钮头部 */
.back-header {
background-color: #fff;
@@ -866,18 +901,18 @@ export default {
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
display: flex;
gap: 15rpx;
flex-wrap: wrap; /* 允许按钮换行 */
flex-wrap: wrap;
}
.action-btn {
flex: 1;
min-width: 120rpx; /* 设置最小宽度 */
min-width: 120rpx;
height: 70rpx;
border-radius: 35rpx;
font-size: 26rpx;
border: none;
margin: 5rpx; /* 添加外边距 */
box-sizing: border-box; /* 确保宽高包含边距 */
margin: 5rpx;
box-sizing: border-box;
}
.action-btn.accept, .action-btn.complete {

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

@@ -4,8 +4,8 @@
<!-- 1. 蓝色头像条profile-header -->
<view class="profile-header">
<!-- 返回按钮:最左边垂直居中 -->
<view class="back-box" @click="backToIndex">
<text class="back-icon"></text>
<view class="nav-left" @click="goBack">
<text class="nav-icon"></text>
</view>
<image :src="driverInfo.avatar_url || '/static/default-avatar.png'" class="driver-avatar" @click="editProfile" />
@@ -30,8 +30,12 @@
<view class="toggle-handle"></view>
</view>
</view>
<view v-if="workStatus === 1" class="current-location">
<!-- 修改点:将定位信息和重新定位按钮合并到一个区域 -->
<view v-if="workStatus === 1" class="current-location-section">
<text class="location-text">📍 {{ currentLocation }}</text>
<view class="relocate-icon" @click.stop="showRelocateConfirm">
<text class="icon-text">↻</text>
</view>
</view>
</view>
</view>
@@ -196,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] || '未知状态'
@@ -343,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 '刚刚'
@@ -354,23 +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() } })
}
function relocate() {
uni.showLoading({ title: '获取位置中...', mask: true })
setTimeout(() => {
uni.hideLoading()
currentLocation.value = '朝阳区建国门外大街附近'
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 = '') {
@@ -378,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/common/help' })
}
function goToFeedback() {
uni.navigateTo({ url: '/pages/mall/common/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>
@@ -410,7 +394,7 @@ function goToFeedback() {
.profile-header {
position: relative;
}
.back-box {
.nav-left {
position: absolute;
left: 30rpx;
top: 50%;
@@ -423,15 +407,15 @@ function goToFeedback() {
align-items: center;
justify-content: center;
}
.back-box:active {
.nav-left:active {
background: rgba(0, 0, 0, .3);
}
.back-icon {
.nav-icon {
font-size: 40rpx;
color: #fff;
}
/* ---------- 以下与原样式一致 ---------- */
/* ---------- 以下与原样式一致,仅修改工作状态区域 ---------- */
.delivery-profile {
padding: 0 0 120rpx 0;
background-color: #f5f5f5;
@@ -541,14 +525,39 @@ function goToFeedback() {
.toggle-switch.active .toggle-handle {
left: 55rpx;
}
.current-location {
/* --- 修改后的定位信息区域:地址 + 图标 --- */
.current-location-section {
display: flex;
align-items: center;
padding: 15rpx 20rpx;
background: #e8f4fd;
background: #e8f4fd; /* 与原样式保持一致 */
border-radius: 15rpx;
}
.location-text {
font-size: 24rpx; /* 与截图字号一致 */
color: #74b9ff; /* 与截图颜色一致 */
flex: 1; /* 让文字占据剩余空间 */
margin-right: 16rpx; /* 与图标留空隙 */
}
.relocate-icon {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(116, 185, 255, 0.15); /* 浅蓝底 */
color: #74b9ff; /* 主色文字 */
font-size: 24rpx;
color: #74b9ff;
cursor: pointer;
transition: all 0.2s ease;
}
.relocate-icon:hover {
background: rgba(116, 185, 255, 0.3); /* 悬停效果 */
}
.relocate-icon:active {
background: rgba(116, 185, 255, 0.4); /* 点击效果 */
}
.task-tabs {
@@ -556,10 +565,10 @@ function goToFeedback() {
justify-content: space-between;
}
.task-tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
position: relative;
}
.tab-icon {

View File

@@ -0,0 +1,418 @@
<template>
<view class="container">
<view class="header">
<text class="title">Supabase 连接测试</text>
</view>
<view class="config-info">
<text class="label">当前配置:</text>
<text class="value">URL: {{ config.url }}</text>
<text class="value">Key: {{ config.keyMasked }}</text>
</view>
<view class="test-section">
<button class="btn-primary" @click="runAllTests" :disabled="isLoading">
{{ isLoading ? '测试中...' : '运行全部测试' }}
</button>
</view>
<view class="results" v-if="testResults.length > 0">
<text class="section-title">测试结果</text>
<view
class="result-item"
v-for="(result, index) in testResults"
:key="index"
:class="{ success: result.success, error: !result.success }"
>
<text class="result-icon">{{ result.success ? '✅' : '❌' }}</text>
<text class="result-text">{{ result.name }}</text>
<text class="result-message">{{ result.message }}</text>
<text v-if="result.errorDetails" class="error-details">{{ result.errorDetails }}</text>
</view>
</view>
<view class="quick-tests">
<text class="section-title">单项测试</text>
<button class="btn-secondary" @click="testConnection">测试连接</button>
<button class="btn-secondary" @click="testQuery">测试查询</button>
<button class="btn-secondary" @click="testAuth">测试认证</button>
</view>
</view>
</template>
<script lang="uts">
// 导入你的封装库和配置
import supa from '@/components/supadb/aksupainstance.uts'
import { SUPA_URL, SUPA_KEY } from '@/ak/config.uts'
// 定义测试结果类型
type TestResult = {
name: string
success: boolean
message: string
errorDetails?: string
}
// 定义用户类型(用于类型安全查询)
type User = {
id?: number
email?: string
nickname?: string
phone?: string
created_at?: string
}
export default {
data() {
return {
config: {
url: SUPA_URL,
key: SUPA_KEY,
keyMasked: SUPA_KEY.substring(0, 20) + '...' // 隐藏部分密钥
},
isLoading: false,
testResults: [] as TestResult[]
}
},
onLoad() {
console.log('🎯 Supabase 测试页面加载完成')
console.log('📋 配置信息:', {
url: SUPA_URL,
hasKey: !!SUPA_KEY
})
},
methods: {
// 获取错误消息的安全函数
getErrorMessage(err: any): string {
if (err == null) return '未知错误'
if (typeof err === 'string') return err
if (typeof err === 'object') {
if (err.message) return err.message
if (err.error) return err.error
if (err.msg) return err.msg
return JSON.stringify(err)
}
return String(err)
},
// 安全执行测试
async safeExecute<T>(promise: Promise<T>, operationName: string): Promise<{ success: boolean; data?: T; error?: string }> {
try {
const result = await promise
return { success: true, data: result }
} catch (err) {
const errorMessage = this.getErrorMessage(err)
console.error(`❌ ${operationName} 失败:`, err)
return { success: false, error: errorMessage }
}
},
// 测试连接
async testConnection(): Promise<TestResult> {
console.log('🔍 开始测试连接...')
const result = await this.safeExecute(
uni.request({
url: `${SUPA_URL}/rest/v1/`,
method: 'GET',
header: {
'apikey': SUPA_KEY,
'Authorization': `Bearer ${SUPA_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}),
'基础连接测试'
)
if (result.success) {
const response = result.data as any
if (response.statusCode === 200 || response.statusCode === 404) {
return {
name: '基础连接',
success: true,
message: `API 可达 (HTTP ${response.statusCode})`,
errorDetails: response.statusCode === 404 ? 'API 响应正常,但请求的资源不存在' : undefined
}
} else {
return {
name: '基础连接',
success: false,
message: `HTTP 错误`,
errorDetails: `状态码: ${response.statusCode}, 数据: ${JSON.stringify(response.data)}`
}
}
} else {
return {
name: '基础连接',
success: false,
message: '连接失败',
errorDetails: result.error
}
}
},
// 测试查询
async testQuery(): Promise<TestResult> {
console.log('🔍 开始测试查询...')
// 尝试查询 users 表
const queryResult = await this.safeExecute(
supa.from('users').select('id, email, nickname').limit(1).execute(),
'查询测试'
)
if (queryResult.success) {
const { data, error } = queryResult.data as any
if (error) {
const errorMsg = this.getErrorMessage(error)
// 检查是否是表不存在的错误
if (errorMsg.includes('relation') && errorMsg.includes('does not exist')) {
return {
name: '查询测试',
success: false,
message: '表不存在',
errorDetails: 'users 表不存在,建议先在 Studio 中创建表'
}
}
return {
name: '查询测试',
success: false,
message: '查询出错',
errorDetails: errorMsg
}
}
return {
name: '查询测试',
success: true,
message: `查询成功,返回 ${data?.length || 0} 条记录`,
errorDetails: data && data.length > 0 ? `第一条: ${JSON.stringify(data[0])}` : '表为空'
}
} else {
return {
name: '查询测试',
success: false,
message: '查询失败',
errorDetails: queryResult.error
}
}
},
// 测试认证
async testAuth(): Promise<TestResult> {
console.log('🔍 开始测试认证...')
const authResult = await this.safeExecute(
supa.auth.getSession(),
'认证测试'
)
if (authResult.success) {
const { data, error } = authResult.data as any
if (error) {
return {
name: '认证测试',
success: false,
message: '获取会话失败',
errorDetails: this.getErrorMessage(error)
}
}
if (data?.session) {
return {
name: '认证测试',
success: true,
message: '用户已登录',
errorDetails: `用户: ${data.session.user.email || data.session.user.phone || 'ID: ' + data.session.user.id}`
}
} else {
return {
name: '认证测试',
success: true,
message: '未登录(正常状态)',
errorDetails: '当前没有活动会话,需要先登录'
}
}
} else {
return {
name: '认证测试',
success: false,
message: '认证失败',
errorDetails: authResult.error
}
}
},
// 运行所有测试
async runAllTests() {
this.isLoading = true
this.testResults = []
try {
// 依次运行测试
const tests = [
{ name: 'connection', func: this.testConnection },
{ name: 'query', func: this.testQuery },
{ name: 'auth', func: this.testAuth }
]
for (const test of tests) {
console.log(`🧪 运行测试: ${test.name}`)
const result = await test.func.call(this)
this.testResults.push(result)
// 短暂延迟,让用户看到测试进度
await new Promise(resolve => setTimeout(resolve, 500))
}
// 总结结果
const passed = this.testResults.filter(r => r.success).length
const total = this.testResults.length
console.log(`📈 测试总结: ${passed}/${total} 通过`)
} catch (error) {
console.error('❌ 运行测试时发生错误:', error)
this.testResults.push({
name: '整体测试',
success: false,
message: '测试框架错误',
errorDetails: this.getErrorMessage(error)
})
} finally {
this.isLoading = false
}
}
}
}
</script>
<style>
.container {
padding: 30rpx;
background-color: #f8f9fa;
min-height: 100vh;
}
.header {
text-align: center;
margin-bottom: 40rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.config-info {
background-color: #fff;
padding: 20rpx;
border-radius: 12rpx;
margin-bottom: 30rpx;
}
.label {
font-size: 28rpx;
color: #666;
display: block;
margin-bottom: 10rpx;
}
.value {
font-size: 24rpx;
color: #333;
display: block;
margin-bottom: 8rpx;
word-break: break-all;
}
.test-section {
margin-bottom: 40rpx;
}
.btn-primary {
width: 100%;
height: 80rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border-radius: 12rpx;
font-size: 28rpx;
border: none;
margin-bottom: 20rpx;
}
.btn-secondary {
width: 100%;
height: 70rpx;
background-color: #6c757d;
color: #fff;
border-radius: 8rpx;
font-size: 26rpx;
border: none;
margin-bottom: 15rpx;
}
.btn-primary:disabled, .btn-secondary:disabled {
background-color: #ccc;
}
.results {
margin-bottom: 40rpx;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 20rpx;
}
.result-item {
background-color: #fff;
padding: 20rpx;
border-radius: 12rpx;
margin-bottom: 15rpx;
}
.result-item.success {
border-left: 4px solid #28a745;
background-color: #d4edda;
}
.result-item.error {
border-left: 4px solid #dc3545;
background-color: #f8d7da;
}
.result-icon {
font-size: 32rpx;
margin-right: 10rpx;
vertical-align: middle;
}
.result-text {
font-size: 26rpx;
color: #333;
font-weight: bold;
vertical-align: middle;
}
.result-message {
display: block;
font-size: 24rpx;
color: #666;
margin-top: 8rpx;
}
.error-details {
display: block;
font-size: 20rpx;
color: #856404;
background-color: #fff3cd;
padding: 8rpx;
border-radius: 4rpx;
margin-top: 8rpx;
word-break: break-all;
white-space: pre-wrap;
}
</style>

View File

@@ -1,4 +1,4 @@
import { supabase, ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
import supabase, { supaReady } from '@/components/supadb/aksupainstance.uts'
import type { DeviceInfo, DeviceParams } from './types.uts'
// 服务响应类型
@@ -17,7 +17,7 @@ export class SenseDataService {
*/
static async getDevices(params: DeviceParams): Promise<ServiceResponse<Array<DeviceInfo>>> {
try {
await ensureSupabaseReady()
await supaReady
const res = await supabase.from(SenseDataService.TABLE_NAME)
.select('*', {})
.eq('user_id', params.user_id)
@@ -46,7 +46,7 @@ export class SenseDataService {
*/
static async bindDevice(deviceData: UTSJSONObject): Promise<ServiceResponse<DeviceInfo>> {
try {
await ensureSupabaseReady()
await supaReady
const res = await supabase.from(SenseDataService.TABLE_NAME)
.insert(deviceData)
.select('*', {})
@@ -75,7 +75,7 @@ export class SenseDataService {
*/
static async unbindDevice(deviceId: string): Promise<ServiceResponse<null>> {
try {
await ensureSupabaseReady()
await supaReady
const res = await supabase.from(SenseDataService.TABLE_NAME)
.delete()
.eq('id', deviceId)
@@ -102,7 +102,7 @@ export class SenseDataService {
*/
static async updateDevice(deviceId: string, configData: UTSJSONObject): Promise<ServiceResponse<DeviceInfo>> {
try {
await ensureSupabaseReady()
await supaReady
const res = await supabase.from(SenseDataService.TABLE_NAME)
.update(configData)
.eq('id', deviceId)

View File

@@ -1,4 +1,4 @@
import { supabase, ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
import supabase, { supaReady } from '@/components/supadb/aksupainstance.uts'
import type { UserProfile } from '@/types/mall-types.uts'
/**
@@ -8,8 +8,8 @@ import type { UserProfile } from '@/types/mall-types.uts'
*/
export async function ensureUserProfile(sessionUser: UTSJSONObject): Promise<UserProfile | null> {
try {
await ensureSupabaseReady()
await supaReady
// 从 sessionUser 中获取用户ID和邮箱
const userId = sessionUser.getString('id')
const email = sessionUser.getString('email') ?? ''

View File

@@ -1,4 +1,4 @@
import { supabase as supa, ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
import supa, { supaReady } from '@/components/supadb/aksupainstance.uts'
import type { UserProfile, UserStats } from '@/pages/user/types.uts'
import type { DeviceInfo } from '@/pages/sense/types.uts'
import { SenseDataService, type DeviceParams } from '@/pages/sense/senseDataService.uts'
@@ -50,7 +50,7 @@ export const setUserProfile = (profile : UserProfile) => {
// 获取当前用户信息(含补全 profile
export async function getCurrentUser() : Promise<UserProfile | null> {
try {
await ensureSupabaseReady()
await supaReady
} catch (_) {}
const sessionInfo = supa.getSession()