/** * 成本中心外壳:来源筛选、总览统计与固定 Tab。 * * 结构(2026-10-25 调整): * 1. 来源不再是「假 Tab」,改为工具栏 Segmented —— 只筛数据,不改变导航; * 2. Tab 固定 4 个,不再随来源增减:标准商品 / 渠道映射 / 成本配置 / 成本异常; * 3. 原来分散的「自动发货 SKU / 人工 SKU 配置 / 发单价格」合并为「成本配置」; * 4. 统计卡按当前来源收敛(overview 接口传 scope),和下面的列表对得上; * 5. 刷新按 queryKey 前缀失效,新增查询不会再漏刷。 */ import { ReloadOutlined } from '@ant-design/icons' import { Alert, Button, Card, Col, Row, Segmented, Space, Statistic, Tabs, Tag, Typography, } from 'antd' import { useQuery, useQueryClient } from '@tanstack/react-query' import PageHeader from '@/components/admin/PageHeader' import FinanceSubNav from '@/components/admin/FinanceSubNav' import { usePersistedState } from '@/hooks/usePersistedState' import { fetchAdminCostCenterOptions, fetchAdminCostCenterOverview } from '@/services/admin' import { hasAdminPermission } from '@/utils/admin-auth' import { ChannelMappingPanel } from './ChannelMappingPanel' import CostConfigPanel from './CostConfigPanel' import { CostExceptionPanel } from './CostExceptionPanel' import { ProductCatalogPanel } from './ProductCatalogPanel' import { CostCenterContext } from './CostCenterContext' import type { CostScope } from './shared' const SCOPES: Array<{ key: CostScope; label: string; hint: string }> = [ { key: 'all', label: '全部', hint: '不区分来源,查看全部商品、映射与成本。' }, { key: 'affiliate_dash', label: '自动发货', hint: '只按上游货源 SKU 配采购成本,不维护商品目录和渠道映射。', }, { key: 'work_order', label: '接单平台', hint: '需要打手接单履约的商品;成本以打手报酬为主,按接单模板配价。', }, { key: 'manual', label: '人工客服', hint: '客服手工处理的商品;成本按内部 SKU 的消耗积分结算。', }, ] const TABS = ['products', 'mappings', 'cost', 'exceptions'] as const type CostCenterTab = (typeof TABS)[number] function isCostCenterTab(value: unknown): value is CostCenterTab { return typeof value === 'string' && (TABS as readonly string[]).includes(value) } function isCostScope(value: unknown): value is CostScope { return SCOPES.some((item) => item.key === value) } export default function CostCenterShell() { const queryClient = useQueryClient() const canConfig = hasAdminPermission('finance.config') const [scope, setScope] = usePersistedState( 'admin-cost-center:scope', 'all', isCostScope, ) const [activeTab, setActiveTab] = usePersistedState( 'admin-cost-center:tab', 'products', isCostCenterTab, ) const overviewQuery = useQuery({ queryKey: ['admin-cost-center-overview', scope], queryFn: () => fetchAdminCostCenterOverview({ scope }), }) const optionsQuery = useQuery({ queryKey: ['admin-cost-center-options'], queryFn: fetchAdminCostCenterOptions, }) const overview = overviewQuery.data?.data const options = optionsQuery.data?.data function refreshAll() { // 按前缀失效:新增一个 admin-cost-center-* 查询不需要再回来改这里。 void queryClient.invalidateQueries({ predicate: (query) => String(query.queryKey[0] ?? '').startsWith('admin-cost-center'), }) } const scopeLabel = SCOPES.find((item) => item.key === scope)?.label ?? '全部' const scopeHint = SCOPES.find((item) => item.key === scope)?.hint ?? '' const productSourceFilter = scope === 'all' ? undefined : scope const fulfillmentModeFilter = scope === 'all' ? undefined : (scope as 'affiliate_dash' | 'work_order' | 'manual') const mappingsApplicable = scope === 'all' || scope === 'work_order' return (
} onClick={refreshAll} loading={overviewQuery.isFetching} > 刷新 } /> 统计口径 {scopeLabel} {scope !== 'all' ? ( (跟随上方来源筛选变化) ) : null} {[ ['标准商品', overview?.productCount || 0], ['渠道映射', overview?.mappingCount || 0], ['系统成本项', overview?.costItemCount || 0], ['生效成本版本', overview?.activeVersionCount || 0], ['待处理异常', overview?.openExceptionCount || 0], ['未映射订单项', overview?.unmappedOrderItemCount || 0], ].map(([label, value]) => ( ))} setScope(value as CostScope)} options={SCOPES.map((item) => ({ value: item.key, label: ( {item.label} {Number( item.key === 'all' ? (overview?.productTotalCount ?? overview?.productCount ?? 0) : (overview?.productSourceCounts?.[item.key] ?? 0), )} ), }))} /> {scopeHint} {!canConfig ? ( ) : null} setActiveTab(key as CostCenterTab)} items={[ { key: 'products', label: '标准商品', children: , }, { key: 'mappings', label: '渠道映射', children: mappingsApplicable ? ( ) : ( 归一,不经过渠道映射;要核对采购价请到「成本配置 → 自动发货采购价」。' : '人工客服的订单项按内部 SKU 归一(man: 前缀),不走渠道映射;要配置消耗积分请到「成本配置 → 人工客服 SKU」。' } /> ), }, { key: 'cost', label: '成本配置', children: , }, { key: 'exceptions', label: ( 成本异常 {Number(overview?.openExceptionCount || 0) > 0 ? ( {Number(overview?.openExceptionCount || 0)} ) : null} ), children: , }, ]} />
) }