2026-09-17 11:42:50 +08:00

434 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Alert, Button, Card, Input, Space, Spin, Table, Tooltip, Typography, message,
} from 'antd';
import { PrinterOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
import { SycmPage } from '../../components/Common';
import { getMkhBomList, getMkhBomDetail } from '../../api/mkhBom';
import './style.css';
const { Title, Text } = Typography;
const number = (value, digits = 0) => (value === null || value === undefined || value === ''
? '0'
: Number(value).toLocaleString('zh-CN', {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
}));
const text = (value) => (value === null || value === undefined || value === '' ? '--' : value);
const EMPTY_FILTERS = { keyword: '' };
// A4 打印预览:表头 MKH商品编码 | 配件明细 | 数量 | 配件仓位
const PrintPreview = ({ data, onClose }) => {
const now = useMemo(() => new Date().toLocaleString('zh-CN'), []);
const items = data.items || [];
useEffect(() => {
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose]);
return (
<div className="mkh-print-mask">
<div className="mkh-print-toolbar">
<Space>
<Button type="primary" icon={<PrinterOutlined />} onClick={() => window.print()}>一键打印</Button>
<Button onClick={onClose}>关闭</Button>
<Text type="secondary">按 Esc 关闭预览</Text>
</Space>
</div>
<div className="mkh-print-sheet">
<div className="mkh-print-head">
<div className="mkh-print-title">MKH-BOM 资料</div>
<div className="mkh-print-sub">模块化商品配件明细 · 打印模板</div>
</div>
<div className="mkh-print-meta">
<div><span>打印时间:</span><b>{now}</b></div>
<div><span>配件种类:</span><b>{items.length}</b></div>
</div>
<table className="mkh-print-table">
<thead>
<tr>
<th style={{ width: '170px' }}>MKH商品编码</th>
<th>配件明细</th>
<th style={{ width: '64px' }}>数量</th>
<th style={{ width: '110px' }}>配件仓位</th>
</tr>
</thead>
<tbody>
{items.map((item, idx) => (
<tr key={item.index}>
{idx === 0 ? (
<td className="mkh-print-sku" rowSpan={items.length}>{text(data.skuId)}</td>
) : null}
<td>{text(item.skuName)}{item.skuId ? <span className="mkh-print-code">{item.skuId}</span> : null}</td>
<td className="mkh-center">{number(item.qty)}</td>
<td className="mkh-center">{item.location ? text(item.location) : <span className="mkh-muted">未维护</span>}</td>
</tr>
))}
{items.length === 0 ? (
<tr>
<td className="mkh-print-sku">{text(data.skuId)}</td>
<td colSpan={3} className="mkh-muted">该 MKH 商品尚无可用的聚水潭商品 BOM 明细</td>
</tr>
) : null}
</tbody>
</table>
<div className="mkh-print-foot">
<span>
MKH 商品编码取自指定仓库商品主档;配件优先取聚水潭商品 BOM,业务组合 BOM 与产品资料系统作为补充;
配件仓位只取箱及仓位库存表;没有箱号或仓位记录时显示「未维护」。
</span>
</div>
</div>
</div>
);
};
const MkhBomData = () => {
const [filters, setFilters] = useState(EMPTY_FILTERS);
const [data, setData] = useState({ rows: [], total: 0, page: 1, pageSize: 50, bomReady: false });
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(50);
const [sorter, setSorter] = useState({ field: 'skuId', order: 'ASC' });
const [expandedKeys, setExpandedKeys] = useState({});
const [detailCache, setDetailCache] = useState({});
const [detailLoading, setDetailLoading] = useState({});
const [printData, setPrintData] = useState(null);
const [printLoading, setPrintLoading] = useState(false);
const tableWrapRef = useRef(null);
const [bodyHeight, setBodyHeight] = useState(420);
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();
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);
};
}, []);
const query = useMemo(() => {
const params = { page, pageSize, sortField: sorter.field, sortOrder: sorter.order };
Object.entries(filters).forEach(([key, value]) => { if (value) params[key] = value; });
return params;
}, [page, pageSize, sorter, filters]);
const load = useCallback(async () => {
setLoading(true);
setError('');
try {
const result = await getMkhBomList(query);
if (!result || result.success === false) {
setError(result?.error || '加载 MKH-BOM 资料失败');
return;
}
setData(result?.data || { rows: [], total: 0, page: 1, pageSize: 50 });
} catch (requestError) {
setError(requestError?.response?.data?.error || requestError?.message || '加载 MKH-BOM 资料失败');
} finally {
setLoading(false);
}
}, [query]);
useEffect(() => { load(); }, [load]);
const changeFilter = (key) => (value) => {
setFilters((old) => ({ ...old, [key]: value || '' }));
setPage(1);
};
const resetFilters = () => { setFilters(EMPTY_FILTERS); setPage(1); };
const loadDetail = useCallback(async (skuId) => {
if (detailCache[skuId] || detailLoading[skuId]) return;
setDetailLoading((old) => ({ ...old, [skuId]: true }));
try {
const result = await getMkhBomDetail(skuId);
if (!result || result.success === false) {
message.error(result?.error || '加载配件明细失败');
return;
}
setDetailCache((old) => ({ ...old, [skuId]: result?.data || { items: [] } }));
} catch (e) {
message.error(e?.response?.data?.error || e?.message || '加载配件明细失败');
} finally {
setDetailLoading((old) => ({ ...old, [skuId]: false }));
}
}, [detailCache, detailLoading]);
const handlePrint = async (skuId) => {
setPrintLoading(true);
try {
const result = await getMkhBomDetail(skuId);
if (!result || result.success === false) {
message.error(result?.error || '加载打印数据失败');
return;
}
setPrintData(result?.data || { skuId, items: [] });
} catch (e) {
message.error(e?.response?.data?.error || e?.message || '加载打印数据失败');
} finally {
setPrintLoading(false);
}
};
const columns = useMemo(() => [
{
title: 'MKH商品编码',
dataIndex: 'skuId',
width: 240,
fixed: 'left',
sorter: true,
render: (value) => <span className="mkh-sku">{text(value)}</span>,
},
{
title: '商品名称',
dataIndex: 'productName',
ellipsis: true,
sorter: true,
render: (value) => <Tooltip title={value}>{text(value)}</Tooltip>,
},
{
title: 'BOM 状态',
dataIndex: 'bomConfigured',
width: 100,
align: 'center',
render: (value, row) => (row.bomConfigured === '已配置'
? <span className="mkh-tag mkh-tag--ok">已配置</span>
: <span className="mkh-tag mkh-tag--muted">未配置</span>),
},
{
title: '配件种类',
dataIndex: 'accessoryCount',
width: 110,
align: 'right',
sorter: true,
render: (value) => number(value),
},
{
title: '配件总数量',
dataIndex: 'accessoryQtySum',
width: 120,
align: 'right',
sorter: true,
render: (value) => number(value),
},
{
title: '操作',
key: 'action',
width: 190,
fixed: 'right',
render: (_value, row) => (
<Space size={6}>
<Button
type="primary"
size="small"
icon={<PrinterOutlined />}
loading={printLoading}
onClick={() => handlePrint(row.skuId)}
>
一键打印
</Button>
<Button
size="small"
onClick={() => {
const next = !expandedKeys[row.skuId];
setExpandedKeys((old) => ({ ...old, [row.skuId]: next }));
if (next) loadDetail(row.skuId);
}}
>
{expandedKeys[row.skuId] ? '收起' : '配件明细'}
</Button>
</Space>
),
},
], [expandedKeys, printLoading, loadDetail]);
const expandedRowRender = (row) => {
const detail = detailCache[row.skuId];
if (detailLoading[row.skuId] && !detail) {
return <div className="mkh-detail-loading"><Spin size="small" /> 加载配件明细…</div>;
}
const items = detail?.items || [];
return (
<div className="mkh-detail">
{items.length === 0 ? (
<div className="mkh-detail-tip">
<Text type="secondary">
该 MKH 商品在聚水潭商品 BOM、业务组合 BOM 和「产品资料系统数据库」中均暂无配件明细。
上游维护好该商品的正式 BOM 后,此处会自动显示配件明细、数量与仓位。
</Text>
</div>
) : null}
<Table
rowKey="index"
size="small"
pagination={false}
dataSource={items}
scroll={{ x: 720 }}
locale={{ emptyText: '暂无配件明细' }}
columns={[
{ title: '序号', dataIndex: 'index', width: 60, align: 'center' },
{
title: '配件明细',
dataIndex: 'skuName',
ellipsis: true,
render: (v, r) => (
<Tooltip title={`${v || ''}${r.skuId ? `(${r.skuId})` : ''}`}>
<span>{text(v)}</span>
{r.skuId ? <span className="mkh-detail-code">{r.skuId}</span> : null}
</Tooltip>
),
},
{ title: '数量', dataIndex: 'qty', width: 90, align: 'right', render: (v) => number(v) },
{
title: '配件仓位',
dataIndex: 'location',
width: 140,
render: (v) => (v ? text(v) : (
<Tooltip title="箱及仓位库存表中没有该配件的有效记录">
<span className="mkh-muted">未维护</span>
</Tooltip>
)),
},
]}
/>
</div>
);
};
return (
<SycmPage className="mkh-page">
<header className="mkh-header">
<div className="mkh-header__heading">
<Title level={3}>MKH-BOM 资料</Title>
<Text type="secondary">
模块化(MKH 开头)成品的编码与配件明细:配件明细、数量、配件仓位
</Text>
<Text type="secondary" className="mkh-note">
数据口径:MKH 商品编码取自业务库 jst_sku 中“简尚-崔桥工厂品牌仓”,并且必须是聚水潭正式 BOM 的父项;
配件明细与数量优先取自聚水潭商品 BOM,业务中台组合商品 BOM 与产品资料系统数据库中维护的配件作为补充;
无指定仓库商品档案的 BOM 组合编码不会进入列表;配件仓位只取 rpt.jst_packInventoryInfo,
优先显示具体仓位,没有具体仓位时显示箱号,表中没有有效记录时显示「未维护」。
</Text>
</div>
</header>
<Card className="mkh-filter-card" bordered={false}>
<Space wrap size={[12, 12]}>
<Input
allowClear
prefix={<SearchOutlined />}
placeholder="MKH商品编码 / 名称"
value={filters.keyword}
onChange={(e) => changeFilter('keyword')(e.target.value)}
style={{ width: 260 }}
/>
<Button onClick={resetFilters}>重置</Button>
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}>刷新数据</Button>
</Space>
</Card>
{error ? <Alert type="error" showIcon message={error} action={<Button size="small" onClick={load}>重试</Button>} /> : null}
{!loading && (data.total || 0) > 0 ? (
<Alert
type={data.bomRowCount > 0 ? 'info' : 'warning'}
showIcon
message={data.bomRowCount > 0
? `已接入 ${data.bomRowCount} 个模块化商品的配件 BOM`
: '聚水潭商品 BOM 及补充来源中尚无可用的模块化商品配件明细'}
description={(
<span>
MKH 商品编码({data.total || 0} 条)取自业务库 jst_sku 中“简尚-崔桥工厂品牌仓”的正式 BOM 父项;
配件明细优先取自聚水潭商品 BOM({data.bomOfficialRowCount || 0} 个),
业务组合 BOM({data.bomBusinessRowCount || 0} 个)与产品资料系统({data.bomProductDbRowCount || 0} 个)作为补充。
BOM 中没有指定仓库商品档案的组合编码不会加入商品列表。
上游补充正式 BOM 后,本页「BOM 状态 / 配件种类 / 配件总数量」与展开明细会自动更新。
</span>
)}
/>
) : null}
<Card
bordered={false}
className="mkh-panel"
title={<span>MKH 模块化成品({data.total || 0} 条)</span>}
>
<Spin spinning={loading} tip="正在加载 MKH-BOM 资料…">
<div ref={tableWrapRef}>
<Table
rowKey="skuId"
size="small"
columns={columns}
dataSource={data.rows || []}
expandable={{
expandedRowKeys: Object.keys(expandedKeys).filter((k) => expandedKeys[k]),
expandedRowRender,
onExpand: (expanded, record) => {
setExpandedKeys((old) => ({ ...old, [record.skuId]: expanded }));
if (expanded) loadDetail(record.skuId);
},
}}
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: 1000, y: bodyHeight }}
/>
</div>
</Spin>
</Card>
{printData ? <PrintPreview data={printData} onClose={() => setPrintData(null)} /> : null}
</SycmPage>
);
};
export default MkhBomData;