From 33a0c48b781e3cb810c378beb58f78a846d83f71 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 6 Aug 2026 15:32:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 10 + .../pages/CarrierRatesView.test.ts | 9 +- .../carrier-rates/pages/CarrierRatesView.vue | 26 +- .../api/platformPartners.test.ts | 4 +- .../counterparties/pages/PartnersView.vue | 110 +------- .../pages/ExpressCompaniesView.test.ts | 2 + .../pages/ExpressCompaniesView.vue | 180 +------------ .../unified-orders/api/unifiedOrders.test.ts | 20 +- .../unified-orders/api/unifiedOrders.ts | 12 + .../pages/UnifiedOrdersView.test.ts | 26 ++ .../pages/UnifiedOrdersView.vue | 187 +++++++++++-- .../locations/api/platformLocations.test.ts | 24 +- .../locations/api/platformLocations.ts | 19 +- .../locations/pages/LocationsView.test.ts | 10 +- .../locations/pages/LocationsView.vue | 28 +- .../pallets/api/platformPallets.test.ts | 20 +- .../pallets/api/platformPallets.ts | 20 +- .../pallets/pages/PalletsView.test.ts | 10 +- .../pallets/pages/PalletsView.vue | 18 +- .../components/BatchImportDialog.test.ts | 90 +++++++ src/shared/components/BatchImportDialog.vue | 245 ++++++++++++++++++ src/shared/components/QueryToolbar.vue | 17 +- 22 files changed, 685 insertions(+), 402 deletions(-) create mode 100644 src/shared/components/BatchImportDialog.test.ts create mode 100644 src/shared/components/BatchImportDialog.vue diff --git a/AGENTS.md b/AGENTS.md index 82088b0a..742ae602 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,6 +52,16 @@ - 保持键盘可操作、可见焦点、表单标签和足够的颜色对比度;不要只用颜色表达状态。 - 新页面至少适配现有 320px 最小宽度策略,并检查侧栏、表格、抽屉和表单在窄屏下的退化方式。 +## 公共前端组件复用 + +- 开发或重构页面前,先检查 `src/shared/components/`,不得在业务页面重复实现已有的查询栏、数据表格、批量导入、弹窗、抽屉、加载错误、状态和复制交互。公共组件无法满足时,先判断能力是否具有跨模块复用价值:通用能力扩展公共组件并补测试;纯业务特例留在所属模块,不用页面级 CSS 覆盖破坏公共组件契约。 +- 查询与筛选统一使用 `src/shared/components/QueryToolbar.vue`。通过 `fields` 配置 `input`、`select`、`date`、`daterange`,使用 `v-model` 管理查询模型,并监听 `search`、`reset`;额外操作放入 `actions` 插槽。不要在每个列表页重新编写筛选容器、查询/重置按钮和移动端换行样式。 +- 业务数据列表统一使用 `src/shared/components/DataTable.vue`。通过 `columns` 和命名插槽定义列,使用 `v-model:page`、`v-model:page-size` 及 `total` 接入分页;无需分页时可省略 `total`。不得重复拼装 `el-table` 与分页器;“操作”列仍须遵守仓库根规则,放在第一列,固定时固定在左侧。 +- 标准 XLSX 批量导入/上传统一使用 `src/shared/components/BatchImportDialog.vue`。页面只维护弹窗开关,传入标题、实体名称、模板文件名、`downloadTemplate` 和 `importFile`,并在 `success` 后刷新列表;文件格式和大小校验、模板下载、提交状态、接口错误、整批校验结果、CSV 错误报告及响应式样式由组件负责。不要在页面内重复创建文件输入框、上传状态、错误报告逻辑或导入弹窗 CSS。 +- 普通模态框和侧滑编辑/详情优先使用 `src/shared/components/AppDialog.vue`、`AppDrawer.vue`,复用提交锁定、加载态、底部按钮和窄屏约束;仅当业务交互与其契约明显不同时直接使用 Element Plus 原始容器。 +- 列表或详情加载失败优先组合 `src/shared/components/LoadErrorState.vue` 与 `ApiErrorMeta.vue`,保留重试入口及 `requestId`、`traceId`;状态展示和复制操作分别优先使用 `StatusBadge.vue`、`CopyButton.vue`。 +- 公共组件的使用示例优先参考已接入页面:查询和表格见 `modules/master-data/express-companies/pages/ExpressCompaniesView.vue`,批量导入见该页面及 `modules/master-data/counterparties/pages/PartnersView.vue`,抽屉见 `modules/downloads/pages/DownloadCenterView.vue`。扩展公共组件时同步检查所有现有消费者,并至少运行组件测试和直接受影响页面测试。 + ## 本地命令与质量门禁 在 `apps/admin-web` 目录执行: diff --git a/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.test.ts b/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.test.ts index 863950b1..3103ddf1 100644 --- a/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.test.ts +++ b/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.test.ts @@ -32,7 +32,7 @@ describe('CarrierRatesView source contract', () => { expect(source.indexOf("key: 'customerId'")).toBeLessThan(source.indexOf("key: 'rateCode'")) expect(source).toContain("'aria-label': '所属企业'") expect(source).toContain('listPlatformTenants') - expect(source).toContain('loadedCustomerId') + expect(source).toContain('masterDataLoaded') expect(source).not.toContain('PlatformTenantSelector') expect(source).not.toContain('selectedTenantId') expect(source).not.toContain('租户') @@ -69,6 +69,13 @@ describe('CarrierRatesView source contract', () => { expect(source).toContain('平台仓库或船司资料加载失败') expect(source).not.toContain('listPlatformPartners(customerId,') expect(source).not.toContain('所属企业的仓库或船司资料加载失败') + expect(source).toContain('async function loadPlatformMasterData()') + expect(source).not.toContain('loadCustomerMasterData') + }) + + it('keeps customer rate queries independent from edit-only master data', () => { + expect(source).toContain('await loadRateData(customerId)') + expect(source).not.toContain('loadPlatformMasterData(customerId)') }) it('inherits QueryToolbar wrapping and full-width mobile controls', () => { diff --git a/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue b/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue index 20b92b04..f9c8b79f 100644 --- a/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue +++ b/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue @@ -54,7 +54,7 @@ const access = useAccessPreviewStore() const customers = ref([]) const customersLoading = ref(false) const customerLoadError = ref(null) -const loadedCustomerId = ref(null) +const masterDataLoaded = ref(false) const rows = ref([]) const warehouses = ref([]) const carriers = ref([]) @@ -188,12 +188,7 @@ const rules: FormRules = { effectiveTo: [{ validator: validateEffectiveTo, trigger: 'change' }], } -watch(() => filters.value.customerId, (customerId) => { - if (customerId === loadedCustomerId.value) return - masterDataSequence += 1 - warehouses.value = [] - carriers.value = [] - loadedCustomerId.value = null +watch(() => filters.value.customerId, () => { page.value = 1 }) @@ -269,8 +264,8 @@ async function loadRateData(customerId = filters.value.customerId) { } } -async function loadCustomerMasterData(customerId: string) { - if (!customerId) return false +async function loadPlatformMasterData() { + if (masterDataLoaded.value) return true const sequence = ++masterDataSequence masterDataLoading.value = true try { @@ -281,13 +276,13 @@ async function loadCustomerMasterData(customerId: string) { if (sequence !== masterDataSequence) return false warehouses.value = warehouseResult.records.map((item) => ({ ...item, id: String(item.id), tenantId: String(item.tenantId) })) carriers.value = partnerResult.records.map((item) => ({ ...item, id: String(item.id), tenantId: String(item.tenantId), dataScopeId: String(item.dataScopeId) })) - loadedCustomerId.value = customerId + masterDataLoaded.value = true return true } catch (error) { if (sequence === masterDataSequence) { warehouses.value = [] carriers.value = [] - loadedCustomerId.value = null + masterDataLoaded.value = false ElMessage.error(getApiErrorDetails(error, '平台仓库或船司资料加载失败').message) } return false @@ -307,10 +302,7 @@ async function applyQuery(model: QueryModel) { status: (model.status || '') as CarrierRateStatus | '', } page.value = 1 - await Promise.all([ - loadRateData(customerId), - customerId && loadedCustomerId.value !== customerId ? loadCustomerMasterData(customerId) : Promise.resolve(true), - ]) + await loadRateData(customerId) } function resetQuery(model: QueryModel) { @@ -349,8 +341,8 @@ async function openCreate(rate?: PlatformCarrierRateResource) { ElMessage.warning('新增价格版本需要先选择所属企业') return } - if (loadedCustomerId.value !== customerId) { - const loaded = await loadCustomerMasterData(customerId) + if (!masterDataLoaded.value) { + const loaded = await loadPlatformMasterData() if (!loaded) return } editorCustomerId.value = customerId diff --git a/src/modules/master-data/counterparties/api/platformPartners.test.ts b/src/modules/master-data/counterparties/api/platformPartners.test.ts index 9ad029fd..923554ed 100644 --- a/src/modules/master-data/counterparties/api/platformPartners.test.ts +++ b/src/modules/master-data/counterparties/api/platformPartners.test.ts @@ -65,7 +65,7 @@ describe('platform partner API', () => { it('normalizes legacy detail types and uses import contracts', async () => { vi.mocked(http.get) .mockResolvedValueOnce({ code: '200', message: 'success', data: { - id: '19001', tenantId: 10001, partnerCode: 'CUS-001', partnerName: '测试单位', + id: '19001', tenantId: 0, dataScopeId: 0, partnerCode: 'CUS-001', partnerName: '测试单位', partnerType: 'BOTH', contactName: '张三', contactPhone: '13800000000', contactEmail: null, countryCode: 'CN', province: null, city: '深圳市', district: null, addressLine: '测试地址', postalCode: null, status: 'ACTIVE', @@ -81,7 +81,7 @@ describe('platform partner API', () => { const result = await importPlatformPartners(file) expect(detail.types).toEqual(['CUSTOMER', 'SUPPLIER']) - expect(detail.dataScopeId).toBe(10001) + expect(detail.dataScopeId).toBe(0) expect(template).toBeInstanceOf(Blob) expect(result.success).toBe(true) expect(http.get).toHaveBeenNthCalledWith(1, '/platform/v1/partners/19001', { params: { tenantId: 0 } }) diff --git a/src/modules/master-data/counterparties/pages/PartnersView.vue b/src/modules/master-data/counterparties/pages/PartnersView.vue index d8278849..b245833d 100644 --- a/src/modules/master-data/counterparties/pages/PartnersView.vue +++ b/src/modules/master-data/counterparties/pages/PartnersView.vue @@ -1,8 +1,9 @@ @@ -250,12 +371,34 @@ onMounted(() => { 刷新 - +
+ +
+ + + + -
+