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 @@ + + + Deploy Ease + + + + + \ 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) => ( + + + + + ), + }, + ]; + + return ( +
+ +
+ +
+ `共 ${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 ( + + +
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) => ( - - - - - - - ) + 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 ( + + + + +
{ 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: {