完成设置模块当中的配送员管理
This commit is contained in:
@@ -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 协议层行为、全局组件化最佳实践),为后续开发提供了完整的故障排除和标准化指导。 🚀
|
||||
|
||||
Reference in New Issue
Block a user