/** * 人工 SKU 配置 Tab:扫描 → 配置 → 重新匹配。 * * 口径(2026-10-24 确认): * * 配置主键是**内部 SKU**(快手 skuNick / 拼多多 outSkuSn),商品名称仅供识别; * * 同名 SKU 算同一个商品,跨渠道共用一份配置; * * 客服人工有四个钱包:南航积分 / 乐玩积分 / ROCK 积分 / 备用金现金; * * 积分换算率按平台不同(南航、乐玩 1 积分 = 1 分;ROCK 1 积分 = 1 元); * * 消耗积分填 0 = 成本不固定,客服确认履约时手填实际成本。 * * 这里没有匹配引擎、没有打分:就是一张带统计的 SKU 配置清单。 */ import { DeleteOutlined, EditOutlined, ReloadOutlined, ScanOutlined, SyncOutlined, } from '@ant-design/icons' import { Alert, App, Button, Card, Col, DatePicker, Form, Input, InputNumber, Modal, Popconfirm, Row, Select, Space, Statistic, Table, Tag, Typography, } from 'antd' import type { TableColumnsType } from 'antd' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import { deleteAdminManualConfig, fetchAdminManualPlatforms, fetchAdminManualSkuOverview, fetchAdminManualSkus, rematchAdminManualFulfillments, saveAdminManualConfig, scanAdminManualSkus, } from '@/services/admin' import type { ManualSkuItem } from '@/types/admin' import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' import dayjs from '@/lib/dayjs' import type { Dayjs } from 'dayjs' type ConfigFormValues = { platformKey: string creditQuantity: number note?: string } const PLATFORM_TAG_COLORS: Record = { nanhang: 'blue', lewan: 'purple', rock: 'cyan', fund_cash: 'orange', } /** 按换算率把「消耗单位」翻译成人话。 */ function unitLabel(platformKey: string, creditUnitPriceFen: number) { if (platformKey === 'fund_cash') return '元' if (creditUnitPriceFen >= 100) return '积分(1积分=1元)' return '积分(1积分=1分)' } export function ManualSkuConfigPanel() { const { message } = App.useApp() const queryClient = useQueryClient() const [form] = Form.useForm() const [keyword, setKeyword] = useState('') const [status, setStatus] = useState<'all' | 'configured' | 'unconfigured'>('unconfigured') const [platformKey, setPlatformKey] = useState('') const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE) const [scanRange, setScanRange] = useState<[Dayjs, Dayjs]>([ dayjs().startOf('month'), dayjs().endOf('month'), ]) const [modalOpen, setModalOpen] = useState(false) const [editing, setEditing] = useState(null) const listQuery = useQuery({ queryKey: ['admin-cost-center-manual-skus', keyword, status, platformKey, page, pageSize], queryFn: () => fetchAdminManualSkus({ keyword: keyword || undefined, status: status === 'all' ? '' : status, platformKey: platformKey || undefined, page, pageSize, }), }) const overviewQuery = useQuery({ queryKey: ['admin-cost-center-manual-skus-overview'], queryFn: fetchAdminManualSkuOverview, }) const platformQuery = useQuery({ queryKey: ['admin-cost-center-manual-platforms'], queryFn: fetchAdminManualPlatforms, }) const overview = overviewQuery.data?.data const platforms = platformQuery.data?.data.items || [] const platformMap = new Map(platforms.map((item) => [item.key, item])) const scanMutation = useMutation({ mutationFn: () => scanAdminManualSkus({ fromDate: scanRange[0].format('YYYY-MM-DD'), toDate: scanRange[1].format('YYYY-MM-DD'), }), onSuccess: (response) => { const result = response.data message.success(`扫描完成,登记 ${result.registered} 个内部 SKU`) void queryClient.invalidateQueries({ queryKey: ['admin-cost-center-manual-skus'] }) void queryClient.invalidateQueries({ queryKey: ['admin-cost-center-manual-skus-overview'], }) }, onError: (error) => { message.error(error instanceof Error ? error.message : '扫描失败') }, }) const saveMutation = useMutation({ mutationFn: (values: ConfigFormValues) => saveAdminManualConfig({ skuKey: editing?.skuKey || '', platformKey: values.platformKey, creditQuantity: values.creditQuantity, note: values.note || '', }), onSuccess: () => { message.success('配置已保存') setModalOpen(false) void queryClient.invalidateQueries({ queryKey: ['admin-cost-center-manual-skus'] }) void queryClient.invalidateQueries({ queryKey: ['admin-cost-center-manual-skus-overview'], }) }, onError: (error) => { message.error(error instanceof Error ? error.message : '保存失败') }, }) const deleteMutation = useMutation({ mutationFn: (skuKey: string) => deleteAdminManualConfig(skuKey), onSuccess: () => { message.success('配置已删除') void queryClient.invalidateQueries({ queryKey: ['admin-cost-center-manual-skus'] }) void queryClient.invalidateQueries({ queryKey: ['admin-cost-center-manual-skus-overview'], }) }, onError: (error) => { message.error(error instanceof Error ? error.message : '删除失败') }, }) const rematchMutation = useMutation({ mutationFn: () => rematchAdminManualFulfillments(), onSuccess: (response) => { message.success(`重新匹配完成,回填 ${response.data.updatedCount} 个订单项`) void queryClient.invalidateQueries({ queryKey: ['admin-cost-center-manual-skus'] }) }, onError: (error) => { message.error(error instanceof Error ? error.message : '重新匹配失败') }, }) function openConfig(item: ManualSkuItem) { setEditing(item) form.setFieldsValue({ platformKey: item.config?.platformKey || undefined, creditQuantity: item.config?.creditQuantity ?? undefined, note: item.config?.note || '', }) setModalOpen(true) } const columns: TableColumnsType = [ { title: '内部 SKU(配置主键)', dataIndex: 'skuKey', width: 280, render: (_, row) => (
{row.skuKey} {row.displayName || '(无名称)'}
), }, { title: '店铺', dataIndex: 'shopCount', width: 70, align: 'right' }, { title: '订单数', dataIndex: 'orderCount', width: 90, align: 'right' }, { title: '订单项', dataIndex: 'itemCount', width: 90, align: 'right' }, { title: '收入', dataIndex: 'revenueFen', width: 110, align: 'right', render: (value: number) => `¥${(Number(value || 0) / 100).toFixed(0)}`, }, { title: '履约渠道', key: 'platform', width: 150, render: (_, row) => row.config ? ( {row.config.platformLabel || row.config.platformKey} {row.config.creditQuantity}{' '} {unitLabel(row.config.platformKey, row.config.creditUnitPriceFen)} ) : ( 未配置 ), }, { title: '成本', key: 'cost', width: 110, align: 'right', render: (_, row) => row.config ? ( row.config.costMode === 'fixed' ? ( ¥{(row.config.costFen / 100).toFixed(2)} ) : ( 不固定·客服填 ) ) : ( '-' ), }, { title: '操作', key: 'actions', width: 130, fixed: 'right', render: (_, row) => ( {row.configured ? ( deleteMutation.mutate(row.skuKey)} > { setKeyword(value.trim()) setPage(1) }} /> { setPlatformKey(value || '') setPage(1) }} options={platforms.map((item) => ({ value: item.key, label: item.label }))} /> { setPage(nextPage) if (nextPageSize !== pageSize) setPageSize(nextPageSize) }, })} /> { void form.validateFields().then((values) => saveMutation.mutate(values)) }} onCancel={() => setModalOpen(false)} destroyOnHidden width={560} > form={form} layout="vertical">