/** * 履约渠道页:待分类队列 + 渠道规则 + 问题队列。 * * 设计口径(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 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 (