From 5c903d7abc3671e0d70d173be98aa90264ba7c25 Mon Sep 17 00:00:00 2001 From: kae Date: Thu, 18 Jun 2026 18:25:00 +0900 Subject: [PATCH] feat: add create purchase order page and route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .umirc.ts | 1 + src/pages/Purchase/Orders/new.tsx | 243 ++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 src/pages/Purchase/Orders/new.tsx diff --git a/.umirc.ts b/.umirc.ts index ff2a59e..541a3f3 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -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' }, diff --git a/src/pages/Purchase/Orders/new.tsx b/src/pages/Purchase/Orders/new.tsx new file mode 100644 index 0000000..2ea321b --- /dev/null +++ b/src/pages/Purchase/Orders/new.tsx @@ -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([]); + const [products, setProducts] = useState([]); + const [details, setDetails] = useState([{ 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) => ( + handleFieldChange(idx, 'spec', e.target.value)} /> + ), + }, + { + title: '颜色', + width: 120, + render: (_: unknown, _row: DetailRow, idx: number) => ( + handleFieldChange(idx, 'color', e.target.value)} /> + ), + }, + { + title: '采购数量', + width: 120, + render: (_: unknown, _row: DetailRow, idx: number) => ( + handleFieldChange(idx, 'quantity', e.target.value)} + /> + ), + }, + { + title: '单价', + width: 120, + render: (_: unknown, _row: DetailRow, idx: number) => ( + 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) => ( + + 新建采购订单 + + + +
+ + + + + + + + + + + + + + + +
+
+ + } onClick={addRow}>添加明细} + > + + + + + + + + + ); +}; + +export default NewOrderPage;