重构:统一商品渠道投影并收敛权威文档
This commit is contained in:
@@ -66,7 +66,7 @@ export function AffiliateSkuPanel() {
|
||||
queryKey: ['admin-cost-center-products', 'affiliate_dash', page, pageSize],
|
||||
queryFn: () =>
|
||||
fetchAdminProductCatalog({
|
||||
carrierType: 'affiliate_dash',
|
||||
channel: 'affiliate_dash',
|
||||
page,
|
||||
pageSize,
|
||||
}),
|
||||
|
||||
@@ -34,7 +34,7 @@ type Props = {
|
||||
providers: string[]
|
||||
canConfig: boolean
|
||||
diagnostics?: MappingDiagnostics
|
||||
sourceLabel: string
|
||||
channelLabel: string
|
||||
expandedRowKeys: string[]
|
||||
detailPageSize: number
|
||||
onKeywordChange: (value: string) => void
|
||||
@@ -60,7 +60,7 @@ export default function ChannelMappingCatalog({
|
||||
providers,
|
||||
canConfig,
|
||||
diagnostics,
|
||||
sourceLabel,
|
||||
channelLabel,
|
||||
expandedRowKeys,
|
||||
detailPageSize,
|
||||
onKeywordChange,
|
||||
@@ -241,7 +241,7 @@ export default function ChannelMappingCatalog({
|
||||
<Alert
|
||||
type="success"
|
||||
showIcon
|
||||
message={`${sourceLabel}的订单项已全部归一`}
|
||||
message={`${channelLabel}的订单项已全部归一`}
|
||||
description={`${Number(diagnostics?.productTotal || 0)} 个标准商品、${Number(diagnostics?.mappingTotal || 0)} 条映射已覆盖全部订单;新订单若命中不到映射,会在每日成本同步后出现在「未归一」队列。`}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* 采用「诊断优先」结构:
|
||||
* 1. 顶部统计卡:标准商品 / 映射条数 / 未归一订单项 / 未归一 SKU 组;
|
||||
* 2. 未归一队列:按来源 + 店铺 + 平台 SKU 聚合成可操作的行,行内直接建映射;
|
||||
* 2. 未归一队列:按渠道 + 店铺 + 平台 SKU 聚合成可操作的行,行内直接建映射;
|
||||
* 3. 映射目录:只保留 4 列(标准商品 / 分类 / 渠道覆盖 / 映射),匹配方式进展开行。
|
||||
*
|
||||
* 2026-10-25 拆分:本文件只保留查询、状态与动作,视图拆到
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
export function ChannelMappingPanel() {
|
||||
const { message, modal } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const { canConfig, productSourceFilter, options, refreshAll } = useCostCenter()
|
||||
const { canConfig, channelFilter, options, refreshAll } = useCostCenter()
|
||||
const [mappingForm] = Form.useForm<MappingFormValues>()
|
||||
|
||||
const [view, setView] = useState<'catalog' | 'queue'>('catalog')
|
||||
@@ -67,11 +67,10 @@ export function ChannelMappingPanel() {
|
||||
const providers = options?.providers || []
|
||||
|
||||
const diagnosticsQuery = useQuery({
|
||||
queryKey: ['admin-cost-center-mapping-diagnostics', productSourceFilter],
|
||||
queryKey: ['admin-cost-center-mapping-diagnostics', channelFilter],
|
||||
queryFn: () =>
|
||||
fetchAdminChannelMappingDiagnostics({
|
||||
carrierType: productSourceFilter,
|
||||
channel: productSourceFilter,
|
||||
channel: channelFilter,
|
||||
}),
|
||||
})
|
||||
const diagnostics = diagnosticsQuery.data?.data
|
||||
@@ -79,7 +78,7 @@ export function ChannelMappingPanel() {
|
||||
const mappingsQuery = useQuery({
|
||||
queryKey: [
|
||||
'admin-cost-center-mappings',
|
||||
productSourceFilter,
|
||||
channelFilter,
|
||||
mappingKeyword,
|
||||
mappingProvider,
|
||||
mappingPage,
|
||||
@@ -89,7 +88,7 @@ export function ChannelMappingPanel() {
|
||||
fetchAdminChannelProductMappings({
|
||||
keyword: mappingKeyword || undefined,
|
||||
provider: mappingProvider || undefined,
|
||||
carrierType: productSourceFilter,
|
||||
channel: channelFilter,
|
||||
page: mappingPage,
|
||||
pageSize: mappingPageSize,
|
||||
}),
|
||||
@@ -99,7 +98,7 @@ export function ChannelMappingPanel() {
|
||||
const queueQuery = useQuery({
|
||||
queryKey: [
|
||||
'admin-cost-center-unmapped-mappings',
|
||||
productSourceFilter,
|
||||
channelFilter,
|
||||
queueKeyword,
|
||||
queueProvider,
|
||||
queuePage,
|
||||
@@ -107,7 +106,7 @@ export function ChannelMappingPanel() {
|
||||
],
|
||||
queryFn: () =>
|
||||
fetchAdminUnmappedMappingGroups({
|
||||
channel: productSourceFilter,
|
||||
channel: channelFilter,
|
||||
provider: queueProvider || undefined,
|
||||
keyword: queueKeyword || undefined,
|
||||
page: queuePage,
|
||||
@@ -126,7 +125,13 @@ export function ChannelMappingPanel() {
|
||||
|
||||
const productOptions =
|
||||
options?.products
|
||||
.filter((item) => !productSourceFilter || item.source === productSourceFilter)
|
||||
.filter(
|
||||
(item) =>
|
||||
!channelFilter ||
|
||||
(channelFilter === 'unrouted'
|
||||
? item.activePrimaryChannel === null
|
||||
: item.activePrimaryChannel === channelFilter),
|
||||
)
|
||||
.map((item) => ({
|
||||
value: item.id,
|
||||
label: `${item.canonicalName}(${item.canonicalSku})`,
|
||||
@@ -274,9 +279,9 @@ export function ChannelMappingPanel() {
|
||||
})
|
||||
}
|
||||
|
||||
const sourceLabel = productSourceFilter
|
||||
? (FULFILLMENT_MODE_LABELS[productSourceFilter] ?? productSourceFilter)
|
||||
: '全部来源'
|
||||
const channelLabel = channelFilter
|
||||
? (FULFILLMENT_MODE_LABELS[channelFilter] ?? channelFilter)
|
||||
: '全部渠道'
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -284,7 +289,7 @@ export function ChannelMappingPanel() {
|
||||
title={
|
||||
<Space size={6} wrap>
|
||||
<Typography.Text strong>渠道映射</Typography.Text>
|
||||
<Tag color="blue">{sourceLabel}</Tag>
|
||||
<Tag color="blue">{channelLabel}</Tag>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
@@ -344,7 +349,7 @@ export function ChannelMappingPanel() {
|
||||
providers={providers}
|
||||
canConfig={canConfig}
|
||||
diagnostics={diagnostics}
|
||||
sourceLabel={sourceLabel}
|
||||
channelLabel={channelLabel}
|
||||
expandedRowKeys={expandedMappingProductIds}
|
||||
detailPageSize={mappingDetailPageSize}
|
||||
onKeywordChange={(value) => {
|
||||
@@ -387,7 +392,7 @@ export function ChannelMappingPanel() {
|
||||
provider={queueProvider}
|
||||
providers={providers}
|
||||
canConfig={canConfig}
|
||||
sourceLabel={sourceLabel}
|
||||
channelLabel={channelLabel}
|
||||
onKeywordChange={(value) => {
|
||||
setQueueKeyword(value)
|
||||
setQueuePage(1)
|
||||
|
||||
@@ -34,7 +34,7 @@ type Props = {
|
||||
provider: string
|
||||
providers: string[]
|
||||
canConfig: boolean
|
||||
sourceLabel: string
|
||||
channelLabel: string
|
||||
onKeywordChange: (value: string) => void
|
||||
onProviderChange: (value: string) => void
|
||||
onPageChange: (page: number, pageSize: number) => void
|
||||
@@ -54,7 +54,7 @@ export default function ChannelMappingQueue({
|
||||
provider,
|
||||
providers,
|
||||
canConfig,
|
||||
sourceLabel,
|
||||
channelLabel,
|
||||
onKeywordChange,
|
||||
onProviderChange,
|
||||
onPageChange,
|
||||
@@ -151,7 +151,7 @@ export default function ChannelMappingQueue({
|
||||
<Alert
|
||||
type="success"
|
||||
showIcon
|
||||
message={`${sourceLabel}没有未归一的平台 SKU`}
|
||||
message={`${channelLabel}没有未归一的平台 SKU`}
|
||||
description="订单项全部命中了标准商品映射;这里为空即代表销售与成本都能按商品归集。"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 成本中心页面上下文。
|
||||
*
|
||||
* 只存放「所有面板都要知道」的三件事:权限、当前来源筛选、刷新动作。
|
||||
* 只存放「所有面板都要知道」的三件事:权限、当前渠道筛选、刷新动作。
|
||||
* 具体数据由各面板自己用 react-query 取——曾经这里塞过 products / versions /
|
||||
* exceptions 等空数组当作「统一数据源」,实际没人读,已删除。
|
||||
*/
|
||||
@@ -15,16 +15,14 @@ export type CostCenterTab = 'products' | 'mappings' | 'cost' | 'exceptions'
|
||||
|
||||
export type CostCenterContextValue = {
|
||||
canConfig: boolean
|
||||
/** 当前来源筛选:all / affiliate_dash / work_order / manual。 */
|
||||
/** 当前渠道筛选。 */
|
||||
scope: CostScope
|
||||
setScope: (scope: CostScope) => void
|
||||
activeTab: CostCenterTab
|
||||
setActiveTab: (tab: CostCenterTab) => void
|
||||
scopeLabel: string
|
||||
/** 商品/映射按 canonical_sku 前缀筛选。 */
|
||||
productSourceFilter?: ProductSource
|
||||
/** 成本项/版本/异常按履约方式筛选。 */
|
||||
channelFilter?: 'affiliate_dash' | 'work_order' | 'third_party_voucher' | 'manual'
|
||||
/** 商品、映射、成本项、版本和异常共用的当前渠道筛选。 */
|
||||
channelFilter?: Exclude<CostScope, 'all'>
|
||||
|
||||
options?: {
|
||||
providers: string[]
|
||||
@@ -34,6 +32,7 @@ export type CostCenterContextValue = {
|
||||
canonicalName: string
|
||||
unitName: string
|
||||
source: ProductSource
|
||||
activePrimaryChannel: string | null
|
||||
}>
|
||||
costItems: Array<{
|
||||
id: number
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 成本中心外壳:来源筛选、总览统计与固定 Tab。
|
||||
* 成本中心外壳:当前渠道筛选、总览统计与固定 Tab。
|
||||
*
|
||||
* 结构(2026-10-25 调整):
|
||||
* 1. 来源不再是「假 Tab」,改为工具栏 Segmented —— 只筛数据,不改变导航;
|
||||
@@ -39,7 +39,7 @@ import { CostCenterContext, type CostCenterTab } from './CostCenterContext'
|
||||
import type { CostScope } from './shared'
|
||||
|
||||
const SCOPES: Array<{ key: CostScope; label: string; hint: string }> = [
|
||||
{ key: 'all', label: '全部', hint: '不区分来源,查看全部商品、映射与成本。' },
|
||||
{ key: 'all', label: '全部', hint: '不区分当前渠道,查看全部商品、映射与成本。' },
|
||||
{
|
||||
key: 'affiliate_dash',
|
||||
label: '自动发货',
|
||||
@@ -60,6 +60,16 @@ const SCOPES: Array<{ key: CostScope; label: string; hint: string }> = [
|
||||
label: '人工客服',
|
||||
hint: '客服手工处理的商品;成本按内部 SKU 的消耗积分结算。',
|
||||
},
|
||||
{
|
||||
key: 'on_account',
|
||||
label: '统一结算',
|
||||
hint: '统一结算商品;不进入人工客服或自动履约。',
|
||||
},
|
||||
{
|
||||
key: 'unrouted',
|
||||
label: '未路由',
|
||||
hint: '尚未配置有效主渠道的商品。',
|
||||
},
|
||||
]
|
||||
|
||||
const TABS: readonly CostCenterTab[] = ['products', 'mappings', 'cost', 'exceptions']
|
||||
@@ -137,11 +147,7 @@ export default function CostCenterShell() {
|
||||
|
||||
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 channelFilter =
|
||||
scope === 'all'
|
||||
? undefined
|
||||
: (scope as 'affiliate_dash' | 'work_order' | 'third_party_voucher' | 'manual')
|
||||
const channelFilter = scope === 'all' ? undefined : scope
|
||||
return (
|
||||
<CostCenterContext.Provider
|
||||
value={{
|
||||
@@ -151,7 +157,6 @@ export default function CostCenterShell() {
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
scopeLabel,
|
||||
productSourceFilter,
|
||||
channelFilter,
|
||||
options,
|
||||
refreshAll,
|
||||
@@ -163,7 +168,7 @@ export default function CostCenterShell() {
|
||||
<Typography.Text type="secondary">统计口径</Typography.Text>
|
||||
<Tag color={scope === 'all' ? 'default' : 'blue'}>{scopeLabel}</Tag>
|
||||
{scope !== 'all' ? (
|
||||
<Typography.Text type="secondary">(跟随上方来源筛选变化)</Typography.Text>
|
||||
<Typography.Text type="secondary">(跟随上方渠道筛选变化)</Typography.Text>
|
||||
) : null}
|
||||
<Button
|
||||
size="small"
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
* 成本配置:按来源展示「成本怎么算」。
|
||||
* 成本配置:按履约渠道展示「成本怎么算」。
|
||||
*
|
||||
* 三个来源的成本形态完全不同,所以这里不做统一表格,而是按来源切换:
|
||||
* 各渠道的成本形态不同,所以这里不做统一表格,而是按渠道切换:
|
||||
* 自动发货 → 上游 SKU 采购价(按 aff:<skuCode> 归一)
|
||||
* 接单平台 → 接单模板发单金额(只读核对,改价去接单模板页)
|
||||
* 第三方卡券 → 卡券商品采购价(按 tpv:<sku> 归一,只统计)
|
||||
* 人工客服 → 内部 SKU 的履约渠道与消耗积分
|
||||
*
|
||||
* 来源在页面顶部工具栏统一选择;选「全部」时这里给出四个入口,
|
||||
* 渠道在页面顶部工具栏统一选择;选「全部」时这里给出四个入口,
|
||||
* 避免把三套配置竖着堆在一页里。
|
||||
*/
|
||||
import { Button, Card, Col, Row, Space, Typography } from 'antd'
|
||||
import { Alert, Button, Card, Col, Row, Space, Typography } from 'antd'
|
||||
|
||||
import { AffiliateSkuPanel } from './AffiliateSkuPanel'
|
||||
import { useCostCenter } from './CostCenterContext'
|
||||
@@ -47,17 +47,42 @@ const CONFIG_ENTRIES: Array<{
|
||||
]
|
||||
|
||||
export default function CostConfigPanel() {
|
||||
const { scope, setScope } = useCostCenter()
|
||||
const { scope, setScope, setActiveTab } = useCostCenter()
|
||||
|
||||
if (scope === 'affiliate_dash') return <AffiliateSkuPanel />
|
||||
if (scope === 'work_order') return <WorkOrderPricePanel />
|
||||
if (scope === 'third_party_voucher') return <VoucherSkuPanel />
|
||||
if (scope === 'manual') return <ManualSkuConfigPanel />
|
||||
if (scope === 'on_account') {
|
||||
return (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="统一结算没有独立商品价格入口"
|
||||
description="统一结算商品不进入人工或自动履约;实际成本以统一结算账单确认结果为准。"
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (scope === 'unrouted') {
|
||||
return (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="未路由商品暂时没有履约成本口径"
|
||||
description="先为标准商品配置明确主渠道,再维护对应渠道的成本。"
|
||||
action={
|
||||
<Button size="small" onClick={() => setActiveTab('products')}>
|
||||
去标准商品
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Typography.Text type="secondary">
|
||||
成本配置按来源分开维护,选择一个来源查看;也可以在页面顶部直接切换来源。
|
||||
成本配置按履约渠道分开维护;也可以在页面顶部直接切换渠道。
|
||||
</Typography.Text>
|
||||
<Row gutter={[12, 12]}>
|
||||
{CONFIG_ENTRIES.map((entry) => (
|
||||
|
||||
@@ -94,7 +94,7 @@ export function ProductCatalogPanel() {
|
||||
const { message, modal } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const navigate = useNavigate()
|
||||
const { canConfig, productSourceFilter, refreshAll, setScope, setActiveTab } = useCostCenter()
|
||||
const { canConfig, channelFilter, refreshAll, setScope, setActiveTab } = useCostCenter()
|
||||
const [productForm] = Form.useForm<ProductFormValues>()
|
||||
// 历史修复只处理最近 30 天,避免很早以前的乱 SKU 再次进入商品与映射。
|
||||
const [backfillRange, setBackfillRange] = useState<[Dayjs, Dayjs]>([
|
||||
@@ -120,7 +120,7 @@ export function ProductCatalogPanel() {
|
||||
const [priceForm] = Form.useForm()
|
||||
|
||||
// 游戏分类与二级目录结构仅为接单平台(work_order)独有,自动发货与人工客服不使用
|
||||
const isWorkOrder = productSourceFilter === 'work_order'
|
||||
const isWorkOrder = channelFilter === 'work_order'
|
||||
const showCategoryBar = isWorkOrder
|
||||
|
||||
useEffect(() => {
|
||||
@@ -133,7 +133,7 @@ export function ProductCatalogPanel() {
|
||||
const productsQuery = useQuery({
|
||||
queryKey: [
|
||||
'admin-cost-center-products',
|
||||
productSourceFilter,
|
||||
channelFilter,
|
||||
productKeyword,
|
||||
isWorkOrder ? productRootCategoryId : null,
|
||||
isWorkOrder ? productSubCategoryId : null,
|
||||
@@ -143,7 +143,7 @@ export function ProductCatalogPanel() {
|
||||
queryFn: () =>
|
||||
fetchAdminProductCatalog({
|
||||
keyword: productKeyword || undefined,
|
||||
carrierType: productSourceFilter,
|
||||
channel: channelFilter,
|
||||
rootCategoryId: isWorkOrder ? (productRootCategoryId ?? undefined) : undefined,
|
||||
categoryId: isWorkOrder ? (productSubCategoryId ?? undefined) : undefined,
|
||||
page: productPage,
|
||||
@@ -451,7 +451,7 @@ export function ProductCatalogPanel() {
|
||||
|
||||
function deleteProduct(item: ProductCatalogItem) {
|
||||
const affiliateHint =
|
||||
item.source === 'affiliate_dash'
|
||||
item.sourceKind === 'affiliate_dash'
|
||||
? '这是自动发货 SKU,删除后再次同步会自动生成;只想隐藏它请改为归档。'
|
||||
: ''
|
||||
modal.confirm({
|
||||
@@ -516,12 +516,6 @@ export function ProductCatalogPanel() {
|
||||
{row.templatePricingMode === 'unit' ? '按件计价' : '固定金额'}
|
||||
</Tag>
|
||||
) : null}
|
||||
{row.activePrimaryChannel && row.activePrimaryChannel !== row.source ? (
|
||||
<Tag color="purple" style={{ marginInlineEnd: 0 }}>
|
||||
当前主渠道:
|
||||
{FULFILLMENT_MODE_LABELS[row.activePrimaryChannel] || row.activePrimaryChannel}
|
||||
</Tag>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
@@ -540,14 +534,40 @@ export function ProductCatalogPanel() {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '已绑定渠道',
|
||||
title: '当前履约',
|
||||
width: 160,
|
||||
render: (_, row) => (
|
||||
<Space direction="vertical" size={2}>
|
||||
{row.activePrimaryChannel ? (
|
||||
<Tag
|
||||
color={row.activePrimaryChannel === 'manual' ? 'orange' : 'purple'}
|
||||
style={{ marginInlineEnd: 0 }}
|
||||
>
|
||||
{FULFILLMENT_MODE_LABELS[row.activePrimaryChannel] || row.activePrimaryChannel}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag style={{ marginInlineEnd: 0 }}>未路由</Tag>
|
||||
)}
|
||||
{row.fallbackChannels?.length ? (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 11 }}>
|
||||
兜底:
|
||||
{row.fallbackChannels
|
||||
.map((channel) => FULFILLMENT_MODE_LABELS[channel] || channel)
|
||||
.join(' / ')}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '平台映射',
|
||||
dataIndex: 'mappingCount',
|
||||
width: 130,
|
||||
render: (value: number, row) => {
|
||||
if (row.source === 'affiliate_dash') {
|
||||
if (row.sourceKind === 'affiliate_dash') {
|
||||
return <Tag color="default">直连无需映射</Tag>
|
||||
}
|
||||
if (row.source === 'manual') {
|
||||
if (row.sourceKind === 'manual') {
|
||||
return <Tag color="default">内部SKU无需映射</Tag>
|
||||
}
|
||||
return value > 0 ? (
|
||||
@@ -562,7 +582,7 @@ export function ProductCatalogPanel() {
|
||||
dataIndex: 'costVersionCount',
|
||||
width: 140,
|
||||
render: (value: number, row) => {
|
||||
if (row.source === 'manual') {
|
||||
if (row.sourceKind === 'manual') {
|
||||
return (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
以人工 SKU 配置为准
|
||||
@@ -596,7 +616,7 @@ export function ProductCatalogPanel() {
|
||||
>
|
||||
{value > 0
|
||||
? `${value} 个版本 · 改价`
|
||||
: row.source === 'work_order'
|
||||
: row.sourceKind === 'work_order'
|
||||
? '配置结算成本'
|
||||
: '未配置 · 立即配置'}
|
||||
</Button>
|
||||
@@ -634,7 +654,7 @@ export function ProductCatalogPanel() {
|
||||
render: (_, row) =>
|
||||
canConfig ? (
|
||||
<Space size={2} wrap>
|
||||
{row.source === 'manual' ? (
|
||||
{row.sourceKind === 'manual' ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
@@ -704,7 +724,7 @@ export function ProductCatalogPanel() {
|
||||
extra={
|
||||
canConfig ? (
|
||||
<Space size={6} wrap>
|
||||
{productSourceFilter === 'work_order' ? (
|
||||
{channelFilter === 'work_order' ? (
|
||||
<>
|
||||
<AdminRangePicker
|
||||
value={backfillRange}
|
||||
@@ -727,7 +747,7 @@ export function ProductCatalogPanel() {
|
||||
loading={productsQuery.isFetching}
|
||||
onClick={() => void productsQuery.refetch()}
|
||||
/>
|
||||
{productSourceFilter === 'manual' ? (
|
||||
{channelFilter === 'manual' ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
@@ -737,7 +757,7 @@ export function ProductCatalogPanel() {
|
||||
>
|
||||
新增人工 SKU
|
||||
</Button>
|
||||
) : productSourceFilter === 'third_party_voucher' ? (
|
||||
) : channelFilter === 'third_party_voucher' ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
@@ -758,7 +778,7 @@ export function ProductCatalogPanel() {
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{productSourceFilter === 'manual' ? (
|
||||
{channelFilter === 'manual' ? (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
@@ -863,7 +883,7 @@ export function ProductCatalogPanel() {
|
||||
dataSource={products}
|
||||
loading={productsQuery.isFetching}
|
||||
rowClassName={(row) => (row.mappingCount > 0 ? 'product-rule-row-mapped' : '')}
|
||||
scroll={{ x: 1180 }}
|
||||
scroll={{ x: 1320 }}
|
||||
pagination={buildAdminTablePagination({
|
||||
current: productPage,
|
||||
pageSize: productPageSize,
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function VoucherSkuPanel() {
|
||||
queryKey: ['cost-center-voucher-products', page, pageSize],
|
||||
queryFn: () =>
|
||||
fetchAdminProductCatalog({
|
||||
carrierType: 'third_party_voucher',
|
||||
channel: 'third_party_voucher',
|
||||
page,
|
||||
pageSize,
|
||||
}),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* 口径(2026-10-23 重写):
|
||||
* 1. 履约渠道唯一真源是 order_items.fulfillment_channel + status,前端只读不改;
|
||||
* 2. 商品来源由 canonical_sku 前缀推导,前端不再存「商品载体」字段;
|
||||
* 2. 商品来源来自 sourceKind,当前渠道来自统一商品投影,两者不能混用;
|
||||
* 3. 成本项用 cost_model 表达「成本怎么算」,没有「适用载体」这一列。
|
||||
*/
|
||||
import type { Dayjs } from 'dayjs'
|
||||
@@ -16,8 +16,15 @@ import type {
|
||||
ProductSource,
|
||||
} from '@/types/admin'
|
||||
|
||||
/** 顶部来源切换:全部 / 自动发货 / 接单平台 / 第三方卡券 / 人工。 */
|
||||
export type CostScope = 'all' | 'affiliate_dash' | 'work_order' | 'third_party_voucher' | 'manual'
|
||||
/** 顶部当前渠道切换。 */
|
||||
export type CostScope =
|
||||
| 'all'
|
||||
| 'affiliate_dash'
|
||||
| 'work_order'
|
||||
| 'third_party_voucher'
|
||||
| 'manual'
|
||||
| 'on_account'
|
||||
| 'unrouted'
|
||||
|
||||
export type CostItemPreset = {
|
||||
key: string
|
||||
@@ -86,12 +93,14 @@ export const COST_MODEL_OPTIONS = [
|
||||
{ value: 'other', label: '其他' },
|
||||
]
|
||||
|
||||
/** 商品来源:由 canonical_sku 前缀推导。 */
|
||||
/** 商品来源:来自 product_catalog.source_kind。 */
|
||||
export const PRODUCT_SOURCE_OPTIONS = [
|
||||
{ value: 'affiliate_dash', label: '自动发货' },
|
||||
{ value: 'work_order', label: '接单平台' },
|
||||
{ value: 'third_party_voucher', label: '第三方卡券' },
|
||||
{ value: 'manual', label: '人工客服' },
|
||||
{ value: 'on_account', label: '统一结算' },
|
||||
{ value: 'unclassified', label: '未分类' },
|
||||
]
|
||||
|
||||
export const PRODUCT_SOURCE_TAG_COLORS: Record<ProductSource, string> = {
|
||||
@@ -99,6 +108,8 @@ export const PRODUCT_SOURCE_TAG_COLORS: Record<ProductSource, string> = {
|
||||
work_order: 'blue',
|
||||
third_party_voucher: 'cyan',
|
||||
manual: 'orange',
|
||||
on_account: 'gold',
|
||||
unclassified: 'default',
|
||||
'': 'default',
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,15 @@ export type FulfillmentChannel =
|
||||
| 'manual'
|
||||
| 'on_account'
|
||||
|
||||
/** 商品来源:由 canonical_sku 前缀推导,空串表示未分类。 */
|
||||
export type ProductSource = 'affiliate_dash' | 'work_order' | 'third_party_voucher' | 'manual' | ''
|
||||
/** 商品来源:来自稳定 source_kind,空串表示未分类。 */
|
||||
export type ProductSource =
|
||||
| 'affiliate_dash'
|
||||
| 'work_order'
|
||||
| 'third_party_voucher'
|
||||
| 'manual'
|
||||
| 'on_account'
|
||||
| 'unclassified'
|
||||
| ''
|
||||
|
||||
/**
|
||||
* 成本模型:只表达「这笔成本怎么算」,与履约方式正交。
|
||||
@@ -45,6 +52,7 @@ export interface ProductCatalogItem {
|
||||
canonicalName: string
|
||||
category: string
|
||||
source: ProductSource
|
||||
sourceKind: ProductSource
|
||||
rootCategoryId: number | null
|
||||
rootCategoryName: string
|
||||
subCategoryId: number | null
|
||||
@@ -60,6 +68,7 @@ export interface ProductCatalogItem {
|
||||
templateEnabled?: boolean | null
|
||||
templateSharingEnabled?: boolean | null
|
||||
activePrimaryChannel?: FulfillmentChannel | null
|
||||
fallbackChannels?: FulfillmentChannel[]
|
||||
/** 自动发货履约任务数,实时取自 fulfillment_tasks。 */
|
||||
affiliateTaskCount: number
|
||||
affiliateLastFulfilledAt: string | null
|
||||
@@ -164,6 +173,7 @@ export interface CostCenterOptions {
|
||||
canonicalName: string
|
||||
unitName: string
|
||||
source: ProductSource
|
||||
activePrimaryChannel: FulfillmentChannel | null
|
||||
}>
|
||||
costItems: Array<{
|
||||
id: number
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import test from 'node:test'
|
||||
|
||||
test('标准商品列表按当前渠道筛选,并单独展示来源、主渠道和兜底渠道', async () => {
|
||||
const source = await readFile(
|
||||
new URL('../src/pages/admin/cost-center/ProductCatalogPanel.tsx', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
assert.ok(source.includes('channel: channelFilter'))
|
||||
assert.ok(!source.includes('carrierType: productSourceFilter'))
|
||||
assert.ok(source.includes("title: '来源'"))
|
||||
assert.ok(source.includes("title: '当前履约'"))
|
||||
assert.ok(source.includes('row.activePrimaryChannel'))
|
||||
assert.ok(source.includes('row.fallbackChannels'))
|
||||
assert.ok(source.includes('未路由'))
|
||||
})
|
||||
|
||||
test('渠道映射的统计、目录、队列和商品下拉都使用当前渠道', async () => {
|
||||
const [panel, catalog, queue] = await Promise.all([
|
||||
readFile(
|
||||
new URL('../src/pages/admin/cost-center/ChannelMappingPanel.tsx', import.meta.url),
|
||||
'utf8',
|
||||
),
|
||||
readFile(
|
||||
new URL('../src/pages/admin/cost-center/ChannelMappingCatalog.tsx', import.meta.url),
|
||||
'utf8',
|
||||
),
|
||||
readFile(
|
||||
new URL('../src/pages/admin/cost-center/ChannelMappingQueue.tsx', import.meta.url),
|
||||
'utf8',
|
||||
),
|
||||
])
|
||||
|
||||
assert.ok(panel.includes('channel: channelFilter'))
|
||||
assert.ok(panel.includes('item.activePrimaryChannel === channelFilter'))
|
||||
assert.ok(!panel.includes('carrierType: productSourceFilter'))
|
||||
assert.ok(catalog.includes('channelLabel'))
|
||||
assert.ok(queue.includes('channelLabel'))
|
||||
})
|
||||
@@ -52,7 +52,7 @@ test('人工商品不再允许手工创建或执行旧批量归档', async () =>
|
||||
|
||||
assert.ok(!productSource.includes('归档跨渠道重复商品'))
|
||||
assert.ok(!productSource.includes('归档纯历史人工商品'))
|
||||
assert.ok(productSource.includes("productSourceFilter === 'manual'"))
|
||||
assert.ok(productSource.includes("channelFilter === 'manual'"))
|
||||
assert.ok(productSource.includes('manualNew=1'))
|
||||
assert.ok(productSource.includes('人工标准商品由人工 SKU 配置自动创建'))
|
||||
assert.ok(serviceSource.includes('cost_manual_product_managed_by_config'))
|
||||
|
||||
@@ -43,6 +43,6 @@ test('第三方卡券新增 SKU 一次完成商品、规则和采购价', async
|
||||
assert.ok(service.includes("'/api/v1/admin/cost-center/voucher-products'"))
|
||||
assert.ok(!service.includes('/voucher-products/ensure-rules'))
|
||||
assert.ok(panel.includes("searchParams.get('voucherNew')"))
|
||||
assert.ok(productCatalog.includes("productSourceFilter === 'third_party_voucher'"))
|
||||
assert.ok(productCatalog.includes("channelFilter === 'third_party_voucher'"))
|
||||
assert.ok(productCatalog.includes('voucherNew=1'))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user