可正常启用
This commit is contained in:
parent
14a7477d55
commit
57e0f025b5
35
frontend/src/components/IconSelect/index.module.less
Normal file
35
frontend/src/components/IconSelect/index.module.less
Normal file
@ -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;
|
||||
}
|
||||
@ -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));
|
||||
const menuData = await getCurrentUserMenus().then((response) => {
|
||||
console.log(response)
|
||||
dispatch(setMenus(response));
|
||||
setLoading(false);
|
||||
})
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// message.error('初始化用户数据失败');
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
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 {
|
||||
@ -21,37 +25,29 @@ const MenuPage: React.FC = () => {
|
||||
baseUrl: '/api/v1/menu'
|
||||
},
|
||||
defaultParams: {
|
||||
sortField: 'sort',
|
||||
sortOrder: 'asc'
|
||||
sortField: 'createTime',
|
||||
sortOrder: 'descend',
|
||||
pageSize: 1000
|
||||
}
|
||||
});
|
||||
|
||||
const [menuTreeData, setMenuTreeData] = useState<MenuDTO[]>([]);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [iconSelectVisible, setIconSelectVisible] = useState(false);
|
||||
const [editingMenu, setEditingMenu] = useState<MenuDTO | null>(null);
|
||||
const [editingMenu, setEditingMenu] = useState<MenuResponse | null>(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 ? <IconComponent /> : null;
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingMenu(null);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
type: MenuTypeEnum.MENU,
|
||||
sort: 0,
|
||||
hidden: false,
|
||||
enabled: true
|
||||
hidden: false
|
||||
});
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (record: MenuDTO) => {
|
||||
const handleEdit = (record: MenuResponse) => {
|
||||
setEditingMenu(record);
|
||||
form.setFieldsValue({
|
||||
...record,
|
||||
@ -60,30 +56,44 @@ const MenuPage: React.FC = () => {
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const submitData = {
|
||||
...values,
|
||||
parentId: values.parentId || 0
|
||||
const handleTableChange = (
|
||||
pagination: TablePaginationConfig,
|
||||
filters: Record<string, FilterValue | null>,
|
||||
sorter: SorterResult<MenuResponse> | SorterResult<MenuResponse>[]
|
||||
) => {
|
||||
const { field, order } = Array.isArray(sorter) ? sorter[0] : sorter;
|
||||
|
||||
fetchMenus({
|
||||
sortField: field as string,
|
||||
sortOrder: order
|
||||
});
|
||||
};
|
||||
|
||||
if (editingMenu) {
|
||||
await updateMenu(editingMenu.id, submitData);
|
||||
message.success('更新成功');
|
||||
const buildMenuTree = (menuList: MenuResponse[]): MenuResponse[] => {
|
||||
const menuMap = new Map<number, MenuResponse>();
|
||||
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 {
|
||||
await createMenu(submitData);
|
||||
message.success('创建成功');
|
||||
}
|
||||
setModalVisible(false);
|
||||
fetchMenus();
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
const parent = menuMap.get(menu.parentId)!;
|
||||
parent.children = parent.children || [];
|
||||
parent.children.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const getTreeSelectData = (menus: MenuDTO[]) => {
|
||||
const treeData = menus.map(menu => ({
|
||||
const getTreeSelectData = () => {
|
||||
const menuTree = buildMenuTree(menus);
|
||||
return menuTree.map(menu => ({
|
||||
title: menu.name,
|
||||
value: menu.id,
|
||||
children: menu.children?.map(child => ({
|
||||
@ -92,82 +102,117 @@ const MenuPage: React.FC = () => {
|
||||
disabled: editingMenu?.id === child.id
|
||||
}))
|
||||
}));
|
||||
|
||||
return treeData;
|
||||
};
|
||||
|
||||
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) => (
|
||||
<Switch checked={enabled} disabled />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: '150px',
|
||||
render: (_: any, record: MenuDTO) => (
|
||||
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 ? <Icon /> : null;
|
||||
};
|
||||
|
||||
const treeData = buildMenuTree(menus).map(menu => ({
|
||||
key: menu.id,
|
||||
title: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
||||
<Space>
|
||||
{menu.type === MenuTypeEnum.DIRECTORY ? <FolderOutlined /> :
|
||||
menu.type === MenuTypeEnum.MENU ? <MenuOutlined /> :
|
||||
<ToolOutlined />}
|
||||
<span>{menu.name}</span>
|
||||
{menu.icon && getIcon(menu.icon)}
|
||||
</Space>
|
||||
<Space size={8}>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEdit(menu);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(record.id)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(menu.id);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
children: menu.children?.map(child => ({
|
||||
key: child.id,
|
||||
title: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
||||
<Space>
|
||||
{child.type === MenuTypeEnum.DIRECTORY ? <FolderOutlined /> :
|
||||
child.type === MenuTypeEnum.MENU ? <MenuOutlined /> :
|
||||
<ToolOutlined />}
|
||||
<span>{child.name}</span>
|
||||
{child.icon && getIcon(child.icon)}
|
||||
</Space>
|
||||
<Space size={8}>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEdit(child);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(child.id);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
}))
|
||||
}));
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
@ -177,15 +222,10 @@ const MenuPage: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={menus}
|
||||
rowKey="id"
|
||||
pagination={pagination}
|
||||
size="middle"
|
||||
bordered
|
||||
onChange={onPageChange}
|
||||
<Tree
|
||||
showLine={{ showLeafIcon: false }}
|
||||
defaultExpandAll
|
||||
treeData={treeData}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
@ -233,7 +273,7 @@ const MenuPage: React.FC = () => {
|
||||
}
|
||||
>
|
||||
<TreeSelect
|
||||
treeData={getTreeSelectData(menuTreeData)}
|
||||
treeData={getTreeSelectData()}
|
||||
placeholder="不选择则为顶级菜单"
|
||||
allowClear
|
||||
treeDefaultExpandAll
|
||||
@ -242,18 +282,9 @@ const MenuPage: React.FC = () => {
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{form.getFieldValue('type') !== MenuTypeEnum.BUTTON && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="path"
|
||||
label={
|
||||
<span>
|
||||
路由地址
|
||||
<Tooltip title="目录示例: /system 菜单示例: /system/user">
|
||||
<QuestionCircleOutlined style={{ marginLeft: 4 }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
label="路由地址"
|
||||
rules={[{ required: true, message: '请输入路由地址' }]}
|
||||
>
|
||||
<Input placeholder="请输入路由地址" />
|
||||
@ -261,19 +292,18 @@ const MenuPage: React.FC = () => {
|
||||
|
||||
<Form.Item
|
||||
name="component"
|
||||
label={
|
||||
<span>
|
||||
组件路径
|
||||
<Tooltip title="目录固定值: LAYOUT 菜单示例: /System/User/index">
|
||||
<QuestionCircleOutlined style={{ marginLeft: 4 }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
rules={[{ required: true, message: '请输入组件路径' }]}
|
||||
label="组件路径"
|
||||
>
|
||||
<Input placeholder="请输入组件路径" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="permission"
|
||||
label="权限标识"
|
||||
>
|
||||
<Input placeholder="请输入权限标识" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="icon"
|
||||
label="图标"
|
||||
@ -285,25 +315,6 @@ const MenuPage: React.FC = () => {
|
||||
suffix={form.getFieldValue('icon') && getIcon(form.getFieldValue('icon'))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{form.getFieldValue('type') === MenuTypeEnum.BUTTON && (
|
||||
<Form.Item
|
||||
name="permission"
|
||||
label={
|
||||
<span>
|
||||
权限标识
|
||||
<Tooltip title="示例: system:user:add">
|
||||
<QuestionCircleOutlined style={{ marginLeft: 4 }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
rules={[{ required: true, message: '请输入权限标识' }]}
|
||||
>
|
||||
<Input placeholder="请输入权限标识" />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
name="sort"
|
||||
@ -315,10 +326,14 @@ const MenuPage: React.FC = () => {
|
||||
|
||||
<Form.Item
|
||||
name="hidden"
|
||||
label="是否隐藏"
|
||||
label="隐藏菜单"
|
||||
valuePropName="checked"
|
||||
tooltip="设置为是则该菜单不会显示在导航栏中"
|
||||
>
|
||||
<Switch />
|
||||
<Switch
|
||||
checkedChildren="是"
|
||||
unCheckedChildren="否"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
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';
|
||||
|
||||
@ -15,8 +15,10 @@ export const getMenuTree = () =>
|
||||
request.get<MenuResponse[]>(`${BASE_URL}/tree`);
|
||||
|
||||
// 获取当前用户菜单
|
||||
export const getCurrentUserMenus = () =>
|
||||
request.get<MenuResponse[]>(`${BASE_URL}/current`);
|
||||
export const getCurrentUserMenus = () => {
|
||||
return request.get<MenuResponse[]>(`${BASE_URL}/current`);
|
||||
}
|
||||
|
||||
|
||||
// 创建菜单
|
||||
export const createMenu = (data: MenuRequest) =>
|
||||
|
||||
@ -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;
|
||||
sortOrder?: 'ascend' | 'descend';
|
||||
name?: string;
|
||||
type?: MenuTypeEnum;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface MenuRequest {
|
||||
name: string;
|
||||
path?: string;
|
||||
icon?: string;
|
||||
type: MenuTypeEnum;
|
||||
parentId?: number;
|
||||
sort?: number;
|
||||
enabled?: boolean;
|
||||
path?: string;
|
||||
component?: string;
|
||||
permission?: string;
|
||||
icon?: string;
|
||||
sort: number;
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
export interface MenuResponse {
|
||||
id: number;
|
||||
export interface MenuResponse extends BaseResponse {
|
||||
name: string;
|
||||
type: MenuTypeEnum;
|
||||
parentId: number;
|
||||
path?: string;
|
||||
component?: string;
|
||||
permission?: string;
|
||||
icon?: string;
|
||||
parentId?: number;
|
||||
sort: number;
|
||||
enabled: boolean;
|
||||
hidden: boolean;
|
||||
children?: MenuResponse[];
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
}
|
||||
@ -1,125 +1,187 @@
|
||||
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<RoleDTO[]>([]);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editingRole, setEditingRole] = useState<RoleDTO | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [permissionModalVisible, setPermissionModalVisible] = useState(false);
|
||||
const [selectedRole, setSelectedRole] = useState<RoleDTO | null>(null);
|
||||
|
||||
const fetchRoles = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await getRoles();
|
||||
setRoles(data || []);
|
||||
} catch (error) {
|
||||
console.error('获取角色列表失败:', error);
|
||||
message.error('获取角色列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
const {
|
||||
list: roles,
|
||||
pagination,
|
||||
loading,
|
||||
loadData: fetchRoles,
|
||||
onPageChange,
|
||||
handleCreate,
|
||||
handleUpdate,
|
||||
handleDelete
|
||||
} = useTableData({
|
||||
service: {
|
||||
baseUrl: '/api/v1/role'
|
||||
},
|
||||
defaultParams: {
|
||||
sortField: 'createTime',
|
||||
sortOrder: 'descend'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editingRole, setEditingRole] = useState<RoleResponse | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [tagModalVisible, setTagModalVisible] = useState(false);
|
||||
const [selectedRole, setSelectedRole] = useState<RoleResponse | null>(null);
|
||||
const [selectedTags, setSelectedTags] = useState<number[]>([]);
|
||||
const [allTags, setAllTags] = useState<RoleTagResponse[] | null>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRoles();
|
||||
getAllTags().then(response => {
|
||||
setAllTags(response);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingRole(null);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
sort: 0
|
||||
});
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (record: RoleDTO) => {
|
||||
const handleEdit = (record: RoleResponse) => {
|
||||
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 handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
if (editingRole) {
|
||||
await updateRole(editingRole.id, {
|
||||
const success = await handleUpdate(editingRole.id, {
|
||||
...values,
|
||||
version: editingRole.version
|
||||
});
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
await createRole(values);
|
||||
message.success('创建成功');
|
||||
}
|
||||
if (success) {
|
||||
setModalVisible(false);
|
||||
fetchRoles();
|
||||
}
|
||||
} else {
|
||||
const success = await handleCreate(values);
|
||||
if (success) {
|
||||
setModalVisible(false);
|
||||
fetchRoles();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
console.error('操作失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePermission = (record: RoleDTO) => {
|
||||
const handleAssignTags = (record: RoleResponse) => {
|
||||
setSelectedRole(record);
|
||||
setPermissionModalVisible(true);
|
||||
if (record.tags) {
|
||||
setSelectedTags(record.tags.map(tag => tag.id));
|
||||
} else {
|
||||
setSelectedTags([]);
|
||||
}
|
||||
setTagModalVisible(true);
|
||||
};
|
||||
|
||||
const handleTagSubmit = async () => {
|
||||
if (selectedRole) {
|
||||
await assignTags(selectedRole.id, selectedTags);
|
||||
setTagModalVisible(false);
|
||||
fetchRoles();
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '角色名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: '20%',
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 60,
|
||||
fixed: 'left' as FixedType,
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '角色编码',
|
||||
dataIndex: 'code',
|
||||
key: 'code',
|
||||
width: '20%',
|
||||
width: 120,
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '角色名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 120,
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: '35%',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort',
|
||||
key: 'sort',
|
||||
width: '10%',
|
||||
width: 80,
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
width: 80,
|
||||
render: (enabled: boolean) => (
|
||||
<Switch checked={enabled} disabled size="small" />
|
||||
)
|
||||
},
|
||||
{
|
||||
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[]) => (
|
||||
<Space>
|
||||
{tags?.map(tag => (
|
||||
<Tag key={tag.id} color={tag.color}>
|
||||
{tag.name}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: '15%',
|
||||
render: (_: any, record: RoleDTO) => (
|
||||
<Space>
|
||||
width: 180,
|
||||
fixed: 'right' as FixedType,
|
||||
render: (_: any, record: RoleResponse) => (
|
||||
<Space size={0}>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
@ -127,26 +189,39 @@ const RolePage: React.FC = () => {
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<KeyOutlined />}
|
||||
onClick={() => handlePermission(record)}
|
||||
disabled={record.code === 'ROLE_ADMIN'}
|
||||
>
|
||||
权限设置
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(record.id)}
|
||||
disabled={record.code === 'ROLE_ADMIN'}
|
||||
disabled={record.code === 'admin'}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<TagOutlined />}
|
||||
onClick={() => handleAssignTags(record)}
|
||||
>
|
||||
标签
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const handleTableChange = (
|
||||
pagination: TablePaginationConfig,
|
||||
filters: Record<string, FilterValue | null>,
|
||||
sorter: SorterResult<RoleResponse> | SorterResult<RoleResponse>[]
|
||||
) => {
|
||||
const { field, order } = Array.isArray(sorter) ? sorter[0] : sorter;
|
||||
fetchRoles({
|
||||
sortField: field as string,
|
||||
sortOrder: order
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
@ -160,7 +235,9 @@ const RolePage: React.FC = () => {
|
||||
columns={columns}
|
||||
dataSource={roles}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
scroll={{ x: 1500 }}
|
||||
pagination={pagination}
|
||||
onChange={handleTableChange}
|
||||
size="middle"
|
||||
bordered
|
||||
/>
|
||||
@ -177,6 +254,14 @@ const RolePage: React.FC = () => {
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="code"
|
||||
label="角色编码"
|
||||
rules={[{ required: true, message: '请输入角色编码' }]}
|
||||
>
|
||||
<Input placeholder="请输入角色编码" disabled={!!editingRole} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="角色名称"
|
||||
@ -184,43 +269,50 @@ const RolePage: React.FC = () => {
|
||||
>
|
||||
<Input placeholder="请输入角色名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="code"
|
||||
label="角色编码"
|
||||
rules={[{ required: true, message: '请输入角色编码' }]}
|
||||
>
|
||||
<Input placeholder="请输入角色编码" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="描述"
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="请输入描述" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="sort"
|
||||
label="排序"
|
||||
rules={[{ required: true, message: '请输入排序' }]}
|
||||
>
|
||||
<Input type="number" placeholder="请输入排序号" />
|
||||
<InputNumber style={{ width: '100%' }} min={0} placeholder="请输入排序" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{selectedRole && (
|
||||
<PermissionModal
|
||||
roleId={selectedRole.id}
|
||||
visible={permissionModalVisible}
|
||||
onCancel={() => {
|
||||
setPermissionModalVisible(false);
|
||||
setSelectedRole(null);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
setPermissionModalVisible(false);
|
||||
setSelectedRole(null);
|
||||
fetchRoles();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Modal
|
||||
title="分配标签"
|
||||
open={tagModalVisible}
|
||||
onOk={handleTagSubmit}
|
||||
onCancel={() => setTagModalVisible(false)}
|
||||
width={600}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="选择标签">
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="请选择标签"
|
||||
value={selectedTags}
|
||||
onChange={setSelectedTags}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{allTags?.map(tag => (
|
||||
<Select.Option key={tag.id} value={tag.id}>
|
||||
<Tag color={tag.color}>{tag.name}</Tag>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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<Page<RoleResponse>>(`${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<RoleResponse>(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<RoleResponse>(`${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, {
|
||||
@ -37,3 +39,14 @@ export const getRoleMenus = async (roleId: number) => {
|
||||
errorMessage: '获取角色菜单失败,请刷新重试'
|
||||
});
|
||||
};
|
||||
|
||||
// 分配标签
|
||||
export const assignTags = (roleId: number, tagIds: number[]) =>
|
||||
request.post(`${BASE_URL}/${roleId}/tags`, tagIds, {
|
||||
successMessage: '分配标签成功'
|
||||
});
|
||||
|
||||
// 获取所有标签
|
||||
export const getAllTags = () =>
|
||||
request.get<RoleTagResponse[]>('/api/v1/role-tag/list');
|
||||
|
||||
@ -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;
|
||||
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;
|
||||
}
|
||||
@ -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<UserResponse> | SorterResult<UserResponse>[]
|
||||
) => {
|
||||
const { field, order } = Array.isArray(sorter) ? sorter[0] : sorter;
|
||||
|
||||
fetchUsers({
|
||||
sortField: field as string,
|
||||
sortOrder: order
|
||||
|
||||
@ -41,53 +41,52 @@ const createRequestConfig = (options?: Partial<RequestOptions>): RequestOptions
|
||||
};
|
||||
|
||||
const responseHandler = (response: AxiosResponse<Response<any>>) => {
|
||||
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<number, string> = {
|
||||
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: <T = any>(url: string, config?: RequestOptions) =>
|
||||
request.get<any, Response<T>>(url, createRequestConfig(config)),
|
||||
get: <T = any>(url: string, config?: RequestOptions) => {
|
||||
return request.get<any, T>(url, createRequestConfig(config));
|
||||
},
|
||||
|
||||
post: <T = any>(url: string, data?: any, config?: RequestOptions) =>
|
||||
request.post<any, Response<T>>(url, data, createRequestConfig(config)),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user