first init
This commit is contained in:
parent
4aba6541f3
commit
8096f4a848
@ -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' },
|
||||
|
||||
@ -199,7 +199,7 @@ const DashboardPage: React.FC = () => {
|
||||
</div>
|
||||
<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/stats', icon: <RiseOutlined /> },
|
||||
{ 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,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 = (
|
||||
<>
|
||||
<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="备注" />
|
||||
</>
|
||||
);
|
||||
type ViewLevel = 'summary' | 'colors' | 'tree';
|
||||
|
||||
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 [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: '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) => <Tag key={c}>{c}</Tag>) },
|
||||
{ title: '位置', dataIndex: 'locations', search: false, ellipsis: true },
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
render: (_, record) => [
|
||||
<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>,
|
||||
title: '操作', valueType: 'option',
|
||||
render: (_, r) => [
|
||||
<a key="detail" onClick={() => { setSelectedProductName(r.productName); setViewLevel('colors'); }}>查看颜色</a>,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const normalizeValues = (values: Record<string, any>) => ({
|
||||
...values,
|
||||
stockQuantity: String(values.stockQuantity || '0'),
|
||||
costPrice: String(values.costPrice || '0'),
|
||||
salesPrice: String(values.salesPrice || '0'),
|
||||
});
|
||||
// ── Panel 2: Color Detail ──
|
||||
const colorColumns: ProColumns<ColorDetailItem>[] = [
|
||||
{ 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) => [
|
||||
<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 (
|
||||
<>
|
||||
<ProTable<Product>
|
||||
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={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>新增产品</Button>,
|
||||
<Button key="import" icon={<UploadOutlined />} onClick={() => window.location.assign('/inventory/import')}>Excel导入</Button>,
|
||||
<Button key="export" icon={<DownloadOutlined />}>Excel导出</Button>,
|
||||
]}
|
||||
/>
|
||||
{breadcrumb()}
|
||||
|
||||
{viewLevel === 'summary' && (
|
||||
<ProTable<ProductSummaryItem>
|
||||
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={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>新增产品</Button>,
|
||||
<Button key="import" icon={<UploadOutlined />} onClick={() => window.location.assign('/inventory/import')}>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
|
||||
title="新增产品"
|
||||
open={createOpen}
|
||||
onOpenChange={setCreateOpen}
|
||||
onFinish={async (values) => {
|
||||
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}
|
||||
<ProFormText name="productName" label="产品名称" rules={[{ required: true }]} />
|
||||
<ProFormText name="spec" label="规格型号" />
|
||||
<ProFormText name="color" label="颜色" />
|
||||
<ProFormText name="salesPrice" label="销售价" />
|
||||
<ProFormTextArea name="remark" label="备注" />
|
||||
</ModalForm>
|
||||
|
||||
{/* Edit product modal */}
|
||||
<ModalForm
|
||||
title="编辑产品"
|
||||
open={editOpen}
|
||||
onOpenChange={setEditOpen}
|
||||
initialValues={currentRow ?? undefined}
|
||||
initialValues={currentColor ? {
|
||||
productName: currentColor.productName,
|
||||
spec: currentColor.spec,
|
||||
color: currentColor.color,
|
||||
salesPrice: currentColor.salesPrice,
|
||||
} : undefined}
|
||||
onFinish={async (values) => {
|
||||
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}
|
||||
<ProFormText name="productName" label="产品名称" rules={[{ required: true }]} />
|
||||
<ProFormText name="spec" label="规格型号" />
|
||||
<ProFormText name="color" label="颜色" />
|
||||
<ProFormText name="salesPrice" label="销售价" />
|
||||
<ProFormTextArea name="remark" label="备注" />
|
||||
</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: '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 || [],
|
||||
|
||||
@ -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<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) => {
|
||||
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: 2 }}>セイフウ</rt></ruby>
|
||||
</h1>
|
||||
<p style={styles.subtitle}>仓储管理系统</p>
|
||||
<p style={styles.subtitle}>综合管理信息系统</p>
|
||||
</div>
|
||||
|
||||
<Form<LoginParams>
|
||||
@ -88,11 +114,20 @@ const LoginPage: React.FC = () => {
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="tenantId" rules={[{ required: true, message: '请输入租户ID' }]}>
|
||||
<Input
|
||||
prefix={<HomeOutlined style={{ color: '#94A3B8' }} />}
|
||||
placeholder="租户ID(必填)"
|
||||
style={styles.input}
|
||||
<Form.Item name="tenantId" rules={[{ required: true, message: '请选择所属公司' }]}>
|
||||
<Select
|
||||
showSearch
|
||||
filterOption={false}
|
||||
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>
|
||||
|
||||
|
||||
@ -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 { Button, Card, Table, Tag, message, Popconfirm } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Card, Table, Tag, message, Popconfirm, Space } from 'antd';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { listMenus, deleteMenu } from '@/services/api';
|
||||
import type { Menu } from '@/types/api';
|
||||
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 }> = {
|
||||
1: { text: '目录', color: 'blue' },
|
||||
2: { text: '菜单', color: 'green' },
|
||||
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 [data, setData] = useState<Menu[]>([]);
|
||||
const [flatData, setFlatData] = useState<Menu[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
const res = await listMenus();
|
||||
if (res?.code === 0) setData(res.data?.list || []);
|
||||
if (res?.code === 0) setFlatData(res.data?.list || []);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => { fetchData(); }, []);
|
||||
|
||||
const treeData = useMemo(() => buildMenuTree(flatData), [flatData]);
|
||||
const defaultExpandedKeys = useMemo(() => collectKeys(treeData), [treeData]);
|
||||
|
||||
const columns: ColumnsType<Menu> = [
|
||||
{ title: '菜单名称', dataIndex: 'menuName', key: 'menuName' },
|
||||
{ title: '路由', dataIndex: 'path', key: 'path' },
|
||||
{ title: '图标', dataIndex: 'icon', key: 'icon' },
|
||||
{
|
||||
title: '菜单名称',
|
||||
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: '类型',
|
||||
dataIndex: 'menuType',
|
||||
key: 'menuType',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
render: (v: number) => {
|
||||
const t = MENU_TYPE_MAP[v] ?? MENU_TYPE_MAP[1];
|
||||
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: '操作',
|
||||
key: 'action',
|
||||
render: (_, record) => [
|
||||
<a key="edit" style={{ marginRight: 8 }}>编辑</a>,
|
||||
<Popconfirm
|
||||
key="del"
|
||||
title="确认删除?"
|
||||
onConfirm={async () => {
|
||||
const res = await deleteMenu(record.menuId);
|
||||
if (res?.code === 0) { message.success('删除成功'); fetchData(); }
|
||||
}}
|
||||
>
|
||||
<a style={{ color: 'var(--ant-color-error)' }}>删除</a>
|
||||
</Popconfirm>,
|
||||
],
|
||||
width: 120,
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
<a key="edit">编辑</a>
|
||||
<Popconfirm
|
||||
key="del"
|
||||
title="确认删除该菜单?子菜单将一并删除。"
|
||||
onConfirm={async () => {
|
||||
const res = await deleteMenu(record.menuId);
|
||||
if (res?.code === 0) { message.success('删除成功'); fetchData(); }
|
||||
}}
|
||||
>
|
||||
<a style={{ color: 'var(--ant-color-error)' }}>删除</a>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Card bordered={false}>
|
||||
<Button type="primary" icon={<PlusOutlined />} style={{ marginBottom: 16 }}>新增菜单</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} style={{ marginBottom: 16 }}>
|
||||
新增菜单
|
||||
</Button>
|
||||
<Table<Menu>
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
dataSource={treeData}
|
||||
rowKey="menuId"
|
||||
loading={loading}
|
||||
childrenColumnName="children"
|
||||
pagination={false}
|
||||
defaultExpandAllRows
|
||||
expandable={{ defaultExpandedRowKeys: defaultExpandedKeys }}
|
||||
size="middle"
|
||||
indentSize={24}
|
||||
/>
|
||||
</Card>
|
||||
</PageContainer>
|
||||
|
||||
@ -1,23 +1,29 @@
|
||||
import { request } from '@umijs/max';
|
||||
import type {
|
||||
ApiResponse,
|
||||
BoltInfo,
|
||||
ColorDetailItem,
|
||||
CurrentUser,
|
||||
LoginParams,
|
||||
LoginResult,
|
||||
PaginatedData,
|
||||
PaginationParams,
|
||||
Product,
|
||||
StockSummary,
|
||||
StockGroup,
|
||||
StockCheck,
|
||||
StockAdjust,
|
||||
User,
|
||||
Role,
|
||||
Menu,
|
||||
OperationLog,
|
||||
SystemConfig,
|
||||
PaginatedData,
|
||||
PaginationParams,
|
||||
PanInfo,
|
||||
PanInput,
|
||||
Product,
|
||||
ProductSummaryItem,
|
||||
ProductTree,
|
||||
Relation,
|
||||
RelationHistory,
|
||||
Role,
|
||||
StockAdjust,
|
||||
StockCheck,
|
||||
StockGroup,
|
||||
StockSummary,
|
||||
SystemConfig,
|
||||
User,
|
||||
} from '@/types/api';
|
||||
|
||||
// ── Auth ──────────────────────────────────────────
|
||||
@ -100,13 +106,13 @@ export const updateConfig = (key: string, value: string) =>
|
||||
|
||||
// ── 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 });
|
||||
|
||||
export const getProduct = (id: string) =>
|
||||
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 });
|
||||
|
||||
export const updateProduct = (id: string, data: Partial<Product>) =>
|
||||
@ -118,6 +124,55 @@ export const deleteProduct = (id: string) =>
|
||||
export const exportProducts = () =>
|
||||
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 ────────────────────────────────────────
|
||||
|
||||
export const getStockSummary = () =>
|
||||
|
||||
109
src/types/api.ts
109
src/types/api.ts
@ -32,27 +32,109 @@ export interface LoginResult {
|
||||
userInfo: CurrentUser;
|
||||
}
|
||||
|
||||
// ── Product (三层模型) ────────────────────────────
|
||||
|
||||
export interface Product {
|
||||
productId: string;
|
||||
productName: string;
|
||||
imageUrl?: string;
|
||||
spec?: string;
|
||||
color?: string;
|
||||
unitPieces: number;
|
||||
unitRolls: number;
|
||||
stockQuantity: string;
|
||||
location?: string;
|
||||
costPrice: string;
|
||||
salesPrice: string;
|
||||
remark?: string;
|
||||
status?: number;
|
||||
createdAt: 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 {
|
||||
productCount: number;
|
||||
totalPieces: number;
|
||||
totalRolls: number;
|
||||
totalCostValue: string;
|
||||
totalPans: number;
|
||||
totalLengthM: string;
|
||||
totalSalesValue: string;
|
||||
}
|
||||
|
||||
@ -81,6 +163,8 @@ export interface StockAdjust {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// ── System ────────────────────────────────────────
|
||||
|
||||
export interface User {
|
||||
userId: string;
|
||||
username: string;
|
||||
@ -104,12 +188,19 @@ export interface Role {
|
||||
|
||||
export interface Menu {
|
||||
menuId: string;
|
||||
parentId: string;
|
||||
menuName: string;
|
||||
path?: string;
|
||||
icon?: string;
|
||||
menuType: number;
|
||||
path: string;
|
||||
component: string;
|
||||
permission: string;
|
||||
icon: string;
|
||||
sortOrder: number;
|
||||
visible: number;
|
||||
status: number;
|
||||
children?: Menu[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface OperationLog {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user