采购单优化

This commit is contained in:
liu
2026-08-13 00:11:44 +08:00
parent 0211e5e98d
commit 6c794d0732
6 changed files with 362 additions and 19 deletions
+170 -18
View File
@@ -15,6 +15,7 @@ import {
Space,
Spin,
Table,
Tabs,
Tag,
Typography,
} from 'antd';
@@ -32,12 +33,16 @@ import type {
IPurchaseCellItem,
IPurchaseDetail,
IPurchaseDetailRow,
IPurchaseStoreItem,
IPurchaseStoreSummary,
} from '@/domain/iPurchaseOrder.ts';
import { PURCHASE_STATUS_MAP } from '@/domain/iPurchaseOrder.ts';
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
import {
getPurchaseCell,
getPurchaseDetail, type PurchaseCellUpdateParams, type PurchaseRowUpdateParams,
getPurchaseDetail,
getPurchaseStoreSummary,
type PurchaseCellUpdateParams, type PurchaseRowUpdateParams,
updatePurchaseCellItem,
updatePurchaseRow,
} from '@/api/purchase/order.ts';
@@ -48,10 +53,10 @@ import AuthButton from '@/components/AuthButton';
const { Title, Text } = Typography;
/** 参考价 = 成本 / 包规数值(包规解析不出正数时按 1 处理,与后端同口径;仅展示参考,不参与金额计算) */
const calcUnitCost = (cost: number, spec: string): number => {
/** 每单位参考价 = 整单价(成本/售价) ÷ 包规数值(包规解析不出正数时按 1 处理,与后端同口径;仅展示参考,不参与金额计算) */
const calcUnitRefPrice = (total: number, spec: string): number => {
const pack = parseFloat(spec);
return Number.isFinite(pack) && pack > 0 ? cost / pack : cost;
return Number.isFinite(pack) && pack > 0 ? total / pack : total;
};
/**
@@ -85,6 +90,12 @@ const PurchaseOrderPage: React.FC = () => {
const [cellItemSaving, setCellItemSaving] = useState(false);
const [cellItemForm] = Form.useForm<PurchaseCellUpdateParams>();
// 门店购买详情(按商品聚合的门店采购汇总)
const [detailTab, setDetailTab] = useState('items');
const [storeId, setStoreId] = useState<number>(0);
const [storeSummary, setStoreSummary] = useState<IPurchaseStoreSummary | null>(null);
const [storeLoading, setStoreLoading] = useState(false);
useEffect(() => {
getSupplierOptions().then((res) => setSuppliers(res.data.data ?? []));
}, []);
@@ -96,6 +107,20 @@ const PurchaseOrderPage: React.FC = () => {
}
}, [cellOpen, cellQuery]);
// 详情加载后默认选中第一个门店(当前选中门店仍在采购单内则保留)
useEffect(() => {
if (detail && detail.stores.length > 0) {
setStoreId((prev) => (detail.stores.some((s) => s.id === prev) ? prev : detail.stores[0].id));
}
}, [detail]);
// 切到「门店购买详情」页签或切换门店时加载汇总
useEffect(() => {
if (detailOpen && detailTab === 'stores' && detail && storeId > 0) {
void loadStoreSummary();
}
}, [detailOpen, detailTab, storeId, detail?.purchase.id]);
const loadDetail = async (id: number) => {
setDetailLoading(true);
try {
@@ -107,10 +132,26 @@ const PurchaseOrderPage: React.FC = () => {
};
const openDetail = async (id: number) => {
setDetailTab('items');
setStoreSummary(null);
setDetailOpen(true);
await loadDetail(id);
};
/** 加载门店购买详情(门店采购汇总) */
const loadStoreSummary = async () => {
if (!detail || storeId <= 0) {
return;
}
setStoreLoading(true);
try {
const res = await getPurchaseStoreSummary(detail.purchase.id!, storeId);
setStoreSummary(res.data.data ?? null);
} finally {
setStoreLoading(false);
}
};
/** 打开单元格下钻:门店 + 商品 → 该采购单下全部订货明细 */
const openCell = (row: IPurchaseDetailRow, store: { id: number; name: string }) => {
setCellQuery({ productId: row.product_id, storeId: store.id });
@@ -226,11 +267,11 @@ const PurchaseOrderPage: React.FC = () => {
render: (_, row) => row.supplier?.name ?? '-',
},
{
title: '参考单价',
title: '参考成本单价',
key: 'unit_cost',
width: 100,
align: 'center',
render: (_, row) => `¥${calcUnitCost(Number(row.cost_price), row.product_spec ?? '').toFixed(2)}`,
render: (_, row) => `¥${calcUnitRefPrice(Number(row.cost_price), row.product_spec ?? '').toFixed(2)}`,
},
{ title: '包规', dataIndex: 'product_spec', align: 'center', width: 90, render: (v) => v || '-' },
{ title: '单位', dataIndex: 'unit', width: 70, align: 'center', render: (v) => v || '-' },
@@ -317,6 +358,66 @@ const PurchaseOrderPage: React.FC = () => {
);
};
/** 门店购买详情列:商品/购买参考零售价(单价÷包规)/包规/单位/单价/数量/预计金额 */
const storeColumns: TableProps<IPurchaseStoreItem>['columns'] = [
{ title: '商品', dataIndex: 'product_name', width: 160, align: 'center' },
{
title: '参考零售价',
key: 'retail_price',
width: 130,
align: 'center',
render: (_, row) => `¥${calcUnitRefPrice(Number(row.price), row.product_spec).toFixed(2)}`,
},
{ title: '包规', dataIndex: 'product_spec', width: 90, align: 'center', render: (v) => v || '-' },
{ title: '单位', dataIndex: 'unit', width: 90, align: 'center', render: (v) => v || '-' },
{
title: '单价',
dataIndex: 'price',
width: 100,
align: 'center',
render: (v) => `¥${Number(v).toFixed(2)}`,
},
{
title: '数量',
dataIndex: 'quantity',
width: 90,
align: 'center',
render: (v) => <Text strong>{v}</Text>,
},
{ title: '重量', dataIndex: 'weight', width: 90, align: 'center', render: (v) => `${v || '-'}` },
{
title: '预计金额',
dataIndex: 'amount',
width: 110,
align: 'center',
render: (v) => <Text strong>¥{Number(v).toFixed(2)}</Text>,
},
];
/** 门店购买详情合计行:总数量 + 总预计金额 */
const renderStoreTotal = () => {
const items = storeSummary?.items ?? [];
const totalQuantity = items.reduce((sum, row) => sum + Number(row.quantity), 0);
const totalAmount = items.reduce((sum, row) => sum + Number(row.amount), 0);
const totalWeight = items.reduce((sum, row) => sum + Number(row.weight), 0);
return (
<Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={5} align="center">
<Text strong></Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={5} align="center">
<Text strong>{totalQuantity}</Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={6} align="center">
<Text strong>{totalWeight.toFixed(3)}</Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={7} align="center">
<Text strong type="danger">¥{totalAmount.toFixed(2)}</Text>
</Table.Summary.Cell>
</Table.Summary.Row>
);
};
const columns: XinTableColumn<IPurchaseOrder>[] = [
{
title: '采购单号',
@@ -444,18 +545,69 @@ const PurchaseOrderPage: React.FC = () => {
<Descriptions.Item label="总重量">{detail.purchase.total_weight}</Descriptions.Item>
</Descriptions>
<Title level={5} className="mt-5! mb-3!">
</Title>
<Table<IPurchaseDetailRow>
rowKey="product_id"
size="small"
bordered
columns={buildItemColumns()}
dataSource={detail.items}
pagination={false}
scroll={{ x: 'max-content' }}
summary={renderSummary}
<Tabs
activeKey={detailTab}
onChange={setDetailTab}
className="mt-3!"
items={[
{
key: 'items',
label: '商品明细',
children: (
<Table<IPurchaseDetailRow>
rowKey="product_id"
size="small"
bordered
columns={buildItemColumns()}
dataSource={detail.items}
pagination={false}
scroll={{ x: 'max-content' }}
summary={renderSummary}
/>
),
},
{
key: 'stores',
label: '门店购买详情',
children: (
<>
<div className="mb-3 flex items-center gap-2">
<Text></Text>
<Select
value={storeId || undefined}
onChange={(value) => setStoreId(value)}
placeholder="选择门店"
className="w-60!"
showSearch
optionFilterProp="label"
options={detail.stores.map((s) => ({ value: s.id, label: s.name }))}
/>
</div>
<Spin spinning={storeLoading}>
{storeSummary && storeSummary.items.length > 0 ? (
<Table<IPurchaseStoreItem>
rowKey="product_id"
size="small"
bordered
columns={storeColumns}
dataSource={storeSummary.items}
pagination={false}
summary={renderStoreTotal}
/>
) : (
!storeLoading && (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="该门店在此采购单中无采购商品"
className="py-8!"
/>
)
)}
</Spin>
</>
),
},
]}
/>
</>
)}