修复bug
This commit is contained in:
@@ -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 **和**事件处理函数两层保护,视觉禁用 ≠ 逻辑禁用。
|
||||
|
||||
Reference in New Issue
Block a user