diff --git a/components/CommonPagination/CommonPagination.uvue b/components/CommonPagination/CommonPagination.uvue
index 093b9f14..be21bff9 100644
--- a/components/CommonPagination/CommonPagination.uvue
+++ b/components/CommonPagination/CommonPagination.uvue
@@ -64,7 +64,7 @@ const onPageSizeChange = (e : any) => {
}
const onPageBtnClick = (p : number) => {
- if (p !== -1) {
+ if (p !== -1 && p >= 1 && p <= props.totalPage) {
emit('page-change', p)
}
}
diff --git a/pages/mall/admin/docs/UNI_APP_X_PAGE_FIX_GUIDE.md b/pages/mall/admin/docs/UNI_APP_X_PAGE_FIX_GUIDE.md
index 1a5566ba..f070fd82 100644
--- a/pages/mall/admin/docs/UNI_APP_X_PAGE_FIX_GUIDE.md
+++ b/pages/mall/admin/docs/UNI_APP_X_PAGE_FIX_GUIDE.md
@@ -1,8 +1,8 @@
---
-🚧 注意:
+� 文档维护说明:
-⚠ 注意:当前使用 mock 数据,后续真实接口完成后替换
-真实接口地址和返回字段未确定,请后续接口联调完成后再替换。
+此文档持续记录 uni-app-x / UTS / Supabase 项目开发中遇到的真实问题与解决方案,
+供后续开发者复盘参考,避免重复踩坑。
文档标记维持在此文件中,以便后续开发和对接。
---
@@ -265,6 +265,147 @@
3. 容器必须设置正确的 `height` 和 `overflow` 属性。
4. 成功的悬停菜单实现应该作为模板复用到其他组件。
+#### **原因二十六:假分页(全量加载 + 前端 slice)导致性能浪费与计数错误**
+
+- **现象**: 页面翻页正常,但 Network 面板显示每次进入页面只发一次请求;`total` 显示的数量与数据库实际行数不符(偏少);切换到第 2 页后数据消失或 `total` 突然跳变。
+- **根本原因**:
+ 1. `fetchUsers()` 没有传入 `limit/offset` 参数,一次性拉取所有记录到前端内存。
+ 2. `pagedList` 通过 `computed(() => userList.value.slice(start, end))` 实现,是纯前端裁切。
+ 3. `total` 为 `computed(() => userList.value.length)`,反映的是当次拉取的行数,而非数据库真实总行数。
+ 4. 翻页 handler 仅更新 `currentPage.value`,没有重新 `fetchUsers()`,所以只有第一次加载时才有网络请求。
+- **识别特征(自查清单)**:
+ ```
+ ❌ pagedList = computed(() => userList.value.slice((page-1)*ps, page*ps))
+ ❌ total = computed(() => userList.value.length)
+ ❌ 翻页 handler 只做 currentPage.value = p,没有 fetchUsers()
+ ❌ select() 调用没有 limit/offset 参数
+ ```
+- **正确实现(服务端分页模板)**:
+ ```typescript
+ // ✅ state
+ const total = ref(0) // 来自服务端,不是 .length
+ const pagedList = computed(() => userList.value) // 无前端 slice,服务端已分页
+
+ // ✅ fetch:每次翻页都重新请求
+ const fetchUsers = async (page: number, ps: number) => {
+ const offset = (page - 1) * ps
+ const offsetFilter = offset > 0 ? `offset=${offset}` : null
+ const res = await supabase.select('table', offsetFilter, {
+ limit: ps, order: 'created_at.desc', count: 'exact'
+ })
+ userList.value = (res.data as UTSJSONObject[]).map(mapDbRow)
+ // 从 Content-Range 头解析总数(见原因二十八)
+ total.value = parseTotalFromContentRange(res)
+ }
+
+ // ✅ 翻页 handler:每次翻页都调用 fetch
+ const handlePageChange = (p: number) => {
+ currentPage.value = p
+ fetchUsers(p, pageSize.value)
+ }
+
+ onMounted(() => fetchUsers(1, pageSize.value))
+ ```
+- **强制规则**: 任何使用 Supabase 的列表页面,`total` 必须来自后端 `Content-Range` 响应头或 `res.count`,严禁使用 `computed(() => localArray.value.length)` 作为总数。
+
+#### **原因二十七(补充):已在另一位置记录(响应式预览网格)**
+
+> 此编号已被响应式预览网格布局条目占用,见本文件第 37 行附近。
+
+#### **原因二十八:aksupa `.page().limit()` 触发 Range 头,导致 PostgREST 416 错误**
+
+- **现象**: 首页(第 1 页)加载正常,翻到第 2 页后浏览器控制台立刻出现 `416 Range Not Satisfiable`,数据消失或显示错误状态;即使页面 UI 上的"下一页"按钮已禁用,错误仍然出现。
+- **根本原因**:
+ 1. aksupa(自定义 PostgREST 封装)的 `.page(n).limit(ps)` 链式调用内部会计算 `rangeFrom = (n-1)*ps`、`rangeTo = n*ps-1`,然后把这两个值注入到 HTTP 请求头 `Range: bytes=rangeFrom-rangeTo`(或 PostgREST 格式的 Content-Range 请求头)中。
+ 2. PostgREST 遵循 HTTP Range 协议规范:只要 `rangeFrom >= total`(即请求的起始偏移 >= 数据库总行数),**必须返回 416**,这是协议强制行为,不是可配置的。
+ 3. UI 层的"禁用"保护(`if (p > totalPage) return`)在 `total` 来自错误来源时本身就不可靠(见原因二十六),因此无法阻止越界请求。
+ 4. 即使 UI 保护正确,在数据量恰好是页大小整数倍时(如共 15 条、每页 15 条),请求第 2 页仍会触发 416(`rangeFrom=15 >= total=15`)。
+- **错误修复思路(踩坑路径)**:
+ ```
+ ❌ 第一轮尝试:在 handlePageChange / onPageBtnClick 中加 if (p > totalPage) return
+ 结果:仍然 416。因为 UI 保护只能拦截按钮点击,无法修复协议层行为。
+
+ ❌ 第二轮尝试:在 fetchUsers 中加 if (res.status === 416) { ... } 分支处理
+ 结果:错误被吞但数据仍为空,用户体验差,根本原因未消除。
+ ```
+- **正确修复方案**:
+ ```typescript
+ // ❌ 禁止使用 Range 头分页
+ const res = await supabase
+ .from('ak_users')
+ .select(columns)
+ .page(page) // ← 产生 Range 头
+ .limit(pageSize) // ← 产生 Range 头
+ .execute()
+
+ // ✅ 使用 URL 参数分页(永远不返回 416)
+ const offset = (page - 1) * ps
+ const offsetFilter = offset > 0 ? `offset=${offset}` : null
+ const res = await supabase.select('ak_users', offsetFilter, {
+ columns: 'id, username, ...',
+ limit: ps,
+ order: 'created_at.desc',
+ count: 'exact' // → Prefer: count=exact → 响应包含 Content-Range: 0-14/26
+ })
+ // PostgREST 对 ?offset=N&limit=N 参数:offset 超出 total 时返回 200 + 空数组,绝不 416
+ ```
+- **总数解析(Content-Range 手动解析)**:
+ ```typescript
+ // 当 aksupa 使用 count: 'exact' 时,响应头会包含 Content-Range: 0-14/26
+ // 必须手动解析,因为绕过了 .page().limit() 的自动汇总逻辑
+ const parseTotal = (res: AkReqResponse): number => {
+ const cr = (res.headers?.['content-range'] ?? res.headers?.['Content-Range']) as string | null
+ if (cr) {
+ const slash = cr.lastIndexOf('/')
+ if (slash !== -1) {
+ const n = parseInt(cr.substring(slash + 1), 10)
+ if (!isNaN(n)) return n
+ }
+ }
+ // 降级:至少知道 offset + 当前页行数
+ return offset + (res.data as UTSJSONObject[]).length
+ }
+ total.value = parseTotal(res)
+ ```
+- **强制规则**:
+ 1. **项目内所有列表页禁止使用 `.page().limit()` 链式调用**(aksupa 或其他类似封装)。
+ 2. 分页必须使用 `offset=N` URL 参数注入方式。
+ 3. 总数必须从 `Content-Range` 响应头解析,不可用本地数组长度代替。
+ 4. RLS 注意:`ak_users` 表默认只允许 `auth.uid() = id`(自读)。Admin 页面需要额外添加 `service_role` 或管理员角色的 RLS 策略,否则即使分页正确,数据也会为空。
+
+#### **原因二十九:CommonPagination `disabled` 状态仅为 CSS 装饰,边界点击仍会触发**
+
+- **现象**: 在第 1 页时点击"上一页"按钮,或在最后一页时点击"下一页"按钮,虽然按钮视觉上呈灰色/禁用态,但仍会向父组件 `emit('page-change', 0)` 或 `emit('page-change', totalPage+1)`,导致 `fetchUsers(0, ps)` 被调用、产生 `offset=-ps`(负数),请求异常。
+- **根本原因**: `disabled` class 仅控制 CSS 样式(`opacity`、`cursor: not-allowed`),没有在事件处理函数中加入边界检查,点击事件照常触发和冒泡。
+- **修复方案**:
+ ```typescript
+ // CommonPagination.uvue — onPageBtnClick
+ // ❌ 修复前:直接 emit,无边界保护
+ const onPageBtnClick = (p: number) => {
+ if (p !== -1) emit('page-change', p)
+ }
+
+ // ✅ 修复后:加入 p >= 1 && p <= props.totalPage 双向边界检查
+ const onPageBtnClick = (p: number) => {
+ if (p !== -1 && p >= 1 && p <= props.totalPage) {
+ emit('page-change', p)
+ }
+ }
+ ```
+- **父页面的双重保护(纵深防御)**:
+ ```typescript
+ // 在 handlePageChange 中同样加入边界检查,防止其他调用路径绕过组件保护
+ const handlePageChange = (p: number) => {
+ if (p < 1 || p > totalPage.value) return
+ currentPage.value = p
+ fetchUsers(p, pageSize.value)
+ }
+ ```
+- **推广规则**:
+ 1. 任何分页组件,`disabled` 状态必须同时在 CSS **和**事件处理函数两层保护,视觉禁用 ≠ 逻辑禁用。
+ 2. 父组件的翻页 handler 应做二次校验,实现纵深防御。
+ 3. `fetchUsers` 的 `offset` 计算前应确保 `page >= 1`,避免负数 offset 进入请求。
+
## 🛠️ 完整修复流程
```
@@ -2264,4 +2405,4 @@ curl -i -X OPTIONS "http://192.168.1.61:9122/rest/v1/ml_coupon_templates?select=
---
-这个指南现在涵盖了 uni-app-x 项目开发中最常见的 33 类问题(包含全局组件化最佳实践),为后续开发提供了完整的故障排除和标准化指导。 🚀
+这个指南现在涵盖了 uni-app-x 项目开发中最常见的 36 类问题(含分页数据集成、PostgREST 协议层行为、全局组件化最佳实践),为后续开发提供了完整的故障排除和标准化指导。 🚀
diff --git a/pages/mall/admin/setting/delivery/management/components/DriverAddDrawer.uvue b/pages/mall/admin/setting/delivery/management/components/DriverAddDrawer.uvue
new file mode 100644
index 00000000..49ea016f
--- /dev/null
+++ b/pages/mall/admin/setting/delivery/management/components/DriverAddDrawer.uvue
@@ -0,0 +1,761 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 关联用户账号
+
+
+ * 手机号码
+
+
+
+
+
+
+ {{ searchError }}
+
+
+
+
+
+
+ {{ foundUser.username }}
+ {{ foundUser.phone }}
+
+ ✓ 已选择
+
+
+
+ ※ 配送员须通过已存在的用户账号关联,如账号不存在请先在"用户管理"中创建
+
+
+
+
+
+
+
+ 基本信息
+
+
+
+ * 真实姓名
+
+
+
+
+
+
+
+
+
+ 证件信息
+
+
+
+ * 身份证号
+
+
+
+ 驾驶证号
+
+
+
+
+
+
+
+
+
+ 车辆信息
+
+
+
+ 车辆类型
+
+
+ {{ vehicleTypeLabels[formVehicleType - 1] ?? '选择类型' }}
+ ▾
+
+
+
+
+ 车牌号码
+
+
+
+ 服务区域
+
+
+
+
+
+
+
+
+
+ 状态设置
+
+
+
+ 账号状态
+
+
+ {{ statusLabels[formStatus - 1] ?? '选择状态' }}
+ ▾
+
+
+
+
+ 接单状态
+
+
+ {{ workStatusLabels[formWorkStatus - 1] ?? '选择状态' }}
+ ▾
+
+
+
+
+
+
+
+
+ {{ saveError }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/mall/admin/setting/delivery/management/components/DriverDetailDrawer.uvue b/pages/mall/admin/setting/delivery/management/components/DriverDetailDrawer.uvue
new file mode 100644
index 00000000..3b8e95fa
--- /dev/null
+++ b/pages/mall/admin/setting/delivery/management/components/DriverDetailDrawer.uvue
@@ -0,0 +1,620 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 加载中...
+
+
+
+
+ {{ detailError }}
+
+
+
+
+
+
+
+
+
+ {{ detail.realName }}
+
+ {{ detail.statusLabel }}
+ {{ detail.workStatusLabel }}
+
+
+
+
+
+
+
+
+ 基本信息
+
+
+
+ 真实姓名:
+ {{ detail.realName }}
+
+
+ 手机号码:
+ {{ detail.phone }}
+
+
+ 注册账号:
+ {{ detail.username }}
+
+
+ ID:
+ {{ detail.id }}
+
+
+
+
+
+
+
+
+ 证件信息
+
+
+
+ 身份证号:
+ {{ detail.idCardMasked }}
+
+
+ 驾驶证号:
+ {{ detail.driverLicense }}
+
+
+
+
+
+
+
+
+ 车辆信息
+
+
+
+ 车辆类型:
+ {{ detail.vehicleTypeLabel }}
+
+
+ 车牌号码:
+ {{ detail.vehicleNumber }}
+
+
+ 服务区域:
+ {{ detail.serviceAreaStr }}
+
+
+
+
+
+
+
+
+ 绩效数据
+
+
+
+ {{ detail.orderCount }}
+ 接单总数
+
+
+ {{ detail.ratingAvg }}
+ 平均评分
+
+
+ {{ detail.ratingCount }}
+ 评价次数
+
+
+
+
+
+
+
+
+ 时间信息
+
+
+
+ 注册时间:
+ {{ detail.createdAt }}
+
+
+ 更新时间:
+ {{ detail.updatedAt }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/mall/admin/setting/delivery/management/components/DriverEditDrawer.uvue b/pages/mall/admin/setting/delivery/management/components/DriverEditDrawer.uvue
new file mode 100644
index 00000000..3ebb0208
--- /dev/null
+++ b/pages/mall/admin/setting/delivery/management/components/DriverEditDrawer.uvue
@@ -0,0 +1,633 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 加载中...
+
+
+
+
+ {{ loadError }}
+
+
+
+
+
+
+
+
+
+ 基本信息
+
+
+
+ * 真实姓名
+
+
+
+ 驾驶证号
+
+
+
+
+ ※ 手机号 / 注册账号属于用户账号信息,如需修改请前往"用户管理"
+
+
+
+
+
+
+
+ 车辆信息
+
+
+
+ 车辆类型
+
+
+ {{ vehicleTypeLabels[formVehicleType - 1] ?? '选择类型' }}
+ ▾
+
+
+
+
+ 车牌号码
+
+
+
+ 服务区域
+
+
+
+
+
+
+
+
+
+ 状态设置
+
+
+
+ 账号状态
+
+
+ {{ statusLabels[formStatus - 1] ?? '选择状态' }}
+ ▾
+
+
+
+
+ 接单状态
+
+
+ {{ workStatusLabels[formWorkStatus - 1] ?? '选择状态' }}
+ ▾
+
+
+
+
+
+
+
+
+ {{ saveError }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/mall/admin/setting/delivery/management/index.uvue b/pages/mall/admin/setting/delivery/management/index.uvue
index e561d2e5..ec76d810 100644
--- a/pages/mall/admin/setting/delivery/management/index.uvue
+++ b/pages/mall/admin/setting/delivery/management/index.uvue
@@ -16,9 +16,21 @@
添加时间
操作
+
+
+ 加载中...
+
+
+
+ {{ fetchError }}
+
+
+
+ 暂无配送员数据
+
- {{ item.id }}
+ {{ item.id.length > 8 ? item.id.substring(0, 8) : item.id }}
@@ -29,6 +41,7 @@
{{ item.addTime }}
+ 详情
编辑
删除
@@ -53,14 +66,40 @@
/>
+
+
+ { drawerVisible = v }"
+ />
+
+
+ { editDrawerVisible = v }"
+ @saved="fetchDrivers(currentPage, pageSize)"
+ />
+
+
+ { addDrawerVisible = v }"
+ @saved="onAddSaved"
+ />
@@ -230,4 +417,24 @@ function onDel(item: CourierItem) {
font-size: 13px;
cursor: pointer;
}
+
+.table-loading,
+.table-error,
+.table-empty {
+ padding: 40px 0;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+}
+
+.table-tip {
+ font-size: 14px;
+ color: #999;
+}
+
+.table-tip-err {
+ font-size: 14px;
+ color: #ed4014;
+}
diff --git a/pages/mall/admin/user/management/index.uvue b/pages/mall/admin/user/management/index.uvue
index d1afc0d7..541a4aac 100644
--- a/pages/mall/admin/user/management/index.uvue
+++ b/pages/mall/admin/user/management/index.uvue
@@ -88,6 +88,19 @@
+
+
+ 加载中...
+
+
+
+ {{ fetchError }}
+
+
+
+
+ 暂无用户数据
+
@@ -161,37 +174,145 @@