import React, { useEffect, useMemo, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Alert, Button, Input, Popover, Segmented, Skeleton, Table, Tag, Tooltip, } from 'antd'; import { InfoCircleOutlined, QuestionCircleOutlined, ReloadOutlined, SearchOutlined, WarningOutlined, } from '@ant-design/icons'; import { SycmBusinessPanel, SycmPage } from '../../components/Common'; import { findWarehouseBySlug, FORMULA_DESCRIPTION, LARGE_STOCK_MONITOR_WAREHOUSES, LEVEL_COLOR, LEVEL_FILTER_OPTIONS, LEVEL_LABEL, OVERVIEW_COLUMNS, } from '../../constants/largeStockMonitor'; import { getLargeStockMonitorOverview, getLargeStockMonitorSkuList, } from '../../api/largeStockMonitor'; import './style.css'; function formatNumber(value) { if (value === null || value === undefined || Number.isNaN(Number(value))) return value; const n = Number(value); if (!Number.isFinite(n)) return value; return n.toLocaleString('zh-CN', { maximumFractionDigits: 2 }); } function formatPercent(value) { if (value === null || value === undefined || Number.isNaN(Number(value))) return value; const n = Number(value); if (!Number.isFinite(n)) return value; return `${n.toLocaleString('zh-CN', { maximumFractionDigits: 1 })}%`; } function formatMonths(value) { if (value === null || value === undefined || Number.isNaN(Number(value))) return value; const n = Number(value); if (!Number.isFinite(n)) return value; return `${n.toLocaleString('zh-CN', { maximumFractionDigits: 1 })} 月`; } function formatStock(value) { if (value === null || value === undefined || Number.isNaN(Number(value))) return value; const n = Number(value); if (!Number.isFinite(n)) return value; return n.toLocaleString('zh-CN', { maximumFractionDigits: 0 }); } const OVERVIEW_RENDERERS = { periodStart: formatStock, openingBalance: formatStock, purchaseReturn: formatStock, purchaseIn: formatStock, salesOut: formatStock, salesReturn: formatStock, closingQty: formatStock, turnoverDays: formatPercent, warnLevel: formatStock, slowLevel: formatStock, staleLevel: formatStock, zombieLevel: formatStock, }; const FormulaHelp = () => (
计算口径
); const LargeStockMonitor = () => { const { warehouseSlug } = useParams(); const navigate = useNavigate(); const warehouse = useMemo(() => { if (!warehouseSlug) return null; return findWarehouseBySlug(warehouseSlug) || LARGE_STOCK_MONITOR_WAREHOUSES[0] || null; }, [warehouseSlug]); const [overview, setOverview] = useState(null); const [overviewLoading, setOverviewLoading] = useState(true); const [overviewError, setOverviewError] = useState(null); const [skuListState, setSkuListState] = useState({ items: [], total: 0, page: 1, pageSize: 20, loading: false, }); const [filterLevel, setFilterLevel] = useState(''); const [keyword, setKeyword] = useState(''); useEffect(() => { if (!warehouse) { setOverview(null); setOverviewLoading(false); return; } let cancelled = false; setOverviewLoading(true); setOverviewError(null); getLargeStockMonitorOverview(warehouse.warehouse) .then((res) => { if (cancelled) return; setOverview(res?.data || null); }) .catch((error) => { if (cancelled) return; setOverviewError(error?.message || '获取概览失败'); setOverview(null); }) .finally(() => { if (cancelled) return; setOverviewLoading(false); }); return () => { cancelled = true; }; }, [warehouse]); const reloadSkuList = (nextPage, nextPageSize, nextLevel, nextKeyword) => { if (!warehouse) return; setSkuListState((prev) => ({ ...prev, loading: true })); getLargeStockMonitorSkuList({ warehouse: warehouse.warehouse, page: nextPage, pageSize: nextPageSize, level: nextLevel, keyword: nextKeyword, }) .then((res) => { setSkuListState({ items: res?.data?.items || [], total: res?.data?.total || 0, page: nextPage, pageSize: nextPageSize, loading: false, }); }) .catch(() => { setSkuListState((prev) => ({ ...prev, loading: false })); }); }; useEffect(() => { if (!warehouse) return; reloadSkuList(1, skuListState.pageSize, filterLevel, keyword.trim()); // eslint-disable-next-line react-hooks/exhaustive-deps }, [warehouse, filterLevel]); const handleTableChange = (pagination) => { const nextPage = pagination.current || 1; const nextPageSize = pagination.pageSize || 20; reloadSkuList(nextPage, nextPageSize, filterLevel, keyword.trim()); }; const handleSearch = () => { reloadSkuList(1, skuListState.pageSize, filterLevel, keyword.trim()); }; const overviewCells = useMemo(() => { const map = overview?.overview || null; return OVERVIEW_COLUMNS.map((column) => { const value = map ? map[column.key] : '—'; const rendered = column.available && value !== '—' && OVERVIEW_RENDERERS[column.key] ? OVERVIEW_RENDERERS[column.key](value) : value; return { ...column, value, rendered }; }); }, [overview]); const skuColumns = useMemo(() => ([ { title: 'SKU', dataIndex: 'sku', key: 'sku', width: 130, fixed: 'left', render: (value, record) => (
{value || '-'} {record.spu ? SPU {record.spu} : null}
), }, { title: '商品名称', dataIndex: 'skuName', key: 'skuName', width: 260, render: (value) => {value || '-'}, }, { title: '分类', dataIndex: 'category', key: 'category', width: 120, render: (value) => value || '-', }, { title: '供应商', dataIndex: 'supplier', key: 'supplier', width: 140, render: (value) => value || '-', }, { title: '当前库存', dataIndex: 'jsStock', key: 'jsStock', width: 100, align: 'right', render: formatStock, }, { title: () => ( 库龄 ), dataIndex: 'ageMonths', key: 'ageMonths', width: 100, align: 'right', render: formatMonths, }, { title: () => ( 月度发货 ), dataIndex: 'monthlyShipment', key: 'monthlyShipment', width: 110, align: 'right', render: formatNumber, }, { title: () => ( 日均库存 ), dataIndex: 'avgDailyStock', key: 'avgDailyStock', width: 110, align: 'right', render: formatStock, }, { title: () => ( 库存周转天数 ), dataIndex: 'turnoverDays', key: 'turnoverDays', width: 130, align: 'right', render: formatPercent, }, { title: '级别', dataIndex: 'level', key: 'level', width: 130, fixed: 'right', render: (value) => { if (!value) return —; return {LEVEL_LABEL[value] || value}; }, }, ]), []); const siblingOptions = useMemo(() => LARGE_STOCK_MONITOR_WAREHOUSES.map((entry) => ({ label: `${entry.group} · ${entry.warehouse.replace(/^简尚-/, '')}`, value: entry.slug, idx: entry.idx, group: entry.group, })), []); const fallbackNotice = warehouse && overview && !overview.hasData; return (

大货库存监控 · {warehouse ? warehouse.warehouse : '请选择仓库'} } title="计算口径" trigger="hover" placement="rightTop" >

