From 9b16146cd0e19b61c2d8753bd3c24e0ff78a6068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=9A=E8=BE=B0=E5=85=88=E7=94=9F?= Date: Mon, 2 Dec 2024 23:37:30 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=89=E6=96=B9=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/config/icons.ts | 10 +- frontend/src/config/icons.tsx | 8 +- frontend/src/hooks/useTableData.ts | 311 ++++++++------ frontend/src/layouts/BasicLayout.tsx | 19 +- .../src/pages/System/Department/index.tsx | 38 +- frontend/src/pages/System/External/index.tsx | 314 ++++++++++++-- frontend/src/pages/System/External/service.ts | 29 ++ frontend/src/pages/System/External/types.ts | 62 +++ frontend/src/pages/System/Menu/index.tsx | 383 +++++++----------- frontend/src/pages/System/User/index.tsx | 363 ++++++++--------- 10 files changed, 909 insertions(+), 628 deletions(-) create mode 100644 frontend/src/pages/System/External/service.ts create mode 100644 frontend/src/pages/System/External/types.ts diff --git a/frontend/src/config/icons.ts b/frontend/src/config/icons.ts index be2aa083..dc37dbdb 100644 --- a/frontend/src/config/icons.ts +++ b/frontend/src/config/icons.ts @@ -1,4 +1,6 @@ +import React from 'react'; import * as AntdIcons from '@ant-design/icons'; +import type { AntdIconProps } from '@ant-design/icons/lib/components/AntdIcon'; // 图标名称映射配置 export const iconMap: Record = { @@ -15,7 +17,7 @@ export const iconMap: Record = { }; // 获取图标组件的通用函数 -export const getIconComponent = (iconName: string | undefined) => { +export const getIconComponent = (iconName: string | undefined): React.ReactNode => { if (!iconName) return null; // 如果在映射中存在,使用映射的名称 @@ -26,8 +28,8 @@ export const getIconComponent = (iconName: string | undefined) => { ? mappedName.charAt(0).toUpperCase() + mappedName.slice(1) : `${mappedName.charAt(0).toUpperCase() + mappedName.slice(1)}Outlined`; - const Icon = (AntdIcons as any)[iconKey]; - return Icon ? : null; + const Icon = (AntdIcons as Record>)[iconKey]; + return Icon ? React.createElement(Icon) : null; }; // 获取所有可用的图标列表 @@ -36,6 +38,6 @@ export const getAvailableIcons = () => { .filter(key => key.endsWith('Outlined')) .map(key => ({ name: key, - component: (AntdIcons as any)[key] + component: (AntdIcons as Record>)[key] })); }; \ No newline at end of file diff --git a/frontend/src/config/icons.tsx b/frontend/src/config/icons.tsx index a7b89a11..f7b9532e 100644 --- a/frontend/src/config/icons.tsx +++ b/frontend/src/config/icons.tsx @@ -1,5 +1,6 @@ import React from 'react'; import * as AntdIcons from '@ant-design/icons'; +import type { ReactNode } from 'react'; // 图标名称映射配置 export const iconMap: Record = { @@ -12,12 +13,11 @@ export const iconMap: Record = { 'department': 'TeamOutlined', 'role': 'UserSwitchOutlined', 'external': 'ApiOutlined', - 'system': 'SettingOutlined', - 'dashboard': 'DashboardOutlined' + 'system': 'SettingOutlined' }; // 获取图标组件的通用函数 -export const getIconComponent = (iconName: string | undefined) => { +export const getIconComponent = (iconName: string | undefined): ReactNode => { if (!iconName) return null; // 如果在映射中存在,使用映射的名称 @@ -29,7 +29,7 @@ export const getIconComponent = (iconName: string | undefined) => { : `${mappedName.charAt(0).toUpperCase() + mappedName.slice(1)}Outlined`; const Icon = (AntdIcons as any)[iconKey]; - return Icon ? React.createElement(Icon) : null; + return Icon ? : null; }; // 获取所有可用的图标列表 diff --git a/frontend/src/hooks/useTableData.ts b/frontend/src/hooks/useTableData.ts index c9edd07f..f7f59725 100644 --- a/frontend/src/hooks/useTableData.ts +++ b/frontend/src/hooks/useTableData.ts @@ -1,143 +1,202 @@ -import {useState, useCallback, useEffect} from 'react'; -import {message, Modal} from 'antd'; -import type {TableState} from '@/types/table'; -import {createInitialState} from '@/utils/table'; -import request from '@/utils/request'; -import type {Page} from '@/types/base/page'; +import { useState, useCallback, useEffect } from 'react'; +import { message, Modal } from 'antd'; +import type { Page } from '@/types/base/page'; +import type { TablePaginationConfig } from 'antd/es/table'; +import type { FilterValue, SorterResult } from 'antd/es/table/interface'; -// 修改TableService接口 -export interface TableService { - baseUrl: string; // 基础URL +// 表格服务接口 +export interface TableService, U = Partial> { + list: (params: Q & { pageNum?: number; pageSize?: number }) => Promise>; + create?: (data: C) => Promise; + update?: (id: number, data: U) => Promise; + delete?: (id: number) => Promise; } -interface SortParams { - sortField?: string; - sortOrder?: 'ascend' | 'descend'; +// 表格状态 +interface TableState { + list: T[]; + pagination: TablePaginationConfig; + loading: boolean; + selectedRowKeys?: React.Key[]; + selectedRows?: T[]; } -export function useTableData({ - service, - defaultParams, - config = {} +// 表格配置 +interface TableConfig { + defaultPageSize?: number; + selection?: boolean; + message?: { + createSuccess?: string; + updateSuccess?: string; + deleteSuccess?: string; + }; +} + +export function useTableData< + T extends { id: number }, + Q extends Record = any, + C = any, + U = any +>({ + service, + defaultParams, + config = {} }: { - service: TableService; - defaultParams?: Partial

