diff --git a/frontend/index.html b/frontend/index.html
index 8df04213..9d5cb775 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -2,9 +2,9 @@
-
+
- 管理系统
+ Deploy Ease Platform
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg
new file mode 100644
index 00000000..47576430
--- /dev/null
+++ b/frontend/public/favicon.svg
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index d3d78a49..3e2543ef 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,8 +1,15 @@
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 = () => {
- return ;
+ return (
+
+
+
+ );
};
export default App;
\ No newline at end of file
diff --git a/frontend/src/components/IconSelect/index.tsx b/frontend/src/components/IconSelect/index.tsx
index c5811622..e856e230 100644
--- a/frontend/src/components/IconSelect/index.tsx
+++ b/frontend/src/components/IconSelect/index.tsx
@@ -1,8 +1,8 @@
import React from 'react';
import { Modal, Input, Space } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
-import * as AntdIcons from '@ant-design/icons';
import styles from './index.module.css';
+import { getAvailableIcons } from '@/config/icons.tsx';
interface IconSelectProps {
value?: string;
@@ -21,18 +21,10 @@ const IconSelect: React.FC = ({
// 获取所有图标
const iconList = React.useMemo(() => {
- return Object.keys(AntdIcons)
- .filter(key => key.endsWith('Outlined'))
- .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
- );
+ const icons = getAvailableIcons();
+ return icons.filter(icon =>
+ search ? icon.name.toLowerCase().includes(search.toLowerCase()) : true
+ );
}, [search]);
const handleSelect = (iconName: string) => {
@@ -63,7 +55,9 @@ const IconSelect: React.FC = ({
onClick={() => handleSelect(name)}
>
{React.createElement(Icon)}
- {name}
+
+ {name.replace('Outlined', '')}
+
))}
diff --git a/frontend/src/config/icons.ts b/frontend/src/config/icons.ts
new file mode 100644
index 00000000..be2aa083
--- /dev/null
+++ b/frontend/src/config/icons.ts
@@ -0,0 +1,41 @@
+import * as AntdIcons from '@ant-design/icons';
+
+// 图标名称映射配置
+export const iconMap: Record = {
+ '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 ? : null;
+};
+
+// 获取所有可用的图标列表
+export const getAvailableIcons = () => {
+ return Object.keys(AntdIcons)
+ .filter(key => key.endsWith('Outlined'))
+ .map(key => ({
+ name: key,
+ component: (AntdIcons as any)[key]
+ }));
+};
\ No newline at end of file
diff --git a/frontend/src/config/icons.tsx b/frontend/src/config/icons.tsx
new file mode 100644
index 00000000..a7b89a11
--- /dev/null
+++ b/frontend/src/config/icons.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import * as AntdIcons from '@ant-design/icons';
+
+// 图标名称映射配置
+export const iconMap: Record = {
+ '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]
+ }));
+};
\ No newline at end of file
diff --git a/frontend/src/layouts/BasicLayout.tsx b/frontend/src/layouts/BasicLayout.tsx
index 5b32a14d..bd4802f1 100644
--- a/frontend/src/layouts/BasicLayout.tsx
+++ b/frontend/src/layouts/BasicLayout.tsx
@@ -19,6 +19,7 @@ import type {RootState} from '../store';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import {MenuResponse, MenuTypeEnum} from "@/pages/System/Menu/types";
+import { getIconComponent } from '@/config/icons.tsx';
const {Header, Content, Sider} = Layout;
const {confirm} = Modal;
@@ -26,7 +27,6 @@ const {confirm} = Modal;
// 设置中文语言
dayjs.locale('zh-cn');
-
const BasicLayout: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
@@ -36,7 +36,32 @@ const BasicLayout: React.FC = () => {
const [loading, setLoading] = useState(true);
const [currentTime, setCurrentTime] = useState(dayjs());
const [weather, setWeather] = useState({temp: '--', weather: '未知', city: '未知'});
- const [openKeys, setOpenKeys] = useState([]);
+
+ // 根据当前路径获取需要展开的父级菜单
+ 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(getDefaultOpenKeys());
+
+ // 当路由变化时,自动展开对应的父级菜单
+ useEffect(() => {
+ const newOpenKeys = getDefaultOpenKeys();
+ setOpenKeys(prevKeys => {
+ const mergedKeys = [...new Set([...prevKeys, ...newOpenKeys])];
+ return mergedKeys;
+ });
+ }, [location.pathname]);
// 将天气获取逻辑提取为useCallback
const fetchWeather = useCallback(async () => {
@@ -126,26 +151,27 @@ const BasicLayout: React.FC = () => {
];
// 获取图标组件
- const getIcon = (iconName: string | undefined) => {
- if (!iconName) return null;
- const iconKey = `${iconName.charAt(0).toUpperCase() + iconName.slice(1)}Outlined`;
- const Icon = (AntdIcons as any)[iconKey];
- return Icon ? : null;
- };
+ const getIcon = getIconComponent;
// 将菜单数据转换为antd Menu需要的格式
const getMenuItems = (menuList: MenuResponse[]): MenuProps['items'] => {
return menuList
- ?.map(menu => {
+ ?.filter(menu => menu.type !== MenuTypeEnum.BUTTON && !menu.hidden) // 过滤掉按钮类型和隐藏的菜单
+ .map(menu => {
// 确保path存在,否则使用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 {
key,
icon: getIcon(menu.icon),
label: menu.name,
- children: menu.children && menu.children.length > 0
- ? getMenuItems(menu.children)
- : undefined
+ children: children
};
});
};
@@ -181,6 +207,9 @@ const BasicLayout: React.FC = () => {
);
}
+ console.log('Current location:', location.pathname);
+ console.log('Current menus:', menus);
+
return (
@@ -254,8 +283,8 @@ const BasicLayout: React.FC = () => {
-
-
+
+
diff --git a/frontend/src/pages/System/External/index.tsx b/frontend/src/pages/System/External/index.tsx
new file mode 100644
index 00000000..b425136e
--- /dev/null
+++ b/frontend/src/pages/System/External/index.tsx
@@ -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([]);
+
+ const columns: ColumnsType = [
+ {
+ title: '系统名称',
+ dataIndex: 'name',
+ key: 'name',
+ width: 200,
+ },
+ {
+ title: '系统地址',
+ dataIndex: 'url',
+ key: 'url',
+ width: 300,
+ render: (url: string) => (
+
+ {url}
+
+ ),
+ },
+ {
+ 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) => (
+
+ }
+ onClick={() => message.info('编辑功能开发中')}
+ >
+ 编辑
+
+ }
+ onClick={() => message.info('删除功能开发中')}
+ >
+ 删除
+
+
+ ),
+ },
+ ];
+
+ return (
+
+
+
+ }
+ onClick={() => message.info('新增功能开发中')}
+ >
+ 新增系统
+
+
+ `共 ${total} 条`,
+ }}
+ />
+
+
+ );
+};
+
+export default ExternalPage;
\ No newline at end of file
diff --git a/frontend/src/pages/System/Menu/index.tsx b/frontend/src/pages/System/Menu/index.tsx
index d649fd3f..fa6b1f07 100644
--- a/frontend/src/pages/System/Menu/index.tsx
+++ b/frontend/src/pages/System/Menu/index.tsx
@@ -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 type { TablePaginationConfig } from 'antd/es/table';
import type { FilterValue, SorterResult } from 'antd/es/table/interface';
-import { getMenuTree } from './service';
+import { getMenuTree, getCurrentUserMenus } from './service';
import type { MenuResponse } from './types';
import { MenuTypeEnum } from './types';
import IconSelect from '@/components/IconSelect';
import { useTableData } from '@/hooks/useTableData';
import * as AntdIcons from '@ant-design/icons';
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 dispatch = useDispatch();
const {
list: menus,
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 () => {
try {
const values = await form.validateFields();
@@ -122,12 +136,16 @@ const MenuPage: React.FC = () => {
if (success) {
setModalVisible(false);
fetchMenus();
+ await updateReduxMenus();
+ message.success('更新成功');
}
} else {
const success = await handleCreate(values);
if (success) {
setModalVisible(false);
fetchMenus();
+ await updateReduxMenus();
+ message.success('创建成功');
}
}
} catch (error) {
@@ -135,13 +153,21 @@ const MenuPage: React.FC = () => {
}
};
- const getIcon = (iconName: string | undefined) => {
- if (!iconName) return null;
- const iconKey = `${iconName.charAt(0).toUpperCase() + iconName.slice(1)}Outlined`;
- const Icon = (AntdIcons as any)[iconKey];
- return Icon ? : null;
+ const handleMenuDelete = async (id: number) => {
+ try {
+ const success = await handleDelete(id);
+ if (success) {
+ fetchMenus();
+ await updateReduxMenus();
+ message.success('删除成功');
+ }
+ } catch (error) {
+ console.error('删除失败:', error);
+ }
};
+ const getIcon = getIconComponent;
+
const columns = [
{
title: '菜单名称',
@@ -229,7 +255,7 @@ const MenuPage: React.FC = () => {
size="small"
danger
icon={}
- onClick={() => handleDelete(record.id)}
+ onClick={() => handleMenuDelete(record.id)}
disabled={record.children?.length > 0}
>
删除
diff --git a/frontend/src/pages/System/Menu/service.ts b/frontend/src/pages/System/Menu/service.ts
index 5dea99cc..1aadda9f 100644
--- a/frontend/src/pages/System/Menu/service.ts
+++ b/frontend/src/pages/System/Menu/service.ts
@@ -13,9 +13,48 @@ export const getMenuTree = () =>
request.get(`${BASE_URL}/tree`);
// 获取当前用户菜单
-export const getCurrentUserMenus = () =>
- request.get(`${BASE_URL}/current`);
+export const getCurrentUserMenus = async () => {
+ const menus = await request.get(`${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) =>
diff --git a/frontend/src/pages/System/Role/components/AssignTagModal.tsx b/frontend/src/pages/System/Role/components/AssignTagModal.tsx
new file mode 100644
index 00000000..c7843324
--- /dev/null
+++ b/frontend/src/pages/System/Role/components/AssignTagModal.tsx
@@ -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 = ({
+ visible,
+ roleId,
+ onCancel,
+ onSuccess,
+ selectedTags = []
+}) => {
+ const [loading, setLoading] = useState(false);
+ const [allTags, setAllTags] = useState([]);
+ const [selectedTagIds, setSelectedTagIds] = useState([]);
+
+ 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 (
+
+
+
+
+ );
+};
+
+export default AssignTagModal;
\ No newline at end of file
diff --git a/frontend/src/pages/System/Role/components/PermissionModal.tsx b/frontend/src/pages/System/Role/components/PermissionModal.tsx
index 2bb1a23a..f696f837 100644
--- a/frontend/src/pages/System/Role/components/PermissionModal.tsx
+++ b/frontend/src/pages/System/Role/components/PermissionModal.tsx
@@ -39,44 +39,50 @@ const PermissionModal: React.FC = ({
}) => {
const [loading, setLoading] = useState(false);
const [treeData, setTreeData] = useState([]);
- const [checkedKeys, setCheckedKeys] = useState(defaultCheckedKeys);
+ const [checkedKeys, setCheckedKeys] = useState([]);
+ const [idMapping, setIdMapping] = useState>({});
+ const [expandedKeys, setExpandedKeys] = useState([]);
// 将菜单和权限数据转换为Tree组件需要的格式
- const convertToTreeData = (menuList: MenuItem[]): { treeData: DataNode[], idMapping: Record } => {
- const idMapping: Record = {};
+ const convertToTreeData = (menuList: MenuItem[]): { treeData: DataNode[], idMapping: Record, expandedKeys: string[] } => {
+ const mapping: Record = {};
+ const expanded: string[] = [];
const convertMenuToNode = (menu: MenuItem): DataNode => {
- const node: DataNode = {
- key: `menu-${menu.id}`,
- title: menu.name,
- children: []
- };
+ const children: DataNode[] = [];
+ const menuKey = `menu-${menu.id}`;
+ expanded.push(menuKey);
// 添加功能权限节点
if (menu.permissions?.length > 0) {
- const permissionNodes = menu.permissions.map(perm => {
+ menu.permissions.forEach(perm => {
const key = `permission-${perm.id}`;
- idMapping[key] = perm.id;
- return {
+ mapping[key] = perm.id;
+ children.push({
key,
title: perm.name,
isLeaf: true
- };
+ });
});
- node.children!.push(...permissionNodes);
}
// 递归处理子菜单
if (menu.permissionChildren?.length > 0) {
- const childNodes = menu.permissionChildren.map(convertMenuToNode);
- node.children!.push(...childNodes);
+ menu.permissionChildren.forEach(child => {
+ 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);
- return { treeData, idMapping };
+ return { treeData, idMapping: mapping, expandedKeys: expanded };
};
useEffect(() => {
@@ -86,16 +92,18 @@ const PermissionModal: React.FC = ({
setLoading(true);
try {
const response = await getPermissionTree();
- const { treeData, idMapping } = convertToTreeData(response);
- setTreeData(treeData);
-
- // 将默认选中的ID转换为Tree需要的key
- const defaultKeys = defaultCheckedKeys.map(id => {
- const key = Object.entries(idMapping).find(([_, value]) => value === id)?.[0];
- return key;
- }).filter(Boolean);
+ const { treeData: newTreeData, idMapping: newIdMapping, expandedKeys: newExpandedKeys } = convertToTreeData(response);
+ setTreeData(newTreeData);
+ setIdMapping(newIdMapping);
+ setExpandedKeys(newExpandedKeys);
+ // 设置选中的权限
setCheckedKeys(defaultCheckedKeys);
+
+ console.log('权限树数据:', response);
+ console.log('转换后的树数据:', newTreeData);
+ console.log('ID映射:', newIdMapping);
+ console.log('默认选中的权限:', defaultCheckedKeys);
} catch (error) {
console.error('获取权限树失败:', error);
} finally {
@@ -113,22 +121,28 @@ const PermissionModal: React.FC = ({
checkedKeys.forEach(key => {
const keyStr = key.toString();
if (keyStr.startsWith('permission-')) {
- const id = parseInt(keyStr.replace('permission-', ''));
- if (!isNaN(id)) {
+ const id = idMapping[keyStr];
+ if (id) {
permissionIds.push(id);
}
}
});
+ console.log('选中的权限ID:', permissionIds);
setCheckedKeys(permissionIds);
};
+ const handleExpand = (newExpandedKeys: Key[]) => {
+ setExpandedKeys(newExpandedKeys);
+ };
+
const handleOk = () => {
onOk(checkedKeys);
};
// 将权限ID转换为Tree需要的key
const getTreeCheckedKeys = (): string[] => {
+ console.log('当前选中的权限:', checkedKeys);
return checkedKeys.map(id => `permission-${id}`);
};
@@ -139,15 +153,23 @@ const PermissionModal: React.FC = ({
onCancel={onCancel}
onOk={handleOk}
width={600}
- bodyStyle={{ maxHeight: '60vh', overflow: 'auto' }}
+ styles={{
+ body: {
+ padding: '12px',
+ maxHeight: 'calc(100vh - 250px)',
+ overflow: 'auto'
+ }
+ }}
>
diff --git a/frontend/src/pages/System/Role/components/TagModal.tsx b/frontend/src/pages/System/Role/components/TagModal.tsx
index 56ba6790..5b78e280 100644
--- a/frontend/src/pages/System/Role/components/TagModal.tsx
+++ b/frontend/src/pages/System/Role/components/TagModal.tsx
@@ -1,154 +1,118 @@
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 type { RoleTagResponse, RoleTagRequest } from '../types';
-import { getAllTags, assignTags, createRoleTag, updateRoleTag, deleteRoleTag } from '../service';
+import { getAllTags, createRoleTag, updateRoleTag, deleteRoleTag } from '../service';
interface TagModalProps {
- roleId: number;
visible: boolean;
onCancel: () => void;
onSuccess: () => void;
- selectedTags?: RoleTagResponse[];
}
const TagModal: React.FC = ({
- roleId,
visible,
onCancel,
- onSuccess,
- selectedTags = []
+ onSuccess
}) => {
const [form] = Form.useForm();
const [tags, setTags] = useState([]);
- const [selectedKeys, setSelectedKeys] = useState([]);
const [loading, setLoading] = useState(false);
- const [tagManageVisible, setTagManageVisible] = useState(false);
+ const [editModalVisible, setEditModalVisible] = useState(false);
const [editingTag, setEditingTag] = useState(null);
- // 获取所有标签
- const fetchTags = async () => {
+ useEffect(() => {
+ if (visible) {
+ loadTags();
+ }
+ }, [visible]);
+
+ const loadTags = async () => {
try {
setLoading(true);
const data = await getAllTags();
setTags(data);
} catch (error) {
- message.error('获取标签数据失败');
+ message.error('获取标签列表失败');
} finally {
setLoading(false);
}
};
- useEffect(() => {
- 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);
+ const handleAdd = () => {
setEditingTag(null);
form.resetFields();
+ setEditModalVisible(true);
};
- const handleTagManageSubmit = async () => {
+ const handleEdit = (record: RoleTagResponse) => {
+ setEditingTag(record);
+ form.setFieldsValue(record);
+ setEditModalVisible(true);
+ };
+
+ const handleDelete = async (id: number) => {
+ try {
+ await deleteRoleTag(id);
+ message.success('删除成功');
+ loadTags();
+ } catch (error) {
+ message.error('删除失败');
+ }
+ };
+
+ const handleSubmit = async () => {
try {
const values = await form.validateFields();
if (editingTag) {
- await updateRoleTag(editingTag.id, {
- ...values,
- color: values.color.toHexString?.() || values.color
- });
- message.success('标签更新成功');
+ await updateRoleTag(editingTag.id, values);
+ message.success('更新成功');
} else {
- await createRoleTag({
- ...values,
- color: values.color.toHexString?.() || values.color
- });
- message.success('标签创建成功');
+ await createRoleTag(values);
+ message.success('创建成功');
}
- setTagManageVisible(false);
- fetchTags();
+ setEditModalVisible(false);
+ loadTags();
} catch (error) {
message.error('操作失败');
}
};
- const handleTagDelete = async (id: number) => {
- Modal.confirm({
- title: '确认删除',
- content: '确定要删除这个标签吗?',
- onOk: async () => {
- try {
- await deleteRoleTag(id);
- message.success('删除成功');
- fetchTags();
- } catch (error) {
- message.error('删除失败');
- }
- }
- });
- };
-
const columns = [
{
title: '标签名称',
dataIndex: 'name',
- width: 150
- },
- {
- title: '标签颜色',
- dataIndex: 'color',
- width: 100,
- render: (color: string) => (
- {color}
+ key: 'name',
+ render: (text: string, record: RoleTagResponse) => (
+ {text}
)
},
{
title: '描述',
dataIndex: 'description',
+ key: 'description',
ellipsis: true
},
{
title: '操作',
key: 'action',
- width: 180,
+ width: 160,
render: (_: any, record: RoleTagResponse) => (
}
- onClick={() => {
- setEditingTag(record);
- form.setFieldsValue({
- ...record,
- color: record.color
- });
- setTagManageVisible(true);
- }}
+ onClick={() => handleEdit(record)}
>
编辑
}
- onClick={() => handleTagDelete(record.id)}
+ onClick={() => handleDelete(record.id)}
>
删除
@@ -160,41 +124,32 @@ const TagModal: React.FC = ({
return (
<>
- } onClick={handleTagManage}>
+ } onClick={handleAdd}>
新建标签
-
- setSelectedKeys(keys as number[])
- }}
- columns={columns}
- dataSource={tags}
- rowKey="id"
- pagination={false}
- scroll={{ y: 400 }}
- />
-
+
+
setTagManageVisible(false)}
- destroyOnClose
+ open={editModalVisible}
+ onOk={handleSubmit}
+ onCancel={() => setEditModalVisible(false)}
>
-
+
{
const [form] = Form.useForm();
@@ -15,6 +16,7 @@ const RolePage: React.FC = () => {
const [confirmLoading, setConfirmLoading] = useState(false);
const [permissionModalVisible, setPermissionModalVisible] = useState(false);
const [tagModalVisible, setTagModalVisible] = useState(false);
+ const [assignTagModalVisible, setAssignTagModalVisible] = useState(false);
const [selectedRole, setSelectedRole] = useState();
const [loading, setLoading] = useState(false);
const [roles, setRoles] = useState([]);
@@ -143,7 +145,9 @@ const RolePage: React.FC = () => {
const permissions = await getRolePermissions(record.id);
setSelectedRole(record);
setPermissionModalVisible(true);
- setDefaultCheckedKeys(permissions);
+ setDefaultCheckedKeys(Array.isArray(permissions) ?
+ (typeof permissions[0] === 'number' ? permissions : permissions.map(p => p.id)) :
+ []);
} catch (error) {
message.error('获取角色权限失败');
}
@@ -164,7 +168,7 @@ const RolePage: React.FC = () => {
const handleAssignTags = (record: RoleResponse) => {
setSelectedRole(record);
- setTagModalVisible(true);
+ setAssignTagModalVisible(true);
};
const columns = [
@@ -202,7 +206,8 @@ const RolePage: React.FC = () => {
{
title: '描述',
dataIndex: 'description',
- ellipsis: true
+ ellipsis: true,
+ width: 200
},
{
title: '创建时间',
@@ -213,50 +218,70 @@ const RolePage: React.FC = () => {
{
title: '操作',
key: 'action',
- width: 280,
- render: (_: any, record: RoleResponse) => (
-
- }
- onClick={() => handleEdit(record)}
- >
- 编辑
-
- }
- onClick={() => handleAssignPermissions(record)}
- >
- 分配权限
-
- }
- onClick={() => handleAssignTags(record)}
- >
- 分配标签
-
- }
- onClick={() => handleDelete(record.id)}
- disabled={record.code === 'admin'}
- >
- 删除
-
-
- )
+ fixed: 'right' as const,
+ width: 120,
+ render: (_: any, record: RoleResponse) => {
+ const items: MenuProps['items'] = [
+ {
+ key: 'permissions',
+ icon: ,
+ label: '分配权限',
+ onClick: () => handleAssignPermissions(record)
+ },
+ {
+ key: 'tags',
+ icon: ,
+ label: '分配标签',
+ onClick: () => handleAssignTags(record)
+ }
+ ];
+
+ // 如果不是admin角色,添加删除选项
+ if (record.code !== 'admin') {
+ items.push({
+ key: 'delete',
+ icon: ,
+ label: '删除',
+ danger: true,
+ onClick: () => handleDelete(record.id)
+ });
+ }
+
+ return (
+
+ }
+ onClick={() => handleEdit(record)}
+ >
+ 编辑
+
+
+ }
+ style={{ padding: '4px 8px' }}
+ />
+
+
+ );
+ }
}
];
return (
-
+
} onClick={handleAdd}>
新建角色
+ } onClick={() => setTagModalVisible(true)}>
+ 标签管理
+
{
loading={loading}
pagination={pagination}
onChange={handleTableChange}
+ scroll={{ x: 1300 }}
/>
{
onOk={handlePermissionAssign}
defaultCheckedKeys={defaultCheckedKeys}
/>
- setTagModalVisible(false)}
+ onCancel={() => setAssignTagModalVisible(false)}
onSuccess={() => {
- setTagModalVisible(false);
+ setAssignTagModalVisible(false);
fetchRoles();
}}
selectedTags={selectedRole.tags}
/>
>
)}
+
+ setTagModalVisible(false)}
+ onSuccess={() => {
+ setTagModalVisible(false);
+ fetchRoles();
+ }}
+ />
);
};
diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx
index 8f2fb229..2c991d5b 100644
--- a/frontend/src/router/index.tsx
+++ b/frontend/src/router/index.tsx
@@ -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 { Spin } from 'antd';
import Login from '../pages/Login';
import BasicLayout from '../layouts/BasicLayout';
import { useSelector } from 'react-redux';
import { RootState } from '../store';
-
-// 懒加载组件
-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'));
+import { MenuResponse } from '@/pages/System/Menu/types';
// 加载中组件
const LoadingComponent = () => (
@@ -34,11 +25,16 @@ const PrivateRoute = ({ children }: { children: React.ReactNode }) => {
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([
- {
- path: '/login',
- element:
- },
{
path: '/',
element: (
@@ -49,7 +45,7 @@ const router = createBrowserRouter([
children: [
{
path: '',
- element:
+ element:
},
{
path: 'dashboard',
@@ -63,30 +59,10 @@ const router = createBrowserRouter([
path: 'system',
children: [
{
- path: '',
- element:
- },
- {
- path: 'dashboard',
+ path: 'user',
element: (
}>
-
-
- )
- },
- {
- path: 'tenant',
- element: (
- }>
-
-
- )
- },
- {
- path: 'department',
- element: (
- }>
-
+
)
},
@@ -98,14 +74,6 @@ const router = createBrowserRouter([
)
},
- {
- path: 'user',
- element: (
- }>
-
-
- )
- },
{
path: 'menu',
element: (
@@ -115,18 +83,18 @@ const router = createBrowserRouter([
)
},
{
- path: 'repository',
+ path: 'department',
element: (
}>
-
+
)
},
{
- path: 'jenkins',
+ path: 'external',
element: (
}>
-
+
)
}
@@ -137,6 +105,10 @@ const router = createBrowserRouter([
element:
}
]
+ },
+ {
+ path: '/login',
+ element:
}
]);
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index e26dba62..e25bacda 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -6,7 +6,22 @@ export default defineConfig({
plugins: [react()],
resolve: {
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: {