diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index 1e761bf6..ef310aec 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -24,6 +24,7 @@ const AdminFinancePage = lazy(() => import('@/pages/admin/AdminFinancePage')) const AdminFinanceLedgerPage = lazy(() => import('@/pages/admin/AdminFinanceLedgerPage')) const AdminSalesProfitPage = lazy(() => import('@/pages/admin/AdminSalesProfitPage')) const AdminCostCenterPage = lazy(() => import('@/pages/admin/AdminCostCenterPage')) +const AdminFulfillmentChannelPage = lazy(() => import('@/pages/admin/AdminFulfillmentChannelPage')) const AdminFundPage = lazy(() => import('@/pages/admin/AdminFundPage')) const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AdminAuditLogsPage')) const AdminKuaishouIndustryPage = lazy(() => import('@/pages/admin/AdminKuaishouIndustryPage')) @@ -153,6 +154,7 @@ function AdminApplication() { } /> } /> } /> + } /> }> } /> diff --git a/apps/frontend/src/components/admin/FinanceSubNav.tsx b/apps/frontend/src/components/admin/FinanceSubNav.tsx index 17880ec0..00adb63f 100644 --- a/apps/frontend/src/components/admin/FinanceSubNav.tsx +++ b/apps/frontend/src/components/admin/FinanceSubNav.tsx @@ -1,6 +1,7 @@ import { AccountBookOutlined, ApartmentOutlined, + BranchesOutlined, AuditOutlined, LineChartOutlined, WalletOutlined, @@ -21,6 +22,11 @@ export const FINANCE_NAV_ITEMS = [ label: '商品与成本中心', icon: , }, + { + key: '/admin/fulfillment-channel', + label: '履约渠道', + icon: , + }, { key: '/admin/fund', label: '钱包管理', diff --git a/apps/frontend/src/pages/admin/AdminFulfillmentChannelPage.tsx b/apps/frontend/src/pages/admin/AdminFulfillmentChannelPage.tsx new file mode 100644 index 00000000..fb496ef1 --- /dev/null +++ b/apps/frontend/src/pages/admin/AdminFulfillmentChannelPage.tsx @@ -0,0 +1,416 @@ +/** + * 履约渠道页:待分类队列 + 渠道规则 + 问题队列。 + * + * 设计口径(2026-10-31 v2): + * · 渠道由 L1 规则决定,不再由「有没有任务」反推; + * · 没有命中规则的新单落在「待分类」,收入进未分类桶,不产生成本噪音; + * · 第三方卡券是「只统计」渠道:不建任务、不发领取页。 + */ +import { PlusOutlined, ReloadOutlined, ThunderboltOutlined } from '@ant-design/icons' +import { + Alert, + Button, + Card, + Form, + Input, + InputNumber, + Modal, + Select, + Space, + Switch, + Tabs, + Tag, + Typography, + message, +} from 'antd' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useState } from 'react' + +import PageHeader from '@/components/admin/PageHeader' +import PercentTable from '@/components/common/PercentTable' +import FinanceSubNav from '@/components/admin/FinanceSubNav' +import { + assignChannelBySku, + deleteChannelRule, + fetchChannelExceptions, + fetchChannelRules, + fetchPendingChannelGroups, + runFinancePipeline, + saveChannelRule, + type ChannelRule, + type PendingChannelGroup, +} from '@/services/admin' +import { formatRedeemAmount } from '@/utils/redeem' +import { formatDateTime } from '@/utils/date-time' + +const CHANNEL_OPTIONS = [ + { value: 'work_order', label: '接单平台' }, + { value: 'affiliate_dash', label: '自动发货' }, + { value: 'third_party_voucher', label: '第三方卡券(仅统计)' }, + { value: 'manual', label: '人工客服' }, +] + +const CHANNEL_COLOR: Record = { + work_order: 'blue', + affiliate_dash: 'green', + third_party_voucher: 'purple', + manual: 'orange', +} + +const MATCH_KEY_OPTIONS = [ + { value: 'sku_code', label: '平台 SKU' }, + { value: 'internal_sku', label: '内部 SKU' }, + { value: 'sku_name', label: 'SKU 名称' }, +] + +export default function AdminFulfillmentChannelPage() { + return ( +
+ + + }, + { key: 'rules', label: '渠道规则', children: }, + { key: 'exceptions', label: '问题队列', children: }, + ]} + /> +
+ ) +} + +function PendingPanel() { + const queryClient = useQueryClient() + const [keyword, setKeyword] = useState('') + const [selections, setSelections] = useState>({}) + + const query = useQuery({ + queryKey: ['fulfillment-pending', keyword], + queryFn: () => fetchPendingChannelGroups({ page: 1, pageSize: 100, keyword }), + }) + + const assignMutation = useMutation({ + mutationFn: (row: PendingChannelGroup) => + assignChannelBySku({ + provider: row.provider, + shopId: row.shop_id, + skuCode: row.sku_code, + channel: selections[rowKey(row)] || '', + }), + onSuccess: (result) => { + message.success(`已指定 ${result.data.updated} 个订单项`) + void queryClient.invalidateQueries({ queryKey: ['fulfillment-pending'] }) + }, + onError: () => message.error('指定渠道失败'), + }) + + const pipelineMutation = useMutation({ + mutationFn: () => runFinancePipeline({ days: 3 }), + onSuccess: () => { + message.success('管道已执行') + void queryClient.invalidateQueries({ queryKey: ['fulfillment-pending'] }) + }, + onError: () => message.error('执行管道失败'), + }) + + const columns = [ + { title: '来源', dataIndex: 'provider', width: 120 }, + { title: '店铺', dataIndex: 'shop_id', width: 140 }, + { title: 'SKU', dataIndex: 'sku_code', width: 220 }, + { title: '商品名', dataIndex: 'sku_name', ellipsis: true }, + { title: '订单数', dataIndex: 'item_count', width: 90 }, + { + title: '收入', + dataIndex: 'revenue_fen', + width: 120, + render: (value: number) => formatRedeemAmount(value), + }, + { + title: '最近出现', + dataIndex: 'last_seen_at', + width: 160, + render: (value: string) => formatDateTime(value), + }, + { + title: '指定渠道', + width: 320, + render: (_: unknown, row: PendingChannelGroup) => ( + + + + + + + + + + + + + + + + + + + ) +} + +function ExceptionsPanel() { + const [reason, setReason] = useState('') + const query = useQuery({ + queryKey: ['fulfillment-exceptions', reason], + queryFn: () => fetchChannelExceptions({ reason, status: 'open', page: 1, pageSize: 100 }), + }) + + const REASON_OPTIONS = [ + { value: '', label: '全部' }, + { value: 'classification_missing', label: '待分类' }, + { value: 'routing_conflict', label: '规则冲突' }, + { value: 'product_mapping_missing', label: '未归一' }, + { value: 'cost_missing', label: '成本缺失' }, + { value: 'manual_config_missing', label: '人工 SKU 未配置' }, + ] + + const columns = [ + { + title: '类型', + dataIndex: 'reason', + width: 200, + render: (value: string) => ( + {REASON_OPTIONS.find((item) => item.value === value)?.label || value} + ), + }, + { title: '范围', dataIndex: 'scope_key', ellipsis: true }, + { title: '涉及订单项', dataIndex: 'item_count', width: 110 }, + { + title: '最近出现', + dataIndex: 'last_seen_at', + width: 160, + render: (value: string) => formatDateTime(value), + }, + ] + + return ( + +