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 = () => (
计算口径
{FORMULA_DESCRIPTION.map((line) => (
- {line}
))}
);
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"
/>
}
onClick={() => {
if (warehouse) navigate(`/planning/large-stock-monitor/${warehouse.slug}`);
}}
>
刷新
{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;