- 侧边栏收敛:将散落在运营分组下的销售毛利、商品与成本、钱包管理、收支明细整合入「财务」二级子菜单,并支持智能展开与角色权限隔离 - 顶部导航联动:新增 FinanceSubNav 通用组件,在销售毛利、商品与成本中心、钱包管理、收支明细 4 个页面顶部统一提供一键切换 Tab - 语义规范化:将原“财务”流水页面标题明确为“收支明细”,消除模块与页面的命名歧义 - 验证方式:npm run check 全量检查通过(Prettier、ESLint、tsc、单测 100% 通过),构建产物已同步至运行容器并验证路由与交互
251 lines
8.7 KiB
TypeScript
251 lines
8.7 KiB
TypeScript
/**
|
|
* 成本中心外壳:来源筛选、总览统计与固定 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<CostScope>(
|
|
'admin-cost-center:scope',
|
|
'all',
|
|
isCostScope,
|
|
)
|
|
const [activeTab, setActiveTab] = usePersistedState<CostCenterTab>(
|
|
'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 (
|
|
<CostCenterContext.Provider
|
|
value={{
|
|
canConfig,
|
|
scope,
|
|
setScope,
|
|
scopeLabel,
|
|
productSourceFilter,
|
|
fulfillmentModeFilter,
|
|
options,
|
|
refreshAll,
|
|
}}
|
|
>
|
|
<section className="page-stack">
|
|
<FinanceSubNav current="/admin/cost-center" />
|
|
<PageHeader
|
|
title="商品与成本中心"
|
|
description="商品 SKU 先归一;成本在各来源自己的配置里维护(人工 SKU 消耗积分 / 自动发货采购价 / 接单模板发单金额),历史成本靠快照固化。"
|
|
extra={
|
|
<Button
|
|
icon={<ReloadOutlined />}
|
|
onClick={refreshAll}
|
|
loading={overviewQuery.isFetching}
|
|
>
|
|
刷新
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
<Space size={6}>
|
|
<Typography.Text type="secondary">统计口径</Typography.Text>
|
|
<Tag color={scope === 'all' ? 'default' : 'blue'}>{scopeLabel}</Tag>
|
|
{scope !== 'all' ? (
|
|
<Typography.Text type="secondary">(跟随上方来源筛选变化)</Typography.Text>
|
|
) : null}
|
|
</Space>
|
|
|
|
<Row gutter={[16, 16]}>
|
|
{[
|
|
['标准商品', overview?.productCount || 0],
|
|
['渠道映射', overview?.mappingCount || 0],
|
|
['系统成本项', overview?.costItemCount || 0],
|
|
['生效成本版本', overview?.activeVersionCount || 0],
|
|
['待处理异常', overview?.openExceptionCount || 0],
|
|
['未映射订单项', overview?.unmappedOrderItemCount || 0],
|
|
].map(([label, value]) => (
|
|
<Col xs={12} md={8} xl={4} key={label}>
|
|
<Card>
|
|
<Statistic title={label} value={value as number} />
|
|
</Card>
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
|
|
<Card>
|
|
<Space wrap size={12} align="center">
|
|
<Segmented
|
|
value={scope}
|
|
onChange={(value) => setScope(value as CostScope)}
|
|
options={SCOPES.map((item) => ({
|
|
value: item.key,
|
|
label: (
|
|
<Space size={4}>
|
|
<span>{item.label}</span>
|
|
<Tag>
|
|
{Number(
|
|
item.key === 'all'
|
|
? (overview?.productTotalCount ?? overview?.productCount ?? 0)
|
|
: (overview?.productSourceCounts?.[item.key] ?? 0),
|
|
)}
|
|
</Tag>
|
|
</Space>
|
|
),
|
|
}))}
|
|
/>
|
|
<Typography.Text type="secondary">{scopeHint}</Typography.Text>
|
|
</Space>
|
|
</Card>
|
|
|
|
{!canConfig ? (
|
|
<Alert
|
|
type="info"
|
|
showIcon
|
|
message="当前账号为只读权限"
|
|
description="可以查看商品、映射、成本配置和成本异常;新增/修改需要 finance.config 权限。"
|
|
/>
|
|
) : null}
|
|
|
|
<Tabs
|
|
activeKey={activeTab}
|
|
onChange={(key) => setActiveTab(key as CostCenterTab)}
|
|
items={[
|
|
{
|
|
key: 'products',
|
|
label: '标准商品',
|
|
children: <ProductCatalogPanel />,
|
|
},
|
|
{
|
|
key: 'mappings',
|
|
label: '渠道映射',
|
|
children: mappingsApplicable ? (
|
|
<ChannelMappingPanel />
|
|
) : (
|
|
<Alert
|
|
type="info"
|
|
showIcon
|
|
message={`${scopeLabel}不维护渠道映射`}
|
|
description={
|
|
scope === 'affiliate_dash'
|
|
? '自动发货的订单项直接按 aff:<skuCode> 归一,不经过渠道映射;要核对采购价请到「成本配置 → 自动发货采购价」。'
|
|
: '人工客服的订单项按内部 SKU 归一(man: 前缀),不走渠道映射;要配置消耗积分请到「成本配置 → 人工客服 SKU」。'
|
|
}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
key: 'cost',
|
|
label: '成本配置',
|
|
children: <CostConfigPanel />,
|
|
},
|
|
{
|
|
key: 'exceptions',
|
|
label: (
|
|
<span>
|
|
成本异常
|
|
{Number(overview?.openExceptionCount || 0) > 0 ? (
|
|
<Tag color="red" style={{ marginInlineStart: 6 }}>
|
|
{Number(overview?.openExceptionCount || 0)}
|
|
</Tag>
|
|
) : null}
|
|
</span>
|
|
),
|
|
children: <CostExceptionPanel />,
|
|
},
|
|
]}
|
|
/>
|
|
</section>
|
|
</CostCenterContext.Provider>
|
|
)
|
|
}
|