Files
cmautobuy/admin/templates/shopee/list.html
T
chengmaandClaude Opus 5 edfe39cc35 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>
2026-08-08 11:40:07 +08:00

143 lines
5.7 KiB
HTML

{{define "shopee/list"}}
{{template "header" .}}
{{/* ── 第一段:顶部工具条 ────────────────────────────── */}}
<div class="toolbar">
<form class="inline" method="post" action="/shopee/import" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="file" name="file" accept=".xlsx" required>
<button type="submit">导入 Excel</button>
</form>
<form class="inline" method="post" action="/shopee/collect">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<button type="submit" data-need-checked>批量采集</button>
</form>
<form class="inline grow" method="get" action="/shopee">
{{/* 状态筛选是刚需(找待补规格 / 找没填链接的),不是锦上添花,
见 docs/admin/05-ui-specification.md §4.1、工单 #43。
放在关键词前面:操作员大多是"看某一类"而不是"查某个 ID"。 */}}
<label for="status">状态</label>
<select id="status" name="status">
{{range .StatusOptions}}
<option value="{{.Value}}" {{if eq .Value $.StatusFilter}}selected{{end}}>{{.Text}}</option>
{{end}}
</select>
<label for="q">商品 ID</label>
<input id="q" type="text" name="goods_id" value="{{.Keyword}}" placeholder="商品 ID">
<button type="submit">搜索</button>
</form>
<form class="inline" method="post" action="/shopee/delete"
data-confirm-delete>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<button type="submit" class="danger" data-need-checked>删除</button>
</form>
</div>
{{/* ── 第二段:带勾选的表格 ──────────────────────────── */}}
{{/* 一个商品一行(不是一个 SKU 一行),见工单 #41。
双击行打开详情弹窗,见 static/js/app.js。 */}}
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="col-check"><input type="checkbox" data-check-all></th>
<th>商品 ID</th>
<th>商品名称</th>
<th>颜色</th>
<th>尺码</th>
{{/* SKU 数是这个商品报表里实际出现过的规格条数。
`[必须]` 必须单独显示——颜色数 × 尺码数经常大于它(蝦皮报表只含
有销售成绩的 SKU),只看前两列会让人误以为要匹配的规格更多,见 #41。 */}}
<th>SKU</th>
{{/* `[必须]` 待补 > 0 说明该商品有 SKU 解析失败,整行标黄(.row-warn)。
聚合成商品级之后,"部分失败"的行光看数字是正常的,
不加这一列坏数据就会被悄悄吃掉,见 #41。 */}}
<th>待补</th>
<th>PDD 链接</th>
<th>采集状态</th>
<th>更新时间</th>
</tr>
</thead>
<tbody>
{{range .Rows}}
<tr data-detail-id="{{.GoodsID}}"{{if gt .PendingCount 0}} class="row-warn"{{end}}>
<td class="col-check"><input type="checkbox" name="ids" value="{{.GoodsID}}"></td>
<td>{{.GoodsID}}</td>
{{/* 商品名称列收窄,用独立的 .col-title 类设 max-width(具体数值见 app.css),
不用 flex——表格单元格和工具条的 .search-narrow 那套不一样,见 #41。
title 属性让悬停能看完整标题,收窄之后必须留着。 */}}
<td class="truncate col-title" title="{{.Title}}">{{.Title}}</td>
<td>{{.ColorCount}}</td>
<td>{{.SizeCount}}</td>
<td>{{.SKUCount}}</td>
<td>{{.PendingCount}}</td>
<td{{if .PddMissing}} class="missing"{{end}}>{{.PddURL}}</td>
<td>{{.StatusText}}</td>
<td>{{.UpdatedAt}}</td>
</tr>
{{else}}
<tr class="empty">
<td colspan="10">
{{if .IsFiltered}}
没有匹配的数据,换个商品 ID 或商品名称试试。
{{else}}
还没有数据,点左上角「导入 Excel」开始。<br>
<small>样本文件不在仓库里,需向项目负责人索取,放到 raw_data/ 下。</small>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<p class="hint">
双击任意一行可以查看这个商品的完整规格表。
</p>
{{/* ── 导入失败行:全部列出来,不折叠、不只显示条数 ──── */}}
{{if .Failures}}
<div class="table-wrap">
<p>本次导入有 {{len .Failures}} 行解析失败,规格原文已按原样保存,需人工补颜色/尺码/建议:</p>
<table>
<thead>
<tr>
<th>行号</th>
<th>规格原文</th>
<th>原因</th>
</tr>
</thead>
<tbody>
{{range .Failures}}
<tr>
<td>{{.Row}}</td>
<td>{{.Raw}}</td>
<td>{{.Reason}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
{{/* ── 详情弹窗的壳子 ───────────────────────────
里面的内容双击行时由 /shopee/detail 返回,前端只负责放进来和显示,
复用 #18 的机制(data-detail-url / data-detail-slot / data-detail-id),
app.js 不需要改,见工单 #41。
这个弹窗**只读**:商品信息 + 完整规格表,没有保存按钮。
从蝦皮商品出发录入 PDD 链接、发起采集的入口(含 ShopeeSave/
ShopeeCollect 的实现)由后续的「蝦皮↔PDD 关联入口」工单负责,
本工单不做,ShopeeSave/ShopeeDelete/ShopeeCollect 仍是 501。 */}}
<div class="modal-backdrop" id="detail-modal" data-detail-url="/shopee/detail" hidden>
<div class="modal" role="dialog" aria-modal="true" data-detail-slot>
<div class="modal-body">正在加载…</div>
</div>
</div>
{{template "footer" .}}
{{end}}