采购单优化

This commit is contained in:
liu
2026-08-12 23:20:05 +08:00
parent c2c56408a0
commit 0211e5e98d
22 changed files with 768 additions and 1425 deletions
+4 -30
View File
@@ -1,8 +1,7 @@
import createAxios from '@/utils/request';
import type IStoreOrder from '@/domain/iStoreOrder.ts';
import type { IOrderSummaryRow, IStoreOrderItem, IStoreOrderItemUpdate } from '@/domain/iStoreOrder.ts';
/** 订单详情(头 + 明细) */
/** 订单详情 */
export async function getStoreOrder(id: number) {
return createAxios<IStoreOrder>({
url: `/order/store/${id}`,
@@ -10,7 +9,7 @@ export async function getStoreOrder(id: number) {
});
}
/** 订单状态流转(1已接单 3配送中 4已完成 9取消;已接单→采购中由生成采购单完成) */
/** 订单状态流转 */
export async function updateOrderStatus(id: number, status: number) {
return createAxios({
url: `/order/store/${id}/status`,
@@ -19,7 +18,7 @@ export async function updateOrderStatus(id: number, status: number) {
});
}
/** 批量订单状态流转(全量校验,任一订单不允许流转则整批中止) */
/** 批量订单状态流转 */
export async function batchUpdateOrderStatus(ids: number[], status: number) {
return createAxios<{ success: number }>({
url: '/order/store/batchStatus',
@@ -37,32 +36,7 @@ export async function updateOrderContainer(id: number, data: { box_num: number;
});
}
/** 已接单预览(按商品聚合,生成采购单前确认) */
export async function getOrderSummary() {
return createAxios<IOrderSummaryRow[]>({
url: '/order/store/summary',
method: 'get',
});
}
/** 修改订单明细(供应商/品名/规格/单位/单价/成本价/订货量/重量,后端重算订单总价) */
export async function updateOrderItem(itemId: number, data: IStoreOrderItemUpdate) {
return createAxios<IStoreOrderItem>({
url: `/order/store/item/${itemId}`,
method: 'put',
data,
});
}
/** 一键同步明细商品快照为最新商品档案信息(供应商/品名/规格/单位/单价/成本价) */
export async function syncOrderItem(itemId: number) {
return createAxios<IStoreOrderItem>({
url: `/order/store/item/${itemId}/sync`,
method: 'put',
});
}
/** 删除订单(软删除,仅已取消订单允许) */
/** 删除订单 */
export async function deleteStoreOrder(id: number) {
return createAxios({
url: `/order/store/${id}`,
+40 -32
View File
@@ -1,12 +1,27 @@
import createAxios from '@/utils/request';
import type {
ExportFormat,
IPurchaseCell,
IPurchaseDetail,
PurchaseExportType,
} from '@/domain/iPurchaseOrder.ts';
import { downloadBlob } from '@/api/common/download.ts';
/** C1 合并「已接单」门店订单生成采购单(order_ids 为空 = 全部已接单;生成后源订单转采购中) */
/** 订单商品参数修改 */
export interface PurchaseRowUpdateParams {
product_name: string;
supplier_id: number;
product_spec: string;
unit: string;
cost_price: number;
}
/** 修改门店订单明细参数 */
export interface PurchaseCellUpdateParams {
quantity: number;
price: number;
weight?: number;
}
/** 生成采购单 */
export async function generatePurchase(purchase_date: string, order_ids?: number[]) {
return createAxios<{ id: number; purchase_no: string }>({
url: '/purchase/order/generate',
@@ -15,7 +30,7 @@ export async function generatePurchase(purchase_date: string, order_ids?: number
});
}
/** 采购单详情(商品行 × 门店列矩阵) */
/** 采购单详情 */
export async function getPurchaseDetail(id: number) {
return createAxios<IPurchaseDetail>({
url: `/purchase/order/${id}`,
@@ -23,36 +38,29 @@ export async function getPurchaseDetail(id: number) {
});
}
/** C2/C3 导出采购单(blob 下载) */
export async function exportPurchase(id: number, type: PurchaseExportType, format: ExportFormat) {
return downloadBlob(
`/purchase/order/${id}/export`,
{ type, format },
`采购单_${id}.${format}`
);
}
/** C4 商品行修改参数(品名/供应商/包规/单位/成本/称重,一键同步该商品全部订货明细;sync_product 追加同步商品档案) */
export interface PurchaseRowUpdateParams {
product_name?: string;
supplier_id?: number;
product_spec?: string;
unit?: string;
cost_price?: number;
weight?: number;
/** true = 同时同步至商品档案(product 表) */
sync_product?: boolean;
}
/** C4 商品行修改 */
export async function updatePurchaseRow(
purchaseId: number,
productId: number,
data: PurchaseRowUpdateParams,
) {
/** 商品行修改 */
export async function updatePurchaseRow(purchaseId: number, productId: number, data: PurchaseRowUpdateParams) {
return createAxios<{ count: number }>({
url: `/purchase/order/${purchaseId}/row/${productId}`,
method: 'put',
data,
});
}
/** 单元格下钻:采购单中某门店某商品的全部订货明细 */
export async function getPurchaseCell(purchaseId: number, productId: number, storeId: number) {
return createAxios<IPurchaseCell>({
url: `/purchase/order/${purchaseId}/cell`,
method: 'get',
params: { product_id: productId, store_id: storeId },
});
}
/** 门店订单明细修改(订货量、重量、单价),自动重算价格 */
export async function updatePurchaseCellItem(itemId: number, data: PurchaseCellUpdateParams) {
return createAxios({
url: `/purchase/order/cell/${itemId}`,
method: 'put',
data,
});
}
+34
View File
@@ -50,6 +50,40 @@ export interface IPurchaseDetail {
items: IPurchaseDetailRow[];
}
/** 单元格下钻明细行(溯源订货单明细,附订单号/状态/可编辑标记) */
export interface IPurchaseCellItem {
id: number;
order_id: number;
order_no: string;
order_date: string;
/** 订货单状态:0待接单 1已接单 2采购中 3配送中 4已完成 9已取消 */
order_status: number;
product_name: string;
product_spec: string;
unit: string;
supplier_id: number;
supplier?: { id: number; name: string } | null;
/** 等级单价 */
price: string;
cost_price: string;
quantity: number;
weight: string;
/** 金额 = 数量×单价(整数金额返回 number) */
amount: number | string;
remark: string;
/** 首图 */
image?: string;
/** 是否可编辑/同步(采购单进行中且订货单未锁定) */
editable: boolean;
}
/** 单元格下钻数据(门店 + 商品 + 全部订货明细) */
export interface IPurchaseCell {
store: { id: number; name: string } | null;
product: { id: number; name: string } | null;
items: IPurchaseCellItem[];
}
export const PURCHASE_STATUS_MAP: Record<number, { text: string; color: string }> = {
0: { text: '进行中', color: 'processing' },
3: { text: '已完成', color: 'success' },
+10 -179
View File
@@ -6,13 +6,11 @@ import {
Drawer,
Empty,
Form,
Input,
InputNumber,
message,
Modal,
Popconfirm,
Radio,
Select,
Space,
Tag,
Typography,
@@ -26,33 +24,25 @@ import type {
XinTableProps,
} from '@/components/XinTable/typings.ts';
import type IStoreOrder from '@/domain/iStoreOrder.ts';
import type { IStoreOrderItem, IStoreOrderItemUpdate } from '@/domain/iStoreOrder.ts';
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
import {
getStoreOrder,
updateOrderContainer,
updateOrderStatus,
batchUpdateOrderStatus,
updateOrderItem,
syncOrderItem,
deleteStoreOrder,
} from '@/api/order/store.ts';
import { generatePurchase } from '@/api/purchase/order.ts';
import { getStoreOptions } from '@/api/customer/store.ts';
import { getSupplierOptions } from '@/api/customer/supplier.ts';
import type IStore from '@/domain/iStore.ts';
import type ISupplier from '@/domain/iSupplier.ts';
import AuthButton from '@/components/AuthButton';
import { DeleteOutlined, EditOutlined, SettingOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons';
import TextArea from "antd/es/input/TextArea";
import { DeleteOutlined, SettingOutlined, UnorderedListOutlined } from '@ant-design/icons';
const { Title, Text } = Typography;
/** 允许修改周转框/托盘数量的订单状态:已接单、采购中、配送中 */
const CONTAINER_EDITABLE_STATUS = [1, 2, 3];
/** 允许修改/同步商品明细的订单状态:待接单、已接单、采购中、配送中(已完成/已取消锁定) */
const ITEM_EDITABLE_STATUS = [0, 1, 2, 3];
/**
* 状态流转合法路径:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成
@@ -99,18 +89,9 @@ const StoreOrderPage: React.FC = () => {
const [generateLoading, setGenerateLoading] = useState(false);
const [generateForm] = Form.useForm<{ purchase_date: dayjs.Dayjs }>();
/** 供应商选项(明细编辑用) */
const [suppliers, setSuppliers] = useState<ISupplier[]>([]);
/** 商品明细编辑(弹窗表单,保存后后端重算订单总价) */
const [itemEditOpen, setItemEditOpen] = useState(false);
const [itemEditTarget, setItemEditTarget] = useState<IStoreOrderItem | null>(null);
const [itemSaving, setItemSaving] = useState(false);
const [itemForm] = Form.useForm<IStoreOrderItemUpdate>();
useEffect(() => {
getStoreOptions().then((res) => setStores(res.data.data ?? []));
getSupplierOptions().then((res) => setSuppliers(res.data.data ?? []));
}, []);
const openDetail = async (id: number) => {
@@ -184,47 +165,6 @@ const StoreOrderPage: React.FC = () => {
}
};
/** 打开明细编辑弹窗(商品快照 + 订货量/重量) */
const openItemEdit = (item: IStoreOrderItem) => {
setItemEditTarget(item);
itemForm.setFieldsValue({
supplier_id: item.supplier_id ?? 0,
product_name: item.product_name ?? '',
product_spec: item.product_spec ?? '',
unit: item.unit ?? '',
price: Number(item.price ?? 0),
cost_price: Number(item.cost_price ?? 0),
quantity: Number(item.quantity ?? 0),
weight: Number(item.weight ?? 0),
remark: item.remark ?? '',
});
setItemEditOpen(true);
};
/** 保存明细修改:后端重算单品金额与订单总价 */
const handleItemSave = async (values: IStoreOrderItemUpdate) => {
if (!itemEditTarget?.id || !detail?.id) return;
setItemSaving(true);
try {
await updateOrderItem(itemEditTarget.id, values);
message.success('明细已更新,订单总价已重算');
setItemEditOpen(false);
await openDetail(detail.id);
await tableRef.current?.reload();
} finally {
setItemSaving(false);
}
};
/** 一键同步:按商品ID拉取最新商品档案(供应商/品名/规格/单位/单价/成本价) */
const handleItemSync = async (item: IStoreOrderItem) => {
if (!item.id || !detail?.id) return;
await syncOrderItem(item.id);
message.success('已同步最新商品信息,订单总价已重算');
await openDetail(detail.id);
await tableRef.current?.reload();
};
/** 删除订单(软删除,仅已取消订单可删;删除后后台/小程序端均不可见) */
const handleDelete = async (id: number) => {
await deleteStoreOrder(id);
@@ -240,9 +180,6 @@ const StoreOrderPage: React.FC = () => {
+ watchTrayNum * Number(containerOrder?.tray_price ?? 0);
const previewTotal = Number(containerOrder?.product_amount ?? 0) + previewAdded;
/** 明细行编辑/同步按钮(仅未完成、未取消订单可见) */
const itemEditable = ITEM_EDITABLE_STATUS.includes(detail?.status ?? -1);
const columns: XinTableColumn<IStoreOrder>[] = [
{
title: '订单号',
@@ -622,8 +559,8 @@ const StoreOrderPage: React.FC = () => {
<div className="w-30 shrink-0 text-center"></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>
{itemEditable ? <div className="w-40 shrink-0 text-center"></div> : null}
<div className="w-33 shrink-0 text-center"></div>
<div className="w-33 shrink-0 text-center"></div>
</div>
{(detail.items ?? []).map((item) => (
<div key={item.id} className="flex items-center border-t border-gray-100 px-4 py-3">
@@ -645,9 +582,8 @@ const StoreOrderPage: React.FC = () => {
<div className="ml-3 min-w-0">
<div className="text-sm font-medium">{item.product_name}</div>
<div className="mt-0.5 text-xs text-gray-500">
{item.product_spec || '-'} / {item.unit || '-'} · ¥
{item.cost_price ?? '0.00'}
{Number(item.weight ?? 0) > 0 ? ` · 重量:${item.weight}` : ''}
{item.product_spec || '-'} {item.unit || '-'}
<div>¥{item.cost_price ?? '0.00'}</div>
</div>
{item.remark ? (
<div className="mt-0.5 truncate text-xs text-gray-500">{item.remark}</div>
@@ -660,42 +596,18 @@ const StoreOrderPage: React.FC = () => {
<div className="w-33 shrink-0 text-center">
<Text strong>¥{item.amount}</Text>
</div>
{itemEditable ? (
<div className="w-40 shrink-0 text-center">
<Space size={0}>
<AuthButton auth="order.store.update">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openItemEdit(item)}
>
</Button>
</AuthButton>
<AuthButton auth="order.store.update">
<Popconfirm
title="确认同步最新商品信息?"
description="将按商品ID同步供应商、品名、规格、单位、成本价,并按门店等级价重算单价"
onConfirm={() => handleItemSync(item)}
>
<Button type="link" size="small" icon={<SyncOutlined />}>
</Button>
</Popconfirm>
</AuthButton>
</Space>
</div>
) : null}
<div className="w-33 shrink-0 text-center">
{item.weight ?? '-'}
</div>
</div>
))}
{(detail.items ?? []).length === 0 ? (
{(detail.items ?? []).length === 0 && (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="暂无商品明细"
className="py-8!"
/>
) : null}
)}
</div>
{/* 附加信息 */}
@@ -828,87 +740,6 @@ const StoreOrderPage: React.FC = () => {
</div>
</Space>
</Modal>
{/* 编辑商品明细(保存后后端重算单品金额与订单总价) */}
<Modal
title="编辑商品明细"
open={itemEditOpen}
onCancel={() => setItemEditOpen(false)}
onOk={() => itemForm.submit()}
confirmLoading={itemSaving}
okText="保存"
width={640}
destroyOnHidden
>
<div className="py-2 text-gray-500">
{detail?.order_no}
</div>
<Form form={itemForm} layout="vertical" onFinish={handleItemSave}>
<div className="grid grid-cols-2 gap-x-4">
<Form.Item
label="供应商"
name="supplier_id"
rules={[{ required: true, message: '请选择供应商' }]}
>
<Select
showSearch={{
optionFilterProp: 'label'
}}
placeholder="请选择供应商"
options={suppliers.map((s) => ({ label: s.name, value: s.id }))}
/>
</Form.Item>
<Form.Item
label="品名"
name="product_name"
rules={[{ required: true, message: '请输入品名' }]}
>
<Input placeholder="请输入品名" maxLength={100} />
</Form.Item>
<Form.Item label="规格/包规" name="product_spec">
<Input placeholder="请输入规格/包规" maxLength={100} />
</Form.Item>
<Form.Item
label="计价单位"
name="unit"
rules={[{ required: true, message: '请输入计价单位' }]}
>
<Input placeholder="斤/件/箱等" maxLength={20} />
</Form.Item>
<Form.Item
label="单价(元)"
name="price"
rules={[{ required: true, message: '请输入单价' }]}
>
<InputNumber className="w-full" min={0} precision={2} placeholder="请输入单价" />
</Form.Item>
<Form.Item
label="成本价(元)"
name="cost_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={1} 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.Item label="备注" name="remark">
<TextArea className="w-full" placeholder="请输入备注" />
</Form.Item>
</div>
</Form>
</Modal>
</>
);
};
+271 -48
View File
@@ -3,13 +3,17 @@ import {
Button,
Descriptions,
Drawer,
Empty,
Form,
Image,
Input,
InputNumber,
message,
Modal,
Popconfirm,
Select,
Space,
Spin,
Table,
Tag,
Typography,
@@ -24,12 +28,17 @@ import type {
} from '@/components/XinTable/typings.ts';
import type IPurchaseOrder from '@/domain/iPurchaseOrder.ts';
import type {
IPurchaseCell,
IPurchaseCellItem,
IPurchaseDetail,
IPurchaseDetailRow,
} from '@/domain/iPurchaseOrder.ts';
import { PURCHASE_STATUS_MAP } from '@/domain/iPurchaseOrder.ts';
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
import {
getPurchaseDetail,
getPurchaseCell,
getPurchaseDetail, type PurchaseCellUpdateParams, type PurchaseRowUpdateParams,
updatePurchaseCellItem,
updatePurchaseRow,
} from '@/api/purchase/order.ts';
import { Update } from '@/api/common/table.ts';
@@ -39,16 +48,7 @@ import AuthButton from '@/components/AuthButton';
const { Title, Text } = Typography;
/** 行修改表单:品名/供应商/包规/单位/成本 */
interface RowEditForm {
product_name: string;
supplier_id?: number;
product_spec: string;
unit: string;
cost_price: number;
}
/** 单价 = 成本 / 包规数值(包规解析不出正数时按 1 处理,与后端同口径) */
/** 参考单价 = 成本 / 包规数值(包规解析不出正数时按 1 处理,与后端同口径;仅展示参考,不参与金额计算) */
const calcUnitCost = (cost: number, spec: string): number => {
const pack = parseFloat(spec);
return Number.isFinite(pack) && pack > 0 ? cost / pack : cost;
@@ -70,14 +70,32 @@ const PurchaseOrderPage: React.FC = () => {
// 行修改弹窗
const [editingRow, setEditingRow] = useState<IPurchaseDetailRow | null>(null);
const [rowSaving, setRowSaving] = useState(false);
const [syncTarget, setSyncTarget] = useState<'purchase' | 'product'>('purchase');
const [editForm] = Form.useForm<RowEditForm>();
const [editForm] = Form.useForm<PurchaseRowUpdateParams>();
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>();
useEffect(() => {
getSupplierOptions().then((res) => setSuppliers(res.data.data ?? []));
}, []);
// 单元格下钻弹窗打开时加载明细
useEffect(() => {
if (cellOpen && cellQuery) {
loadCell();
}
}, [cellOpen, cellQuery]);
const loadDetail = async (id: number) => {
setDetailLoading(true);
try {
@@ -93,6 +111,63 @@ const PurchaseOrderPage: React.FC = () => {
await loadDetail(id);
};
/** 打开单元格下钻:门店 + 商品 → 该采购单下全部订货明细 */
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 openEdit = (row: IPurchaseDetailRow) => {
setEditingRow(row);
editForm.setFieldsValue({
@@ -105,7 +180,7 @@ const PurchaseOrderPage: React.FC = () => {
};
/** 提交行修改:同步该商品全部订货明细;syncTarget=product 时追加同步商品档案 */
const handleEditSave = async (values: RowEditForm) => {
const handleEditSave = async (values: PurchaseRowUpdateParams) => {
if (!detail || !editingRow) {
return;
}
@@ -114,13 +189,8 @@ const PurchaseOrderPage: React.FC = () => {
const res = await updatePurchaseRow(detail.purchase.id!, editingRow.product_id, {
...values,
supplier_id: values.supplier_id ?? 0,
sync_product: syncTarget === 'product',
});
message.success(
syncTarget === 'product'
? `已同步 ${res.data.data?.count ?? 0} 条订货明细,并更新商品档案`
: `已同步 ${res.data.data?.count ?? 0} 条订货明细`,
);
message.success(`已更新 ${res.data.data?.count ?? 0} 条订货明细`);
setEditingRow(null);
await loadDetail(detail.purchase.id!);
await tableRef.current?.reload();
@@ -156,9 +226,9 @@ const PurchaseOrderPage: React.FC = () => {
render: (_, row) => row.supplier?.name ?? '-',
},
{
title: '单价',
title: '参考单价',
key: 'unit_cost',
width: 90,
width: 100,
align: 'center',
render: (_, row) => `¥${calcUnitCost(Number(row.cost_price), row.product_spec ?? '').toFixed(2)}`,
},
@@ -174,7 +244,11 @@ const PurchaseOrderPage: React.FC = () => {
align: 'center' as const,
render: (_: unknown, row: IPurchaseDetailRow) => {
const quantity = row.cells[store.id];
return quantity !== undefined ? <Text>{quantity}</Text> : <Text type="secondary">-</Text>;
return quantity !== undefined ? (
<Typography.Link onClick={() => openCell(row, store)}>{quantity}</Typography.Link>
) : (
<Text type="secondary">-</Text>
);
},
}));
@@ -387,7 +461,7 @@ const PurchaseOrderPage: React.FC = () => {
)}
</Drawer>
{/* 行修改:品名/供应商/包规/单位/成本,同步采购单明细 / 追加同步商品档案 */}
{/* 行修改:品名/供应商/包规/单位/成本 */}
<Modal
title={editingRow ? `修改「${editingRow.product_name}` : '修改明细行'}
open={editingRow !== null}
@@ -397,31 +471,13 @@ const PurchaseOrderPage: React.FC = () => {
<Button key="cancel" onClick={() => setEditingRow(null)}>
</Button>,
<Button
key="product"
loading={rowSaving && syncTarget === 'product'}
onClick={() => {
setSyncTarget('product');
editForm.submit();
}}
>
</Button>,
<Button
key="purchase"
type="primary"
loading={rowSaving && syncTarget === 'purchase'}
onClick={() => {
setSyncTarget('purchase');
editForm.submit();
}}
>
<Button key="purchase" type="primary" loading={rowSaving} onClick={() => editForm.submit()}>
</Button>,
</Button>
]}
>
<div className="py-2 text-gray-500">
</div>
<Form form={editForm} layout="vertical" onFinish={handleEditSave}>
<Form.Item
@@ -431,7 +487,11 @@ const PurchaseOrderPage: React.FC = () => {
>
<Input maxLength={100} />
</Form.Item>
<Form.Item label="供应商" name="supplier_id">
<Form.Item
label="供应商"
name="supplier_id"
rules={[{ required: true, message: '请选择供应商' }]}
>
<Select
allowClear
showSearch={{ optionFilterProp: 'label' }}
@@ -439,10 +499,18 @@ const PurchaseOrderPage: React.FC = () => {
options={suppliers.map((s) => ({ value: s.id, label: s.name }))}
/>
</Form.Item>
<Form.Item label="包规" name="product_spec" rules={[{ max: 100 }]}>
<Form.Item
label="包规"
name="product_spec"
rules={[{ required: true, message: '请输入包规' }, { max: 100 }]}
>
<Input maxLength={100} placeholder="如:10斤/箱" />
</Form.Item>
<Form.Item label="单位" name="unit" rules={[{ max: 20 }]}>
<Form.Item
label="单位"
name="unit"
rules={[{ required: true, message: '请输入单位' },{ max: 20 }]}
>
<Input maxLength={20} placeholder="如:斤" />
</Form.Item>
<Form.Item
@@ -454,6 +522,161 @@ const PurchaseOrderPage: React.FC = () => {
</Form.Item>
</Form>
</Modal>
{/* 单元格下钻 */}
<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">
{item.editable ? (
<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>
</>
);
};