diff --git a/frontend/src/components/IconSelect/index.module.less b/frontend/src/components/IconSelect/index.module.less new file mode 100644 index 00000000..21abe9ce --- /dev/null +++ b/frontend/src/components/IconSelect/index.module.less @@ -0,0 +1,35 @@ +.iconGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 16px; + max-height: 400px; + overflow-y: auto; +} + +.iconItem { + display: flex; + flex-direction: column; + align-items: center; + padding: 8px; + cursor: pointer; + border: 1px solid transparent; + border-radius: 4px; + transition: all 0.3s; + + &:hover { + border-color: #1890ff; + background: #e6f7ff; + } +} + +.icon { + font-size: 24px; + margin-bottom: 4px; +} + +.name { + font-size: 12px; + color: #666; + text-align: center; + word-break: break-all; +} \ No newline at end of file diff --git a/frontend/src/layouts/BasicLayout.tsx b/frontend/src/layouts/BasicLayout.tsx index 59a3e097..f620da9c 100644 --- a/frontend/src/layouts/BasicLayout.tsx +++ b/frontend/src/layouts/BasicLayout.tsx @@ -11,9 +11,8 @@ import { CloudOutlined } from '@ant-design/icons'; import * as AntdIcons from '@ant-design/icons'; -import {logout, setMenus, setUserInfo} from '../store/userSlice'; +import {logout, setMenus} from '../store/userSlice'; import type {MenuProps} from 'antd'; -import {getCurrentUser} from '../pages/Login/service'; import {getCurrentUserMenus} from '@/pages/System/Menu/service'; import {getWeather} from '../services/weather'; import type {MenuResponse} from '@/pages/System/Menu/types'; @@ -52,9 +51,11 @@ const BasicLayout: React.FC = () => { const initializeUserData = async () => { // try { setLoading(true); - const menuData = await getCurrentUserMenus() - dispatch(setMenus(menuData)); - setLoading(false); + const menuData = await getCurrentUserMenus().then((response) => { + console.log(response) + dispatch(setMenus(response)); + setLoading(false); + }) // } catch (error) { // console.log(error); // message.error('初始化用户数据失败'); diff --git a/frontend/src/pages/System/Menu/index.tsx b/frontend/src/pages/System/Menu/index.tsx index b05501c3..494e40db 100644 --- a/frontend/src/pages/System/Menu/index.tsx +++ b/frontend/src/pages/System/Menu/index.tsx @@ -1,339 +1,354 @@ import React, { useEffect, useState } from 'react'; -import { Table, Button, Modal, Form, Input, Space, message, Switch, Select, TreeSelect, Tooltip, InputNumber } from 'antd'; -import { PlusOutlined, EditOutlined, DeleteOutlined, QuestionCircleOutlined } from '@ant-design/icons'; -import * as AntdIcons from '@ant-design/icons'; -import { getMenuTree, createMenu, updateMenu, deleteMenu } from './service'; +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 } from './service'; +import type { MenuResponse } from './types'; +import { MenuTypeEnum, MenuTypeNames } from './types'; import IconSelect from '@/components/IconSelect'; import { useTableData } from '@/hooks/useTableData'; +import * as AntdIcons from '@ant-design/icons'; const MenuPage: React.FC = () => { - const { - list: menus, - pagination, - loading, - loadData: fetchMenus, - onPageChange, - handleCreate, - handleUpdate, - handleDelete - } = useTableData({ - service: { - baseUrl: '/api/v1/menu' - }, - defaultParams: { - sortField: 'sort', - sortOrder: 'asc' - } - }); - - const [menuTreeData, setMenuTreeData] = useState([]); - const [modalVisible, setModalVisible] = useState(false); - const [iconSelectVisible, setIconSelectVisible] = useState(false); - const [editingMenu, setEditingMenu] = useState(null); - const [form] = Form.useForm(); - - const getIcon = (iconName: string | undefined) => { - if (!iconName) return null; - const iconKey = `${iconName.charAt(0).toUpperCase() + iconName.slice(1)}Outlined`; - const IconComponent = (AntdIcons as any)[iconKey]; - return IconComponent ? : null; - }; - - const handleAdd = () => { - setEditingMenu(null); - form.resetFields(); - form.setFieldsValue({ - type: MenuTypeEnum.MENU, - sort: 0, - hidden: false, - enabled: true + const { + list: menus, + pagination, + loading, + loadData: fetchMenus, + onPageChange, + handleCreate, + handleUpdate, + handleDelete + } = useTableData({ + service: { + baseUrl: '/api/v1/menu' + }, + defaultParams: { + sortField: 'createTime', + sortOrder: 'descend', + pageSize: 1000 + } }); - setModalVisible(true); - }; - const handleEdit = (record: MenuDTO) => { - setEditingMenu(record); - form.setFieldsValue({ - ...record, - parentId: record.parentId === 0 ? undefined : record.parentId - }); - setModalVisible(true); - }; + const [modalVisible, setModalVisible] = useState(false); + const [iconSelectVisible, setIconSelectVisible] = useState(false); + const [editingMenu, setEditingMenu] = useState(null); + const [form] = Form.useForm(); - const handleSubmit = async () => { - try { - const values = await form.validateFields(); - const submitData = { - ...values, - parentId: values.parentId || 0 - }; + const handleAdd = () => { + setEditingMenu(null); + form.resetFields(); + form.setFieldsValue({ + type: MenuTypeEnum.MENU, + sort: 0, + hidden: false + }); + setModalVisible(true); + }; - if (editingMenu) { - await updateMenu(editingMenu.id, submitData); - message.success('更新成功'); - } else { - await createMenu(submitData); - message.success('创建成功'); - } - setModalVisible(false); - fetchMenus(); - } catch (error) { - message.error('操作失败'); - } - }; + const handleEdit = (record: MenuResponse) => { + setEditingMenu(record); + form.setFieldsValue({ + ...record, + parentId: record.parentId === 0 ? undefined : record.parentId + }); + setModalVisible(true); + }; - const getTreeSelectData = (menus: MenuDTO[]) => { - const treeData = menus.map(menu => ({ - title: menu.name, - value: menu.id, - children: menu.children?.map(child => ({ - title: child.name, - value: child.id, - disabled: editingMenu?.id === child.id - })) + 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 buildMenuTree = (menuList: MenuResponse[]): MenuResponse[] => { + const menuMap = new Map(); + const result: MenuResponse[] = []; + + menuList.forEach(menu => { + menuMap.set(menu.id, { ...menu, children: [] }); + }); + + menuList.forEach(menu => { + const node = menuMap.get(menu.id)!; + if (menu.parentId === 0 || !menuMap.has(menu.parentId)) { + result.push(node); + } else { + const parent = menuMap.get(menu.parentId)!; + parent.children = parent.children || []; + parent.children.push(node); + } + }); + + return result; + }; + + const getTreeSelectData = () => { + const menuTree = buildMenuTree(menus); + return menuTree.map(menu => ({ + title: menu.name, + value: menu.id, + children: menu.children?.map(child => ({ + title: child.name, + value: child.id, + disabled: editingMenu?.id === child.id + })) + })); + }; + + 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(); + } + } else { + const success = await handleCreate(values); + if (success) { + setModalVisible(false); + fetchMenus(); + } + } + } catch (error) { + console.error('操作失败:', error); + } + }; + + 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 treeData = buildMenuTree(menus).map(menu => ({ + key: menu.id, + title: ( +
+ + {menu.type === MenuTypeEnum.DIRECTORY ? : + menu.type === MenuTypeEnum.MENU ? : + } + {menu.name} + {menu.icon && getIcon(menu.icon)} + + + + + +
+ ), + children: menu.children?.map(child => ({ + key: child.id, + title: ( +
+ + {child.type === MenuTypeEnum.DIRECTORY ? : + child.type === MenuTypeEnum.MENU ? : + } + {child.name} + {child.icon && getIcon(child.icon)} + + + + + +
+ ) + })) })); - return treeData; - }; + return ( +
+
+ +
- const columns = [ - { - title: '菜单名称', - dataIndex: 'name', - key: 'name', - width: '200px', - }, - { - title: '类型', - dataIndex: 'type', - key: 'type', - width: '100px', - render: (type: MenuTypeEnum) => MenuTypeNames[type], - }, - { - title: '权限标识', - dataIndex: 'permission', - key: 'permission', - width: '150px', - }, - { - title: '路由地址', - dataIndex: 'path', - key: 'path', - width: '150px', - }, - { - title: '组件路径', - dataIndex: 'component', - key: 'component', - width: '150px', - }, - { - title: '排序', - dataIndex: 'sort', - key: 'sort', - width: '80px', - }, - { - title: '状态', - dataIndex: 'enabled', - key: 'enabled', - width: '80px', - render: (enabled: boolean) => ( - - ), - }, - { - title: '操作', - key: 'action', - width: '150px', - render: (_: any, record: MenuDTO) => ( - - - - - ), - }, - ]; - - return ( -
-
- -
- - - - setModalVisible(false)} - width={600} - destroyOnClose - > -
- - - - - - - - - - 上级菜单 - - - - - } - > - - - {form.getFieldValue('type') !== MenuTypeEnum.BUTTON && ( - <> - - 路由地址 - - - - - } - rules={[{ required: true, message: '请输入路由地址' }]} - > - - - - - 组件路径 - - - - - } - rules={[{ required: true, message: '请输入组件路径' }]} - > - - - - - setIconSelectVisible(true)} - suffix={form.getFieldValue('icon') && getIcon(form.getFieldValue('icon'))} - /> - - - )} - - {form.getFieldValue('type') === MenuTypeEnum.BUTTON && ( - - 权限标识 - - - - - } - rules={[{ required: true, message: '请输入权限标识' }]} + setModalVisible(false)} + width={600} + destroyOnClose > - - - )} + + + + - - - + + + - - - - -
+ + 上级菜单 + + + + + } + > + + - setIconSelectVisible(false)} - value={form.getFieldValue('icon')} - onChange={value => { - form.setFieldValue('icon', value); - setIconSelectVisible(false); - }} - /> - - ); + + + + + + + + + + + + + + setIconSelectVisible(true)} + suffix={form.getFieldValue('icon') && getIcon(form.getFieldValue('icon'))} + /> + + + + + + + + + + + + + setIconSelectVisible(false)} + value={form.getFieldValue('icon')} + onChange={value => { + form.setFieldValue('icon', value); + setIconSelectVisible(false); + }} + /> + + ); }; export default MenuPage; \ No newline at end of file diff --git a/frontend/src/pages/System/Menu/service.ts b/frontend/src/pages/System/Menu/service.ts index cc7323a9..7db94036 100644 --- a/frontend/src/pages/System/Menu/service.ts +++ b/frontend/src/pages/System/Menu/service.ts @@ -1,37 +1,39 @@ import request from '@/utils/request'; -import type { Page } from '@/types/base/page'; -import type { MenuResponse, MenuRequest, MenuQuery } from './types'; +import type {Page} from '@/types/base/page'; +import type {MenuResponse, MenuRequest, MenuQuery} from './types'; const BASE_URL = '/api/v1/menu'; // 获取菜单列表(分页) -export const getMenus = (params?: MenuQuery) => +export const getMenus = (params?: MenuQuery) => request.get>(`${BASE_URL}/page`, { params }); // 获取菜单树 -export const getMenuTree = () => +export const getMenuTree = () => request.get(`${BASE_URL}/tree`); // 获取当前用户菜单 -export const getCurrentUserMenus = () => - request.get(`${BASE_URL}/current`); +export const getCurrentUserMenus = () => { + return request.get(`${BASE_URL}/current`); +} + // 创建菜单 -export const createMenu = (data: MenuRequest) => +export const createMenu = (data: MenuRequest) => request.post(BASE_URL, data, { successMessage: '创建菜单成功' }); // 更新菜单 -export const updateMenu = (id: number, data: MenuRequest) => +export const updateMenu = (id: number, data: MenuRequest) => request.put(`${BASE_URL}/${id}`, data, { successMessage: '更新菜单成功' }); // 删除菜单 -export const deleteMenu = (id: number) => +export const deleteMenu = (id: number) => request.delete(`${BASE_URL}/${id}`, { successMessage: '删除菜单成功' }); \ No newline at end of file diff --git a/frontend/src/pages/System/Menu/types.ts b/frontend/src/pages/System/Menu/types.ts index 06f013af..ad8a5a73 100644 --- a/frontend/src/pages/System/Menu/types.ts +++ b/frontend/src/pages/System/Menu/types.ts @@ -1,31 +1,48 @@ +import type { BaseResponse } from '@/types/base/response'; + +export enum MenuTypeEnum { + DIRECTORY = 1, // 目录 + MENU = 2, // 菜单 + BUTTON = 3 // 按钮 +} + +export const MenuTypeNames = { + [MenuTypeEnum.DIRECTORY]: '目录', + [MenuTypeEnum.MENU]: '菜单', + [MenuTypeEnum.BUTTON]: '按钮' +}; + export interface MenuQuery { - name?: string; - path?: string; - enabled?: boolean; - pageNum?: number; - pageSize?: number; - sortField?: string; - sortOrder?: string; + pageNum?: number; + pageSize?: number; + sortField?: string; + sortOrder?: 'ascend' | 'descend'; + name?: string; + type?: MenuTypeEnum; + enabled?: boolean; } export interface MenuRequest { - name: string; - path?: string; - icon?: string; - parentId?: number; - sort?: number; - enabled?: boolean; + name: string; + type: MenuTypeEnum; + parentId?: number; + path?: string; + component?: string; + permission?: string; + icon?: string; + sort: number; + hidden: boolean; } -export interface MenuResponse { - id: number; - name: string; - path?: string; - icon?: string; - parentId?: number; - sort: number; - enabled: boolean; - children?: MenuResponse[]; - createTime: string; - updateTime: string; +export interface MenuResponse extends BaseResponse { + name: string; + type: MenuTypeEnum; + parentId: number; + path?: string; + component?: string; + permission?: string; + icon?: string; + sort: number; + hidden: boolean; + children?: MenuResponse[]; } \ No newline at end of file diff --git a/frontend/src/pages/System/Role/index.tsx b/frontend/src/pages/System/Role/index.tsx index a4061b31..16fddecf 100644 --- a/frontend/src/pages/System/Role/index.tsx +++ b/frontend/src/pages/System/Role/index.tsx @@ -1,228 +1,320 @@ -import React, { useEffect, useState } from 'react'; -import { Table, Button, Modal, Form, Input, Space, message } from 'antd'; -import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined } from '@ant-design/icons'; -import type { RoleDTO } from './types'; -import { getRoles, createRole, updateRole, deleteRole } from './service'; -import PermissionModal from './components/PermissionModal'; +import React, { useState, useEffect } from 'react'; +import { Table, Button, Modal, Form, Input, Space, message, InputNumber, Switch, Select, Tag } from 'antd'; +import { PlusOutlined, EditOutlined, DeleteOutlined, TagOutlined } from '@ant-design/icons'; +import type { RoleResponse, RoleTagResponse } from './types'; +import { useTableData } from '@/hooks/useTableData'; +import type { FixedType } from 'rc-table/lib/interface'; +import type { TablePaginationConfig } from 'antd/es/table'; +import type { FilterValue, SorterResult } from 'antd/es/table/interface'; +import { assignTags, getAllTags } from './service'; const RolePage: React.FC = () => { - const [roles, setRoles] = useState([]); - const [modalVisible, setModalVisible] = useState(false); - const [editingRole, setEditingRole] = useState(null); - const [loading, setLoading] = useState(false); - const [form] = Form.useForm(); - const [permissionModalVisible, setPermissionModalVisible] = useState(false); - const [selectedRole, setSelectedRole] = useState(null); - - const fetchRoles = async () => { - try { - setLoading(true); - const data = await getRoles(); - setRoles(data || []); - } catch (error) { - console.error('获取角色列表失败:', error); - message.error('获取角色列表失败'); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchRoles(); - }, []); - - const handleAdd = () => { - setEditingRole(null); - form.resetFields(); - form.setFieldsValue({ - sort: 0 - }); - setModalVisible(true); - }; - - const handleEdit = (record: RoleDTO) => { - setEditingRole(record); - form.setFieldsValue(record); - setModalVisible(true); - }; - - const handleDelete = async (id: number) => { - Modal.confirm({ - title: '确认删除', - content: '确定要删除这个角色吗?', - onOk: async () => { - try { - await deleteRole(id); - message.success('删除成功'); - fetchRoles(); - } catch (error) { - message.error('删除失败'); + const { + list: roles, + pagination, + loading, + loadData: fetchRoles, + onPageChange, + handleCreate, + handleUpdate, + handleDelete + } = useTableData({ + service: { + baseUrl: '/api/v1/role' + }, + defaultParams: { + sortField: 'createTime', + sortOrder: 'descend' } - }, }); - }; - const handleSubmit = async () => { - try { - const values = await form.validateFields(); - if (editingRole) { - await updateRole(editingRole.id, { - ...values, - version: editingRole.version + const [modalVisible, setModalVisible] = useState(false); + const [editingRole, setEditingRole] = useState(null); + const [form] = Form.useForm(); + + const [tagModalVisible, setTagModalVisible] = useState(false); + const [selectedRole, setSelectedRole] = useState(null); + const [selectedTags, setSelectedTags] = useState([]); + const [allTags, setAllTags] = useState([]); + + useEffect(() => { + getAllTags().then(response => { + setAllTags(response); }); - message.success('更新成功'); - } else { - await createRole(values); - message.success('创建成功'); - } - setModalVisible(false); - fetchRoles(); - } catch (error) { - message.error('操作失败'); - } - }; + }, []); - const handlePermission = (record: RoleDTO) => { - setSelectedRole(record); - setPermissionModalVisible(true); - }; + const handleAdd = () => { + setEditingRole(null); + form.resetFields(); + setModalVisible(true); + }; - const columns = [ - { - title: '角色名称', - dataIndex: 'name', - key: 'name', - width: '20%', - }, - { - title: '角色编码', - dataIndex: 'code', - key: 'code', - width: '20%', - }, - { - title: '描述', - dataIndex: 'description', - key: 'description', - width: '35%', - }, - { - title: '排序', - dataIndex: 'sort', - key: 'sort', - width: '10%', - }, - { - title: '操作', - key: 'action', - width: '15%', - render: (_: any, record: RoleDTO) => ( - - - - - - ), - }, - ]; + const handleEdit = (record: RoleResponse) => { + setEditingRole(record); + form.setFieldsValue(record); + setModalVisible(true); + }; - return ( -
-
- -
+ const handleSubmit = async () => { + try { + const values = await form.validateFields(); + if (editingRole) { + const success = await handleUpdate(editingRole.id, { + ...values, + version: editingRole.version + }); + if (success) { + setModalVisible(false); + fetchRoles(); + } + } else { + const success = await handleCreate(values); + if (success) { + setModalVisible(false); + fetchRoles(); + } + } + } catch (error) { + console.error('操作失败:', error); + } + }; -
+ const handleAssignTags = (record: RoleResponse) => { + setSelectedRole(record); + if (record.tags) { + setSelectedTags(record.tags.map(tag => tag.id)); + } else { + setSelectedTags([]); + } + setTagModalVisible(true); + }; - setModalVisible(false)} - width={600} - destroyOnClose - > -
- - - - - - - - - - - - - -
- - {selectedRole && ( - { - setPermissionModalVisible(false); - setSelectedRole(null); - }} - onSuccess={() => { - setPermissionModalVisible(false); - setSelectedRole(null); + const handleTagSubmit = async () => { + if (selectedRole) { + await assignTags(selectedRole.id, selectedTags); + setTagModalVisible(false); fetchRoles(); - }} - /> - )} - - ); + } + }; + + const columns = [ + { + title: 'ID', + dataIndex: 'id', + key: 'id', + width: 60, + fixed: 'left' as FixedType, + sorter: true + }, + { + title: '角色编码', + dataIndex: 'code', + key: 'code', + width: 120, + sorter: true + }, + { + title: '角色名称', + dataIndex: 'name', + key: 'name', + width: 120, + sorter: true + }, + { + title: '描述', + dataIndex: 'description', + key: 'description', + width: 200 + }, + { + title: '排序', + dataIndex: 'sort', + key: 'sort', + width: 80, + sorter: true + }, + { + title: '状态', + dataIndex: 'enabled', + key: 'enabled', + width: 80, + render: (enabled: boolean) => ( + + ) + }, + { + title: '创建时间', + dataIndex: 'createTime', + key: 'createTime', + width: 150, + sorter: true, + defaultSortOrder: 'descend' + }, + { + title: '更新时间', + dataIndex: 'updateTime', + key: 'updateTime', + width: 150, + sorter: true + }, + { + title: '标签', + dataIndex: 'tags', + key: 'tags', + width: 200, + render: (tags: RoleTagResponse[]) => ( + + {tags?.map(tag => ( + + {tag.name} + + ))} + + ) + }, + { + title: '操作', + key: 'action', + width: 180, + fixed: 'right' as FixedType, + render: (_: any, record: RoleResponse) => ( + + + + + + ) + } + ]; + + const handleTableChange = ( + pagination: TablePaginationConfig, + filters: Record, + sorter: SorterResult | SorterResult[] + ) => { + const { field, order } = Array.isArray(sorter) ? sorter[0] : sorter; + fetchRoles({ + sortField: field as string, + sortOrder: order + }); + }; + + return ( +
+
+ +
+ +
+ + setModalVisible(false)} + width={600} + destroyOnClose + > +
+ + + + + + + + + + + + + + + + +
+ + setTagModalVisible(false)} + width={600} + destroyOnClose + > +
+ + + + +
+ + ); }; export default RolePage; \ No newline at end of file diff --git a/frontend/src/pages/System/Role/service.ts b/frontend/src/pages/System/Role/service.ts index 1b436fbd..484e6abf 100644 --- a/frontend/src/pages/System/Role/service.ts +++ b/frontend/src/pages/System/Role/service.ts @@ -1,30 +1,32 @@ import request from '@/utils/request'; -import type { RoleResponse, RoleRequest, RoleQuery } from './types'; +import type { Page } from '@/types/base/page'; +import type { RoleResponse, RoleRequest, RoleQuery, RoleTagResponse } from './types'; -export const getRoles = async (params?: RoleQuery) => { - return request.get('/api/v1/role', { - params, - errorMessage: '获取角色列表失败,请刷新重试' - }); -}; +const BASE_URL = '/api/v1/role'; -export const createRole = async (data: RoleRequest) => { - return request.post('/api/v1/role', data, { - errorMessage: '创建角色失败,请稍后重试' - }); -}; +// 获取角色列表(分页) +export const getRoles = (params?: RoleQuery) => + request.get>(`${BASE_URL}/page`, { + params + }); -export const updateRole = async (id: number, data: RoleRequest) => { - return request.put(`/api/v1/role/${id}`, data, { - errorMessage: '更新角色失败,请稍后重试' - }); -}; +// 创建角色 +export const createRole = (data: RoleRequest) => + request.post(BASE_URL, data, { + successMessage: '创建角色成功' + }); -export const deleteRole = async (id: number) => { - return request.delete(`/api/v1/role/${id}`, { - errorMessage: '删除角色失败,请稍后重试' - }); -}; +// 更新角色 +export const updateRole = (id: number, data: RoleRequest) => + request.put(`${BASE_URL}/${id}`, data, { + successMessage: '更新角色成功' + }); + +// 删除角色 +export const deleteRole = (id: number) => + request.delete(`${BASE_URL}/${id}`, { + successMessage: '删除角色成功' + }); export const assignMenus = async (roleId: number, menuIds: number[]) => { return request.post(`/api/v1/role/${roleId}/menus`, menuIds, { @@ -36,4 +38,15 @@ export const getRoleMenus = async (roleId: number) => { return request.get(`/api/v1/role/${roleId}/menus`, { errorMessage: '获取角色菜单失败,请刷新重试' }); -}; \ No newline at end of file +}; + +// 分配标签 +export const assignTags = (roleId: number, tagIds: number[]) => + request.post(`${BASE_URL}/${roleId}/tags`, tagIds, { + successMessage: '分配标签成功' + }); + +// 获取所有标签 +export const getAllTags = () => + request.get('/api/v1/role-tag/list'); + \ No newline at end of file diff --git a/frontend/src/pages/System/Role/types.ts b/frontend/src/pages/System/Role/types.ts index 5c830fcf..0f6e7a5f 100644 --- a/frontend/src/pages/System/Role/types.ts +++ b/frontend/src/pages/System/Role/types.ts @@ -1,17 +1,38 @@ -export interface RoleDTO { - id: number; - name: string; - code: string; - description?: string; - sort: number; - version?: number; - createTime?: string; - updateTime?: string; -} +import type { BaseResponse } from '@/types/base/response'; export interface RoleQuery { - page: number; - size: number; - name?: string; - code?: string; + pageNum?: number; + pageSize?: number; + sortField?: string; + sortOrder?: 'ascend' | 'descend'; + code?: string; + name?: string; +} + +export interface RoleRequest { + code: string; + name: string; + description?: string; + sort: number; +} + +export interface RoleResponse extends BaseResponse { + code: string; + name: string; + description?: string; + sort: number; + enabled: boolean; + tags?: RoleTagResponse[]; +} + +export interface RoleTagRequest { + name: string; + color?: string; + description?: string; +} + +export interface RoleTagResponse extends BaseResponse { + name: string; + color: string; + description?: string; } \ No newline at end of file diff --git a/frontend/src/pages/System/User/index.tsx b/frontend/src/pages/System/User/index.tsx index 655d5d79..f0f6676d 100644 --- a/frontend/src/pages/System/User/index.tsx +++ b/frontend/src/pages/System/User/index.tsx @@ -3,10 +3,12 @@ import {Table, Button, Modal, Form, Input, Space, message, Switch, TreeSelect} f import {PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, TeamOutlined} from '@ant-design/icons'; import type {UserResponse, Role} from './types'; import type {DepartmentDTO} from '../Department/types'; -import {getUsers, createUser, updateUser, deleteUser, resetPassword} from './service'; +import {resetPassword} 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"; const UserPage: React.FC = () => { const { @@ -219,7 +221,6 @@ const UserPage: React.FC = () => { sorter: SorterResult | SorterResult[] ) => { const { field, order } = Array.isArray(sorter) ? sorter[0] : sorter; - fetchUsers({ sortField: field as string, sortOrder: order diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index 580daa29..c9bc04d3 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -41,53 +41,52 @@ const createRequestConfig = (options?: Partial): RequestOptions }; const responseHandler = (response: AxiosResponse>) => { - const res = response.data; + const data = response.data; const config = response.config as RequestOptions; - if (res.success && res.code === 200) { + if (data.success && data.code === 200) { config.successMessage && message.success(config.successMessage); - return res.data; + return data.data; } else { - message.error(config.errorMessage || res.message); - return Promise.reject(res); + message.error(config.errorMessage || data.message); + return Promise.reject(response); // 隐式返回 Promise } }; + +const statusMessages: Record = { + 401: '未授权,请重新登录', + 403: '拒绝访问', + 404: '请求错误,未找到该资源', + 500: '服务器错误' +}; + +const defaultErrorMessage = '服务器异常,请稍后再试!'; + const errorHandler = (error: any) => { const config = error.config as RequestOptions; - + + // 检查是否有响应 if (!error.response) { message.error('网络连接异常,请检查网络'); return Promise.reject(error); } + // 检查是否为取消请求 if (axios.isCancel(error)) { return Promise.reject(error); } const status = error.response.status; - let msg = ''; - switch (status) { - case 401: - msg = '未授权,请重新登录'; - break; - case 403: - msg = '拒绝访问'; - break; - case 404: - msg = '请求错误,未找到该资源'; - break; - case 500: - msg = '服务器错误'; - break; - default: - msg = '服务器异常,请稍后再试!'; - } + const msg = statusMessages[status] || defaultErrorMessage; // 获取相应的消息 + // 显示错误信息 message.error(config.errorMessage || msg); - return Promise.reject(error); + + return Promise.reject(error); // 拦截错误并返回 }; + request.interceptors.request.use( (config) => { const token = localStorage.getItem('token'); @@ -102,8 +101,9 @@ request.interceptors.request.use( request.interceptors.response.use(responseHandler, errorHandler); const http = { - get: (url: string, config?: RequestOptions) => - request.get>(url, createRequestConfig(config)), + get: (url: string, config?: RequestOptions) => { + return request.get(url, createRequestConfig(config)); + }, post: (url: string, data?: any, config?: RequestOptions) => request.post>(url, data, createRequestConfig(config)),