采购单修改

This commit is contained in:
liu
2026-08-31 21:45:50 +08:00
parent f8cc40a609
commit 6c809d893b
3 changed files with 56 additions and 286 deletions
+54 -284
View File
@@ -5,7 +5,6 @@ import {
Drawer,
Empty,
Form,
Image,
Input,
InputNumber,
message,
@@ -32,8 +31,6 @@ import type IPurchaseOrder from '@/domain/iPurchaseOrder.ts';
import type {
IBill,
IBillPrepare,
IPurchaseCell,
IPurchaseCellItem,
IPurchaseDetail,
IPurchaseDetailRow,
IPurchaseStoreItem,
@@ -43,7 +40,6 @@ import type {
PurchaseStoreItemUpdateParams,
} from '@/domain/iPurchaseOrder.ts';
import { PURCHASE_STATUS_MAP, BILL_STATUS_MAP } from '@/domain/iPurchaseOrder.ts';
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
import {
addPurchaseStoreItem,
exportPurchase,
@@ -51,14 +47,11 @@ import {
exportPurchaseSuppliers,
generateBill,
getBillPrepare,
getPurchaseCell,
getPurchaseDetail,
getPurchaseStoreSummary,
removePurchaseStoreItem,
type BillGenerateStoreParams,
type PurchaseCellUpdateParams,
type PurchaseRowUpdateParams,
updatePurchaseCellItem,
updatePurchaseRow,
updatePurchaseStoreItem,
} from '@/api/purchase/order.ts';
@@ -105,22 +98,9 @@ const PurchaseOrderPage: React.FC = () => {
const [detailLoading, setDetailLoading] = useState(false);
const [completing, setCompleting] = useState(false);
// 行内编辑:正在编辑的单元格(点击 品名/供应商/包规/单位/成本 进入编辑
const [editingCell, setEditingCell] = useState<{ productId: number; dataIndex: InlineEditField } | null>(null);
// 行内编辑:供应商选项(品名/供应商/包规/单位/成本与各门店订货量直接在表格中编辑)
const [suppliers, setSuppliers] = useState<ISupplier[]>([]);
// 单元格下钻弹窗(门店 × 商品订货明细)
const [cellOpen, setCellOpen] = useState(false);
const [cellLoading, setCellLoading] = useState(false);
const [cellData, setCellData] = useState<IPurchaseCell | null>(null);
const [cellQuery, setCellQuery] = useState<{ productId: number; storeId: number } | null>(null);
// 单元格明细编辑(数量/称重)
const [cellItemOpen, setCellItemOpen] = useState(false);
const [cellItemTarget, setCellItemTarget] = useState<IPurchaseCellItem | null>(null);
const [cellItemSaving, setCellItemSaving] = useState(false);
const [cellItemForm] = Form.useForm<PurchaseCellUpdateParams>();
// 门店购买详情(按商品聚合的门店采购汇总)
const [detailTab, setDetailTab] = useState('items');
const [storeId, setStoreId] = useState<number>(0);
@@ -243,13 +223,6 @@ const PurchaseOrderPage: React.FC = () => {
getSupplierOptions().then((res) => setSuppliers(res.data.data ?? []));
}, []);
// 单元格下钻弹窗打开时加载明细
useEffect(() => {
if (cellOpen && cellQuery) {
loadCell();
}
}, [cellOpen, cellQuery]);
// 详情加载后默认选中第一个门店(当前选中门店仍在采购单内则保留)
useEffect(() => {
if (detail && detail.stores.length > 0) {
@@ -375,75 +348,17 @@ const PurchaseOrderPage: React.FC = () => {
await refreshAfterStoreItemChange();
};
/** 打开单元格下钻:门店 + 商品 → 该采购单下全部订货明细 */
const openCell = (row: IPurchaseDetailRow, store: { id: number; name: string }) => {
setCellQuery({ productId: row.product_id, storeId: store.id });
setCellData(null);
setCellOpen(true);
};
/** 加载单元格明细 */
const loadCell = async () => {
if (!detail || !cellQuery) {
return;
}
setCellLoading(true);
try {
const res = await getPurchaseCell(detail.purchase.id!, cellQuery.productId, cellQuery.storeId);
setCellData(res.data.data ?? null);
} finally {
setCellLoading(false);
}
};
/** 单元格明细修改/同步后:刷新弹窗、采购单详情与列表 */
const refreshAfterCellChange = async () => {
await loadCell();
if (detail) {
await loadDetail(detail.purchase.id!);
}
await tableRef.current?.reload();
};
const openCellItemEdit = (item: IPurchaseCellItem) => {
setCellItemTarget(item);
cellItemForm.setFieldsValue({
quantity: item.quantity,
price: Number(item.price ?? 0),
weight: Number(item.weight ?? 0),
});
setCellItemOpen(true);
};
/** 提交单元格明细修改:级联重算明细金额、订货单与采购单汇总 */
const handleCellItemSave = async (values: PurchaseCellUpdateParams) => {
if (!cellItemTarget?.id) {
return;
}
setCellItemSaving(true);
try {
await updatePurchaseCellItem(cellItemTarget.id, values);
message.success('明细已更新,订货单与采购单汇总已重算');
setCellItemOpen(false);
setCellItemTarget(null);
await refreshAfterCellChange();
} finally {
setCellItemSaving(false);
}
};
/** 商品明细是否可行内编辑(进行中采购单 + 修改权限) */
const canUpdateRow = detail?.purchase.status === 0 && auth('purchase.order.update');
/**
* 行内编辑保存:合并该行当前值整行提交(后端要求全字段);未变更 / 校验失败时不请求。
* Enter 或失焦触发保存,保存后关闭编辑态并刷新详情与列表。
* Enter 或失焦触发保存,保存后刷新详情与列表。
*/
const handleInlineSave = async (row: IPurchaseDetailRow, dataIndex: InlineEditField, rawValue: string | number) => {
if (!detail) {
return;
}
setEditingCell(null);
const params: PurchaseRowUpdateParams = {
product_name: row.product_name,
@@ -492,7 +407,29 @@ const PurchaseOrderPage: React.FC = () => {
await tableRef.current?.reload();
};
/** 可编辑单元格:点击进入编辑态(文本 Input / 供应商 Select / 成本 InputNumber),Esc 取消 */
/**
* 门店订货量保存:复用门店单品修改接口(仅提交订货量,单价/称重不改动),
* 同商品多笔订单明细合并到最早一条并级联重算;未变更 / 校验失败时不请求。
*/
const handleStoreCellSave = async (row: IPurchaseDetailRow, storeId: number, rawValue: string) => {
if (!detail) {
return;
}
const quantity = Number(rawValue);
if (rawValue.trim() === '' || !Number.isInteger(quantity) || quantity < 0) {
message.warning('订货量无效,已取消修改');
return;
}
if (quantity === row.cells[storeId]) {
return;
}
await updatePurchaseStoreItem(detail.purchase.id!, storeId, row.product_id, { quantity });
message.success('订货量已更新,订货单与采购单汇总已重算');
await loadDetail(detail.purchase.id!);
await tableRef.current?.reload();
};
/** 行内编辑单元格:直接渲染编辑组件(文本 Input / 供应商 Select / 成本 InputNumber),Enter 或失焦保存 */
const renderEditableCell = (
row: IPurchaseDetailRow,
dataIndex: InlineEditField,
@@ -501,41 +438,17 @@ const PurchaseOrderPage: React.FC = () => {
if (!canUpdateRow) {
return display;
}
const editing = editingCell?.productId === row.product_id && editingCell.dataIndex === dataIndex;
if (!editing) {
return (
<div
className="-mx-1 cursor-pointer rounded px-1 transition-colors hover:bg-blue-50"
title="点击修改"
onClick={() => setEditingCell({ productId: row.product_id, dataIndex })}
>
{display}
</div>
);
}
const cancelOnEscape = {
onKeyDown: (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
setEditingCell(null);
}
},
};
if (dataIndex === 'supplier_id') {
return (
<Select
size="small"
autoFocus
defaultOpen
className="w-full"
defaultValue={row.supplier_id > 0 ? row.supplier_id : undefined}
value={row.supplier_id > 0 ? row.supplier_id : undefined}
placeholder="选择供应商"
showSearch={{ optionFilterProp: 'label' }}
options={suppliers.map((s) => ({ value: s.id, label: s.name }))}
onChange={(value) => void handleInlineSave(row, dataIndex, value)}
onBlur={() => setEditingCell(null)}
/>
);
}
@@ -543,8 +456,8 @@ const PurchaseOrderPage: React.FC = () => {
if (dataIndex === 'cost_price') {
return (
<InputNumber
key={`${row.product_id}-cost-${row.cost_price}`}
size="small"
autoFocus
min={0}
precision={2}
prefix="¥"
@@ -552,20 +465,19 @@ const PurchaseOrderPage: React.FC = () => {
defaultValue={Number(row.cost_price)}
onPressEnter={(e) => void handleInlineSave(row, dataIndex, (e.target as HTMLInputElement).value)}
onBlur={(e) => void handleInlineSave(row, dataIndex, e.target.value)}
{...cancelOnEscape}
/>
);
}
return (
<Input
key={`${row.product_id}-${dataIndex}-${row[dataIndex]}`}
size="small"
autoFocus
style={{ textAlign: 'center' }}
defaultValue={row[dataIndex]}
maxLength={dataIndex === 'unit' ? 20 : 100}
onPressEnter={(e) => void handleInlineSave(row, dataIndex, (e.target as HTMLInputElement).value)}
onBlur={(e) => void handleInlineSave(row, dataIndex, e.target.value)}
{...cancelOnEscape}
/>
);
};
@@ -653,7 +565,7 @@ const PurchaseOrderPage: React.FC = () => {
title: '市场',
fixed: 'left',
dataIndex: 'market',
width: 90,
width: 110,
align: 'center',
filters: itemMarketOptions.map((m) => ({ text: m, value: m })),
filteredValue: itemColumnFilters.market ?? null,
@@ -663,7 +575,7 @@ const PurchaseOrderPage: React.FC = () => {
{
title: '单价',
key: 'retail_price',
width: 100,
width: 110,
align: 'center',
render: (_, row) => {
// 加权平均售价 ÷ 包规数值(无订货数量时无参考价)
@@ -677,27 +589,27 @@ const PurchaseOrderPage: React.FC = () => {
title: '包规',
dataIndex: 'product_spec',
align: 'center',
width: 90,
width: 110,
render: (v, row) => renderEditableCell(row, 'product_spec', v || '-'),
},
{
title: '单位',
dataIndex: 'unit',
width: 70,
width: 110,
align: 'center',
render: (v, row) => renderEditableCell(row, 'unit', v || '-'),
},
{
title: '成本',
dataIndex: 'cost_price',
width: 100,
width: 110,
align: 'center',
render: (v, row) => renderEditableCell(row, 'cost_price', `¥${Number(v).toFixed(2)}`),
},
{
title: '合计数量',
key: 'total_quantity',
width: 90,
width: 110,
align: 'center',
render: (_, row) => <Text strong>{row.quantity}</Text>,
},
@@ -706,14 +618,27 @@ const PurchaseOrderPage: React.FC = () => {
const storeColumns = (detail?.stores ?? []).map((store) => ({
title: <span className={'text-[red]'}>{store.name}</span>,
key: `store-${store.id}`,
width: 100,
align: 'center' as const,
render: (_: unknown, row: IPurchaseDetailRow) => {
const quantity = row.cells[store.id];
return quantity !== undefined ? (
<Typography.Link onClick={() => openCell(row, store)}>{quantity}</Typography.Link>
) : (
<Text type="secondary">-</Text>
if (quantity === undefined) {
return <Text type="secondary">-</Text>;
}
if (!canUpdateRow) {
return quantity;
}
// 订货量直接在单元格编辑:Enter 或失焦保存(单价/称重不改动)
return (
<InputNumber
key={`${row.product_id}-${store.id}-${quantity}`}
size="small"
min={0}
precision={0}
className="w-full"
defaultValue={quantity}
onPressEnter={(e) => void handleStoreCellSave(row, store.id, (e.target as HTMLInputElement).value)}
onBlur={(e) => void handleStoreCellSave(row, store.id, e.target.value)}
/>
);
},
}));
@@ -1352,7 +1277,7 @@ const PurchaseOrderPage: React.FC = () => {
</AuthButton>
{canUpdateRow && (
<Text type="secondary" className="text-xs">
////
</Text>
)}
</Space>
@@ -1373,161 +1298,6 @@ const PurchaseOrderPage: React.FC = () => {
)}
</Drawer>
{/* 单元格下钻 */}
<Modal
title={cellData ? `${cellData.store?.name ?? ''} · ${cellData.product?.name ?? ''}` : '门店商品明细'}
open={cellOpen}
onCancel={() => setCellOpen(false)}
footer={null}
width={1000}
destroyOnHidden
styles={{ body: {paddingTop: 16} }}
>
<Spin spinning={cellLoading}>
{cellData && (
<>
{cellData.items.length === 0 ? (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="该门店无此商品明细"
className="py-8!"
/>
) : (
<div className="overflow-hidden rounded border border-gray-200">
<div className="flex bg-gray-50 px-4 py-2 text-sm text-gray-500">
<div className="flex-1"></div>
<div className="w-30 shrink-0 text-center"></div>
<div className="w-26 shrink-0 text-center"></div>
<div className="w-33 shrink-0 text-center"></div>
<div className="w-33 shrink-0 text-center"></div>
<div className="w-40 shrink-0 text-center"></div>
</div>
{cellData.items.map((item) => (
<div key={item.id} className="flex items-center border-t border-gray-100 px-4 py-3">
<div className="flex min-w-0 flex-1 items-center">
<Image.PreviewGroup>
{item.image ? (
<Image
src={item.image}
width={48}
height={48}
style={{ objectFit: 'cover', borderRadius: 4 }}
/>
) : (
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded bg-gray-100 text-xs text-gray-400">
</div>
)}
</Image.PreviewGroup>
<div className="ml-3 min-w-0">
<div className="text-sm font-medium">
{item.product_name}
<Tag
className="ml-2!"
color={STORE_ORDER_STATUS_MAP[item.order_status]?.color}
>
{STORE_ORDER_STATUS_MAP[item.order_status]?.text}
</Tag>
</div>
<div className="mt-0.5 text-xs text-gray-500">
{item.order_no}
</div>
{item.remark ? (
<div className="mt-0.5 truncate text-xs text-gray-500">
{item.remark}
</div>
) : null}
</div>
</div>
<div className="w-30 shrink-0 text-center">¥{item.price}</div>
<div className="w-26 shrink-0 text-center">{item.quantity}</div>
<div className="w-33 shrink-0 text-center">
<Text strong>¥{item.amount}</Text>
</div>
<div className="w-33 shrink-0 text-center">
{item.weight ?? '-'}
</div>
<div className="w-40 shrink-0 text-center">
{detail?.purchase.status === 0 ? (
<Space size={0}>
<AuthButton auth="purchase.order.update">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openCellItemEdit(item)}
>
</Button>
</AuthButton>
</Space>
) : (
<Text type="secondary">-</Text>
)}
</div>
</div>
))}
</div>
)}
{cellData.items.length > 0 && (
<div className="mt-3! flex justify-end gap-6 text-sm">
<Text type="secondary">
<Text strong>{cellData.items.reduce((sum, item) => sum + item.quantity, 0)}</Text>
</Text>
<Text type="secondary">
<Text strong type="danger">
¥
{cellData.items
.reduce((sum, item) => sum + Number(item.amount), 0)
.toFixed(2)}
</Text>
</Text>
</div>
)}
</>
)}
</Spin>
</Modal>
{/* 单元格明细编辑 */}
<Modal
title={cellItemTarget ? `编辑「${cellItemTarget.product_name}` : '编辑明细'}
open={cellItemOpen}
onCancel={() => {
setCellItemOpen(false);
setCellItemTarget(null);
}}
onOk={() => cellItemForm.submit()}
confirmLoading={cellItemSaving}
okText="保存"
destroyOnHidden
>
<div className="py-2 text-gray-500">
{cellItemTarget?.order_no}
</div>
<Form form={cellItemForm} layout="vertical" onFinish={handleCellItemSave}>
<Form.Item
label="单价(元)"
name="price"
rules={[{ required: true, message: '请输入单价' }]}
>
<InputNumber className="w-full" min={0} precision={2} placeholder="请输入单价" />
</Form.Item>
<Form.Item
label="订货量"
name="quantity"
rules={[{ required: true, message: '请输入订货量' }]}
>
<InputNumber className="w-full" min={0} precision={0} placeholder="请输入订货量" />
</Form.Item>
<Form.Item label="称重" name="weight" rules={[{ required: true, message: '请输入称重' }]}>
<InputNumber className="w-full" min={0} precision={3} placeholder="请输入称重" />
</Form.Item>
</Form>
</Modal>
{/* 生成账单:按门店填写配送费/周转筐/托盘数量与售后金额(商品金额只读,由订单汇总) */}
<Modal
title={billPrepare ? `生成账单 · ${billPrepare.purchase.purchase_no}` : '生成账单'}