完成设置模块当中的配送员管理

This commit is contained in:
2026-03-18 16:12:37 +08:00
parent b7c8881e55
commit f1a6c18dfb
8 changed files with 2603 additions and 260 deletions

View File

@@ -1,8 +1,8 @@
---
🚧 注意
<EFBFBD> 文档维护说明
⚠ 注意:当前使用 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 协议层行为、全局组件化最佳实践),为后续开发提供了完整的故障排除和标准化指导。 🚀