From 601c10d2a57bc2ec41aa4b94e6a699a5397ae245 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 25 Oct 2026 12:15:37 +0800 Subject: [PATCH] =?UTF-8?q?refactor(cost-center-ui):=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=E6=B8=A0=E9=81=93=E6=98=A0=E5=B0=84=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ChannelMappingPanel 908 行 → 427 行外壳 + 3 个视图组件: - ChannelMappingCatalog.tsx 322 映射目录 Tab(标准商品行 + 展开明细列) - ChannelMappingQueue.tsx 218 未归一队列 Tab(含「按内部 SKU 归一」提示) - MappingFormModal.tsx 135 新建/编辑弹窗表单 拆分边界: - 查询、筛选/分页/展开状态、提交与回填动作仍由外壳持有,切 Tab 不丢条件; - 视图组件只接收数据与回调,不直接访问 queryClient / message / modal; - 履约方式文案与颜色、MappingPrefill 提到 shared.ts,避免「视图依赖视图」。 行为保持: - JSX 与中文文案逐字保留,字面量集合比对只剩两类预期差异 (4 个履约标签移到 shared、2 处模板字符串改用局部变量,输出一致); - 队列空态判定、人工单提示、刷新 loading、展开本页/收起全部等逻辑语义不变。 验证:typecheck / lint / 前端测试 / build 通过。 --- .../cost-center/ChannelMappingCatalog.tsx | 322 +++++++++ .../admin/cost-center/ChannelMappingPanel.tsx | 663 +++--------------- .../admin/cost-center/ChannelMappingQueue.tsx | 218 ++++++ .../admin/cost-center/MappingFormModal.tsx | 135 ++++ .../src/pages/admin/cost-center/shared.ts | 23 + 5 files changed, 789 insertions(+), 572 deletions(-) create mode 100644 apps/frontend/src/pages/admin/cost-center/ChannelMappingCatalog.tsx create mode 100644 apps/frontend/src/pages/admin/cost-center/ChannelMappingQueue.tsx create mode 100644 apps/frontend/src/pages/admin/cost-center/MappingFormModal.tsx diff --git a/apps/frontend/src/pages/admin/cost-center/ChannelMappingCatalog.tsx b/apps/frontend/src/pages/admin/cost-center/ChannelMappingCatalog.tsx new file mode 100644 index 00000000..a139892c --- /dev/null +++ b/apps/frontend/src/pages/admin/cost-center/ChannelMappingCatalog.tsx @@ -0,0 +1,322 @@ +/** + * 渠道映射 · 映射目录 Tab。 + * + * 行是「标准商品」,展开后是该商品下的渠道映射明细。 + * 筛选、分页、展开状态都由上层持有,这里只负责渲染与回调。 + */ +import { SearchOutlined } from '@ant-design/icons' +import { Alert, Button, Input, Select, Space, Tag, Typography } from 'antd' +import type { TableColumnsType } from 'antd' + +import PercentTable from '@/components/common/PercentTable' +import type { ChannelProductMapping, ChannelProductMappingGroup } from '@/types/admin' +import { buildAdminLocalTablePagination, buildAdminTablePagination } from '@/utils/admin-pagination' + +import { mappingMatchTypeLabel, providerLabel, type MappingPrefill } from './shared' + +export type MappingDiagnostics = { + productTotal?: number + mappingTotal?: number + mappedProductTotal?: number + unmappedItemTotal?: number + unmappedGroupTotal?: number +} + +type Props = { + groups: ChannelProductMappingGroup[] + loading: boolean + page: number + pageSize: number + groupTotal: number + mappingTotal: number + keyword: string + provider: string + providers: string[] + canConfig: boolean + diagnostics?: MappingDiagnostics + sourceLabel: string + expandedRowKeys: string[] + detailPageSize: number + onKeywordChange: (value: string) => void + onProviderChange: (value: string) => void + onPageChange: (page: number, pageSize: number) => void + onExpandedRowKeysChange: (keys: string[]) => void + onDetailPageSizeChange: (size: number) => void + onGoQueue: () => void + onCreateMapping: (prefill?: MappingPrefill) => void + onEditMapping: (row: ChannelProductMapping) => void + onDeleteMapping: (row: ChannelProductMapping) => void +} + +export default function ChannelMappingCatalog({ + groups, + loading, + page, + pageSize, + groupTotal, + mappingTotal, + keyword, + provider, + providers, + canConfig, + diagnostics, + sourceLabel, + expandedRowKeys, + detailPageSize, + onKeywordChange, + onProviderChange, + onPageChange, + onExpandedRowKeysChange, + onDetailPageSizeChange, + onGoQueue, + onCreateMapping, + onEditMapping, + onDeleteMapping, +}: Props) { + const unmappedGroupTotal = Number(diagnostics?.unmappedGroupTotal || 0) + const unmappedItemTotal = Number(diagnostics?.unmappedItemTotal || 0) + + const detailColumns: TableColumnsType = [ + { title: '来源', dataIndex: 'provider', width: 130, render: providerLabel }, + { + title: '店铺', + dataIndex: 'shopId', + width: 150, + render: (value: string) => value || 全局, + }, + { + title: '平台 SKU', + dataIndex: 'platformSkuCode', + width: 230, + render: (value: string, row) => ( + + {value || row.platformSkuName || '-'} + + {value ? row.platformSkuName || '-' : row.platformSkuNameKey || '-'} + + + ), + }, + { + title: '匹配方式', + dataIndex: 'matchType', + width: 180, + render: (value: ChannelProductMapping['matchType']) => ( + {mappingMatchTypeLabel(value)} + ), + }, + { title: '倍率', dataIndex: 'quantityMultiplier', width: 70 }, + { + title: '启用', + dataIndex: 'enabled', + width: 80, + render: (value: boolean) => ( + {value ? '启用' : '停用'} + ), + }, + { + title: '操作', + width: 140, + render: (_, row) => + canConfig ? ( + + + + + ) : null, + }, + ] + + const groupColumns: TableColumnsType = [ + { + title: '标准商品', + dataIndex: 'canonicalName', + width: 340, + render: (value: string, row) => ( + + + + {value} + + {row.status !== 'active' ? 已归档 : null} + + + {row.canonicalSku} · {row.unitName} + + + ), + }, + { + title: '分类', + dataIndex: 'category', + width: 130, + render: (value: string) => + value || 未分类, + }, + { + title: '渠道覆盖', + width: 260, + render: (_, row) => { + const rowProviders = Array.from(new Set(row.mappings.map((item) => item.provider))) + const shops = new Set(row.mappings.map((item) => `${item.provider}:${item.shopId || ''}`)) + .size + return ( + + {rowProviders.map((item) => ( + + {providerLabel(item)} + + ))} + {shops} 个店铺键 + + ) + }, + }, + { + title: '映射', + width: 180, + render: (_, row) => { + const enabledCount = row.mappings.filter((item) => item.enabled).length + const disabledCount = row.mappings.length - enabledCount + return ( + + {row.mappings.length} 条 + {disabledCount > 0 ? ( + + 启用 {enabledCount} · 停用 {disabledCount} + + ) : ( + + 全部启用 + + )} + + ) + }, + }, + { + title: '明细', + width: 110, + render: (_, row) => { + const key = String(row.canonicalProductId) + const expanded = expandedRowKeys.includes(key) + return ( + + ) + }, + }, + ] + + return ( + + {unmappedGroupTotal > 0 ? ( + + 去处理 + + } + /> + ) : ( + + )} + + + } + placeholder="标准商品 / 平台 SKU" + value={keyword} + onChange={(event) => onKeywordChange(event.target.value)} + style={{ width: 260 }} + /> + } - placeholder="标准商品 / 平台 SKU" - value={mappingKeyword} - onChange={(event) => { - setMappingKeyword(event.target.value) - setMappingPage(1) - setExpandedMappingProductIds([]) - }} - style={{ width: 260 }} - /> - } - placeholder="平台 SKU / 商品名 / 店铺" - value={queueKeyword} - onChange={(event) => { - setQueueKeyword(event.target.value) - setQueuePage(1) - }} - style={{ width: 260 }} - /> - ({ - value: provider, - label: providerLabel(provider), - }))} - /> - - - - - - - - - - - - ) - }} - - - - - - } + placeholder="平台 SKU / 商品名 / 店铺" + value={keyword} + onChange={(event) => onKeywordChange(event.target.value)} + style={{ width: 260 }} + /> + ({ + value: provider, + label: providerLabel(provider), + }))} + /> + + + + + + + + + + + + ) + }} + + + + + +