Ver Fonte

角色页面提交

pm há 5 meses atrás
pai
commit
bfa0268694

+ 108 - 0
src/api/Role.ts

@@ -0,0 +1,108 @@
+import axiosInstance from '../utils/axios';
+
+// 角色 VO
+export interface RoleVO {
+  id?: number;
+  name: string;
+  code: string;
+  sort: number;
+  status: number;
+  type: number;
+  dataScope: number;
+  dataScopeDeptIds?: number[];
+  remark?: string;
+  createTime?: Date;
+}
+
+// 更新状态请求 VO
+export interface UpdateStatusReqVO {
+  id: number;
+  status: number;
+}
+
+// 分页参数类型
+export interface PageParam {
+  pageNo?: number;
+  pageSize?: number;
+  name?: string;
+  code?: string;
+  status?: number;
+  createTime?: string[];
+  [key: string]: any;
+}
+
+// 分页响应类型
+export interface PageResponse<T> {
+  list: T[];
+  total: number;
+}
+
+// 角色管理 API
+export const roleApi = {
+  // 查询角色列表(分页)
+  getRolePage: (params?: PageParam) => {
+    return axiosInstance.get<PageResponse<RoleVO>>('/system/role/page', { params });
+  },
+
+  // 查询角色(精简)列表
+  getSimpleRoleList: () => {
+    return axiosInstance.get<RoleVO[]>('/system/role/simple-list');
+  },
+
+  // 查询角色详情
+  getRole: (id: number) => {
+    return axiosInstance.get<RoleVO>(`/system/role/get?id=${id}`);
+  },
+
+  // 新增角色
+  createRole: (data: RoleVO) => {
+    return axiosInstance.post('/system/role/create', data);
+  },
+
+  // 修改角色
+  updateRole: (data: RoleVO) => {
+    return axiosInstance.put('/system/role/update', data);
+  },
+
+  // 修改角色状态
+  updateRoleStatus: (data: UpdateStatusReqVO) => {
+    return axiosInstance.put('/system/role/update-status', data);
+  },
+
+  // 删除角色
+  deleteRole: (id: number) => {
+    return axiosInstance.delete(`/system/role/delete?id=${id}`);
+  },
+
+  // 导出角色
+  exportRole: (params?: PageParam) => {
+    return axiosInstance.get('/system/role/export-excel', { 
+      params,
+      responseType: 'blob'
+    });
+  },
+
+  // 获取角色菜单权限列表
+  getRoleMenuList: (roleId: number) => {
+    return axiosInstance.get<number[]>(`/system/permission/get-role-menu-list?roleId=${roleId}`);
+  },
+
+  // 分配角色菜单权限
+  assignRoleMenu: (params: {
+    roleId: number;
+    menuIds: number[];
+  }) => {
+    return axiosInstance.post('/system/permission/assign-role-menu', params);
+  },
+
+  // 分配角色数据权限
+  assignRoleDataScope: (params: {
+    roleId: number;
+    dataScope: number;
+    dataScopeDeptIds?: number[];
+  }) => {
+    return axiosInstance.put('/system/permission/assign-role-data-scope', params);
+  },
+};
+
+

+ 82 - 0
src/api/RolePage.ts

@@ -0,0 +1,82 @@
+import request from '@/config/axios';
+
+// 分页参数类型
+export interface PageParam {
+  pageNo?: number;
+  pageSize?: number;
+  [key: string]: any;
+}
+
+// 角色页面保存请求 VO
+export interface RolePageSaveReqVO {
+  id: number;
+  roleId: number;
+  roleName: string;
+  pageType?: number;
+}
+
+// 页面UI组件保存请求 VO
+export interface PageUiComponentSaveReqVO {
+  id: number;
+  pageId: number;
+  componentId: number;
+  componentRow?: string;
+  componentColumn?: string;
+}
+
+// 角色页面管理 API
+export const rolePageApi = {
+  // 查询角色页面数据列表
+  getRolePagePage: async (params?: PageParam) => {
+    return await request.get({ url: '/sys/role-page/getRolePagePage', params });
+  },
+
+  // 获取角色数据详细信息
+  selectRolePageById: async (id: number) => {
+    return await request.get({ url: '/sys/role-page/selectRolePageById', params: { id } });
+  },
+
+  // 创建角色页面
+  insertRolePage: async (data: RolePageSaveReqVO) => {
+    return await request.post({ url: '/sys/role-page/insertRolePage', data });
+  },
+
+  // 更新角色页面
+  updateRolePage: async (data: RolePageSaveReqVO) => {
+    return await request.put({ url: '/sys/role-page/updateRolePage', data });
+  },
+
+  // 删除角色页面数据
+  deleteRolePageList: async (ids: number[]) => {
+    return await request.delete({ url: `/sys/role-page/deleteRolePageList?ids=${ids.join(',')}` });
+  },
+};
+
+// 驾驶舱页面和组件关联 API
+export const pageUiComponentApi = {
+  // 获得分页数据
+  getPageUiComponentPage: async (params?: PageParam) => {
+    return await request.get({ url: '/sys/page-ui-component/getPageUiComponentPage', params });
+  },
+
+  // 获得详情数据
+  selectPageUiComponentById: async (id: number) => {
+    return await request.get({ url: '/sys/page-ui-component/selectPageUiComponentById', params: { id } });
+  },
+
+  // 创建页面和组件关联
+  insertPageUiComponent: async (data: PageUiComponentSaveReqVO) => {
+    return await request.post({ url: '/sys/page-ui-component/insertPageUiComponent', data });
+  },
+
+  // 修改页面和组件关联
+  updatePageUiComponent: async (data: PageUiComponentSaveReqVO) => {
+    return await request.put({ url: '/sys/page-ui-component/updatePageUiComponent', data });
+  },
+
+  // 批量删除
+  deletePageUiComponentList: async (ids: number[]) => {
+    return await request.delete({ url: `/sys/page-ui-component/deletePageUiComponentList?ids=${ids.join(',')}` });
+  },
+};
+

