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), + }))} + /> + + + + + + + + + + + + ) + }} + + + + + +