Files
cmautobuy/admin/templates/syb/list.html
T

433 lines
22 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 "syb/list"}}
{{template "header" .}}
<div class="toolbar syb-toolbar">
<form class="inline sync-range-form" method="post" action="/syb/sync" data-syb-sync-form>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" name="order_no" value="{{.Keyword}}">
<input type="hidden" name="shop" value="{{.ShopFilter}}">
<input type="hidden" name="stage" value="{{.StageFilter}}">
<input type="hidden" name="page" value="{{.CurrentPage}}">
<label class="visually-hidden" for="sync-date-from">开始日期</label>
<input id="sync-date-from" type="date" name="date_from"
value="{{.SyncDateFrom}}" max="{{.Today}}" required>
<span class="syb-date-separator" aria-hidden="true">-</span>
<label class="visually-hidden" for="sync-date-to">结束日期</label>
<input id="sync-date-to" type="date" name="date_to"
value="{{.SyncDateTo}}" max="{{.Today}}" required>
<button type="submit" class="primary" data-syb-sync-submit
{{if .SyncRunning}}disabled aria-busy="true" title="同步正在进行,请耐心等待"{{end}}>
{{if .SyncRunning}}同步中…{{else}}同步{{end}}
</button>
</form>
<button type="button" data-modal-open="sync-history-modal">同步记录</button>
<form id="syb-ai-match-form" method="post" action="/syb/ai-match" hidden>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" name="order_no" value="{{.Keyword}}">
<input type="hidden" name="shop" value="{{.ShopFilter}}">
<input type="hidden" name="stage" value="{{.StageFilter}}">
<input type="hidden" name="page" value="{{.CurrentPage}}">
<input type="hidden" name="date_from" value="{{.SyncDateFrom}}">
<input type="hidden" name="date_to" value="{{.SyncDateTo}}">
</form>
<button type="button" data-modal-open="syb-ai-match-create-modal" data-ai-match-open
data-need-checked="ai" aria-label="批量 AI 规格匹配">AI匹配</button>
{{if .LatestAIMatchBatchURL}}<a class="button-link" href="{{.LatestAIMatchBatchURL}}"
aria-label="查看 AI 规格匹配记录">匹配记录</a>{{end}}
<form id="syb-collect-form" method="post" action="/syb/collect-pdd-batch" hidden></form>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" form="syb-collect-form">
<input type="hidden" name="order_no" value="{{.Keyword}}" form="syb-collect-form">
<input type="hidden" name="shop" value="{{.ShopFilter}}" form="syb-collect-form">
<input type="hidden" name="stage" value="{{.StageFilter}}" form="syb-collect-form">
<input type="hidden" name="page" value="{{.CurrentPage}}" form="syb-collect-form">
<button type="button" data-modal-open="syb-collect-modal" data-collect-open
data-collect-form="syb-collect-form" data-collect-action="collect"
data-need-checked="collect" aria-label="创建采集任务">创建采集</button>
<button type="button" data-need-checked="purchase" data-modal-open="purchase-modal"
data-purchase-open aria-label="创建采购任务">创建采购</button>
<form class="inline grow syb-filter-form" method="get" action="/syb">
<label for="stage">处理阶段</label>
<select id="stage" name="stage">
{{range .StageOptions}}
<option value="{{.Value}}" {{if eq .Value $.StageFilter}}selected{{end}}>{{.Text}}</option>
{{end}}
</select>
<label for="shop">店铺</label>
<input id="shop" type="text" name="shop" value="{{.ShopFilter}}"
class="syb-search-input" placeholder="店铺名">
<label for="q">订单号(可多条)</label>
<textarea id="q" name="order_no" rows="1" data-syb-order-search
class="syb-search-input syb-order-search-input" placeholder="多个可直接粘贴" maxlength="4000"
title="支持中英文逗号、分号、空格、Tab 或换行分隔,最多 100 个订单号"
{{if .OrderNoError}}aria-invalid="true" aria-describedby="order-no-error"{{end}}>{{.Keyword}}</textarea>
<a class="button-link" href="/syb?date_from={{urlquery .SyncDateFrom}}&date_to={{urlquery .SyncDateTo}}">清除</a>
<button type="submit">搜索</button>
</form>
<form class="inline" method="post" action="/syb/delete" data-confirm-delete>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" name="order_no" value="{{.Keyword}}">
<input type="hidden" name="shop" value="{{.ShopFilter}}">
<input type="hidden" name="stage" value="{{.StageFilter}}">
<input type="hidden" name="page" value="{{.CurrentPage}}">
<button type="submit" class="danger" data-need-checked>删除</button>
</form>
</div>
{{if .OrderNoError}}<p id="order-no-error" class="missing filter-feedback" role="alert">{{.OrderNoError}}</p>{{end}}
{{if .OrderNoFeedback}}<p class="hint filter-feedback" role="status">{{.OrderNoFeedback}}</p>{{end}}
{{if .RangeError}}<p class="missing sync-range-message" role="alert">{{.RangeError}}</p>{{end}}
{{if .RangeWarning}}<p class="hint sync-range-message">{{.RangeWarning}}</p>{{end}}
<div class="modal-backdrop" id="sync-history-modal" {{if not .NeedSyncHistory}}hidden{{end}}>
<div class="modal modal-wide" role="dialog" aria-modal="true"
aria-labelledby="sync-history-modal-title">
<div class="modal-head">
<h2 id="sync-history-modal-title">同步记录</h2>
<button type="button" class="modal-x" data-modal-close aria-label="关闭">×</button>
</div>
<div data-syb-history-slot>
{{template "syb/history_partial" .}}
</div>
</div>
</div>
{{if .ConfigProblem}}
<p class="missing">
顺运宝配置有问题:{{.ConfigProblem}}
</p>
{{end}}
{{/* 未登录只给一行提示,**不弹窗**(工单 #48)。
打开页面纯粹是看数据,不该触发任何对外部系统的动作;
真正会登录的只有「同步」那一个按钮。
用 .hint 不用 .missing——这是状态说明不是错误,
红色留给「PDD 链接未填写」那种要立刻处理的。 */}}
{{if .LoginHint}}
<p class="hint">{{.LoginHint}}</p>
{{end}}
<div class="table-wrap syb-table-wrap">
<table>
<thead>
<tr>
<th class="col-check"><input type="checkbox" data-check-all></th>
<th>订单号</th>
<th class="syb-col-shop">店铺</th>
<th>商品标题</th>
<th class="syb-col-spec">规格</th>
<th>蝦皮商品 ID</th>
<th>数量</th>
<th>价格</th>
<th>图片</th>
<th>匹配来源</th>
<th>下一步</th>
<th>更新时间</th>
</tr>
</thead>
<tbody>
{{range .Rows}}
<tr {{if .NeedsAttention}}class="row-warn"{{end}}>
<td class="col-check">
<span title="{{.SelectionHint}}">
<input type="checkbox" value="{{.SybID}}" name="ids"
data-select-action="{{.SelectionActions}}"
{{if .CanCollect}}form="syb-collect-form"{{end}}
{{if not .SelectionActions}}disabled{{end}}
aria-label="选择货运单明细 {{.SybID}}:{{.SelectionHint}}">
</span>
</td>
<td>{{.OrderNo}}</td>
<td class="truncate syb-col-shop" title="{{.ShopName}}">{{if .ShopName}}{{.ShopName}}{{else}}—{{end}}</td>
<td class="truncate" title="{{.Title}}">
<button type="button" class="table-title-action" data-detail-open-id="{{.SybID}}"
title="打开商品处理详情">{{.Title}}</button>
</td>
<td class="truncate syb-col-spec" title="{{.ProductSpec}}">{{.ProductSpec}}</td>
<td>{{.ShopeeGoodsID}}</td>
<td>{{.Quantity}}</td>
<td>{{.PriceText}}</td>
<td>
{{if .ImageURL}}
<button type="button" class="thumb-action" data-image-preview-url="{{.ImageURL}}"
data-image-preview-title="{{.Title}}" title="查看商品原图"
aria-label="查看 {{.Title}} 的原图">
<img src="{{.ImageURL}}" alt="{{.Title}} 缩略图" class="thumb"
width="32" height="32" loading="lazy">
</button>
{{else}}—{{end}}
</td>
<td>{{if .MappingSourceText}}<span class="status-pill mapping-source {{.MappingSourceClass}}" title="{{.MappingSourceHelp}}">{{.MappingSourceText}}</span>{{else}}—{{end}}</td>
<td>
{{if eq .Stage "purchase_ready"}}
<button type="button" data-modal-open="purchase-modal" data-purchase-open
data-purchase-id="{{.SybID}}" title="{{.StageHelp}}">{{.ActionText}}</button>
{{else if or (eq .Stage "task_created") (eq .Stage "purchase_completed") (eq .Stage "purchase_review")}}
<a class="button-link" href="/tasks?type=purchase&q={{urlquery .OrderNo}}"
title="{{.StageHelp}}">{{.ActionText}}</a>
{{else}}
<button type="button" data-detail-open-id="{{.SybID}}"
title="{{.StageHelp}}">{{.ActionText}}</button>
{{end}}
</td>
<td>{{.UpdatedAt}}</td>
</tr>
{{else}}
<tr class="empty">
<td colspan="12">
{{if .IsFiltered}}
当前筛选条件下没有货运单明细。<br>
<small>换个店铺、订单号或清空筛选条件再试。<a href="/syb">查看全部</a></small>
{{else}}
还没有货运单明细。<br>
<small>点上方「同步」从顺运宝拉取,第一次同步需要先登录(验证码需要手工输入)。</small>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{if .AIMatchError}}
<div class="modal-backdrop" id="syb-ai-match-error-modal" data-auto-open-modal hidden>
<div class="modal" role="alertdialog" aria-modal="true"
aria-labelledby="syb-ai-match-error-title" aria-describedby="syb-ai-match-error-message">
<div class="modal-head">
<h2 id="syb-ai-match-error-title">AI 规格匹配失败</h2>
<button type="button" class="modal-x" data-modal-close aria-label="关闭">×</button>
</div>
<div class="modal-body">
<p class="missing" id="syb-ai-match-error-message">{{.AIMatchError}}</p>
<p class="hint">规格映射和采购任务都没有被改动。请按提示处理后重新勾选并匹配。</p>
</div>
<div class="modal-foot">
<button type="button" data-modal-close autofocus>关闭</button>
</div>
</div>
</div>
{{end}}
<div class="modal-backdrop" id="syb-ai-match-create-modal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="syb-ai-match-create-title">
<div class="modal-head">
<h2 id="syb-ai-match-create-title">批量 AI 规格匹配</h2>
<button type="button" class="modal-x" data-modal-close aria-label="关闭">×</button>
</div>
<div class="modal-body form-stack">
<p>将处理已选择的 <strong data-ai-match-count>0</strong> 条顺运宝商品,单批最多 100 条。</p>
<p class="hint">相同商品、SYB 规格和 PDD 上下文只调用一次;已有有效映射直接复用。条件不足或不确定的条目保留给人工处理。</p>
<p class="hint">此操作只保存规格映射,不创建采集任务、采购任务,也不会触发 Client 下单。</p>
</div>
<div class="modal-foot">
<button type="button" data-modal-close>取消</button>
<button type="submit" form="syb-ai-match-form" class="primary" data-ai-match-submit>开始匹配</button>
</div>
</div>
</div>
{{with .AIMatchBatch}}
<div class="modal-backdrop" id="syb-ai-match-result-modal" data-ai-match-batch
data-ai-match-fetch-url="{{$.AIMatchBatchFetchURL}}">
<div class="modal modal-wide ai-match-result-modal" role="dialog" aria-modal="true" aria-labelledby="syb-ai-match-result-title">
<div class="modal-head">
<h2 id="syb-ai-match-result-title">AI 规格匹配进度</h2>
<button type="button" class="modal-x" data-modal-close aria-label="关闭">×</button>
</div>
<div class="modal-body form-stack">
<p><strong data-ai-batch-status>{{.StatusText}}</strong> · {{.ProviderName}} / {{.Model}}</p>
<progress data-ai-batch-progress max="{{.Total}}" value="{{.Processed}}">{{.Processed}} / {{.Total}}</progress>
<p class="ai-match-summary" aria-live="polite">
已处理 <strong data-ai-count="processed">{{.Processed}}</strong> / <span data-ai-count="total">{{.Total}}</span>;
成功 <strong data-ai-count="success">{{.Success}}</strong>;复用 <strong data-ai-count="reused">{{.Reused}}</strong>;
待人工 <strong data-ai-count="manual">{{.Manual}}</strong>;失败 <strong data-ai-count="failed">{{.Failed}}</strong>
</p>
<p class="missing" data-ai-batch-error {{if not .ErrorMessage}}hidden{{end}}>{{.ErrorMessage}}</p>
<div class="table-wrap ai-match-items-wrap">
<table>
<thead><tr><th>明细 ID</th><th>结果</th><th>来源</th><th>置信度</th><th>原因</th></tr></thead>
<tbody data-ai-batch-items>
{{range .Items}}<tr><td>{{.SybID}}</td><td>{{.StatusText}}</td><td>{{if .SourceText}}{{.SourceText}}{{else}}—{{end}}</td><td>{{if .ConfidenceText}}{{.ConfidenceText}}{{else}}—{{end}}</td><td>{{.Message}}</td></tr>{{end}}
</tbody>
</table>
</div>
</div>
<div class="modal-foot">
<button type="button" data-ai-match-refresh>刷新进度</button>
<button type="button" data-ai-match-page-refresh>刷新列表</button>
<button type="button" data-modal-close>关闭</button>
</div>
</div>
</div>
{{end}}
<div class="modal-backdrop" id="syb-collect-modal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="syb-collect-modal-title">
<div class="modal-head">
<h2 id="syb-collect-modal-title">创建采集任务</h2>
<button type="button" class="modal-x" data-modal-close aria-label="关闭">×</button>
</div>
<div class="modal-body">
<p>将处理已选择的 <strong data-collect-count>0</strong> 条可采集货运单明细。</p>
<div class="field">
<label for="syb-collect-client">执行客户端</label>
<select id="syb-collect-client" name="client_id" form="syb-collect-form" autofocus>
<option value="">不指定(任意客户端可领取)</option>
{{range .AssignableClients}}
<option value="{{.ClientID}}">{{.Name}}({{.ClientID}},{{.Status}})</option>
{{end}}
</select>
</div>
<p class="hint">
同一 PDD 商品关联多条货运单时只创建一条任务。通常保持不指定即可;
指定后任务只等待该客户端领取。
</p>
</div>
<div class="modal-foot">
<button type="button" data-modal-close>取消</button>
<button type="submit" form="syb-collect-form" class="primary"
data-collect-submit>确认创建</button>
</div>
</div>
</div>
<div class="modal-backdrop" id="purchase-modal" hidden>
<div class="modal modal-wide" role="dialog" aria-modal="true" aria-labelledby="purchase-modal-title">
<div class="modal-head">
<h2 id="purchase-modal-title">确认创建采购任务</h2>
<button type="button" class="modal-x" data-modal-close aria-label="关闭">×</button>
</div>
<form method="post" action="/syb/create-task" data-purchase-form
data-auto-open-purchase-id="{{.OpenPurchaseSybID}}">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" name="order_no" value="{{.Keyword}}">
<input type="hidden" name="shop" value="{{.ShopFilter}}">
<input type="hidden" name="stage" value="{{.StageFilter}}">
<input type="hidden" name="page" value="{{.CurrentPage}}">
<div class="modal-body form-stack">
<div class="field">
<label for="purchase-client">执行客户端</label>
<select id="purchase-client" name="client_id" required autofocus>
<option value="">请选择执行客户端</option>
{{range .AssignableClients}}<option value="{{.ClientID}}" {{if eq $.PurchaseClientCount 1}}selected{{end}}>{{.Name}}({{.ClientID}}) — {{.Status}}</option>{{end}}
</select>
{{if eq .PurchaseClientCount 0}}<small class="missing">当前账号没有可见的客户端,请先联系管理员绑定客户端。</small>{{else}}<small>所有当前账号可见的客户端均可指派;未就绪或离线客户端会等待其就绪后领取。</small>{{end}}
</div>
<p class="hint">单价上限默认取已映射 PDD 规格的人民币采集价;订单总价上限按单价乘以顺运宝数量自动计算,不使用顺运宝的台币售价。</p>
{{range .PurchaseRows}}
<div class="purchase-confirm-row" data-purchase-row="{{.SybID}}" data-purchase-quantity="{{.Quantity}}" hidden>
<input type="hidden" name="ids" value="{{.SybID}}" disabled>
<input type="hidden" name="mapping_option_key" value="{{.MappingOptionKey}}" disabled>
<input type="hidden" name="context_version" value="{{.ContextVersion}}" disabled>
<div><strong>{{.OrderNo}}</strong> · {{.Title}} × {{.Quantity}}</div>
<div class="hint">顺运宝规格:{{.ProductSpec}}</div>
<div><strong>已选 PDD 规格:</strong>{{.MappedPddChoice}}</div>
<label for="purchase-unit-price-{{.SybID}}">选定规格的人民币单价上限(元)</label>
<input id="purchase-unit-price-{{.SybID}}" type="number" name="unit_price_limit_yuan"
min="0.01" step="0.01" value="{{.DefaultUnitPrice}}" required disabled
data-unit-price-input aria-describedby="purchase-price-help-{{.SybID}}">
<p class="hint" id="purchase-price-help-{{.SybID}}">
数量:{{.Quantity}} 件 · 订单总价上限:
<output data-total-price-output aria-live="polite">{{if .DefaultTotalPrice}}¥{{.DefaultTotalPrice}}{{else}}—{{end}}</output>
</p>
<p class="hint">提交时后端会按最新数量重新计算订单总价上限。</p>
</div>
{{end}}
<p class="missing" data-purchase-empty hidden>没有选择可采购的明细,请关闭后重新勾选。</p>
</div>
<div class="modal-foot">
<button type="button" data-modal-close>取消</button>
<button type="submit" class="primary" data-purchase-submit {{if eq .PurchaseClientCount 0}}disabled{{end}}>创建真实采购任务</button>
</div>
</form>
</div>
</div>
<div class="modal-backdrop" id="image-preview-modal" hidden>
<div class="modal image-preview-modal" role="dialog" aria-modal="true"
aria-labelledby="image-preview-title">
<div class="modal-head">
<h2 id="image-preview-title" data-image-preview-title>商品原图</h2>
<button type="button" class="modal-x" data-modal-close aria-label="关闭">×</button>
</div>
<div class="modal-body image-preview-body">
<p class="hint image-preview-status" data-image-preview-status role="status">正在加载原图…</p>
<img data-image-preview-image alt="" hidden>
</div>
<div class="modal-foot">
<a class="button-link" data-image-preview-link href="#" target="_blank"
rel="noopener noreferrer">新窗口打开原图</a>
<button type="button" data-modal-close>关闭</button>
</div>
</div>
</div>
<div class="modal-backdrop" id="detail-modal" data-detail-url="{{.DetailURL}}"
data-auto-open-detail-id="{{.OpenSybID}}" hidden>
<div class="modal modal-wide" role="dialog" aria-modal="true" data-detail-slot>
<div class="modal-body">正在加载…</div>
</div>
</div>
{{/* ── 登录弹窗 ─────────────────────────────────
会话未登录/已过期时自动打开(不加 hidden);
`[必须]` 密码不在界面上显示、也不回显到 HTML,只显示只读账号。 */}}
<div class="modal-backdrop" id="login-modal" {{if not .NeedLogin}}hidden{{end}}>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="login-modal-title">
<div class="modal-head">
<h2 id="login-modal-title">登录顺运宝</h2>
<button type="button" class="modal-x" data-modal-close aria-label="关闭">×</button>
</div>
<form method="post" action="/syb/login-and-sync">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" name="order_no" value="{{.Keyword}}">
<input type="hidden" name="shop" value="{{.ShopFilter}}">
<input type="hidden" name="stage" value="{{.StageFilter}}">
<input type="hidden" name="page" value="{{.CurrentPage}}">
{{/* 登录和 OCR 降级后必须把工具栏里已确认的日期范围原样带回来。 */}}
<input type="hidden" name="date_from" value="{{.SyncDateFrom}}">
<input type="hidden" name="date_to" value="{{.SyncDateTo}}">
<div class="modal-body">
{{/* `[必须]` 工单 #47:自动识别验证码失败/降级时要说明原因
(已尝试 N 次 / 服务不可用),操作员才知道是配置错了还是
服务挂了。ocr_url 留空时压根没尝试过自动识别,不显示这条。 */}}
{{if .NeedLoginReason}}
<p class="hint login-degrade-reason">{{.NeedLoginReason}}</p>
{{end}}
<div class="field">
<label for="login-username">账号</label>
<input id="login-username" type="text" value="{{.Username}}" readonly>
</div>
<div class="field">
<label for="login-captcha-img">验证码</label>
{{/* 图片地址带一个时间戳查询参数,确保每次打开弹窗/点"换一张"
都拿到新图,不被浏览器缓存吃掉旧的 */}}
<img id="login-captcha-img" src="/syb/captcha" alt="验证码"
class="captcha-img" data-captcha-refresh="/syb/captcha">
<button type="button" id="login-captcha-refresh">换一张</button>
</div>
<div class="field">
<label for="login-code">验证码文字</label>
<input id="login-code" type="text" name="code" required autocomplete="off"
maxlength="8" placeholder="图片里的 4 位字母数字">
</div>
<p class="hint">
账号密码取自 <code>admin/config.yaml</code>,界面上不显示密码。
</p>
</div>
<div class="modal-foot">
<button type="button" data-modal-close>取消</button>
<button type="submit" class="primary">登录并同步</button>
</div>
</form>
</div>
</div>
{{template "footer" .}}
{{end}}