可正常启用
This commit is contained in:
parent
1582caf776
commit
c733f0c1d8
@ -2,9 +2,9 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>管理系统</title>
|
<title>Deploy Ease Platform</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
8
frontend/public/favicon.svg
Normal file
8
frontend/public/favicon.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<title>Deploy Ease</title>
|
||||||
|
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<circle fill="#1890FF" cx="16" cy="16" r="16"/>
|
||||||
|
<path d="M16,8 L24,16 L20,16 L20,24 L12,24 L12,16 L8,16 L16,8 Z" fill="#FFFFFF"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 405 B |
@ -1,8 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
|
import { ConfigProvider } from 'antd';
|
||||||
|
import zhCN from 'antd/locale/zh_CN';
|
||||||
|
import router from './router';
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
return <Outlet />;
|
return (
|
||||||
|
<ConfigProvider locale={zhCN}>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Modal, Input, Space } from 'antd';
|
import { Modal, Input, Space } from 'antd';
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
import * as AntdIcons from '@ant-design/icons';
|
|
||||||
import styles from './index.module.css';
|
import styles from './index.module.css';
|
||||||
|
import { getAvailableIcons } from '@/config/icons.tsx';
|
||||||
|
|
||||||
interface IconSelectProps {
|
interface IconSelectProps {
|
||||||
value?: string;
|
value?: string;
|
||||||
@ -21,16 +21,8 @@ const IconSelect: React.FC<IconSelectProps> = ({
|
|||||||
|
|
||||||
// 获取所有图标
|
// 获取所有图标
|
||||||
const iconList = React.useMemo(() => {
|
const iconList = React.useMemo(() => {
|
||||||
return Object.keys(AntdIcons)
|
const icons = getAvailableIcons();
|
||||||
.filter(key => key.endsWith('Outlined'))
|
return icons.filter(icon =>
|
||||||
.map(key => {
|
|
||||||
const IconComponent = AntdIcons[key as keyof typeof AntdIcons] as any;
|
|
||||||
return {
|
|
||||||
name: key.replace('Outlined', '').toLowerCase(),
|
|
||||||
component: IconComponent
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter(icon =>
|
|
||||||
search ? icon.name.toLowerCase().includes(search.toLowerCase()) : true
|
search ? icon.name.toLowerCase().includes(search.toLowerCase()) : true
|
||||||
);
|
);
|
||||||
}, [search]);
|
}, [search]);
|
||||||
@ -63,7 +55,9 @@ const IconSelect: React.FC<IconSelectProps> = ({
|
|||||||
onClick={() => handleSelect(name)}
|
onClick={() => handleSelect(name)}
|
||||||
>
|
>
|
||||||
{React.createElement(Icon)}
|
{React.createElement(Icon)}
|
||||||
<div className={styles.iconName}>{name}</div>
|
<div className={styles.iconName}>
|
||||||
|
{name.replace('Outlined', '')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
41
frontend/src/config/icons.ts
Normal file
41
frontend/src/config/icons.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import * as AntdIcons from '@ant-design/icons';
|
||||||
|
|
||||||
|
// 图标名称映射配置
|
||||||
|
export const iconMap: Record<string, string> = {
|
||||||
|
'setting': 'SettingOutlined',
|
||||||
|
'user': 'UserOutlined',
|
||||||
|
'tree-table': 'TableOutlined',
|
||||||
|
'tree': 'ApartmentOutlined',
|
||||||
|
'api': 'ApiOutlined',
|
||||||
|
'menu': 'MenuOutlined',
|
||||||
|
'department': 'TeamOutlined',
|
||||||
|
'role': 'UserSwitchOutlined',
|
||||||
|
'external': 'ApiOutlined',
|
||||||
|
'system': 'SettingOutlined'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取图标组件的通用函数
|
||||||
|
export const getIconComponent = (iconName: string | undefined) => {
|
||||||
|
if (!iconName) return null;
|
||||||
|
|
||||||
|
// 如果在映射中存在,使用映射的名称
|
||||||
|
const mappedName = iconMap[iconName] || iconName;
|
||||||
|
|
||||||
|
// 确保首字母大写并添加Outlined后缀(如果需要)
|
||||||
|
const iconKey = mappedName.endsWith('Outlined')
|
||||||
|
? mappedName.charAt(0).toUpperCase() + mappedName.slice(1)
|
||||||
|
: `${mappedName.charAt(0).toUpperCase() + mappedName.slice(1)}Outlined`;
|
||||||
|
|
||||||
|
const Icon = (AntdIcons as any)[iconKey];
|
||||||
|
return Icon ? <Icon/> : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取所有可用的图标列表
|
||||||
|
export const getAvailableIcons = () => {
|
||||||
|
return Object.keys(AntdIcons)
|
||||||
|
.filter(key => key.endsWith('Outlined'))
|
||||||
|
.map(key => ({
|
||||||
|
name: key,
|
||||||
|
component: (AntdIcons as any)[key]
|
||||||
|
}));
|
||||||
|
};
|
||||||
43
frontend/src/config/icons.tsx
Normal file
43
frontend/src/config/icons.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import * as AntdIcons from '@ant-design/icons';
|
||||||
|
|
||||||
|
// 图标名称映射配置
|
||||||
|
export const iconMap: Record<string, string> = {
|
||||||
|
'setting': 'SettingOutlined',
|
||||||
|
'user': 'UserOutlined',
|
||||||
|
'tree-table': 'TableOutlined',
|
||||||
|
'tree': 'ApartmentOutlined',
|
||||||
|
'api': 'ApiOutlined',
|
||||||
|
'menu': 'MenuOutlined',
|
||||||
|
'department': 'TeamOutlined',
|
||||||
|
'role': 'UserSwitchOutlined',
|
||||||
|
'external': 'ApiOutlined',
|
||||||
|
'system': 'SettingOutlined',
|
||||||
|
'dashboard': 'DashboardOutlined'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取图标组件的通用函数
|
||||||
|
export const getIconComponent = (iconName: string | undefined) => {
|
||||||
|
if (!iconName) return null;
|
||||||
|
|
||||||
|
// 如果在映射中存在,使用映射的名称
|
||||||
|
const mappedName = iconMap[iconName] || iconName;
|
||||||
|
|
||||||
|
// 确保首字母大写并添加Outlined后缀(如果需要)
|
||||||
|
const iconKey = mappedName.endsWith('Outlined')
|
||||||
|
? mappedName.charAt(0).toUpperCase() + mappedName.slice(1)
|
||||||
|
: `${mappedName.charAt(0).toUpperCase() + mappedName.slice(1)}Outlined`;
|
||||||
|
|
||||||
|
const Icon = (AntdIcons as any)[iconKey];
|
||||||
|
return Icon ? React.createElement(Icon) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取所有可用的图标列表
|
||||||
|
export const getAvailableIcons = () => {
|
||||||
|
return Object.keys(AntdIcons)
|
||||||
|
.filter(key => key.endsWith('Outlined'))
|
||||||
|
.map(key => ({
|
||||||
|
name: key,
|
||||||
|
component: (AntdIcons as any)[key]
|
||||||
|
}));
|
||||||
|
};
|
||||||
@ -19,6 +19,7 @@ import type {RootState} from '../store';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
import {MenuResponse, MenuTypeEnum} from "@/pages/System/Menu/types";
|
import {MenuResponse, MenuTypeEnum} from "@/pages/System/Menu/types";
|
||||||
|
import { getIconComponent } from '@/config/icons.tsx';
|
||||||
|
|
||||||
const {Header, Content, Sider} = Layout;
|
const {Header, Content, Sider} = Layout;
|
||||||
const {confirm} = Modal;
|
const {confirm} = Modal;
|
||||||
@ -26,7 +27,6 @@ const {confirm} = Modal;
|
|||||||
// 设置中文语言
|
// 设置中文语言
|
||||||
dayjs.locale('zh-cn');
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
|
|
||||||
const BasicLayout: React.FC = () => {
|
const BasicLayout: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -36,7 +36,32 @@ const BasicLayout: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [currentTime, setCurrentTime] = useState(dayjs());
|
const [currentTime, setCurrentTime] = useState(dayjs());
|
||||||
const [weather, setWeather] = useState({temp: '--', weather: '未知', city: '未知'});
|
const [weather, setWeather] = useState({temp: '--', weather: '未知', city: '未知'});
|
||||||
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
|
||||||
|
// 根据当前路径获取需要展开的父级菜单
|
||||||
|
const getDefaultOpenKeys = () => {
|
||||||
|
const pathSegments = location.pathname.split('/').filter(Boolean);
|
||||||
|
const openKeys: string[] = [];
|
||||||
|
let currentPath = '';
|
||||||
|
|
||||||
|
pathSegments.forEach(segment => {
|
||||||
|
currentPath += `/${segment}`;
|
||||||
|
openKeys.push(currentPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
return openKeys;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置默认展开的菜单
|
||||||
|
const [openKeys, setOpenKeys] = useState<string[]>(getDefaultOpenKeys());
|
||||||
|
|
||||||
|
// 当路由变化时,自动展开对应的父级菜单
|
||||||
|
useEffect(() => {
|
||||||
|
const newOpenKeys = getDefaultOpenKeys();
|
||||||
|
setOpenKeys(prevKeys => {
|
||||||
|
const mergedKeys = [...new Set([...prevKeys, ...newOpenKeys])];
|
||||||
|
return mergedKeys;
|
||||||
|
});
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
// 将天气获取逻辑提取为useCallback
|
// 将天气获取逻辑提取为useCallback
|
||||||
const fetchWeather = useCallback(async () => {
|
const fetchWeather = useCallback(async () => {
|
||||||
@ -126,26 +151,27 @@ const BasicLayout: React.FC = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// 获取图标组件
|
// 获取图标组件
|
||||||
const getIcon = (iconName: string | undefined) => {
|
const getIcon = getIconComponent;
|
||||||
if (!iconName) return null;
|
|
||||||
const iconKey = `${iconName.charAt(0).toUpperCase() + iconName.slice(1)}Outlined`;
|
|
||||||
const Icon = (AntdIcons as any)[iconKey];
|
|
||||||
return Icon ? <Icon/> : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 将菜单数据转换为antd Menu需要的格式
|
// 将菜单数据转换为antd Menu需要的格式
|
||||||
const getMenuItems = (menuList: MenuResponse[]): MenuProps['items'] => {
|
const getMenuItems = (menuList: MenuResponse[]): MenuProps['items'] => {
|
||||||
return menuList
|
return menuList
|
||||||
?.map(menu => {
|
?.filter(menu => menu.type !== MenuTypeEnum.BUTTON && !menu.hidden) // 过滤掉按钮类型和隐藏的菜单
|
||||||
|
.map(menu => {
|
||||||
// 确保path存在,否则使用id
|
// 确保path存在,否则使用id
|
||||||
const key = menu.path || `menu-${menu.id}`;
|
const key = menu.path || `menu-${menu.id}`;
|
||||||
|
|
||||||
|
// 如果有子菜单,递归处理子菜单
|
||||||
|
const children = menu.children && menu.children.length > 0
|
||||||
|
? getMenuItems(menu.children.filter(child =>
|
||||||
|
child.type !== MenuTypeEnum.BUTTON && !child.hidden))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
icon: getIcon(menu.icon),
|
icon: getIcon(menu.icon),
|
||||||
label: menu.name,
|
label: menu.name,
|
||||||
children: menu.children && menu.children.length > 0
|
children: children
|
||||||
? getMenuItems(menu.children)
|
|
||||||
: undefined
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -181,6 +207,9 @@ const BasicLayout: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Current location:', location.pathname);
|
||||||
|
console.log('Current menus:', menus);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{minHeight: '100vh'}}>
|
<Layout style={{minHeight: '100vh'}}>
|
||||||
<Sider>
|
<Sider>
|
||||||
@ -254,8 +283,8 @@ const BasicLayout: React.FC = () => {
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
</Space>
|
</Space>
|
||||||
</Header>
|
</Header>
|
||||||
<Content style={{margin: '24px 16px', padding: 24, background: '#fff', minHeight: 280}}>
|
<Content style={{ margin: '24px 16px', padding: 24, background: '#fff', minHeight: 280 }}>
|
||||||
<Outlet/>
|
<Outlet />
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
110
frontend/src/pages/System/External/index.tsx
vendored
Normal file
110
frontend/src/pages/System/External/index.tsx
vendored
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, Table, Button, Space, Modal, Form, Input, message } from 'antd';
|
||||||
|
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
|
|
||||||
|
interface ExternalSystem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
description?: string;
|
||||||
|
createTime: string;
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExternalPage: React.FC = () => {
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const [data, setData] = React.useState<ExternalSystem[]>([]);
|
||||||
|
|
||||||
|
const columns: ColumnsType<ExternalSystem> = [
|
||||||
|
{
|
||||||
|
title: '系统名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '系统地址',
|
||||||
|
dataIndex: 'url',
|
||||||
|
key: 'url',
|
||||||
|
width: 300,
|
||||||
|
render: (url: string) => (
|
||||||
|
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||||
|
{url}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '描述',
|
||||||
|
dataIndex: 'description',
|
||||||
|
key: 'description',
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createTime',
|
||||||
|
key: 'createTime',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '更新时间',
|
||||||
|
dataIndex: 'updateTime',
|
||||||
|
key: 'updateTime',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 180,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => message.info('编辑功能开发中')}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => message.info('删除功能开发中')}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Card>
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => message.info('新增功能开发中')}
|
||||||
|
>
|
||||||
|
新增系统
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data}
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
pagination={{
|
||||||
|
total: data.length,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExternalPage;
|
||||||
@ -3,15 +3,20 @@ import { Table, Button, Modal, Form, Input, Space, Switch, Select, TreeSelect, T
|
|||||||
import { PlusOutlined, EditOutlined, DeleteOutlined, QuestionCircleOutlined, FolderOutlined, MenuOutlined, ToolOutlined, CaretRightOutlined } from '@ant-design/icons';
|
import { PlusOutlined, EditOutlined, DeleteOutlined, QuestionCircleOutlined, FolderOutlined, MenuOutlined, ToolOutlined, CaretRightOutlined } from '@ant-design/icons';
|
||||||
import type { TablePaginationConfig } from 'antd/es/table';
|
import type { TablePaginationConfig } from 'antd/es/table';
|
||||||
import type { FilterValue, SorterResult } from 'antd/es/table/interface';
|
import type { FilterValue, SorterResult } from 'antd/es/table/interface';
|
||||||
import { getMenuTree } from './service';
|
import { getMenuTree, getCurrentUserMenus } from './service';
|
||||||
import type { MenuResponse } from './types';
|
import type { MenuResponse } from './types';
|
||||||
import { MenuTypeEnum } from './types';
|
import { MenuTypeEnum } from './types';
|
||||||
import IconSelect from '@/components/IconSelect';
|
import IconSelect from '@/components/IconSelect';
|
||||||
import { useTableData } from '@/hooks/useTableData';
|
import { useTableData } from '@/hooks/useTableData';
|
||||||
import * as AntdIcons from '@ant-design/icons';
|
import * as AntdIcons from '@ant-design/icons';
|
||||||
import {FixedType} from "rc-table/lib/interface";
|
import {FixedType} from "rc-table/lib/interface";
|
||||||
|
import { getIconComponent } from '@/config/icons.tsx';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { setMenus } from '@/store/userSlice';
|
||||||
|
import { message } from 'antd';
|
||||||
|
|
||||||
const MenuPage: React.FC = () => {
|
const MenuPage: React.FC = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
const {
|
const {
|
||||||
list: menus,
|
list: menus,
|
||||||
loading,
|
loading,
|
||||||
@ -111,6 +116,15 @@ const MenuPage: React.FC = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateReduxMenus = async () => {
|
||||||
|
try {
|
||||||
|
const menus = await getCurrentUserMenus();
|
||||||
|
dispatch(setMenus(menus));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新菜单数据失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
@ -122,12 +136,16 @@ const MenuPage: React.FC = () => {
|
|||||||
if (success) {
|
if (success) {
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
fetchMenus();
|
fetchMenus();
|
||||||
|
await updateReduxMenus();
|
||||||
|
message.success('更新成功');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const success = await handleCreate(values);
|
const success = await handleCreate(values);
|
||||||
if (success) {
|
if (success) {
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
fetchMenus();
|
fetchMenus();
|
||||||
|
await updateReduxMenus();
|
||||||
|
message.success('创建成功');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -135,13 +153,21 @@ const MenuPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIcon = (iconName: string | undefined) => {
|
const handleMenuDelete = async (id: number) => {
|
||||||
if (!iconName) return null;
|
try {
|
||||||
const iconKey = `${iconName.charAt(0).toUpperCase() + iconName.slice(1)}Outlined`;
|
const success = await handleDelete(id);
|
||||||
const Icon = (AntdIcons as any)[iconKey];
|
if (success) {
|
||||||
return Icon ? <Icon /> : null;
|
fetchMenus();
|
||||||
|
await updateReduxMenus();
|
||||||
|
message.success('删除成功');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getIcon = getIconComponent;
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: '菜单名称',
|
title: '菜单名称',
|
||||||
@ -229,7 +255,7 @@ const MenuPage: React.FC = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
danger
|
danger
|
||||||
icon={<DeleteOutlined/>}
|
icon={<DeleteOutlined/>}
|
||||||
onClick={() => handleDelete(record.id)}
|
onClick={() => handleMenuDelete(record.id)}
|
||||||
disabled={record.children?.length > 0}
|
disabled={record.children?.length > 0}
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
|
|||||||
@ -13,9 +13,48 @@ export const getMenuTree = () =>
|
|||||||
request.get<MenuResponse[]>(`${BASE_URL}/tree`);
|
request.get<MenuResponse[]>(`${BASE_URL}/tree`);
|
||||||
|
|
||||||
// 获取当前用户菜单
|
// 获取当前用户菜单
|
||||||
export const getCurrentUserMenus = () =>
|
export const getCurrentUserMenus = async () => {
|
||||||
request.get<MenuResponse[]>(`${BASE_URL}/current`);
|
const menus = await request.get<MenuResponse[]>(`${BASE_URL}/current`);
|
||||||
|
// 添加首页路由
|
||||||
|
const dashboard: MenuResponse = {
|
||||||
|
id: 0,
|
||||||
|
createTime: new Date().toISOString(),
|
||||||
|
updateTime: new Date().toISOString(),
|
||||||
|
version: 0,
|
||||||
|
deleted: false,
|
||||||
|
name: "首页",
|
||||||
|
path: "/dashboard",
|
||||||
|
component: "/Dashboard/index",
|
||||||
|
icon: "dashboard",
|
||||||
|
type: 2,
|
||||||
|
parentId: 0,
|
||||||
|
sort: 0,
|
||||||
|
hidden: false,
|
||||||
|
enabled: true,
|
||||||
|
createBy: "system",
|
||||||
|
updateBy: "system"
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理组件路径格式
|
||||||
|
const processMenu = (menu: MenuResponse): MenuResponse => {
|
||||||
|
const processed = { ...menu };
|
||||||
|
|
||||||
|
// 确保组件路径格式正确
|
||||||
|
if (processed.component && !processed.component.startsWith('/')) {
|
||||||
|
processed.component = `/${processed.component}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归处理子菜单
|
||||||
|
if (processed.children) {
|
||||||
|
processed.children = processed.children.map(processMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
return processed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processedMenus = menus.map(processMenu);
|
||||||
|
return [dashboard, ...processedMenus];
|
||||||
|
};
|
||||||
|
|
||||||
// 创建菜单
|
// 创建菜单
|
||||||
export const createMenu = (data: MenuRequest) =>
|
export const createMenu = (data: MenuRequest) =>
|
||||||
|
|||||||
84
frontend/src/pages/System/Role/components/AssignTagModal.tsx
Normal file
84
frontend/src/pages/System/Role/components/AssignTagModal.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Modal, Select, message, Spin } from 'antd';
|
||||||
|
import type { RoleTagResponse } from '../types';
|
||||||
|
import { getAllTags, assignTags } from '../service';
|
||||||
|
|
||||||
|
interface AssignTagModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
roleId: number;
|
||||||
|
onCancel: () => void;
|
||||||
|
onSuccess: () => void;
|
||||||
|
selectedTags?: RoleTagResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const AssignTagModal: React.FC<AssignTagModalProps> = ({
|
||||||
|
visible,
|
||||||
|
roleId,
|
||||||
|
onCancel,
|
||||||
|
onSuccess,
|
||||||
|
selectedTags = []
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [allTags, setAllTags] = useState<RoleTagResponse[]>([]);
|
||||||
|
const [selectedTagIds, setSelectedTagIds] = useState<number[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
loadTags();
|
||||||
|
setSelectedTagIds(selectedTags.map(tag => tag.id));
|
||||||
|
}
|
||||||
|
}, [visible, selectedTags]);
|
||||||
|
|
||||||
|
const loadTags = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const tags = await getAllTags();
|
||||||
|
setAllTags(tags);
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取标签列表失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
try {
|
||||||
|
await assignTags(roleId, selectedTagIds);
|
||||||
|
message.success('标签分配成功');
|
||||||
|
onSuccess();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('标签分配失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="分配标签"
|
||||||
|
open={visible}
|
||||||
|
onCancel={onCancel}
|
||||||
|
onOk={handleOk}
|
||||||
|
width={500}
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
padding: '12px'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="请选择标签"
|
||||||
|
value={selectedTagIds}
|
||||||
|
onChange={setSelectedTagIds}
|
||||||
|
options={allTags.map(tag => ({
|
||||||
|
label: tag.name,
|
||||||
|
value: tag.id
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Spin>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AssignTagModal;
|
||||||
@ -39,44 +39,50 @@ const PermissionModal: React.FC<PermissionModalProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [treeData, setTreeData] = useState<DataNode[]>([]);
|
const [treeData, setTreeData] = useState<DataNode[]>([]);
|
||||||
const [checkedKeys, setCheckedKeys] = useState<number[]>(defaultCheckedKeys);
|
const [checkedKeys, setCheckedKeys] = useState<number[]>([]);
|
||||||
|
const [idMapping, setIdMapping] = useState<Record<string, number>>({});
|
||||||
|
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
|
||||||
|
|
||||||
// 将菜单和权限数据转换为Tree组件需要的格式
|
// 将菜单和权限数据转换为Tree组件需要的格式
|
||||||
const convertToTreeData = (menuList: MenuItem[]): { treeData: DataNode[], idMapping: Record<string, number> } => {
|
const convertToTreeData = (menuList: MenuItem[]): { treeData: DataNode[], idMapping: Record<string, number>, expandedKeys: string[] } => {
|
||||||
const idMapping: Record<string, number> = {};
|
const mapping: Record<string, number> = {};
|
||||||
|
const expanded: string[] = [];
|
||||||
|
|
||||||
const convertMenuToNode = (menu: MenuItem): DataNode => {
|
const convertMenuToNode = (menu: MenuItem): DataNode => {
|
||||||
const node: DataNode = {
|
const children: DataNode[] = [];
|
||||||
key: `menu-${menu.id}`,
|
const menuKey = `menu-${menu.id}`;
|
||||||
title: menu.name,
|
expanded.push(menuKey);
|
||||||
children: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加功能权限节点
|
// 添加功能权限节点
|
||||||
if (menu.permissions?.length > 0) {
|
if (menu.permissions?.length > 0) {
|
||||||
const permissionNodes = menu.permissions.map(perm => {
|
menu.permissions.forEach(perm => {
|
||||||
const key = `permission-${perm.id}`;
|
const key = `permission-${perm.id}`;
|
||||||
idMapping[key] = perm.id;
|
mapping[key] = perm.id;
|
||||||
return {
|
children.push({
|
||||||
key,
|
key,
|
||||||
title: perm.name,
|
title: perm.name,
|
||||||
isLeaf: true
|
isLeaf: true
|
||||||
};
|
|
||||||
});
|
});
|
||||||
node.children!.push(...permissionNodes);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 递归处理子菜单
|
// 递归处理子菜单
|
||||||
if (menu.permissionChildren?.length > 0) {
|
if (menu.permissionChildren?.length > 0) {
|
||||||
const childNodes = menu.permissionChildren.map(convertMenuToNode);
|
menu.permissionChildren.forEach(child => {
|
||||||
node.children!.push(...childNodes);
|
children.push(convertMenuToNode(child));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return node;
|
return {
|
||||||
|
key: menuKey,
|
||||||
|
title: menu.name,
|
||||||
|
children: children.length > 0 ? children : undefined,
|
||||||
|
selectable: false // 菜单节点不可选择,只能展开/收缩
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const treeData = menuList.map(convertMenuToNode);
|
const treeData = menuList.map(convertMenuToNode);
|
||||||
return { treeData, idMapping };
|
return { treeData, idMapping: mapping, expandedKeys: expanded };
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -86,16 +92,18 @@ const PermissionModal: React.FC<PermissionModalProps> = ({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await getPermissionTree();
|
const response = await getPermissionTree();
|
||||||
const { treeData, idMapping } = convertToTreeData(response);
|
const { treeData: newTreeData, idMapping: newIdMapping, expandedKeys: newExpandedKeys } = convertToTreeData(response);
|
||||||
setTreeData(treeData);
|
setTreeData(newTreeData);
|
||||||
|
setIdMapping(newIdMapping);
|
||||||
// 将默认选中的ID转换为Tree需要的key
|
setExpandedKeys(newExpandedKeys);
|
||||||
const defaultKeys = defaultCheckedKeys.map(id => {
|
|
||||||
const key = Object.entries(idMapping).find(([_, value]) => value === id)?.[0];
|
|
||||||
return key;
|
|
||||||
}).filter(Boolean);
|
|
||||||
|
|
||||||
|
// 设置选中的权限
|
||||||
setCheckedKeys(defaultCheckedKeys);
|
setCheckedKeys(defaultCheckedKeys);
|
||||||
|
|
||||||
|
console.log('权限树数据:', response);
|
||||||
|
console.log('转换后的树数据:', newTreeData);
|
||||||
|
console.log('ID映射:', newIdMapping);
|
||||||
|
console.log('默认选中的权限:', defaultCheckedKeys);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取权限树失败:', error);
|
console.error('获取权限树失败:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@ -113,22 +121,28 @@ const PermissionModal: React.FC<PermissionModalProps> = ({
|
|||||||
checkedKeys.forEach(key => {
|
checkedKeys.forEach(key => {
|
||||||
const keyStr = key.toString();
|
const keyStr = key.toString();
|
||||||
if (keyStr.startsWith('permission-')) {
|
if (keyStr.startsWith('permission-')) {
|
||||||
const id = parseInt(keyStr.replace('permission-', ''));
|
const id = idMapping[keyStr];
|
||||||
if (!isNaN(id)) {
|
if (id) {
|
||||||
permissionIds.push(id);
|
permissionIds.push(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('选中的权限ID:', permissionIds);
|
||||||
setCheckedKeys(permissionIds);
|
setCheckedKeys(permissionIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExpand = (newExpandedKeys: Key[]) => {
|
||||||
|
setExpandedKeys(newExpandedKeys);
|
||||||
|
};
|
||||||
|
|
||||||
const handleOk = () => {
|
const handleOk = () => {
|
||||||
onOk(checkedKeys);
|
onOk(checkedKeys);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 将权限ID转换为Tree需要的key
|
// 将权限ID转换为Tree需要的key
|
||||||
const getTreeCheckedKeys = (): string[] => {
|
const getTreeCheckedKeys = (): string[] => {
|
||||||
|
console.log('当前选中的权限:', checkedKeys);
|
||||||
return checkedKeys.map(id => `permission-${id}`);
|
return checkedKeys.map(id => `permission-${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -139,15 +153,23 @@ const PermissionModal: React.FC<PermissionModalProps> = ({
|
|||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
onOk={handleOk}
|
onOk={handleOk}
|
||||||
width={600}
|
width={600}
|
||||||
bodyStyle={{ maxHeight: '60vh', overflow: 'auto' }}
|
styles={{
|
||||||
|
body: {
|
||||||
|
padding: '12px',
|
||||||
|
maxHeight: 'calc(100vh - 250px)',
|
||||||
|
overflow: 'auto'
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<Tree
|
<Tree
|
||||||
checkable
|
checkable
|
||||||
checkedKeys={getTreeCheckedKeys()}
|
checkedKeys={getTreeCheckedKeys()}
|
||||||
|
expandedKeys={expandedKeys}
|
||||||
|
onExpand={handleExpand}
|
||||||
onCheck={handleCheck}
|
onCheck={handleCheck}
|
||||||
treeData={treeData}
|
treeData={treeData}
|
||||||
defaultExpandAll
|
autoExpandParent={false}
|
||||||
/>
|
/>
|
||||||
</Spin>
|
</Spin>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@ -1,154 +1,118 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Modal, Table, message, Spin, Tag, Button, Form, Input, ColorPicker, Space } from 'antd';
|
import { Modal, Table, Button, Form, Input, Space, message, Tag } from 'antd';
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
import type { RoleTagResponse, RoleTagRequest } from '../types';
|
import type { RoleTagResponse, RoleTagRequest } from '../types';
|
||||||
import { getAllTags, assignTags, createRoleTag, updateRoleTag, deleteRoleTag } from '../service';
|
import { getAllTags, createRoleTag, updateRoleTag, deleteRoleTag } from '../service';
|
||||||
|
|
||||||
interface TagModalProps {
|
interface TagModalProps {
|
||||||
roleId: number;
|
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
selectedTags?: RoleTagResponse[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagModal: React.FC<TagModalProps> = ({
|
const TagModal: React.FC<TagModalProps> = ({
|
||||||
roleId,
|
|
||||||
visible,
|
visible,
|
||||||
onCancel,
|
onCancel,
|
||||||
onSuccess,
|
onSuccess
|
||||||
selectedTags = []
|
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [tags, setTags] = useState<RoleTagResponse[]>([]);
|
const [tags, setTags] = useState<RoleTagResponse[]>([]);
|
||||||
const [selectedKeys, setSelectedKeys] = useState<number[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [tagManageVisible, setTagManageVisible] = useState(false);
|
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||||
const [editingTag, setEditingTag] = useState<RoleTagResponse | null>(null);
|
const [editingTag, setEditingTag] = useState<RoleTagResponse | null>(null);
|
||||||
|
|
||||||
// 获取所有标签
|
useEffect(() => {
|
||||||
const fetchTags = async () => {
|
if (visible) {
|
||||||
|
loadTags();
|
||||||
|
}
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
const loadTags = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await getAllTags();
|
const data = await getAllTags();
|
||||||
setTags(data);
|
setTags(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('获取标签数据失败');
|
message.error('获取标签列表失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const handleAdd = () => {
|
||||||
if (visible) {
|
|
||||||
fetchTags();
|
|
||||||
// 设置已选标签
|
|
||||||
setSelectedKeys(selectedTags.map(tag => tag.id));
|
|
||||||
}
|
|
||||||
}, [visible, selectedTags]);
|
|
||||||
|
|
||||||
const handleOk = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
await assignTags(roleId, selectedKeys);
|
|
||||||
message.success('标签更新成功');
|
|
||||||
onSuccess();
|
|
||||||
} catch (error) {
|
|
||||||
message.error('标签更新失败');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTagManage = () => {
|
|
||||||
setTagManageVisible(true);
|
|
||||||
setEditingTag(null);
|
setEditingTag(null);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
setEditModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTagManageSubmit = async () => {
|
const handleEdit = (record: RoleTagResponse) => {
|
||||||
try {
|
setEditingTag(record);
|
||||||
const values = await form.validateFields();
|
form.setFieldsValue(record);
|
||||||
if (editingTag) {
|
setEditModalVisible(true);
|
||||||
await updateRoleTag(editingTag.id, {
|
|
||||||
...values,
|
|
||||||
color: values.color.toHexString?.() || values.color
|
|
||||||
});
|
|
||||||
message.success('标签更新成功');
|
|
||||||
} else {
|
|
||||||
await createRoleTag({
|
|
||||||
...values,
|
|
||||||
color: values.color.toHexString?.() || values.color
|
|
||||||
});
|
|
||||||
message.success('标签创建成功');
|
|
||||||
}
|
|
||||||
setTagManageVisible(false);
|
|
||||||
fetchTags();
|
|
||||||
} catch (error) {
|
|
||||||
message.error('操作失败');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTagDelete = async (id: number) => {
|
const handleDelete = async (id: number) => {
|
||||||
Modal.confirm({
|
|
||||||
title: '确认删除',
|
|
||||||
content: '确定要删除这个标签吗?',
|
|
||||||
onOk: async () => {
|
|
||||||
try {
|
try {
|
||||||
await deleteRoleTag(id);
|
await deleteRoleTag(id);
|
||||||
message.success('删除成功');
|
message.success('删除成功');
|
||||||
fetchTags();
|
loadTags();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('删除失败');
|
message.error('删除失败');
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
if (editingTag) {
|
||||||
|
await updateRoleTag(editingTag.id, values);
|
||||||
|
message.success('更新成功');
|
||||||
|
} else {
|
||||||
|
await createRoleTag(values);
|
||||||
|
message.success('创建成功');
|
||||||
|
}
|
||||||
|
setEditModalVisible(false);
|
||||||
|
loadTags();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('操作失败');
|
||||||
}
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: '标签名称',
|
title: '标签名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
width: 150
|
key: 'name',
|
||||||
},
|
render: (text: string, record: RoleTagResponse) => (
|
||||||
{
|
<Tag color={record.color}>{text}</Tag>
|
||||||
title: '标签颜色',
|
|
||||||
dataIndex: 'color',
|
|
||||||
width: 100,
|
|
||||||
render: (color: string) => (
|
|
||||||
<Tag color={color}>{color}</Tag>
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '描述',
|
title: '描述',
|
||||||
dataIndex: 'description',
|
dataIndex: 'description',
|
||||||
|
key: 'description',
|
||||||
ellipsis: true
|
ellipsis: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: 180,
|
width: 160,
|
||||||
render: (_: any, record: RoleTagResponse) => (
|
render: (_: any, record: RoleTagResponse) => (
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
|
size="small"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
onClick={() => {
|
onClick={() => handleEdit(record)}
|
||||||
setEditingTag(record);
|
|
||||||
form.setFieldsValue({
|
|
||||||
...record,
|
|
||||||
color: record.color
|
|
||||||
});
|
|
||||||
setTagManageVisible(true);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
|
size="small"
|
||||||
danger
|
danger
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
onClick={() => handleTagDelete(record.id)}
|
onClick={() => handleDelete(record.id)}
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
@ -160,41 +124,32 @@ const TagModal: React.FC<TagModalProps> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
title="分配标签"
|
title="标签管理"
|
||||||
open={visible}
|
open={visible}
|
||||||
onOk={handleOk}
|
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
|
footer={null}
|
||||||
width={800}
|
width={800}
|
||||||
confirmLoading={loading}
|
|
||||||
destroyOnClose
|
|
||||||
>
|
>
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleTagManage}>
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
|
||||||
新建标签
|
新建标签
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Spin spinning={loading}>
|
|
||||||
<Table
|
<Table
|
||||||
rowSelection={{
|
|
||||||
type: 'checkbox',
|
|
||||||
selectedRowKeys: selectedKeys,
|
|
||||||
onChange: (keys) => setSelectedKeys(keys as number[])
|
|
||||||
}}
|
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={tags}
|
dataSource={tags}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
scroll={{ y: 400 }}
|
|
||||||
/>
|
/>
|
||||||
</Spin>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={editingTag ? '编辑标签' : '新建标签'}
|
title={editingTag ? '编辑标签' : '新建标签'}
|
||||||
open={tagManageVisible}
|
open={editModalVisible}
|
||||||
onOk={handleTagManageSubmit}
|
onOk={handleSubmit}
|
||||||
onCancel={() => setTagManageVisible(false)}
|
onCancel={() => setEditModalVisible(false)}
|
||||||
destroyOnClose
|
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
@ -211,9 +166,9 @@ const TagModal: React.FC<TagModalProps> = ({
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
name="color"
|
name="color"
|
||||||
label="标签颜色"
|
label="标签颜色"
|
||||||
rules={[{ required: true, message: '请选择标签颜色' }]}
|
rules={[{ required: true, message: '请输入标签颜色' }]}
|
||||||
>
|
>
|
||||||
<ColorPicker />
|
<Input type="color" style={{ width: 60 }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Card, Button, Table, Space, Modal, Form, Input, InputNumber, message, Tag } from 'antd';
|
import { Card, Button, Table, Space, Modal, Form, Input, InputNumber, message, Tag, Dropdown, MenuProps } from 'antd';
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, TagsOutlined } from '@ant-design/icons';
|
import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, TagsOutlined, MoreOutlined, SettingOutlined } from '@ant-design/icons';
|
||||||
import type { RoleResponse, RoleTagResponse, RoleQuery } from './types';
|
import type { RoleResponse, RoleTagResponse, RoleQuery } from './types';
|
||||||
import { getRoleList, createRole, updateRole, deleteRole, getRolePermissions, assignPermissions } from './service';
|
import { getRoleList, createRole, updateRole, deleteRole, getRolePermissions, assignPermissions } from './service';
|
||||||
import type { TablePaginationConfig } from 'antd/es/table';
|
import type { TablePaginationConfig } from 'antd/es/table';
|
||||||
import type {FilterValue, SorterResult} from 'antd/es/table/interface';
|
import type {FilterValue, SorterResult} from 'antd/es/table/interface';
|
||||||
import PermissionModal from './components/PermissionModal';
|
import PermissionModal from './components/PermissionModal';
|
||||||
import TagModal from './components/TagModal';
|
import TagModal from './components/TagModal';
|
||||||
|
import AssignTagModal from './components/AssignTagModal';
|
||||||
|
|
||||||
const RolePage: React.FC = () => {
|
const RolePage: React.FC = () => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
@ -15,6 +16,7 @@ const RolePage: React.FC = () => {
|
|||||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
const [permissionModalVisible, setPermissionModalVisible] = useState(false);
|
const [permissionModalVisible, setPermissionModalVisible] = useState(false);
|
||||||
const [tagModalVisible, setTagModalVisible] = useState(false);
|
const [tagModalVisible, setTagModalVisible] = useState(false);
|
||||||
|
const [assignTagModalVisible, setAssignTagModalVisible] = useState(false);
|
||||||
const [selectedRole, setSelectedRole] = useState<RoleResponse>();
|
const [selectedRole, setSelectedRole] = useState<RoleResponse>();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [roles, setRoles] = useState<RoleResponse[]>([]);
|
const [roles, setRoles] = useState<RoleResponse[]>([]);
|
||||||
@ -143,7 +145,9 @@ const RolePage: React.FC = () => {
|
|||||||
const permissions = await getRolePermissions(record.id);
|
const permissions = await getRolePermissions(record.id);
|
||||||
setSelectedRole(record);
|
setSelectedRole(record);
|
||||||
setPermissionModalVisible(true);
|
setPermissionModalVisible(true);
|
||||||
setDefaultCheckedKeys(permissions);
|
setDefaultCheckedKeys(Array.isArray(permissions) ?
|
||||||
|
(typeof permissions[0] === 'number' ? permissions : permissions.map(p => p.id)) :
|
||||||
|
[]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('获取角色权限失败');
|
message.error('获取角色权限失败');
|
||||||
}
|
}
|
||||||
@ -164,7 +168,7 @@ const RolePage: React.FC = () => {
|
|||||||
|
|
||||||
const handleAssignTags = (record: RoleResponse) => {
|
const handleAssignTags = (record: RoleResponse) => {
|
||||||
setSelectedRole(record);
|
setSelectedRole(record);
|
||||||
setTagModalVisible(true);
|
setAssignTagModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
@ -202,7 +206,8 @@ const RolePage: React.FC = () => {
|
|||||||
{
|
{
|
||||||
title: '描述',
|
title: '描述',
|
||||||
dataIndex: 'description',
|
dataIndex: 'description',
|
||||||
ellipsis: true
|
ellipsis: true,
|
||||||
|
width: 200
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '创建时间',
|
title: '创建时间',
|
||||||
@ -213,8 +218,36 @@ const RolePage: React.FC = () => {
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: 280,
|
fixed: 'right' as const,
|
||||||
render: (_: any, record: RoleResponse) => (
|
width: 120,
|
||||||
|
render: (_: any, record: RoleResponse) => {
|
||||||
|
const items: MenuProps['items'] = [
|
||||||
|
{
|
||||||
|
key: 'permissions',
|
||||||
|
icon: <KeyOutlined />,
|
||||||
|
label: '分配权限',
|
||||||
|
onClick: () => handleAssignPermissions(record)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'tags',
|
||||||
|
icon: <TagsOutlined />,
|
||||||
|
label: '分配标签',
|
||||||
|
onClick: () => handleAssignTags(record)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 如果不是admin角色,添加删除选项
|
||||||
|
if (record.code !== 'admin') {
|
||||||
|
items.push({
|
||||||
|
key: 'delete',
|
||||||
|
icon: <DeleteOutlined />,
|
||||||
|
label: '删除',
|
||||||
|
danger: true,
|
||||||
|
onClick: () => handleDelete(record.id)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
@ -223,40 +256,32 @@ const RolePage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Dropdown
|
||||||
type="link"
|
menu={{ items }}
|
||||||
icon={<KeyOutlined />}
|
placement="bottomRight"
|
||||||
onClick={() => handleAssignPermissions(record)}
|
trigger={['click']}
|
||||||
>
|
>
|
||||||
分配权限
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="text"
|
||||||
icon={<TagsOutlined />}
|
icon={<MoreOutlined />}
|
||||||
onClick={() => handleAssignTags(record)}
|
style={{ padding: '4px 8px' }}
|
||||||
>
|
/>
|
||||||
分配标签
|
</Dropdown>
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
danger
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
onClick={() => handleDelete(record.id)}
|
|
||||||
disabled={record.code === 'admin'}
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Space>
|
</Space>
|
||||||
)
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
|
||||||
新建角色
|
新建角色
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button icon={<SettingOutlined />} onClick={() => setTagModalVisible(true)}>
|
||||||
|
标签管理
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
@ -266,6 +291,7 @@ const RolePage: React.FC = () => {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
|
scroll={{ x: 1300 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
@ -321,18 +347,27 @@ const RolePage: React.FC = () => {
|
|||||||
onOk={handlePermissionAssign}
|
onOk={handlePermissionAssign}
|
||||||
defaultCheckedKeys={defaultCheckedKeys}
|
defaultCheckedKeys={defaultCheckedKeys}
|
||||||
/>
|
/>
|
||||||
<TagModal
|
<AssignTagModal
|
||||||
|
visible={assignTagModalVisible}
|
||||||
roleId={selectedRole.id}
|
roleId={selectedRole.id}
|
||||||
visible={tagModalVisible}
|
onCancel={() => setAssignTagModalVisible(false)}
|
||||||
onCancel={() => setTagModalVisible(false)}
|
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setTagModalVisible(false);
|
setAssignTagModalVisible(false);
|
||||||
fetchRoles();
|
fetchRoles();
|
||||||
}}
|
}}
|
||||||
selectedTags={selectedRole.tags}
|
selectedTags={selectedRole.tags}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<TagModal
|
||||||
|
visible={tagModalVisible}
|
||||||
|
onCancel={() => setTagModalVisible(false)}
|
||||||
|
onSuccess={() => {
|
||||||
|
setTagModalVisible(false);
|
||||||
|
fetchRoles();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,20 +1,11 @@
|
|||||||
import { createBrowserRouter, Navigate } from 'react-router-dom';
|
import { createBrowserRouter, Navigate, RouteObject } from 'react-router-dom';
|
||||||
import { lazy, Suspense } from 'react';
|
import { lazy, Suspense } from 'react';
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import Login from '../pages/Login';
|
import Login from '../pages/Login';
|
||||||
import BasicLayout from '../layouts/BasicLayout';
|
import BasicLayout from '../layouts/BasicLayout';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { RootState } from '../store';
|
import { RootState } from '../store';
|
||||||
|
import { MenuResponse } from '@/pages/System/Menu/types';
|
||||||
// 懒加载组件
|
|
||||||
const Dashboard = lazy(() => import('../pages/Dashboard'));
|
|
||||||
const Department = lazy(() => import('../pages/System/Department'));
|
|
||||||
const Role = lazy(() => import('../pages/System/Role'));
|
|
||||||
const User = lazy(() => import('../pages/System/User'));
|
|
||||||
const Menu = lazy(() => import('../pages/System/Menu'));
|
|
||||||
const Tenant = lazy(() => import('../pages/System/Tenant'));
|
|
||||||
const Repository = lazy(() => import('../pages/System/Repository'));
|
|
||||||
const Jenkins = lazy(() => import('../pages/System/Jenkins'));
|
|
||||||
|
|
||||||
// 加载中组件
|
// 加载中组件
|
||||||
const LoadingComponent = () => (
|
const LoadingComponent = () => (
|
||||||
@ -34,11 +25,16 @@ const PrivateRoute = ({ children }: { children: React.ReactNode }) => {
|
|||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 懒加载组件
|
||||||
|
const Dashboard = lazy(() => import('../pages/Dashboard'));
|
||||||
|
const User = lazy(() => import('../pages/System/User'));
|
||||||
|
const Role = lazy(() => import('../pages/System/Role'));
|
||||||
|
const Menu = lazy(() => import('../pages/System/Menu'));
|
||||||
|
const Department = lazy(() => import('../pages/System/Department'));
|
||||||
|
const External = lazy(() => import('../pages/System/External'));
|
||||||
|
|
||||||
|
// 创建路由
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
|
||||||
path: '/login',
|
|
||||||
element: <Login />
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
element: (
|
element: (
|
||||||
@ -49,7 +45,7 @@ const router = createBrowserRouter([
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
element: <Navigate to="/dashboard" />
|
element: <Navigate to="/dashboard" replace />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'dashboard',
|
path: 'dashboard',
|
||||||
@ -63,30 +59,10 @@ const router = createBrowserRouter([
|
|||||||
path: 'system',
|
path: 'system',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: 'user',
|
||||||
element: <Navigate to="/system/dashboard" />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'dashboard',
|
|
||||||
element: (
|
element: (
|
||||||
<Suspense fallback={<LoadingComponent />}>
|
<Suspense fallback={<LoadingComponent />}>
|
||||||
<Dashboard />
|
<User />
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'tenant',
|
|
||||||
element: (
|
|
||||||
<Suspense fallback={<LoadingComponent />}>
|
|
||||||
<Tenant />
|
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'department',
|
|
||||||
element: (
|
|
||||||
<Suspense fallback={<LoadingComponent />}>
|
|
||||||
<Department />
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -98,14 +74,6 @@ const router = createBrowserRouter([
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'user',
|
|
||||||
element: (
|
|
||||||
<Suspense fallback={<LoadingComponent />}>
|
|
||||||
<User />
|
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'menu',
|
path: 'menu',
|
||||||
element: (
|
element: (
|
||||||
@ -115,18 +83,18 @@ const router = createBrowserRouter([
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'repository',
|
path: 'department',
|
||||||
element: (
|
element: (
|
||||||
<Suspense fallback={<LoadingComponent />}>
|
<Suspense fallback={<LoadingComponent />}>
|
||||||
<Repository />
|
<Department />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'jenkins',
|
path: 'external',
|
||||||
element: (
|
element: (
|
||||||
<Suspense fallback={<LoadingComponent />}>
|
<Suspense fallback={<LoadingComponent />}>
|
||||||
<Jenkins />
|
<External />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -137,6 +105,10 @@ const router = createBrowserRouter([
|
|||||||
element: <Navigate to="/dashboard" />
|
element: <Navigate to="/dashboard" />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
element: <Login />
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,22 @@ export default defineConfig({
|
|||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, './src')
|
'@': path.resolve(__dirname, 'src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
'system': [
|
||||||
|
'./src/pages/System/User/index.tsx',
|
||||||
|
'./src/pages/System/Role/index.tsx',
|
||||||
|
'./src/pages/System/Menu/index.tsx',
|
||||||
|
'./src/pages/System/Department/index.tsx',
|
||||||
|
'./src/pages/System/External/index.tsx'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user