diff --git a/admin/handler/web/shopee.go b/admin/handler/web/shopee.go index fec7612..b288fd8 100644 --- a/admin/handler/web/shopee.go +++ b/admin/handler/web/shopee.go @@ -6,6 +6,7 @@ import ( "net/url" "strconv" "strings" + "time" "github.com/gin-gonic/gin" @@ -119,6 +120,7 @@ func (h *Handler) renderShopeeList(c *gin.Context, keyword, searchFieldRaw, stat "Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()), "CurrentPage": result.Page, "DetailURL": "/shopee/detail?" + detailValuesForShopee(values, result.Page), + "AutoOpenDetailID": strings.TrimSpace(c.Query("open_id")), "AssignableClients": assignableClients, })) } @@ -158,6 +160,22 @@ func (h *Handler) ShopeeSave(c *gin.Context) { h.shopeeRedirect(c, fmt.Sprintf("已关联 PDD 商品 %s,可继续创建采集任务", goodsID)) } +// ShopeeSpecSave 保存一条现有蝦皮 SKU 的人工颜色、尺码和建议说明。 +func (h *Handler) ShopeeSpecSave(c *gin.Context) { + err := service.UpdateShopeeSKUManual(h.db, currentUser(c), c.PostForm("shopee_goods_id"), + c.PostForm("sku_record_id"), c.PostForm("color"), c.PostForm("size"), + c.PostForm("advice"), time.Now()) + if err != nil { + if service.IsValidationError(err) { + h.shopeeRedirect(c, "规格未保存:"+err.Error()) + return + } + fail(c, http.StatusInternalServerError, "保存蝦皮规格失败,数据没有被改动。请刷新页面后重试。") + return + } + h.shopeeRedirect(c, "蝦皮规格已保存,人工字段不会被后续导入覆盖") +} + // ShopeeDelete 批量删除勾选的行。 func (h *Handler) ShopeeDelete(c *gin.Context) { count, err := service.DeleteShopeeProducts(h.db, currentUser(c), c.PostFormArray("ids")) @@ -246,6 +264,9 @@ func (h *Handler) shopeeRedirect(c *gin.Context, msg string) { if value := strings.TrimSpace(c.PostForm("page")); value != "" { params.Set("page", value) } + if value := strings.TrimSpace(c.PostForm("open_id")); value != "" { + params.Set("open_id", value) + } if msg != "" { params.Set("msg", msg) } diff --git a/admin/handler/web/web.go b/admin/handler/web/web.go index 954a851..618dd88 100644 --- a/admin/handler/web/web.go +++ b/admin/handler/web/web.go @@ -57,6 +57,7 @@ func Register(r *gin.Engine, db *sql.DB, onlineThreshold time.Duration) { pages.GET("/shopee", h.ShopeeList) pages.GET("/shopee/detail", h.ShopeeDetail) // 双击行时前端来取弹窗内容 pages.POST("/shopee/save", h.ShopeeSave) + pages.POST("/shopee/spec/save", h.ShopeeSpecSave) pages.POST("/shopee/collect", h.ShopeeCollect) pages.POST("/shopee/collect-batch", h.ShopeeCollectBatch) shopeeAdmin := pages.Group("/shopee", AdminRequired()) diff --git a/admin/main_test.go b/admin/main_test.go index 58907f4..5969536 100644 --- a/admin/main_test.go +++ b/admin/main_test.go @@ -385,6 +385,27 @@ func TestShopeePage_展示主图店铺且保留标题宽度(t *testing.T) { } } +func TestShopeeDetail_现有SKU支持渐进式人工编辑(t *testing.T) { + detail, err := os.ReadFile("templates/shopee/detail_modal.html") + if err != nil { + t.Fatal(err) + } + routes, err := os.ReadFile("handler/web/web.go") + if err != nil { + t.Fatal(err) + } + content := string(detail) + string(routes) + for _, want := range []string{ + `action="/shopee/spec/save"`, `name="sku_record_id"`, `name="open_id"`, + `name="color"`, `name="size"`, `name="advice"`, `maxlength="500"`, + `规格原文和 SKU 身份不会修改`, `pages.POST("/shopee/spec/save"`, + } { + if !strings.Contains(content, want) { + t.Errorf("蝦皮规格编辑界面或路由缺少 %q", want) + } + } +} + func TestShopeePage_分类搜索回收站和状态保留(t *testing.T) { list, err := os.ReadFile("templates/shopee/list.html") if err != nil { diff --git a/admin/repository/catalog_import.go b/admin/repository/catalog_import.go index 4004d1d..38c2382 100644 --- a/admin/repository/catalog_import.go +++ b/admin/repository/catalog_import.go @@ -351,7 +351,8 @@ func UpsertCatalogShopeeSKU(q Execer, in CatalogShopeeSKUInput) (CatalogSKUOutco sources, times := catalogFieldProvenance(current) changed := external != current.ShopeeSKUID.String canFill := func(field, current, incoming string) bool { - return strings.TrimSpace(incoming) != "" && (current == "" || (sources[field] == "syb" && in.Source != "syb")) + return sources[field] != "manual" && strings.TrimSpace(incoming) != "" && + (current == "" || (sources[field] == "syb" && in.Source != "syb")) } if canFill("color", color, in.Color) { color = in.Color diff --git a/admin/repository/shopee.go b/admin/repository/shopee.go index a19578f..563f340 100644 --- a/admin/repository/shopee.go +++ b/admin/repository/shopee.go @@ -438,6 +438,50 @@ func GetShopeeSKUByID(q Execer, skuID string) (*model.ShopeeSKU, error) { return &sk, nil } +// ShopeeSKUManualState 是人工编辑前需要读取的最小状态。 +// 字段来源 JSON 必须先读出再合并,不能直接重建,否则会丢掉 sku_code 等 +// 本次表单没有修改的字段来源。 +type ShopeeSKUManualState struct { + RecordID, GoodsID string + FieldSources, FieldObservedAt string +} + +// GetShopeeSKUManualState 查询一条仍属于正常商品的 SKU 及其字段来源。 +// 查不到返回 (nil, nil)。 +func GetShopeeSKUManualState(q Execer, skuID string) (*ShopeeSKUManualState, error) { + var state ShopeeSKUManualState + err := q.QueryRow(`SELECT sku_id,goods_id,COALESCE(field_sources,'{}'),COALESCE(field_observed_at,'{}') + FROM shopee_skus sk WHERE sku_id=? + AND EXISTS (SELECT 1 FROM shopee_products sp WHERE sp.goods_id=sk.goods_id AND sp.deleted_at IS NULL)`, skuID). + Scan(&state.RecordID, &state.GoodsID, &state.FieldSources, &state.FieldObservedAt) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("查询待编辑蝦皮规格 %s 失败: %w", skuID, err) + } + return &state, nil +} + +// UpdateShopeeSKUManualFields 原子保存人工确认的颜色、尺码和建议说明。 +// spec_raw、spec_key、真实 SKU ID、SKU 编码和 is_manual 均不在更新清单中。 +func UpdateShopeeSKUManualFields(q Execer, skuID, goodsID, color, size, advice, + fieldSources, fieldObservedAt, updatedAt string) (bool, error) { + result, err := q.Exec(`UPDATE shopee_skus SET color=?,size=?,advice=NULLIF(?,''),parse_ok=1, + field_sources=?,field_observed_at=?,updated_at=? + WHERE sku_id=? AND goods_id=? + AND EXISTS (SELECT 1 FROM shopee_products sp WHERE sp.goods_id=shopee_skus.goods_id AND sp.deleted_at IS NULL)`, + color, size, advice, fieldSources, fieldObservedAt, updatedAt, skuID, goodsID) + if err != nil { + return false, fmt.Errorf("保存蝦皮规格 %s 失败: %w", skuID, err) + } + count, err := result.RowsAffected() + if err != nil { + return false, fmt.Errorf("确认蝦皮规格 %s 保存结果失败: %w", skuID, err) + } + return count == 1, nil +} + // CountShopeeProducts 统计蝦皮商品总数,供列表页判断"是否已导入过任何数据"。 func CountShopeeProducts(q Execer) (int, error) { var n int diff --git a/admin/service/shopee_list.go b/admin/service/shopee_list.go index f4a2713..e5d6138 100644 --- a/admin/service/shopee_list.go +++ b/admin/service/shopee_list.go @@ -214,17 +214,16 @@ func ParseShopeeSearchField(s string) string { // ShopeeSpecView 是弹窗规格表的一行。 type ShopeeSpecView struct { - SKUID string - Color string - Size string - Advice string + RecordID string + SKUID string + Color string + Size string + Advice string // StatusText 是"✓"或"待补",`[必须]` 要有文字,不能只靠颜色区分(见 #41)。 StatusText string - // Pending 为 true 时模板整行标黄,并在下面加一行显示 SpecRaw 原文。 + // Pending 为 true 时模板整行标黄。 Pending bool - // SpecRaw 只在 Pending 为 true 时才有意义。 - // `[必须]` 待补的行必须显示规格原文——不显示的话人工不知道该填什么, - // 这正是保留 spec_raw 这个设计的全部意义,见 #41。 + // SpecRaw 对所有行保持只读展示,人工编辑时用它核对颜色和尺码。 SpecRaw string } @@ -317,18 +316,19 @@ func GetShopeeProductDetail(db *sql.DB, goodsID string) (*ShopeeProductDetail, e } d.SKUs = make([]ShopeeSpecView, 0, len(skus)) for _, sk := range skus { - row := ShopeeSpecView{SKUID: sk.SKUID} + row := ShopeeSpecView{ + RecordID: sk.RecordID, + SKUID: sk.SKUID, + Color: sk.Color, + Size: sk.Size, + Advice: sk.Advice, + SpecRaw: sk.SpecRaw, + } if sk.ParseOK { - row.Color, row.Size, row.Advice = sk.Color, sk.Size, sk.Advice - if row.Advice == "" { - row.Advice = placeholder - } row.StatusText = "✓" } else { - row.Color, row.Size, row.Advice = placeholder, placeholder, placeholder row.StatusText = "待补" row.Pending = true - row.SpecRaw = sk.SpecRaw d.PendingCount++ } d.SKUs = append(d.SKUs, row) diff --git a/admin/service/shopee_list_test.go b/admin/service/shopee_list_test.go index 6f56a24..3a89d1f 100644 --- a/admin/service/shopee_list_test.go +++ b/admin/service/shopee_list_test.go @@ -297,6 +297,9 @@ func TestGetShopeeProductDetail_待补行显示原文(t *testing.T) { if pending.SpecRaw != "紅色,3XL建議80-90公斤】" { t.Errorf("SpecRaw = %q,想要显示原文", pending.SpecRaw) } + if pending.RecordID != "s2" || pending.Color != "" || pending.Size != "" { + t.Errorf("待补规格的编辑值或内部定位错误:%+v", pending) + } if pending.StatusText != "待补" { t.Errorf("StatusText = %q,想要 \"待补\"", pending.StatusText) } diff --git a/admin/service/shopee_sku.go b/admin/service/shopee_sku.go new file mode 100644 index 0000000..51a57e6 --- /dev/null +++ b/admin/service/shopee_sku.go @@ -0,0 +1,102 @@ +package service + +import ( + "database/sql" + "encoding/json" + "fmt" + "strings" + "time" + "unicode/utf8" + + "cmautobuy/admin/model" + "cmautobuy/admin/repository" +) + +const maxShopeeSKUFieldRunes = 500 + +// UpdateShopeeSKUManual 保存采购员对现有蝦皮规格的人工修正。 +// 只修改颜色、尺码和建议说明;规格原文、身份字段和既有采购数据保持不变。 +func UpdateShopeeSKUManual(db *sql.DB, actor *model.User, goodsID, skuRecordID, + color, size, advice string, now time.Time) error { + if actor == nil || actor.Status != model.UserActive { + return ErrUnauthenticated + } + goodsID = strings.TrimSpace(goodsID) + skuRecordID = strings.TrimSpace(skuRecordID) + color = strings.TrimSpace(color) + size = strings.TrimSpace(size) + advice = strings.TrimSpace(advice) + if goodsID == "" { + return invalidFieldInput("shopee_goods_id", "蝦皮商品编号不能为空") + } + if skuRecordID == "" { + return invalidFieldInput("sku_record_id", "蝦皮规格记录编号不能为空") + } + if color == "" { + return invalidFieldInput("color", "颜色不能为空") + } + if size == "" { + return invalidFieldInput("size", "尺码不能为空") + } + for _, field := range []struct { + name, label, value string + }{{"color", "颜色", color}, {"size", "尺码", size}, {"advice", "建议说明", advice}} { + if utf8.RuneCountInString(field.value) > maxShopeeSKUFieldRunes { + return invalidFieldInput(field.name, "%s最多 500 个字符", field.label) + } + } + + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + state, err := repository.GetShopeeSKUManualState(tx, skuRecordID) + if err != nil { + return err + } + if state == nil || state.GoodsID != goodsID { + return invalidFieldInput("sku_record_id", "蝦皮规格不存在或不属于当前商品,请刷新页面后重试") + } + sources, err := decodeShopeeSKUProvenance(state.FieldSources) + if err != nil { + return fmt.Errorf("读取蝦皮规格字段来源失败: %w", err) + } + observedTimes, err := decodeShopeeSKUProvenance(state.FieldObservedAt) + if err != nil { + return fmt.Errorf("读取蝦皮规格字段时间失败: %w", err) + } + at := now.UTC().Format(model.TimeLayout) + for _, field := range []string{"color", "size", "advice"} { + sources[field] = "manual" + observedTimes[field] = at + } + sourcesJSON, err := json.Marshal(sources) + if err != nil { + return err + } + timesJSON, err := json.Marshal(observedTimes) + if err != nil { + return err + } + updated, err := repository.UpdateShopeeSKUManualFields(tx, skuRecordID, goodsID, + color, size, advice, string(sourcesJSON), string(timesJSON), at) + if err != nil { + return err + } + if !updated { + return invalidFieldInput("sku_record_id", "蝦皮规格已发生变化,请刷新页面后重试") + } + return tx.Commit() +} + +func decodeShopeeSKUProvenance(raw string) (map[string]string, error) { + values := map[string]string{} + if strings.TrimSpace(raw) == "" { + return values, nil + } + if err := json.Unmarshal([]byte(raw), &values); err != nil { + return nil, err + } + return values, nil +} diff --git a/admin/service/shopee_sku_test.go b/admin/service/shopee_sku_test.go new file mode 100644 index 0000000..d6fdb17 --- /dev/null +++ b/admin/service/shopee_sku_test.go @@ -0,0 +1,122 @@ +package service + +import ( + "encoding/json" + "strings" + "testing" + "time" + + "cmautobuy/admin/model" + "cmautobuy/admin/repository" +) + +func TestUpdateShopeeSKUManual_保存并保留身份字段(t *testing.T) { + db := newCatalogTestDB(t) + actor := &model.User{UserID: "USER-1", Role: model.RolePurchaser, Status: model.UserActive} + goodsID, recordID := "S-EDIT", "REC-EDIT" + nowText := "2026-08-12T00:00:00Z" + if _, err := db.Exec(`INSERT INTO shopee_products(goods_id,title,source,created_at,updated_at) VALUES(?,?,'api',?,?)`, goodsID, "测试商品", nowText, nowText); err != nil { + t.Fatal(err) + } + if _, err := db.Exec(`INSERT INTO shopee_skus(sku_id,shopee_sku_id,goods_id,spec_raw,spec_key,color,size,advice,parse_ok,sku_code,is_manual,source,field_sources,field_observed_at,source_observed_at,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, recordID, "REAL-SKU-1", goodsID, "旧黑色,M【建议50公斤】", "稳定规格键", + "旧黑色", "M", "旧建议", 0, "CODE-1", 0, "partner", + `{"color":"partner","size":"partner","advice":"partner","sku_code":"partner"}`, + `{"color":"2026-08-12T00:00:00Z","size":"2026-08-12T00:00:00Z","advice":"2026-08-12T00:00:00Z","sku_code":"2026-08-12T00:00:00Z"}`, + nowText, nowText, nowText); err != nil { + t.Fatal(err) + } + updatedAt := time.Date(2026, 8, 13, 3, 4, 5, 0, time.UTC) + if err := UpdateShopeeSKUManual(db, actor, goodsID, recordID, " 新黑色 ", " L ", "", updatedAt); err != nil { + t.Fatal(err) + } + var shopeeSKU, specRaw, specKey, color, size, advice, skuCode, source, sourcesRaw, timesRaw, storedAt string + var parseOK, isManual int + if err := db.QueryRow(`SELECT shopee_sku_id,spec_raw,spec_key,color,size,COALESCE(advice,''),parse_ok,sku_code,is_manual,source,field_sources,field_observed_at,updated_at FROM shopee_skus WHERE sku_id=?`, recordID). + Scan(&shopeeSKU, &specRaw, &specKey, &color, &size, &advice, &parseOK, &skuCode, &isManual, &source, &sourcesRaw, ×Raw, &storedAt); err != nil { + t.Fatal(err) + } + if color != "新黑色" || size != "L" || advice != "" || parseOK != 1 { + t.Fatalf("人工字段未正确保存:color=%q size=%q advice=%q parse=%d", color, size, advice, parseOK) + } + if shopeeSKU != "REAL-SKU-1" || specRaw != "旧黑色,M【建议50公斤】" || specKey != "稳定规格键" || skuCode != "CODE-1" || isManual != 0 || source != "partner" { + t.Fatalf("不可变身份被修改:sku=%q raw=%q key=%q code=%q manual=%d source=%q", shopeeSKU, specRaw, specKey, skuCode, isManual, source) + } + var sources, times map[string]string + if err := json.Unmarshal([]byte(sourcesRaw), &sources); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(timesRaw), ×); err != nil { + t.Fatal(err) + } + for _, field := range []string{"color", "size", "advice"} { + if sources[field] != "manual" || times[field] != updatedAt.Format(model.TimeLayout) { + t.Fatalf("%s 的人工来源错误:sources=%v times=%v", field, sources, times) + } + } + if sources["sku_code"] != "partner" || storedAt != updatedAt.Format(model.TimeLayout) { + t.Fatalf("未编辑字段来源或更新时间错误:sources=%v updated=%q", sources, storedAt) + } + + for _, policy := range []string{"fill_missing", "overwrite_same_source"} { + outcome, err := repository.UpsertCatalogShopeeSKU(db, repository.CatalogShopeeSKUInput{ + RecordID: "IGNORED", ShopeeSKUID: "REAL-SKU-1", GoodsID: goodsID, + SpecRaw: specRaw, SpecKey: specKey, Color: "导入黑色", Size: "XL", Advice: "导入重新填入的建议", + ParseOK: true, SKUCode: "CODE-2", Source: "partner", ObservedAt: "2026-08-14T00:00:00Z", + Now: "2026-08-14T00:00:00Z", UpdatePolicy: policy, + }) + if err != nil { + t.Fatalf("%s 导入失败:%v", policy, err) + } + if outcome != repository.CatalogSKUFilled && outcome != repository.CatalogSKUSkipped && outcome != repository.CatalogSKUSameSourceUpdated { + t.Fatalf("%s 返回意外结果:%s", policy, outcome) + } + } + if err := db.QueryRow(`SELECT color,size,COALESCE(advice,'') FROM shopee_skus WHERE sku_id=?`, recordID).Scan(&color, &size, &advice); err != nil { + t.Fatal(err) + } + if color != "新黑色" || size != "L" || advice != "" { + t.Fatalf("人工字段被后续导入覆盖:%q/%q/%q", color, size, advice) + } +} + +func TestUpdateShopeeSKUManual_拒绝无效输入且不改数据(t *testing.T) { + cases := []struct { + name string + actor *model.User + goodsID, recordID string + color, size string + }{ + {"未登录", nil, "S-EDIT", "REC-EDIT", "黑色", "M"}, + {"停用账号", &model.User{Status: model.UserDisabled}, "S-EDIT", "REC-EDIT", "黑色", "M"}, + {"规格不存在", &model.User{Status: model.UserActive}, "S-EDIT", "NOT-FOUND", "黑色", "M"}, + {"跨商品", &model.User{Status: model.UserActive}, "S-OTHER", "REC-EDIT", "黑色", "M"}, + {"颜色为空", &model.User{Status: model.UserActive}, "S-EDIT", "REC-EDIT", " ", "M"}, + {"尺码为空", &model.User{Status: model.UserActive}, "S-EDIT", "REC-EDIT", "黑色", " "}, + {"颜色过长", &model.User{Status: model.UserActive}, "S-EDIT", "REC-EDIT", strings.Repeat("色", 501), "M"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + db := newCatalogTestDB(t) + now := "2026-08-12T00:00:00Z" + if _, err := db.Exec(`INSERT INTO shopee_products(goods_id,title,source,created_at,updated_at) VALUES('S-EDIT','测试','api',?,?)`, now, now); err != nil { + t.Fatal(err) + } + if _, err := db.Exec(`INSERT INTO shopee_skus(sku_id,goods_id,spec_raw,spec_key,color,size,parse_ok,is_manual,source,field_sources,field_observed_at,created_at,updated_at) + VALUES('REC-EDIT','S-EDIT','黑色,M','黑色,M','旧色','旧码',1,0,'partner','{}','{}',?,?)`, now, now); err != nil { + t.Fatal(err) + } + err := UpdateShopeeSKUManual(db, tc.actor, tc.goodsID, tc.recordID, tc.color, tc.size, "建议", time.Now()) + if err == nil { + t.Fatal("期望拒绝无效输入") + } + var color, size string + if err := db.QueryRow(`SELECT color,size FROM shopee_skus WHERE sku_id='REC-EDIT'`).Scan(&color, &size); err != nil { + t.Fatal(err) + } + if color != "旧色" || size != "旧码" { + t.Fatalf("失败后发生部分更新:%q/%q", color, size) + } + }) + } +} diff --git a/admin/static/css/app.css b/admin/static/css/app.css index d283192..ade8e50 100644 --- a/admin/static/css/app.css +++ b/admin/static/css/app.css @@ -256,6 +256,22 @@ tr.empty small { color: #aaa; } .thumb-placeholder, .thumb-fallback { color: #6b7280; font-size: 12px; white-space: nowrap; } .detail-product-image { width: 120px; max-height: 120px; object-fit: contain; border: 1px solid #ccd1d6; border-radius: 4px; } +/* 蝦皮 SKU 默认保持只读摘要,只在当前行展开编辑,避免规格多时整张表 + 被输入框淹没。表格自身负责横向滚动。 */ +.sku-editor > summary { color: #1f5f99; cursor: pointer; } +.sku-editor[open] > summary { margin-bottom: 8px; } +.sku-edit-form { min-width: 360px; white-space: normal; } +.sku-edit-grid { + display: grid; + grid-template-columns: repeat(2, minmax(140px, 1fr)); + gap: 8px; + margin-bottom: 8px; +} +.sku-edit-field { display: flex; flex-direction: column; gap: 4px; color: #555; } +.sku-edit-field input { min-width: 0; width: 100%; } +.sku-edit-advice { grid-column: 1 / -1; } +.sku-edit-form small { display: block; color: #777; margin-bottom: 8px; } + /* ── 弹窗 ─────────────────────────────── */ /* 用 hidden 属性控制显示隐藏,JS 只改这一个属性,不拼样式 */ .modal-backdrop[hidden] { display: none; } diff --git a/admin/templates/shopee/detail_modal.html b/admin/templates/shopee/detail_modal.html index 0e6856a..5373334 100644 --- a/admin/templates/shopee/detail_modal.html +++ b/admin/templates/shopee/detail_modal.html @@ -60,16 +60,47 @@ {{end}}

