feat: 区分顺运宝标题与图片交互 (#105)
This commit is contained in:
@@ -511,6 +511,49 @@ select {
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
vertical-align: middle;
|
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 {
|
.error-box {
|
||||||
|
|||||||
@@ -296,6 +296,8 @@
|
|||||||
var base = modal.getAttribute("data-detail-url") || "";
|
var base = modal.getAttribute("data-detail-url") || "";
|
||||||
|
|
||||||
function loadDetail(id) {
|
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>";
|
slot.innerHTML = "<div class='modal-body'>正在加载…</div>";
|
||||||
openModal(modal);
|
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();
|
setupUploadFeedback();
|
||||||
setupModals();
|
setupModals();
|
||||||
setupRowDetail();
|
setupRowDetail();
|
||||||
|
setupImagePreview();
|
||||||
setupCaptchaRefresh();
|
setupCaptchaRefresh();
|
||||||
setupSybSyncFeedback();
|
setupSybSyncFeedback();
|
||||||
setupSybHistoryRefresh();
|
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>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range .Rows}}
|
{{range .Rows}}
|
||||||
<tr data-detail-id="{{.SybID}}" {{if .NeedsAttention}}class="row-warn"{{end}}>
|
<tr {{if .NeedsAttention}}class="row-warn"{{end}}>
|
||||||
<td class="col-check">
|
<td class="col-check">
|
||||||
<input type="checkbox" value="{{.SybID}}" name="ids" {{if not .CanPurchase}}disabled{{end}}
|
<input type="checkbox" value="{{.SybID}}" name="ids" {{if not .CanPurchase}}disabled{{end}}
|
||||||
aria-label="选择货运单明细 {{.SybID}}">
|
aria-label="选择货运单明细 {{.SybID}}">
|
||||||
</td>
|
</td>
|
||||||
<td>{{.SybID}}</td>
|
<td>{{.SybID}}</td>
|
||||||
<td>{{.OrderNo}}</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 class="truncate" title="{{.ProductSpec}}">{{.ProductSpec}}</td>
|
||||||
<td>{{.ShopeeGoodsID}}</td>
|
<td>{{.ShopeeGoodsID}}</td>
|
||||||
<td>{{.Quantity}}</td>
|
<td>{{.Quantity}}</td>
|
||||||
<td>{{.PriceText}}</td>
|
<td>{{.PriceText}}</td>
|
||||||
<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>
|
||||||
<td><span class="status-pill">{{.StageText}}</span><br><small>{{.StageHelp}}</small></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>
|
<td><button type="button" data-detail-open-id="{{.SybID}}">{{.ActionText}}</button></td>
|
||||||
@@ -201,9 +211,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="hint">
|
<p class="hint">
|
||||||
每行只显示当前最需要完成的下一步;双击行或点击“下一步”按钮可以打开处理详情。
|
每行只显示当前最需要完成的下一步;点击商品标题进入处理详情,点击图片查看原图。
|
||||||
</p>
|
</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-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 modal-wide" role="dialog" aria-modal="true" data-detail-slot>
|
||||||
<div class="modal-body">正在加载…</div>
|
<div class="modal-body">正在加载…</div>
|
||||||
|
|||||||
@@ -499,15 +499,17 @@ HTML 片段,只替换弹窗内部的表格、总数和分页。主货运单表
|
|||||||
|
|
||||||
- 一行对应顺运宝一张货运单的**一个商品明细**,不是一张货运单——一张货运单
|
- 一行对应顺运宝一张货运单的**一个商品明细**,不是一张货运单——一张货运单
|
||||||
可以有多个商品,各占一行。
|
可以有多个商品,各占一行。
|
||||||
- 图片显示小缩略图。`[必须]` 存 URL,不要把图片塞进数据库。
|
- 图片显示小缩略图。点击缩略图在弹窗中按比例查看顺运宝原图,并提供新窗口打开入口;
|
||||||
|
图片加载失败时显示可读提示。`[必须]` 只存 URL,不代理、缓存或把图片塞进数据库。
|
||||||
- 处理阶段是按当前关联事实**算出来的**,不是存一份容易过期的状态字段。阶段至少区分:
|
- 处理阶段是按当前关联事实**算出来的**,不是存一份容易过期的状态字段。阶段至少区分:
|
||||||
采购数据异常(顺运宝未提供规格)、未关联 PDD、PDD 待采集、采集中、
|
采购数据异常(顺运宝未提供规格)、未关联 PDD、PDD 待采集、采集中、
|
||||||
采集失败、规格待匹配、可采购和已创建任务。
|
采集失败、规格待匹配、可采购和已创建任务。
|
||||||
- 每行显示一个有文字的“下一步”按钮;双击行和按钮打开同一个服务端渲染处理弹窗。
|
- 每行显示一个有文字的“下一步”按钮。商品标题是服务端渲染处理弹窗的明确入口,
|
||||||
|
支持鼠标和键盘;双击其他普通单元格不执行动作。图片入口只负责查看原图,不能打开处理详情。
|
||||||
- 规格原文为空时不显示匹配表单,明确提示“顺运宝未提供规格”,并禁止建立映射或创建采购任务。
|
- 规格原文为空时不显示匹配表单,明确提示“顺运宝未提供规格”,并禁止建立映射或创建采购任务。
|
||||||
- 完整货运单 JSON 不作为列显示,落在 `syb_data` 里,供后续排查用。
|
- 完整货运单 JSON 不作为列显示,落在 `syb_data` 里,供后续排查用。
|
||||||
|
|
||||||
### 6.3 采购处理与规格匹配弹窗(双击行或点击下一步打开)
|
### 6.3 采购处理与规格匹配弹窗(点击商品标题或处理类下一步打开)
|
||||||
|
|
||||||
弹窗按当前处理阶段渐进展示。它直接以顺运宝规格原文及其 `spec_key`
|
弹窗按当前处理阶段渐进展示。它直接以顺运宝规格原文及其 `spec_key`
|
||||||
连接当前 PDD 商品选项,不再经过蝦皮 SKU 确认步骤。PDD 关联、采集和规格映射使用同一个弹窗完成。
|
连接当前 PDD 商品选项,不再经过蝦皮 SKU 确认步骤。PDD 关联、采集和规格映射使用同一个弹窗完成。
|
||||||
@@ -567,6 +569,7 @@ HTML 片段,只替换弹窗内部的表格、总数和分页。主货运单表
|
|||||||
`[必须]` 创建前弹出确认框,让操作员选**分配给哪个客户端**,并确认价格上限。
|
`[必须]` 创建前弹出确认框,让操作员选**分配给哪个客户端**,并确认价格上限。
|
||||||
价格上限默认从当前映射的 PDD 规格采集价带出,可改,**不允许为空**。
|
价格上限默认从当前映射的 PDD 规格采集价带出,可改,**不允许为空**。
|
||||||
|
|
||||||
|
|
||||||
## 7. 采集采购页
|
## 7. 采集采购页
|
||||||
|
|
||||||
`tasks` 一张表用 `task_type` 区分采集和采购,本页把两种任务放在同一个
|
`tasks` 一张表用 `task_type` 区分采集和采购,本页把两种任务放在同一个
|
||||||
|
|||||||
Reference in New Issue
Block a user