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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ /> + + + + + + + + + @@ -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 @@