规格({{len .SKUs}} 个{{if gt .PendingCount 0}},{{.PendingCount}} 个待人工补{{end}})

+ {{$product := .}} {{if not .SKUs}}

这个商品在报表里没有任何 SKU。

{{else}}
- + {{range .SKUs}} - - {{if .Pending}}{{end}} + + + + + + + + + {{end}}
规格ID颜色尺码建議状态
蝦皮 SKU ID规格原文颜色尺码建议说明状态操作
{{.SKUID}}{{.Color}}{{.Size}}{{.Advice}}{{.StatusText}}
原文:{{.SpecRaw}}
{{if .SKUID}}{{.SKUID}}{{else}}—{{end}}{{if .SpecRaw}}{{.SpecRaw}}{{else}}—{{end}}{{if .Color}}{{.Color}}{{else}}—{{end}}{{if .Size}}{{.Size}}{{else}}—{{end}}{{if .Advice}}{{.Advice}}{{else}}—{{end}}{{.StatusText}} +
+ 编辑 +
+ + + + + + + + + + +
+ + + +
+ 规格原文和 SKU 身份不会修改;保存后这三个字段按人工值保护。 +
+
+
+
diff --git a/admin/templates/shopee/list.html b/admin/templates/shopee/list.html index 3b81ffe..5a2baf2 100644 --- a/admin/templates/shopee/list.html +++ b/admin/templates/shopee/list.html @@ -158,7 +158,7 @@ app.js 不需要改,见工单 #41。 弹窗同时提供当前 PDD 关联和单商品采集入口。 */}} -