; - config?: any; + service: TableService; + defaultParams?: Partial; + config?: TableConfig; }) { - const [state, setState] = useState>(() => - createInitialState(config.defaultPageSize || 10) - ); + // 初始化状态 + const [state, setState] = useState>({ + list: [], + pagination: { + current: 1, + pageSize: config.defaultPageSize || 10, + total: 0, + showSizeChanger: true, + showQuickJumper: true, + showTotal: (total) => `共 ${total} 条` + }, + loading: false + }); - // 加载数据 - const loadData = useCallback(async (params?: Partial

) => { - setState(prev => ({ ...prev, loading: true })); - try { - const pageData = await request.get>(`${service.baseUrl}/page`, { - params: { - ...defaultParams, - ...params, - pageNum: state.pagination.current, - pageSize: state.pagination.pageSize, - sortField: params?.sortField, - sortOrder: params?.sortOrder === 'ascend' ? 'asc' : params?.sortOrder === 'descend' ? 'desc' : undefined - } - }); + // 加载数据 + const loadData = useCallback(async (params?: Partial) => { + setState(prev => ({ ...prev, loading: true })); + try { + const pageData = await service.list({ + ...defaultParams, + ...params, + pageNum: state.pagination.current, + pageSize: state.pagination.pageSize + }); - setState(prev => ({ - ...prev, - list: pageData.content || [], - pagination: { - ...prev.pagination, - total: pageData.totalElements || 0 - }, - loading: false - })); - } catch (error) { - setState(prev => ({ ...prev, loading: false })); - } - }, [service, defaultParams, state.pagination]); + setState(prev => ({ + ...prev, + list: pageData.content || [], + pagination: { + ...prev.pagination, + total: pageData.totalElements || 0 + }, + loading: false + })); + } catch (error) { + setState(prev => ({ ...prev, loading: false })); + throw error; + } + }, [service, defaultParams, state.pagination]); - // CRUD操作 - const handleCreate = useCallback(async (data: Partial) => { - try { - await request.post(service.baseUrl, data); - message.success("创建成功") - await loadData(); - return true; - } catch (error) { - return false; - } - }, [service, loadData]); + // 表格变化处理 + const handleTableChange = ( + pagination: TablePaginationConfig, + filters: Record, + sorter: SorterResult | SorterResult[] + ) => { + const { current, pageSize } = pagination; + setState(prev => ({ + ...prev, + pagination: { + ...prev.pagination, + current, + pageSize + } + })); - const handleUpdate = useCallback(async (id: number, data: Partial) => { - try { - await request.put(`${service.baseUrl}/${id}`, data); - message.success("更新成功") - await loadData(); - return true; - } catch (error) { - return false; - } - }, [service, loadData]); + const params: Record = {}; + + // 处理排序 + if (!Array.isArray(sorter)) { + const { field, order } = sorter; + if (field && order) { + params.sortField = field as string; + params.sortOrder = order === 'ascend' ? 'asc' : 'desc'; + } + } - const handleDelete = useCallback(async (id: number) => { - return new Promise((resolve) => { - Modal.confirm({ - title: '确认删除', - content: '确定要删除该记录吗?', - okText: '确定', - okType: 'danger', - cancelText: '取消', - onOk: async () => { - try { - await request.delete(`${service.baseUrl}/${id}`); - message.success("删除成功") - loadData(); - resolve(true); - } catch (error) { - resolve(false); - } - }, - onCancel: () => resolve(false) - }); - }); - }, [service, loadData]); + // 处理筛选 + Object.entries(filters).forEach(([key, value]) => { + if (value) { + params[key] = value; + } + }); - // 分页变化 - const onPageChange = useCallback((page: number, pageSize?: number) => { - setState(prev => ({ - ...prev, - pagination: { - ...prev.pagination, - current: page, - pageSize: pageSize || prev.pagination.pageSize - } - })); - }, []); + loadData(params as Partial); + }; - // 选择行 - const onSelectChange = useCallback((selectedRowKeys: React.Key[], selectedRows: T[]) => { - if (!config.selection) return; - setState(prev => ({ - ...prev, - selectedRowKeys, - selectedRows - })); - }, [config.selection]); + // 创建 + const handleCreate = async (data: C) => { + if (!service.create) return false; + try { + await service.create(data); + message.success(config.message?.createSuccess || '创建成功'); + loadData(); + return true; + } catch (error) { + return false; + } + }; - // 监听分页变化自动加载数据 - useEffect(() => { - loadData(); - }, [state.pagination.current, state.pagination.pageSize]); + // 更新 + const handleUpdate = async (id: number, data: U) => { + if (!service.update) return false; + try { + await service.update(id, data); + message.success(config.message?.updateSuccess || '更新成功'); + loadData(); + return true; + } catch (error) { + return false; + } + }; - return { - ...state, - loadData, - onPageChange, - handleCreate, - handleUpdate, - handleDelete, - onSelectChange, - reset: () => setState(createInitialState(config.defaultPageSize || 10)) - }; + // 删除 + const handleDelete = async (id: number) => { + if (!service.delete) return false; + return new Promise((resolve) => { + Modal.confirm({ + title: '确认删除', + content: '确定要删除该记录吗?', + okText: '确定', + okType: 'danger', + cancelText: '取消', + onOk: async () => { + try { + await service.delete(id); + message.success(config.message?.deleteSuccess || '删除成功'); + loadData(); + resolve(true); + } catch (error) { + resolve(false); + } + }, + onCancel: () => resolve(false) + }); + }); + }; + + // 选择行 + const handleSelectChange = (selectedRowKeys: React.Key[], selectedRows: T[]) => { + if (!config.selection) return; + setState(prev => ({ + ...prev, + selectedRowKeys, + selectedRows + })); + }; + + // 监听分页变化自动加载数据 + useEffect(() => { + loadData(); + }, [state.pagination.current, state.pagination.pageSize]); + + return { + ...state, + loadData, + handleTableChange, + handleCreate, + handleUpdate, + handleDelete, + handleSelectChange, + refresh: () => loadData() + }; } \ No newline at end of file diff --git a/frontend/src/layouts/BasicLayout.tsx b/frontend/src/layouts/BasicLayout.tsx index bd4802f1..e7dd2108 100644 --- a/frontend/src/layouts/BasicLayout.tsx +++ b/frontend/src/layouts/BasicLayout.tsx @@ -156,22 +156,13 @@ const BasicLayout: React.FC = () => { // 将菜单数据转换为antd Menu需要的格式 const getMenuItems = (menuList: MenuResponse[]): MenuProps['items'] => { return menuList - ?.filter(menu => menu.type !== MenuTypeEnum.BUTTON && !menu.hidden) // 过滤掉按钮类型和隐藏的菜单 + ?.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), + key: menu.path || String(menu.id), + icon: getIconComponent(menu.icon), label: menu.name, - children: children + children: menu.children ? getMenuItems(menu.children) : undefined }; }); }; @@ -207,8 +198,6 @@ const BasicLayout: React.FC = () => { ); } - console.log('Current location:', location.pathname); - console.log('Current menus:', menus); return ( diff --git a/frontend/src/pages/System/Department/index.tsx b/frontend/src/pages/System/Department/index.tsx index e0bf8742..3c5ea83b 100644 --- a/frontend/src/pages/System/Department/index.tsx +++ b/frontend/src/pages/System/Department/index.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; -import { Table, Button, Modal, Form, Input, Space, InputNumber, Switch, TreeSelect, Select, message } from 'antd'; -import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; +import { Table, Button, Modal, Form, Input, Space, InputNumber, Switch, TreeSelect, Select, message, Card } from 'antd'; +import { PlusOutlined, EditOutlined, DeleteOutlined, CaretDownOutlined, CaretRightOutlined } from '@ant-design/icons'; import type { DepartmentResponse } from './types'; import { getDepartmentTree } from './service'; import type { UserResponse } from '@/pages/System/User/types'; @@ -23,12 +23,21 @@ const DepartmentPage: React.FC = () => { const [editingDepartment, setEditingDepartment] = useState(null); const [form] = Form.useForm(); + // 处理树形数据,添加 hasChildren 属性 + const processTreeData = (departments: DepartmentResponse[]) => { + return departments.map(dept => ({ + ...dept, + hasChildren: Boolean(dept.children?.length), + children: dept.children ? processTreeData(dept.children) : undefined + })); + }; + // 加载部门树数据 const loadDepartmentTree = async () => { setLoading(true); try { const response = await getDepartmentTree(); - setDepartmentTree(response || []); + setDepartmentTree(processTreeData(response || [])); } catch (error) { message.error('加载部门数据失败'); } finally { @@ -191,9 +200,9 @@ const DepartmentPage: React.FC = () => { ]; return ( -

-
-
@@ -203,11 +212,24 @@ const DepartmentPage: React.FC = () => { columns={columns} dataSource={departmentTree} rowKey="id" - scroll={{x: 1200}} + scroll={{ x: 1200 }} pagination={false} size="middle" bordered defaultExpandAllRows + expandable={{ + showIcon: true, + expandIcon: ({ expanded, onExpand, record }) => { + if (!record.hasChildren) { + return null; + } + return expanded ? ( + onExpand(record, e)} /> + ) : ( + onExpand(record, e)} /> + ); + } + }} /> { -
+ ); }; diff --git a/frontend/src/pages/System/External/index.tsx b/frontend/src/pages/System/External/index.tsx index b425136e..ac66c0ab 100644 --- a/frontend/src/pages/System/External/index.tsx +++ b/frontend/src/pages/System/External/index.tsx @@ -1,32 +1,116 @@ -import React from 'react'; -import { Card, Table, Button, Space, Modal, Form, Input, message } from 'antd'; -import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; +import React, { useState } from 'react'; +import { Card, Table, Button, Space, Modal, Form, Input, message, Select, InputNumber, Switch, Tag, Tooltip } from 'antd'; +import { PlusOutlined, EditOutlined, DeleteOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, LinkOutlined, MinusCircleOutlined } 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; -} +import { useTableData } from '@/hooks/useTableData'; +import * as service from './service'; +import { SystemType, AuthType, SyncStatus, ExternalSystemResponse, ExternalSystemRequest, ExternalSystemQuery } from './types'; const ExternalPage: React.FC = () => { - const [loading, setLoading] = React.useState(false); - const [data, setData] = React.useState([]); + const [form] = Form.useForm(); + const [modalVisible, setModalVisible] = useState(false); + const [editingSystem, setEditingSystem] = useState(null); - const columns: ColumnsType = [ + const { + list, + loading, + pagination, + handleTableChange, + handleCreate, + handleUpdate, + handleDelete, + refresh + } = useTableData({ + service: { + list: service.getExternalSystems, + create: service.createExternalSystem, + update: service.updateExternalSystem, + delete: service.deleteExternalSystem + }, + config: { + message: { + createSuccess: '创建系统成功', + updateSuccess: '更新系统成功', + deleteSuccess: '删除系统成功' + } + } + }); + + const handleAdd = () => { + setEditingSystem(null); + form.resetFields(); + form.setFieldsValue({ + enabled: true, + sort: 1, + authType: AuthType.BASIC + }); + setModalVisible(true); + }; + + const handleEdit = (record: ExternalSystemResponse) => { + setEditingSystem(record); + form.setFieldsValue({ + ...record, + password: undefined // 不显示密码 + }); + setModalVisible(true); + }; + + const handleTestConnection = async (id: number) => { + try { + const success = await service.testConnection(id); + message.success(success ? '连接成功' : '连接失败'); + } catch (error) { + message.error('测试连接失败'); + } + }; + + const handleStatusChange = async (id: number, enabled: boolean) => { + try { + await service.updateStatus(id, enabled); + message.success('更新状态成功'); + refresh(); + } catch (error) { + message.error('更新状态失败'); + } + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + if (editingSystem) { + await handleUpdate(editingSystem.id, values); + } else { + await handleCreate(values); + } + setModalVisible(false); + } catch (error) { + console.error('操作失败:', error); + } + }; + + const columns: ColumnsType = [ { title: '系统名称', dataIndex: 'name', - key: 'name', width: 200, }, + { + title: '系统类型', + dataIndex: 'type', + width: 120, + render: (type: SystemType) => { + const typeMap = { + [SystemType.JENKINS]: 'Jenkins', + [SystemType.GIT]: 'Git', + [SystemType.ZENTAO]: '禅道' + }; + return typeMap[type]; + } + }, { title: '系统地址', dataIndex: 'url', - key: 'url', width: 300, render: (url: string) => ( @@ -35,41 +119,75 @@ const ExternalPage: React.FC = () => { ), }, { - title: '描述', - dataIndex: 'description', - key: 'description', - ellipsis: true, + title: '认证方式', + dataIndex: 'authType', + width: 120, + render: (authType: AuthType) => { + const authTypeMap = { + [AuthType.BASIC]: '用户名密码', + [AuthType.TOKEN]: '令牌', + [AuthType.OAUTH]: 'OAuth2' + }; + return authTypeMap[authType]; + } }, { - title: '创建时间', - dataIndex: 'createTime', - key: 'createTime', - width: 180, + title: '同步状态', + dataIndex: 'syncStatus', + width: 120, + render: (status: SyncStatus, record) => { + const statusConfig = { + [SyncStatus.SUCCESS]: { icon: , color: 'success', text: '成功' }, + [SyncStatus.FAILED]: { icon: , color: 'error', text: '失败' }, + [SyncStatus.RUNNING]: { icon: , color: 'processing', text: '同步中' }, + NONE: { icon: , color: 'default', text: '未同步' } + }; + const config = statusConfig[status] || statusConfig.NONE; + return ( + + + {config.text} + + + ); + } }, { - title: '更新时间', - dataIndex: 'updateTime', - key: 'updateTime', - width: 180, + title: '状态', + dataIndex: 'enabled', + width: 100, + render: (enabled: boolean, record) => ( + handleStatusChange(record.id, checked)} + /> + ) }, { title: '操作', key: 'action', - width: 180, + width: 280, render: (_, record) => ( + @@ -85,24 +203,140 @@ const ExternalPage: React.FC = () => { `共 ${total} 条`, - }} + pagination={pagination} + onChange={handleTableChange} /> + + setModalVisible(false)} + width={600} + > +
+ + + + + + + + + + + + + + + + + prevValues.authType !== currentValues.authType} + > + {({ getFieldValue }) => { + const authType = getFieldValue('authType'); + if (authType === AuthType.BASIC) { + return ( + <> + + + + + + + + ); + } + if (authType === AuthType.TOKEN) { + return ( + + + + ); + } + return null; + }} + + + + + + + + + + + + + + +
); }; diff --git a/frontend/src/pages/System/External/service.ts b/frontend/src/pages/System/External/service.ts new file mode 100644 index 00000000..22788093 --- /dev/null +++ b/frontend/src/pages/System/External/service.ts @@ -0,0 +1,29 @@ +import request from '@/utils/request'; +import type { Page } from '@/types/base/page'; +import type { ExternalSystemResponse, ExternalSystemRequest, ExternalSystemQuery } from './types'; + +const BASE_URL = '/api/v1/external-system'; + +// 获取三方系统列表 +export const getExternalSystems = (params: ExternalSystemQuery) => + request.get>(`${BASE_URL}/page`, { params }); + +// 创建三方系统 +export const createExternalSystem = (data: ExternalSystemRequest) => + request.post(BASE_URL, data); + +// 更新三方系统 +export const updateExternalSystem = (id: number, data: ExternalSystemRequest) => + request.put(`${BASE_URL}/${id}`, data); + +// 删除三方系统 +export const deleteExternalSystem = (id: number) => + request.delete(`${BASE_URL}/${id}`); + +// 测试连接 +export const testConnection = (id: number) => + request.get(`${BASE_URL}/${id}/test-connection`); + +// 更新状态 +export const updateStatus = (id: number, enabled: boolean) => + request.put(`${BASE_URL}/${id}/status`, null, { params: { enabled } }); \ No newline at end of file diff --git a/frontend/src/pages/System/External/types.ts b/frontend/src/pages/System/External/types.ts new file mode 100644 index 00000000..0220e6ce --- /dev/null +++ b/frontend/src/pages/System/External/types.ts @@ -0,0 +1,62 @@ +import { BaseResponse } from '@/types/base/response'; +import { BaseQuery } from '@/types/base/query'; + +// 系统类型枚举 +export enum SystemType { + JENKINS = 'JENKINS', + GIT = 'GIT', + ZENTAO = 'ZENTAO' +} + +// 认证方式枚举 +export enum AuthType { + BASIC = 'BASIC', + TOKEN = 'TOKEN', + OAUTH = 'OAUTH' +} + +// 同步状态枚举 +export enum SyncStatus { + SUCCESS = 'SUCCESS', + FAILED = 'FAILED', + RUNNING = 'RUNNING' +} + +// 查询参数接口 +export interface ExternalSystemQuery extends BaseQuery { + name?: string; + type?: SystemType; + enabled?: boolean; +} + +// 响应数据接口 +export interface ExternalSystemResponse extends BaseResponse { + name: string; + type: SystemType; + url: string; + remark?: string; + sort: number; + enabled: boolean; + authType: AuthType; + username?: string; + password?: string; + token?: string; + syncStatus: SyncStatus; + lastSyncTime?: string; + config?: string; +} + +// 请求数据接口 +export interface ExternalSystemRequest { + name: string; + type: SystemType; + url: string; + remark?: string; + sort: number; + enabled: boolean; + authType: AuthType; + username?: string; + password?: string; + token?: string; + config?: string; +} \ 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 fa6b1f07..3119a6f9 100644 --- a/frontend/src/pages/System/Menu/index.tsx +++ b/frontend/src/pages/System/Menu/index.tsx @@ -1,58 +1,46 @@ import React, { useEffect, useState } from 'react'; -import { Table, Button, Modal, Form, Input, Space, Switch, Select, TreeSelect, Tooltip, InputNumber, Tree } from 'antd'; -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, getCurrentUserMenus } from './service'; -import type { MenuResponse } from './types'; -import { MenuTypeEnum } from './types'; +import { Card, Table, Button, Modal, Form, Input, Space, message, Select, TreeSelect, Tooltip, Switch } from 'antd'; +import { PlusOutlined, EditOutlined, DeleteOutlined, QuestionCircleOutlined } from '@ant-design/icons'; +import type { ColumnsType } from 'antd/es/table'; +import * as service from './service'; +import { MenuTypeEnum, MenuResponse, MenuRequest } 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'; +import { getIconComponent } from '@/config/icons.tsx'; const MenuPage: React.FC = () => { const dispatch = useDispatch(); - const { - list: menus, - loading, - loadData: fetchMenus, - handleCreate, - handleUpdate, - handleDelete - } = useTableData({ - service: { - baseUrl: '/api/v1/menu' - }, - defaultParams: { - sortField: 'sort', - sortOrder: 'asc' - } - }); - + const [form] = Form.useForm(); const [modalVisible, setModalVisible] = useState(false); const [editingMenu, setEditingMenu] = useState(null); - const [menuTree, setMenuTree] = useState([]); const [iconSelectVisible, setIconSelectVisible] = useState(false); - const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [menuTree, setMenuTree] = useState([]); + + // 加载菜单树 + const loadMenuTree = async () => { + try { + setLoading(true); + const data = await service.getMenuTree(); + setMenuTree(data); + } catch (error) { + message.error('加载菜单数据失败'); + } finally { + setLoading(false); + } + }; useEffect(() => { - getMenuTree().then(menus => setMenuTree(menus)); + loadMenuTree(); }, []); const handleAdd = () => { setEditingMenu(null); form.resetFields(); - - const maxSort = Math.max(0, ...menus.map(menu => menu.sort)); - form.setFieldsValue({ type: MenuTypeEnum.MENU, - sort: maxSort + 10, + sort: 1, hidden: false }); setModalVisible(true); @@ -67,45 +55,50 @@ const MenuPage: React.FC = () => { setModalVisible(true); }; - const handleTableChange = ( - pagination: TablePaginationConfig, - filters: Record, - sorter: SorterResult | SorterResult[] - ) => { - const { field, order } = Array.isArray(sorter) ? sorter[0] : sorter; - fetchMenus({ - sortField: field as string, - sortOrder: order - }); + const handleDelete = async (id: number) => { + try { + await service.deleteMenu(id); + message.success('删除成功'); + loadMenuTree(); + await updateReduxMenus(); + } catch (error) { + message.error('删除失败'); + } }; - const buildMenuTree = (menuList: MenuResponse[]): MenuResponse[] => { - const menuMap = new Map(); - const result: MenuResponse[] = []; - - menuList.forEach(menu => { - menuMap.set(menu.id, { ...menu }); - }); - - menuList.forEach(menu => { - const node = menuMap.get(menu.id)!; - if (menu.parentId === 0 || !menuMap.has(menu.parentId)) { - result.push(node); + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + if (editingMenu) { + await service.updateMenu(editingMenu.id, { + ...values, + version: editingMenu.version + }); + message.success('更新成功'); } else { - const parent = menuMap.get(menu.parentId)!; - if (!parent.children) { - parent.children = []; - } - parent.children.push(node); + await service.createMenu(values); + message.success('创建成功'); } - }); - - return result; + setModalVisible(false); + loadMenuTree(); + await updateReduxMenus(); + } catch (error) { + message.error('操作失败'); + } }; - const getTreeSelectData = () => { - const menuTree = buildMenuTree(menus); - return menuTree.map(menu => ({ + // 更新 Redux 中的菜单数据 + const updateReduxMenus = async () => { + try { + const menus = await service.getCurrentUserMenus(); + dispatch(setMenus(menus)); + } catch (error) { + console.error('更新菜单数据失败:', error); + } + }; + + const getTreeSelectData = (menuList: MenuResponse[]) => { + return menuList.map(menu => ({ title: menu.name, value: menu.id, children: menu.children?.map(child => ({ @@ -116,79 +109,34 @@ 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(); - if (editingMenu) { - const success = await handleUpdate(editingMenu.id, { - ...values, - version: editingMenu.version - }); - 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) { - console.error('操作失败:', error); - } - }; - - 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 = [ + const columns: ColumnsType = [ { title: '菜单名称', dataIndex: 'name', - key: 'name', - width: 250, - fixed: 'left' as FixedType, - sorter: true + width: 200 }, { title: '图标', dataIndex: 'icon', - key: 'icon', - width: 80, - render: (icon: string) => getIcon(icon) + width: 100, + render: (icon: string) => { + return getIconComponent(icon); + } }, { - title: '类型', + title: '路由地址', + dataIndex: 'path', + width: 200 + }, + { + title: '组件路径', + dataIndex: 'component', + width: 200 + }, + { + title: '菜单类型', dataIndex: 'type', - key: 'type', - width: 100, + width: 120, render: (type: MenuTypeEnum) => { const typeMap = { [MenuTypeEnum.DIRECTORY]: '目录', @@ -199,63 +147,28 @@ const MenuPage: React.FC = () => { } }, { - title: '路由地址', - dataIndex: 'path', - key: 'path', - width: 200, - ellipsis: true - }, - { - title: '组件路径', - dataIndex: 'component', - key: 'component', - width: 200, - ellipsis: true - }, - { - title: '权限标识', - dataIndex: 'permission', - key: 'permission', - width: 150, - ellipsis: true - }, - { - title: '排序', + title: '显示排序', dataIndex: 'sort', - key: 'sort', - width: 80, - sorter: true - }, - { - title: '状态', - dataIndex: 'enabled', - key: 'enabled', - width: 80, - render: (enabled: boolean) => ( - - ) + width: 100 }, { title: '操作', key: 'action', - width: 160, - fixed: 'right' as FixedType, - render: (_: any, record: MenuResponse) => ( - + width: 200, + render: (_, record) => ( +
String(record.id)} + rowKey="id" + loading={loading} pagination={false} size="middle" bordered @@ -290,21 +207,11 @@ const MenuPage: React.FC = () => { onOk={handleSubmit} onCancel={() => setModalVisible(false)} width={600} - destroyOnClose > - - - - { + + + + { } > { prevValues.type !== currentValues.type} > - - + {({ getFieldValue }) => { + const type = getFieldValue('type'); + if (type === MenuTypeEnum.BUTTON) { + return null; + } + return ( + <> + + setIconSelectVisible(true)} + suffix={getIconComponent(form.getFieldValue('icon'))} + /> + - - - + + + - - - - - - setIconSelectVisible(true)} - suffix={form.getFieldValue('icon') && getIcon(form.getFieldValue('icon'))} - /> + {type === MenuTypeEnum.MENU && ( + + + + )} + + ); + }} - + - + @@ -408,7 +325,7 @@ const MenuPage: React.FC = () => { setIconSelectVisible(false); }} /> - + ); }; diff --git a/frontend/src/pages/System/User/index.tsx b/frontend/src/pages/System/User/index.tsx index f67566c5..2d0f7753 100644 --- a/frontend/src/pages/System/User/index.tsx +++ b/frontend/src/pages/System/User/index.tsx @@ -1,15 +1,13 @@ -import React, {useEffect, useState} from 'react'; -import {Table, Button, Modal, Form, Input, Space, message, Switch, TreeSelect, Select, Tag} from 'antd'; -import {PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, TeamOutlined} from '@ant-design/icons'; -import type {UserResponse, Role} from './types'; -import type {DepartmentDTO} from '../Department/types'; -import {resetPassword, assignRoles, getAllRoles} from './service'; -import {useTableData} from '@/hooks/useTableData'; -import type {FixedType, AlignType, SortOrder} from 'rc-table/lib/interface'; -import {Response} from "@/utils/request.ts"; -import {TablePaginationConfig} from "antd/es/table"; -import {FilterValue, SorterResult} from "antd/es/table/interface"; -import { getDepartmentTree } from '../Department/service'; // 导入部门树接口 +import React, { useEffect, useState } from 'react'; +import { Card, Table, Button, Modal, Form, Input, Space, message, Switch, TreeSelect, Select, Tag, Dropdown } from 'antd'; +import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, TeamOutlined, MoreOutlined } from '@ant-design/icons'; +import type { ColumnsType } from 'antd/es/table'; +import type { MenuProps } from 'antd'; +import { useTableData } from '@/hooks/useTableData'; +import * as service from './service'; +import type { UserResponse, UserRequest, UserQuery, Role } from './types'; +import type { DepartmentResponse } from '../Department/types'; +import { getDepartmentTree } from '../Department/service'; interface TreeNode { title: string; @@ -18,39 +16,68 @@ interface TreeNode { } const UserPage: React.FC = () => { - const { - list: users, - pagination, - loading, - loadData: fetchUsers, - handleCreate, - handleUpdate, - handleDelete - } = useTableData({ - service: { - baseUrl: '/api/v1/user' - }, - defaultParams: { - sortField: 'createTime', - sortOrder: 'descend' - } - }); - - const [departments, setDepartments] = useState([]); + const [form] = Form.useForm(); + const [passwordForm] = Form.useForm(); const [modalVisible, setModalVisible] = useState(false); const [resetPasswordModalVisible, setResetPasswordModalVisible] = useState(false); const [editingUser, setEditingUser] = useState(null); - const [form] = Form.useForm(); - const [passwordForm] = Form.useForm(); + const [departments, setDepartments] = useState([]); const [roleModalVisible, setRoleModalVisible] = useState(false); const [selectedUser, setSelectedUser] = useState(null); const [selectedRoles, setSelectedRoles] = useState([]); - const [allRoles, setAllRoles] = useState([]); + const [allRoles, setAllRoles] = useState([]); + + const { + list, + loading, + pagination, + handleTableChange, + handleCreate, + handleUpdate, + handleDelete, + refresh + } = useTableData({ + service: { + list: service.getUsers, + create: service.createUser, + update: service.updateUser, + delete: service.deleteUser + }, + defaultParams: { + sortField: 'createTime', + sortOrder: 'desc' + }, + config: { + message: { + createSuccess: '创建用户成功', + updateSuccess: '更新用户成功', + deleteSuccess: '删除用户成功' + } + } + }); useEffect(() => { - getAllRoles().then(roles => setAllRoles(roles)); + service.getAllRoles().then(roles => setAllRoles(roles)); + loadDepartmentTree(); }, []); + const loadDepartmentTree = async () => { + try { + const data = await getDepartmentTree(); + setDepartments(data); + } catch (error) { + message.error('加载部门数据失败'); + } + }; + + const getTreeData = (departments: DepartmentResponse[]): TreeNode[] => { + return departments.map(dept => ({ + title: dept.name, + value: dept.id, + children: dept.children ? getTreeData(dept.children) : undefined + })); + }; + const handleAdd = () => { setEditingUser(null); form.resetFields(); @@ -63,7 +90,8 @@ const UserPage: React.FC = () => { const handleEdit = (record: UserResponse) => { setEditingUser(record); form.setFieldsValue({ - ...record + ...record, + password: undefined // 不显示密码 }); setModalVisible(true); }; @@ -78,64 +106,31 @@ const UserPage: React.FC = () => { try { const values = await passwordForm.validateFields(); if (editingUser) { - await resetPassword(editingUser.id, values.password); + await service.resetPassword(editingUser.id, values.password); message.success('密码重置成功'); setResetPasswordModalVisible(false); + refresh(); } } catch (error: any) { message.error(error.message || '密码重置失败'); } }; - const handleSubmit = async (values: any) => { + const handleSubmit = async () => { try { + const values = await form.validateFields(); if (editingUser) { - const success = await handleUpdate(editingUser.id, { - ...values, - enabled: values.enabled ?? true - }); - if (success) { - message.success('更新成功'); - setModalVisible(false); - fetchUsers(); - } + await handleUpdate(editingUser.id, values); } else { - const success = await handleCreate({ - ...values, - enabled: values.enabled ?? true - }); - if (success) { - message.success('创建成功'); - setModalVisible(false); - fetchUsers(); - } + await handleCreate(values); } + setModalVisible(false); + refresh(); } catch (error: any) { message.error(error.message || '操作失败'); } }; - const loadDepartmentTree = async () => { - try { - const data = await getDepartmentTree(); - setDepartments(data); - } catch (error) { - message.error('加载部门数据失败'); - } - }; - - useEffect(() => { - loadDepartmentTree(); - }, []); - - const getTreeData = (departments: DepartmentDTO[]): TreeNode[] => { - return departments.map(dept => ({ - title: dept.name, - value: dept.id, - children: dept.children ? getTreeData(dept.children) : undefined - })); - }; - const handleAssignRoles = (record: UserResponse) => { setSelectedUser(record); setSelectedRoles(record.roles?.map(role => role.id) || []); @@ -145,56 +140,48 @@ const UserPage: React.FC = () => { const handleAssignRoleSubmit = async () => { if (selectedUser) { try { - await assignRoles(selectedUser.id, selectedRoles); - message.success("角色分配成功") + await service.assignRoles(selectedUser.id, selectedRoles); + message.success('角色分配成功'); setRoleModalVisible(false); - fetchUsers(); + refresh(); } catch (error) { - message.error("角色分配失败") - console.log(error); + message.error('角色分配失败'); } - } }; - const columns = [ + const columns: ColumnsType = [ { title: 'ID', dataIndex: 'id', - key: 'id', width: 60, - fixed: 'left' as FixedType, + fixed: 'left', sorter: true }, { title: '用户名', dataIndex: 'username', - key: 'username', width: 100, sorter: true }, { title: '昵称', dataIndex: 'nickname', - key: 'nickname', width: 100, }, { title: '邮箱', dataIndex: 'email', - key: 'email', width: 200, }, { title: '部门', dataIndex: 'departmentName', - key: 'departmentName', width: 150, }, { title: '状态', dataIndex: 'enabled', - key: 'enabled', width: 100, render: (enabled: boolean) => ( @@ -205,13 +192,11 @@ const UserPage: React.FC = () => { { title: '手机号', dataIndex: 'phone', - key: 'phone', width: 120, }, { title: '角色', dataIndex: 'roles', - key: 'roles', width: 120, ellipsis: true, render: (roles: Role[]) => roles?.map(role => role.name).join(', ') || '-' @@ -219,104 +204,98 @@ const UserPage: React.FC = () => { { title: '创建时间', dataIndex: 'createTime', - key: 'createTime', width: 150, sorter: true, - defaultSortOrder: 'descend' as SortOrder }, { title: '更新时间', dataIndex: 'updateTime', - key: 'updateTime', width: 150, sorter: true }, { title: '操作', key: 'action', - width: 320, - fixed: 'right' as FixedType, - render: (_: any, record: UserResponse) => ( - - - - - - - ), + width: 180, + fixed: 'right', + render: (_, record) => { + const items: MenuProps['items'] = [ + { + key: 'resetPassword', + icon: , + label: '重置密码', + onClick: () => handleResetPassword(record) + }, + { + key: 'assignRoles', + icon: , + label: '分配角色', + onClick: () => handleAssignRoles(record), + disabled: record.username === 'admin' + } + ]; + + // 如果不是 admin 用户,添加删除选项 + if (record.username !== 'admin') { + items.push({ + key: 'delete', + icon: , + label: '删除', + danger: true, + onClick: () => handleDelete(record.id) + }); + } + + return ( + + + +
form.validateFields().then(handleSubmit)} + onOk={handleSubmit} onCancel={() => setModalVisible(false)} width={600} - destroyOnClose >
{ - + {!editingUser && ( @@ -335,11 +314,11 @@ const UserPage: React.FC = () => { name="password" label="密码" rules={[ - {required: true, message: '请输入密码'}, - {min: 6, message: '密码长度不能小于6位'} + { required: true, message: '请输入密码' }, + { min: 6, message: '密码长度不能小于6位' } ]} > - + )} @@ -347,22 +326,28 @@ const UserPage: React.FC = () => { name="nickname" label="昵称" > - + + - + + - + - + + { treeNodeFilterProp="title" /> + - +
- {/* 注释掉RoleModal相关代码 - {selectedUser && ( - { - setRoleModalVisible(false); - setSelectedUser(null); - }} - onSuccess={() => { - setRoleModalVisible(false); - setSelectedUser(null); - fetchUsers(); - }} - /> - )} - */} - setResetPasswordModalVisible(false)} - destroyOnClose >
{ name="password" label="新密码" rules={[ - {required: true, message: '请输入新密码'}, - {min: 6, message: '密码长度不能小于6位'} + { required: true, message: '请输入新密码' }, + { min: 6, message: '密码长度不能小于6位' } ]} > - + + ({ + { required: true, message: '请确认新密码' }, + ({ getFieldValue }) => ({ validator(_, value) { if (!value || getFieldValue('password') === value) { return Promise.resolve(); @@ -438,7 +406,7 @@ const UserPage: React.FC = () => { }), ]} > - +
@@ -449,7 +417,6 @@ const UserPage: React.FC = () => { onOk={handleAssignRoleSubmit} onCancel={() => setRoleModalVisible(false)} width={600} - destroyOnClose >
@@ -458,7 +425,7 @@ const UserPage: React.FC = () => { placeholder="请选择角色" value={selectedRoles} onChange={setSelectedRoles} - style={{width: '100%'}} + style={{ width: '100%' }} > {allRoles.map(role => ( @@ -469,7 +436,7 @@ const UserPage: React.FC = () => { - + ); };