From 0fccbc99c1d675b64d52ae337737498f8f8c709d Mon Sep 17 00:00:00 2001
From: QiuSW <105186638@qq.com>
Date: Mon, 24 Aug 2026 15:10:47 +0800
Subject: [PATCH] =?UTF-8?q?feat(#74):=20=E4=BC=98=E5=8C=96=20SYB=20?=
=?UTF-8?q?=E5=95=86=E5=93=81=E7=AD=9B=E9=80=89=E5=92=8C=E5=88=97=E8=A1=A8?=
=?UTF-8?q?=E5=B8=83=E5=B1=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/02-architecture-and-code-map.md | 6 +--
docs/03-business-rules-and-glossary.md | 5 +-
server/app/goauto/sybimport/handler.go | 2 +-
server/app/goauto/sybimport/service.go | 40 +++++++++++++--
server/app/goauto/sybimport/service_test.go | 56 +++++++++++++++++++--
web/src/views/goauto/syb-products/index.vue | 26 ++++++----
web/tests/e2e/syb-network-retry.spec.ts | 4 +-
web/tests/e2e/syb-product-layout.spec.ts | 24 +++++++--
8 files changed, 134 insertions(+), 29 deletions(-)
diff --git a/docs/02-architecture-and-code-map.md b/docs/02-architecture-and-code-map.md
index cafd0a9..db7a30a 100644
--- a/docs/02-architecture-and-code-map.md
+++ b/docs/02-architecture-and-code-map.md
@@ -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
# 架构与代码地图
@@ -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`:明确选择语义、底部购买文字、评论上下文排除、一次返回重试和结构化错误;不保存控件树或截图 |
diff --git a/docs/03-business-rules-and-glossary.md b/docs/03-business-rules-and-glossary.md
index ee1fbcc..0402d8d 100644
--- a/docs/03-business-rules-and-glossary.md
+++ b/docs/03-business-rules-and-glossary.md
@@ -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
# 业务规则与术语
@@ -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 店铺过滤
diff --git a/server/app/goauto/sybimport/handler.go b/server/app/goauto/sybimport/handler.go
index bd8fde4..e1cb72e 100644
--- a/server/app/goauto/sybimport/handler.go
+++ b/server/app/goauto/sybimport/handler.go
@@ -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)
diff --git a/server/app/goauto/sybimport/service.go b/server/app/goauto/sybimport/service.go
index 7e7c0f7..c5c543f 100644
--- a/server/app/goauto/sybimport/service.go
+++ b/server/app/goauto/sybimport/service.go
@@ -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:
diff --git a/server/app/goauto/sybimport/service_test.go b/server/app/goauto/sybimport/service_test.go
index 5e6394d..87d3974 100644
--- a/server/app/goauto/sybimport/service_test.go
+++ b/server/app/goauto/sybimport/service_test.go
@@ -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)
diff --git a/web/src/views/goauto/syb-products/index.vue b/web/src/views/goauto/syb-products/index.vue
index 3dab71b..cbb10ba 100644
--- a/web/src/views/goauto/syb-products/index.vue
+++ b/web/src/views/goauto/syb-products/index.vue
@@ -6,7 +6,8 @@
本页不展示采购任务、PDD 订单号、快递单号和支付状态。
@@ -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} diff --git a/web/tests/e2e/syb-network-retry.spec.ts b/web/tests/e2e/syb-network-retry.spec.ts index e135370..3233a4f 100644 --- a/web/tests/e2e/syb-network-retry.spec.ts +++ b/web/tests/e2e/syb-network-retry.spec.ts @@ -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() diff --git a/web/tests/e2e/syb-product-layout.spec.ts b/web/tests/e2e/syb-product-layout.spec.ts index 7316dbb..2b97d13 100644 --- a/web/tests/e2e/syb-product-layout.spec.ts +++ b/web/tests/e2e/syb-product-layout.spec.ts @@ -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') })