/** * 人工 SKU 配置 Tab:扫描 → 配置 → 重新匹配。 * * 口径(2026-10-24 确认): * * 配置主键是**内部 SKU**(快手 skuNick / 拼多多 outSkuSn),商品名称仅供识别; * * 同名 SKU 算同一个商品,跨渠道共用一份配置; * * 客服人工有四个钱包:南航积分 / 乐玩积分 / ROCK 积分 / 备用金现金; * * 积分换算率按平台不同(南航、乐玩 1 积分 = 1 分;ROCK 1 积分 = 1 元); * * 消耗积分填 0 = 成本不固定,客服确认履约时手填实际成本。 * * 这里没有匹配引擎、没有打分:就是一张带统计的 SKU 配置清单。 */ import { DeleteOutlined, EditOutlined, MinusCircleOutlined, PlusOutlined, ReloadOutlined, ScanOutlined, SyncOutlined, } from '@ant-design/icons' import { Alert, App, Button, Card, Col, DatePicker, Form, Input, InputNumber, Modal, Popconfirm, Row, Select, Space, Statistic, Tag, Typography, } from 'antd' import PercentTable from '@/components/common/PercentTable' import type { TableColumnsType } from 'antd' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import { deleteAdminManualSku, fetchAdminManualPlatforms, fetchAdminManualSkuOverview, fetchAdminManualSkus, rematchAdminManualFulfillments, saveAdminManualConfig, scanAdminManualSkus, } from '@/services/admin' import type { ManualPlatformKey, ManualSkuItem } from '@/types/admin' import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' import { formatCreditQuantity, formatCreditUnitPriceLabel, formatCreditUnitPriceYuan, } from '@/utils/credit-unit-price' import dayjs from '@/lib/dayjs' import type { Dayjs } from 'dayjs' type ConfigFormValues = { channels: Array<{ platformKey: string creditQuantity: number }> note?: string } const PLATFORM_TAG_COLORS: Record = { nanhang: 'blue', lewan: 'purple', rock: 'cyan', fund_cash: 'orange', on_account: 'gold', } function isCashChannel(platformKey: string) { return platformKey === 'fund_cash' || platformKey === 'on_account' } function channelQuantityText(platformKey: string, creditQuantity: number) { const quantity = formatCreditQuantity(creditQuantity) return isCashChannel(platformKey) ? `¥${quantity}` : `${quantity} 积分` } function platformOptionLabel(platformKey: string, label: string, creditUnitPriceFen: number) { if (isCashChannel(platformKey)) return `${label} · 按元填写` return `${label} · ${formatCreditUnitPriceLabel(creditUnitPriceFen)}` } 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 || '', channels: values.channels.map((channel) => ({ platformKey: channel.platformKey, creditQuantity: Number(channel.creditQuantity || 0), })), note: values.note || '', }), onSuccess: (response) => { const summary = response.data.syncSummary const syncHint = summary ? `;自动关联标准商品并重试 ${summary.pendingAssigned} 个待处理订单项` : '' message.success(`配置已保存${syncHint}`) setModalOpen(false) void queryClient.invalidateQueries({ queryKey: ['admin-cost-center'] }) void queryClient.invalidateQueries({ queryKey: ['cost-center-voucher-products'] }) void queryClient.invalidateQueries({ queryKey: ['fulfillment-rules'] }) void queryClient.invalidateQueries({ queryKey: ['fulfillment-pending'] }) }, onError: (error) => { message.error(error instanceof Error ? error.message : '保存失败') }, }) const deleteMutation = useMutation({ mutationFn: (skuKey: string) => deleteAdminManualSku(skuKey), onSuccess: (response) => { message.success( `人工分类已移除;${response.data.affectedItems} 个订单项已退回待分类,原始订单保留`, ) void queryClient.invalidateQueries({ queryKey: ['admin-cost-center-manual-skus'] }) void queryClient.invalidateQueries({ queryKey: ['admin-cost-center-manual-skus-overview'], }) void queryClient.invalidateQueries({ queryKey: ['fulfillment-rules'] }) void queryClient.invalidateQueries({ queryKey: ['fulfillment-pending'] }) }, 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) const configuredChannels = item.config?.channels?.length ? item.config.channels : item.config ? [ { platformKey: item.config.platformKey, creditQuantity: item.config.creditQuantity, }, ] : [{ platformKey: '', creditQuantity: 0 }] form.setFieldsValue({ channels: configuredChannels.map((channel) => ({ platformKey: channel.platformKey, creditQuantity: channel.creditQuantity, })), 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) => { if (!row.config) { return 未配置 } const channels = row.config.channels?.length ? row.config.channels : [ { platformKey: row.config.platformKey, platformLabel: row.config.platformLabel, creditQuantity: row.config.creditQuantity, creditUnitPriceFen: row.config.creditUnitPriceFen, costFen: row.config.costFen, }, ] return ( {channels.map((channel) => (
{channel.platformLabel} {channelQuantityText(channel.platformKey, channel.creditQuantity)} {' · '}¥{((channel.costFen || 0) / 100).toFixed(2)}
))}
) }, }, { title: '成本', key: 'cost', width: 110, align: 'right', render: (_, row) => row.config ? ( (row.config.channels?.length || 0) > 1 ? ( 按实际渠道 ) : row.config.costMode === 'fixed' ? ( ¥{(row.config.costFen / 100).toFixed(2)} ) : ( 不固定·客服填 ) ) : ( '-' ), }, { title: '操作', key: 'actions', width: 150, fixed: 'right', render: (_, row) => ( deleteMutation.mutate(row.skuKey)} > ), }, ] function previewChannelCost(platformKey: string, creditQuantity: number) { const unitPrice = platformMap.get(platformKey as ManualPlatformKey)?.creditUnitPriceFen ?? 0 return Number(creditQuantity) > 0 ? Math.round(Number(creditQuantity) * unitPrice) : 0 } return ( { if (value?.[0] && value?.[1]) setScanRange([value[0], value[1]]) }} /> { 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"> { if (!Array.isArray(channels) || channels.length === 0) { throw new Error('请至少配置一个履约渠道') } }, }, ]} > {(fields, { add, remove }, { errors }) => ( {fields.map((field, index) => (
履约渠道 {index + 1}