import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Alert, Button, Card, Col, Input, Modal, Row, Select, Space, Spin, Statistic, Table, Tag, Tooltip, Typography, message, } from 'antd'; import { DownloadOutlined, InfoCircleOutlined, ReloadOutlined } from '@ant-design/icons'; import { SycmPage } from '../../components/Common'; import { exportPickingBinSalesAnalysis, getPickingBinSalesAnalysis, } from '../../api/pickingBinSalesAnalysis'; import { downloadBlobAsFile, readBlobErrorMessage } from '../../utils/tableExport'; import { PICKING_BIN_COLUMNS, PICKING_BIN_WINDOW_OPTIONS, PICKING_BIN_DEFAULT_DAYS, PICKING_BIN_GRADE_COLORS, PICKING_BIN_DESCRIPTION, } from '../../constants/pickingBinSalesAnalysis'; import './style.css'; const { Title, Text } = Typography; const int = (value) => { if (value === null || value === undefined || value === '') return '—'; return Number(value).toLocaleString('zh-CN', { maximumFractionDigits: 0 }); }; const num = (value, digits = 2) => { if (value === null || value === undefined || value === '') return '—'; return Number(value).toLocaleString('zh-CN', { minimumFractionDigits: digits, maximumFractionDigits: digits, }); }; const TipTitle = ({ tip, children }) => ( {children} ); const PickingBinSalesAnalysis = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [data, setData] = useState(null); const [helpOpen, setHelpOpen] = useState(false); const [days, setDays] = useState(PICKING_BIN_DEFAULT_DAYS); const [warehouse, setWarehouse] = useState(''); const [grades, setGrades] = useState([]); const [keyword, setKeyword] = useState(''); const [exporting, setExporting] = useState(false); const params = useMemo(() => ({ days, ...(warehouse ? { wmsCoId: warehouse } : {}), }), [days, warehouse]); const load = useCallback(async (options = {}) => { setLoading(true); setError(''); try { const res = await getPickingBinSalesAnalysis({ ...(options.params ?? params), ...(options.force ? { refresh: '1' } : {}), }); setData(res?.data ?? null); } catch (err) { setError(err?.response?.data?.error || err?.message || '拣选库位动销分析数据加载失败'); setData(null); } finally { setLoading(false); } }, [params]); useEffect(() => { load(); }, [load]); const items = useMemo(() => data?.items ?? [], [data]); const gradeMap = useMemo(() => new Map((data?.grades ?? []).map((g) => [g.key, g])), [data]); const filteredRows = useMemo(() => { const kw = keyword.trim().toLowerCase(); return items.filter((it) => { if (grades.length > 0 && !grades.includes(it.gradeKey)) return false; if (kw && !String(it.binName).toLowerCase().includes(kw) && !String(it.skuId).toLowerCase().includes(kw)) return false; return true; }); }, [items, grades, keyword]); const columns = useMemo(() => PICKING_BIN_COLUMNS.map((col) => ({ title: {col.label}, key: col.key, dataIndex: col.key, align: ['warehouse', 'binName', 'binType', 'skuId', 'gradeKey'].includes(col.key) ? 'left' : 'right', width: col.key === 'warehouse' ? 240 : (col.key === 'gradeKey' ? 170 : (col.key === 'skuId' ? 150 : 110)), sorter: ['stockQty', 'salesQty', 'dailySales'].includes(col.key) ? (a, b) => (a[col.key] ?? 0) - (b[col.key] ?? 0) : undefined, render: (value) => { if (col.key === 'gradeKey') { const g = gradeMap.get(value); return {g?.label || value}; } if (col.key === 'binType' || col.key === 'skuId') return {value || '—'}; if (col.key === 'dailySales') return num(value, 2); if (col.key === 'salesQty') return num(value, 2); if (col.key === 'stockQty') return int(value); return value || '—'; }, })), [gradeMap]); const handleExport = useCallback(async () => { setExporting(true); try { const blob = await exportPickingBinSalesAnalysis({ ...params, ...(grades.length > 0 ? { grade: grades.join(',') } : {}), format: 'xlsx', }); const dateKey = (data?.latestBizDate || '').replace(/-/g, '') || '未知日期'; const fileName = `拣选库位动销分析-${dateKey}.xlsx`; await downloadBlobAsFile(blob, fileName); message.success(`已开始下载:${fileName}`); } catch (exportError) { message.error(await readBlobErrorMessage(exportError, '库位动销明细导出失败')); } finally { setExporting(false); } }, [params, grades, data?.latestBizDate]); const toggleGrade = useCallback((key) => { setGrades((prev) => (prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key])); }, []); const summary = data?.summary ?? []; const total = data?.total ?? null; 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}
统计天数 ({ label: w.warehouse, value: w.wmsCoId }))} onChange={(v) => setWarehouse(v || '')} showSearch optionFilterProp="label" style={{ minWidth: 240 }} placeholder="全部仓库" /> setKeyword(e.target.value)} placeholder="搜索仓位 / 商品编码" style={{ width: 180 }} />
{error ? : null} {summary.map((s) => ( toggleGrade(s.key)} className={grades.includes(s.key) ? 'picking-bin-card is-active' : 'picking-bin-card'} >
明细 {int(s.rowCount)} 行 · 库存 {int(s.stockQty)} · 日销 {num(s.dailySales, 1)}
))} {total ? (
明细 {int(total.rowCount)} 行 · 库存 {int(total.stockQty)} · 日销 {num(total.dailySales, 1)}
) : null}
`${r.wmsCoId}::${r.binName}::${r.skuId}`} size="small" bordered pagination={{ pageSize: 50, showSizeChanger: true, pageSizeOptions: [50, 100, 200], showTotal: (t) => `共 ${t} 条明细`, }} scroll={{ x: 'max-content', y: 'calc(100vh - 420px)' }} /> 当前筛选出 {filteredRows.length} 条明细 {items.length !== filteredRows.length ? `(共 ${items.length} 条)` : ''}; 去重商品 {int(data?.total?.skuCount ?? 0)} 个;统计范围:仅拣选位。 setHelpOpen(false)} footer={null} width={760} >
{PICKING_BIN_DESCRIPTION.map((line, i) => (

{line}

))}
); }; export default PickingBinSalesAnalysis;