178 lines
6.3 KiB
JavaScript
178 lines
6.3 KiB
JavaScript
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||
import { Alert, Button, Modal, Space, Spin, Table, Tooltip, Typography } from 'antd';
|
||
import { InfoCircleOutlined, ReloadOutlined } from '@ant-design/icons';
|
||
import { SycmPage } from '../../components/Common';
|
||
import { getDailyInventory } from '../../api/dailyInventory';
|
||
import {
|
||
DAILY_INVENTORY_COLUMNS,
|
||
COLUMN_SOURCE,
|
||
FORMULA_DESCRIPTION,
|
||
} from '../../constants/dailyInventory.js';
|
||
import './style.css';
|
||
|
||
const { Title, Text } = Typography;
|
||
|
||
const TipTitle = ({ tip, children }) => (
|
||
<Tooltip title={tip}>
|
||
<span className="daily-inventory-th-tip">{children}</span>
|
||
</Tooltip>
|
||
);
|
||
|
||
const int = (value) => {
|
||
if (value === null || value === undefined || value === '') return '—';
|
||
return Number(value).toLocaleString('zh-CN', { maximumFractionDigits: 0 });
|
||
};
|
||
|
||
const decimal = (value, digits = 1) => {
|
||
if (value === null || value === undefined || value === '') return '—';
|
||
return Number(value).toLocaleString('zh-CN', {
|
||
minimumFractionDigits: digits,
|
||
maximumFractionDigits: digits,
|
||
});
|
||
};
|
||
|
||
/** 负数标红、正数标黑:出入库列以负数表示流出,便于一眼分辨方向 */
|
||
const renderSigned = (value) => {
|
||
if (value === null || value === undefined || value === '') return '—';
|
||
const n = Number(value);
|
||
if (!Number.isFinite(n) || n === 0) return <span className="daily-inventory-cell--zero">0</span>;
|
||
return <span className={n < 0 ? 'daily-inventory-cell--out' : 'daily-inventory-cell--in'}>{int(n)}</span>;
|
||
};
|
||
|
||
const DailyInventory = () => {
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState('');
|
||
const [data, setData] = useState(null);
|
||
const [helpOpen, setHelpOpen] = useState(false);
|
||
|
||
const load = useCallback(async () => {
|
||
setLoading(true);
|
||
setError('');
|
||
try {
|
||
const res = await getDailyInventory();
|
||
setData(res?.data ?? null);
|
||
} catch (err) {
|
||
setError(err?.response?.data?.error || err?.message || '每日进销存数据加载失败');
|
||
setData(null);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => { load(); }, [load]);
|
||
|
||
const items = useMemo(() => data?.items ?? [], [data]);
|
||
|
||
const columns = useMemo(() => DAILY_INVENTORY_COLUMNS.map((col) => {
|
||
const isRuntime = col.source === COLUMN_SOURCE.runtime;
|
||
return {
|
||
title: <TipTitle tip={col.tip}>{col.label}</TipTitle>,
|
||
dataIndex: col.key,
|
||
key: col.key,
|
||
align: col.key === 'warehouse' ? 'left' : 'right',
|
||
width: col.key === 'warehouse' ? 240 : 130,
|
||
fixed: col.key === 'warehouse' ? 'left' : undefined,
|
||
className: isRuntime ? 'daily-inventory-col--runtime' : undefined,
|
||
render: (value, record) => {
|
||
if (record?.isTotal) {
|
||
// 合计行:周转天数不累加,显示「—」
|
||
if (col.key === 'turnoverDays') return '—';
|
||
return int(value);
|
||
}
|
||
if (col.key === 'warehouse') return <span className="daily-inventory-warehouse">{value}</span>;
|
||
if (col.key === 'closingQty') return int(value);
|
||
if (col.key === 'turnoverDays') return decimal(value, 1);
|
||
return renderSigned(value);
|
||
},
|
||
};
|
||
}), []);
|
||
|
||
const tableRows = useMemo(() => {
|
||
if (!data?.totals) return items;
|
||
return [...items, { ...data.totals, key: '__total__' }];
|
||
}, [items, data]);
|
||
|
||
const noGradeCount = useMemo(() => items.filter((it) => !it.hasGrade).length, [items]);
|
||
|
||
return (
|
||
<SycmPage className="daily-inventory-page">
|
||
<div className="daily-inventory-header">
|
||
<div className="daily-inventory-header__title">
|
||
<Title level={4} style={{ margin: 0 }}>每日进销存</Title>
|
||
<Button
|
||
type="text"
|
||
icon={<InfoCircleOutlined />}
|
||
onClick={() => setHelpOpen(true)}
|
||
className="daily-inventory-header__help"
|
||
>
|
||
口径说明
|
||
</Button>
|
||
</div>
|
||
<div className="daily-inventory-header__meta">
|
||
<span>基准日期:{data?.baselineDate || '—'}</span>
|
||
<span>库存快照日:{data?.latestBizDate || '—'}</span>
|
||
<span>分级基准时刻:{data?.asOf || '—'}</span>
|
||
{data?.items?.[0]?.closingTakenAt ? (
|
||
<span>
|
||
库存取数时间:{new Date(data.items[0].closingTakenAt).toLocaleString('zh-CN', { hour12: false })}
|
||
</span>
|
||
) : null}
|
||
</div>
|
||
<Space>
|
||
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}>刷新</Button>
|
||
</Space>
|
||
</div>
|
||
|
||
<Alert
|
||
type="info"
|
||
showIcon
|
||
message="数据来源混合,请注意口径"
|
||
description={
|
||
<span>
|
||
期初数量与 12 个流水列取自 <b>{data?.baselineDate || '2026-09-22'}</b> 聚水潭进销存导出值(业务库缺少调拨/盘点/加工/其它出入库单据表,无法还原);
|
||
期末数量与右侧 5 列为运行时实时取数。悬浮表头可看每列口径。
|
||
{noGradeCount > 0 ? ` 其中 ${noGradeCount} 个仓在库存快照中无数据,周转天数与四级库存量显示「—」。` : ''}
|
||
</span>
|
||
}
|
||
style={{ marginBottom: 12 }}
|
||
/>
|
||
|
||
{error ? <Alert type="error" showIcon message={error} style={{ marginBottom: 12 }} /> : null}
|
||
|
||
<Spin spinning={loading}>
|
||
<Table
|
||
className="daily-inventory-table"
|
||
columns={columns}
|
||
dataSource={tableRows}
|
||
rowKey={(record) => record.key || record.warehouse}
|
||
size="small"
|
||
bordered
|
||
pagination={false}
|
||
scroll={{ x: 'max-content', y: 'calc(100vh - 300px)' }}
|
||
rowClassName={(record) => (record?.isTotal ? 'daily-inventory-row--total' : '')}
|
||
/>
|
||
</Spin>
|
||
|
||
<Text type="secondary" className="daily-inventory-footer">
|
||
共 {items.length} 个仓库;蓝色底纹列为运行时取数,其余为基准值。
|
||
</Text>
|
||
|
||
<Modal
|
||
title="计算口径说明"
|
||
open={helpOpen}
|
||
onCancel={() => setHelpOpen(false)}
|
||
footer={null}
|
||
width={720}
|
||
>
|
||
<div className="daily-inventory-help">
|
||
{FORMULA_DESCRIPTION.map((line, i) => (
|
||
<p key={i} className={line.startsWith(' ') ? 'is-indent' : ''}>{line}</p>
|
||
))}
|
||
</div>
|
||
</Modal>
|
||
</SycmPage>
|
||
);
|
||
};
|
||
|
||
export default DailyInventory;
|