修复bug

This commit is contained in:
2026-03-20 15:24:59 +08:00
parent 14b506036c
commit 29f588a2b2
13 changed files with 321 additions and 2003 deletions

View File

@@ -281,31 +281,35 @@
❌ select() 调用没有 limit/offset 参数
```
- **正确实现(服务端分页模板)**:
```typescript
// ✅ state
const total = ref(0) // 来自服务端,不是 .length
const pagedList = computed(() => userList.value) // 无前端 slice服务端已分页
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)
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)
}
total.value = parseTotalFromContentRange(res);
};
// ✅ 翻页 handler每次翻页都调用 fetch
const handlePageChange = (p: number) => {
currentPage.value = p
fetchUsers(p, pageSize.value)
}
currentPage.value = p;
fetchUsers(p, pageSize.value);
};
onMounted(() => fetchUsers(1, pageSize.value))
onMounted(() => fetchUsers(1, pageSize.value));
```
- **强制规则**: 任何使用 Supabase 的列表页面,`total` 必须来自后端 `Content-Range` 响应头或 `res.count`,严禁使用 `computed(() => localArray.value.length)` 作为总数。
#### **原因二十七(补充):已在另一位置记录(响应式预览网格)**
@@ -321,51 +325,56 @@
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')
.from("ak_users")
.select(columns)
.page(page) // ← 产生 Range 头
.limit(pageSize) // ← 产生 Range 头
.execute()
.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, ...',
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
})
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
const cr = (res.headers?.["content-range"] ??
res.headers?.["Content-Range"]) as string | null;
if (cr) {
const slash = cr.lastIndexOf('/')
const slash = cr.lastIndexOf("/");
if (slash !== -1) {
const n = parseInt(cr.substring(slash + 1), 10)
if (!isNaN(n)) return n
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)
return offset + (res.data as UTSJSONObject[]).length;
};
total.value = parseTotal(res);
```
- **强制规则**:
1. **项目内所有列表页禁止使用 `.page().limit()` 链式调用**aksupa 或其他类似封装)。
@@ -378,28 +387,30 @@
- **现象**: 在第 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)
}
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)
emit("page-change", p);
}
}
};
```
- **父页面的双重保护(纵深防御)**:
```typescript
// 在 handlePageChange 中同样加入边界检查,防止其他调用路径绕过组件保护
const handlePageChange = (p: number) => {
if (p < 1 || p > totalPage.value) return
currentPage.value = p
fetchUsers(p, pageSize.value)
}
if (p < 1 || p > totalPage.value) return;
currentPage.value = p;
fetchUsers(p, pageSize.value);
};
```
- **推广规则**:
1. 任何分页组件,`disabled` 状态必须同时在 CSS **和**事件处理函数两层保护,视觉禁用 ≠ 逻辑禁用。