433 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 商品的配件明细尚未录入「产品资料系统数据库」</td>
</tr>
) : null}
</tbody>
</table>
<div className="mkh-print-foot">
<span>
MKH 商品编码与组合配件取自业务库;「产品资料系统数据库」中维护的配件 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 和「产品资料系统数据库」中均暂无配件明细。
上游维护好该商品的组合配件后,此处会自动显示配件明细、数量与仓位。
</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 中“简尚-崔桥工厂品牌仓”的成品(当前 4408 条);
配件明细与数量优先取自业务中台组合商品 BOM,产品资料系统数据库(PostgreSQL / furniture3d_prod)中维护的配件明细作为补充;
无指定仓库商品档案的 BOM 组合编码不会进入列表;配件仓位未维护时显示「未维护」。
</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({data.bomBusinessRowCount || 0} 个),
「产品资料系统数据库」维护的 BOM({data.bomProductDbRowCount || 0} 个)作为补充。
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;