增加审批组件

This commit is contained in:
dengqichen 2025-10-24 20:04:13 +08:00
parent dfa6abf0a9
commit 913f56f0f9
10 changed files with 1416 additions and 807 deletions

View File

@ -0,0 +1,166 @@
import React, { useEffect, useState } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { useToast } from '@/components/ui/use-toast';
import { Loader2, Tag as TagIcon } from 'lucide-react';
import type { RoleTagResponse } from '../types';
import { getAllTags, assignTags } from '../service';
interface AssignTagDialogProps {
open: boolean;
roleId: number;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
selectedTags?: RoleTagResponse[];
}
/**
*
*/
const AssignTagDialog: React.FC<AssignTagDialogProps> = ({
open,
roleId,
onOpenChange,
onSuccess,
selectedTags = []
}) => {
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [allTags, setAllTags] = useState<RoleTagResponse[]>([]);
const [selectedTagIds, setSelectedTagIds] = useState<number[]>([]);
useEffect(() => {
if (open) {
loadTags();
setSelectedTagIds(selectedTags.map(tag => tag.id));
}
}, [open, selectedTags]);
const loadTags = async () => {
try {
setLoading(true);
const tags = await getAllTags();
setAllTags(tags);
} catch (error) {
console.error('获取标签列表失败:', error);
toast({
title: '加载失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
} finally {
setLoading(false);
}
};
const handleToggle = (tagId: number) => {
setSelectedTagIds(prev =>
prev.includes(tagId)
? prev.filter(id => id !== tagId)
: [...prev, tagId]
);
};
const handleSubmit = async () => {
setSubmitting(true);
try {
await assignTags(roleId, selectedTagIds);
toast({
title: '分配成功',
description: '标签已成功分配给角色',
});
onSuccess();
} catch (error) {
console.error('分配标签失败:', error);
toast({
title: '分配失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
} finally {
setSubmitting(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<TagIcon className="h-5 w-5" />
</DialogTitle>
</DialogHeader>
<div className="py-4">
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin mr-2" />
<span className="text-sm text-muted-foreground">...</span>
</div>
) : (
<>
<Label className="mb-3 block"></Label>
<div className="space-y-3 max-h-[300px] overflow-y-auto border rounded-md p-4">
{allTags.length > 0 ? (
allTags.map(tag => (
<div key={tag.id} className="flex items-center space-x-3">
<Checkbox
id={`tag-${tag.id}`}
checked={selectedTagIds.includes(tag.id)}
onCheckedChange={() => handleToggle(tag.id)}
/>
<label
htmlFor={`tag-${tag.id}`}
className="flex items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer flex-1"
>
<Badge
variant="outline"
style={{ backgroundColor: tag.color, color: '#fff' }}
>
{tag.name}
</Badge>
{tag.description && (
<span className="text-muted-foreground text-xs">
{tag.description}
</span>
)}
</label>
</div>
))
) : (
<div className="text-sm text-muted-foreground text-center py-4">
</div>
)}
</div>
</>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={submitting}
>
</Button>
<Button onClick={handleSubmit} disabled={submitting || loading}>
{submitting ? '保存中...' : '确定'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default AssignTagDialog;

View File

@ -1,84 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Modal, Select, message, Spin } from 'antd';
import type { RoleTagResponse } from '../types';
import { getAllTags, assignTags } from '../service';
interface AssignTagModalProps {
visible: boolean;
roleId: number;
onCancel: () => void;
onSuccess: () => void;
selectedTags?: RoleTagResponse[];
}
const AssignTagModal: React.FC<AssignTagModalProps> = ({
visible,
roleId,
onCancel,
onSuccess,
selectedTags = []
}) => {
const [loading, setLoading] = useState(false);
const [allTags, setAllTags] = useState<RoleTagResponse[]>([]);
const [selectedTagIds, setSelectedTagIds] = useState<number[]>([]);
useEffect(() => {
if (visible) {
loadTags();
setSelectedTagIds(selectedTags.map(tag => tag.id));
}
}, [visible, selectedTags]);
const loadTags = async () => {
try {
setLoading(true);
const tags = await getAllTags();
setAllTags(tags);
} catch (error) {
message.error('获取标签列表失败');
} finally {
setLoading(false);
}
};
const handleOk = async () => {
try {
await assignTags(roleId, selectedTagIds);
message.success('标签分配成功');
onSuccess();
} catch (error) {
message.error('标签分配失败');
}
};
return (
<Modal
title="分配标签"
open={visible}
onCancel={onCancel}
onOk={handleOk}
width={500}
styles={{
body: {
padding: '12px'
}
}}
>
<Spin spinning={loading}>
<Select
mode="multiple"
style={{ width: '100%' }}
placeholder="请选择标签"
value={selectedTagIds}
onChange={setSelectedTagIds}
options={allTags.map(tag => ({
label: tag.name,
value: tag.id
}))}
/>
</Spin>
</Modal>
);
};
export default AssignTagModal;

View File

@ -0,0 +1,81 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogDescription,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { AlertCircle } from 'lucide-react';
import type { RoleResponse } from '../types';
interface DeleteDialogProps {
open: boolean;
record: RoleResponse | null;
onOpenChange: (open: boolean) => void;
onConfirm: () => Promise<void>;
}
/**
*
*/
const DeleteDialog: React.FC<DeleteDialogProps> = ({
open,
record,
onOpenChange,
onConfirm,
}) => {
if (!record) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-red-600">
<AlertCircle className="h-5 w-5" />
</DialogTitle>
<DialogDescription>
"<span className="font-semibold text-foreground">{record.name}</span>"
</DialogDescription>
</DialogHeader>
<div className="space-y-2 text-sm text-muted-foreground">
<p>
<span className="font-medium">:</span> {record.code}
</p>
{record.description && (
<p>
<span className="font-medium">:</span> {record.description}
</p>
)}
{record.tags && record.tags.length > 0 && (
<div>
<span className="font-medium">:</span>
<div className="flex flex-wrap gap-1 mt-1">
{record.tags.map(tag => (
<Badge key={tag.id} variant="outline" style={{ backgroundColor: tag.color }}>
{tag.name}
</Badge>
))}
</div>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button variant="destructive" onClick={onConfirm}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default DeleteDialog;

View File

@ -0,0 +1,162 @@
import React, { useEffect, useState } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { useToast } from '@/components/ui/use-toast';
import { createRole, updateRole } from '../service';
import type { RoleResponse, RoleRequest } from '../types';
interface EditDialogProps {
open: boolean;
record?: RoleResponse;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
}
/**
*
*/
const EditDialog: React.FC<EditDialogProps> = ({
open,
record,
onOpenChange,
onSuccess,
}) => {
const { toast } = useToast();
const [formData, setFormData] = useState<Partial<RoleRequest>>({
sort: 0,
});
useEffect(() => {
if (open) {
if (record) {
setFormData({
code: record.code,
name: record.name,
description: record.description,
sort: record.sort,
});
} else {
setFormData({ sort: 0 });
}
}
}, [open, record]);
const handleSubmit = async () => {
try {
// 验证
if (!formData.code?.trim()) {
toast({
title: '提示',
description: '请输入角色编码',
variant: 'destructive'
});
return;
}
if (!formData.name?.trim()) {
toast({
title: '提示',
description: '请输入角色名称',
variant: 'destructive'
});
return;
}
if (record) {
await updateRole(record.id, formData as RoleRequest);
toast({
title: '更新成功',
description: `角色 "${formData.name}" 已更新`,
});
} else {
await createRole(formData as RoleRequest);
toast({
title: '创建成功',
description: `角色 "${formData.name}" 已创建`,
});
}
onSuccess();
onOpenChange(false);
} catch (error) {
console.error('保存失败:', error);
toast({
title: '保存失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>{record ? '编辑角色' : '新建角色'}</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="code"> *</Label>
<Input
id="code"
value={formData.code || ''}
onChange={(e) => setFormData({ ...formData, code: e.target.value })}
placeholder="请输入角色编码"
disabled={!!record}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="name"> *</Label>
<Input
id="name"
value={formData.name || ''}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="请输入角色名称"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="sort"></Label>
<Input
id="sort"
type="number"
value={formData.sort || 0}
onChange={(e) => setFormData({ ...formData, sort: Number(e.target.value) })}
placeholder="请输入排序值"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={formData.description || ''}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="请输入描述"
rows={4}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSubmit}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default EditDialog;

View File

@ -0,0 +1,297 @@
import React, { useEffect, useState } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
import { useToast } from '@/components/ui/use-toast';
import { Loader2, KeyRound, Folder, Key } from 'lucide-react';
import { getPermissionTree } from '../service';
interface PermissionDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: (permissionIds: number[]) => Promise<void>;
defaultCheckedKeys?: number[];
}
interface Permission {
id: number;
code: string;
name: string;
type: string;
sort: number;
}
interface MenuItem {
id: number;
name: string;
path: string;
icon: string;
type: number;
permissionChildren: MenuItem[];
permissions: Permission[];
}
/**
* 使 Accordion
*/
const PermissionDialog: React.FC<PermissionDialogProps> = ({
open,
onOpenChange,
onConfirm,
defaultCheckedKeys = []
}) => {
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [menuTree, setMenuTree] = useState<MenuItem[]>([]);
const [checkedPermissionIds, setCheckedPermissionIds] = useState<number[]>([]);
useEffect(() => {
const loadPermissionTree = async () => {
if (!open) return;
setLoading(true);
try {
const response = await getPermissionTree();
setMenuTree(response);
setCheckedPermissionIds(defaultCheckedKeys);
} catch (error) {
console.error('获取权限树失败:', error);
toast({
title: '加载失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
} finally {
setLoading(false);
}
};
loadPermissionTree();
}, [open, defaultCheckedKeys]);
// 切换权限选中状态
const handleTogglePermission = (permissionId: number) => {
setCheckedPermissionIds(prev =>
prev.includes(permissionId)
? prev.filter(id => id !== permissionId)
: [...prev, permissionId]
);
};
// 递归获取菜单下所有权限ID
const getAllPermissionIds = (menu: MenuItem): number[] => {
const ids: number[] = [];
if (menu.permissions) {
ids.push(...menu.permissions.map(p => p.id));
}
if (menu.permissionChildren) {
menu.permissionChildren.forEach(child => {
ids.push(...getAllPermissionIds(child));
});
}
return ids;
};
// 检查菜单下所有权限是否全选
const isMenuFullyChecked = (menu: MenuItem): boolean => {
const allIds = getAllPermissionIds(menu);
return allIds.length > 0 && allIds.every(id => checkedPermissionIds.includes(id));
};
// 检查菜单下是否有部分权限选中
const isMenuPartiallyChecked = (menu: MenuItem): boolean => {
const allIds = getAllPermissionIds(menu);
const checkedCount = allIds.filter(id => checkedPermissionIds.includes(id)).length;
return checkedCount > 0 && checkedCount < allIds.length;
};
// 切换菜单的全选/取消全选
const handleToggleMenu = (menu: MenuItem) => {
const allIds = getAllPermissionIds(menu);
const isFullyChecked = isMenuFullyChecked(menu);
if (isFullyChecked) {
// 取消全选
setCheckedPermissionIds(prev => prev.filter(id => !allIds.includes(id)));
} else {
// 全选
setCheckedPermissionIds(prev => {
const newIds = [...prev];
allIds.forEach(id => {
if (!newIds.includes(id)) {
newIds.push(id);
}
});
return newIds;
});
}
};
// 渲染菜单项
const renderMenuItem = (menu: MenuItem, level: number = 0): React.ReactNode => {
const hasChildren = menu.permissionChildren && menu.permissionChildren.length > 0;
const hasPermissions = menu.permissions && menu.permissions.length > 0;
const isChecked = isMenuFullyChecked(menu);
const isPartial = isMenuPartiallyChecked(menu);
if (!hasChildren && !hasPermissions) {
return null;
}
return (
<div key={menu.id} className={level > 0 ? 'ml-4' : ''}>
{hasChildren ? (
<AccordionItem value={`menu-${menu.id}`} className="border-b-0">
<AccordionTrigger className="py-2 hover:no-underline hover:bg-muted/50 px-2 rounded">
<div className="flex items-center gap-2 flex-1">
<Checkbox
checked={isChecked}
ref={(el) => {
if (el) {
// @ts-ignore - 设置 indeterminate 状态
el.indeterminate = isPartial && !isChecked;
}
}}
onCheckedChange={() => handleToggleMenu(menu)}
onClick={(e) => e.stopPropagation()}
/>
<Folder className="h-4 w-4 text-blue-500" />
<span className="text-sm font-medium">{menu.name}</span>
</div>
</AccordionTrigger>
<AccordionContent className="pb-0 pt-2">
{/* 渲染当前菜单的权限 */}
{hasPermissions && (
<div className="space-y-2 mb-2 ml-8">
{menu.permissions.map(permission => (
<div key={permission.id} className="flex items-center gap-2 py-1">
<Checkbox
id={`permission-${permission.id}`}
checked={checkedPermissionIds.includes(permission.id)}
onCheckedChange={() => handleTogglePermission(permission.id)}
/>
<label
htmlFor={`permission-${permission.id}`}
className="text-sm cursor-pointer flex items-center gap-2"
>
<Key className="h-3 w-3 text-muted-foreground" />
{permission.name}
</label>
</div>
))}
</div>
)}
{/* 递归渲染子菜单 */}
<Accordion type="multiple" className="w-full">
{menu.permissionChildren.map(child => renderMenuItem(child, level + 1))}
</Accordion>
</AccordionContent>
</AccordionItem>
) : (
// 没有子菜单,只显示权限列表
<div className="py-2 px-2">
<div className="flex items-center gap-2 mb-2">
<Checkbox
checked={isChecked}
ref={(el) => {
if (el) {
// @ts-ignore
el.indeterminate = isPartial && !isChecked;
}
}}
onCheckedChange={() => handleToggleMenu(menu)}
/>
<Folder className="h-4 w-4 text-blue-500" />
<span className="text-sm font-medium">{menu.name}</span>
</div>
{hasPermissions && (
<div className="space-y-2 ml-8">
{menu.permissions.map(permission => (
<div key={permission.id} className="flex items-center gap-2 py-1">
<Checkbox
id={`permission-${permission.id}`}
checked={checkedPermissionIds.includes(permission.id)}
onCheckedChange={() => handleTogglePermission(permission.id)}
/>
<label
htmlFor={`permission-${permission.id}`}
className="text-sm cursor-pointer flex items-center gap-2"
>
<Key className="h-3 w-3 text-muted-foreground" />
{permission.name}
</label>
</div>
))}
</div>
)}
</div>
)}
</div>
);
};
const handleSubmit = async () => {
setSubmitting(true);
try {
await onConfirm(checkedPermissionIds);
onOpenChange(false);
} catch (error) {
console.error('分配权限失败:', error);
} finally {
setSubmitting(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[700px] max-h-[80vh]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<KeyRound className="h-5 w-5" />
</DialogTitle>
</DialogHeader>
<div className="overflow-y-auto max-h-[500px] pr-2">
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin mr-2" />
<span className="text-sm text-muted-foreground">...</span>
</div>
) : menuTree.length > 0 ? (
<Accordion type="multiple" className="w-full">
{menuTree.map(menu => renderMenuItem(menu))}
</Accordion>
) : (
<div className="flex items-center justify-center py-12 text-muted-foreground">
</div>
)}
</div>
<DialogFooter>
<div className="flex items-center justify-between w-full">
<span className="text-sm text-muted-foreground">
{checkedPermissionIds.length}
</span>
<div className="flex gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={submitting}>
</Button>
<Button onClick={handleSubmit} disabled={submitting || loading}>
{submitting ? '保存中...' : '确定'}
</Button>
</div>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default PermissionDialog;

View File

@ -1,179 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Modal, Tree, Spin } from 'antd';
import type { DataNode } from 'antd/es/tree';
import type { Key } from 'rc-tree/lib/interface';
import { getPermissionTree } from '../service';
interface PermissionModalProps {
visible: boolean;
roleId: number;
onCancel: () => void;
onOk: (permissionIds: number[]) => void;
defaultCheckedKeys?: number[];
}
interface Permission {
id: number;
code: string;
name: string;
type: string;
sort: number;
}
interface MenuItem {
id: number;
name: string;
path: string;
icon: string;
type: number;
permissionChildren: MenuItem[];
permissions: Permission[];
}
const PermissionModal: React.FC<PermissionModalProps> = ({
visible,
roleId,
onCancel,
onOk,
defaultCheckedKeys = []
}) => {
const [loading, setLoading] = useState(false);
const [treeData, setTreeData] = useState<DataNode[]>([]);
const [checkedKeys, setCheckedKeys] = useState<number[]>([]);
const [idMapping, setIdMapping] = useState<Record<string, number>>({});
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
// 将菜单和权限数据转换为Tree组件需要的格式
const convertToTreeData = (menuList: MenuItem[]): { treeData: DataNode[], idMapping: Record<string, number>, expandedKeys: string[] } => {
const mapping: Record<string, number> = {};
const expanded: string[] = [];
const convertMenuToNode = (menu: MenuItem): DataNode => {
const children: DataNode[] = [];
const menuKey = `menu-${menu.id}`;
expanded.push(menuKey);
// 添加功能权限节点
if (menu.permissions?.length > 0) {
menu.permissions.forEach(perm => {
const key = `permission-${perm.id}`;
mapping[key] = perm.id;
children.push({
key,
title: perm.name,
isLeaf: true
});
});
}
// 递归处理子菜单
if (menu.permissionChildren?.length > 0) {
menu.permissionChildren.forEach(child => {
children.push(convertMenuToNode(child));
});
}
return {
key: menuKey,
title: menu.name,
children: children.length > 0 ? children : undefined,
selectable: false // 菜单节点不可选择,只能展开/收缩
};
};
const treeData = menuList.map(convertMenuToNode);
return { treeData, idMapping: mapping, expandedKeys: expanded };
};
useEffect(() => {
const loadPermissionTree = async () => {
if (!visible) return;
setLoading(true);
try {
const response = await getPermissionTree();
const { treeData: newTreeData, idMapping: newIdMapping, expandedKeys: newExpandedKeys } = convertToTreeData(response);
setTreeData(newTreeData);
setIdMapping(newIdMapping);
setExpandedKeys(newExpandedKeys);
// 设置选中的权限
setCheckedKeys(defaultCheckedKeys);
console.log('权限树数据:', response);
console.log('转换后的树数据:', newTreeData);
console.log('ID映射:', newIdMapping);
console.log('默认选中的权限:', defaultCheckedKeys);
} catch (error) {
console.error('获取权限树失败:', error);
} finally {
setLoading(false);
}
};
loadPermissionTree();
}, [visible, defaultCheckedKeys]);
const handleCheck = (checked: Key[] | { checked: Key[]; halfChecked: Key[] }) => {
const checkedKeys = Array.isArray(checked) ? checked : checked.checked;
const permissionIds: number[] = [];
checkedKeys.forEach(key => {
const keyStr = key.toString();
if (keyStr.startsWith('permission-')) {
const id = idMapping[keyStr];
if (id) {
permissionIds.push(id);
}
}
});
console.log('选中的权限ID:', permissionIds);
setCheckedKeys(permissionIds);
};
const handleExpand = (newExpandedKeys: Key[]) => {
setExpandedKeys(newExpandedKeys);
};
const handleOk = () => {
onOk(checkedKeys);
};
// 将权限ID转换为Tree需要的key
const getTreeCheckedKeys = (): string[] => {
console.log('当前选中的权限:', checkedKeys);
return checkedKeys.map(id => `permission-${id}`);
};
return (
<Modal
title="分配权限"
open={visible}
onCancel={onCancel}
onOk={handleOk}
width={600}
styles={{
body: {
padding: '12px',
maxHeight: 'calc(100vh - 250px)',
overflow: 'auto'
}
}}
>
<Spin spinning={loading}>
<Tree
checkable
checkedKeys={getTreeCheckedKeys()}
expandedKeys={expandedKeys}
onExpand={handleExpand}
onCheck={handleCheck}
treeData={treeData}
autoExpandParent={false}
/>
</Spin>
</Modal>
);
};
export default PermissionModal;

View File

@ -0,0 +1,296 @@
import React, { useEffect, useState } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table';
import { useToast } from '@/components/ui/use-toast';
import { Loader2, Plus, Edit, Trash2, Settings } from 'lucide-react';
import type { RoleTagResponse, RoleTagRequest } from '../types';
import { getAllTags, createRoleTag, updateRoleTag, deleteRoleTag } from '../service';
interface TagDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
}
/**
*
*/
const TagDialog: React.FC<TagDialogProps> = ({
open,
onOpenChange,
onSuccess
}) => {
const { toast } = useToast();
const [tags, setTags] = useState<RoleTagResponse[]>([]);
const [loading, setLoading] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [editingTag, setEditingTag] = useState<RoleTagResponse | null>(null);
const [formData, setFormData] = useState<Partial<RoleTagRequest>>({
color: '#1677ff',
});
useEffect(() => {
if (open) {
loadTags();
}
}, [open]);
const loadTags = async () => {
try {
setLoading(true);
const data = await getAllTags();
setTags(data);
} catch (error) {
console.error('获取标签列表失败:', error);
toast({
title: '加载失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
} finally {
setLoading(false);
}
};
const handleAdd = () => {
setEditingTag(null);
setFormData({ color: '#1677ff' });
setEditDialogOpen(true);
};
const handleEdit = (record: RoleTagResponse) => {
setEditingTag(record);
setFormData({
name: record.name,
color: record.color,
description: record.description,
});
setEditDialogOpen(true);
};
const handleDelete = async (id: number, name: string) => {
try {
await deleteRoleTag(id);
toast({
title: '删除成功',
description: `标签 "${name}" 已删除`,
});
loadTags();
} catch (error) {
console.error('删除失败:', error);
toast({
title: '删除失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
}
};
const handleSubmit = async () => {
try {
// 验证
if (!formData.name?.trim()) {
toast({
title: '提示',
description: '请输入标签名称',
variant: 'destructive'
});
return;
}
if (!formData.color) {
toast({
title: '提示',
description: '请选择标签颜色',
variant: 'destructive'
});
return;
}
if (editingTag) {
await updateRoleTag(editingTag.id, formData as RoleTagRequest);
toast({
title: '更新成功',
description: `标签 "${formData.name}" 已更新`,
});
} else {
await createRoleTag(formData as RoleTagRequest);
toast({
title: '创建成功',
description: `标签 "${formData.name}" 已创建`,
});
}
setEditDialogOpen(false);
loadTags();
} catch (error) {
console.error('保存失败:', error);
toast({
title: '保存失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
}
};
return (
<>
{/* 标签列表对话框 */}
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[700px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Settings className="h-5 w-5" />
</DialogTitle>
</DialogHeader>
<div className="py-4">
<div className="mb-4">
<Button onClick={handleAdd}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[200px]"></TableHead>
<TableHead></TableHead>
<TableHead className="w-[150px] text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={3} className="h-24 text-center">
<div className="flex items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin mr-2" />
<span className="text-sm text-muted-foreground">...</span>
</div>
</TableCell>
</TableRow>
) : tags.length > 0 ? (
tags.map((tag) => (
<TableRow key={tag.id}>
<TableCell>
<Badge
variant="outline"
style={{ backgroundColor: tag.color, color: '#fff' }}
>
{tag.name}
</Badge>
</TableCell>
<TableCell className="max-w-[300px] truncate" title={tag.description}>
{tag.description || '-'}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(tag)}
title="编辑"
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(tag.id, tag.name)}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
title="删除"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={3} className="h-24 text-center text-muted-foreground">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
<DialogFooter>
<Button onClick={() => onOpenChange(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 标签编辑对话框 */}
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
<DialogContent className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle>{editingTag ? '编辑标签' : '新建标签'}</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name"> *</Label>
<Input
id="name"
value={formData.name || ''}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="请输入标签名称"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="color"> *</Label>
<div className="flex items-center gap-2">
<Input
id="color"
type="color"
value={formData.color || '#1677ff'}
onChange={(e) => setFormData({ ...formData, color: e.target.value })}
className="w-20 h-10"
/>
<Badge
variant="outline"
style={{ backgroundColor: formData.color || '#1677ff', color: '#fff' }}
>
</Badge>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={formData.description || ''}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="请输入描述"
rows={4}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setEditDialogOpen(false)}>
</Button>
<Button onClick={handleSubmit}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};
export default TagDialog;

View File

@ -1,186 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Modal, Table, Button, Form, Input, Space, message, Tag } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import type { RoleTagResponse, RoleTagRequest } from '../types';
import { getAllTags, createRoleTag, updateRoleTag, deleteRoleTag } from '../service';
interface TagModalProps {
visible: boolean;
onCancel: () => void;
onSuccess: () => void;
}
const TagModal: React.FC<TagModalProps> = ({
visible,
onCancel,
onSuccess
}) => {
const [form] = Form.useForm();
const [tags, setTags] = useState<RoleTagResponse[]>([]);
const [loading, setLoading] = useState(false);
const [editModalVisible, setEditModalVisible] = useState(false);
const [editingTag, setEditingTag] = useState<RoleTagResponse | null>(null);
useEffect(() => {
if (visible) {
loadTags();
}
}, [visible]);
const loadTags = async () => {
try {
setLoading(true);
const data = await getAllTags();
setTags(data);
} catch (error) {
message.error('获取标签列表失败');
} finally {
setLoading(false);
}
};
const handleAdd = () => {
setEditingTag(null);
form.resetFields();
setEditModalVisible(true);
};
const handleEdit = (record: RoleTagResponse) => {
setEditingTag(record);
form.setFieldsValue(record);
setEditModalVisible(true);
};
const handleDelete = async (id: number) => {
try {
await deleteRoleTag(id);
message.success('删除成功');
loadTags();
} catch (error) {
message.error('删除失败');
}
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
if (editingTag) {
await updateRoleTag(editingTag.id, values);
message.success('更新成功');
} else {
await createRoleTag(values);
message.success('创建成功');
}
setEditModalVisible(false);
loadTags();
} catch (error) {
message.error('操作失败');
}
};
const columns = [
{
title: '标签名称',
dataIndex: 'name',
key: 'name',
render: (text: string, record: RoleTagResponse) => (
<Tag color={record.color}>{text}</Tag>
)
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true
},
{
title: '操作',
key: 'action',
width: 160,
render: (_: any, record: RoleTagResponse) => (
<Space>
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
>
</Button>
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record.id)}
>
</Button>
</Space>
)
}
];
return (
<>
<Modal
title="标签管理"
open={visible}
onCancel={onCancel}
footer={null}
width={800}
>
<div style={{ marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
</Button>
</div>
<Table
columns={columns}
dataSource={tags}
rowKey="id"
loading={loading}
pagination={false}
/>
</Modal>
<Modal
title={editingTag ? '编辑标签' : '新建标签'}
open={editModalVisible}
onOk={handleSubmit}
onCancel={() => setEditModalVisible(false)}
>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="name"
label="标签名称"
rules={[{ required: true, message: '请输入标签名称' }]}
>
<Input placeholder="请输入标签名称" />
</Form.Item>
<Form.Item
name="color"
label="标签颜色"
rules={[{ required: true, message: '请输入标签颜色' }]}
>
<Input type="color" style={{ width: 60 }} />
</Form.Item>
<Form.Item
name="description"
label="描述"
>
<Input.TextArea rows={4} placeholder="请输入描述" />
</Form.Item>
</Form>
</Modal>
</>
);
};
export default TagModal;

View File

@ -1,375 +1,430 @@
import React, { useEffect, useState } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { Card, Button, Table, Space, Modal, Form, Input, InputNumber, message, Tag, Dropdown, MenuProps } from 'antd'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, TagsOutlined, MoreOutlined, SettingOutlined } from '@ant-design/icons'; import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table';
import type { RoleResponse, RoleTagResponse, RoleQuery } from './types'; import { Badge } from '@/components/ui/badge';
import { getRoleList, createRole, updateRole, deleteRole, getRolePermissions, assignPermissions } from './service'; import { Button } from '@/components/ui/button';
import type { TablePaginationConfig } from 'antd/es/table'; import { Input } from '@/components/ui/input';
import type {FilterValue, SorterResult} from 'antd/es/table/interface'; import { DataTablePagination } from '@/components/ui/pagination';
import PermissionModal from './components/PermissionModal'; import {
import TagModal from './components/TagModal'; Loader2, Plus, Search, Edit, Trash2, KeyRound, Tag as TagIcon,
import AssignTagModal from './components/AssignTagModal'; ShieldCheck, Settings
} from 'lucide-react';
import { useToast } from '@/components/ui/use-toast';
import { getRoleList, deleteRole, getRolePermissions, assignPermissions } from './service';
import type { RoleResponse, RoleQuery } from './types';
import type { Page } from '@/types/base';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import EditDialog from './components/EditDialog';
import DeleteDialog from './components/DeleteDialog';
import PermissionDialog from './components/PermissionDialog';
import TagDialog from './components/TagDialog';
import AssignTagDialog from './components/AssignTagDialog';
import dayjs from 'dayjs';
/**
*
*/
const RolePage: React.FC = () => { const RolePage: React.FC = () => {
const [form] = Form.useForm(); const { toast } = useToast();
const [visible, setVisible] = useState(false); const [loading, setLoading] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null); const [data, setData] = useState<Page<RoleResponse> | null>(null);
const [confirmLoading, setConfirmLoading] = useState(false); const [editDialogOpen, setEditDialogOpen] = useState(false);
const [permissionModalVisible, setPermissionModalVisible] = useState(false); const [editRecord, setEditRecord] = useState<RoleResponse>();
const [tagModalVisible, setTagModalVisible] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [assignTagModalVisible, setAssignTagModalVisible] = useState(false); const [deleteRecord, setDeleteRecord] = useState<RoleResponse | null>(null);
const [selectedRole, setSelectedRole] = useState<RoleResponse>(); const [permissionDialogOpen, setPermissionDialogOpen] = useState(false);
const [loading, setLoading] = useState(false); const [tagDialogOpen, setTagDialogOpen] = useState(false);
const [roles, setRoles] = useState<RoleResponse[]>([]); const [assignTagDialogOpen, setAssignTagDialogOpen] = useState(false);
const [defaultCheckedKeys, setDefaultCheckedKeys] = useState<number[]>([]); const [selectedRole, setSelectedRole] = useState<RoleResponse | null>(null);
const [pagination, setPagination] = useState<TablePaginationConfig>({ const [defaultCheckedKeys, setDefaultCheckedKeys] = useState<number[]>([]);
current: 1, const [query, setQuery] = useState<RoleQuery>({
pageSize: 10, pageNum: DEFAULT_CURRENT - 1,
total: 0, pageSize: DEFAULT_PAGE_SIZE,
showSizeChanger: true, code: '',
showQuickJumper: true name: '',
});
// 加载数据
const loadData = async () => {
setLoading(true);
try {
const result = await getRoleList(query);
setData(result);
} catch (error) {
console.error('加载角色列表失败:', error);
toast({
title: '加载失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
}, [query]);
// 搜索
const handleSearch = () => {
setQuery(prev => ({ ...prev, pageNum: 0 }));
};
// 重置
const handleReset = () => {
setQuery({
pageNum: 0,
pageSize: DEFAULT_PAGE_SIZE,
code: '',
name: '',
}); });
const [sortField, setSortField] = useState<string>('sort'); };
const [sortOrder, setSortOrder] = useState<'ascend' | 'descend'>('ascend');
const fetchRoles = async (params: RoleQuery = {}) => { // 新建
try { const handleCreate = () => {
setLoading(true); setEditRecord(undefined);
const { pageNum = 1, pageSize = 10 } = pagination; setEditDialogOpen(true);
const result = await getRoleList({ };
pageNum,
pageSize,
sortField,
sortOrder: sortOrder === 'ascend' ? 'asc' : sortOrder === 'descend' ? 'desc' : undefined,
...params
});
setRoles(result.content);
setPagination({
...pagination,
current: result.number + 1,
pageSize: result.size,
total: result.totalElements
});
} catch (error) {
message.error('获取角色列表失败');
} finally {
setLoading(false);
}
};
useEffect(() => { // 编辑
fetchRoles(); const handleEdit = (record: RoleResponse) => {
}, []); setEditRecord(record);
setEditDialogOpen(true);
};
const handleAdd = () => { // 删除
setEditingId(null); const handleDelete = (record: RoleResponse) => {
form.resetFields(); setDeleteRecord(record);
form.setFieldsValue({ setDeleteDialogOpen(true);
enabled: true, };
sort: 0
});
setVisible(true);
};
const handleEdit = (record: RoleResponse) => { const confirmDelete = async () => {
setEditingId(record.id); if (!deleteRecord) return;
form.setFieldsValue(record); try {
setVisible(true); await deleteRole(deleteRecord.id);
}; toast({
title: '删除成功',
description: `角色 "${deleteRecord.name}" 已删除`,
});
loadData();
setDeleteDialogOpen(false);
setDeleteRecord(null);
} catch (error) {
console.error('删除失败:', error);
toast({
title: '删除失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
}
};
const handleDelete = async (id: number) => { // 分配权限
Modal.confirm({ const handleAssignPermissions = async (record: RoleResponse) => {
title: '确认删除', try {
content: '确定要删除这个角色吗?', const permissions = await getRolePermissions(record.id);
onOk: async () => { setSelectedRole(record);
try { setPermissionDialogOpen(true);
await deleteRole(id); setDefaultCheckedKeys(
message.success('删除成功'); Array.isArray(permissions)
fetchRoles(); ? typeof permissions[0] === 'number'
} catch (error) { ? permissions
message.error('删除失败'); : permissions.map((p: any) => p.id)
} : []
} );
}); } catch (error) {
}; console.error('获取角色权限失败:', error);
toast({
title: '获取权限失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
}
};
const handleSubmit = async () => { const confirmAssignPermissions = async (permissionIds: number[]) => {
try { if (!selectedRole) return;
const values = await form.validateFields(); try {
setConfirmLoading(true); await assignPermissions(selectedRole.id, permissionIds);
toast({
title: '分配成功',
description: `已为角色 "${selectedRole.name}" 分配权限`,
});
setPermissionDialogOpen(false);
loadData();
} catch (error) {
console.error('分配权限失败:', error);
toast({
title: '分配失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
}
};
if (editingId) { // 分配标签
await updateRole(editingId, values); const handleAssignTags = (record: RoleResponse) => {
message.success('更新成功'); setSelectedRole(record);
} else { setAssignTagDialogOpen(true);
await createRole(values); };
message.success('创建成功');
}
setVisible(false); // 统计数据
fetchRoles(); const stats = useMemo(() => {
} catch (error) { const total = data?.totalElements || 0;
message.error('操作失败'); return { total };
} finally { }, [data]);
setConfirmLoading(false);
}
};
// 处理表格变化 const pageCount = data?.totalElements ? Math.ceil(data.totalElements / (query.pageSize || DEFAULT_PAGE_SIZE)) : 0;
const handleTableChange = (
newPagination: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: SorterResult<RoleResponse> | SorterResult<RoleResponse>[]
) => {
const { field, order } = Array.isArray(sorter) ? sorter[0] : sorter;
setSortField(field as string);
setSortOrder(order as 'ascend' | 'descend');
const params = { return (
current: newPagination.current, <div className="p-6">
pageSize: newPagination.pageSize, <div className="mb-6">
sortField: field as string, <h1 className="text-3xl font-bold text-foreground"></h1>
sortOrder: order === 'ascend' ? 'asc' : order === 'descend' ? 'desc' : undefined <p className="text-muted-foreground mt-2">
};
</p>
</div>
fetchRoles({ {/* 统计卡片 */}
pageNum: params.current, <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
pageSize: params.pageSize, <Card className="bg-gradient-to-br from-purple-500/10 to-purple-500/5 border-purple-500/20">
sortField: params.sortField, <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
sortOrder: params.sortOrder <CardTitle className="text-sm font-medium text-purple-700"></CardTitle>
}); <ShieldCheck className="h-4 w-4 text-purple-500" />
}; </CardHeader>
<CardContent>
// 处理分配权限 <div className="text-2xl font-bold">{stats.total}</div>
const handleAssignPermissions = async (record: RoleResponse) => { <p className="text-xs text-muted-foreground mt-1"></p>
try { </CardContent>
const permissions = await getRolePermissions(record.id);
setSelectedRole(record);
setPermissionModalVisible(true);
setDefaultCheckedKeys(Array.isArray(permissions) ?
(typeof permissions[0] === 'number' ? permissions : permissions.map(p => p.id)) :
[]);
} catch (error) {
message.error('获取角色权限失败');
}
};
// 处理权限分配确认
const handlePermissionAssign = async (permissionIds: number[]) => {
if (!selectedRole) return;
try {
await assignPermissions(selectedRole.id, permissionIds);
message.success('权限分配成功');
setPermissionModalVisible(false);
fetchRoles();
} catch (error) {
message.error('权限分配失败');
}
};
const handleAssignTags = (record: RoleResponse) => {
setSelectedRole(record);
setAssignTagModalVisible(true);
};
const columns = [
{
title: '角色编码',
dataIndex: 'code',
width: 150,
sorter: true
},
{
title: '角色名称',
dataIndex: 'name',
width: 150
},
{
title: '标签',
dataIndex: 'tags',
width: 200,
render: (tags: RoleTagResponse[]) => (
<Space size={[0, 4]} wrap>
{tags?.map(tag => (
<Tag key={tag.id} color={tag.color}>
{tag.name}
</Tag>
))}
</Space>
)
},
{
title: '排序',
dataIndex: 'sort',
width: 80,
sorter: true
},
{
title: '描述',
dataIndex: 'description',
ellipsis: true,
width: 200
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 180,
sorter: true
},
{
title: '操作',
key: 'action',
fixed: 'right' as const,
width: 120,
render: (_: any, record: RoleResponse) => {
const items: MenuProps['items'] = [
{
key: 'permissions',
icon: <KeyOutlined />,
label: '分配权限',
onClick: () => handleAssignPermissions(record)
},
{
key: 'tags',
icon: <TagsOutlined />,
label: '分配标签',
onClick: () => handleAssignTags(record)
}
];
// 如果不是admin角色添加删除选项
if (record.code !== 'admin') {
items.push({
key: 'delete',
icon: <DeleteOutlined />,
label: '删除',
danger: true,
onClick: () => handleDelete(record.id)
});
}
return (
<Space>
<Button
type="link"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
>
</Button>
<Dropdown
menu={{ items }}
placement="bottomRight"
trigger={['click']}
>
<Button
type="text"
icon={<MoreOutlined />}
style={{ padding: '4px 8px' }}
/>
</Dropdown>
</Space>
);
}
}
];
return (
<Card>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
</Button>
<Button icon={<SettingOutlined />} onClick={() => setTagModalVisible(true)}>
</Button>
</div>
<Table
columns={columns}
dataSource={roles}
rowKey="id"
loading={loading}
pagination={pagination}
onChange={handleTableChange}
scroll={{ x: 1300 }}
/>
<Modal
title={editingId ? '编辑角色' : '新建角色'}
open={visible}
onOk={handleSubmit}
onCancel={() => setVisible(false)}
confirmLoading={confirmLoading}
>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="code"
label="角色编码"
rules={[{ required: true, message: '请输入角色编码' }]}
>
<Input placeholder="请输入角色编码" disabled={!!editingId} />
</Form.Item>
<Form.Item
name="name"
label="角色名称"
rules={[{ required: true, message: '请输入角色名称' }]}
>
<Input placeholder="请输入角色名称" />
</Form.Item>
<Form.Item
name="sort"
label="排序"
initialValue={0}
>
<InputNumber min={0} />
</Form.Item>
<Form.Item
name="description"
label="描述"
>
<Input.TextArea rows={4} placeholder="请输入描述" />
</Form.Item>
</Form>
</Modal>
{selectedRole && (
<>
<PermissionModal
visible={permissionModalVisible}
roleId={selectedRole.id}
onCancel={() => setPermissionModalVisible(false)}
onOk={handlePermissionAssign}
defaultCheckedKeys={defaultCheckedKeys}
/>
<AssignTagModal
visible={assignTagModalVisible}
roleId={selectedRole.id}
onCancel={() => setAssignTagModalVisible(false)}
onSuccess={() => {
setAssignTagModalVisible(false);
fetchRoles();
}}
selectedTags={selectedRole.tags}
/>
</>
)}
<TagModal
visible={tagModalVisible}
onCancel={() => setTagModalVisible(false)}
onSuccess={() => {
setTagModalVisible(false);
fetchRoles();
}}
/>
</Card> </Card>
); </div>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle></CardTitle>
<div className="flex gap-2">
<Button variant="outline" onClick={() => setTagDialogOpen(true)}>
<Settings className="h-4 w-4 mr-2" />
</Button>
<Button onClick={handleCreate}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</CardHeader>
<CardContent>
{/* 搜索栏 */}
<div className="flex flex-wrap items-center gap-4 mb-4">
<div className="flex-1 max-w-xs">
<Input
placeholder="搜索角色编码"
value={query.code}
onChange={(e) => setQuery(prev => ({ ...prev, code: e.target.value }))}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className="h-9"
/>
</div>
<div className="flex-1 max-w-xs">
<Input
placeholder="搜索角色名称"
value={query.name}
onChange={(e) => setQuery(prev => ({ ...prev, name: e.target.value }))}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className="h-9"
/>
</div>
<Button onClick={handleSearch} className="h-9">
<Search className="h-4 w-4 mr-2" />
</Button>
<Button variant="outline" onClick={handleReset} className="h-9">
</Button>
</div>
{/* 表格 */}
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[150px]"></TableHead>
<TableHead className="w-[150px]"></TableHead>
<TableHead className="w-[200px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[200px]"></TableHead>
<TableHead className="w-[180px]"></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={7} className="h-24 text-center">
<div className="flex items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin mr-2" />
<span className="text-sm text-muted-foreground">...</span>
</div>
</TableCell>
</TableRow>
) : data?.content && data.content.length > 0 ? (
data.content.map((record) => {
const isAdmin = record.code === 'admin';
return (
<TableRow key={record.id} className="hover:bg-muted/50">
<TableCell className="font-medium">
<code className="text-sm">{record.code}</code>
</TableCell>
<TableCell>{record.name}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{record.tags && record.tags.length > 0 ? (
record.tags.map(tag => (
<Badge
key={tag.id}
variant="outline"
style={{ backgroundColor: tag.color, color: '#fff' }}
className="text-xs"
>
{tag.name}
</Badge>
))
) : (
<span className="text-muted-foreground">-</span>
)}
</div>
</TableCell>
<TableCell>{record.sort}</TableCell>
<TableCell className="text-sm max-w-[200px] truncate" title={record.description}>
{record.description || '-'}
</TableCell>
<TableCell className="text-sm">
{record.createTime ? dayjs(record.createTime).format('YYYY-MM-DD HH:mm') : '-'}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(record)}
title="编辑"
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleAssignPermissions(record)}
title="分配权限"
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50"
>
<KeyRound className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleAssignTags(record)}
title="分配标签"
className="text-purple-600 hover:text-purple-700 hover:bg-purple-50"
>
<TagIcon className="h-4 w-4" />
</Button>
{!isAdmin && (
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(record)}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
title="删除"
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
</TableCell>
</TableRow>
);
})
) : (
<TableRow>
<TableCell colSpan={7} className="h-24 text-center">
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<ShieldCheck className="w-16 h-16 mb-4 text-muted-foreground/50" />
<div className="text-lg font-semibold mb-2"></div>
<div className="text-sm">"新建角色"</div>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* 分页 */}
{pageCount > 1 && (
<DataTablePagination
pageIndex={(query.pageNum || 0) + 1}
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
pageCount={pageCount}
onPageChange={(page) => setQuery(prev => ({
...prev,
pageNum: page - 1
}))}
/>
)}
</CardContent>
</Card>
{/* 编辑对话框 */}
<EditDialog
open={editDialogOpen}
record={editRecord}
onOpenChange={setEditDialogOpen}
onSuccess={loadData}
/>
{/* 删除确认对话框 */}
<DeleteDialog
open={deleteDialogOpen}
record={deleteRecord}
onOpenChange={setDeleteDialogOpen}
onConfirm={confirmDelete}
/>
{/* 权限分配对话框 */}
{selectedRole && (
<>
<PermissionDialog
open={permissionDialogOpen}
onOpenChange={setPermissionDialogOpen}
onConfirm={confirmAssignPermissions}
defaultCheckedKeys={defaultCheckedKeys}
/>
<AssignTagDialog
open={assignTagDialogOpen}
roleId={selectedRole.id}
onOpenChange={setAssignTagDialogOpen}
onSuccess={() => {
setAssignTagDialogOpen(false);
loadData();
}}
selectedTags={selectedRole.tags}
/>
</>
)}
{/* 标签管理对话框 */}
<TagDialog
open={tagDialogOpen}
onOpenChange={setTagDialogOpen}
onSuccess={() => {
setTagDialogOpen(false);
loadData();
}}
/>
</div>
);
}; };
export default RolePage; export default RolePage;

View File

@ -26,6 +26,7 @@ export interface RoleRequest extends BaseRequest{
code: string; code: string;
name: string; name: string;
description?: string; description?: string;
sort?: number;
} }
// 角色响应数据 // 角色响应数据