Files
cmautobuy/admin/templates/pdd/list.html
T
chengmaandClaude Opus 5 eb8d357f5a feat: PDD 商品页面 (#18)
打通 Client↔Admin 闭环的一环:录入 PDD 链接 → 创建采集任务 →
客户端领走去采 → 规格和价格显示在页面上。#16 写的 MarkCollecting 和
SoftDeletePddProduct 至此才有生产调用方。

链接解析严格、不做容错兜底:goods_id 上有 UNIQUE 约束,防重全靠它。
猜一个的话同一商品会存成好几行、采好几遍,规格映射还说不清指向哪一行。
短链一律拒绝,卡域名是为了拦"粘了淘宝链接"这种失误。

创建采集任务先占状态再建任务:MarkCollecting 只在 pending/failed 时成功,
同时充当"有没有人已经在采"的判断,与建任务在同一事务里,
所以并发点多次只会建出一个任务(实测 4 并发 → 1 条)。

submit.go 的 collectedData 增加 Dimensions —— 没有它就只能按 Go 的 map
遍历,而 map 无序,同一商品每次刷新"颜色/尺码"的先后都可能变。

顺带修 #16 一处缺陷:EnsurePddProduct 复活分支清空了 skus_json 却漏了
title,导致复活后状态显示"未采集"但标题还留着旧值。

审查打回一次:清理"PDD 链接唯一的录入口"这一过期说法(全库 5 处),
以及 shopee.go 里"空 → no_link"的过期 TODO —— no_link 已在 #16
从 CHECK 约束删除,照写会直接撞约束。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:27:06 +08:00

143 lines
6.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "pdd/list"}}
{{template "header" .}}
{{/* 三段式布局的第一段:顶部工具条。
和另外四个页面保持一致,见 docs/admin/05-ui-specification.md §2。 */}}
<div class="toolbar">
<button type="button" data-modal-open="create-modal">创建</button>
{{/* 「创建采集任务」和「删除」用的是**同一个表单**,靠按钮上的
formaction 决定提交到哪个地址。这样表格里的勾选框只需要挂一份,
不用为每个操作各复制一套(复制出来的两套迟早会不同步)。 */}}
<form id="pdd-form" method="post" action="/pdd/collect" hidden></form>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" form="pdd-form">
{{/* 把当前筛选一起带过去,操作完还停在原来的筛选上 */}}
<input type="hidden" name="status" value="{{.StatusFilter}}" form="pdd-form">
<input type="hidden" name="q" value="{{.Keyword}}" form="pdd-form">
{{/* 文案是「创建采集任务」不是「采集」:点完只是排了个队,
真正去手机上采要等客户端来领,可能几秒也可能几分钟 */}}
<button type="submit" form="pdd-form" formaction="/pdd/collect"
data-need-checked>创建采集任务</button>
<form class="inline grow" method="get" action="/pdd">
<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="q" value="{{.Keyword}}" placeholder="商品 ID 或链接的一部分">
<button type="submit">搜索</button>
</form>
<button type="submit" form="pdd-form" formaction="/pdd/delete" class="danger"
data-need-checked
data-confirm-delete="将删除 {n} 条记录。删除后重新创建同一链接可恢复,但采集结果会清空。"
>删除</button>
</div>
{{/* 第二段:表格。双击一行打开弹窗,见 static/js/app.js */}}
<div class="table-wrap" id="pddProductPage">
<table>
<thead>
<tr>
<th class="col-check"><input type="checkbox" data-check-all></th>
<th>商品 ID</th>
<th>标题</th>
<th>PDD 链接</th>
<th>采集状态</th>
{{/* 采到 1 个和采到 20 个差别很大:只采到 1 个通常是没点开规格面板,
说明这次采集有问题。这一列就是为了让它一眼能看出来 */}}
<th>规格数</th>
<th>采集时间</th>
<th>更新时间</th>
</tr>
</thead>
<tbody>
{{range .Rows}}
<tr data-detail-id="{{.ID}}" {{if .IsFailed}}class="row-warn"{{end}}>
<td class="col-check">
{{/* 勾选框的值是 goods_id:删除和建采集任务都按它定位。
form 属性把它挂到工具条里那个表单上 */}}
<input type="checkbox" value="{{.GoodsID}}" name="ids" form="pdd-form"
aria-label="选择商品 {{.GoodsID}}">
</td>
<td>{{.GoodsID}}</td>
<td class="truncate" title="{{.Title}}">{{.Title}}</td>
<td class="truncate">
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer" title="{{.URL}}">{{.URL}}</a>
</td>
{{/* 状态是中文文字,不能只靠颜色区分 */}}
<td>{{.StatusText}}{{if .CollectMsg}}<br><small class="missing">{{.CollectMsg}}</small>{{end}}</td>
<td>{{.SkuCountText}}</td>
<td>{{.CollectedAt}}</td>
<td>{{.UpdatedAt}}</td>
</tr>
{{else}}
{{/* 空状态要分情况:从没建过 和 筛选没结果,下一步动作完全不同 */}}
<tr class="empty">
<td colspan="8">
{{if .IsFiltered}}
当前筛选条件下没有商品。<br>
<small>换个采集状态或清空搜索词再试。<a href="/pdd">查看全部</a></small>
{{else}}
还没有 PDD 商品。<br>
<small>点左上角「创建」,粘贴一个拼多多商品链接就行,其余信息采集后自动回填。</small>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<p class="hint">
双击任意一行可以查看和编辑这个商品,包括采回来的规格和价格。
</p>
{{/* ── 创建弹窗 ─────────────────────────────────
内容是固定的(只有一个输入框),所以直接写在页面里藏着,
不像编辑弹窗那样要去服务端取。 */}}
<div class="modal-backdrop" id="create-modal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="create-modal-title">
<div class="modal-head">
<h2 id="create-modal-title">创建 PDD 商品</h2>
<button type="button" class="modal-x" data-modal-close aria-label="关闭">×</button>
</div>
<form method="post" action="/pdd/create">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" name="status" value="{{.StatusFilter}}">
<input type="hidden" name="q" value="{{.Keyword}}">
<div class="modal-body">
<div class="field">
<label for="create-url">PDD 链接</label>
<input id="create-url" type="text" name="url" required autocomplete="off"
placeholder="https://mobile.yangkeduo.com/goods.html?goods_id=737116531267">
</div>
<p class="hint">
只需要链接,标题和规格由客户端采集后自动回填。<br>
链接里必须带 <code>goods_id=</code>,短链接不行——请在拼多多 App
的商品页点「分享」→「复制链接」。
</p>
</div>
<div class="modal-foot">
<button type="button" data-modal-close>取消</button>
<button type="submit" class="primary">保存</button>
</div>
</form>
</div>
</div>
{{/* ── 编辑弹窗的壳子 ───────────────────────────
里面的内容双击行时由 /pdd/detail 返回,前端只负责放进来和显示。 */}}
<div class="modal-backdrop" id="detail-modal" data-detail-url="/pdd/detail" hidden>
<div class="modal" role="dialog" aria-modal="true" data-detail-slot>
<div class="modal-body">正在加载…</div>
</div>
</div>
{{template "footer" .}}
{{end}}