可正常启用

This commit is contained in:
戚辰先生 2024-12-02 22:50:42 +08:00
parent 1582caf776
commit c733f0c1d8
16 changed files with 653 additions and 273 deletions

View File

@ -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>

View 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

View File

@ -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;

View File

@ -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>

View 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]
}));
};

View 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]
}));
};

View File

@ -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>

View 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;

View File

@ -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}
> >

View File

@ -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) =>

View 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;

View File

@ -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>

View File

@ -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

View File

@ -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>
); );
}; };

View File

@ -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 />
} }
]); ]);

View File

@ -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: {