增加审批组件
This commit is contained in:
parent
dfa6abf0a9
commit
913f56f0f9
166
frontend/src/pages/System/Role/components/AssignTagDialog.tsx
Normal file
166
frontend/src/pages/System/Role/components/AssignTagDialog.tsx
Normal 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;
|
||||||
|
|
||||||
@ -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;
|
|
||||||
81
frontend/src/pages/System/Role/components/DeleteDialog.tsx
Normal file
81
frontend/src/pages/System/Role/components/DeleteDialog.tsx
Normal 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;
|
||||||
|
|
||||||
162
frontend/src/pages/System/Role/components/EditDialog.tsx
Normal file
162
frontend/src/pages/System/Role/components/EditDialog.tsx
Normal 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;
|
||||||
|
|
||||||
297
frontend/src/pages/System/Role/components/PermissionDialog.tsx
Normal file
297
frontend/src/pages/System/Role/components/PermissionDialog.tsx
Normal 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;
|
||||||
@ -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;
|
|
||||||
296
frontend/src/pages/System/Role/components/TagDialog.tsx
Normal file
296
frontend/src/pages/System/Role/components/TagDialog.tsx
Normal 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;
|
||||||
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
||||||
@ -26,6 +26,7 @@ export interface RoleRequest extends BaseRequest{
|
|||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
sort?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 角色响应数据
|
// 角色响应数据
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user