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:
@@ -47,7 +47,7 @@
|
||||
见 §5.6。含糊其辞和吓唬人一样糟——两种都会让操作员不敢动手。
|
||||
- `[必须]` 删除、导入用 **POST**,不得用 GET。浏览器和插件会预取 GET 链接。
|
||||
- `[建议]` 默认按 `更新时间 DESC` 排序。
|
||||
- `[建议]` 一页 50 条,超过分页。搜索走数据库,不要一次查出来在内存里过滤。
|
||||
- `[必须]` 每页 **20** 条,超过分页,见 §3.2。搜索走数据库,不要一次查出来在内存里过滤。
|
||||
- `[必须]` 长文本(商品标题)截断显示,鼠标悬停给完整内容。
|
||||
- `[必须]` 空状态要分情况,文案不能都是"暂无数据":
|
||||
- 从没导入过 → "还没有数据,点左上角『导入』开始"
|
||||
@@ -72,12 +72,50 @@
|
||||
`[建议]` placeholder 要短。收到 30% 之后长文案显示不全——
|
||||
真装不下就缩短文案,**不要把宽度调回去**。
|
||||
|
||||
### 3.2 分页
|
||||
|
||||
`[必须]` 五个模块**统一每页 20 条**(原来这里写的是"建议一页 50 条",
|
||||
已按工单 #43 改为 20 并升级为 `[必须]`)——20 行在 1366×768 上正好一屏,
|
||||
不用滚动就能看完;50 条要滚动,操作员容易漏看最下面几行。
|
||||
|
||||
蝦皮数据页(§4)是全项目第一个做分页的页面,本节是它定下的通用规则,
|
||||
**其余四个页面照抄**,不要各写一套:
|
||||
|
||||
- `[必须]` 分页用 SQL 的 `LIMIT ? OFFSET ?`,**不得**把全量查出来在 Go 里
|
||||
切片——这正是蝦皮数据页改之前一次吐 3.4MB HTML 的成因(实测 5195 行)。
|
||||
- `[必须]` 总数用**单独的 `COUNT(*)`** 查询,并且和列表查询**共用同一套
|
||||
筛选条件拼装函数**。分开写两份 `WHERE` 迟早会漏改一处,页码跟着算错,
|
||||
而且不会报错、不容易发现(#19 已经踩过一次)。
|
||||
- `[必须]` 页码参数是 `?page=N`,从 **1** 开始,不是 0——给操作员看的东西
|
||||
不要 0-based。
|
||||
- `[必须]` 越界要兜住:
|
||||
- `page` 非数字、`0`、负数 → 当作第 1 页;
|
||||
- `page` 超过总页数 → 显示**最后一页**(有数据的那页),不是空表格;
|
||||
- 总数为 0 → 显示"第 1/1 页",不出现"第 1/0 页"。
|
||||
- `[必须]` 底部状态条显示**全量总数**(当前筛选条件下的总数),不是本页
|
||||
行数——"共 20 个商品"会让操作员以为总共就 20 个。筛选后显示筛选结果的
|
||||
总数,例如"待补规格:6 个商品 · 第 1/1 页",不是本页凑出来的数字。
|
||||
- `[必须]` 翻页时**保留当前筛选和关键词**——用查询参数原样带过去,
|
||||
丢了的话操作员翻到第二页筛选就没了,会以为数据变了。
|
||||
- `[必须]` 分页控件用 `<a href>`,**不要用 JS**。这是纯 GET 导航,
|
||||
浏览器的前进后退和书签都该正常工作。
|
||||
- `[必须]` 第一页时"首页 / 上一页"禁用,最后一页时"下一页 / 末页"禁用;
|
||||
禁用要有**视觉 + 语义**区分(不可点的元素不要还是 `<a>`),不能只靠颜色。
|
||||
- `[建议]` 不做 `1 2 3 … N` 这种页码列表。页数一多列出来没有意义,
|
||||
操作员应该靠筛选定位,不是靠翻页数页码。
|
||||
|
||||
公共实现放在 `admin/service/pagination.go`(`PageSize` 常量、
|
||||
`ParsePage` / `ClampPage` / `TotalPages` / `NewPaginationView`)和
|
||||
`admin/templates/partials/footer.html`(分页控件,`Pagination` 字段
|
||||
为空时不渲染,不影响还没做分页的页面),后面四个页面直接复用,
|
||||
不要各写一份。
|
||||
|
||||
## 4. 蝦皮数据页
|
||||
|
||||
### 4.1 工具条
|
||||
|
||||
```text
|
||||
[导入 Excel] [批量采集] 商品ID [________] [搜索] [删除]
|
||||
[导入 Excel] [批量采集] 状态[全部▾] 商品ID [________] [搜索] [删除]
|
||||
```
|
||||
|
||||
- **导入 Excel**:选文件 → POST 上传 → 显示结果统计
|
||||
@@ -85,6 +123,21 @@
|
||||
`[必须]` 失败行要列出行号和原因,不能静默跳过。
|
||||
- **批量采集**:勾选若干行 → 服务端**按商品去重**后建采集任务。
|
||||
`[必须]` 已是"采集中"的商品跳过,并在结果里说明跳过了几个。
|
||||
- **状态筛选**(`[必须]`,工单 #43):全部 / 待补规格 / 未填 PDD 链接 / 已填链接。
|
||||
这个筛选是**刚需**,不是锦上添花——本页主要用途是维护
|
||||
(找出需要补规格的商品、找出还没关联 PDD 链接的商品),只靠商品 ID
|
||||
搜索的话,操作员得先知道 ID 才能搜,而"哪些商品需要处理"恰恰是
|
||||
不知道 ID 的时候才要问的。实测样本 5195 个商品里只有 6 个待补规格,
|
||||
只做分页(260 页)翻不出来。
|
||||
- 「待补规格」:`EXISTS (SELECT 1 FROM shopee_skus WHERE goods_id = 该商品 AND parse_ok = 0)`。
|
||||
`[必须]` 用 `EXISTS`,不用 `JOIN` + `DISTINCT`——一个商品有多个失败
|
||||
SKU 时 `JOIN` 会出重复行,`DISTINCT` 又会让分页的 `LIMIT/OFFSET`
|
||||
行为难推理。
|
||||
- 「未填 PDD 链接」:`pdd_goods_url IS NULL OR pdd_goods_url = ''`。
|
||||
- 「已填链接」:`pdd_goods_url IS NOT NULL AND pdd_goods_url <> ''`。
|
||||
- `[必须]` 筛选参数认不出来的一律当"全部",不报错——地址栏是用户可以
|
||||
随便改的。
|
||||
- `[必须]` 筛选与关键词搜索可以叠加,翻页时都要保留,见 §3.2。
|
||||
|
||||
### 4.2 表格列
|
||||
|
||||
@@ -145,6 +198,23 @@
|
||||
|
||||
新增的行 `is_manual = 1`,`[必须]` 后续导入**不得删除**它们。
|
||||
|
||||
### 4.5 底部状态条
|
||||
|
||||
```text
|
||||
共 5195 个商品 · 第 1/260 页 [首页] [上一页] [下一页] [末页]
|
||||
```
|
||||
|
||||
筛选后(例如状态选了"待补规格"):
|
||||
|
||||
```text
|
||||
待补规格:6 个商品 · 第 1/1 页 [首页] [上一页] [下一页] [末页]
|
||||
```
|
||||
|
||||
`[必须]` 分页与状态条的通用规则见 §3.2,不在这里重复。这里只强调
|
||||
蝦皮页特有的一点:导入完成后状态条**显示导入结果**("导入完成:N 个商品 /
|
||||
M 个 SKU,K 行解析失败"),覆盖掉正常的统计文案,并且回到第 1 页、清空筛选——
|
||||
刚导入的数据从第一页就能看到,沿用旧筛选反而可能让人以为导入没生效。
|
||||
|
||||
## 5. PDD 商品页
|
||||
|
||||
页面 `objectName` 为 `pddProductPage`,路由 `/pdd`。
|
||||
|
||||
Reference in New Issue
Block a user