feat: 区分顺运宝标题与图片交互 (#105)
This commit is contained in:
@@ -511,6 +511,49 @@ select {
|
||||
border-radius: 3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.table-title-action,
|
||||
.thumb-action {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
.table-title-action {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
padding: 2px 0;
|
||||
overflow: hidden;
|
||||
color: #0969da;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.table-title-action:hover { text-decoration: underline; }
|
||||
.thumb-action {
|
||||
padding: 3px;
|
||||
border-radius: 4px;
|
||||
line-height: 0;
|
||||
}
|
||||
.thumb-action:hover { background: #e8edf3; }
|
||||
.table-title-action:focus-visible,
|
||||
.thumb-action:focus-visible {
|
||||
outline: 2px solid #0969da;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.image-preview-modal { max-width: min(1120px, calc(100vw - 32px)); }
|
||||
.image-preview-body {
|
||||
display: flex;
|
||||
min-height: 160px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.image-preview-body img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: calc(100vh - 190px);
|
||||
object-fit: contain;
|
||||
}
|
||||
.image-preview-body img[hidden] { display: none; }
|
||||
.image-preview-status { margin: 0; }
|
||||
|
||||
/* ── 错误页 ───────────────────────────── */
|
||||
.error-box {
|
||||
|
||||
@@ -296,6 +296,8 @@
|
||||
var base = modal.getAttribute("data-detail-url") || "";
|
||||
|
||||
function loadDetail(id) {
|
||||
if (!modal.hidden && modal.getAttribute("data-loaded-detail-id") === id) return;
|
||||
modal.setAttribute("data-loaded-detail-id", id);
|
||||
slot.innerHTML = "<div class='modal-body'>正在加载…</div>";
|
||||
openModal(modal);
|
||||
|
||||
@@ -336,6 +338,45 @@
|
||||
});
|
||||
}
|
||||
|
||||
/* 顺运宝缩略图使用上游图片 URL。这里只在弹窗里放大展示,
|
||||
不代理、不下载图片;加载失败时保留新窗口重试入口。 */
|
||||
function setupImagePreview() {
|
||||
var modal = document.getElementById("image-preview-modal");
|
||||
if (!modal) return;
|
||||
var image = modal.querySelector("[data-image-preview-image]");
|
||||
var title = modal.querySelector("[data-image-preview-title]");
|
||||
var status = modal.querySelector("[data-image-preview-status]");
|
||||
var link = modal.querySelector("[data-image-preview-link]");
|
||||
if (!image || !title || !status || !link) return;
|
||||
|
||||
document.querySelectorAll("[data-image-preview-url]").forEach(function (button) {
|
||||
button.addEventListener("click", function () {
|
||||
var url = button.getAttribute("data-image-preview-url") || "";
|
||||
var productTitle = button.getAttribute("data-image-preview-title") || "商品";
|
||||
title.textContent = productTitle + " · 原图";
|
||||
link.href = url;
|
||||
image.alt = productTitle + " 原图";
|
||||
image.hidden = true;
|
||||
status.hidden = false;
|
||||
status.classList.remove("missing");
|
||||
status.textContent = "正在加载原图…";
|
||||
openModal(modal);
|
||||
|
||||
image.onload = function () {
|
||||
status.hidden = true;
|
||||
image.hidden = false;
|
||||
};
|
||||
image.onerror = function () {
|
||||
image.hidden = true;
|
||||
status.hidden = false;
|
||||
status.classList.add("missing");
|
||||
status.textContent = "原图加载失败,请在新窗口打开或稍后重试。";
|
||||
};
|
||||
image.src = url;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ── 顺运宝登录弹窗:验证码"换一张" ─────────────
|
||||
图片本身、以及旁边的"换一张"按钮,点了都重新请求验证码接口。
|
||||
`[必须]` 每次请求带一个新的时间戳查询参数,绕开浏览器缓存——
|
||||
@@ -446,6 +487,7 @@
|
||||
setupUploadFeedback();
|
||||
setupModals();
|
||||
setupRowDetail();
|
||||
setupImagePreview();
|
||||
setupCaptchaRefresh();
|
||||
setupSybSyncFeedback();
|
||||
setupSybHistoryRefresh();
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cmautobuy/admin/service"
|
||||
)
|
||||
|
||||
func TestSybListTemplate_区分标题图片与阶段动作(t *testing.T) {
|
||||
tmpl, err := template.ParseFS(templateFS, "templates/*/*.html")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows := []service.SybOrderView{{
|
||||
SybID: "SYB-READY", OrderNo: "ORDER-READY", Title: "可采购商品",
|
||||
ImageURL: "https://example.test/original.jpg", Stage: service.SybStagePurchaseReady,
|
||||
StageText: "可创建采购任务", StageHelp: "规格映射有效", ActionText: "核对规格",
|
||||
CanPurchase: true, DefaultMaxPrice: "39.90",
|
||||
}}
|
||||
var output bytes.Buffer
|
||||
data := map[string]any{
|
||||
"Title": "顺运宝数据", "Active": "syb", "CSRFToken": "test-csrf",
|
||||
"Rows": rows, "DetailURL": "/syb/detail", "StageOptions": service.SybStageOptions(),
|
||||
}
|
||||
if err := tmpl.ExecuteTemplate(&output, "syb/list", data); err != nil {
|
||||
t.Fatalf("渲染顺运宝列表失败: %v", err)
|
||||
}
|
||||
body := output.String()
|
||||
for _, want := range []string{
|
||||
`class="table-title-action" data-detail-open-id="SYB-READY"`,
|
||||
`data-image-preview-url="https://example.test/original.jpg"`,
|
||||
`aria-label="查看 可采购商品 的原图"`,
|
||||
`id="image-preview-modal"`,
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("顺运宝列表缺少 %q", want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(body, `<tr data-detail-id=`) {
|
||||
t.Error("顺运宝列表不应继续使用含义模糊的整行双击入口")
|
||||
}
|
||||
}
|
||||
@@ -101,20 +101,30 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Rows}}
|
||||
<tr data-detail-id="{{.SybID}}" {{if .NeedsAttention}}class="row-warn"{{end}}>
|
||||
<tr {{if .NeedsAttention}}class="row-warn"{{end}}>
|
||||
<td class="col-check">
|
||||
<input type="checkbox" value="{{.SybID}}" name="ids" {{if not .CanPurchase}}disabled{{end}}
|
||||
aria-label="选择货运单明细 {{.SybID}}">
|
||||
</td>
|
||||
<td>{{.SybID}}</td>
|
||||
<td>{{.OrderNo}}</td>
|
||||
<td class="truncate" title="{{.Title}}">{{.Title}}</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" title="{{.ProductSpec}}">{{.ProductSpec}}</td>
|
||||
<td>{{.ShopeeGoodsID}}</td>
|
||||
<td>{{.Quantity}}</td>
|
||||
<td>{{.PriceText}}</td>
|
||||
<td>
|
||||
{{if .ImageURL}}<img src="{{.ImageURL}}" alt="" class="thumb">{{else}}—{{end}}
|
||||
{{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><span class="status-pill">{{.StageText}}</span><br><small>{{.StageHelp}}</small></td>
|
||||
<td><button type="button" data-detail-open-id="{{.SybID}}">{{.ActionText}}</button></td>
|
||||
@@ -201,9 +211,28 @@
|
||||
</div>
|
||||
|
||||
<p class="hint">
|
||||
每行只显示当前最需要完成的下一步;双击行或点击“下一步”按钮可以打开处理详情。
|
||||
每行只显示当前最需要完成的下一步;点击商品标题进入处理详情,点击图片查看原图。
|
||||
</p>
|
||||
|
||||
<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}}" hidden>
|
||||
<div class="modal modal-wide" role="dialog" aria-modal="true" data-detail-slot>
|
||||
<div class="modal-body">正在加载…</div>
|
||||
|
||||
@@ -499,15 +499,17 @@ HTML 片段,只替换弹窗内部的表格、总数和分页。主货运单表
|
||||
|
||||
- 一行对应顺运宝一张货运单的**一个商品明细**,不是一张货运单——一张货运单
|
||||
可以有多个商品,各占一行。
|
||||
- 图片显示小缩略图。`[必须]` 存 URL,不要把图片塞进数据库。
|
||||
- 图片显示小缩略图。点击缩略图在弹窗中按比例查看顺运宝原图,并提供新窗口打开入口;
|
||||
图片加载失败时显示可读提示。`[必须]` 只存 URL,不代理、缓存或把图片塞进数据库。
|
||||
- 处理阶段是按当前关联事实**算出来的**,不是存一份容易过期的状态字段。阶段至少区分:
|
||||
采购数据异常(顺运宝未提供规格)、未关联 PDD、PDD 待采集、采集中、
|
||||
采集失败、规格待匹配、可采购和已创建任务。
|
||||
- 每行显示一个有文字的“下一步”按钮;双击行和按钮打开同一个服务端渲染处理弹窗。
|
||||
- 每行显示一个有文字的“下一步”按钮。商品标题是服务端渲染处理弹窗的明确入口,
|
||||
支持鼠标和键盘;双击其他普通单元格不执行动作。图片入口只负责查看原图,不能打开处理详情。
|
||||
- 规格原文为空时不显示匹配表单,明确提示“顺运宝未提供规格”,并禁止建立映射或创建采购任务。
|
||||
- 完整货运单 JSON 不作为列显示,落在 `syb_data` 里,供后续排查用。
|
||||
|
||||
### 6.3 采购处理与规格匹配弹窗(双击行或点击下一步打开)
|
||||
### 6.3 采购处理与规格匹配弹窗(点击商品标题或处理类下一步打开)
|
||||
|
||||
弹窗按当前处理阶段渐进展示。它直接以顺运宝规格原文及其 `spec_key`
|
||||
连接当前 PDD 商品选项,不再经过蝦皮 SKU 确认步骤。PDD 关联、采集和规格映射使用同一个弹窗完成。
|
||||
@@ -567,6 +569,7 @@ HTML 片段,只替换弹窗内部的表格、总数和分页。主货运单表
|
||||
`[必须]` 创建前弹出确认框,让操作员选**分配给哪个客户端**,并确认价格上限。
|
||||
价格上限默认从当前映射的 PDD 规格采集价带出,可改,**不允许为空**。
|
||||
|
||||
|
||||
## 7. 采集采购页
|
||||
|
||||
`tasks` 一张表用 `task_type` 区分采集和采购,本页把两种任务放在同一个
|
||||
|
||||
Reference in New Issue
Block a user