feat: initial commit for muyu-portal
Umi-based frontend portal with TypeScript config. Made-with: Cursor
This commit is contained in:
commit
0437aa04ed
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.umi/
|
||||||
|
.umi-production/
|
||||||
|
|
||||||
|
*.local
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
106
.umirc.ts
Normal file
106
.umirc.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { defineConfig } from '@umijs/max';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
antd: {},
|
||||||
|
access: {},
|
||||||
|
model: {},
|
||||||
|
initialState: {},
|
||||||
|
request: {},
|
||||||
|
layout: {
|
||||||
|
title: 'muyuqingfeng仓储管理系统',
|
||||||
|
locale: false,
|
||||||
|
},
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8888',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
layout: false,
|
||||||
|
component: './Login',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/dashboard',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '首页',
|
||||||
|
path: '/dashboard',
|
||||||
|
component: './Dashboard',
|
||||||
|
icon: 'DashboardOutlined',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '库存管理',
|
||||||
|
path: '/inventory',
|
||||||
|
icon: 'DatabaseOutlined',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: '产品档案',
|
||||||
|
path: '/inventory/products',
|
||||||
|
component: './Inventory/Products',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '库存查询',
|
||||||
|
path: '/inventory/stocks',
|
||||||
|
component: './Inventory/Stocks',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '库存统计',
|
||||||
|
path: '/inventory/stats',
|
||||||
|
component: './Inventory/Stats',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '库存盘点',
|
||||||
|
path: '/inventory/checks',
|
||||||
|
component: './Inventory/Checks',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '库存调整',
|
||||||
|
path: '/inventory/adjusts',
|
||||||
|
component: './Inventory/Adjusts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Excel导入',
|
||||||
|
path: '/inventory/import',
|
||||||
|
component: './Inventory/Import',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '系统管理',
|
||||||
|
path: '/system',
|
||||||
|
icon: 'SettingOutlined',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: '用户管理',
|
||||||
|
path: '/system/users',
|
||||||
|
component: './System/Users',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '角色管理',
|
||||||
|
path: '/system/roles',
|
||||||
|
component: './System/Roles',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '菜单管理',
|
||||||
|
path: '/system/menus',
|
||||||
|
component: './System/Menus',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '操作日志',
|
||||||
|
path: '/system/logs',
|
||||||
|
component: './System/Logs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '系统配置',
|
||||||
|
path: '/system/configs',
|
||||||
|
component: './System/Configs',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
npmClient: 'pnpm',
|
||||||
|
});
|
||||||
27
package.json
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "muyu-portal",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Muyu WMS Frontend Portal",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "max dev",
|
||||||
|
"build": "max build",
|
||||||
|
"postinstall": "max setup",
|
||||||
|
"lint": "max lint",
|
||||||
|
"format": "prettier --write ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.6.1",
|
||||||
|
"@ant-design/pro-components": "^2.8.7",
|
||||||
|
"@umijs/max": "^4.4.8",
|
||||||
|
"antd": "^5.24.2",
|
||||||
|
"axios": "^1.8.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.18",
|
||||||
|
"@types/react-dom": "^18.3.5",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.12.4+sha512.5ea8b0deed94ed68691c9bad4c955492705c5eeb8a87ef86bc62c74a26b037b08ff9570f108b2e4dbd1dd1a9186fea925e527f141c648e85af45631074680184"
|
||||||
|
}
|
||||||
14039
pnpm-lock.yaml
generated
Normal file
14039
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
115
src/app.ts
Normal file
115
src/app.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { history, RequestConfig, RunTimeLayoutConfig } from '@umijs/max';
|
||||||
|
import { message } from 'antd';
|
||||||
|
|
||||||
|
const loginPath = '/login';
|
||||||
|
let redirectingToLogin = false;
|
||||||
|
|
||||||
|
function clearSessionAndRedirect(tip?: string) {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
if (tip && !redirectingToLogin) {
|
||||||
|
message.warning(tip);
|
||||||
|
}
|
||||||
|
if (history.location.pathname !== loginPath && !redirectingToLogin) {
|
||||||
|
redirectingToLogin = true;
|
||||||
|
history.push(loginPath);
|
||||||
|
setTimeout(() => {
|
||||||
|
redirectingToLogin = false;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getInitialState(): Promise<{
|
||||||
|
currentUser?: any;
|
||||||
|
menuPaths?: string[];
|
||||||
|
}> {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) {
|
||||||
|
if (history.location.pathname !== loginPath) {
|
||||||
|
history.push(loginPath);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/auth/info', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.code === 0 && data.data) {
|
||||||
|
return {
|
||||||
|
currentUser: data.data,
|
||||||
|
menuPaths: data.data.menuPaths || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
clearSessionAndRedirect();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const layout: RunTimeLayoutConfig = (initialState) => {
|
||||||
|
return {
|
||||||
|
logo: '/logo.svg',
|
||||||
|
menu: { locale: false },
|
||||||
|
logout: () => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
history.push(loginPath);
|
||||||
|
},
|
||||||
|
menuDataRender: (menuData: any[]) => {
|
||||||
|
const paths = initialState?.initialState?.menuPaths;
|
||||||
|
if (!paths || paths.length === 0) return menuData;
|
||||||
|
return filterMenuByPaths(menuData, paths);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function filterMenuByPaths(menus: any[], allowedPaths: string[]): any[] {
|
||||||
|
const pathSet = new Set(allowedPaths);
|
||||||
|
return menus
|
||||||
|
.map((menu) => {
|
||||||
|
if (menu.children && menu.children.length > 0) {
|
||||||
|
const filteredChildren = filterMenuByPaths(menu.children, allowedPaths);
|
||||||
|
if (filteredChildren.length > 0) {
|
||||||
|
return { ...menu, children: filteredChildren };
|
||||||
|
}
|
||||||
|
return pathSet.has(menu.path) ? { ...menu, children: [] } : null;
|
||||||
|
}
|
||||||
|
return pathSet.has(menu.path) ? menu : null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const request: RequestConfig = {
|
||||||
|
baseURL: '',
|
||||||
|
timeout: 10000,
|
||||||
|
requestInterceptors: [
|
||||||
|
(config: any) => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
config.headers = {
|
||||||
|
...config.headers,
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
responseInterceptors: [
|
||||||
|
(response: any) => {
|
||||||
|
const { data } = response;
|
||||||
|
if (data?.code === 1003 || data?.code === 1004) {
|
||||||
|
clearSessionAndRedirect('登录状态已失效,请重新登录');
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
errorConfig: {
|
||||||
|
errorHandler: (error: any) => {
|
||||||
|
const status = error?.response?.status;
|
||||||
|
if (status === 401 || status === 403) {
|
||||||
|
clearSessionAndRedirect('登录状态已失效,请重新登录');
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
72
src/pages/Dashboard/index.tsx
Normal file
72
src/pages/Dashboard/index.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { PageContainer, StatisticCard } from '@ant-design/pro-components';
|
||||||
|
import { Col, Row } from 'antd';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { getStockSummary } from '@/services/api';
|
||||||
|
|
||||||
|
const { Statistic } = StatisticCard;
|
||||||
|
|
||||||
|
const DashboardPage: React.FC = () => {
|
||||||
|
const [summary, setSummary] = useState<any>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getStockSummary().then((res) => {
|
||||||
|
if (res?.code === 0) {
|
||||||
|
setSummary(res.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={4}>
|
||||||
|
<StatisticCard
|
||||||
|
statistic={{
|
||||||
|
title: '产品总数',
|
||||||
|
value: summary.productCount || 0,
|
||||||
|
suffix: '种',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={5}>
|
||||||
|
<StatisticCard
|
||||||
|
statistic={{
|
||||||
|
title: '总匹数',
|
||||||
|
value: summary.totalPieces || 0,
|
||||||
|
suffix: '匹',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={5}>
|
||||||
|
<StatisticCard
|
||||||
|
statistic={{
|
||||||
|
title: '总盘数',
|
||||||
|
value: summary.totalRolls || 0,
|
||||||
|
suffix: '盘',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={5}>
|
||||||
|
<StatisticCard
|
||||||
|
statistic={{
|
||||||
|
title: '库存总值(成本)',
|
||||||
|
value: summary.totalCostValue || '0.00',
|
||||||
|
prefix: '¥',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={5}>
|
||||||
|
<StatisticCard
|
||||||
|
statistic={{
|
||||||
|
title: '库存总值(销售)',
|
||||||
|
value: summary.totalSalesValue || '0.00',
|
||||||
|
prefix: '¥',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardPage;
|
||||||
51
src/pages/Inventory/Adjusts/index.tsx
Normal file
51
src/pages/Inventory/Adjusts/index.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { ProColumns, ProTable } from '@ant-design/pro-components';
|
||||||
|
import { Button, Tag } from 'antd';
|
||||||
|
import { listStockAdjusts } from '@/services/api';
|
||||||
|
|
||||||
|
const statusMap: Record<number, { text: string; color: string }> = {
|
||||||
|
0: { text: '待审核', color: 'warning' },
|
||||||
|
1: { text: '已审核', color: 'success' },
|
||||||
|
2: { text: '已驳回', color: 'error' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ProColumns[] = [
|
||||||
|
{ title: '调整单号', dataIndex: 'adjustNo' },
|
||||||
|
{ title: '调整日期', dataIndex: 'adjustDate', valueType: 'date', search: false },
|
||||||
|
{ title: '调整原因', dataIndex: 'adjustReason', search: false },
|
||||||
|
{ title: '操作人', dataIndex: 'operator', search: false },
|
||||||
|
{
|
||||||
|
title: '状态', dataIndex: 'status', search: false,
|
||||||
|
render: (_, record) => {
|
||||||
|
const s = statusMap[record.status] || statusMap[0];
|
||||||
|
return <Tag color={s.color}>{s.text}</Tag>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ title: '创建时间', dataIndex: 'createdAt', valueType: 'dateTime', search: false },
|
||||||
|
{ title: '操作', valueType: 'option', render: () => [<a key="view">查看</a>] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const AdjustsPage: React.FC = () => (
|
||||||
|
<ProTable
|
||||||
|
headerTitle="库存调整"
|
||||||
|
rowKey="adjustId"
|
||||||
|
columns={columns}
|
||||||
|
search={false}
|
||||||
|
request={async (params) => {
|
||||||
|
const res = await listStockAdjusts({
|
||||||
|
page: params.current,
|
||||||
|
pageSize: params.pageSize,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
data: res?.data?.list || [],
|
||||||
|
total: res?.data?.total || 0,
|
||||||
|
success: res?.code === 0,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
toolBarRender={() => [
|
||||||
|
<Button key="add" type="primary" icon={<PlusOutlined />}>创建调整单</Button>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AdjustsPage;
|
||||||
50
src/pages/Inventory/Checks/index.tsx
Normal file
50
src/pages/Inventory/Checks/index.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { ProColumns, ProTable } from '@ant-design/pro-components';
|
||||||
|
import { Button, Tag } from 'antd';
|
||||||
|
import { listStockChecks } from '@/services/api';
|
||||||
|
|
||||||
|
const statusMap: Record<number, { text: string; color: string }> = {
|
||||||
|
0: { text: '草稿', color: 'default' },
|
||||||
|
1: { text: '进行中', color: 'processing' },
|
||||||
|
2: { text: '已完成', color: 'success' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ProColumns[] = [
|
||||||
|
{ title: '盘点单号', dataIndex: 'checkNo' },
|
||||||
|
{ title: '盘点日期', dataIndex: 'checkDate', valueType: 'date', search: false },
|
||||||
|
{ title: '盘点人', dataIndex: 'checker', search: false },
|
||||||
|
{
|
||||||
|
title: '状态', dataIndex: 'status', search: false,
|
||||||
|
render: (_, record) => {
|
||||||
|
const s = statusMap[record.status] || statusMap[0];
|
||||||
|
return <Tag color={s.color}>{s.text}</Tag>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ title: '创建时间', dataIndex: 'createdAt', valueType: 'dateTime', search: false },
|
||||||
|
{ title: '操作', valueType: 'option', render: () => [<a key="view">查看</a>] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ChecksPage: React.FC = () => (
|
||||||
|
<ProTable
|
||||||
|
headerTitle="库存盘点"
|
||||||
|
rowKey="checkId"
|
||||||
|
columns={columns}
|
||||||
|
search={false}
|
||||||
|
request={async (params) => {
|
||||||
|
const res = await listStockChecks({
|
||||||
|
page: params.current,
|
||||||
|
pageSize: params.pageSize,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
data: res?.data?.list || [],
|
||||||
|
total: res?.data?.total || 0,
|
||||||
|
success: res?.code === 0,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
toolBarRender={() => [
|
||||||
|
<Button key="add" type="primary" icon={<PlusOutlined />}>创建盘点单</Button>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ChecksPage;
|
||||||
50
src/pages/Inventory/Import/index.tsx
Normal file
50
src/pages/Inventory/Import/index.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
|
import { InboxOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||||
|
import { Upload, Card, message, Button, Alert } from 'antd';
|
||||||
|
|
||||||
|
const { Dragger } = Upload;
|
||||||
|
|
||||||
|
const ImportPage: React.FC = () => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<Card title="Excel 批量导入">
|
||||||
|
<Alert
|
||||||
|
message="导入说明"
|
||||||
|
description="请按照模板格式准备 Excel 文件,必填字段:产品名称、库存数量。支持 .xlsx 和 .xls 格式。"
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 24 }}
|
||||||
|
/>
|
||||||
|
<Button icon={<DownloadOutlined />} style={{ marginBottom: 16 }}>
|
||||||
|
下载导入模板
|
||||||
|
</Button>
|
||||||
|
<Dragger
|
||||||
|
name="file"
|
||||||
|
action="/api/v1/inventory/products/import"
|
||||||
|
accept=".xlsx,.xls,.csv"
|
||||||
|
headers={{ Authorization: `Bearer ${token}` }}
|
||||||
|
onChange={(info) => {
|
||||||
|
if (info.file.status === 'done') {
|
||||||
|
const res = info.file.response;
|
||||||
|
if (res?.code === 0) {
|
||||||
|
message.success(`导入完成:成功 ${res.data.successCount} 条,失败 ${res.data.failCount} 条`);
|
||||||
|
} else {
|
||||||
|
message.error(res?.msg || '导入失败');
|
||||||
|
}
|
||||||
|
} else if (info.file.status === 'error') {
|
||||||
|
message.error('文件上传失败');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className="ant-upload-drag-icon"><InboxOutlined /></p>
|
||||||
|
<p className="ant-upload-text">点击或拖拽文件到此处上传</p>
|
||||||
|
<p className="ant-upload-hint">支持 .xlsx、.xls、.csv 格式</p>
|
||||||
|
</Dragger>
|
||||||
|
</Card>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportPage;
|
||||||
94
src/pages/Inventory/Products/index.tsx
Normal file
94
src/pages/Inventory/Products/index.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { PlusOutlined, UploadOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||||
|
import { ActionType, ProColumns, ProTable, ModalForm, ProFormText, ProFormDigit, ProFormTextArea } from '@ant-design/pro-components';
|
||||||
|
import { Button, message, Popconfirm } from 'antd';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { listProducts, createProduct, updateProduct, deleteProduct } from '@/services/api';
|
||||||
|
|
||||||
|
const ProductsPage: React.FC = () => {
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
|
const [currentRow, setCurrentRow] = useState<any>(null);
|
||||||
|
|
||||||
|
const columns: 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: '操作', 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: '#ff4d4f' }}>删除</a>
|
||||||
|
</Popconfirm>,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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="备注" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProTable
|
||||||
|
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.href = '/inventory/import'}>Excel导入</Button>,
|
||||||
|
<Button key="export" icon={<DownloadOutlined />}>Excel导出</Button>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<ModalForm title="新增产品" open={createOpen} onOpenChange={setCreateOpen}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
const res = await createProduct({ ...values, stockQuantity: String(values.stockQuantity || '0'), costPrice: String(values.costPrice || '0'), salesPrice: String(values.salesPrice || '0') });
|
||||||
|
if (res?.code === 0) { message.success('创建成功'); actionRef.current?.reload(); return true; }
|
||||||
|
message.error(res?.msg || '创建失败'); return false;
|
||||||
|
}}>
|
||||||
|
{productFormFields}
|
||||||
|
</ModalForm>
|
||||||
|
<ModalForm title="编辑产品" open={editOpen} onOpenChange={setEditOpen} initialValues={currentRow}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
const res = await updateProduct(currentRow.productId, { ...values, stockQuantity: String(values.stockQuantity || '0'), costPrice: String(values.costPrice || '0'), salesPrice: String(values.salesPrice || '0') });
|
||||||
|
if (res?.code === 0) { message.success('更新成功'); actionRef.current?.reload(); return true; }
|
||||||
|
message.error(res?.msg || '更新失败'); return false;
|
||||||
|
}}>
|
||||||
|
{productFormFields}
|
||||||
|
</ModalForm>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductsPage;
|
||||||
60
src/pages/Inventory/Stats/index.tsx
Normal file
60
src/pages/Inventory/Stats/index.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { PageContainer, StatisticCard } from '@ant-design/pro-components';
|
||||||
|
import { Col, Row, Table } from 'antd';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { getStockSummary, getStockByColor, getStockByLocation } from '@/services/api';
|
||||||
|
|
||||||
|
const StatsPage: React.FC = () => {
|
||||||
|
const [summary, setSummary] = useState<any>({});
|
||||||
|
const [colorData, setColorData] = useState<any[]>([]);
|
||||||
|
const [locationData, setLocationData] = useState<any[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getStockSummary().then((res) => res?.code === 0 && setSummary(res.data));
|
||||||
|
getStockByColor().then((res) => res?.code === 0 && setColorData(res.data?.list || []));
|
||||||
|
getStockByLocation().then((res) => res?.code === 0 && setLocationData(res.data?.list || []));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||||
|
<Col span={5}><StatisticCard statistic={{ title: '产品总数', value: summary.productCount || 0 }} /></Col>
|
||||||
|
<Col span={5}><StatisticCard statistic={{ title: '总匹数', value: summary.totalPieces || 0 }} /></Col>
|
||||||
|
<Col span={5}><StatisticCard statistic={{ title: '总盘数', value: summary.totalRolls || 0 }} /></Col>
|
||||||
|
<Col span={5}><StatisticCard statistic={{ title: '库存总值(成本)', value: summary.totalCostValue || '0', prefix: '¥' }} /></Col>
|
||||||
|
<Col span={4}><StatisticCard statistic={{ title: '库存总值(销售)', value: summary.totalSalesValue || '0', prefix: '¥' }} /></Col>
|
||||||
|
</Row>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Table
|
||||||
|
title={() => '按颜色分类统计'}
|
||||||
|
dataSource={colorData}
|
||||||
|
rowKey="name"
|
||||||
|
columns={[
|
||||||
|
{ title: '颜色', dataIndex: 'name' },
|
||||||
|
{ title: '产品数', dataIndex: 'count' },
|
||||||
|
{ title: '库存量', dataIndex: 'quantity' },
|
||||||
|
]}
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Table
|
||||||
|
title={() => '按位置分布统计'}
|
||||||
|
dataSource={locationData}
|
||||||
|
rowKey="name"
|
||||||
|
columns={[
|
||||||
|
{ title: '存放位置', dataIndex: 'name' },
|
||||||
|
{ title: '产品数', dataIndex: 'count' },
|
||||||
|
{ title: '库存量', dataIndex: 'quantity' },
|
||||||
|
]}
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatsPage;
|
||||||
38
src/pages/Inventory/Stocks/index.tsx
Normal file
38
src/pages/Inventory/Stocks/index.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { ProColumns, ProTable } from '@ant-design/pro-components';
|
||||||
|
import { listProducts } from '@/services/api';
|
||||||
|
|
||||||
|
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' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const StocksPage: React.FC = () => (
|
||||||
|
<ProTable
|
||||||
|
headerTitle="库存查询"
|
||||||
|
rowKey="productId"
|
||||||
|
columns={columns}
|
||||||
|
request={async (params) => {
|
||||||
|
const res = await listProducts({
|
||||||
|
page: params.current,
|
||||||
|
pageSize: params.pageSize,
|
||||||
|
productName: params.productName,
|
||||||
|
color: params.color,
|
||||||
|
location: params.location,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
data: res?.data?.list || [],
|
||||||
|
total: res?.data?.total || 0,
|
||||||
|
success: res?.code === 0,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default StocksPage;
|
||||||
64
src/pages/Login/index.tsx
Normal file
64
src/pages/Login/index.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { LoginForm, ProFormText } from '@ant-design/pro-components';
|
||||||
|
import { history, useModel } from '@umijs/max';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { getUserInfo, login } from '@/services/api';
|
||||||
|
|
||||||
|
const LoginPage: React.FC = () => {
|
||||||
|
const { setInitialState } = useModel('@@initialState');
|
||||||
|
|
||||||
|
const handleSubmit = async (values: { username: string; password: string }) => {
|
||||||
|
try {
|
||||||
|
const res = await login(values);
|
||||||
|
if (res.code === 0) {
|
||||||
|
localStorage.setItem('token', res.data.token);
|
||||||
|
|
||||||
|
// Important: load authorized menu paths immediately after login.
|
||||||
|
// Otherwise, first render may temporarily show full menus until refresh.
|
||||||
|
const infoRes = await getUserInfo();
|
||||||
|
if (infoRes?.code === 0 && infoRes.data) {
|
||||||
|
await setInitialState({
|
||||||
|
currentUser: infoRes.data,
|
||||||
|
menuPaths: infoRes.data.menuPaths || [],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await setInitialState({
|
||||||
|
currentUser: res.data.userInfo,
|
||||||
|
menuPaths: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success('登录成功');
|
||||||
|
history.push('/dashboard');
|
||||||
|
} else {
|
||||||
|
message.error(res.msg || '登录失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('网络异常,请稍后重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', background: '#f0f2f5' }}>
|
||||||
|
<LoginForm
|
||||||
|
title="muyuqingfeng仓储管理系统"
|
||||||
|
subTitle="Warehouse Management System"
|
||||||
|
onFinish={handleSubmit}
|
||||||
|
>
|
||||||
|
<ProFormText
|
||||||
|
name="username"
|
||||||
|
fieldProps={{ size: 'large' }}
|
||||||
|
placeholder="用户名"
|
||||||
|
rules={[{ required: true, message: '请输入用户名' }]}
|
||||||
|
/>
|
||||||
|
<ProFormText.Password
|
||||||
|
name="password"
|
||||||
|
fieldProps={{ size: 'large' }}
|
||||||
|
placeholder="密码"
|
||||||
|
rules={[{ required: true, message: '请输入密码' }]}
|
||||||
|
/>
|
||||||
|
</LoginForm>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
63
src/pages/System/Configs/index.tsx
Normal file
63
src/pages/System/Configs/index.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
|
import { Table, Input, Button, message, Card } from 'antd';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { listConfigs, updateConfig } from '@/services/api';
|
||||||
|
|
||||||
|
const ConfigsPage: React.FC = () => {
|
||||||
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [editingKey, setEditingKey] = useState('');
|
||||||
|
const [editValue, setEditValue] = useState('');
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await listConfigs();
|
||||||
|
if (res?.code === 0) setData(res.data?.list || []);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => { fetchData(); }, []);
|
||||||
|
|
||||||
|
const handleSave = async (key: string) => {
|
||||||
|
const res = await updateConfig(key, editValue);
|
||||||
|
if (res?.code === 0) {
|
||||||
|
message.success('更新成功');
|
||||||
|
setEditingKey('');
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: '配置名称', dataIndex: 'configName', key: 'configName' },
|
||||||
|
{ title: '配置键', dataIndex: 'configKey', key: 'configKey' },
|
||||||
|
{
|
||||||
|
title: '配置值', dataIndex: 'configValue', key: 'configValue',
|
||||||
|
render: (v: string, record: any) =>
|
||||||
|
editingKey === record.configKey
|
||||||
|
? <Input value={editValue} onChange={(e) => setEditValue(e.target.value)} style={{ width: 300 }} />
|
||||||
|
: v,
|
||||||
|
},
|
||||||
|
{ title: '分组', dataIndex: 'configGroup', key: 'configGroup' },
|
||||||
|
{ title: '备注', dataIndex: 'remark', key: 'remark', ellipsis: true },
|
||||||
|
{
|
||||||
|
title: '操作', key: 'action',
|
||||||
|
render: (_: any, record: any) =>
|
||||||
|
editingKey === record.configKey
|
||||||
|
? <>
|
||||||
|
<a onClick={() => handleSave(record.configKey)} style={{ marginRight: 8 }}>保存</a>
|
||||||
|
<a onClick={() => setEditingKey('')}>取消</a>
|
||||||
|
</>
|
||||||
|
: <a onClick={() => { setEditingKey(record.configKey); setEditValue(record.configValue); }}>编辑</a>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<Card>
|
||||||
|
<Table columns={columns} dataSource={data} rowKey="configKey" loading={loading} pagination={false} />
|
||||||
|
</Card>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigsPage;
|
||||||
36
src/pages/System/Logs/index.tsx
Normal file
36
src/pages/System/Logs/index.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { ProColumns, ProTable } from '@ant-design/pro-components';
|
||||||
|
import { Tag } from 'antd';
|
||||||
|
import { listLogs } from '@/services/api';
|
||||||
|
|
||||||
|
const columns: ProColumns[] = [
|
||||||
|
{ title: '操作人', dataIndex: 'username' },
|
||||||
|
{ title: '模块', dataIndex: 'module' },
|
||||||
|
{ title: '操作', dataIndex: 'operation' },
|
||||||
|
{ title: '方法', dataIndex: 'method', search: false, render: (v) => <Tag>{v}</Tag> },
|
||||||
|
{ title: '路径', dataIndex: 'path', search: false, ellipsis: true },
|
||||||
|
{ title: 'IP', dataIndex: 'ip', search: false },
|
||||||
|
{ title: '耗时(ms)', dataIndex: 'duration', search: false },
|
||||||
|
{
|
||||||
|
title: '结果', dataIndex: 'status', search: false,
|
||||||
|
render: (_, r) => <Tag color={r.status === 1 ? 'success' : 'error'}>{r.status === 1 ? '成功' : '失败'}</Tag>,
|
||||||
|
},
|
||||||
|
{ title: '操作时间', dataIndex: 'createdAt', valueType: 'dateTime', search: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
const LogsPage: React.FC = () => (
|
||||||
|
<ProTable
|
||||||
|
headerTitle="操作日志"
|
||||||
|
rowKey="logId"
|
||||||
|
columns={columns}
|
||||||
|
request={async (params) => {
|
||||||
|
const res = await listLogs({
|
||||||
|
page: params.current, pageSize: params.pageSize,
|
||||||
|
username: params.username, module: params.module, operation: params.operation,
|
||||||
|
});
|
||||||
|
return { data: res?.data?.list || [], total: res?.data?.total || 0, success: res?.code === 0 };
|
||||||
|
}}
|
||||||
|
search={{ defaultCollapsed: false }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default LogsPage;
|
||||||
67
src/pages/System/Menus/index.tsx
Normal file
67
src/pages/System/Menus/index.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
|
import { Button, Table, Tag, message, Popconfirm } from 'antd';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { listMenus, deleteMenu } from '@/services/api';
|
||||||
|
|
||||||
|
const menuTypeMap: Record<number, { text: string; color: string }> = {
|
||||||
|
1: { text: '目录', color: 'blue' },
|
||||||
|
2: { text: '菜单', color: 'green' },
|
||||||
|
3: { text: '按钮', color: 'orange' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: '菜单名称', dataIndex: 'menuName', key: 'menuName' },
|
||||||
|
{ title: '路由', dataIndex: 'path', key: 'path' },
|
||||||
|
{ title: '图标', dataIndex: 'icon', key: 'icon' },
|
||||||
|
{
|
||||||
|
title: '类型', dataIndex: 'menuType', key: 'menuType',
|
||||||
|
render: (v: number) => {
|
||||||
|
const t = menuTypeMap[v] || menuTypeMap[1];
|
||||||
|
return <Tag color={t.color}>{t.text}</Tag>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder' },
|
||||||
|
{
|
||||||
|
title: '操作', key: 'action',
|
||||||
|
render: (_: any, record: any) => [
|
||||||
|
<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('删除成功');
|
||||||
|
}}>
|
||||||
|
<a style={{ color: '#ff4d4f' }}>删除</a>
|
||||||
|
</Popconfirm>,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const MenusPage: React.FC = () => {
|
||||||
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await listMenus();
|
||||||
|
if (res?.code === 0) setData(res.data?.list || []);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => { fetchData(); }, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<Button type="primary" icon={<PlusOutlined />} style={{ marginBottom: 16 }}>新增菜单</Button>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data}
|
||||||
|
rowKey="menuId"
|
||||||
|
loading={loading}
|
||||||
|
childrenColumnName="children"
|
||||||
|
pagination={false}
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenusPage;
|
||||||
74
src/pages/System/Roles/index.tsx
Normal file
74
src/pages/System/Roles/index.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { ActionType, ProColumns, ProTable, ModalForm, ProFormText, ProFormDigit, ProFormTextArea } from '@ant-design/pro-components';
|
||||||
|
import { Button, message, Popconfirm } from 'antd';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { listRoles, createRole, updateRole, deleteRole } from '@/services/api';
|
||||||
|
|
||||||
|
const RolesPage: React.FC = () => {
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
|
const [currentRow, setCurrentRow] = useState<any>(null);
|
||||||
|
|
||||||
|
const columns: ProColumns[] = [
|
||||||
|
{ title: '角色名称', dataIndex: 'roleName' },
|
||||||
|
{ title: '角色标识', dataIndex: 'roleKey', search: false },
|
||||||
|
{ title: '描述', dataIndex: 'roleDesc', search: false, ellipsis: true },
|
||||||
|
{ title: '排序', dataIndex: 'sortOrder', search: false },
|
||||||
|
{ title: '创建时间', dataIndex: 'createdAt', valueType: 'dateTime', search: false },
|
||||||
|
{
|
||||||
|
title: '操作', valueType: 'option',
|
||||||
|
render: (_, record) => [
|
||||||
|
<a key="edit" onClick={() => { setCurrentRow(record); setEditOpen(true); }}>编辑</a>,
|
||||||
|
<Popconfirm key="del" title="确认删除?" onConfirm={async () => {
|
||||||
|
const res = await deleteRole(record.roleId);
|
||||||
|
if (res?.code === 0) { message.success('删除成功'); actionRef.current?.reload(); }
|
||||||
|
}}>
|
||||||
|
<a style={{ color: '#ff4d4f' }}>删除</a>
|
||||||
|
</Popconfirm>,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProTable
|
||||||
|
headerTitle="角色管理"
|
||||||
|
actionRef={actionRef}
|
||||||
|
rowKey="roleId"
|
||||||
|
columns={columns}
|
||||||
|
request={async (params) => {
|
||||||
|
const res = await listRoles({ page: params.current, pageSize: params.pageSize, roleName: params.roleName });
|
||||||
|
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>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<ModalForm title="新增角色" open={createOpen} onOpenChange={setCreateOpen}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
const res = await createRole(values);
|
||||||
|
if (res?.code === 0) { message.success('创建成功'); actionRef.current?.reload(); return true; }
|
||||||
|
message.error(res?.msg || '创建失败'); return false;
|
||||||
|
}}>
|
||||||
|
<ProFormText name="roleName" label="角色名称" rules={[{ required: true }]} />
|
||||||
|
<ProFormText name="roleKey" label="角色标识" rules={[{ required: true }]} placeholder="如: admin, warehouse, user" />
|
||||||
|
<ProFormTextArea name="roleDesc" label="描述" />
|
||||||
|
<ProFormDigit name="sortOrder" label="排序" initialValue={0} />
|
||||||
|
</ModalForm>
|
||||||
|
<ModalForm title="编辑角色" open={editOpen} onOpenChange={setEditOpen} initialValues={currentRow}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
const res = await updateRole(currentRow.roleId, values);
|
||||||
|
if (res?.code === 0) { message.success('更新成功'); actionRef.current?.reload(); return true; }
|
||||||
|
message.error(res?.msg || '更新失败'); return false;
|
||||||
|
}}>
|
||||||
|
<ProFormText name="roleName" label="角色名称" />
|
||||||
|
<ProFormText name="roleKey" label="角色标识" />
|
||||||
|
<ProFormTextArea name="roleDesc" label="描述" />
|
||||||
|
<ProFormDigit name="sortOrder" label="排序" />
|
||||||
|
</ModalForm>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RolesPage;
|
||||||
120
src/pages/System/Users/index.tsx
Normal file
120
src/pages/System/Users/index.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { ActionType, ProColumns, ProTable, ModalForm, ProFormText, ProFormSelect } from '@ant-design/pro-components';
|
||||||
|
import { Button, message, Popconfirm, Switch } from 'antd';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { listUsers, createUser, updateUser, deleteUser, updateUserStatus, listRoles } from '@/services/api';
|
||||||
|
|
||||||
|
const UsersPage: React.FC = () => {
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
|
const [currentRow, setCurrentRow] = useState<any>(null);
|
||||||
|
const [roles, setRoles] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const fetchRoles = async () => {
|
||||||
|
const res = await listRoles({ page: 1, pageSize: 100 });
|
||||||
|
if (res?.code === 0) {
|
||||||
|
setRoles(res.data?.list?.map((r: any) => ({ label: r.roleName, value: r.roleId })) || []);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ProColumns[] = [
|
||||||
|
{ title: '用户名', dataIndex: 'username' },
|
||||||
|
{ title: '真实姓名', dataIndex: 'realName' },
|
||||||
|
{ title: '手机号', dataIndex: 'phone', search: false },
|
||||||
|
{ title: '邮箱', dataIndex: 'email', search: false },
|
||||||
|
{ title: '角色', dataIndex: 'roleName', search: false },
|
||||||
|
{
|
||||||
|
title: '状态', dataIndex: 'status', search: false,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Switch
|
||||||
|
checked={record.status === 1}
|
||||||
|
onChange={async (checked) => {
|
||||||
|
const res = await updateUserStatus(record.userId, checked ? 1 : 0);
|
||||||
|
if (res?.code === 0) { message.success('状态更新成功'); actionRef.current?.reload(); }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ title: '最后登录', dataIndex: 'lastLoginTime', valueType: 'dateTime', search: false },
|
||||||
|
{
|
||||||
|
title: '操作', valueType: 'option',
|
||||||
|
render: (_, record) => [
|
||||||
|
<a key="edit" onClick={() => { setCurrentRow(record); setEditOpen(true); fetchRoles(); }}>编辑</a>,
|
||||||
|
<Popconfirm key="del" title="确认删除?" onConfirm={async () => {
|
||||||
|
const res = await deleteUser(record.userId);
|
||||||
|
if (res?.code === 0) { message.success('删除成功'); actionRef.current?.reload(); }
|
||||||
|
}}>
|
||||||
|
<a style={{ color: '#ff4d4f' }}>删除</a>
|
||||||
|
</Popconfirm>,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProTable
|
||||||
|
headerTitle="用户管理"
|
||||||
|
actionRef={actionRef}
|
||||||
|
rowKey="userId"
|
||||||
|
columns={columns}
|
||||||
|
request={async (params) => {
|
||||||
|
const res = await listUsers({ page: params.current, pageSize: params.pageSize, username: params.username, realName: params.realName });
|
||||||
|
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); fetchRoles(); }}>
|
||||||
|
新增用户
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModalForm
|
||||||
|
title="新增用户"
|
||||||
|
open={createOpen}
|
||||||
|
onOpenChange={setCreateOpen}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
const res = await createUser(values);
|
||||||
|
if (res?.code === 0) {
|
||||||
|
message.success('创建成功');
|
||||||
|
actionRef.current?.reload();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
message.error(res?.msg || '创建失败');
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProFormText name="username" label="用户名" rules={[{ required: true }]} />
|
||||||
|
<ProFormText.Password name="password" label="密码" rules={[{ required: true, min: 6, message: '密码至少6位' }]} />
|
||||||
|
<ProFormText name="realName" label="真实姓名" rules={[{ required: true }]} />
|
||||||
|
<ProFormText name="phone" label="手机号" />
|
||||||
|
<ProFormText name="email" label="邮箱" />
|
||||||
|
<ProFormSelect name="roleId" label="角色" options={roles} rules={[{ required: true }]} />
|
||||||
|
</ModalForm>
|
||||||
|
|
||||||
|
<ModalForm
|
||||||
|
title="编辑用户"
|
||||||
|
open={editOpen}
|
||||||
|
onOpenChange={setEditOpen}
|
||||||
|
initialValues={currentRow}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
const res = await updateUser(currentRow.userId, values);
|
||||||
|
if (res?.code === 0) {
|
||||||
|
message.success('更新成功');
|
||||||
|
actionRef.current?.reload();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
message.error(res?.msg || '更新失败');
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProFormText name="realName" label="真实姓名" />
|
||||||
|
<ProFormText name="phone" label="手机号" />
|
||||||
|
<ProFormText name="email" label="邮箱" />
|
||||||
|
<ProFormSelect name="roleId" label="角色" options={roles} />
|
||||||
|
</ModalForm>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UsersPage;
|
||||||
177
src/services/api.ts
Normal file
177
src/services/api.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import { request } from '@umijs/max';
|
||||||
|
|
||||||
|
// ==================== Auth ====================
|
||||||
|
|
||||||
|
export async function login(data: { username: string; password: string }) {
|
||||||
|
return request('/api/v1/auth/login', { method: 'POST', data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logout() {
|
||||||
|
return request('/api/v1/auth/logout', { method: 'POST' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserInfo() {
|
||||||
|
return request('/api/v1/auth/info', { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changePassword(data: { oldPassword: string; newPassword: string }) {
|
||||||
|
return request('/api/v1/auth/password', { method: 'PUT', data });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Users ====================
|
||||||
|
|
||||||
|
export async function listUsers(params: any) {
|
||||||
|
return request('/api/v1/system/users', { method: 'GET', params });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUser(id: string) {
|
||||||
|
return request(`/api/v1/system/users/${id}`, { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUser(data: any) {
|
||||||
|
return request('/api/v1/system/users', { method: 'POST', data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUser(id: string, data: any) {
|
||||||
|
return request(`/api/v1/system/users/${id}`, { method: 'PUT', data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteUser(id: string) {
|
||||||
|
return request(`/api/v1/system/users/${id}`, { method: 'DELETE' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUserStatus(id: string, status: number) {
|
||||||
|
return request(`/api/v1/system/users/${id}/status`, { method: 'PUT', data: { status } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Roles ====================
|
||||||
|
|
||||||
|
export async function listRoles(params?: any) {
|
||||||
|
return request('/api/v1/system/roles', { method: 'GET', params });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRole(data: any) {
|
||||||
|
return request('/api/v1/system/roles', { method: 'POST', data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateRole(id: string, data: any) {
|
||||||
|
return request(`/api/v1/system/roles/${id}`, { method: 'PUT', data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteRole(id: string) {
|
||||||
|
return request(`/api/v1/system/roles/${id}`, { method: 'DELETE' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setRolePermissions(id: string, menuIds: string[]) {
|
||||||
|
return request(`/api/v1/system/roles/${id}/permissions`, { method: 'PUT', data: { menuIds } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Menus ====================
|
||||||
|
|
||||||
|
export async function listMenus() {
|
||||||
|
return request('/api/v1/system/menus', { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createMenu(data: any) {
|
||||||
|
return request('/api/v1/system/menus', { method: 'POST', data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateMenu(id: string, data: any) {
|
||||||
|
return request(`/api/v1/system/menus/${id}`, { method: 'PUT', data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteMenu(id: string) {
|
||||||
|
return request(`/api/v1/system/menus/${id}`, { method: 'DELETE' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Logs ====================
|
||||||
|
|
||||||
|
export async function listLogs(params: any) {
|
||||||
|
return request('/api/v1/system/logs', { method: 'GET', params });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Configs ====================
|
||||||
|
|
||||||
|
export async function listConfigs() {
|
||||||
|
return request('/api/v1/system/configs', { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateConfig(key: string, value: string) {
|
||||||
|
return request(`/api/v1/system/configs/${key}`, { method: 'PUT', data: { configValue: value } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Products ====================
|
||||||
|
|
||||||
|
export async function listProducts(params: any) {
|
||||||
|
return request('/api/v1/inventory/products', { method: 'GET', params });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProduct(id: string) {
|
||||||
|
return request(`/api/v1/inventory/products/${id}`, { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createProduct(data: any) {
|
||||||
|
return request('/api/v1/inventory/products', { method: 'POST', data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateProduct(id: string, data: any) {
|
||||||
|
return request(`/api/v1/inventory/products/${id}`, { method: 'PUT', data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteProduct(id: string) {
|
||||||
|
return request(`/api/v1/inventory/products/${id}`, { method: 'DELETE' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportProducts() {
|
||||||
|
return request('/api/v1/inventory/products/export', { method: 'GET', responseType: 'blob' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Stocks ====================
|
||||||
|
|
||||||
|
export async function getStockSummary() {
|
||||||
|
return request('/api/v1/inventory/stocks/summary', { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStockByColor() {
|
||||||
|
return request('/api/v1/inventory/stocks/by-color', { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStockByLocation() {
|
||||||
|
return request('/api/v1/inventory/stocks/by-location', { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Stock Checks ====================
|
||||||
|
|
||||||
|
export async function listStockChecks(params: any) {
|
||||||
|
return request('/api/v1/inventory/checks', { method: 'GET', params });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStockCheck(id: string) {
|
||||||
|
return request(`/api/v1/inventory/checks/${id}`, { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createStockCheck(data: any) {
|
||||||
|
return request('/api/v1/inventory/checks', { method: 'POST', data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function confirmStockCheck(id: string) {
|
||||||
|
return request(`/api/v1/inventory/checks/${id}/confirm`, { method: 'POST' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Stock Adjusts ====================
|
||||||
|
|
||||||
|
export async function listStockAdjusts(params: any) {
|
||||||
|
return request('/api/v1/inventory/adjusts', { method: 'GET', params });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStockAdjust(id: string) {
|
||||||
|
return request(`/api/v1/inventory/adjusts/${id}`, { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createStockAdjust(data: any) {
|
||||||
|
return request('/api/v1/inventory/adjusts', { method: 'POST', data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function approveStockAdjust(id: string, action: number) {
|
||||||
|
return request(`/api/v1/inventory/adjusts/${id}/approve`, { method: 'POST', data: { action } });
|
||||||
|
}
|
||||||
3
tsconfig.json
Normal file
3
tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "./src/.umi/tsconfig.json"
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user