feat: 支持采购任务追溯并安全重新创建 (#186)
This commit is contained in:
+52
-23
@@ -61,6 +61,31 @@ func (h *Handler) renderSybListWithLoginReason(c *gin.Context, keyword, pageRaw,
|
||||
"读取顺运宝货运单列表失败,数据没有被改动。刷新页面重试;一直失败请把这句话报给维护者。")
|
||||
return
|
||||
}
|
||||
purchaseRows := append([]service.SybOrderView(nil), result.Rows...)
|
||||
openSybID := strings.TrimSpace(c.Query("open_syb_id"))
|
||||
openPurchaseSybID := ""
|
||||
if openSybID != "" && c.Query("purchase_intent") == "1" {
|
||||
focused, err := service.GetSybOrderView(h.db, openSybID)
|
||||
if err != nil {
|
||||
fail(c, http.StatusInternalServerError,
|
||||
"读取原货运单采购信息失败,数据没有被改动。刷新页面重试。")
|
||||
return
|
||||
}
|
||||
if focused != nil && focused.CanPurchase {
|
||||
found := false
|
||||
for _, row := range purchaseRows {
|
||||
if row.SybID == focused.SybID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
purchaseRows = append(purchaseRows, *focused)
|
||||
}
|
||||
openPurchaseSybID = focused.SybID
|
||||
openSybID = ""
|
||||
}
|
||||
}
|
||||
history, err := service.ListSybSyncHistory(h.db, service.ParsePage(c.Query("history_page")))
|
||||
if err != nil {
|
||||
fail(c, http.StatusInternalServerError,
|
||||
@@ -161,28 +186,31 @@ func (h *Handler) renderSybListWithLoginReason(c *gin.Context, keyword, pageRaw,
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "syb/list", page(c, "syb", "顺运宝数据", gin.H{
|
||||
"Keyword": keyword,
|
||||
"ShopFilter": shop,
|
||||
"StageFilter": stage,
|
||||
"StageOptions": service.SybStageOptions(),
|
||||
"CurrentPage": result.Page,
|
||||
"Rows": result.Rows,
|
||||
"Status": status,
|
||||
"HasAny": result.HasAny,
|
||||
"IsFiltered": result.IsFiltered,
|
||||
"NeedLogin": needLogin,
|
||||
"NeedLoginReason": loginReason,
|
||||
"LoginHint": loginHint,
|
||||
"Username": username,
|
||||
"ConfigProblem": configProblem,
|
||||
"Today": today,
|
||||
"SyncDateFrom": dateFrom,
|
||||
"SyncDateTo": dateTo,
|
||||
"RangeError": c.Query("range_error"),
|
||||
"RangeWarning": rangeWarning,
|
||||
"SyncRunning": syncStatus.Running,
|
||||
"SyncHistory": history,
|
||||
"NeedSyncHistory": c.Query("history") == "1",
|
||||
"Keyword": keyword,
|
||||
"ShopFilter": shop,
|
||||
"StageFilter": stage,
|
||||
"StageOptions": service.SybStageOptions(),
|
||||
"CurrentPage": result.Page,
|
||||
"Rows": result.Rows,
|
||||
"PurchaseRows": purchaseRows,
|
||||
"OpenSybID": openSybID,
|
||||
"OpenPurchaseSybID": openPurchaseSybID,
|
||||
"Status": status,
|
||||
"HasAny": result.HasAny,
|
||||
"IsFiltered": result.IsFiltered,
|
||||
"NeedLogin": needLogin,
|
||||
"NeedLoginReason": loginReason,
|
||||
"LoginHint": loginHint,
|
||||
"Username": username,
|
||||
"ConfigProblem": configProblem,
|
||||
"Today": today,
|
||||
"SyncDateFrom": dateFrom,
|
||||
"SyncDateTo": dateTo,
|
||||
"RangeError": c.Query("range_error"),
|
||||
"RangeWarning": rangeWarning,
|
||||
"SyncRunning": syncStatus.Running,
|
||||
"SyncHistory": history,
|
||||
"NeedSyncHistory": c.Query("history") == "1",
|
||||
"HistoryFetchURL": sybHistoryPartialURL(history.Page,
|
||||
keyword, shop, stage, dateFrom, dateTo),
|
||||
"HistoryPagination": sybHistoryPagination(history.Page, history.TotalPages,
|
||||
@@ -805,7 +833,8 @@ func (h *Handler) TaskList(c *gin.Context) {
|
||||
|
||||
// TaskDetail 渲染双击行弹出的只读详情弹窗内容(不是整页)。
|
||||
//
|
||||
// `[必须]` 只读。改派 / 重试 / 取消是后续工单的范围,这里不提供入口。
|
||||
// `[必须]` 字段只读,不提供改派 / 重试 / 取消或直接创建入口。失败/取消采购
|
||||
// 只生成返回原顺运宝明细的导航链接,真正创建仍走原确认表单和服务端门禁。
|
||||
// 弹窗机制复用 #18 已有的那套(static/js/app.js 的 setupRowDetail),
|
||||
// 不新造一套。
|
||||
func (h *Handler) TaskDetail(c *gin.Context) {
|
||||
|
||||
+34
-1
@@ -83,7 +83,10 @@ func TestModuleNavigation_按账号保存稳定列表状态且安全回退(t *te
|
||||
t.Fatal("没有找到模块查询参数白名单")
|
||||
}
|
||||
whitelist := jsSource[whitelistStart:whitelistEnd]
|
||||
for _, forbidden := range []string{"\"msg\"", "\"error\"", "\"history\"", "\"login\"", "\"change_password\""} {
|
||||
for _, forbidden := range []string{
|
||||
"\"msg\"", "\"error\"", "\"history\"", "\"login\"", "\"change_password\"",
|
||||
"\"open_syb_id\"", "\"purchase_intent\"",
|
||||
} {
|
||||
if strings.Contains(whitelist, forbidden) {
|
||||
t.Errorf("稳定状态白名单不应包含临时参数 %s", forbidden)
|
||||
}
|
||||
@@ -189,6 +192,36 @@ func TestTaskDetail_采购核单结果使用独立只读分组(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskDetail_采购来源与安全重新创建入口(t *testing.T) {
|
||||
files := map[string][]string{
|
||||
"templates/task/detail_modal.html": {
|
||||
"来源信息", "蝦皮订单号", "顺运宝明细 ID", "蝦皮商品 ID", "PDD 商品 ID",
|
||||
`{{if .HasSybSource}}`, `href="{{.ViewSybURL}}"`, "查看原货运单",
|
||||
`{{if .CanRecreatePurchase}}`, `href="{{.RecreatePurchaseURL}}"`, "重新创建采购任务",
|
||||
},
|
||||
"templates/syb/list.html": {
|
||||
`data-auto-open-purchase-id="{{.OpenPurchaseSybID}}"`, `{{range .PurchaseRows}}`,
|
||||
`data-auto-open-detail-id="{{.OpenSybID}}"`,
|
||||
},
|
||||
"static/js/app.js": {
|
||||
`form.getAttribute("data-auto-open-purchase-id")`,
|
||||
`modal.getAttribute("data-auto-open-detail-id")`,
|
||||
`showPurchaseSelection(autoSelected)`, `loadDetail(autoDetailID)`,
|
||||
},
|
||||
}
|
||||
for path, wants := range files {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, want := range wants {
|
||||
if !strings.Contains(string(content), want) {
|
||||
t.Errorf("%s 缺少采购来源恢复约束 %q", path, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskPage_创建人筛选列表列和详情字段齐全(t *testing.T) {
|
||||
files := map[string][]string{
|
||||
"templates/task/list.html": {
|
||||
|
||||
@@ -158,6 +158,27 @@ func seedPurchasableWorkflow(t *testing.T, db *sql.DB, sybID string) string {
|
||||
return key
|
||||
}
|
||||
|
||||
func TestGetSybOrderView_按明细ID准备单条采购确认(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
key := seedPurchasableWorkflow(t, db, "SYB-DEEP-LINK")
|
||||
if err := SaveSybMapping(db, "SYB-DEEP-LINK", key, "USR-1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
view, err := GetSybOrderView(db, " SYB-DEEP-LINK ")
|
||||
if err != nil || view == nil {
|
||||
t.Fatalf("精确读取失败: view=%+v err=%v", view, err)
|
||||
}
|
||||
if !view.CanPurchase || view.SybID != "SYB-DEEP-LINK" ||
|
||||
view.MappingOptionKey != key || view.DefaultUnitPrice != "39.90" {
|
||||
t.Fatalf("深链接采购确认数据不完整: %+v", view)
|
||||
}
|
||||
missing, err := GetSybOrderView(db, "NOT-FOUND")
|
||||
if err != nil || missing != nil {
|
||||
t.Fatalf("不存在明细应返回 nil: view=%+v err=%v", missing, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSybMapping_动态维度保存并复用(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
key := seedPurchasableWorkflow(t, db, "SYB-1")
|
||||
|
||||
@@ -1197,6 +1197,17 @@ func sybOrderViewFor(context repository.SybOrderContext) SybOrderView {
|
||||
return v
|
||||
}
|
||||
|
||||
// GetSybOrderView 按稳定明细 ID 读取一行完整视图,供任务来源深链接在分页之外
|
||||
// 准备采购确认数据。查不到返回 (nil, nil)。
|
||||
func GetSybOrderView(db *sql.DB, sybID string) (*SybOrderView, error) {
|
||||
context, err := repository.GetSybOrderContext(db, strings.TrimSpace(sybID))
|
||||
if err != nil || context == nil {
|
||||
return nil, err
|
||||
}
|
||||
view := sybOrderViewFor(*context)
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
// SybProcessingDetail 是顺运宝“下一步”弹窗第一阶段需要的上下文。
|
||||
type SybProcessingDetail struct {
|
||||
SybID string
|
||||
|
||||
+41
-6
@@ -12,6 +12,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -456,7 +457,8 @@ const resultDataLimit = 4000
|
||||
|
||||
// TaskDetailView 是双击弹窗要显示的全部内容。
|
||||
//
|
||||
// `[必须]` 只读——本工单不做改派、重试、取消,弹窗里不应该有对应的表单。
|
||||
// `[必须]` 字段只读——弹窗不直接改派、重试、取消或创建任务;安全恢复只导航
|
||||
// 回原顺运宝明细,真正创建仍走原确认表单和服务端门禁。
|
||||
type TaskDetailView struct {
|
||||
TaskID string
|
||||
TypeText string
|
||||
@@ -471,12 +473,20 @@ type TaskDetailView struct {
|
||||
PddGoodsURL string
|
||||
PddShopText string
|
||||
|
||||
// IsPurchase 为 false 时,模板要把下面四个采购专有字段整段隐藏,
|
||||
// IsPurchase 为 false 时,模板要把下面的采购专有字段整段隐藏,
|
||||
// 不能显示空行——见 #19。
|
||||
IsPurchase bool
|
||||
SpecText string
|
||||
QuantityText string
|
||||
PriceLimitText string
|
||||
IsPurchase bool
|
||||
ShopeeOrderNo string
|
||||
SybID string
|
||||
ShopeeGoodsID string
|
||||
PddGoodsID string
|
||||
HasSybSource bool
|
||||
ViewSybURL string
|
||||
CanRecreatePurchase bool
|
||||
RecreatePurchaseURL string
|
||||
SpecText string
|
||||
QuantityText string
|
||||
PriceLimitText string
|
||||
|
||||
// PDD 核单结果只在采购任务详情显示。付款状态是 Client 核单上报时的快照,
|
||||
// 不是 Admin 对 PDD 的实时查询结果。
|
||||
@@ -522,6 +532,18 @@ func GetTaskDetail(db *sql.DB, taskID string) (*TaskDetailView, error) {
|
||||
IsPurchase: t.TaskType == model.TaskPurchase,
|
||||
}
|
||||
if v.IsPurchase {
|
||||
v.ShopeeOrderNo = orPlaceholder(t.OrderNo)
|
||||
v.SybID = orPlaceholder(t.SybID)
|
||||
v.ShopeeGoodsID = orPlaceholder(t.GoodsID)
|
||||
v.PddGoodsID = orPlaceholder(t.PddGoodsID)
|
||||
v.HasSybSource = strings.TrimSpace(t.SybID) != ""
|
||||
if v.HasSybSource {
|
||||
v.ViewSybURL = taskSybURL(*t, false)
|
||||
v.CanRecreatePurchase = t.Status == model.TaskFailed || t.Status == model.TaskCancelled
|
||||
if v.CanRecreatePurchase {
|
||||
v.RecreatePurchaseURL = taskSybURL(*t, true)
|
||||
}
|
||||
}
|
||||
v.SpecText = specText(t.PddOptions)
|
||||
v.QuantityText = quantityText(t.Quantity)
|
||||
v.PriceLimitText = priceLimitText(t.MaxPriceCent)
|
||||
@@ -541,6 +563,19 @@ func GetTaskDetail(db *sql.DB, taskID string) (*TaskDetailView, error) {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// taskSybURL 生成采购任务来源的稳定深链接。syb_id 用于精确定位;订单号只负责
|
||||
// 让跳转后的列表保留有意义的上下文,不能拿它代替明细主键。
|
||||
func taskSybURL(task model.Task, purchaseIntent bool) string {
|
||||
values := url.Values{"open_syb_id": {task.SybID}}
|
||||
if strings.TrimSpace(task.OrderNo) != "" {
|
||||
values.Set("order_no", task.OrderNo)
|
||||
}
|
||||
if purchaseIntent {
|
||||
values.Set("purchase_intent", "1")
|
||||
}
|
||||
return "/syb?" + values.Encode()
|
||||
}
|
||||
|
||||
// GetTaskDetailForUser 对采购员隐藏其他人和历史任务,统一表现为不存在。
|
||||
func GetTaskDetailForUser(db *sql.DB, actor *model.User, taskID string) (*TaskDetailView, error) {
|
||||
visibleUserID, err := visibleClientUserID(actor)
|
||||
|
||||
@@ -44,7 +44,9 @@ type testTaskParams struct {
|
||||
taskType model.TaskType
|
||||
status model.TaskStatus
|
||||
assignedClient string
|
||||
sybID string
|
||||
orderNo string
|
||||
goodsID string
|
||||
pddGoodsID string
|
||||
pddOptions string
|
||||
quantity int
|
||||
@@ -61,13 +63,19 @@ func insertTestTask(t *testing.T, db *sql.DB, p testTaskParams) {
|
||||
t.Helper()
|
||||
now := model.NowISO()
|
||||
|
||||
var assigned, orderNo, pddGoodsID, pddOptions, errCode, errMsg, resultData, claimedAt, finishedAt any
|
||||
var assigned, sybID, orderNo, goodsID, pddGoodsID, pddOptions, errCode, errMsg, resultData, claimedAt, finishedAt any
|
||||
if p.assignedClient != "" {
|
||||
assigned = p.assignedClient
|
||||
}
|
||||
if p.orderNo != "" {
|
||||
orderNo = p.orderNo
|
||||
}
|
||||
if p.sybID != "" {
|
||||
sybID = p.sybID
|
||||
}
|
||||
if p.goodsID != "" {
|
||||
goodsID = p.goodsID
|
||||
}
|
||||
if p.pddGoodsID != "" {
|
||||
pddGoodsID = p.pddGoodsID
|
||||
}
|
||||
@@ -100,14 +108,14 @@ func insertTestTask(t *testing.T, db *sql.DB, p testTaskParams) {
|
||||
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO tasks (task_id, task_type, status, assigned_client, claimed_at,
|
||||
order_no, pdd_goods_url, pdd_goods_id, pdd_options,
|
||||
syb_id, order_no, goods_id, pdd_goods_url, pdd_goods_id, pdd_options,
|
||||
quantity, max_price_cent,
|
||||
error_code, error_message, result_data, finished_at, created_by_user_id,
|
||||
created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'https://mobile.yangkeduo.com/goods.html?goods_id=1', ?, ?,
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'https://mobile.yangkeduo.com/goods.html?goods_id=1', ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
p.taskID, string(p.taskType), string(p.status), assigned, claimedAt,
|
||||
orderNo, pddGoodsID, pddOptions,
|
||||
sybID, orderNo, goodsID, pddGoodsID, pddOptions,
|
||||
quantity, maxPrice,
|
||||
errCode, errMsg, resultData, finishedAt, nullableString(p.createdByUserID),
|
||||
now, now)
|
||||
@@ -425,8 +433,9 @@ func TestGetTaskDetail_采购任务显示专有字段(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
insertTestTask(t, db, testTaskParams{
|
||||
taskID: "PUR-1", taskType: model.TaskPurchase, status: model.TaskClaimed,
|
||||
orderNo: "SO-001", pddOptions: `{"color":"黑色","size":"M"}`,
|
||||
quantity: 2, maxPriceCent: 4200, assignedClient: "client-001",
|
||||
sybID: "SYB-001", orderNo: "SO-001", goodsID: "SHOPEE-001", pddGoodsID: "PDD-001",
|
||||
pddOptions: `{"color":"黑色","size":"M"}`,
|
||||
quantity: 2, maxPriceCent: 4200, assignedClient: "client-001",
|
||||
claimedAt: model.NowISO(),
|
||||
})
|
||||
|
||||
@@ -446,6 +455,40 @@ func TestGetTaskDetail_采购任务显示专有字段(t *testing.T) {
|
||||
if d.ClientText != "client-001" {
|
||||
t.Errorf("分配客户端 = %q, want client-001", d.ClientText)
|
||||
}
|
||||
if d.ShopeeOrderNo != "SO-001" || d.SybID != "SYB-001" ||
|
||||
d.ShopeeGoodsID != "SHOPEE-001" || d.PddGoodsID != "PDD-001" || !d.HasSybSource {
|
||||
t.Errorf("采购来源字段不完整: %+v", d)
|
||||
}
|
||||
if d.ViewSybURL != "/syb?open_syb_id=SYB-001&order_no=SO-001" || d.CanRecreatePurchase || d.RecreatePurchaseURL != "" {
|
||||
t.Errorf("执行中的采购任务只应允许查看来源: view=%q recreate=%t url=%q",
|
||||
d.ViewSybURL, d.CanRecreatePurchase, d.RecreatePurchaseURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTaskDetail_只有失败和取消允许进入重新创建确认(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
statuses := []model.TaskStatus{
|
||||
model.TaskPending, model.TaskAssigned, model.TaskClaimed, model.TaskSucceeded,
|
||||
model.TaskManualReview, model.TaskFailed, model.TaskCancelled,
|
||||
}
|
||||
for _, status := range statuses {
|
||||
taskID := "PUR-" + string(status)
|
||||
insertTestTask(t, db, testTaskParams{
|
||||
taskID: taskID, taskType: model.TaskPurchase, status: status,
|
||||
sybID: "SYB A&B", orderNo: "ORDER/1", goodsID: "SHOPEE-1", pddGoodsID: "PDD-1",
|
||||
})
|
||||
detail, err := GetTaskDetail(db, taskID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := status == model.TaskFailed || status == model.TaskCancelled
|
||||
if detail.CanRecreatePurchase != want {
|
||||
t.Errorf("状态 %s CanRecreatePurchase=%t,want %t", status, detail.CanRecreatePurchase, want)
|
||||
}
|
||||
if want && detail.RecreatePurchaseURL != "/syb?open_syb_id=SYB+A%26B&order_no=ORDER%2F1&purchase_intent=1" {
|
||||
t.Errorf("状态 %s 的深链接编码错误: %q", status, detail.RecreatePurchaseURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTaskDetail_采购成功独立显示PDD核单结果(t *testing.T) {
|
||||
@@ -525,6 +568,9 @@ func TestGetTaskDetail_采集任务不带采购专有字段(t *testing.T) {
|
||||
if d.HasPddOrder || d.PddOrderResultText != "" {
|
||||
t.Error("采集任务不应生成 PDD 核单结果展示")
|
||||
}
|
||||
if d.HasSybSource || d.ShopeeOrderNo != "" || d.CanRecreatePurchase {
|
||||
t.Error("采集任务不应生成采购来源和重新创建入口")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTaskDetail_有错误信息时显示(t *testing.T) {
|
||||
|
||||
@@ -363,6 +363,12 @@ tr.empty small { color: #aaa; }
|
||||
}
|
||||
.button-link:hover { background: #f0f2f4; }
|
||||
.button-link.disabled { color: #999; background: #f5f6f8; }
|
||||
.button-link.primary {
|
||||
background: #1f6feb;
|
||||
border-color: #1f6feb;
|
||||
color: #fff;
|
||||
}
|
||||
.button-link.primary:hover { background: #1a5fd0; }
|
||||
.sync-history-table { max-height: 460px; }
|
||||
.sync-history-table td { vertical-align: top; }
|
||||
.sync-history-table .sync-error {
|
||||
|
||||
+34
-12
@@ -255,6 +255,22 @@
|
||||
var input = row.querySelector("[data-unit-price-input]");
|
||||
if (input) input.addEventListener("input", function () { updateTotalPrice(row); });
|
||||
});
|
||||
function showPurchaseSelection(selected) {
|
||||
var shown = 0;
|
||||
form.querySelectorAll("[data-purchase-row]").forEach(function (row) {
|
||||
var enabled = !!selected[row.getAttribute("data-purchase-row")];
|
||||
row.hidden = !enabled;
|
||||
row.querySelectorAll("input").forEach(function (input) {
|
||||
input.disabled = !enabled;
|
||||
});
|
||||
if (enabled) updateTotalPrice(row);
|
||||
if (enabled) shown += 1;
|
||||
});
|
||||
var empty = form.querySelector("[data-purchase-empty]");
|
||||
if (empty) empty.hidden = shown > 0;
|
||||
return shown;
|
||||
}
|
||||
|
||||
openButtons.forEach(function (openButton) {
|
||||
openButton.addEventListener("click", function () {
|
||||
var selected = {};
|
||||
@@ -266,20 +282,23 @@
|
||||
if (supportsSelectAction(box, "purchase")) selected[box.value] = true;
|
||||
});
|
||||
}
|
||||
var shown = 0;
|
||||
form.querySelectorAll("[data-purchase-row]").forEach(function (row) {
|
||||
var enabled = !!selected[row.getAttribute("data-purchase-row")];
|
||||
row.hidden = !enabled;
|
||||
row.querySelectorAll("input").forEach(function (input) {
|
||||
input.disabled = !enabled;
|
||||
});
|
||||
if (enabled) updateTotalPrice(row);
|
||||
if (enabled) shown += 1;
|
||||
});
|
||||
var empty = form.querySelector("[data-purchase-empty]");
|
||||
if (empty) empty.hidden = shown > 0;
|
||||
showPurchaseSelection(selected);
|
||||
});
|
||||
});
|
||||
|
||||
/* 任务详情的“重新创建”只把稳定 syb_id 带回当前确认流程。
|
||||
找不到或当前已不可采购时服务端不会输出这个 ID,前端绝不自行放宽。 */
|
||||
var autoPurchaseID = form.getAttribute("data-auto-open-purchase-id") || "";
|
||||
var autoSelected = {};
|
||||
if (autoPurchaseID) autoSelected[autoPurchaseID] = true;
|
||||
if (autoPurchaseID && showPurchaseSelection(autoSelected) > 0) {
|
||||
var purchaseModal = form.closest(".modal-backdrop");
|
||||
if (purchaseModal) {
|
||||
openModal(purchaseModal);
|
||||
var clientSelect = form.querySelector("select[name=client_id]");
|
||||
if (clientSelect) clientSelect.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
form.addEventListener("submit", function (event) {
|
||||
if (event.defaultPrevented || !submitButton) return;
|
||||
submitButton.disabled = true;
|
||||
@@ -438,6 +457,9 @@
|
||||
loadDetail(button.getAttribute("data-detail-open-id"));
|
||||
});
|
||||
});
|
||||
|
||||
var autoDetailID = modal.getAttribute("data-auto-open-detail-id") || "";
|
||||
if (autoDetailID) loadDetail(autoDetailID);
|
||||
}
|
||||
|
||||
/* 顺运宝缩略图使用上游图片 URL。这里只在弹窗里放大展示,
|
||||
|
||||
@@ -53,7 +53,8 @@ func TestSybListTemplate_区分标题图片与阶段动作(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
data := map[string]any{
|
||||
"Title": "顺运宝数据", "Active": "syb", "CSRFToken": "test-csrf",
|
||||
"Rows": rows, "DetailURL": "/syb/detail", "StageOptions": service.SybStageOptions(),
|
||||
"Rows": rows, "PurchaseRows": rows,
|
||||
"DetailURL": "/syb/detail", "StageOptions": service.SybStageOptions(),
|
||||
}
|
||||
if err := tmpl.ExecuteTemplate(&output, "syb/list", data); err != nil {
|
||||
t.Fatalf("渲染顺运宝列表失败: %v", err)
|
||||
|
||||
@@ -215,7 +215,8 @@
|
||||
<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>
|
||||
<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}}">
|
||||
@@ -231,7 +232,7 @@
|
||||
{{if eq .PurchaseClientCount 0}}<small class="missing">当前账号没有可见的客户端,请先联系管理员绑定客户端。</small>{{else}}<small>所有当前账号可见的客户端均可指派;未就绪或离线客户端会等待其就绪后领取。</small>{{end}}
|
||||
</div>
|
||||
<p class="hint">单价上限默认取已映射 PDD 规格的人民币采集价;订单总价上限按单价乘以顺运宝数量自动计算,不使用顺运宝的台币售价。</p>
|
||||
{{range .Rows}}
|
||||
{{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>
|
||||
@@ -283,7 +284,8 @@
|
||||
</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}}"
|
||||
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>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
{{/* 双击一行时弹出的内容。
|
||||
这是一个**片段**,不是整页——外面的弹窗壳子在 task/list.html 里。
|
||||
|
||||
`[必须]` 只读:本工单不做改派、重试、取消,弹窗里不放对应的表单和按钮,
|
||||
见 #19。采购专有字段(目标规格/数量/订单总价上限)在采集任务这里
|
||||
`[必须]` 字段只读:弹窗不直接改派、重试、取消或复制旧任务;失败/取消采购
|
||||
只允许导航回原顺运宝明细重新确认,见 #186。采购专有字段在采集任务这里
|
||||
**整段隐藏**,不显示空行。 */}}
|
||||
{{with .D}}
|
||||
<div class="modal-head">
|
||||
@@ -23,6 +23,16 @@
|
||||
<dt>完成时间</dt><dd>{{.FinishedAt}}</dd>
|
||||
</dl>
|
||||
|
||||
{{if .IsPurchase}}
|
||||
<h3>来源信息</h3>
|
||||
<dl class="detail">
|
||||
<dt>蝦皮订单号</dt><dd>{{.ShopeeOrderNo}}</dd>
|
||||
<dt>顺运宝明细 ID</dt><dd>{{.SybID}}</dd>
|
||||
<dt>蝦皮商品 ID</dt><dd>{{.ShopeeGoodsID}}</dd>
|
||||
<dt>PDD 商品 ID</dt><dd>{{.PddGoodsID}}</dd>
|
||||
</dl>
|
||||
{{end}}
|
||||
|
||||
<h3>执行参数</h3>
|
||||
<dl class="detail">
|
||||
<dt>PDD 链接</dt>
|
||||
@@ -70,6 +80,13 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-foot">
|
||||
{{if .HasSybSource}}
|
||||
<span class="grow"></span>
|
||||
<a class="button-link" href="{{.ViewSybURL}}">查看原货运单</a>
|
||||
{{if .CanRecreatePurchase}}
|
||||
<a class="button-link primary" href="{{.RecreatePurchaseURL}}">重新创建采购任务</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<button type="button" data-modal-close>关闭</button>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user