import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Alert, Button, Card, Col, DatePicker, Descriptions, Drawer, Dropdown, Input, Row, Select, Space, Spin, Statistic, Table, Tabs, Tag, Typography, message, } from 'antd'; import { DownloadOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import { SycmPage } from '../../components/Common'; import { getOpsBIncentiveBatches, getOpsBIncentiveBoard, getOpsBIncentiveDaily } from '../../api/planningUpload'; import { exportTableFile } from '../../utils/tableExport'; import './opsBIncentiveBoard.css'; const { Title, Text } = Typography; const FALLBACK_BATCH = { key: 'ops-b-20260715-20260721', batchNo: 1, scopeStart: '2026-07-15', scopeEnd: '2026-07-21', pullDate: '2026-08-29', feeCap: 0.9 }; const batchLabel = (item) => { const mmdd = (value) => dayjs(value).format('MM-DD'); const cap = `${parseFloat((Number(item.feeCap) * 100).toFixed(1))}%`; return `第${item.batchNo}批(${mmdd(item.scopeStart)} 至 ${mmdd(item.scopeEnd)} · 拉取 ${mmdd(item.pullDate)} · 费比≤${cap})${item.finalized ? ' · 已定档' : ''}`; }; const number = (value, digits = 2) => value === null || value === undefined || value === '' ? '--' : Number(value).toLocaleString('zh-CN', { minimumFractionDigits: digits, maximumFractionDigits: digits }); const money = (value) => value === null || value === undefined ? '--' : `¥${number(value)}`; const percent = (value) => value === null || value === undefined ? '--' : `${(Number(value) * 100).toFixed(2)}%`; const dateText = (value) => value ? dayjs(value).format('YYYY-MM-DD') : '--'; const OpsBIncentiveBoardPanel = () => { const [batches, setBatches] = useState([]); const [batch, setBatch] = useState(FALLBACK_BATCH.key); const [metricEnd, setMetricEnd] = useState(dayjs(FALLBACK_BATCH.pullDate)); const [filters, setFilters] = useState({ ider: '', shopName: '', spu: '', goodsId: '' }); const [data, setData] = useState({ rows: [], ownerSummary: [], summary: {} }); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [selected, setSelected] = useState(null); const [dailyRows, setDailyRows] = useState([]); const [dailyLoading, setDailyLoading] = useState(false); const selectedBatch = useMemo( () => batches.find((item) => item.key === batch) || FALLBACK_BATCH, [batches, batch] ); useEffect(() => { let cancelled = false; getOpsBIncentiveBatches() .then((result) => { const list = result?.data?.data || result?.data || []; if (cancelled || !Array.isArray(list) || !list.length) return; setBatches(list); if (!list.some((item) => item.key === FALLBACK_BATCH.key)) { setBatch(list[0].key); setMetricEnd(dayjs(list[0].pullDate)); } }) .catch(() => {}); return () => { cancelled = true; }; }, []); const query = useMemo(() => ({ scopeStart: selectedBatch.scopeStart, scopeEnd: selectedBatch.scopeEnd, metricEnd: metricEnd?.format('YYYY-MM-DD') || selectedBatch.pullDate, ...filters, }), [selectedBatch, metricEnd, filters]); const changeBatch = (value) => { setBatch(value); const next = batches.find((item) => item.key === value); if (next) setMetricEnd(dayjs(next.pullDate)); }; const load = useCallback(async () => { setLoading(true); setError(''); try { const result = await getOpsBIncentiveBoard(query); setData(result?.data || result || { rows: [], ownerSummary: [], summary: {} }); } catch (requestError) { setError(requestError?.message || '加载运营中心B看板失败'); } finally { setLoading(false); } }, [query]); useEffect(() => { load(); }, [load]); const openDetail = async (record) => { setSelected(record); setDailyRows([]); setDailyLoading(true); try { const result = await getOpsBIncentiveDaily({ ...query, goodsId: record.goodsId }); setDailyRows(result?.data?.rows || result?.rows || []); } catch (requestError) { message.error(requestError?.message || '加载每日明细失败'); } finally { setDailyLoading(false); } }; const iderOptions = useMemo(() => [...new Set((data.rows || []).map((row) => row.ider).filter(Boolean))].map((value) => ({ value, label: value })), [data.rows]); const shopOptions = useMemo(() => [...new Set((data.rows || []).map((row) => row.shopName).filter(Boolean))].map((value) => ({ value, label: value })), [data.rows]); // 分组:0=动销率与费比均达标,1=仅一项达标,2=均不达标;组内按动销率降序。 const sortedRows = useMemo(() => [...(data.rows || [])].sort((a, b) => { const group = (row) => (row.bonusQualified ? 0 : (row.feeQualified || row.sellThroughQualified ? 1 : 2)); const rate = (row) => (row.sellThroughRate30d === null || row.sellThroughRate30d === undefined ? -1 : row.sellThroughRate30d); return group(a) - group(b) || rate(b) - rate(a) || String(a.goodsId).localeCompare(String(b.goodsId)); }), [data.rows]); const num = (value) => (value === null || value === undefined || value === '' ? -Infinity : Number(value)); const qualifyFilters = [ { text: '达标', value: 'yes' }, { text: '未达标', value: 'no' }, ]; const qualifyFilter = (field) => ({ filters: qualifyFilters, onFilter: (value, row) => (value === 'yes' ? Boolean(row[field]) : !row[field]), }); const columns = [ { title: 'ID负责人', dataIndex: 'ider', fixed: 'left', width: 100 }, { title: '商品ID', dataIndex: 'goodsId', fixed: 'left', width: 130, render: (value, row) => openDetail(row)}>{value} }, { title: '商品名称', dataIndex: 'goodsName', width: 220, ellipsis: true }, { title: '店铺名称', dataIndex: 'shopName', width: 150, ellipsis: true }, { title: 'SPU', dataIndex: 'spu', width: 120 }, { title: '链接类型', dataIndex: 'linkType', width: 100 }, { title: '状态', dataIndex: 'status', width: 80, render: (v) => {v} }, { title: 'SKU数', dataIndex: 'skuCount', width: 80, align: 'right', sorter: (a, b) => num(a.skuCount) - num(b.skuCount) }, { title: '上架时间', dataIndex: 'firstCreatedAt', width: 120, render: dateText, exportValue: dateText }, { title: '上架天数', dataIndex: 'listingDays', width: 90, align: 'right', sorter: (a, b) => num(a.listingDays) - num(b.listingDays), render: (v) => number(v, 0) }, { title: '近30天ID动销率', dataIndex: 'sellThroughRate30d', width: 130, align: 'right', sorter: (a, b) => num(a.sellThroughRate30d) - num(b.sellThroughRate30d), render: percent, exportValue: percent }, { title: '动销率是否达标', dataIndex: 'sellThroughQualified', width: 120, ...qualifyFilter('sellThroughQualified'), render: (v) => {v ? '达标' : '未达标'}, exportValue: (v) => (v ? '达标' : '未达标') }, { title: '近30天费比', dataIndex: 'feeRatio30d', width: 110, align: 'right', sorter: (a, b) => num(a.feeRatio30d) - num(b.feeRatio30d), render: percent, exportValue: percent }, { title: '费比是否达标', dataIndex: 'feeQualified', width: 105, ...qualifyFilter('feeQualified'), render: (v) => {v ? '达标' : '未达标'}, exportValue: (v) => (v ? '达标' : '未达标') }, { title: '近30天真实业绩', dataIndex: 'performance30d', width: 135, align: 'right', sorter: (a, b) => num(a.performance30d) - num(b.performance30d), render: money }, { title: '近30天真实费用', dataIndex: 'fee30d', width: 135, align: 'right', sorter: (a, b) => num(a.fee30d) - num(b.fee30d), render: money }, { title: '近30天发货件数', dataIndex: 'goodsCount30d', width: 125, align: 'right', sorter: (a, b) => num(a.goodsCount30d) - num(b.goodsCount30d), render: (v) => number(v, 0) }, { title: '近30天发货天数', dataIndex: 'shippedDays30d', width: 125, align: 'right', sorter: (a, b) => num(a.shippedDays30d) - num(b.shippedDays30d), render: (v) => number(v, 0) }, { title: '应发奖金', dataIndex: 'bonus', width: 100, align: 'right', sorter: (a, b) => num(a.bonus) - num(b.bonus), render: money }, ]; const ownerSummaryColumns = [ { title: 'ID负责人', dataIndex: 'ider' }, { title: '商品ID数', dataIndex: 'goodsIdCount' }, { title: '近30天发货件数', dataIndex: 'goodsCount30d', render: (v) => number(v, 0) }, { title: '费比达标', dataIndex: 'feeQualifiedCount' }, { title: '奖金达标', dataIndex: 'bonusQualifiedCount' }, { title: '应发奖金', dataIndex: 'bonus', render: money }, ]; const exportFileName = (label) => ( `激励看板_${label}_${selectedBatch.scopeStart}_${selectedBatch.scopeEnd}_统计至${metricEnd?.format('YYYYMMDD') || '最新'}` ); const handleExport = async (scope, format) => { const isSummary = scope === 'summary'; const exportColumns = isSummary ? ownerSummaryColumns : columns; const exportRows = isSummary ? (data.ownerSummary || []) : sortedRows; if (!exportRows.length) { message.warning(isSummary ? '负责人汇总暂无数据可导出' : 'ID核心明细暂无数据可导出'); return; } try { await exportTableFile(format, exportFileName(isSummary ? '负责人汇总' : 'ID核心明细'), exportColumns, exportRows, isSummary ? '负责人汇总' : 'ID核心明细'); message.success(`已导出 ${exportRows.length} 条数据(${format.toUpperCase()})`); } catch (exportError) { message.error(exportError?.message || '导出失败'); } }; const exportDropdown = (scope) => ( handleExport(scope, key), }}> }>导出 ); const summary = data.summary || {}; return ( <> ({ value: item.key, label: batchLabel(item) }))} onChange={changeBatch} style={{ width: 360 }} /> setFilters((old) => ({ ...old, ider: value || '' }))} style={{ width: 150 }} /> setFilters((old) => ({ ...old, shopName: value || '' }))} style={{ width: 180 }} /> } placeholder="SPU" value={filters.spu} onChange={(e) => setFilters((old) => ({ ...old, spu: e.target.value }))} style={{ width: 150 }} /> } placeholder="商品ID" value={filters.goodsId} onChange={(e) => setFilters((old) => ({ ...old, goodsId: e.target.value }))} style={{ width: 170 }} /> } onClick={load} loading={loading}>刷新数据 {error ? 重试} /> : null} setSelected(null)}> {selected ? <>达标 ¥{AWARD} : 未达标 }]} /> number(v, 0) }, { title: '是否动销', dataIndex: 'isActive' }, { title: '真实业绩', dataIndex: 'realPerformance', align: 'right', render: money }, { title: '真实费用', dataIndex: 'realFee', align: 'right', render: money }]} />> : null} > ); }; const OpsBIncentiveBoard = () => ( 激励看板 按中心切换查看商品激励奖金、费比与动销达成情况 }, ]} /> ); const AWARD = 400; export default OpsBIncentiveBoard;