615 lines
24 KiB
JavaScript
615 lines
24 KiB
JavaScript
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||
import {
|
||
Alert, Button, Card, Col, Dropdown, Image, Input, Modal, Row, Select,
|
||
Space, Spin, Statistic, Switch, Table, Tabs, Tag, Tooltip, Typography, message,
|
||
} from 'antd';
|
||
import { DownloadOutlined, ReloadOutlined, SearchOutlined, SyncOutlined } from '@ant-design/icons';
|
||
import { SycmPage } from '../../components/Common';
|
||
import { getModularStockList, getModularStockFilters, getPurchaseInTransitList, syncModularStock } from '../../api/modularStockMonitor';
|
||
import { exportTableFile } from '../../utils/tableExport';
|
||
import './style.css';
|
||
|
||
const { Title, Text } = Typography;
|
||
|
||
const WARNING_COLOR = { 缺货: 'red', 预警: 'orange', 正常: 'green' };
|
||
|
||
// 表头带口径说明,避免把「真实值 0」误读成「没数据」
|
||
const TipTitle = ({ title, tip }) => (
|
||
<Tooltip title={tip}>
|
||
<span className="msm-th-tip">{title}</span>
|
||
</Tooltip>
|
||
);
|
||
|
||
const number = (value, digits = 2) => value === null || value === undefined || value === ''
|
||
? '--' : Number(value).toLocaleString('zh-CN', { minimumFractionDigits: digits, maximumFractionDigits: digits });
|
||
const int = (value) => number(value, 0);
|
||
const text = (value) => (value === null || value === undefined || value === '' ? '--' : value);
|
||
|
||
const toOptions = (list = []) => (Array.isArray(list) ? list : [])
|
||
.filter(Boolean)
|
||
.map((value) => ({ value, label: value }));
|
||
|
||
const EMPTY_FILTERS = { keyword: '', category: '', warningLevel: '', mkhStatus: '' };
|
||
|
||
const AccessoryStockPanel = () => {
|
||
const [filters, setFilters] = useState(EMPTY_FILTERS);
|
||
const [onlyWarning, setOnlyWarning] = useState(false);
|
||
const [filterOptions, setFilterOptions] = useState({ categories: [], warningLevels: [] });
|
||
const [data, setData] = useState({ rows: [], total: 0, page: 1, pageSize: 50, summary: {}, monitorDate: null });
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState('');
|
||
const [syncing, setSyncing] = useState(false);
|
||
const [page, setPage] = useState(1);
|
||
const [pageSize, setPageSize] = useState(50);
|
||
const [sorter, setSorter] = useState({ field: 'realtimeStock', order: 'DESC' });
|
||
const tableWrapRef = useRef(null);
|
||
const [bodyHeight, setBodyHeight] = useState(420);
|
||
|
||
// 采购在途明细弹窗状态
|
||
const [transitVisible, setTransitVisible] = useState(false);
|
||
const [transitData, setTransitData] = useState({ rows: [], total: 0 });
|
||
const [transitLoading, setTransitLoading] = useState(false);
|
||
const [transitPage, setTransitPage] = useState(1);
|
||
const [transitPageSize, setTransitPageSize] = useState(50);
|
||
const [transitKeyword, setTransitKeyword] = useState('');
|
||
const [transitSorter, setTransitSorter] = useState({ field: 'transitQty', order: 'DESC' });
|
||
|
||
// 表体高度按「表格顶部 → 内容区底部」实测计算,替代纯 CSS calc:
|
||
// 不同显示器缩放(125%/150%)下 100vh 偏小,写死的 calc 会把表格压到只剩一两行
|
||
useEffect(() => {
|
||
const el = tableWrapRef.current;
|
||
if (!el) return undefined;
|
||
|
||
// 分页器 + 卡片内边距 + 横向滚动条余量
|
||
const RESERVE_BELOW = 104;
|
||
|
||
const measure = () => {
|
||
const scroller = el.closest('.planning-subsystem-content');
|
||
const elRect = el.getBoundingClientRect();
|
||
let available;
|
||
if (scroller) {
|
||
const scrollerRect = scroller.getBoundingClientRect();
|
||
// 加回 scrollTop,抵消滚动对 getBoundingClientRect 的影响
|
||
available = scrollerRect.bottom - elRect.top - RESERVE_BELOW + scroller.scrollTop;
|
||
} else {
|
||
available = window.innerHeight - elRect.top - RESERVE_BELOW;
|
||
}
|
||
const next = Math.max(240, Math.round(available));
|
||
setBodyHeight((prev) => (Math.abs(prev - next) > 1 ? next : prev));
|
||
};
|
||
|
||
measure();
|
||
const ro = new ResizeObserver(measure);
|
||
ro.observe(el);
|
||
if (el.parentElement) ro.observe(el.parentElement);
|
||
window.addEventListener('resize', measure);
|
||
return () => {
|
||
ro.disconnect();
|
||
window.removeEventListener('resize', measure);
|
||
};
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
getModularStockFilters()
|
||
.then((result) => {
|
||
const payload = result?.data || {};
|
||
if (cancelled) return;
|
||
setFilterOptions({
|
||
categories: payload.categories || [],
|
||
warningLevels: payload.warningLevels || ['缺货', '预警', '正常'],
|
||
mkhStatuses: payload.mkhStatuses || ['已配置MKH', '未配置MKH'],
|
||
});
|
||
})
|
||
.catch(() => {});
|
||
return () => { cancelled = true; };
|
||
}, []);
|
||
|
||
const query = useMemo(() => {
|
||
const params = {
|
||
page,
|
||
pageSize,
|
||
sortField: sorter.field,
|
||
sortOrder: sorter.order,
|
||
};
|
||
Object.entries(filters).forEach(([key, value]) => {
|
||
if (value) params[key] = value;
|
||
});
|
||
if (onlyWarning) params.onlyWarning = '1';
|
||
return params;
|
||
}, [page, pageSize, sorter, filters, onlyWarning]);
|
||
|
||
const load = useCallback(async () => {
|
||
setLoading(true);
|
||
setError('');
|
||
try {
|
||
const result = await getModularStockList(query);
|
||
setData(result?.data || { rows: [], total: 0, summary: {} });
|
||
} catch (requestError) {
|
||
setError(requestError?.response?.data?.error || requestError?.message || '加载配件库存监控数据失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [query]);
|
||
|
||
useEffect(() => { load(); }, [load]);
|
||
|
||
const changeFilter = (key) => (value) => {
|
||
setFilters((old) => ({ ...old, [key]: value || '' }));
|
||
setPage(1);
|
||
};
|
||
const resetFilters = () => {
|
||
setFilters(EMPTY_FILTERS);
|
||
setOnlyWarning(false);
|
||
setPage(1);
|
||
};
|
||
|
||
// 点击顶部统计卡片 → 自动设置对应筛选条件并滚动到表格明细
|
||
const applySummaryFilter = (type) => () => {
|
||
const next = { ...EMPTY_FILTERS };
|
||
let warning = false;
|
||
if (type === 'outOfStock') {
|
||
next.warningLevel = '缺货';
|
||
warning = true;
|
||
} else if (type === 'lowStockDays') {
|
||
next.warningLevel = '预警';
|
||
warning = true;
|
||
} else if (type === 'configured') {
|
||
next.mkhStatus = '已配置MKH';
|
||
} else if (type === 'unconfigured') {
|
||
next.mkhStatus = '未配置MKH';
|
||
}
|
||
setFilters(next);
|
||
setOnlyWarning(warning);
|
||
setPage(1);
|
||
setTimeout(() => {
|
||
tableWrapRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
}, 0);
|
||
};
|
||
|
||
// 打开采购在途明细弹窗
|
||
const openPurchaseTransit = () => {
|
||
setTransitVisible(true);
|
||
setTransitPage(1);
|
||
setTransitKeyword('');
|
||
setTransitSorter({ field: 'transitQty', order: 'DESC' });
|
||
loadPurchaseTransit(1, '', { field: 'transitQty', order: 'DESC' });
|
||
};
|
||
|
||
const loadPurchaseTransit = useCallback(async (p = transitPage, kw = transitKeyword, sort = transitSorter) => {
|
||
setTransitLoading(true);
|
||
try {
|
||
const result = await getPurchaseInTransitList({
|
||
page: p,
|
||
pageSize: transitPageSize,
|
||
keyword: kw,
|
||
sortField: sort.field,
|
||
sortOrder: sort.order,
|
||
});
|
||
setTransitData(result?.data || { rows: [], total: 0 });
|
||
} catch (err) {
|
||
message.error(err?.response?.data?.error || err?.message || '加载采购在途明细失败');
|
||
} finally {
|
||
setTransitLoading(false);
|
||
}
|
||
}, [transitPage, transitPageSize, transitKeyword, transitSorter]);
|
||
|
||
const columns = useMemo(() => [
|
||
{
|
||
title: '图片',
|
||
dataIndex: 'imageUrl',
|
||
width: 56,
|
||
fixed: 'left',
|
||
exportable: false,
|
||
render: (value) => (value
|
||
? <Image src={value} width={36} height={36} style={{ objectFit: 'cover', borderRadius: 4 }} placeholder />
|
||
: <span className="msm-muted">--</span>),
|
||
},
|
||
{
|
||
title: '预警',
|
||
dataIndex: 'warningLevel',
|
||
width: 76,
|
||
sorter: true,
|
||
render: (value) => (value
|
||
? <Tag color={WARNING_COLOR[value] || 'default'}>{value}</Tag>
|
||
: <span className="msm-muted">--</span>),
|
||
},
|
||
{
|
||
title: <TipTitle
|
||
title="状态"
|
||
tip="该商品(配件)是否作为「配件商品编码」出现在聚水潭 SKU 级 BOM 维护中、被某 MKH 模块化成品引用。已配置MKH = 业务库 rpt.jst_skuBomItem 中 map_outer_sku_id 命中该商品编码、且父项 sku_id 以 MKH 开头(被某 MKH 成品 BOM 引用为配件);未配置MKH = 未出现在任何 MKH 成品 BOM 的配件清单中"
|
||
/>,
|
||
dataIndex: 'mkhStatus',
|
||
width: 110,
|
||
sorter: true,
|
||
render: (value) => {
|
||
const v = value || '未配置MKH';
|
||
return v === '已配置MKH'
|
||
? <Tag color="green">{v}</Tag>
|
||
: <Tag color="default">{v}</Tag>;
|
||
},
|
||
},
|
||
{
|
||
title: '商品编码',
|
||
dataIndex: 'skuId',
|
||
width: 176,
|
||
fixed: 'left',
|
||
sorter: true,
|
||
render: (value) => <span className="msm-sku">{text(value)}</span>,
|
||
},
|
||
{
|
||
// 必须显式给 width:antd 在 scroll.x 场景下会把无宽度列压成 0 宽(此前该列不可见的原因)
|
||
title: '商品名称',
|
||
dataIndex: 'productName',
|
||
width: 220,
|
||
ellipsis: true,
|
||
sorter: true,
|
||
render: (value) => <Tooltip title={value}>{text(value)}</Tooltip>,
|
||
},
|
||
{
|
||
title: <TipTitle
|
||
title="当日入库库存"
|
||
tip="监控日(pm_stockMonitor 最新快照日)的采购到货入库数量,取自 jst_purchaseItem.inQty。业务库无出入库流水表,0 表示该 SKU 当日无到货"
|
||
/>,
|
||
dataIndex: 'inQtyToday',
|
||
width: 116,
|
||
align: 'right',
|
||
render: (value) => (value === null || value === undefined
|
||
? <span className="msm-muted">--</span>
|
||
: int(value)),
|
||
},
|
||
{
|
||
title: <TipTitle
|
||
title="当日出库库存"
|
||
tip="监控日的最近一日发货量,取自 pm_stockMonitor.nearSecondSale。业务库无出入库流水表,0 表示该 SKU 当日无发货"
|
||
/>,
|
||
dataIndex: 'outQtyToday',
|
||
width: 116,
|
||
align: 'right',
|
||
render: (value) => (value === null || value === undefined
|
||
? <span className="msm-muted">--</span>
|
||
: int(value)),
|
||
},
|
||
{
|
||
title: <TipTitle
|
||
title="实时库存"
|
||
tip="优先取目标仓 jst_inventoryInfo;该表为 0 或缺少记录、但箱及仓位库存已有正库存时,用同仓 jst_packInventoryInfo 合计补齐"
|
||
/>,
|
||
dataIndex: 'realtimeStock',
|
||
width: 96,
|
||
align: 'right',
|
||
sorter: true,
|
||
render: int,
|
||
},
|
||
{
|
||
title: <TipTitle
|
||
title="采购在途数量"
|
||
tip="采购单中『已下单 − 已入库』的数量,取自 jst_purchaseItem.qty − inQty;它与实时库存独立,上游采购到货字段滞后时可能同时显示库存和在途"
|
||
/>,
|
||
dataIndex: 'purchaseTransit',
|
||
width: 120,
|
||
align: 'right',
|
||
render: (value) => (value === null || value === undefined || Number(value) === 0
|
||
? <span className="msm-muted">--</span>
|
||
: int(value)),
|
||
},
|
||
{
|
||
title: <TipTitle
|
||
title="仓位"
|
||
tip="取自聚水潭「箱及仓位库存」jst_packInventoryInfo 的真货位(bin),同商品多箱时显示库存最大的主货位;该表查不到才显示「未维护」"
|
||
/>,
|
||
dataIndex: 'location',
|
||
width: 88,
|
||
sorter: true,
|
||
render: (value) => (value
|
||
? <span>{value}</span>
|
||
: <Tooltip title="上游未提供库位数据">
|
||
<span className="msm-muted">未维护</span>
|
||
</Tooltip>),
|
||
},
|
||
{
|
||
title: '近15天日均发货量',
|
||
dataIndex: 'avgShip15d',
|
||
width: 138,
|
||
align: 'right',
|
||
sorter: true,
|
||
render: (value) => number(value),
|
||
},
|
||
{
|
||
title: '库存可用天数',
|
||
dataIndex: 'stockDays',
|
||
width: 116,
|
||
align: 'right',
|
||
sorter: true,
|
||
render: (value) => number(value, 1),
|
||
},
|
||
{
|
||
title: '所在仓库',
|
||
dataIndex: 'warehouse',
|
||
width: 200,
|
||
ellipsis: true,
|
||
sorter: true,
|
||
render: (value) => <Tooltip title={value}>{text(value)}</Tooltip>,
|
||
},
|
||
], []);
|
||
|
||
const exportColumns = useMemo(
|
||
() => columns.filter((column) => column.exportable !== false),
|
||
[columns]
|
||
);
|
||
|
||
const handleExport = async (format) => {
|
||
if (!data.rows.length) {
|
||
message.warning('暂无数据可导出');
|
||
return;
|
||
}
|
||
try {
|
||
await exportTableFile(
|
||
format,
|
||
`配件库存监控_${data.monitorDate || '最新'}`,
|
||
exportColumns,
|
||
data.rows,
|
||
'配件库存监控'
|
||
);
|
||
message.success(`已导出 ${data.rows.length} 条数据(${format.toUpperCase()})`);
|
||
} catch (exportError) {
|
||
message.error(exportError?.message || '导出失败');
|
||
}
|
||
};
|
||
|
||
// 手动同步:后端重新取最新监控日(清服务端缓存)后再拉一次列表,
|
||
// 拿到的是副本库当前最新快照;可随时点击,不受 24 点定时任务限制
|
||
const handleSync = async () => {
|
||
setSyncing(true);
|
||
try {
|
||
await syncModularStock();
|
||
await load();
|
||
message.success('已同步最新数据');
|
||
} catch (syncError) {
|
||
message.error(syncError?.response?.data?.error || syncError?.message || '同步失败');
|
||
} finally {
|
||
setSyncing(false);
|
||
}
|
||
};
|
||
|
||
const summary = data.summary || {};
|
||
|
||
return (
|
||
<>
|
||
<Card className="msm-filter-card" bordered={false}>
|
||
<Space wrap size={[12, 12]}>
|
||
<Input
|
||
allowClear
|
||
prefix={<SearchOutlined />}
|
||
placeholder="商品编码 / 名称"
|
||
value={filters.keyword}
|
||
onChange={(e) => changeFilter('keyword')(e.target.value)}
|
||
style={{ width: 220 }}
|
||
/>
|
||
<Select
|
||
allowClear
|
||
showSearch
|
||
placeholder="品类"
|
||
options={toOptions(filterOptions.categories)}
|
||
value={filters.category || undefined}
|
||
onChange={changeFilter('category')}
|
||
style={{ width: 200 }}
|
||
/>
|
||
<Select
|
||
allowClear
|
||
placeholder="预警等级"
|
||
options={toOptions(filterOptions.warningLevels)}
|
||
value={filters.warningLevel || undefined}
|
||
onChange={changeFilter('warningLevel')}
|
||
style={{ width: 140 }}
|
||
/>
|
||
<Select
|
||
allowClear
|
||
placeholder="MKH 配置状态"
|
||
options={toOptions(filterOptions.mkhStatuses)}
|
||
value={filters.mkhStatus || undefined}
|
||
onChange={changeFilter('mkhStatus')}
|
||
style={{ width: 160 }}
|
||
/>
|
||
<Space size={6}>
|
||
<Switch checked={onlyWarning} onChange={(checked) => { setOnlyWarning(checked); setPage(1); }} size="small" />
|
||
<Text type="secondary">仅看缺货 / 预警</Text>
|
||
</Space>
|
||
<Button onClick={resetFilters}>重置</Button>
|
||
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}>刷新数据</Button>
|
||
<Tooltip title="立即重新拉取副本库最新数据(清服务端缓存后重查),可随时点击,不必等凌晨的定时刷新">
|
||
<Button type="primary" icon={<SyncOutlined />} onClick={handleSync} loading={syncing}>同步最新数据</Button>
|
||
</Tooltip>
|
||
</Space>
|
||
</Card>
|
||
|
||
{error ? <Alert type="error" showIcon message={error} action={<Button size="small" onClick={load}>重试</Button>} /> : null}
|
||
|
||
<Row gutter={[12, 12]} className="msm-summary-row">
|
||
<Col xs={12} md={4}><Card bordered={false} className="msm-summary-card"><Statistic title="SKU 总数" value={summary.skuCount || 0} /></Card></Col>
|
||
<Col xs={12} md={4}>
|
||
<Card bordered={false} className="msm-summary-card is-red is-clickable" onClick={applySummaryFilter('outOfStock')}>
|
||
<Statistic title="缺货 SKU" value={summary.outOfStockCount || 0} />
|
||
</Card>
|
||
</Col>
|
||
<Col xs={12} md={4}>
|
||
<Card bordered={false} className="msm-summary-card is-orange is-clickable" onClick={applySummaryFilter('lowStockDays')}>
|
||
<Statistic title="可用天数不足" value={summary.lowStockDaysCount || 0} suffix="个 SKU" />
|
||
</Card>
|
||
</Col>
|
||
<Col xs={12} md={4}><Card bordered={false} className="msm-summary-card is-green"><Statistic title="实时库存合计" value={summary.totalStock || 0} /></Card></Col>
|
||
<Col xs={12} md={4}>
|
||
<Card bordered={false} className="msm-summary-card is-blue is-clickable" onClick={openPurchaseTransit}>
|
||
<Statistic title="采购在途" value={summary.totalPurchaseTransit || 0} suffix="件" />
|
||
</Card>
|
||
</Col>
|
||
<Col xs={12} md={4}>
|
||
<Card bordered={false} className="msm-summary-card is-clickable" onClick={applySummaryFilter('configured')}>
|
||
<Statistic title="已配置MKH" value={summary.configuredCount || 0} />
|
||
</Card>
|
||
</Col>
|
||
<Col xs={12} md={4}>
|
||
<Card bordered={false} className="msm-summary-card is-clickable" onClick={applySummaryFilter('unconfigured')}>
|
||
<Statistic title="未配置MKH" value={summary.unconfiguredCount || 0} />
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
|
||
<Card
|
||
bordered={false}
|
||
className="msm-panel"
|
||
title={(
|
||
<Space size={12} wrap>
|
||
<span>配件库存明细({data.total || 0} 条)</span>
|
||
{data.monitorDate ? <Text type="secondary" className="msm-panel__date">监控日 {data.monitorDate}</Text> : null}
|
||
</Space>
|
||
)}
|
||
extra={(
|
||
<Dropdown menu={{
|
||
items: [
|
||
{ key: 'xlsx', label: '导出 XLSX' },
|
||
{ key: 'csv', label: '导出 CSV' },
|
||
],
|
||
onClick: ({ key }) => handleExport(key),
|
||
}}>
|
||
<Button icon={<DownloadOutlined />}>导出</Button>
|
||
</Dropdown>
|
||
)}
|
||
>
|
||
<Spin spinning={loading} tip="正在加载配件库存监控数据…">
|
||
<div ref={tableWrapRef}>
|
||
<Table
|
||
rowKey="skuId"
|
||
size="small"
|
||
columns={columns}
|
||
dataSource={data.rows || []}
|
||
onChange={(pagination, _filter, nextSorter) => {
|
||
setPage(pagination.current || 1);
|
||
setPageSize(pagination.pageSize || 50);
|
||
if (nextSorter?.field) {
|
||
setSorter({
|
||
field: nextSorter.field,
|
||
order: nextSorter.order === 'ascend' ? 'ASC' : 'DESC',
|
||
});
|
||
}
|
||
}}
|
||
pagination={{
|
||
current: data.page || page,
|
||
pageSize: data.pageSize || pageSize,
|
||
total: data.total || 0,
|
||
showSizeChanger: true,
|
||
pageSizeOptions: [50, 100, 200],
|
||
showTotal: (total) => `共 ${total} 条`,
|
||
}}
|
||
scroll={{ x: 1508, y: bodyHeight }}
|
||
/>
|
||
</div>
|
||
</Spin>
|
||
</Card>
|
||
|
||
<Modal
|
||
title={(
|
||
<Space>
|
||
<span>采购在途明细</span>
|
||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||
共 {transitData.total || 0} 条采购单 / 在途 {summary.totalPurchaseTransit || 0} 件
|
||
</Text>
|
||
</Space>
|
||
)}
|
||
open={transitVisible}
|
||
onCancel={() => setTransitVisible(false)}
|
||
width={960}
|
||
footer={null}
|
||
destroyOnHidden
|
||
>
|
||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||
<Input.Search
|
||
allowClear
|
||
placeholder="商品编码 / 名称"
|
||
defaultValue={transitKeyword}
|
||
onSearch={(value) => { setTransitKeyword(value); setTransitPage(1); loadPurchaseTransit(1, value, transitSorter); }}
|
||
style={{ width: 260 }}
|
||
/>
|
||
<Table
|
||
rowKey={(row) => `${row.skuId}-${row.poiId || row.poId || Math.random()}`}
|
||
size="small"
|
||
loading={transitLoading}
|
||
columns={[
|
||
{
|
||
title: '商品编码', dataIndex: 'skuId', width: 176, render: (v) => <span className="msm-sku">{text(v)}</span>,
|
||
},
|
||
{
|
||
title: '商品名称', dataIndex: 'productName', width: 240, ellipsis: true, render: (v) => <Tooltip title={v}>{text(v)}</Tooltip>,
|
||
},
|
||
{ title: '采购数量', dataIndex: 'purchaseQty', width: 96, align: 'right', render: int, sorter: true },
|
||
{ title: '已入库', dataIndex: 'inQty', width: 84, align: 'right', render: int, sorter: true },
|
||
{
|
||
title: '在途数量', dataIndex: 'transitQty', width: 96, align: 'right', sorter: true,
|
||
render: (value) => <strong style={{ color: '#1677ff' }}>{int(value)}</strong>,
|
||
},
|
||
{
|
||
title: '计划到货日期', dataIndex: 'deliveryDate', width: 120, align: 'center', sorter: true,
|
||
render: (value) => text(value),
|
||
},
|
||
{ title: '采购单号', dataIndex: 'poId', width: 116, render: text },
|
||
]}
|
||
dataSource={transitData.rows || []}
|
||
pagination={{
|
||
current: transitPage,
|
||
pageSize: transitPageSize,
|
||
total: transitData.total || 0,
|
||
showSizeChanger: true,
|
||
pageSizeOptions: [50, 100, 200],
|
||
showTotal: (total) => `共 ${total} 条`,
|
||
}}
|
||
onChange={(pagination, _filters, nextSorter) => {
|
||
setTransitPage(pagination.current || 1);
|
||
setTransitPageSize(pagination.pageSize || 50);
|
||
const nextSort = nextSorter?.field
|
||
? { field: nextSorter.field, order: nextSorter.order === 'ascend' ? 'ASC' : 'DESC' }
|
||
: { field: 'transitQty', order: 'DESC' };
|
||
setTransitSorter(nextSort);
|
||
loadPurchaseTransit(pagination.current || 1, transitKeyword, nextSort);
|
||
}}
|
||
scroll={{ x: 860, y: 360 }}
|
||
/>
|
||
</Space>
|
||
</Modal>
|
||
</>
|
||
);
|
||
};
|
||
|
||
const ModularStockMonitor = () => (
|
||
<SycmPage className="msm-page">
|
||
<header className="msm-header">
|
||
<div className="msm-header__heading">
|
||
<Title level={3}>模块化库存监控</Title>
|
||
<Text type="secondary">
|
||
配件库存监控:仅统计「简尚-崔桥工厂半成品包装项目仓」下的 SKU 实时库存、近 15 天日均发货量与可用天数
|
||
</Text>
|
||
<Text type="secondary" className="msm-note">
|
||
数据口径:实时库存取 jst_inventoryInfo(wms_co_id 15718196);近 15 天日均发货量、
|
||
库存可用天数、安全天数取 pm_stockMonitor 最新快照(按完整 SKU 编码关联,794 个 SKU 全覆盖);
|
||
当日入库取采购到货、当日出库取近一日发货量,均以监控日为基准;
|
||
仓位取聚水潭「箱及仓位库存」(jst_packInventoryInfo)的真货位,同商品多箱时显示库存最大的主货位,查不到才显示「未维护」;
|
||
图片取自「产品资料系统数据库」商品档案(业务库图片字段均为空)。
|
||
注:日均发货量 2026-09-17 起由近 7 天改为近 15 天(业务原要求 30 天,但业务库无 30 天日销,最长只到 15 天);
|
||
库存可用天数仍沿用监控表 jsUseDays(源系统口径),故两者并非同一时间窗。
|
||
状态列:基于聚水潭 SKU 级 BOM 维护(rpt.jst_skuBomItem)判断该商品是否作为「配件商品编码」被某 MKH 成品 BOM 引用——已配置MKH = 出现在某 MKH 成品的 BOM 配件清单中;未配置MKH = 未出现。
|
||
</Text>
|
||
</div>
|
||
</header>
|
||
<Tabs
|
||
className="msm-tabs"
|
||
activeKey="accessory"
|
||
items={[{
|
||
key: 'accessory',
|
||
label: '配件库存监控',
|
||
children: <AccessoryStockPanel />,
|
||
}]}
|
||
/>
|
||
</SycmPage>
|
||
);
|
||
|
||
export default ModularStockMonitor;
|