import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Alert, Button, Modal, Select, Space, Spin, Table, Tooltip, Typography, message, } from 'antd'; import { DownloadOutlined, InfoCircleOutlined, ReloadOutlined } from '@ant-design/icons'; import { SycmPage } from '../../components/Common'; import { exportStockSalesZeroDetail, getStockSalesAnalysis } from '../../api/stockSalesAnalysis'; import { downloadBlobAsFile, readBlobErrorMessage } from '../../utils/tableExport'; import { STOCK_SALES_COLUMNS, STOCK_SALES_WINDOW_OPTIONS, STOCK_SALES_DEFAULT_DAYS, STOCK_SALES_DESCRIPTION, } from '../../constants/stockSalesAnalysis'; import './style.css'; const { Title, Text } = Typography; const ALL_WAREHOUSES = '__all__'; const int = (value) => { if (value === null || value === undefined || value === '') return '—'; return Number(value).toLocaleString('zh-CN', { maximumFractionDigits: 0 }); }; const percent = (value) => { if (value === null || value === undefined || value === '') return '—'; return `${(Number(value) * 100).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2, })}%`; }; const TipTitle = ({ tip, children }) => ( {children} ); const StockSalesAnalysis = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [data, setData] = useState(null); const [helpOpen, setHelpOpen] = useState(false); const [days, setDays] = useState(STOCK_SALES_DEFAULT_DAYS); const [warehouse, setWarehouse] = useState(ALL_WAREHOUSES); const [exporting, setExporting] = useState(false); const load = useCallback(async (options = {}) => { setLoading(true); setError(''); try { const res = await getStockSalesAnalysis({ days: options.days ?? STOCK_SALES_DEFAULT_DAYS, ...(options.force ? { refresh: '1' } : {}), }); setData(res?.data ?? null); } catch (err) { setError(err?.response?.data?.error || err?.message || '库存动销分析数据加载失败'); setData(null); } finally { setLoading(false); } }, []); useEffect(() => { load({ days: STOCK_SALES_DEFAULT_DAYS }); }, [load]); const handleDaysChange = useCallback((value) => { setDays(value); load({ days: value }); }, [load]); const items = useMemo(() => data?.items ?? [], [data]); const grand = data?.grand ?? null; /** 导出「销售=0」档 SKU 明细:跟随当前统计天数与仓库筛选 */ const handleExportZero = useCallback(async () => { setExporting(true); try { const wmsCoId = warehouse === ALL_WAREHOUSES ? undefined : items.find((it) => it.warehouse === warehouse)?.wmsCoId; const params = { days, format: 'xlsx', ...(wmsCoId != null ? { wmsCoId: String(wmsCoId) } : {}) }; const blob = await exportStockSalesZeroDetail(params); const dateKey = (data?.latestBizDate || '').replace(/-/g, '') || '未知日期'; const scope = warehouse === ALL_WAREHOUSES ? '' : `-${warehouse}`; const fileName = `库存动销分析-销售0明细${scope}-${dateKey}.xlsx`; downloadBlobAsFile(blob, fileName); message.success(`已开始下载:${fileName}`); } catch (exportError) { message.error(await readBlobErrorMessage(exportError, '导出「销售=0」明细失败')); } finally { setExporting(false); } }, [days, warehouse, items, data?.latestBizDate]); const warehouseOptions = useMemo(() => ([ { label: '全部仓库', value: ALL_WAREHOUSES }, ...items.map((it) => ({ label: it.warehouse, value: it.warehouse })), ]), [items]); const visibleItems = useMemo(() => ( warehouse === ALL_WAREHOUSES ? items : items.filter((it) => it.warehouse === warehouse) ), [items, warehouse]); /** 表格行:每个仓库 6 个等级 + 1 行汇总;仓库列跨行合并 */ const tableRows = useMemo(() => { const rows = []; const pushGroup = (warehouseName, buckets, total, extra = {}) => { // 注意:bucket / total 自带 key(等级 key),必须放在展开之后覆盖,否则跨仓行 key 会重复 buckets.forEach((bucket, index) => { rows.push({ ...bucket, ...extra, key: `${warehouseName}::${bucket.key}`, warehouse: warehouseName, warehouseRowSpan: index === 0 ? buckets.length + 1 : 0, grade: bucket.label, }); }); rows.push({ ...total, ...extra, key: `${warehouseName}::__total__`, warehouse: warehouseName, warehouseRowSpan: 0, grade: total.label, isSubtotal: true, }); }; visibleItems.forEach((item) => pushGroup(item.warehouse, item.buckets, item.total)); if (grand && (warehouse === ALL_WAREHOUSES || visibleItems.length === 0)) { pushGroup(grand.warehouse, grand.buckets, grand.total, { isGrand: true }); } return rows; }, [visibleItems, grand, warehouse]); const columns = useMemo(() => STOCK_SALES_COLUMNS.map((col) => ({ title: {col.label}, key: col.key, dataIndex: col.key, align: ['warehouse', 'grade'].includes(col.key) ? 'left' : 'right', width: col.key === 'warehouse' ? 260 : (col.key === 'grade' ? 180 : 130), fixed: col.key === 'warehouse' ? 'left' : undefined, onCell: col.key === 'warehouse' ? (record) => ({ rowSpan: record.warehouseRowSpan }) : undefined, render: (value, record) => { if (col.key === 'warehouse') { if (record.warehouseRowSpan === 0) return null; return {value}; } if (col.key === 'grade') { return {value}; } if (col.key === 'skuRatio' || col.key === 'stockRatio') return percent(value); return int(value); }, })), []); const gradeCount = data?.grades?.length ?? 0; return (
库存动销分析
统计基准日:{data?.latestBizDate || '—'} {data?.shipWindow ? ( 销量窗口:{data.shipWindow.shipFrom} ~ {data.shipWindow.shipTo}(近 {data.shipWindow.days} 天) ) : null} {data?.takenAt ? ( 库存取数时间:{new Date(data.takenAt).toLocaleString('zh-CN', { hour12: false })} ) : null}
统计天数
{error ? : null} [ record?.isGrand ? 'stock-sales-row--grand' : '', record?.isSubtotal ? 'stock-sales-row--subtotal' : '', ].filter(Boolean).join(' ')} /> 共 {visibleItems.length} 个仓库;每个仓库 {gradeCount} 个动销等级 + 1 行汇总 {data?.emptyWarehouseCount > 0 ? `;其中 ${data.emptyWarehouseCount} 个仓在业务库无有效库存或销量` : ''}。 setHelpOpen(false)} footer={null} width={720} >
{STOCK_SALES_DESCRIPTION.map((line, i) => (

{line}

))}
); }; export default StockSalesAnalysis;