+ 7 - 0
src/api/index.ts

@@ -1,6 +1,8 @@
 import { loginApi } from './Login';
 import { menuApi } from './Menu';
 import { postApi } from './Post';
+import { roleApi } from './Role';
+import { rolePageApi, pageUiComponentApi } from './RolePage';
 import { userApi } from './user';
 import { userPermissionApi } from './user/permission';
 import { userImportApi } from './user/import';
@@ -20,6 +22,8 @@ export interface ApiResponse<T = any> {
 export { loginApi } from './Login';
 export { menuApi } from './Menu';
 export { postApi } from './Post';
+export { roleApi } from './Role';
+export { rolePageApi, pageUiComponentApi } from './RolePage';
 export { userApi } from './user';
 export { userPermissionApi } from './user/permission';
 export { userImportApi } from './user/import';
@@ -37,6 +41,9 @@ export default {
   auth: loginApi, // 兼容旧代码
   menu: menuApi,
   post: postApi,
+  role: roleApi,
+  rolePage: rolePageApi,
+  pageUiComponent: pageUiComponentApi,
   user: userApi,
   userPermission: userPermissionApi,
   userImport: userImportApi,

+ 253 - 0
src/components/RoleAssignMenuForm.tsx

@@ -0,0 +1,253 @@
+import React, { useState, useImperativeHandle, forwardRef, useEffect } from 'react';
+import { roleApi, RoleVO } from '../api/Role';
+import { menuApi, MenuVO } from '../api/Menu';
+import { toast } from 'sonner';
+import { Modal, Form, Tag, Card, Switch, Tree, Spin, Space } from 'antd';
+import { handleTree } from '../utils/tree';
+import type { DataNode } from 'antd/es/tree';
+
+interface RoleAssignMenuFormProps {
+  onSuccess?: () => void;
+}
+
+export interface RoleAssignMenuFormRef {
+  open: (row: RoleVO) => void;
+}
+
+const RoleAssignMenuForm = forwardRef<RoleAssignMenuFormRef, RoleAssignMenuFormProps>(({ onSuccess }, ref) => {
+  const [dialogVisible, setDialogVisible] = useState(false);
+  const [formLoading, setFormLoading] = useState(false);
+  const [formData, setFormData] = useState<{
+    id?: number;
+    name: string;
+    code: string;
+    menuIds: number[];
+  }>({
+    id: undefined,
+    name: '',
+    code: '',
+    menuIds: [],
+  });
+  const [menuOptions, setMenuOptions] = useState<DataNode[]>([]);
+  const [checkedKeys, setCheckedKeys] = useState<React.Key[]>([]);
+  const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
+  const [menuExpand, setMenuExpand] = useState(false);
+  const [treeNodeAll, setTreeNodeAll] = useState(false);
+  const [form] = Form.useForm();
+
+  // 将菜单数据转换为 Tree 组件需要的格式
+  const convertMenuToTreeData = (menus: MenuVO[]): DataNode[] => {
+    return menus.map((menu) => ({
+      title: menu.name,
+      key: menu.id!,
+      children: menu.children ? convertMenuToTreeData(menu.children) : undefined,
+    }));
+  };
+
+  // 暴露方法给父组件
+  useImperativeHandle(ref, () => ({
+    open: async (row: RoleVO) => {
+      setDialogVisible(true);
+      resetForm();
+      
+      // 设置数据
+      setFormData({
+        id: row.id,
+        name: row.name,
+        code: row.code,
+        menuIds: [],
+      });
+
+      // 加载菜单列表
+      try {
+        const menus = await menuApi.getSimpleMenusList();
+        const menuData = (menus as any)?.data || menus;
+        const treeData = handleTree(menuData || []);
+        const treeNodes = convertMenuToTreeData(treeData);
+        setMenuOptions(treeNodes);
+        
+        // 获取角色菜单权限列表
+        setFormLoading(true);
+        try {
+          const menuIds = await roleApi.getRoleMenuList(row.id!);
+          const menuIdsData = (menuIds as any)?.data || menuIds;
+          setFormData(prev => ({ ...prev, menuIds: menuIdsData || [] }));
+          setCheckedKeys((menuIdsData || []).map((id: number) => id));
+        } catch (error: any) {
+          console.error('获取角色菜单权限失败:', error);
+        } finally {
+          setFormLoading(false);
+        }
+      } catch (error: any) {
+        console.error('加载菜单列表失败:', error);
+        toast.error('加载菜单列表失败');
+      }
+    },
+  }));
+
+  // 提交表单
+  const submitForm = async () => {
+    try {
+      setFormLoading(true);
+      
+      // 获取所有选中的节点(包括半选中的父节点)
+      const checked = checkedKeys as number[];
+      
+      const data = {
+        roleId: formData.id!,
+        menuIds: checked,
+      };
+
+      await roleApi.assignRoleMenu(data);
+      toast.success('更新成功');
+      setDialogVisible(false);
+      onSuccess?.();
+    } catch (error: any) {
+      toast.error(error.message || '操作失败');
+    } finally {
+      setFormLoading(false);
+    }
+  };
+
+  // 重置表单
+  const resetForm = () => {
+    setTreeNodeAll(false);
+    setMenuExpand(false);
+    setFormData({
+      id: undefined,
+      name: '',
+      code: '',
+      menuIds: [],
+    });
+    setCheckedKeys([]);
+    setExpandedKeys([]);
+    form.resetFields();
+  };
+
+  // 全选/全不选
+  const handleCheckedTreeNodeAll = (checked: boolean) => {
+    setTreeNodeAll(checked);
+    if (checked) {
+      // 获取所有节点的 key
+      const getAllKeys = (nodes: DataNode[]): React.Key[] => {
+        let keys: React.Key[] = [];
+        nodes.forEach((node) => {
+          keys.push(node.key);
+          if (node.children) {
+            keys = keys.concat(getAllKeys(node.children));
+          }
+        });
+        return keys;
+      };
+      setCheckedKeys(getAllKeys(menuOptions));
+    } else {
+      setCheckedKeys([]);
+    }
+  };
+
+  // 展开/折叠全部
+  const handleCheckedTreeExpand = (expanded: boolean) => {
+    setMenuExpand(expanded);
+    if (expanded) {
+      const getAllKeys = (nodes: DataNode[]): React.Key[] => {
+        let keys: React.Key[] = [];
+        nodes.forEach((node) => {
+          keys.push(node.key);
+          if (node.children) {
+            keys = keys.concat(getAllKeys(node.children));
+          }
+        });
+        return keys;
+      };
+      setExpandedKeys(getAllKeys(menuOptions));
+    } else {
+      setExpandedKeys([]);
+    }
+  };
+
+  // 树节点选中变化
+  const onCheck = (checked: React.Key[]) => {
+    setCheckedKeys(checked);
+    // 如果全部选中,设置全选状态
+    const getAllKeys = (nodes: DataNode[]): React.Key[] => {
+      let keys: React.Key[] = [];
+      nodes.forEach((node) => {
+        keys.push(node.key);
+        if (node.children) {
+          keys = keys.concat(getAllKeys(node.children));
+        }
+      });
+      return keys;
+    };
+    const allKeys = getAllKeys(menuOptions);
+    setTreeNodeAll(checked.length === allKeys.length && allKeys.length > 0);
+  };
+
+  return (
+    <Modal
+      title="菜单权限"
+      open={dialogVisible}
+      onCancel={() => setDialogVisible(false)}
+      onOk={submitForm}
+      confirmLoading={formLoading}
+      width={800}
+      destroyOnClose
+    >
+      <Spin spinning={formLoading}>
+        <Form
+          form={form}
+          layout="horizontal"
+          labelCol={{ span: 4 }}
+          wrapperCol={{ span: 20 }}
+        >
+          <Form.Item label="角色名称">
+            <Tag>{formData.name}</Tag>
+          </Form.Item>
+
+          <Form.Item label="角色标识">
+            <Tag>{formData.code}</Tag>
+          </Form.Item>
+
+          <Form.Item label="菜单权限">
+            <Card
+              title={
+                <Space>
+                  <span>全选/全不选:</span>
+                  <Switch
+                    checked={treeNodeAll}
+                    onChange={handleCheckedTreeNodeAll}
+                    checkedChildren="是"
+                    unCheckedChildren="否"
+                  />
+                  <span style={{ marginLeft: 16 }}>全部展开/折叠:</span>
+                  <Switch
+                    checked={menuExpand}
+                    onChange={handleCheckedTreeExpand}
+                    checkedChildren="展开"
+                    unCheckedChildren="折叠"
+                  />
+                </Space>
+              }
+              style={{ maxHeight: 400, overflowY: 'auto' }}
+            >
+              <Tree
+                checkable
+                checkedKeys={checkedKeys}
+                expandedKeys={expandedKeys}
+                onCheck={onCheck}
+                onExpand={setExpandedKeys}
+                treeData={menuOptions}
+                defaultExpandAll={false}
+              />
+            </Card>
+          </Form.Item>
+        </Form>
+      </Spin>
+    </Modal>
+  );
+});
+
+RoleAssignMenuForm.displayName = 'RoleAssignMenuForm';
+
+export default RoleAssignMenuForm;
+

+ 277 - 0
src/components/RoleDataPermissionForm.tsx

@@ -0,0 +1,277 @@
+import React, { useState, useImperativeHandle, forwardRef, useEffect } from 'react';
+import { roleApi, RoleVO } from '../api/Role';
+import { deptApi, DeptVO } from '../api/dept';
+import { toast } from 'sonner';
+import { Modal, Form, Tag, Card, Switch, Tree, Spin, Select, Space } from 'antd';
+import { handleTree } from '../utils/tree';
+import { getIntDictOptions, DICT_TYPE } from '../utils/dict';
+import { SystemDataScopeEnum } from '../utils/constants';
+import type { DataNode } from 'antd/es/tree';
+
+interface RoleDataPermissionFormProps {
+  onSuccess?: () => void;
+}
+
+export interface RoleDataPermissionFormRef {
+  open: (row: RoleVO) => void;
+}
+
+const RoleDataPermissionForm = forwardRef<RoleDataPermissionFormRef, RoleDataPermissionFormProps>(({ onSuccess }, ref) => {
+  const [dialogVisible, setDialogVisible] = useState(false);
+  const [formLoading, setFormLoading] = useState(false);
+  const [formData, setFormData] = useState<{
+    id?: number;
+    name: string;
+    code: string;
+    dataScope?: number;
+    dataScopeDeptIds: number[];
+  }>({
+    id: undefined,
+    name: '',
+    code: '',
+    dataScope: undefined,
+    dataScopeDeptIds: [],
+  });
+  const [deptOptions, setDeptOptions] = useState<DataNode[]>([]);
+  const [checkedKeys, setCheckedKeys] = useState<React.Key[]>([]);
+  const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
+  const [deptExpand, setDeptExpand] = useState(true);
+  const [treeNodeAll, setTreeNodeAll] = useState(false);
+  const [checkStrictly, setCheckStrictly] = useState(true);
+  const [form] = Form.useForm();
+
+  // 将部门数据转换为 Tree 组件需要的格式
+  const convertDeptToTreeData = (depts: DeptVO[]): DataNode[] => {
+    return depts.map((dept) => ({
+      title: dept.name,
+      key: dept.id!,
+      children: (dept as any).children ? convertDeptToTreeData((dept as any).children) : undefined,
+    }));
+  };
+
+  // 暴露方法给父组件
+  useImperativeHandle(ref, () => ({
+    open: async (row: RoleVO) => {
+      setDialogVisible(true);
+      resetForm();
+      
+      // 加载部门列表
+      try {
+        const depts = await deptApi.getSimpleDeptList();
+        const deptData = (depts as any)?.data || depts;
+        const treeData = handleTree(deptData || []);
+        const treeNodes = convertDeptToTreeData(treeData);
+        setDeptOptions(treeNodes);
+        
+        // 设置数据
+        setFormData({
+          id: row.id,
+          name: row.name,
+          code: row.code,
+          dataScope: row.dataScope,
+          dataScopeDeptIds: row.dataScopeDeptIds || [],
+        });
+
+        form.setFieldsValue({
+          dataScope: row.dataScope,
+        });
+
+        // 如果是自定义数据权限,设置选中的部门
+        if (row.dataScope === SystemDataScopeEnum.DEPT_CUSTOM && row.dataScopeDeptIds) {
+          setCheckedKeys(row.dataScopeDeptIds.map((id: number) => id));
+          setExpandedKeys(row.dataScopeDeptIds.map((id: number) => id));
+        }
+      } catch (error: any) {
+        console.error('加载部门列表失败:', error);
+        toast.error('加载部门列表失败');
+      }
+    },
+  }));
+
+  // 提交表单
+  const submitForm = async () => {
+    try {
+      setFormLoading(true);
+      
+      const data = {
+        roleId: formData.id!,
+        dataScope: formData.dataScope!,
+        dataScopeDeptIds:
+          formData.dataScope !== SystemDataScopeEnum.DEPT_CUSTOM
+            ? []
+            : (checkedKeys as number[]),
+      };
+
+      await roleApi.assignRoleDataScope(data);
+      toast.success('更新成功');
+      setDialogVisible(false);
+      onSuccess?.();
+    } catch (error: any) {
+      toast.error(error.message || '操作失败');
+    } finally {
+      setFormLoading(false);
+    }
+  };
+
+  // 重置表单
+  const resetForm = () => {
+    setTreeNodeAll(false);
+    setDeptExpand(true);
+    setCheckStrictly(true);
+    setFormData({
+      id: undefined,
+      name: '',
+      code: '',
+      dataScope: undefined,
+      dataScopeDeptIds: [],
+    });
+    setCheckedKeys([]);
+    setExpandedKeys([]);
+    form.resetFields();
+  };
+
+  // 全选/全不选
+  const handleCheckedTreeNodeAll = (checked: boolean) => {
+    setTreeNodeAll(checked);
+    if (checked) {
+      const getAllKeys = (nodes: DataNode[]): React.Key[] => {
+        let keys: React.Key[] = [];
+        nodes.forEach((node) => {
+          keys.push(node.key);
+          if (node.children) {
+            keys = keys.concat(getAllKeys(node.children));
+          }
+        });
+        return keys;
+      };
+      setCheckedKeys(getAllKeys(deptOptions));
+    } else {
+      setCheckedKeys([]);
+    }
+  };
+
+  // 展开/折叠全部
+  const handleCheckedTreeExpand = (expanded: boolean) => {
+    setDeptExpand(expanded);
+    if (expanded) {
+      const getAllKeys = (nodes: DataNode[]): React.Key[] => {
+        let keys: React.Key[] = [];
+        nodes.forEach((node) => {
+          keys.push(node.key);
+          if (node.children) {
+            keys = keys.concat(getAllKeys(node.children));
+          }
+        });
+        return keys;
+      };
+      setExpandedKeys(getAllKeys(deptOptions));
+    } else {
+      setExpandedKeys([]);
+    }
+  };
+
+  // 数据权限范围变化
+  const handleDataScopeChange = (value: number) => {
+    setFormData(prev => ({ ...prev, dataScope: value }));
+    if (value !== SystemDataScopeEnum.DEPT_CUSTOM) {
+      setCheckedKeys([]);
+    }
+  };
+
+  const dataScopeOptions = getIntDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE);
+
+  return (
+    <Modal
+      title="数据权限"
+      open={dialogVisible}
+      onCancel={() => setDialogVisible(false)}
+      onOk={submitForm}
+      confirmLoading={formLoading}
+      width={800}
+      destroyOnClose
+    >
+      <Spin spinning={formLoading}>
+        <Form
+          form={form}
+          layout="horizontal"
+          labelCol={{ span: 4 }}
+          wrapperCol={{ span: 20 }}
+        >
+          <Form.Item label="角色名称">
+            <Tag>{formData.name}</Tag>
+          </Form.Item>
+
+          <Form.Item label="角色标识">
+            <Tag>{formData.code}</Tag>
+          </Form.Item>
+
+          <Form.Item
+            label="权限范围"
+            name="dataScope"
+            rules={[{ required: true, message: '请选择权限范围' }]}
+          >
+            <Select
+              placeholder="请选择权限范围"
+              onChange={handleDataScopeChange}
+            >
+              {dataScopeOptions.map((option) => (
+                <Select.Option key={option.value} value={option.value}>
+                  {option.label}
+                </Select.Option>
+              ))}
+            </Select>
+          </Form.Item>
+
+          {formData.dataScope === SystemDataScopeEnum.DEPT_CUSTOM && (
+            <Form.Item label="部门范围" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
+              <Card
+                title={
+                  <Space>
+                    <span>全选/全不选:</span>
+                    <Switch
+                      checked={treeNodeAll}
+                      onChange={handleCheckedTreeNodeAll}
+                      checkedChildren="是"
+                      unCheckedChildren="否"
+                    />
+                    <span style={{ marginLeft: 16 }}>全部展开/折叠:</span>
+                    <Switch
+                      checked={deptExpand}
+                      onChange={handleCheckedTreeExpand}
+                      checkedChildren="展开"
+                      unCheckedChildren="折叠"
+                    />
+                    <span style={{ marginLeft: 16 }}>父子联动(选中父节点,自动选择子节点):</span>
+                    <Switch
+                      checked={checkStrictly}
+                      onChange={setCheckStrictly}
+                      checkedChildren="是"
+                      unCheckedChildren="否"
+                    />
+                  </Space>
+                }
+                style={{ maxHeight: 400, overflowY: 'auto' }}
+              >
+                <Tree
+                  checkable
+                  checkedKeys={checkedKeys}
+                  expandedKeys={expandedKeys}
+                  onCheck={setCheckedKeys}
+                  onExpand={setExpandedKeys}
+                  treeData={deptOptions}
+                  checkStrictly={!checkStrictly}
+                  defaultExpandAll
+                />
+              </Card>
+            </Form.Item>
+          )}
+        </Form>
+      </Spin>
+    </Modal>
+  );
+});
+
+RoleDataPermissionForm.displayName = 'RoleDataPermissionForm';
+
+export default RoleDataPermissionForm;
+

+ 189 - 0
src/components/RoleForm.tsx

@@ -0,0 +1,189 @@
+import React, { useState, useImperativeHandle, forwardRef } from 'react';
+import { roleApi, RoleVO } from '../api/Role';
+import { toast } from 'sonner';
+import { Modal, Form, Input, Button, Select, Spin } from 'antd';
+import { CommonStatusEnum } from '../utils/constants';
+import { getIntDictOptions, DICT_TYPE } from '../utils/dict';
+
+interface RoleFormProps {
+  onSuccess?: () => void;
+}
+
+export interface RoleFormRef {
+  open: (type: string, id?: number) => void;
+}
+
+const RoleForm = forwardRef<RoleFormRef, RoleFormProps>(({ onSuccess }, ref) => {
+  const [dialogVisible, setDialogVisible] = useState(false);
+  const [dialogTitle, setDialogTitle] = useState('');
+  const [formType, setFormType] = useState<'create' | 'update'>('create');
+  const [formLoading, setFormLoading] = useState(false);
+  const [currentId, setCurrentId] = useState<number | undefined>();
+  const [form] = Form.useForm();
+
+  // 暴露方法给父组件
+  useImperativeHandle(ref, () => ({
+    open: (type: string, id?: number) => {
+      setDialogTitle(type === 'create' ? '新增角色' : '修改角色');
+      setFormType(type as 'create' | 'update');
+      setCurrentId(id);
+      form.resetFields();
+
+      if (id) {
+        loadRoleData(id);
+      } else {
+        form.setFieldsValue({
+          name: '',
+          code: '',
+          sort: 0,
+          status: CommonStatusEnum.ENABLE,
+          remark: '',
+        });
+        setFormLoading(false);
+      }
+
+      setDialogVisible(true);
+    },
+  }));
+
+  // 加载角色数据
+  const loadRoleData = async (id: number) => {
+    setFormLoading(true);
+    try {
+      const res = await roleApi.getRole(id);
+      const data = (res as any)?.data || res;
+      form.setFieldsValue({
+        name: data.name,
+        code: data.code,
+        sort: data.sort ?? 0,
+        status: typeof data.status === 'string' ? Number(data.status) : (data.status ?? CommonStatusEnum.ENABLE),
+        remark: data.remark || '',
+      });
+    } catch (error: any) {
+      toast.error(error.message || '获取角色详情失败');
+    } finally {
+      setFormLoading(false);
+    }
+  };
+
+  // 提交表单
+  const submitForm = async () => {
+    try {
+      const values = await form.validateFields();
+      setFormLoading(true);
+      
+      const submitData: RoleVO = {
+        ...values,
+        sort: Number(values.sort) || 0,
+        status: Number(values.status),
+        type: 1, // 默认类型,可根据实际需求调整
+        dataScope: 1, // 默认数据权限,可根据实际需求调整
+      };
+
+      if (formType === 'create') {
+        await roleApi.createRole(submitData);
+        toast.success('创建成功');
+      } else {
+        if (currentId) {
+          submitData.id = currentId;
+        }
+        await roleApi.updateRole(submitData);
+        toast.success('更新成功');
+      }
+      setDialogVisible(false);
+      onSuccess?.();
+    } catch (error: any) {
+      if (error.errorFields) {
+        // 表单验证错误
+        return;
+      }
+      toast.error(error.message || '操作失败');
+    } finally {
+      setFormLoading(false);
+    }
+  };
+
+  const statusOptions = getIntDictOptions(DICT_TYPE.COMMON_STATUS);
+
+  return (
+    <Modal
+      title={dialogTitle}
+      open={dialogVisible}
+      onCancel={() => setDialogVisible(false)}
+      onOk={submitForm}
+      confirmLoading={formLoading}
+      width={600}
+      destroyOnClose
+    >
+      <Spin spinning={formLoading && formType === 'update'}>
+        <Form
+          form={form}
+          layout="horizontal"
+          labelCol={{ span: 4 }}
+          wrapperCol={{ span: 20 }}
+          initialValues={{
+            sort: 0,
+            status: CommonStatusEnum.ENABLE,
+            remark: '',
+          }}
+        >
+          <Form.Item
+            label="角色名称"
+            name="name"
+            rules={[
+              { required: true, message: '角色名称不能为空' },
+              { max: 50, message: '角色名称不能超过50个字符' },
+            ]}
+          >
+            <Input placeholder="请输入角色名称" />
+          </Form.Item>
+
+          <Form.Item
+            label="角色标识"
+            name="code"
+            rules={[
+              { required: true, message: '角色标识不能为空' },
+              { max: 50, message: '角色标识不能超过50个字符' },
+            ]}
+          >
+            <Input placeholder="请输入角色标识" />
+          </Form.Item>
+
+          <Form.Item
+            label="显示顺序"
+            name="sort"
+            rules={[{ required: true, message: '显示顺序不能为空' }]}
+          >
+            <Input type="number" placeholder="请输入显示顺序" min={0} />
+          </Form.Item>
+
+          <Form.Item
+            label="状态"
+            name="status"
+            rules={[{ required: true, message: '状态不能为空' }]}
+          >
+            <Select placeholder="请选择状态">
+              {statusOptions.map((option) => (
+                <Select.Option key={option.value} value={option.value}>
+                  {option.label}
+                </Select.Option>
+              ))}
+            </Select>
+          </Form.Item>
+
+          <Form.Item
+            label="备注"
+            name="remark"
+          >
+            <Input.TextArea placeholder="请输入备注" rows={4} />
+          </Form.Item>
+        </Form>
+      </Spin>
+    </Modal>
+  );
+});
+
+RoleForm.displayName = 'RoleForm';
+
+export default RoleForm;
+

+ 389 - 0
src/components/RoleManagement.tsx

@@ -0,0 +1,389 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { Search, Plus, RefreshCw, Edit2, Trash2, Download, Settings, Shield } from 'lucide-react';
+import { roleApi, RoleVO, PageParam } from '../api/Role';
+import { toast } from 'sonner';
+import { formatDateTimeFull } from '../utils/formatTime';
+import { getIntDictOptions, DICT_TYPE, getDictLabel } from '../utils/dict';
+import { Modal } from 'antd';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+import { Button } from './ui/button';
+import { Input } from './ui/input';
+import { Label } from './ui/label';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
+import RoleForm, { RoleFormRef } from './RoleForm';
+import RoleAssignMenuForm, { RoleAssignMenuFormRef } from './RoleAssignMenuForm';
+import RoleDataPermissionForm, { RoleDataPermissionFormRef } from './RoleDataPermissionForm';
+
+export default function RoleManagement() {
+  const [loading, setLoading] = useState(true);
+  const [list, setList] = useState<RoleVO[]>([]);
+  const [total, setTotal] = useState(0);
+  const [queryParams, setQueryParams] = useState<PageParam>({
+    pageNo: 1,
+    pageSize: 10,
+    name: undefined,
+    code: undefined,
+  });
+  const [exportLoading, setExportLoading] = useState(false);
+  const formRef = useRef<RoleFormRef>(null);
+  const assignMenuFormRef = useRef<RoleAssignMenuFormRef>(null);
+  const dataPermissionFormRef = useRef<RoleDataPermissionFormRef>(null);
+
+  // 获取角色列表
+  const getList = async (params?: PageParam) => {
+    const currentParams = params || queryParams;
+    setLoading(true);
+    try {
+      const response = await roleApi.getRolePage(currentParams);
+      const data = (response as any)?.data || response;
+      
+      if (!data || typeof data !== 'object') {
+        setList([]);
+        setTotal(0);
+        return;
+      }
+      
+      setList(data.list || []);
+      setTotal(data.total || 0);
+    } catch (error: any) {
+      console.error('获取角色列表失败', error);
+      setList([]);
+      setTotal(0);
+      const errorMessage = error?.response?.data?.message || error?.message || '获取角色列表失败';
+      toast.error(errorMessage);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 组件挂载和分页参数变化时获取数据
+  useEffect(() => {
+    getList();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [queryParams.pageNo, queryParams.pageSize]);
+
+  // 搜索
+  const handleQuery = () => {
+    const newParams = { ...queryParams, pageNo: 1 };
+    setQueryParams(newParams);
+    getList(newParams);
+  };
+
+  // 重置搜索
+  const resetQuery = () => {
+    const resetParams: PageParam = {
+      pageNo: 1,
+      pageSize: 10,
+      name: undefined,
+      code: undefined,
+    };
+    setQueryParams(resetParams);
+    getList(resetParams);
+  };
+
+  // 打开表单
+  const openForm = (type: string, id?: number) => {
+    if (formRef.current) {
+      formRef.current.open(type, id);
+    }
+  };
+
+  // 打开菜单权限表单
+  const openAssignMenuForm = (row: RoleVO) => {
+    if (assignMenuFormRef.current) {
+      assignMenuFormRef.current.open(row);
+    }
+  };
+
+  // 打开数据权限表单
+  const openDataPermissionForm = (row: RoleVO) => {
+    if (dataPermissionFormRef.current) {
+      dataPermissionFormRef.current.open(row);
+    }
+  };
+
+  // 删除角色
+  const handleDelete = async (id: number, name: string) => {
+    Modal.confirm({
+      title: '确认删除',
+      icon: <ExclamationCircleOutlined />,
+      content: (
+        <div>
+          <p>确定要删除角色 <strong>"{name}"</strong> 吗?</p>
+          <p style={{ color: '#ff4d4f', marginTop: '8px' }}>删除后无法恢复,请谨慎操作!</p>
+        </div>
+      ),
+      okText: '确定删除',
+      okType: 'danger',
+      cancelText: '取消',
+      onOk: async () => {
+        try {
+          await roleApi.deleteRole(id);
+          toast.success('删除成功');
+          await getList();
+        } catch (error: any) {
+          toast.error(error.message || '删除失败');
+        }
+      },
+    });
+  };
+
+  // 导出角色
+  const handleExport = async () => {
+    Modal.confirm({
+      title: '确认导出',
+      icon: <ExclamationCircleOutlined />,
+      content: '确定要导出角色数据吗?',
+      okText: '确定导出',
+      cancelText: '取消',
+      onOk: async () => {
+        setExportLoading(true);
+        try {
+          const response = await roleApi.exportRole(queryParams);
+          const blob = (response as any)?.data || response;
+          const url = window.URL.createObjectURL(blob);
+          const link = document.createElement('a');
+          link.href = url;
+          link.download = '角色列表.xls';
+          document.body.appendChild(link);
+          link.click();
+          document.body.removeChild(link);
+          window.URL.revokeObjectURL(url);
+          toast.success('导出成功');
+        } catch (error: any) {
+          toast.error(error.message || '导出失败');
+        } finally {
+          setExportLoading(false);
+        }
+      },
+    });
+  };
+
+
+  // 获取状态标签
+  const getStatusLabel = (status: number) => {
+    return getDictLabel(DICT_TYPE.COMMON_STATUS, status) || (status === 0 ? '启用' : '禁用');
+  };
+
+  // 获取状态样式
+  const getStatusStyle = (status: number) => {
+    return status === 0
+      ? 'bg-green-100 text-green-700'
+      : 'bg-gray-100 text-gray-700';
+  };
+
+  // 获取角色类型标签
+  const getRoleTypeLabel = (type: number) => {
+    return getDictLabel(DICT_TYPE.SYSTEM_ROLE_TYPE, type) || '未知';
+  };
+
+  return (
+    <div className="space-y-4">
+      {/* 搜索栏 */}
+      <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm p-5">
+        <div className="flex items-center justify-between gap-4 flex-wrap">
+          {/* 搜索输入框 */}
+          <div className="flex items-center gap-3 flex-wrap flex-1">
+            <div className="flex items-center gap-3">
+              <Label htmlFor="role-name-search" className="text-sm font-medium text-gray-700 whitespace-nowrap">
+                角色名称
+              </Label>
+              <Input
+                id="role-name-search"
+                placeholder="请输入角色名称"
+                value={queryParams.name || ''}
+                onChange={(e) => setQueryParams(prev => ({ ...prev, name: e.target.value }))}
+                onKeyDown={(e) => e.key === 'Enter' && handleQuery()}
+                className="h-10 bg-gray-50 border-gray-200 focus:bg-white focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all w-60"
+              />
+            </div>
+
+            <div className="flex items-center gap-3">
+              <Label htmlFor="role-code-search" className="text-sm font-medium text-gray-700 whitespace-nowrap">
+                角色标识
+              </Label>
+              <Input
+                id="role-code-search"
+                placeholder="请输入角色标识"
+                value={queryParams.code || ''}
+                onChange={(e) => setQueryParams(prev => ({ ...prev, code: e.target.value }))}
+                onKeyDown={(e) => e.key === 'Enter' && handleQuery()}
+                className="h-10 bg-gray-50 border-gray-200 focus:bg-white focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all w-60"
+              />
+            </div>
+
+          </div>
+
+          {/* 操作按钮组 */}
+          <div className="flex items-center gap-3">
+            <button
+              onClick={handleQuery}
+              className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
+            >
+              <Search className="w-4 h-4" strokeWidth={2.5} />
+              <span className="text-sm">搜索</span>
+            </button>
+            
+            <button
+              onClick={resetQuery}
+              className="flex items-center gap-2 px-4 py-2.5 bg-white border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 hover:border-gray-400 transition-all duration-300"
+            >
+              <RefreshCw className="w-4 h-4" strokeWidth={2.5} />
+              <span className="text-sm">重置</span>
+            </button>
+            
+            <button
+              onClick={() => openForm('create')}
+              className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
+            >
+              <Plus className="w-4 h-4" strokeWidth={2.5} />
+              <span className="text-sm">新增</span>
+            </button>
+            
+            <button
+              onClick={handleExport}
+              disabled={exportLoading}
+              className="flex items-center gap-2 px-4 py-2.5 bg-white border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 hover:border-gray-400 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed"
+            >
+              <Download className="w-4 h-4" strokeWidth={2.5} />
+              <span className="text-sm">{exportLoading ? '导出中...' : '导出'}</span>
+            </button>
+          </div>
+        </div>
+      </div>
+
+      {/* 表格 */}
+      <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
+        <Table>
+          <TableHeader>
+            <TableRow>
+              <TableHead className="w-[100px] text-center">角色编号</TableHead>
+              <TableHead className="w-[150px]">角色名称</TableHead>
+              <TableHead className="w-[120px] text-center">角色类型</TableHead>
+              <TableHead className="w-[150px]">角色标识</TableHead>
+              <TableHead className="w-[100px] text-center">显示顺序</TableHead>
+              <TableHead>备注</TableHead>
+              <TableHead className="w-[100px] text-center">状态</TableHead>
+              <TableHead className="w-[180px] text-center">创建时间</TableHead>
+              <TableHead className="w-[350px] text-center">操作</TableHead>
+            </TableRow>
+          </TableHeader>
+          <TableBody>
+            {loading ? (
+              <TableRow>
+                <TableCell colSpan={9} className="text-center py-8 text-gray-500">
+                  加载中...
+                </TableCell>
+              </TableRow>
+            ) : list.length === 0 ? (
+              <TableRow>
+                <TableCell colSpan={9} className="text-center py-8 text-gray-500">
+                  暂无数据
+                </TableCell>
+              </TableRow>
+            ) : (
+              list.map((row) => (
+                <TableRow key={row.id} className="hover:bg-gray-50">
+                  <TableCell className="text-center">{row.id}</TableCell>
+                  <TableCell className="font-medium">{row.name}</TableCell>
+                  <TableCell className="text-center text-sm">
+                    {getRoleTypeLabel(row.type)}
+                  </TableCell>
+                  <TableCell>{row.code}</TableCell>
+                  <TableCell className="text-center">{row.sort}</TableCell>
+                  <TableCell className="text-sm text-gray-600">{row.remark || '-'}</TableCell>
+                  <TableCell className="text-center">
+                    <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusStyle(row.status)}`}>
+                      {getStatusLabel(row.status)}
+                    </span>
+                  </TableCell>
+                  <TableCell className="text-center text-sm text-gray-600">
+                    {formatDateTimeFull(row.createTime)}
+                  </TableCell>
+                  <TableCell>
+                    <div className="flex items-center gap-2 justify-center">
+                      <Button
+                        variant="ghost"
+                        size="sm"
+                        onClick={() => openForm('update', row.id)}
+                        className="h-8 px-2"
+                      >
+                        <Edit2 className="w-4 h-4" />
+                        <span className="ml-1">编辑</span>
+                      </Button>
+                      <Button
+                        variant="ghost"
+                        size="sm"
+                        onClick={() => openAssignMenuForm(row)}
+                        className="h-8 px-2"
+                      >
+                        <Settings className="w-4 h-4" />
+                        <span className="ml-1">菜单权限</span>
+                      </Button>
+                      <Button
+                        variant="ghost"
+                        size="sm"
+                        onClick={() => openDataPermissionForm(row)}
+                        className="h-8 px-2"
+                      >
+                        <Shield className="w-4 h-4" />
+                        <span className="ml-1">数据权限</span>
+                      </Button>
+                      <Button
+                        variant="ghost"
+                        size="sm"
+                        onClick={() => handleDelete(row.id!, row.name)}
+                        className="h-8 px-2 text-red-600 hover:text-red-700"
+                      >
+                        <Trash2 className="w-4 h-4" />
+                        <span className="ml-1">删除</span>
+                      </Button>
+                    </div>
+                  </TableCell>
+                </TableRow>
+              ))
+            )}
+          </TableBody>
+        </Table>
+      </div>
+
+      {/* 分页 */}
+      {!loading && list.length > 0 && (
+        <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
+          <div className="flex items-center justify-between">
+            <div className="text-sm text-gray-600">
+              共 <span className="text-blue-600 font-medium">{total}</span> 条记录
+            </div>
+            <div className="flex gap-2">
+              <Button
+                variant="outline"
+                size="sm"
+                onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! - 1 })}
+                disabled={queryParams.pageNo! <= 1}
+              >
+                上一页
+              </Button>
+              <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
+                {queryParams.pageNo} / {Math.ceil(total / queryParams.pageSize!) || 1}
+              </span>
+              <Button
+                variant="outline"
+                size="sm"
+                onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! + 1 })}
+                disabled={queryParams.pageNo! >= Math.ceil(total / queryParams.pageSize!)}
+              >
+                下一页
+              </Button>
+            </div>
+          </div>
+        </div>
+      )}
+
+      {/* 表单弹窗 */}
+      <RoleForm ref={formRef} onSuccess={getList} />
+      <RoleAssignMenuForm ref={assignMenuFormRef} onSuccess={getList} />
+      <RoleDataPermissionForm ref={dataPermissionFormRef} onSuccess={getList} />
+    </div>
+  );
+}
+

+ 14 - 0
src/components/SystemConfig.tsx

@@ -3,6 +3,7 @@ import { Plus, Search, Edit2, Trash2, MoreVertical, ChevronRight, ChevronDown }
 import DepartmentManagement from './DepartmentManagement';
 import MenuManagement from './MenuManagement';
 import PostManagement from './PostManagement';
+import RoleManagement from './RoleManagement';
 
 interface TableRow {
   id: number;
@@ -422,6 +423,19 @@ export default function SystemConfig({ subMenu }: SystemConfigProps) {
     console.log('SystemConfig: ❌ 不匹配,继续执行其他逻辑');
   }
 
+  // 角色管理使用专门的组件
+  // 支持多种可能的 subMenu 值:'角色管理'、'roleManagement'、'role'
+  const isRoleManagement = subMenu === '角色管理' || 
+                           subMenu === 'roleManagement' || 
+                           subMenu === 'role';
+  console.log('SystemConfig: 检查角色管理条件,subMenu =', subMenu, '是否匹配:', isRoleManagement);
+  if (isRoleManagement) {
+    console.log('SystemConfig: ✅ 匹配成功,渲染角色管理组件');
+    return <RoleManagement />;
+  } else {
+    console.log('SystemConfig: ❌ 不匹配,继续执行其他逻辑');
+  }
+
   // 字典管理使用卡片展示
   if (subMenu === '字典管理') {
     return (

+ 27 - 0
src/config/axios.ts

@@ -0,0 +1,27 @@
+import axiosInstance from '../utils/axios';
+
+// 包装 axiosInstance 为 request 对象,提供统一的 API 调用方式
+const request = {
+  get: async (config: { url: string; params?: any; responseType?: string }) => {
+    const { url, params, responseType } = config;
+    return await axiosInstance.get(url, { params, responseType: responseType as any });
+  },
+
+  post: async (config: { url: string; data?: any }) => {
+    const { url, data } = config;
+    return await axiosInstance.post(url, data);
+  },
+
+  put: async (config: { url: string; data?: any }) => {
+    const { url, data } = config;
+    return await axiosInstance.put(url, data);
+  },
+
+  delete: async (config: { url: string }) => {
+    const { url } = config;
+    return await axiosInstance.delete(url);
+  },
+};
+
+export default request;
+

+ 10 - 0
src/utils/constants.ts

@@ -9,3 +9,13 @@ export const CommonStatusEnum = {
   DISABLE: 1, // 禁用
 };
 
+// ========== SYSTEM 模块 ==========
+// 数据权限范围枚举
+export const SystemDataScopeEnum = {
+  ALL: 1, // 全部数据权限
+  DEPT_CUSTOM: 2, // 自定义数据权限
+  DEPT_ONLY: 3, // 本部门数据权限
+  DEPT_AND_CHILD: 4, // 本部门及以下数据权限
+  SELF: 5, // 仅本人数据权限
+};
+