feat(#74): 优化 SYB 商品筛选和列表布局
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Architecture-and-Code-Map
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Architecture-and-Code-Map.-
|
||||
wiki_revision: 9bb0da18b68d368b37c060af9867983ace6b49f6
|
||||
synchronized_at: 2026-08-24T03:42:12Z
|
||||
wiki_revision: 5f88de723d9ca9124383cd4abcfa5c27ddcd6d48
|
||||
synchronized_at: 2026-08-24T07:03:25Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 架构与代码地图
|
||||
@@ -130,7 +130,7 @@ Android Portal/Agent
|
||||
| Admin AI 规格匹配设置 | `web/src/views/goauto/ai-matching-settings/`、`web/src/api/goauto/ai-matching-settings.js`;管理员可查看、维护和测试 Provider(包括内部明文 API Key),采购员只可查看启用状态 |
|
||||
| Admin 失败采购任务批量重试 | `POST /api/admin/v1/purchase-tasks/batch-retry`;服务端 `server/app/goauto/purchase/retry.go` 负责资格判定、逐项幂等创建与部分成功结果,Admin 页面只允许选择服务端标记可重试的行;不修改 Android Agent |
|
||||
| SYB 店铺管理页面与接口封装 | `web/src/views/goauto/syb-shops/`、`web/src/api/goauto/syb-shops.js`;确认原型快照 `prototypes/49/v1/index.html` |
|
||||
| SYB 异步导入、当前页采购选择/确认/逐条结果与同步记录页面 | `web/src/views/goauto/syb-products/`、`web/src/views/goauto/syb-sync-runs/`、`web/src/api/goauto/syb-products.js`、`web/src/api/goauto/purchase-tasks.js`;确认原型见 #44 设计证据,导入原型快照为 `prototypes/50/v2/index.html` |
|
||||
| SYB 异步导入、商品筛选、当前页采购选择/确认/逐条结果与同步记录页面 | `web/src/views/goauto/syb-products/`、`web/src/views/goauto/syb-sync-runs/`、`web/src/api/goauto/syb-products.js`、`web/src/api/goauto/purchase-tasks.js`;商品列表按店铺名称包含匹配,并支持最多 100 个多行订单号精确筛选;确认原型见 #44 设计证据,导入原型快照为 `prototypes/50/v2/index.html` |
|
||||
| Android Agent 基线 | `android/app/src/main/java/cn/ilapage/goauto/agent/` |
|
||||
| Android 无障碍规则执行 | `android/app/src/main/java/cn/ilapage/goauto/agent/automation/` |
|
||||
| PDD 安全规格入口与评论页误触恢复 | `automation/PddProductDetailCollector.kt`:明确选择语义、底部购买文字、评论上下文排除、一次返回重试和结构化错误;不保存控件树或截图 |
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Business-Rules-and-Glossary
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Business-Rules-and-Glossary.-
|
||||
wiki_revision: 14e4596838288a4685ffac6b55b17fcecba40326
|
||||
synchronized_at: 2026-08-24T03:42:20Z
|
||||
wiki_revision: 5adb7b0f05c4562e23fe92fe7ff89625d697c2d4
|
||||
synchronized_at: 2026-08-24T07:03:44Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 业务规则与术语
|
||||
@@ -46,6 +46,7 @@ synchronized_at: 2026-08-24T03:42:20Z
|
||||
- 关联虾皮商品档案:命中存活记录直接关联;命中软删除记录则复活并保留原有人工映射;均不存在时创建最小档案。命中既有档案时只补写为空的参考图与售价,不覆盖人工修改过的标题、店铺和映射。
|
||||
- 重新解析只读取已保存的原始 JSON,不请求 SYB 接口,永不改写 `RawJSON`;标记 `manuallyConfirmed` 的明细默认跳过重新解析,除非显式指定强制覆盖。
|
||||
- 人工修正的颜色/尺码视为与解析成功同等可信,同样会合并进虾皮商品档案;标记为已人工确认。
|
||||
- Admin 商品列表的订单号筛选每行一个、去空去重后精确匹配,最多 100 个且单项最长 64 字符;店铺名称使用包含匹配。店铺、订单号与解析状态之间使用 AND,不再用该筛选框搜索蝦皮商品 ID 或商品标题。
|
||||
- SYB 商品数据本身不保存 PDD 订单号、快递单号、支付或有效订单状态;Admin 的采购任务创建入口位于 SYB 商品列表,任务事实仍写入独立采购域。
|
||||
|
||||
## SYB 店铺过滤
|
||||
|
||||
@@ -47,7 +47,7 @@ func (handler Handler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
response, err := service.List(c.Request.Context(), ListRequest{
|
||||
Page: page, PageSize: pageSize, Keyword: c.Query("keyword"), ParseStatus: strings.TrimSpace(c.Query("parseStatus")),
|
||||
Page: page, PageSize: pageSize, ShopName: c.Query("shopName"), OrderCodes: []string{c.Query("orderCodes")}, ParseStatus: strings.TrimSpace(c.Query("parseStatus")),
|
||||
})
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
|
||||
@@ -39,7 +39,8 @@ func NewService(db *gorm.DB) *Service { return &Service{DB: db} }
|
||||
|
||||
type ListRequest struct {
|
||||
Page, PageSize int
|
||||
Keyword string
|
||||
ShopName string
|
||||
OrderCodes []string
|
||||
ParseStatus string
|
||||
}
|
||||
|
||||
@@ -65,9 +66,18 @@ func (service *Service) List(ctx context.Context, request ListRequest) (ListResp
|
||||
request.PageSize = 100
|
||||
}
|
||||
query := service.DB.WithContext(ctx).Model(&models.SYBProduct{})
|
||||
if request.Keyword = strings.TrimSpace(request.Keyword); request.Keyword != "" {
|
||||
like := "%" + request.Keyword + "%"
|
||||
query = query.Where("order_code LIKE ? OR shopee_item_id LIKE ? OR product_title LIKE ?", like, like, like)
|
||||
if request.ShopName = strings.TrimSpace(request.ShopName); request.ShopName != "" {
|
||||
if len([]rune(request.ShopName)) > 255 {
|
||||
return ListResponse{}, invalidRequest("店铺名称不能超过 255 个字符")
|
||||
}
|
||||
query = query.Where("shop_name LIKE ?", "%"+request.ShopName+"%")
|
||||
}
|
||||
orderCodes, err := normalizeOrderCodes(request.OrderCodes)
|
||||
if err != nil {
|
||||
return ListResponse{}, err
|
||||
}
|
||||
if len(orderCodes) > 0 {
|
||||
query = query.Where("order_code IN ?", orderCodes)
|
||||
}
|
||||
if request.ParseStatus != "" {
|
||||
if request.ParseStatus != models.SYBParseStatusSuccess && request.ParseStatus != models.SYBParseStatusUncertain && request.ParseStatus != models.SYBParseStatusFailed {
|
||||
@@ -86,6 +96,28 @@ func (service *Service) List(ctx context.Context, request ListRequest) (ListResp
|
||||
return ListResponse{Items: items, Total: total, Page: request.Page, PageSize: request.PageSize}, nil
|
||||
}
|
||||
|
||||
func normalizeOrderCodes(raw []string) ([]string, error) {
|
||||
seen := make(map[string]bool, len(raw))
|
||||
result := make([]string, 0, len(raw))
|
||||
for _, chunk := range raw {
|
||||
for _, value := range strings.FieldsFunc(chunk, func(r rune) bool { return r == ',' || r == '\r' || r == '\n' }) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || seen[value] {
|
||||
continue
|
||||
}
|
||||
if len([]rune(value)) > 64 {
|
||||
return nil, invalidRequest("订单号不能超过 64 个字符")
|
||||
}
|
||||
seen[value] = true
|
||||
result = append(result, value)
|
||||
if len(result) > 100 {
|
||||
return nil, invalidRequest("一次最多查询 100 个订单号")
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DetailView exposes RawJSON, which the model deliberately keeps json:"-" so
|
||||
// List (and any other place a bare models.SYBProduct gets serialized) never
|
||||
// leaks the full raw payload by accident. This was caught before it shipped:
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -20,7 +21,7 @@ func serviceErrCode(t *testing.T, err error) string {
|
||||
return target.Code
|
||||
}
|
||||
|
||||
func TestServiceListFiltersByKeywordAndParseStatus(t *testing.T) {
|
||||
func TestServiceListFiltersByOrderCodesShopAndParseStatus(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
if _, err := sybimport.ApplyDetail(context.Background(), db, realOrder(), realDetailA()); err != nil {
|
||||
t.Fatalf("apply a: %v", err)
|
||||
@@ -28,14 +29,39 @@ func TestServiceListFiltersByKeywordAndParseStatus(t *testing.T) {
|
||||
if _, err := sybimport.ApplyDetail(context.Background(), db, realOrder(), realDetailB()); err != nil {
|
||||
t.Fatalf("apply b: %v", err)
|
||||
}
|
||||
otherOrder := realOrder()
|
||||
otherOrder.Code = "260729OTHER"
|
||||
otherOrder.StockID++
|
||||
otherOrder.ShopName = "其他店铺"
|
||||
otherDetail := realDetailA()
|
||||
otherDetail.ID++
|
||||
if _, err := sybimport.ApplyDetail(context.Background(), db, otherOrder, otherDetail); err != nil {
|
||||
t.Fatalf("apply other order: %v", err)
|
||||
}
|
||||
service := sybimport.NewService(db)
|
||||
|
||||
byKeyword, err := service.List(context.Background(), sybimport.ListRequest{Keyword: "260728TB95MJTQ"})
|
||||
byOrders, err := service.List(context.Background(), sybimport.ListRequest{OrderCodes: []string{" 260728TB95MJTQ\n", "260729OTHER", "260728TB95MJTQ"}})
|
||||
if err != nil {
|
||||
t.Fatalf("list by keyword: %v", err)
|
||||
t.Fatalf("list by order codes: %v", err)
|
||||
}
|
||||
if byKeyword.Total != 2 {
|
||||
t.Fatalf("expected 2 rows for the order code, got %d", byKeyword.Total)
|
||||
if byOrders.Total != 3 {
|
||||
t.Fatalf("expected all 3 rows for two exact order codes, got %d", byOrders.Total)
|
||||
}
|
||||
|
||||
combined, err := service.List(context.Background(), sybimport.ListRequest{ShopName: " 韓舍 ", OrderCodes: []string{"260728TB95MJTQ", "260729OTHER"}, ParseStatus: models.SYBParseStatusSuccess})
|
||||
if err != nil {
|
||||
t.Fatalf("list by combined filters: %v", err)
|
||||
}
|
||||
if combined.Total != 1 || combined.Items[0].OrderCode != "260728TB95MJTQ" {
|
||||
t.Fatalf("shop, order and status filters must use AND: %+v", combined)
|
||||
}
|
||||
|
||||
partialOrder, err := service.List(context.Background(), sybimport.ListRequest{OrderCodes: []string{"260728"}})
|
||||
if err != nil {
|
||||
t.Fatalf("list by partial order: %v", err)
|
||||
}
|
||||
if partialOrder.Total != 0 {
|
||||
t.Fatalf("order codes must use exact matching, got %d rows", partialOrder.Total)
|
||||
}
|
||||
|
||||
byStatus, err := service.List(context.Background(), sybimport.ListRequest{ParseStatus: models.SYBParseStatusUncertain})
|
||||
@@ -47,6 +73,26 @@ func TestServiceListFiltersByKeywordAndParseStatus(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceListRejectsTooManyOrTooLongOrderCodes(t *testing.T) {
|
||||
service := sybimport.NewService(openTestDB(t))
|
||||
tooMany := make([]string, 101)
|
||||
for i := range tooMany {
|
||||
tooMany[i] = fmt.Sprintf("ORDER-%03d", i)
|
||||
}
|
||||
if code := serviceErrCode(t, func() error {
|
||||
_, err := service.List(context.Background(), sybimport.ListRequest{OrderCodes: tooMany})
|
||||
return err
|
||||
}()); code != sybimport.CodeInvalidRequest {
|
||||
t.Fatalf("expected too-many rejection, got %s", code)
|
||||
}
|
||||
if code := serviceErrCode(t, func() error {
|
||||
_, err := service.List(context.Background(), sybimport.ListRequest{OrderCodes: []string{strings.Repeat("A", 65)}})
|
||||
return err
|
||||
}()); code != sybimport.CodeInvalidRequest {
|
||||
t.Fatalf("expected too-long rejection, got %s", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceListRejectsInvalidParseStatus(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
service := sybimport.NewService(db)
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
<el-form-item><el-tooltip :disabled="isAdmin" content="只有管理员可以开始导入" placement="bottom"><span><el-button type="primary" :icon="Download" :disabled="!isAdmin" @click="openImport">导入</el-button></span></el-tooltip></el-form-item>
|
||||
<el-form-item><el-button @click="$router.push('/syb-sync-runs/index')">同步记录</el-button></el-form-item>
|
||||
<el-form-item v-if="canPurchase"><span class="selection-count" aria-live="polite">已选择 <strong>{{ selectedProducts.length }}</strong> 条</span></el-form-item>
|
||||
<el-form-item label="搜索"><el-input v-model="query.keyword" class="keyword-input" placeholder="订单号、虾皮商品ID或商品标题" clearable @keyup.enter="search" /></el-form-item>
|
||||
<el-form-item label="店铺"><el-input v-model="query.shopName" class="shop-input" placeholder="输入店铺名称" clearable /></el-form-item>
|
||||
<el-form-item label="订单号"><el-input v-model="query.orderCodesText" type="textarea" :autosize="{ minRows: 1, maxRows: 4 }" resize="none" class="order-codes-input" placeholder="每行一个订单号,最多 100 个" @keydown.ctrl.enter.prevent="search" /></el-form-item>
|
||||
<el-form-item label="解析状态"><el-select v-model="query.parseStatus" clearable placeholder="全部" style="width: 110px"><el-option label="成功" value="success" /><el-option label="存疑" value="uncertain" /><el-option label="失败" value="failed" /></el-select></el-form-item>
|
||||
<el-form-item class="toolbar-actions"><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="RefreshLeft" @click="reset">重置</el-button><el-button v-if="canPurchase" type="primary" :disabled="selectedProducts.length === 0" @click="openPurchaseBatch">创建采购</el-button></el-form-item>
|
||||
</el-form>
|
||||
@@ -24,15 +25,14 @@
|
||||
<el-table-column v-if="canPurchase" type="selection" width="48" :selectable="rowSelectable" />
|
||||
<el-table-column label="参考图" width="76"><template #default="{ row }"><el-image v-if="row.imageUrl" :src="row.imageUrl" fit="cover" class="thumb" :preview-src-list="[row.imageUrl]" preview-teleported /><div v-else class="thumb placeholder">无图</div></template></el-table-column>
|
||||
<el-table-column label="订单号" prop="orderCode" min-width="120" />
|
||||
<el-table-column label="虾皮商品" min-width="421"><template #default="{ row }"><a class="link" href="javascript:void(0)" @click="openShopeeDetail(row.shopeeProductId)">{{ row.shopeeItemId }} ↗</a><div class="muted">{{ row.productTitle || '未取到标题' }}</div></template></el-table-column>
|
||||
<el-table-column label="虾皮商品" min-width="421"><template #default="{ row }"><template v-if="row.shopeeProductId"><a class="link" href="javascript:void(0)" @click="openShopeeDetail(row.shopeeProductId)">{{ row.shopeeItemId }} ↗</a></template><template v-else><span>{{ row.shopeeItemId || '—' }}</span><el-tag size="small" type="warning" effect="plain" class="unlinked-tag">未关联</el-tag></template><div class="muted">{{ row.productTitle || '未取到标题' }}</div></template></el-table-column>
|
||||
<el-table-column label="店铺" prop="shopName" min-width="100" />
|
||||
<el-table-column label="目标颜色 / 尺码" min-width="128"><template #default="{ row }"><div :class="{ 'missing': !row.targetColor }">颜色:{{ row.targetColor || '—' }}</div><div :class="{ 'missing': !row.targetSize }">尺码:{{ row.targetSize || '—' }}</div></template></el-table-column>
|
||||
<el-table-column label="数量" prop="quantity" width="56" />
|
||||
<el-table-column label="售价" width="110"><template #default="{ row }">{{ priceText(row) }}</template></el-table-column>
|
||||
<el-table-column label="虾皮关联" width="80"><template #default="{ row }"><el-tag :type="row.shopeeProductId ? 'success' : 'info'">{{ row.shopeeProductId ? '已关联' : '未关联' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="解析状态" width="80"><template #default="{ row }"><el-tag :type="parseMeta(row.parseStatus).type">{{ parseMeta(row.parseStatus).label }}</el-tag><el-tag v-if="row.manuallyConfirmed" size="small" type="success" effect="plain" style="margin-left:4px">人工已确认</el-tag></template></el-table-column>
|
||||
<el-table-column v-if="canPurchase" label="采购准备" min-width="170"><template #default="{ row }"><span v-if="purchaseReadinessLoading" class="muted">正在检查</span><template v-else-if="purchaseReady(row).eligible"><el-tag type="success">可以创建</el-tag><div class="muted">{{ purchasePriceText(purchaseReady(row)) }}</div></template><template v-else><el-tag type="warning">暂不可创建</el-tag><div class="purchase-reason">{{ purchaseReady(row).reason || '请刷新后重试' }}</div><el-button v-if="purchaseActionLabel(purchaseReady(row))" type="primary" link @click="runPurchaseNextAction(row, purchaseReady(row))">{{ purchaseActionLabel(purchaseReady(row)) }}</el-button></template></template></el-table-column>
|
||||
<el-table-column label="操作" width="92" fixed="right"><template #default="{ row }"><el-button type="primary" link @click="openDetail(row.id)">详情</el-button><el-button v-if="canPurchase && purchaseReady(row).eligible" type="primary" link @click="openSinglePurchase(row)">采购</el-button></template></el-table-column>
|
||||
<el-table-column v-if="canPurchase" label="采购准备" min-width="210"><template #default="{ row }"><span v-if="purchaseReadinessLoading" class="muted">正在检查</span><template v-else-if="purchaseReady(row).eligible"><el-tag type="success">可以创建</el-tag><div class="muted">{{ purchasePriceText(purchaseReady(row)) }}</div></template><template v-else><el-tag type="warning">暂不可创建</el-tag><div class="purchase-reason">{{ purchaseReady(row).reason || '请刷新后重试' }}</div><el-button v-if="purchaseActionLabel(purchaseReady(row))" type="primary" link @click="runPurchaseNextAction(row, purchaseReady(row))">{{ purchaseActionLabel(purchaseReady(row)) }}</el-button></template></template></el-table-column>
|
||||
<el-table-column label="操作" width="132" fixed="right"><template #default="{ row }"><el-button type="primary" link @click="openDetail(row.id)">详情</el-button><el-button v-if="canPurchase && purchaseReady(row).eligible" type="primary" link @click="openSinglePurchase(row)">采购</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
<pagination v-show="total > 0" v-model:current-page="query.page" v-model:page-size="query.pageSize" :total="total" @pagination="load" />
|
||||
<p class="scope-note">本页不展示采购任务、PDD 订单号、快递单号和支付状态。</p>
|
||||
@@ -157,7 +157,7 @@ export default {
|
||||
purchaseDevices: [],
|
||||
purchaseDialog: { open: false, loading: false, saving: false, ids: [], deviceId: null, items: [], eligibleCount: 0, skippedCount: 0 },
|
||||
purchaseResult: { open: false, items: [], createdCount: 0, failedCount: 0 },
|
||||
query: { page: 1, pageSize: 20, keyword: '', parseStatus: '' },
|
||||
query: { page: 1, pageSize: 20, shopName: '', orderCodesText: '', parseStatus: '' },
|
||||
importDialog: { open: false, saving: false, range: null, spanDays: 0 },
|
||||
activeRun: null, syncPollTimer: null,
|
||||
detail: { open: false, loading: false, item: null },
|
||||
@@ -185,13 +185,15 @@ export default {
|
||||
beforeUnmount() { this.stopSyncPolling() },
|
||||
methods: {
|
||||
async load(allowNetworkRetry = true) {
|
||||
const orderCodes = this.normalizedOrderCodes()
|
||||
if (orderCodes === null) return
|
||||
const generation = ++this.loadGeneration
|
||||
this.loading = true
|
||||
this.selectedProducts = []
|
||||
this.$refs.productTable?.clearSelection()
|
||||
const requestOptions = allowNetworkRetry ? { suppressNetworkError: true } : {}
|
||||
try {
|
||||
const r = await listSybProducts(this.query, requestOptions)
|
||||
const r = await listSybProducts({ page: this.query.page, pageSize: this.query.pageSize, shopName: this.query.shopName.trim(), orderCodes: orderCodes.join(','), parseStatus: this.query.parseStatus }, requestOptions)
|
||||
if (generation !== this.loadGeneration) return
|
||||
this.products = r.data.items
|
||||
this.total = r.data.total
|
||||
@@ -210,7 +212,13 @@ export default {
|
||||
}
|
||||
},
|
||||
search() { this.query.page = 1; this.load() },
|
||||
reset() { this.query = { page: 1, pageSize: 20, keyword: '', parseStatus: '' }; this.load() },
|
||||
reset() { this.query = { page: 1, pageSize: 20, shopName: '', orderCodesText: '', parseStatus: '' }; this.load() },
|
||||
normalizedOrderCodes() {
|
||||
const values = [...new Set(this.query.orderCodesText.split(/\r?\n/).map(value => value.trim()).filter(Boolean))]
|
||||
if (values.length > 100) { ElMessage.warning('一次最多查询 100 个订单号'); return null }
|
||||
if (values.some(value => [...value].length > 64)) { ElMessage.warning('订单号不能超过 64 个字符'); return null }
|
||||
return values
|
||||
},
|
||||
handleSelectionChange(rows) { this.selectedProducts = rows },
|
||||
rowSelectable(row) { return this.canPurchase && this.purchaseReady(row).eligible },
|
||||
purchaseReady(row) { return this.purchaseReadiness[row.id] || { sybProductId: row.id, eligible: false, reason: this.purchaseReadinessLoading ? '正在检查' : '请刷新后重试' } },
|
||||
@@ -379,7 +387,7 @@ export default {
|
||||
.search-form{display:flex;flex-wrap:wrap;align-items:center;column-gap:8px;padding:12px 16px 0;margin-bottom:16px;border:1px solid #e5e7eb;border-radius:8px;background:#f8fafc}
|
||||
.search-form :deep(.el-form-item){margin-right:0;margin-bottom:12px}
|
||||
.search-form :deep(.el-form-item__content){flex-wrap:nowrap}
|
||||
.keyword-input{width:170px}
|
||||
.shop-input{width:130px}.order-codes-input{width:210px}.unlinked-tag{margin-left:8px}
|
||||
.toolbar-actions :deep(.el-form-item__content){gap:8px}
|
||||
.toolbar-actions :deep(.el-button + .el-button){margin-left:0}
|
||||
.selection-count{display:inline-flex;align-items:center;min-height:32px;white-space:nowrap;color:#606266}
|
||||
|
||||
@@ -85,7 +85,7 @@ test('新的查询结果不会被旧采购预检覆盖', async({ page, context }
|
||||
if (url.pathname.startsWith('/src/api/')) return route.continue()
|
||||
if (url.pathname.endsWith('/api/v1/getinfo')) return route.fulfill({ json: { code: 200, data: { roles: ['admin'], name: '管理员', avatar: '', introduction: '', permissions: [] }}})
|
||||
if (url.pathname.endsWith('/api/admin/v1/syb-products')) {
|
||||
const searched = url.searchParams.get('keyword') === '新商品'
|
||||
const searched = url.searchParams.get('orderCodes') === 'NEW-76'
|
||||
const item = searched
|
||||
? { id: 76, orderCode: 'NEW-76', shopeeItemId: '10076', productTitle: '新商品', quantity: 1, parseStatus: 'success' }
|
||||
: { id: 75, orderCode: 'OLD-75', shopeeItemId: '10075', productTitle: '旧商品', quantity: 1, parseStatus: 'success' }
|
||||
@@ -104,7 +104,7 @@ test('新的查询结果不会被旧采购预检覆盖', async({ page, context }
|
||||
|
||||
await page.goto('http://localhost:9527/#/syb-products/index')
|
||||
await expect(page.getByText('旧商品')).toBeVisible()
|
||||
await page.getByPlaceholder('订单号、虾皮商品ID或商品标题').fill('新商品')
|
||||
await page.getByPlaceholder('每行一个订单号,最多 100 个').fill('NEW-76')
|
||||
await page.getByRole('button', { name: '查询' }).click()
|
||||
await expect(page.getByText('新商品', { exact: true })).toBeVisible()
|
||||
await expect(page.getByText('新商品暂不可创建')).toBeVisible()
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test('SYB 商品工具栏与列宽按紧凑布局显示', async({ page, context }) => {
|
||||
await page.setViewportSize({ width: 1920, height: 1080 })
|
||||
await context.addCookies([{ name: 'Admin-Token', value: 'prototype-test-token', domain: 'localhost', path: '/' }])
|
||||
const listURLs: URL[] = []
|
||||
await page.route('**/api/**', async route => {
|
||||
const url = new URL(route.request().url())
|
||||
if (url.pathname.startsWith('/src/api/')) return route.continue()
|
||||
if (url.pathname.endsWith('/api/v1/getinfo')) return route.fulfill({ json: { code: 200, data: { roles: ['admin'], name: '管理员', avatar: '', introduction: '', permissions: [] }}})
|
||||
if (url.pathname.endsWith('/api/admin/v1/syb-products')) return route.fulfill({ json: { code: 200, data: { items: [{ id: 1, orderCode: 'SYB001', shopeeItemId: '26154802794', shopeeProductId: 9, productTitle: '用于验证加宽后的虾皮商品标题区域', shopName: '大碼臻選', targetColor: '黑色', targetSize: 'XL', quantity: 2, unitPriceCent: 1000, parseStatus: 'success', manuallyConfirmed: false }], total: 1, page: 1, pageSize: 20 }}})
|
||||
if (url.pathname.endsWith('/api/admin/v1/purchase-tasks/batch-preview')) return route.fulfill({ json: { code: 200, data: { items: [{ sybProductId: 1, eligible: true, minUnitPriceCent: 200, maxUnitPriceCent: 1500 }], eligibleCount: 1, skippedCount: 0 }}})
|
||||
if (url.pathname.endsWith('/api/admin/v1/syb-products')) { listURLs.push(url); return route.fulfill({ json: { code: 200, data: { items: [{ id: 1, orderCode: 'SYB001', shopeeItemId: '26154802794', shopeeProductId: 9, productTitle: '用于验证加宽后的虾皮商品标题区域', shopName: '大碼臻選', targetColor: '黑色', targetSize: 'XL', quantity: 2, unitPriceCent: 1000, parseStatus: 'success', manuallyConfirmed: false }, { id: 2, orderCode: 'SYB002', shopeeItemId: '26154802795', productTitle: '尚未关联的商品', shopName: '大碼臻選', targetColor: '白色', targetSize: 'L', quantity: 1, unitPriceCent: 900, parseStatus: 'success', manuallyConfirmed: false }], total: 2, page: 1, pageSize: 20 }}}) }
|
||||
if (url.pathname.endsWith('/api/admin/v1/purchase-tasks/batch-preview')) return route.fulfill({ json: { code: 200, data: { items: [{ sybProductId: 1, eligible: true, minUnitPriceCent: 200, maxUnitPriceCent: 1500 }, { sybProductId: 2, eligible: false, reason: '尚未关联蝦皮商品' }], eligibleCount: 1, skippedCount: 1 }}})
|
||||
return route.fulfill({ json: { code: 200, data: [] }})
|
||||
})
|
||||
|
||||
@@ -17,6 +19,9 @@ test('SYB 商品工具栏与列宽按紧凑布局显示', async({ page, context
|
||||
await expect(page.getByRole('button', { name: '从 SYB 导入', exact: true })).toHaveCount(0)
|
||||
await expect(page.getByText('已选择 0 条', { exact: true })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: '重新解析', exact: true })).toHaveCount(0)
|
||||
await expect(page.getByRole('columnheader', { name: '蝦皮关联' })).toHaveCount(0)
|
||||
await expect(page.getByText('未关联', { exact: true }).last()).toBeVisible()
|
||||
await expect(page.getByText('26154802795 ↗', { exact: true })).toHaveCount(0)
|
||||
|
||||
const importButton = await page.getByRole('button', { name: '导入', exact: true }).boundingBox()
|
||||
const queryButton = await page.getByRole('button', { name: '查询', exact: true }).boundingBox()
|
||||
@@ -27,11 +32,24 @@ test('SYB 商品工具栏与列宽按紧凑布局显示', async({ page, context
|
||||
expect(Math.abs((reset?.y || 0) - (purchase?.y || 0))).toBeLessThan(2)
|
||||
expect(purchase?.x || 0).toBeGreaterThan((reset?.x || 0) + (reset?.width || 0))
|
||||
await expect(page.getByRole('button', { name: '创建采购', exact: true })).toBeDisabled()
|
||||
const shopInput = await page.getByPlaceholder('输入店铺名称').boundingBox()
|
||||
const orderInput = await page.getByPlaceholder('每行一个订单号,最多 100 个').boundingBox()
|
||||
expect(shopInput).not.toBeNull(); expect(orderInput).not.toBeNull()
|
||||
expect(shopInput?.x || 0).toBeLessThan(orderInput?.x || 0)
|
||||
|
||||
await page.locator('.el-table__body-wrapper tbody tr').first().locator('.el-checkbox').click()
|
||||
await expect(page.getByText('已选择 1 条', { exact: true })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: '创建采购', exact: true })).toBeEnabled()
|
||||
|
||||
const widths = await page.locator('.el-table__header-wrapper colgroup col').evaluateAll(cols => cols.map(col => Number(col.getAttribute('width'))))
|
||||
expect(widths.slice(0, 10)).toEqual([48, 76, 120, 421, 100, 128, 56, 110, 80, 80])
|
||||
expect(widths[9]).toBeGreaterThanOrEqual(210)
|
||||
expect(widths[10]).toBe(132)
|
||||
|
||||
await page.getByPlaceholder('输入店铺名称').fill('大碼')
|
||||
await page.getByPlaceholder('每行一个订单号,最多 100 个').fill(' SYB001\n\nSYB002\nSYB001 ')
|
||||
await page.getByPlaceholder('每行一个订单号,最多 100 个').press('Control+Enter')
|
||||
await expect.poll(() => listURLs.length).toBeGreaterThan(1)
|
||||
const latest = listURLs.at(-1)
|
||||
expect(latest?.searchParams.get('shopName')).toBe('大碼')
|
||||
expect(latest?.searchParams.get('orderCodes')).toBe('SYB001,SYB002')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user