二级分类:{warehouse ? warehouse.group : '-'} 分布时间:{overview?.asOf || overview?.latestBizDate || '—'} {overview?.latestBizDate ? 库存快照日:{overview.latestBizDate} : null} {overview?.closingTakenAt ? ( 库存取数时间:{new Date(overview.closingTakenAt).toLocaleString('zh-CN', { hour12: false })}(实时) ) : null} {overview?.period ? ( 流水区间:{overview.period.start} ~ {overview.period.end}(左闭右开) ) : null} SKU 数:{overview?.skuCount ?? '—'} 平均周转率:{overview?.turnoverDaysAvg != null ? formatPercent(overview.turnoverDaysAvg) : '—'} 最大周转率:{overview?.turnoverDaysMax != null ? formatPercent(overview.turnoverDaysMax) : '—'}

{ if (value) { navigate(`/planning/large-stock-monitor/${value}`); } }} options={siblingOptions.slice(0, 6)} className="large-stock-monitor-quick-switch" />
{overviewError ? ( ) : null} {fallbackNotice ? ( } message="业务库暂未同步该仓库的库存快照,周转率与四级库存量无数据源,显示「—」" style={{ marginBottom: 12 }} /> ) : null} {overview && overview.hasGrade === false ? ( ) : null} {overview?.hasGrade && overview?.gradeBasisStock ? ( } message={`期末数量按聚水潭实际库存统计;四级库存量由库存快照分级得出,快照为「归属仓」口径(含已调拨到外仓的货,合计 ${formatStock(overview.gradeBasisStock)}),两者口径不同,不能直接相加核对`} style={{ marginBottom: 12 }} /> ) : null} {overview?.requestedDate && overview?.fallbackDate ? ( ) : null} {overview?.requestedDate && !overview?.fallbackDate && !overview?.hasData ? ( ) : null}
{overviewLoading ? ( ) : (
{overviewCells.map((cell) => (
{cell.label} {cell.estimated ? 估 : null} {cell.tip ? ( ) : null} {cell.rendered}
))}
)}
明细按库存快照(归属仓口径)分级,用于识别滞销/慢流/呆滞/僵尸 SKU setKeyword(event.target.value)} onSearch={handleSearch} enterButton={<> 查询} style={{ maxWidth: 320 }} />
`${record.sku}-${record.warehouse}`} columns={skuColumns} dataSource={skuListState.items} loading={skuListState.loading} scroll={{ x: 1280 }} pagination={{ current: skuListState.page, pageSize: skuListState.pageSize, total: skuListState.total, showSizeChanger: true, showTotal: (total) => `共 ${total} 条`, pageSizeOptions: ['20', '50', '100'], }} onChange={handleTableChange} locale={{ emptyText: '该仓库暂无 SKU 库存快照' }} /> ); }; export default LargeStockMonitor;