feat: 蝦皮数据页分页与状态筛选 (#43)

导入真实样本后 /shopee 一次吐 3.4MB / 5195 行。但只加分页会把问题从
「5195 行糊在一起」变成「260 页里藏着 6 个」——那 6 个待补规格的商品
仍然找不到。所以分页和状态筛选一起做。

HTML 3.4MB → 16.7KB。

状态条显示全量而不是本页:「共 5195 个商品 · 第 1/260 页」。
显示「共 20 个商品」会让操作员以为总共就 20 个。筛选后显示筛选结果
总数:「待补规格:6 个商品」。

列表查询和 COUNT 共用同一套筛选条件拼装。分开写两份 WHERE,迟早
有天忘了给 COUNT 也加条件,页码算错而且没人发现(#19 踩过一次)。

page 越界兜到最后一页而不是显示空表格——空表格会让操作员以为数据没了。
总数为 0 时显示「第 1/1 页」,不出现「第 1/0 页」。

「待补规格」用 EXISTS 不用 JOIN+DISTINCT:一个商品有多个失败 SKU 时
JOIN 会出重复行,DISTINCT 又让 LIMIT/OFFSET 的行为难推理。

分页控件是 <a href> 纯 GET,浏览器前进后退和书签都正常。首末页用
<span class="disabled"> 禁用,语义上不再是链接,不只靠颜色区分。

这是全项目第一个分页页面,通用逻辑单独放 service/pagination.go 供
后面四页复用,规则写进 05 §3.2 而不是蝦皮页那一节(#34 踩过这个错)。
05 §3 的每页条数从「建议 50」改为「统一 20」并写明理由。

实现踩到 html/template 的 URL 上下文转义:夹在字面量 & 中间的动态内容
会被整体当成一个参数值转义,?/= 变成 %3F/%3D 让链接失效。改为在 Go 里
把整段 URL 拼好,模板作为单个 pipeline 输出,并加了回归测试。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
chengma
2026-08-08 11:40:07 +08:00
co-authored by Claude Opus 5
parent 033bef0614
commit edfe39cc35
11 changed files with 734 additions and 42 deletions
+30 -2
View File
@@ -1,8 +1,36 @@
{{define "footer"}}
</main>
{{/* 底部状态条:三段式布局的第三段,五个页面都有 */}}
<footer class="statusbar">{{.Status}}</footer>
{{/* 底部状态条:三段式布局的第三段,五个页面都有。
Pagination 是可选的(只有做了分页的页面才传),
没做分页的页面不受影响,见 docs/admin/05-ui-specification.md §3.2。
`[必须]` 每个链接的 href 必须整体来自 .Pagination.XxxURL 这一个 pipeline,
不要在这里把筛选参数和 page 分开拼接(比如 "?{{.BaseQuery}}&page=...")——
html/template 会把夹在字面量 & 中间的动态内容当成单个参数值整体转义,
? 和 = 变成 %3F/%3D,链接直接失效。已经在 service.NewPaginationView
里把完整链接拼好,这里只管原样输出。 */}}
<footer class="statusbar">
<span>{{.Status}}</span>
{{if .Pagination}}
<nav class="pagination" aria-label="分页">
{{if .Pagination.HasPrev}}
<a href="{{.Pagination.FirstURL}}">首页</a>
<a href="{{.Pagination.PrevURL}}">上一页</a>
{{else}}
<span class="disabled">首页</span>
<span class="disabled">上一页</span>
{{end}}
{{if .Pagination.HasNext}}
<a href="{{.Pagination.NextURL}}">下一页</a>
<a href="{{.Pagination.LastURL}}">末页</a>
{{else}}
<span class="disabled">下一页</span>
<span class="disabled">末页</span>
{{end}}
</nav>
{{end}}
</footer>
<script src="/static/js/app.js"></script>
</body>