|
|
@@ -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>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|