feat: add create purchase order page and route
The "创建采购订单" button navigated to /purchase/orders/new which matched the /:id detail route with id="new", causing the detail page to request a non-existent order and render blank. Added a dedicated new.tsx form page (supplier select, date, dynamic detail rows with product autocomplete and computed line totals) and registered /purchase/orders/new in .umirc.ts before the /:id route so it takes precedence.
This commit is contained in:
parent
391ea29f1a
commit
5c903d7abc
@ -75,6 +75,7 @@ export default defineConfig({
|
||||
routes: [
|
||||
{ name: '供应商管理', path: '/purchase/suppliers', component: './Purchase/Suppliers' },
|
||||
{ name: '采购订单', path: '/purchase/orders', component: './Purchase/Orders' },
|
||||
{ name: '新建采购订单', path: '/purchase/orders/new', component: './Purchase/Orders/new', hideInMenu: true },
|
||||
{ name: '采购订单详情', path: '/purchase/orders/:id', component: './Purchase/Orders/detail', hideInMenu: true },
|
||||
{ name: '入库记录', path: '/purchase/receipts', component: './Purchase/Receipts' },
|
||||
{ name: '采购报表', path: '/purchase/stats', component: './Purchase/Stats' },
|
||||
|
||||
243
src/pages/Purchase/Orders/new.tsx
Normal file
243
src/pages/Purchase/Orders/new.tsx
Normal file
@ -0,0 +1,243 @@
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Col, Form, Input, Row, Select, Space, Table, Typography, message } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { history } from '@umijs/max';
|
||||
import { createPurchaseOrder, listProducts, listSuppliers } from '@/services/api';
|
||||
import type { Product, Supplier } from '@/types/api';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
interface DetailRow {
|
||||
key: number;
|
||||
productId?: string;
|
||||
productName?: string;
|
||||
spec?: string;
|
||||
color?: string;
|
||||
quantity?: string;
|
||||
unitPrice?: string;
|
||||
}
|
||||
|
||||
let rowKey = 0;
|
||||
|
||||
const NewOrderPage: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [suppliers, setSuppliers] = useState<Supplier[]>([]);
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [details, setDetails] = useState<DetailRow[]>([{ key: rowKey++ }]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
listSuppliers({ page: 1, pageSize: 200, status: 1 }).then((res) => {
|
||||
if (res?.code === 0) setSuppliers(res.data?.list || []);
|
||||
});
|
||||
listProducts({ page: 1, pageSize: 500 }).then((res) => {
|
||||
if (res?.code === 0) setProducts(res.data?.list || []);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const productMap = Object.fromEntries(products.map((p) => [p.productId, p]));
|
||||
|
||||
const handleProductChange = (rowIdx: number, productId: string) => {
|
||||
const p = productMap[productId];
|
||||
if (!p) return;
|
||||
setDetails((prev) =>
|
||||
prev.map((r, i) =>
|
||||
i === rowIdx
|
||||
? { ...r, productId, productName: p.productName, spec: p.spec || '', color: p.color || '' }
|
||||
: r,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleFieldChange = (rowIdx: number, field: keyof DetailRow, value: string) => {
|
||||
setDetails((prev) => prev.map((r, i) => (i === rowIdx ? { ...r, [field]: value } : r)));
|
||||
};
|
||||
|
||||
const addRow = () => setDetails((prev) => [...prev, { key: rowKey++ }]);
|
||||
|
||||
const removeRow = (rowIdx: number) => {
|
||||
setDetails((prev) => prev.filter((_, i) => i !== rowIdx));
|
||||
};
|
||||
|
||||
const calcAmount = (row: DetailRow) => {
|
||||
const q = parseFloat(row.quantity || '0');
|
||||
const p = parseFloat(row.unitPrice || '0');
|
||||
return isNaN(q) || isNaN(p) ? '' : (q * p).toFixed(2);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
let headerValues: { supplierId: string; orderDate: string; purchaseBy?: string; remark?: string };
|
||||
try {
|
||||
headerValues = await form.validateFields();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const validDetails = details.filter((r) => r.productId && r.quantity && r.unitPrice);
|
||||
if (validDetails.length === 0) {
|
||||
message.error('请至少添加一条采购明细');
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const res = await createPurchaseOrder({
|
||||
...headerValues,
|
||||
details: validDetails.map((r) => ({
|
||||
productId: r.productId!,
|
||||
productName: r.productName!,
|
||||
spec: r.spec,
|
||||
color: r.color,
|
||||
quantity: r.quantity!,
|
||||
unitPrice: r.unitPrice!,
|
||||
})),
|
||||
});
|
||||
if (res?.code !== 0) {
|
||||
message.error(res?.msg || '创建失败');
|
||||
return;
|
||||
}
|
||||
message.success('采购订单已创建');
|
||||
history.push(`/purchase/orders/${res.data.id}`);
|
||||
} catch {
|
||||
message.error('创建失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '产品',
|
||||
width: 220,
|
||||
render: (_: unknown, _row: DetailRow, idx: number) => (
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
showSearch
|
||||
placeholder="选择产品"
|
||||
value={details[idx].productId}
|
||||
optionFilterProp="label"
|
||||
options={products.map((p) => ({ value: p.productId, label: `${p.productName}${p.spec ? ` ${p.spec}` : ''}${p.color ? ` ${p.color}` : ''}` }))}
|
||||
onChange={(v) => handleProductChange(idx, v)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '规格',
|
||||
width: 120,
|
||||
render: (_: unknown, _row: DetailRow, idx: number) => (
|
||||
<Input value={details[idx].spec} onChange={(e) => handleFieldChange(idx, 'spec', e.target.value)} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '颜色',
|
||||
width: 120,
|
||||
render: (_: unknown, _row: DetailRow, idx: number) => (
|
||||
<Input value={details[idx].color} onChange={(e) => handleFieldChange(idx, 'color', e.target.value)} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '采购数量',
|
||||
width: 120,
|
||||
render: (_: unknown, _row: DetailRow, idx: number) => (
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
value={details[idx].quantity}
|
||||
onChange={(e) => handleFieldChange(idx, 'quantity', e.target.value)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '单价',
|
||||
width: 120,
|
||||
render: (_: unknown, _row: DetailRow, idx: number) => (
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
step="0.01"
|
||||
value={details[idx].unitPrice}
|
||||
onChange={(e) => handleFieldChange(idx, 'unitPrice', e.target.value)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '小计',
|
||||
width: 100,
|
||||
render: (_: unknown, _row: DetailRow, idx: number) => calcAmount(details[idx]),
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
width: 48,
|
||||
render: (_: unknown, _row: DetailRow, idx: number) => (
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
disabled={details.length === 1}
|
||||
onClick={() => removeRow(idx)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||
<Space>
|
||||
<Button onClick={() => history.back()}>返回</Button>
|
||||
<Title level={4} style={{ margin: 0 }}>新建采购订单</Title>
|
||||
</Space>
|
||||
|
||||
<Card>
|
||||
<Form form={form} layout="vertical" initialValues={{ orderDate: today }}>
|
||||
<Row gutter={24}>
|
||||
<Col span={8}>
|
||||
<Form.Item name="supplierId" label="供应商" rules={[{ required: true, message: '请选择供应商' }]}>
|
||||
<Select
|
||||
showSearch
|
||||
placeholder="选择供应商"
|
||||
optionFilterProp="label"
|
||||
options={suppliers.map((s) => ({ value: s.supplierId, label: s.supplierName }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="orderDate" label="采购日期" rules={[{ required: true, message: '请选择日期' }]}>
|
||||
<Input type="date" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="purchaseBy" label="采购员">
|
||||
<Input placeholder="可选" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={2} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="采购明细"
|
||||
extra={<Button icon={<PlusOutlined />} onClick={addRow}>添加明细</Button>}
|
||||
>
|
||||
<Table
|
||||
rowKey="key"
|
||||
dataSource={details}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Space>
|
||||
<Button onClick={() => history.back()}>取消</Button>
|
||||
<Button type="primary" loading={submitting} onClick={handleSubmit}>提交</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewOrderPage;
|
||||
Loading…
x
Reference in New Issue
Block a user