From 8096f4a84811e981d9c0b2f04a49259979672340 Mon Sep 17 00:00:00 2001 From: Chever John Date: Mon, 20 Apr 2026 01:58:36 +0000 Subject: [PATCH] first init --- .umirc.ts | 4 +- src/pages/Dashboard/index.tsx | 2 +- src/pages/Inventory/Bolts/index.tsx | 53 ++++ src/pages/Inventory/Pans/index.tsx | 62 +++++ src/pages/Inventory/Products/index.tsx | 345 +++++++++++++++++++------ src/pages/Inventory/Stocks/index.tsx | 7 +- src/pages/Login/index.tsx | 55 +++- src/pages/System/Menus/index.tsx | 172 ++++++++++-- src/services/api.ts | 79 +++++- src/types/api.ts | 109 +++++++- 10 files changed, 742 insertions(+), 146 deletions(-) create mode 100644 src/pages/Inventory/Bolts/index.tsx create mode 100644 src/pages/Inventory/Pans/index.tsx diff --git a/.umirc.ts b/.umirc.ts index d351dc4..372343f 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -57,7 +57,9 @@ export default defineConfig({ path: '/inventory', icon: 'DatabaseOutlined', routes: [ - { name: '产品档案', path: '/inventory/products', component: './Inventory/Products' }, + { name: '产品库存', path: '/inventory/products', component: './Inventory/Products' }, + { name: '盘库存', path: '/inventory/pans', component: './Inventory/Pans' }, + { name: '匹库存', path: '/inventory/bolts', component: './Inventory/Bolts' }, { name: '库存查询', path: '/inventory/stocks', component: './Inventory/Stocks' }, { name: '库存统计', path: '/inventory/stats', component: './Inventory/Stats' }, { name: '库存盘点', path: '/inventory/checks', component: './Inventory/Checks' }, diff --git a/src/pages/Dashboard/index.tsx b/src/pages/Dashboard/index.tsx index 5eb6f12..02947ac 100644 --- a/src/pages/Dashboard/index.tsx +++ b/src/pages/Dashboard/index.tsx @@ -199,7 +199,7 @@ const DashboardPage: React.FC = () => { {[ - { title: '产品档案', desc: '管理产品信息', path: '/inventory/products', icon: }, + { title: '产品库存', desc: '管理产品信息', path: '/inventory/products', icon: }, { title: '库存查询', desc: '实时库存数据', path: '/inventory/stocks', icon: }, { title: '库存统计', desc: '分类统计分析', path: '/inventory/stats', icon: }, { title: 'Excel导入', desc: '批量导入产品', path: '/inventory/import', icon: }, diff --git a/src/pages/Inventory/Bolts/index.tsx b/src/pages/Inventory/Bolts/index.tsx new file mode 100644 index 0000000..04ca2b4 --- /dev/null +++ b/src/pages/Inventory/Bolts/index.tsx @@ -0,0 +1,53 @@ +import { ProColumns, ProTable } from '@ant-design/pro-components'; +import { Tag } from 'antd'; +import { listBoltView } from '@/services/api'; +import type { BoltListItem } from '@/types/api'; + +const columns: ProColumns[] = [ + { title: '产品名称', dataIndex: 'productName', ellipsis: true }, + { + title: '颜色', + dataIndex: 'color', + search: false, + render: (_, r) => r.color ? {r.color} : -, + }, + { title: '盘标签', dataIndex: 'panName', search: false, width: 80 }, + { + title: '位置', + dataIndex: 'position', + render: (_, r) => + r.position ? {r.position} : -, + }, + { title: '序号', dataIndex: 'sortOrder', search: false, width: 70 }, + { + title: '长度(m)', + dataIndex: 'lengthM', + search: false, + sorter: true, + render: (_, r) => {r.lengthM} m, + }, +]; + +const BoltsPage: React.FC = () => ( + + headerTitle="匹维度库存" + rowKey="boltId" + columns={columns} + request={async (params) => { + const res = await listBoltView({ + page: params.current, + pageSize: params.pageSize, + productName: params.productName, + position: params.position, + }); + return { + data: res?.data?.list || [], + total: res?.data?.total || 0, + success: res?.code === 0, + }; + }} + search={{ labelWidth: 'auto' }} + /> +); + +export default BoltsPage; diff --git a/src/pages/Inventory/Pans/index.tsx b/src/pages/Inventory/Pans/index.tsx new file mode 100644 index 0000000..dff6b6d --- /dev/null +++ b/src/pages/Inventory/Pans/index.tsx @@ -0,0 +1,62 @@ +import { ProColumns, ProTable } from '@ant-design/pro-components'; +import { Tag, Space } from 'antd'; +import { listPanView } from '@/services/api'; +import type { PanListItem } from '@/types/api'; + +const columns: ProColumns[] = [ + { title: '产品名称', dataIndex: 'productName', ellipsis: true }, + { title: '盘标签', dataIndex: 'name', search: false, width: 80 }, + { + title: '位置', + dataIndex: 'position', + render: (_, r) => + r.position ? {r.position} : -, + }, + { + title: '颜色汇总', + dataIndex: 'colors', + search: false, + render: (_, r) => + r.colors ? ( + + {r.colors.split(',').map((c) => ( + {c.trim()} + ))} + + ) : ( + - + ), + }, + { title: '匹数', dataIndex: 'boltCount', search: false, sorter: true, width: 80 }, + { + title: '库存(m)', + dataIndex: 'totalLengthM', + search: false, + sorter: true, + render: (_, r) => `${r.totalLengthM} m`, + }, +]; + +const PansPage: React.FC = () => ( + + headerTitle="盘维度库存" + rowKey="panId" + columns={columns} + request={async (params) => { + const res = await listPanView({ + page: params.current, + pageSize: params.pageSize, + productName: params.productName, + position: params.position, + }); + return { + data: res?.data?.list || [], + total: res?.data?.total || 0, + success: res?.code === 0, + }; + }} + search={{ labelWidth: 'auto' }} + /> +); + +export default PansPage; diff --git a/src/pages/Inventory/Products/index.tsx b/src/pages/Inventory/Products/index.tsx index cc4a7df..41a5eea 100644 --- a/src/pages/Inventory/Products/index.tsx +++ b/src/pages/Inventory/Products/index.tsx @@ -1,120 +1,301 @@ -import { PlusOutlined, UploadOutlined, DownloadOutlined } from '@ant-design/icons'; -import { ActionType, ModalForm, ProColumns, ProFormDigit, ProFormText, ProFormTextArea, ProTable } from '@ant-design/pro-components'; -import { Button, message, Popconfirm } from 'antd'; -import { useRef, useState } from 'react'; -import { listProducts, createProduct, updateProduct, deleteProduct } from '@/services/api'; -import type { Product } from '@/types/api'; +import { PlusOutlined, UploadOutlined, DownloadOutlined, ArrowLeftOutlined } from '@ant-design/icons'; +import { ActionType, ModalForm, ProColumns, ProFormText, ProFormTextArea, ProTable } from '@ant-design/pro-components'; +import { Button, Card, Descriptions, Drawer, Input, InputNumber, List, message, Popconfirm, Space, Tag } from 'antd'; +import { useRef, useState, useCallback } from 'react'; +import { + getProductSummary, + getColorDetail, + getProductTree, + createProduct, + updateProduct, + deleteProduct, + savePans, +} from '@/services/api'; +import type { ProductSummaryItem, ColorDetailItem, ProductTree, PanInput } from '@/types/api'; -const productFormFields = ( - <> - - - - - - - - - - - -); +type ViewLevel = 'summary' | 'colors' | 'tree'; const ProductsPage: React.FC = () => { - const actionRef = useRef(); + const summaryRef = useRef(); + const [viewLevel, setViewLevel] = useState('summary'); + const [selectedProductName, setSelectedProductName] = useState(''); + const [selectedProductId, setSelectedProductId] = useState(''); + const [treeData, setTreeData] = useState(null); const [createOpen, setCreateOpen] = useState(false); const [editOpen, setEditOpen] = useState(false); - const [currentRow, setCurrentRow] = useState(null); + const [editTreeDrawer, setEditTreeDrawer] = useState(false); + const [currentColor, setCurrentColor] = useState(null); + const [panInputs, setPanInputs] = useState([]); - const columns: ProColumns[] = [ + const loadTree = useCallback(async (productId: string) => { + const res = await getProductTree(productId); + if (res?.code === 0) { + setTreeData(res.data); + setSelectedProductId(productId); + setViewLevel('tree'); + } + }, []); + + // ── Panel 1: Product Summary ── + const summaryColumns: ProColumns[] = [ { title: '产品名称', dataIndex: 'productName', ellipsis: true }, - { title: '规格型号', dataIndex: 'spec', ellipsis: true }, - { title: '颜色', dataIndex: 'color' }, - { title: '匹数', dataIndex: 'unitPieces', search: false }, - { title: '盘数', dataIndex: 'unitRolls', search: false }, - { title: '库存数量', dataIndex: 'stockQuantity', search: false }, - { title: '存放位置', dataIndex: 'location' }, - { title: '成本价', dataIndex: 'costPrice', search: false, valueType: 'money' }, - { title: '销售价', dataIndex: 'salesPrice', search: false, valueType: 'money' }, - { title: '更新时间', dataIndex: 'updatedAt', valueType: 'dateTime', search: false }, + { title: '规格型号', dataIndex: 'spec', search: false }, + { title: '颜色数', dataIndex: 'colorCount', search: false }, + { title: '总盘数', dataIndex: 'totalPanCount', search: false }, + { title: '总匹数', dataIndex: 'totalBoltCount', search: false }, + { title: '总米数', dataIndex: 'totalLengthM', search: false }, + { title: '颜色', dataIndex: 'colors', search: false, render: (_, r) => r.colors?.split(',').map((c) => {c}) }, + { title: '位置', dataIndex: 'locations', search: false, ellipsis: true }, { - title: '操作', - valueType: 'option', - render: (_, record) => [ - { setCurrentRow(record); setEditOpen(true); }}>编辑, - { - const res = await deleteProduct(record.productId); - if (res?.code === 0) { message.success('删除成功'); actionRef.current?.reload(); } - }} - > - 删除 - , + title: '操作', valueType: 'option', + render: (_, r) => [ + { setSelectedProductName(r.productName); setViewLevel('colors'); }}>查看颜色, ], }, ]; - const normalizeValues = (values: Record) => ({ - ...values, - stockQuantity: String(values.stockQuantity || '0'), - costPrice: String(values.costPrice || '0'), - salesPrice: String(values.salesPrice || '0'), - }); + // ── Panel 2: Color Detail ── + const colorColumns: ProColumns[] = [ + { title: '颜色', dataIndex: 'color' }, + { title: '规格', dataIndex: 'spec', search: false }, + { title: '盘数', dataIndex: 'panCount', search: false }, + { title: '匹数', dataIndex: 'boltCount', search: false }, + { title: '库存(m)', dataIndex: 'totalLengthM', search: false }, + { title: '位置', dataIndex: 'locations', search: false, ellipsis: true }, + { title: '销售价', dataIndex: 'salesPrice', search: false, valueType: 'money' }, + { + title: '操作', valueType: 'option', + render: (_, r) => [ + loadTree(r.productId)}>盘/匹详情, + { setCurrentColor(r); setEditOpen(true); }}>编辑, + { + const res = await deleteProduct(r.productId); + if (res?.code === 0) message.success('删除成功'); + }}>删除, + ], + }, + ]; + + // ── Panel 3: Tree Detail ── + const renderTree = () => { + if (!treeData) return null; + const { product, pans, totalPanCount, totalBoltCount, totalLengthM } = treeData; + return ( +
+ + {product.spec} + {totalPanCount} + {totalBoltCount} + {totalLengthM}m + + + {pans.map((pan) => ( + {pan.position || '未设置位置'}}> + ( + 匹{idx + 1}: {bolt.lengthM}m + )} + footer={
小计: {pan.boltCount}匹, {pan.totalLength}m
} + /> +
+ ))} +
+ +
+ ); + }; + + const breadcrumb = () => ( + + {viewLevel !== 'summary' && ( + + )} + setViewLevel('summary')}>产品总览 + {viewLevel !== 'summary' && / {selectedProductName}} + {viewLevel === 'tree' && treeData && / {treeData.product.color}} + + ); return ( <> - - headerTitle="产品档案" - actionRef={actionRef} - rowKey="productId" - columns={columns} - request={async (params) => { - const res = await listProducts({ - page: params.current, - pageSize: params.pageSize, - productName: params.productName, - spec: params.spec, - color: params.color, - location: params.location, - }); - return { data: res?.data?.list || [], total: res?.data?.total || 0, success: res?.code === 0 }; - }} - toolBarRender={() => [ - , - , - , - ]} - /> + {breadcrumb()} + + {viewLevel === 'summary' && ( + + headerTitle="产品总览" + actionRef={summaryRef} + rowKey="productName" + columns={summaryColumns} + search={false} + request={async () => { + const res = await getProductSummary(); + return { data: res?.data?.list || [], success: res?.code === 0, total: res?.data?.list?.length || 0 }; + }} + toolBarRender={() => [ + , + , + , + ]} + /> + )} + + {viewLevel === 'colors' && ( + + headerTitle={`${selectedProductName} - 颜色明细`} + rowKey="productId" + columns={colorColumns} + search={false} + request={async () => { + const res = await getColorDetail(selectedProductName); + return { data: res?.data?.list || [], success: res?.code === 0, total: res?.data?.list?.length || 0 }; + }} + /> + )} + + {viewLevel === 'tree' && renderTree()} + + {/* Create product modal */} { - const res = await createProduct(normalizeValues(values)); - if (res?.code === 0) { message.success('创建成功'); actionRef.current?.reload(); return true; } + const res = await createProduct({ ...values, salesPrice: String(values.salesPrice || '0') }); + if (res?.code === 0) { message.success('创建成功'); summaryRef.current?.reload(); return true; } message.error(res?.msg || '创建失败'); return false; }} > - {productFormFields} + + + + + + + {/* Edit product modal */} { - if (!currentRow) return false; - const res = await updateProduct(currentRow.productId, normalizeValues(values)); - if (res?.code === 0) { message.success('更新成功'); actionRef.current?.reload(); return true; } + if (!currentColor) return false; + const res = await updateProduct(currentColor.productId, { ...values, salesPrice: String(values.salesPrice || '0') }); + if (res?.code === 0) { message.success('更新成功'); return true; } message.error(res?.msg || '更新失败'); return false; }} > - {productFormFields} + + + + + + + {/* Edit Pans/Bolts Drawer */} + setEditTreeDrawer(false)} + extra={ + + } + > + {panInputs.map((pan, pi) => ( + setPanInputs((p) => p.filter((_, i) => i !== pi))}>删除盘 + } + > + + { + const next = [...panInputs]; + next[pi] = { ...next[pi], name: e.target.value }; + setPanInputs(next); + }} + style={{ width: 80 }} + /> + { + const next = [...panInputs]; + next[pi] = { ...next[pi], position: e.target.value }; + setPanInputs(next); + }} + style={{ width: 120 }} + /> + + {pan.boltLengths?.map((len, bi) => ( + + 匹{bi + 1}: + { + const next = [...panInputs]; + const bolts = [...(next[pi].boltLengths || [])]; + bolts[bi] = String(v || 0); + next[pi] = { ...next[pi], boltLengths: bolts }; + setPanInputs(next); + }} + /> + + + ))} + + + ))} + + ); }; diff --git a/src/pages/Inventory/Stocks/index.tsx b/src/pages/Inventory/Stocks/index.tsx index 42d1b3c..fb61e75 100644 --- a/src/pages/Inventory/Stocks/index.tsx +++ b/src/pages/Inventory/Stocks/index.tsx @@ -6,12 +6,8 @@ const columns: ProColumns[] = [ { title: '产品名称', dataIndex: 'productName', ellipsis: true }, { title: '规格型号', dataIndex: 'spec' }, { title: '颜色', dataIndex: 'color' }, - { title: '匹数', dataIndex: 'unitPieces', search: false }, - { title: '盘数', dataIndex: 'unitRolls', search: false }, - { title: '库存数量', dataIndex: 'stockQuantity', search: false, sorter: true }, - { title: '存放位置', dataIndex: 'location' }, - { title: '成本价', dataIndex: 'costPrice', search: false, valueType: 'money' }, { title: '销售价', dataIndex: 'salesPrice', search: false, valueType: 'money' }, + { title: '更新时间', dataIndex: 'updatedAt', search: false, valueType: 'dateTime' }, ]; const StocksPage: React.FC = () => ( @@ -25,7 +21,6 @@ const StocksPage: React.FC = () => ( pageSize: params.pageSize, productName: params.productName, color: params.color, - location: params.location, }); return { data: res?.data?.list || [], diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index 21b78ba..846b988 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -1,13 +1,39 @@ -import React, { useState } from 'react'; -import { history, useModel } from '@umijs/max'; -import { Button, Form, Input, message } from 'antd'; -import { LockOutlined, UserOutlined, HomeOutlined } from '@ant-design/icons'; +import React, { useCallback, useRef, useState } from 'react'; +import { history, useModel, request } from '@umijs/max'; +import { Button, Form, Input, message, Select } from 'antd'; +import { LockOutlined, UserOutlined, BankOutlined } from '@ant-design/icons'; import { getUserInfo, login } from '@/services/api'; import type { LoginParams } from '@/types/api'; const LoginPage: React.FC = () => { const { setInitialState } = useModel('@@initialState'); const [loading, setLoading] = useState(false); + const [tenantOptions, setTenantOptions] = useState<{ label: string; value: string }[]>([]); + const [searching, setSearching] = useState(false); + const debounceRef = useRef>(); + + const handleTenantSearch = useCallback((keyword: string) => { + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(async () => { + setSearching(true); + try { + const res = await request('/api/v1/public/tenants/search', { + method: 'GET', + params: { keyword, limit: 20 }, + }); + if (res?.code === 0 && res.data?.list) { + setTenantOptions( + res.data.list.map((t: { id: string; name: string }) => ({ + label: `${t.name}(${t.id})`, + value: t.id, + })), + ); + } + } finally { + setSearching(false); + } + }, 300); + }, []); const handleSubmit = async (values: LoginParams) => { setLoading(true); @@ -63,7 +89,7 @@ const LoginPage: React.FC = () => { 木羽キハネ 清風セイフウ -

仓储管理系统

+

综合管理信息系统

@@ -88,11 +114,20 @@ const LoginPage: React.FC = () => { /> - - } - placeholder="租户ID(必填)" - style={styles.input} + +