first init
This commit is contained in:
parent
4aba6541f3
commit
8096f4a848
@ -57,7 +57,9 @@ export default defineConfig({
|
|||||||
path: '/inventory',
|
path: '/inventory',
|
||||||
icon: 'DatabaseOutlined',
|
icon: 'DatabaseOutlined',
|
||||||
routes: [
|
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/stocks', component: './Inventory/Stocks' },
|
||||||
{ name: '库存统计', path: '/inventory/stats', component: './Inventory/Stats' },
|
{ name: '库存统计', path: '/inventory/stats', component: './Inventory/Stats' },
|
||||||
{ name: '库存盘点', path: '/inventory/checks', component: './Inventory/Checks' },
|
{ name: '库存盘点', path: '/inventory/checks', component: './Inventory/Checks' },
|
||||||
|
|||||||
@ -199,7 +199,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
{[
|
{[
|
||||||
{ title: '产品档案', desc: '管理产品信息', path: '/inventory/products', icon: <ShoppingOutlined /> },
|
{ title: '产品库存', desc: '管理产品信息', path: '/inventory/products', icon: <ShoppingOutlined /> },
|
||||||
{ title: '库存查询', desc: '实时库存数据', path: '/inventory/stocks', icon: <DatabaseOutlined /> },
|
{ title: '库存查询', desc: '实时库存数据', path: '/inventory/stocks', icon: <DatabaseOutlined /> },
|
||||||
{ title: '库存统计', desc: '分类统计分析', path: '/inventory/stats', icon: <RiseOutlined /> },
|
{ title: '库存统计', desc: '分类统计分析', path: '/inventory/stats', icon: <RiseOutlined /> },
|
||||||
{ title: 'Excel导入', desc: '批量导入产品', path: '/inventory/import', icon: <InboxOutlined /> },
|
{ title: 'Excel导入', desc: '批量导入产品', path: '/inventory/import', icon: <InboxOutlined /> },
|
||||||
|
|||||||
53
src/pages/Inventory/Bolts/index.tsx
Normal file
53
src/pages/Inventory/Bolts/index.tsx
Normal file
@ -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<BoltListItem>[] = [
|
||||||
|
{ title: '产品名称', dataIndex: 'productName', ellipsis: true },
|
||||||
|
{
|
||||||
|
title: '颜色',
|
||||||
|
dataIndex: 'color',
|
||||||
|
search: false,
|
||||||
|
render: (_, r) => r.color ? <Tag>{r.color}</Tag> : <span style={{ color: '#bbb' }}>-</span>,
|
||||||
|
},
|
||||||
|
{ title: '盘标签', dataIndex: 'panName', search: false, width: 80 },
|
||||||
|
{
|
||||||
|
title: '位置',
|
||||||
|
dataIndex: 'position',
|
||||||
|
render: (_, r) =>
|
||||||
|
r.position ? <Tag color="blue">{r.position}</Tag> : <span style={{ color: '#bbb' }}>-</span>,
|
||||||
|
},
|
||||||
|
{ title: '序号', dataIndex: 'sortOrder', search: false, width: 70 },
|
||||||
|
{
|
||||||
|
title: '长度(m)',
|
||||||
|
dataIndex: 'lengthM',
|
||||||
|
search: false,
|
||||||
|
sorter: true,
|
||||||
|
render: (_, r) => <strong>{r.lengthM} m</strong>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const BoltsPage: React.FC = () => (
|
||||||
|
<ProTable<BoltListItem>
|
||||||
|
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;
|
||||||
62
src/pages/Inventory/Pans/index.tsx
Normal file
62
src/pages/Inventory/Pans/index.tsx
Normal file
@ -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<PanListItem>[] = [
|
||||||
|
{ title: '产品名称', dataIndex: 'productName', ellipsis: true },
|
||||||
|
{ title: '盘标签', dataIndex: 'name', search: false, width: 80 },
|
||||||
|
{
|
||||||
|
title: '位置',
|
||||||
|
dataIndex: 'position',
|
||||||
|
render: (_, r) =>
|
||||||
|
r.position ? <Tag color="blue">{r.position}</Tag> : <span style={{ color: '#bbb' }}>-</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '颜色汇总',
|
||||||
|
dataIndex: 'colors',
|
||||||
|
search: false,
|
||||||
|
render: (_, r) =>
|
||||||
|
r.colors ? (
|
||||||
|
<Space size={4} wrap>
|
||||||
|
{r.colors.split(',').map((c) => (
|
||||||
|
<Tag key={c.trim()}>{c.trim()}</Tag>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<span style={{ color: '#bbb' }}>-</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ 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 = () => (
|
||||||
|
<ProTable<PanListItem>
|
||||||
|
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;
|
||||||
@ -1,85 +1,147 @@
|
|||||||
import { PlusOutlined, UploadOutlined, DownloadOutlined } from '@ant-design/icons';
|
import { PlusOutlined, UploadOutlined, DownloadOutlined, ArrowLeftOutlined } from '@ant-design/icons';
|
||||||
import { ActionType, ModalForm, ProColumns, ProFormDigit, ProFormText, ProFormTextArea, ProTable } from '@ant-design/pro-components';
|
import { ActionType, ModalForm, ProColumns, ProFormText, ProFormTextArea, ProTable } from '@ant-design/pro-components';
|
||||||
import { Button, message, Popconfirm } from 'antd';
|
import { Button, Card, Descriptions, Drawer, Input, InputNumber, List, message, Popconfirm, Space, Tag } from 'antd';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState, useCallback } from 'react';
|
||||||
import { listProducts, createProduct, updateProduct, deleteProduct } from '@/services/api';
|
import {
|
||||||
import type { Product } from '@/types/api';
|
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';
|
||||||
<>
|
|
||||||
<ProFormText name="productName" label="产品名称" rules={[{ required: true }]} />
|
|
||||||
<ProFormText name="spec" label="规格型号" />
|
|
||||||
<ProFormText name="color" label="颜色" />
|
|
||||||
<ProFormDigit name="unitPieces" label="匹数" min={0} />
|
|
||||||
<ProFormDigit name="unitRolls" label="盘数" min={0} />
|
|
||||||
<ProFormText name="stockQuantity" label="库存数量" rules={[{ required: true }]} />
|
|
||||||
<ProFormText name="location" label="存放位置" />
|
|
||||||
<ProFormText name="costPrice" label="成本价" />
|
|
||||||
<ProFormText name="salesPrice" label="销售价" />
|
|
||||||
<ProFormTextArea name="remark" label="备注" />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const ProductsPage: React.FC = () => {
|
const ProductsPage: React.FC = () => {
|
||||||
const actionRef = useRef<ActionType>();
|
const summaryRef = useRef<ActionType>();
|
||||||
|
const [viewLevel, setViewLevel] = useState<ViewLevel>('summary');
|
||||||
|
const [selectedProductName, setSelectedProductName] = useState('');
|
||||||
|
const [selectedProductId, setSelectedProductId] = useState('');
|
||||||
|
const [treeData, setTreeData] = useState<ProductTree | null>(null);
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
const [currentRow, setCurrentRow] = useState<Product | null>(null);
|
const [editTreeDrawer, setEditTreeDrawer] = useState(false);
|
||||||
|
const [currentColor, setCurrentColor] = useState<ColorDetailItem | null>(null);
|
||||||
|
const [panInputs, setPanInputs] = useState<PanInput[]>([]);
|
||||||
|
|
||||||
const columns: ProColumns<Product>[] = [
|
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<ProductSummaryItem>[] = [
|
||||||
{ title: '产品名称', dataIndex: 'productName', ellipsis: true },
|
{ title: '产品名称', dataIndex: 'productName', ellipsis: true },
|
||||||
{ title: '规格型号', dataIndex: 'spec', ellipsis: true },
|
{ title: '规格型号', dataIndex: 'spec', search: false },
|
||||||
{ title: '颜色', dataIndex: 'color' },
|
{ title: '颜色数', dataIndex: 'colorCount', search: false },
|
||||||
{ title: '匹数', dataIndex: 'unitPieces', search: false },
|
{ title: '总盘数', dataIndex: 'totalPanCount', search: false },
|
||||||
{ title: '盘数', dataIndex: 'unitRolls', search: false },
|
{ title: '总匹数', dataIndex: 'totalBoltCount', search: false },
|
||||||
{ title: '库存数量', dataIndex: 'stockQuantity', search: false },
|
{ title: '总米数', dataIndex: 'totalLengthM', search: false },
|
||||||
{ title: '存放位置', dataIndex: 'location' },
|
{ title: '颜色', dataIndex: 'colors', search: false, render: (_, r) => r.colors?.split(',').map((c) => <Tag key={c}>{c}</Tag>) },
|
||||||
{ title: '成本价', dataIndex: 'costPrice', search: false, valueType: 'money' },
|
{ title: '位置', dataIndex: 'locations', search: false, ellipsis: true },
|
||||||
{ title: '销售价', dataIndex: 'salesPrice', search: false, valueType: 'money' },
|
|
||||||
{ title: '更新时间', dataIndex: 'updatedAt', valueType: 'dateTime', search: false },
|
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作', valueType: 'option',
|
||||||
valueType: 'option',
|
render: (_, r) => [
|
||||||
render: (_, record) => [
|
<a key="detail" onClick={() => { setSelectedProductName(r.productName); setViewLevel('colors'); }}>查看颜色</a>,
|
||||||
<a key="edit" onClick={() => { setCurrentRow(record); setEditOpen(true); }}>编辑</a>,
|
|
||||||
<Popconfirm
|
|
||||||
key="delete"
|
|
||||||
title="确认删除?"
|
|
||||||
onConfirm={async () => {
|
|
||||||
const res = await deleteProduct(record.productId);
|
|
||||||
if (res?.code === 0) { message.success('删除成功'); actionRef.current?.reload(); }
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a style={{ color: 'var(--ant-color-error)' }}>删除</a>
|
|
||||||
</Popconfirm>,
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const normalizeValues = (values: Record<string, any>) => ({
|
// ── Panel 2: Color Detail ──
|
||||||
...values,
|
const colorColumns: ProColumns<ColorDetailItem>[] = [
|
||||||
stockQuantity: String(values.stockQuantity || '0'),
|
{ title: '颜色', dataIndex: 'color' },
|
||||||
costPrice: String(values.costPrice || '0'),
|
{ title: '规格', dataIndex: 'spec', search: false },
|
||||||
salesPrice: String(values.salesPrice || '0'),
|
{ 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) => [
|
||||||
|
<a key="tree" onClick={() => loadTree(r.productId)}>盘/匹详情</a>,
|
||||||
|
<a key="edit" onClick={() => { setCurrentColor(r); setEditOpen(true); }}>编辑</a>,
|
||||||
|
<Popconfirm key="del" title="确认删除?" onConfirm={async () => {
|
||||||
|
const res = await deleteProduct(r.productId);
|
||||||
|
if (res?.code === 0) message.success('删除成功');
|
||||||
|
}}><a style={{ color: 'var(--ant-color-error)' }}>删除</a></Popconfirm>,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Panel 3: Tree Detail ──
|
||||||
|
const renderTree = () => {
|
||||||
|
if (!treeData) return null;
|
||||||
|
const { product, pans, totalPanCount, totalBoltCount, totalLengthM } = treeData;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Descriptions title={`${product.productName} - ${product.color}`} bordered size="small" column={4} style={{ marginBottom: 16 }}>
|
||||||
|
<Descriptions.Item label="规格">{product.spec}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="总盘数">{totalPanCount}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="总匹数">{totalBoltCount}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="总米数">{totalLengthM}m</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
{pans.map((pan) => (
|
||||||
|
<Card key={pan.panId} size="small" title={`盘${pan.name}`} extra={<Tag color="blue">{pan.position || '未设置位置'}</Tag>}>
|
||||||
|
<List
|
||||||
|
size="small"
|
||||||
|
dataSource={pan.bolts}
|
||||||
|
renderItem={(bolt, idx) => (
|
||||||
|
<List.Item>匹{idx + 1}: {bolt.lengthM}m</List.Item>
|
||||||
|
)}
|
||||||
|
footer={<div>小计: {pan.boltCount}匹, {pan.totalLength}m</div>}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
<Button type="primary" style={{ marginTop: 16 }} onClick={() => {
|
||||||
|
setPanInputs(pans.map((p) => ({
|
||||||
|
name: p.name,
|
||||||
|
position: p.position,
|
||||||
|
boltLengths: p.bolts.map((b) => b.lengthM),
|
||||||
|
})));
|
||||||
|
setEditTreeDrawer(true);
|
||||||
|
}}>编辑盘/匹</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const breadcrumb = () => (
|
||||||
|
<Space style={{ marginBottom: 16 }}>
|
||||||
|
{viewLevel !== 'summary' && (
|
||||||
|
<Button icon={<ArrowLeftOutlined />} onClick={() => {
|
||||||
|
if (viewLevel === 'tree') setViewLevel('colors');
|
||||||
|
else setViewLevel('summary');
|
||||||
|
}}>返回</Button>
|
||||||
|
)}
|
||||||
|
<a onClick={() => setViewLevel('summary')}>产品总览</a>
|
||||||
|
{viewLevel !== 'summary' && <span> / {selectedProductName}</span>}
|
||||||
|
{viewLevel === 'tree' && treeData && <span> / {treeData.product.color}</span>}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProTable<Product>
|
{breadcrumb()}
|
||||||
headerTitle="产品档案"
|
|
||||||
actionRef={actionRef}
|
{viewLevel === 'summary' && (
|
||||||
rowKey="productId"
|
<ProTable<ProductSummaryItem>
|
||||||
columns={columns}
|
headerTitle="产品总览"
|
||||||
request={async (params) => {
|
actionRef={summaryRef}
|
||||||
const res = await listProducts({
|
rowKey="productName"
|
||||||
page: params.current,
|
columns={summaryColumns}
|
||||||
pageSize: params.pageSize,
|
search={false}
|
||||||
productName: params.productName,
|
request={async () => {
|
||||||
spec: params.spec,
|
const res = await getProductSummary();
|
||||||
color: params.color,
|
return { data: res?.data?.list || [], success: res?.code === 0, total: res?.data?.list?.length || 0 };
|
||||||
location: params.location,
|
|
||||||
});
|
|
||||||
return { data: res?.data?.list || [], total: res?.data?.total || 0, success: res?.code === 0 };
|
|
||||||
}}
|
}}
|
||||||
toolBarRender={() => [
|
toolBarRender={() => [
|
||||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>新增产品</Button>,
|
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>新增产品</Button>,
|
||||||
@ -87,34 +149,153 @@ const ProductsPage: React.FC = () => {
|
|||||||
<Button key="export" icon={<DownloadOutlined />}>Excel导出</Button>,
|
<Button key="export" icon={<DownloadOutlined />}>Excel导出</Button>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{viewLevel === 'colors' && (
|
||||||
|
<ProTable<ColorDetailItem>
|
||||||
|
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 */}
|
||||||
<ModalForm
|
<ModalForm
|
||||||
title="新增产品"
|
title="新增产品"
|
||||||
open={createOpen}
|
open={createOpen}
|
||||||
onOpenChange={setCreateOpen}
|
onOpenChange={setCreateOpen}
|
||||||
onFinish={async (values) => {
|
onFinish={async (values) => {
|
||||||
const res = await createProduct(normalizeValues(values));
|
const res = await createProduct({ ...values, salesPrice: String(values.salesPrice || '0') });
|
||||||
if (res?.code === 0) { message.success('创建成功'); actionRef.current?.reload(); return true; }
|
if (res?.code === 0) { message.success('创建成功'); summaryRef.current?.reload(); return true; }
|
||||||
message.error(res?.msg || '创建失败');
|
message.error(res?.msg || '创建失败');
|
||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{productFormFields}
|
<ProFormText name="productName" label="产品名称" rules={[{ required: true }]} />
|
||||||
|
<ProFormText name="spec" label="规格型号" />
|
||||||
|
<ProFormText name="color" label="颜色" />
|
||||||
|
<ProFormText name="salesPrice" label="销售价" />
|
||||||
|
<ProFormTextArea name="remark" label="备注" />
|
||||||
</ModalForm>
|
</ModalForm>
|
||||||
|
|
||||||
|
{/* Edit product modal */}
|
||||||
<ModalForm
|
<ModalForm
|
||||||
title="编辑产品"
|
title="编辑产品"
|
||||||
open={editOpen}
|
open={editOpen}
|
||||||
onOpenChange={setEditOpen}
|
onOpenChange={setEditOpen}
|
||||||
initialValues={currentRow ?? undefined}
|
initialValues={currentColor ? {
|
||||||
|
productName: currentColor.productName,
|
||||||
|
spec: currentColor.spec,
|
||||||
|
color: currentColor.color,
|
||||||
|
salesPrice: currentColor.salesPrice,
|
||||||
|
} : undefined}
|
||||||
onFinish={async (values) => {
|
onFinish={async (values) => {
|
||||||
if (!currentRow) return false;
|
if (!currentColor) return false;
|
||||||
const res = await updateProduct(currentRow.productId, normalizeValues(values));
|
const res = await updateProduct(currentColor.productId, { ...values, salesPrice: String(values.salesPrice || '0') });
|
||||||
if (res?.code === 0) { message.success('更新成功'); actionRef.current?.reload(); return true; }
|
if (res?.code === 0) { message.success('更新成功'); return true; }
|
||||||
message.error(res?.msg || '更新失败');
|
message.error(res?.msg || '更新失败');
|
||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{productFormFields}
|
<ProFormText name="productName" label="产品名称" rules={[{ required: true }]} />
|
||||||
|
<ProFormText name="spec" label="规格型号" />
|
||||||
|
<ProFormText name="color" label="颜色" />
|
||||||
|
<ProFormText name="salesPrice" label="销售价" />
|
||||||
|
<ProFormTextArea name="remark" label="备注" />
|
||||||
</ModalForm>
|
</ModalForm>
|
||||||
|
|
||||||
|
{/* Edit Pans/Bolts Drawer */}
|
||||||
|
<Drawer
|
||||||
|
title="编辑盘/匹"
|
||||||
|
width={640}
|
||||||
|
open={editTreeDrawer}
|
||||||
|
onClose={() => setEditTreeDrawer(false)}
|
||||||
|
extra={
|
||||||
|
<Button type="primary" onClick={async () => {
|
||||||
|
const res = await savePans(selectedProductId, panInputs);
|
||||||
|
if (res?.code === 0) {
|
||||||
|
message.success('保存成功');
|
||||||
|
setEditTreeDrawer(false);
|
||||||
|
loadTree(selectedProductId);
|
||||||
|
}
|
||||||
|
}}>保存</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{panInputs.map((pan, pi) => (
|
||||||
|
<Card
|
||||||
|
key={pi}
|
||||||
|
size="small"
|
||||||
|
title={`盘${pan.name}`}
|
||||||
|
style={{ marginBottom: 12 }}
|
||||||
|
extra={
|
||||||
|
<Button danger size="small" onClick={() => setPanInputs((p) => p.filter((_, i) => i !== pi))}>删除盘</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Space style={{ marginBottom: 8 }}>
|
||||||
|
<Input
|
||||||
|
placeholder="盘名"
|
||||||
|
value={pan.name}
|
||||||
|
onChange={(e) => {
|
||||||
|
const next = [...panInputs];
|
||||||
|
next[pi] = { ...next[pi], name: e.target.value };
|
||||||
|
setPanInputs(next);
|
||||||
|
}}
|
||||||
|
style={{ width: 80 }}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="位置"
|
||||||
|
value={pan.position}
|
||||||
|
onChange={(e) => {
|
||||||
|
const next = [...panInputs];
|
||||||
|
next[pi] = { ...next[pi], position: e.target.value };
|
||||||
|
setPanInputs(next);
|
||||||
|
}}
|
||||||
|
style={{ width: 120 }}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
{pan.boltLengths?.map((len, bi) => (
|
||||||
|
<Space key={bi} style={{ display: 'flex', marginBottom: 4 }}>
|
||||||
|
<span>匹{bi + 1}:</span>
|
||||||
|
<InputNumber
|
||||||
|
value={Number(len)}
|
||||||
|
min={0}
|
||||||
|
step={0.01}
|
||||||
|
addonAfter="m"
|
||||||
|
onChange={(v) => {
|
||||||
|
const next = [...panInputs];
|
||||||
|
const bolts = [...(next[pi].boltLengths || [])];
|
||||||
|
bolts[bi] = String(v || 0);
|
||||||
|
next[pi] = { ...next[pi], boltLengths: bolts };
|
||||||
|
setPanInputs(next);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button danger size="small" onClick={() => {
|
||||||
|
const next = [...panInputs];
|
||||||
|
const bolts = [...(next[pi].boltLengths || [])];
|
||||||
|
bolts.splice(bi, 1);
|
||||||
|
next[pi] = { ...next[pi], boltLengths: bolts };
|
||||||
|
setPanInputs(next);
|
||||||
|
}}>删除</Button>
|
||||||
|
</Space>
|
||||||
|
))}
|
||||||
|
<Button size="small" type="dashed" block style={{ marginTop: 8 }} onClick={() => {
|
||||||
|
const next = [...panInputs];
|
||||||
|
next[pi] = { ...next[pi], boltLengths: [...(next[pi].boltLengths || []), '0'] };
|
||||||
|
setPanInputs(next);
|
||||||
|
}}>+ 添加匹</Button>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
<Button type="dashed" block onClick={() => {
|
||||||
|
setPanInputs([...panInputs, { name: String.fromCharCode(65 + panInputs.length), position: '', boltLengths: [] }]);
|
||||||
|
}}>+ 添加盘</Button>
|
||||||
|
</Drawer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,12 +6,8 @@ const columns: ProColumns<Product>[] = [
|
|||||||
{ title: '产品名称', dataIndex: 'productName', ellipsis: true },
|
{ title: '产品名称', dataIndex: 'productName', ellipsis: true },
|
||||||
{ title: '规格型号', dataIndex: 'spec' },
|
{ title: '规格型号', dataIndex: 'spec' },
|
||||||
{ title: '颜色', dataIndex: 'color' },
|
{ 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: 'salesPrice', search: false, valueType: 'money' },
|
||||||
|
{ title: '更新时间', dataIndex: 'updatedAt', search: false, valueType: 'dateTime' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const StocksPage: React.FC = () => (
|
const StocksPage: React.FC = () => (
|
||||||
@ -25,7 +21,6 @@ const StocksPage: React.FC = () => (
|
|||||||
pageSize: params.pageSize,
|
pageSize: params.pageSize,
|
||||||
productName: params.productName,
|
productName: params.productName,
|
||||||
color: params.color,
|
color: params.color,
|
||||||
location: params.location,
|
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
data: res?.data?.list || [],
|
data: res?.data?.list || [],
|
||||||
|
|||||||
@ -1,13 +1,39 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useCallback, useRef, useState } from 'react';
|
||||||
import { history, useModel } from '@umijs/max';
|
import { history, useModel, request } from '@umijs/max';
|
||||||
import { Button, Form, Input, message } from 'antd';
|
import { Button, Form, Input, message, Select } from 'antd';
|
||||||
import { LockOutlined, UserOutlined, HomeOutlined } from '@ant-design/icons';
|
import { LockOutlined, UserOutlined, BankOutlined } from '@ant-design/icons';
|
||||||
import { getUserInfo, login } from '@/services/api';
|
import { getUserInfo, login } from '@/services/api';
|
||||||
import type { LoginParams } from '@/types/api';
|
import type { LoginParams } from '@/types/api';
|
||||||
|
|
||||||
const LoginPage: React.FC = () => {
|
const LoginPage: React.FC = () => {
|
||||||
const { setInitialState } = useModel('@@initialState');
|
const { setInitialState } = useModel('@@initialState');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [tenantOptions, setTenantOptions] = useState<{ label: string; value: string }[]>([]);
|
||||||
|
const [searching, setSearching] = useState(false);
|
||||||
|
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
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) => {
|
const handleSubmit = async (values: LoginParams) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -63,7 +89,7 @@ const LoginPage: React.FC = () => {
|
|||||||
<ruby>木羽<rt style={{ fontSize: 11, color: '#94A3B8', fontWeight: 400, letterSpacing: 4 }}>キハネ</rt></ruby>
|
<ruby>木羽<rt style={{ fontSize: 11, color: '#94A3B8', fontWeight: 400, letterSpacing: 4 }}>キハネ</rt></ruby>
|
||||||
<ruby>清風<rt style={{ fontSize: 11, color: '#94A3B8', fontWeight: 400, letterSpacing: 2 }}>セイフウ</rt></ruby>
|
<ruby>清風<rt style={{ fontSize: 11, color: '#94A3B8', fontWeight: 400, letterSpacing: 2 }}>セイフウ</rt></ruby>
|
||||||
</h1>
|
</h1>
|
||||||
<p style={styles.subtitle}>仓储管理系统</p>
|
<p style={styles.subtitle}>综合管理信息系统</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form<LoginParams>
|
<Form<LoginParams>
|
||||||
@ -88,11 +114,20 @@ const LoginPage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="tenantId" rules={[{ required: true, message: '请输入租户ID' }]}>
|
<Form.Item name="tenantId" rules={[{ required: true, message: '请选择所属公司' }]}>
|
||||||
<Input
|
<Select
|
||||||
prefix={<HomeOutlined style={{ color: '#94A3B8' }} />}
|
showSearch
|
||||||
placeholder="租户ID(必填)"
|
filterOption={false}
|
||||||
style={styles.input}
|
placeholder="搜索并选择公司"
|
||||||
|
onSearch={handleTenantSearch}
|
||||||
|
onFocus={() => handleTenantSearch('')}
|
||||||
|
loading={searching}
|
||||||
|
options={tenantOptions}
|
||||||
|
notFoundContent={searching ? '搜索中...' : '无匹配公司'}
|
||||||
|
suffixIcon={<BankOutlined style={{ color: '#94A3B8' }} />}
|
||||||
|
style={{ height: 48 }}
|
||||||
|
popupMatchSelectWidth={true}
|
||||||
|
size="large"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
|||||||
@ -1,74 +1,196 @@
|
|||||||
import { PlusOutlined } from '@ant-design/icons';
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
AppstoreOutlined,
|
||||||
|
ApartmentOutlined,
|
||||||
|
AuditOutlined,
|
||||||
|
BarChartOutlined,
|
||||||
|
BlockOutlined,
|
||||||
|
DashboardOutlined,
|
||||||
|
DatabaseOutlined,
|
||||||
|
DeploymentUnitOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
ImportOutlined,
|
||||||
|
MenuOutlined,
|
||||||
|
NodeIndexOutlined,
|
||||||
|
ScissorOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
SwapOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
ToolOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import { PageContainer } from '@ant-design/pro-components';
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
import { Button, Card, Table, Tag, message, Popconfirm } from 'antd';
|
import { Button, Card, Table, Tag, message, Popconfirm, Space } from 'antd';
|
||||||
import { useEffect, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { listMenus, deleteMenu } from '@/services/api';
|
import { listMenus, deleteMenu } from '@/services/api';
|
||||||
import type { Menu } from '@/types/api';
|
import type { Menu } from '@/types/api';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
|
|
||||||
|
const ICON_MAP: Record<string, React.ComponentType> = {
|
||||||
|
AppstoreOutlined, ApartmentOutlined, AuditOutlined, BarChartOutlined,
|
||||||
|
BlockOutlined, DashboardOutlined, DatabaseOutlined, DeploymentUnitOutlined,
|
||||||
|
FileTextOutlined, ImportOutlined, MenuOutlined, NodeIndexOutlined,
|
||||||
|
ScissorOutlined, SearchOutlined, SettingOutlined, SwapOutlined,
|
||||||
|
TeamOutlined, ToolOutlined, UserOutlined,
|
||||||
|
};
|
||||||
|
|
||||||
const MENU_TYPE_MAP: Record<number, { text: string; color: string }> = {
|
const MENU_TYPE_MAP: Record<number, { text: string; color: string }> = {
|
||||||
1: { text: '目录', color: 'blue' },
|
1: { text: '目录', color: 'blue' },
|
||||||
2: { text: '菜单', color: 'green' },
|
2: { text: '菜单', color: 'green' },
|
||||||
3: { text: '按钮', color: 'orange' },
|
3: { text: '按钮', color: 'orange' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function renderIcon(name: string): React.ReactNode {
|
||||||
|
if (!name) return null;
|
||||||
|
const IconComp = ICON_MAP[name];
|
||||||
|
if (!IconComp) return <span style={{ color: '#999', fontSize: 12 }}>{name}</span>;
|
||||||
|
return <IconComp />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMenuTree(flat: Menu[]): Menu[] {
|
||||||
|
const map = new Map<string, Menu>();
|
||||||
|
const roots: Menu[] = [];
|
||||||
|
|
||||||
|
const sorted = [...flat].sort((a, b) => a.sortOrder - b.sortOrder);
|
||||||
|
|
||||||
|
for (const item of sorted) {
|
||||||
|
map.set(item.menuId, { ...item, children: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of sorted) {
|
||||||
|
const node = map.get(item.menuId)!;
|
||||||
|
if (!item.parentId) {
|
||||||
|
roots.push(node);
|
||||||
|
} else {
|
||||||
|
const parent = map.get(item.parentId);
|
||||||
|
if (parent) {
|
||||||
|
parent.children!.push(node);
|
||||||
|
} else {
|
||||||
|
roots.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const prune = (nodes: Menu[]): Menu[] =>
|
||||||
|
nodes.map((n) => ({
|
||||||
|
...n,
|
||||||
|
children: n.children?.length ? prune(n.children) : undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return prune(roots);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectKeys(nodes: Menu[]): string[] {
|
||||||
|
const keys: string[] = [];
|
||||||
|
for (const n of nodes) {
|
||||||
|
if (n.children?.length) {
|
||||||
|
keys.push(n.menuId);
|
||||||
|
keys.push(...collectKeys(n.children));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
const MenusPage: React.FC = () => {
|
const MenusPage: React.FC = () => {
|
||||||
const [data, setData] = useState<Menu[]>([]);
|
const [flatData, setFlatData] = useState<Menu[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await listMenus();
|
const res = await listMenus();
|
||||||
if (res?.code === 0) setData(res.data?.list || []);
|
if (res?.code === 0) setFlatData(res.data?.list || []);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => { fetchData(); }, []);
|
useEffect(() => { fetchData(); }, []);
|
||||||
|
|
||||||
|
const treeData = useMemo(() => buildMenuTree(flatData), [flatData]);
|
||||||
|
const defaultExpandedKeys = useMemo(() => collectKeys(treeData), [treeData]);
|
||||||
|
|
||||||
const columns: ColumnsType<Menu> = [
|
const columns: ColumnsType<Menu> = [
|
||||||
{ title: '菜单名称', dataIndex: 'menuName', key: 'menuName' },
|
{
|
||||||
{ title: '路由', dataIndex: 'path', key: 'path' },
|
title: '菜单名称',
|
||||||
{ title: '图标', dataIndex: 'icon', key: 'icon' },
|
dataIndex: 'menuName',
|
||||||
|
key: 'menuName',
|
||||||
|
width: 240,
|
||||||
|
render: (text: string, record) => (
|
||||||
|
<Space>
|
||||||
|
{renderIcon(record.icon)}
|
||||||
|
<span>{text}</span>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '路由',
|
||||||
|
dataIndex: 'path',
|
||||||
|
key: 'path',
|
||||||
|
width: 220,
|
||||||
|
render: (v: string) => v ? <code style={{ fontSize: 13 }}>{v}</code> : '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '类型',
|
title: '类型',
|
||||||
dataIndex: 'menuType',
|
dataIndex: 'menuType',
|
||||||
key: 'menuType',
|
key: 'menuType',
|
||||||
|
width: 80,
|
||||||
|
align: 'center',
|
||||||
render: (v: number) => {
|
render: (v: number) => {
|
||||||
const t = MENU_TYPE_MAP[v] ?? MENU_TYPE_MAP[1];
|
const t = MENU_TYPE_MAP[v] ?? MENU_TYPE_MAP[1];
|
||||||
return <Tag color={t.color}>{t.text}</Tag>;
|
return <Tag color={t.color}>{t.text}</Tag>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder' },
|
{
|
||||||
|
title: '权限标识',
|
||||||
|
dataIndex: 'permission',
|
||||||
|
key: 'permission',
|
||||||
|
width: 200,
|
||||||
|
render: (v: string) => v ? <code style={{ fontSize: 12, color: '#666' }}>{v}</code> : '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '排序',
|
||||||
|
dataIndex: 'sortOrder',
|
||||||
|
key: 'sortOrder',
|
||||||
|
width: 70,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
render: (_, record) => [
|
width: 120,
|
||||||
<a key="edit" style={{ marginRight: 8 }}>编辑</a>,
|
render: (_, record) => (
|
||||||
|
<Space size="small">
|
||||||
|
<a key="edit">编辑</a>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
key="del"
|
key="del"
|
||||||
title="确认删除?"
|
title="确认删除该菜单?子菜单将一并删除。"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
const res = await deleteMenu(record.menuId);
|
const res = await deleteMenu(record.menuId);
|
||||||
if (res?.code === 0) { message.success('删除成功'); fetchData(); }
|
if (res?.code === 0) { message.success('删除成功'); fetchData(); }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<a style={{ color: 'var(--ant-color-error)' }}>删除</a>
|
<a style={{ color: 'var(--ant-color-error)' }}>删除</a>
|
||||||
</Popconfirm>,
|
</Popconfirm>
|
||||||
],
|
</Space>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<Card bordered={false}>
|
<Card bordered={false}>
|
||||||
<Button type="primary" icon={<PlusOutlined />} style={{ marginBottom: 16 }}>新增菜单</Button>
|
<Button type="primary" icon={<PlusOutlined />} style={{ marginBottom: 16 }}>
|
||||||
|
新增菜单
|
||||||
|
</Button>
|
||||||
<Table<Menu>
|
<Table<Menu>
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data}
|
dataSource={treeData}
|
||||||
rowKey="menuId"
|
rowKey="menuId"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
childrenColumnName="children"
|
|
||||||
pagination={false}
|
pagination={false}
|
||||||
|
defaultExpandAllRows
|
||||||
|
expandable={{ defaultExpandedRowKeys: defaultExpandedKeys }}
|
||||||
|
size="middle"
|
||||||
|
indentSize={24}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
|
|||||||
@ -1,23 +1,29 @@
|
|||||||
import { request } from '@umijs/max';
|
import { request } from '@umijs/max';
|
||||||
import type {
|
import type {
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
|
BoltInfo,
|
||||||
|
ColorDetailItem,
|
||||||
CurrentUser,
|
CurrentUser,
|
||||||
LoginParams,
|
LoginParams,
|
||||||
LoginResult,
|
LoginResult,
|
||||||
PaginatedData,
|
|
||||||
PaginationParams,
|
|
||||||
Product,
|
|
||||||
StockSummary,
|
|
||||||
StockGroup,
|
|
||||||
StockCheck,
|
|
||||||
StockAdjust,
|
|
||||||
User,
|
|
||||||
Role,
|
|
||||||
Menu,
|
Menu,
|
||||||
OperationLog,
|
OperationLog,
|
||||||
SystemConfig,
|
PaginatedData,
|
||||||
|
PaginationParams,
|
||||||
|
PanInfo,
|
||||||
|
PanInput,
|
||||||
|
Product,
|
||||||
|
ProductSummaryItem,
|
||||||
|
ProductTree,
|
||||||
Relation,
|
Relation,
|
||||||
RelationHistory,
|
RelationHistory,
|
||||||
|
Role,
|
||||||
|
StockAdjust,
|
||||||
|
StockCheck,
|
||||||
|
StockGroup,
|
||||||
|
StockSummary,
|
||||||
|
SystemConfig,
|
||||||
|
User,
|
||||||
} from '@/types/api';
|
} from '@/types/api';
|
||||||
|
|
||||||
// ── Auth ──────────────────────────────────────────
|
// ── Auth ──────────────────────────────────────────
|
||||||
@ -100,13 +106,13 @@ export const updateConfig = (key: string, value: string) =>
|
|||||||
|
|
||||||
// ── Products ──────────────────────────────────────
|
// ── Products ──────────────────────────────────────
|
||||||
|
|
||||||
export const listProducts = (params: PaginationParams & { productName?: string; spec?: string; color?: string; location?: string }) =>
|
export const listProducts = (params: PaginationParams & { productName?: string; spec?: string; color?: string }) =>
|
||||||
request<ApiResponse<PaginatedData<Product>>>('/api/v1/inventory/products', { method: 'GET', params });
|
request<ApiResponse<PaginatedData<Product>>>('/api/v1/inventory/products', { method: 'GET', params });
|
||||||
|
|
||||||
export const getProduct = (id: string) =>
|
export const getProduct = (id: string) =>
|
||||||
request<ApiResponse<Product>>(`/api/v1/inventory/products/${id}`, { method: 'GET' });
|
request<ApiResponse<Product>>(`/api/v1/inventory/products/${id}`, { method: 'GET' });
|
||||||
|
|
||||||
export const createProduct = (data: Partial<Product>) =>
|
export const createProduct = (data: Partial<Product> & { pans?: PanInput[] }) =>
|
||||||
request<ApiResponse>('/api/v1/inventory/products', { method: 'POST', data });
|
request<ApiResponse>('/api/v1/inventory/products', { method: 'POST', data });
|
||||||
|
|
||||||
export const updateProduct = (id: string, data: Partial<Product>) =>
|
export const updateProduct = (id: string, data: Partial<Product>) =>
|
||||||
@ -118,6 +124,55 @@ export const deleteProduct = (id: string) =>
|
|||||||
export const exportProducts = () =>
|
export const exportProducts = () =>
|
||||||
request('/api/v1/inventory/products/export', { method: 'GET', responseType: 'blob' });
|
request('/api/v1/inventory/products/export', { method: 'GET', responseType: 'blob' });
|
||||||
|
|
||||||
|
// ── Product Panels ────────────────────────────────
|
||||||
|
|
||||||
|
export const getProductSummary = () =>
|
||||||
|
request<ApiResponse<{ list: ProductSummaryItem[] }>>('/api/v1/inventory/products/summary', { method: 'GET' });
|
||||||
|
|
||||||
|
export const getColorDetail = (productName: string) =>
|
||||||
|
request<ApiResponse<{ list: ColorDetailItem[] }>>('/api/v1/inventory/products/colors', { method: 'GET', params: { productName } });
|
||||||
|
|
||||||
|
export const getProductTree = (id: string) =>
|
||||||
|
request<ApiResponse<ProductTree>>(`/api/v1/inventory/products/${id}/tree`, { method: 'GET' });
|
||||||
|
|
||||||
|
// ── Pan / Bolt CRUD ───────────────────────────────
|
||||||
|
|
||||||
|
export const listPans = (productId: string) =>
|
||||||
|
request<ApiResponse<{ list: PanInfo[] }>>(`/api/v1/inventory/products/${productId}/pans`, { method: 'GET' });
|
||||||
|
|
||||||
|
export const savePans = (productId: string, pans: PanInput[]) =>
|
||||||
|
request<ApiResponse>(`/api/v1/inventory/products/${productId}/pans`, { method: 'PUT', data: { pans } });
|
||||||
|
|
||||||
|
export const listBolts = (panId: string) =>
|
||||||
|
request<ApiResponse<{ list: BoltInfo[] }>>(`/api/v1/inventory/pans/${panId}/bolts`, { method: 'GET' });
|
||||||
|
|
||||||
|
export const saveBolts = (panId: string, lengths: string[]) =>
|
||||||
|
request<ApiResponse>(`/api/v1/inventory/pans/${panId}/bolts`, { method: 'PUT', data: { lengths } });
|
||||||
|
|
||||||
|
// ── Pan / Bolt dimension panels ───────────────────
|
||||||
|
|
||||||
|
export const listPanView = (params: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
productName?: string;
|
||||||
|
position?: string;
|
||||||
|
}) =>
|
||||||
|
request<ApiResponse<{ total: number; list: import('@/types/api').PanListItem[] }>>(
|
||||||
|
'/api/v1/inventory/pans',
|
||||||
|
{ method: 'GET', params },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const listBoltView = (params: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
productName?: string;
|
||||||
|
position?: string;
|
||||||
|
}) =>
|
||||||
|
request<ApiResponse<{ total: number; list: import('@/types/api').BoltListItem[] }>>(
|
||||||
|
'/api/v1/inventory/bolts',
|
||||||
|
{ method: 'GET', params },
|
||||||
|
);
|
||||||
|
|
||||||
// ── Stocks ────────────────────────────────────────
|
// ── Stocks ────────────────────────────────────────
|
||||||
|
|
||||||
export const getStockSummary = () =>
|
export const getStockSummary = () =>
|
||||||
|
|||||||
109
src/types/api.ts
109
src/types/api.ts
@ -32,27 +32,109 @@ export interface LoginResult {
|
|||||||
userInfo: CurrentUser;
|
userInfo: CurrentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Product (三层模型) ────────────────────────────
|
||||||
|
|
||||||
export interface Product {
|
export interface Product {
|
||||||
productId: string;
|
productId: string;
|
||||||
productName: string;
|
productName: string;
|
||||||
|
imageUrl?: string;
|
||||||
spec?: string;
|
spec?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
unitPieces: number;
|
|
||||||
unitRolls: number;
|
|
||||||
stockQuantity: string;
|
|
||||||
location?: string;
|
|
||||||
costPrice: string;
|
|
||||||
salesPrice: string;
|
salesPrice: string;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
|
status?: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BoltInfo {
|
||||||
|
boltId: string;
|
||||||
|
panId: string;
|
||||||
|
lengthM: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PanInfo {
|
||||||
|
panId: string;
|
||||||
|
productId: string;
|
||||||
|
name: string;
|
||||||
|
position: string;
|
||||||
|
sortOrder: number;
|
||||||
|
bolts: BoltInfo[];
|
||||||
|
boltCount: number;
|
||||||
|
totalLength: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PanInput {
|
||||||
|
name: string;
|
||||||
|
position?: string;
|
||||||
|
boltLengths?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductTree {
|
||||||
|
product: Product;
|
||||||
|
pans: PanInfo[];
|
||||||
|
totalPanCount: number;
|
||||||
|
totalBoltCount: number;
|
||||||
|
totalLengthM: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductSummaryItem {
|
||||||
|
productName: string;
|
||||||
|
spec: string;
|
||||||
|
colorCount: number;
|
||||||
|
totalPanCount: number;
|
||||||
|
totalBoltCount: number;
|
||||||
|
totalLengthM: string;
|
||||||
|
colors: string;
|
||||||
|
locations: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColorDetailItem {
|
||||||
|
productId: string;
|
||||||
|
productName: string;
|
||||||
|
color: string;
|
||||||
|
spec: string;
|
||||||
|
imageUrl: string;
|
||||||
|
panCount: number;
|
||||||
|
boltCount: number;
|
||||||
|
totalLengthM: string;
|
||||||
|
locations: string;
|
||||||
|
salesPrice: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pan / Bolt dimension view ─────────────────────
|
||||||
|
|
||||||
|
export interface PanListItem {
|
||||||
|
panId: string;
|
||||||
|
productId: string;
|
||||||
|
name: string;
|
||||||
|
position: string;
|
||||||
|
productName: string;
|
||||||
|
colors: string; // aggregated from bolts, e.g. "红色, 蓝色"
|
||||||
|
boltCount: number;
|
||||||
|
totalLengthM: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BoltListItem {
|
||||||
|
boltId: string;
|
||||||
|
panId: string;
|
||||||
|
lengthM: string;
|
||||||
|
color: string; // bolt's own color
|
||||||
|
sortOrder: number;
|
||||||
|
panName: string;
|
||||||
|
position: string;
|
||||||
|
productId: string;
|
||||||
|
productName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Stock ─────────────────────────────────────────
|
||||||
|
|
||||||
export interface StockSummary {
|
export interface StockSummary {
|
||||||
productCount: number;
|
productCount: number;
|
||||||
totalPieces: number;
|
totalPieces: number;
|
||||||
totalRolls: number;
|
totalPans: number;
|
||||||
totalCostValue: string;
|
totalLengthM: string;
|
||||||
totalSalesValue: string;
|
totalSalesValue: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,6 +163,8 @@ export interface StockAdjust {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── System ────────────────────────────────────────
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
userId: string;
|
userId: string;
|
||||||
username: string;
|
username: string;
|
||||||
@ -104,12 +188,19 @@ export interface Role {
|
|||||||
|
|
||||||
export interface Menu {
|
export interface Menu {
|
||||||
menuId: string;
|
menuId: string;
|
||||||
|
parentId: string;
|
||||||
menuName: string;
|
menuName: string;
|
||||||
path?: string;
|
|
||||||
icon?: string;
|
|
||||||
menuType: number;
|
menuType: number;
|
||||||
|
path: string;
|
||||||
|
component: string;
|
||||||
|
permission: string;
|
||||||
|
icon: string;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
visible: number;
|
||||||
|
status: number;
|
||||||
children?: Menu[];
|
children?: Menu[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OperationLog {
|
export interface OperationLog {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user