增加权限弹窗
This commit is contained in:
parent
95e66480a2
commit
774523696a
@ -99,7 +99,7 @@ const DialogBody = ({
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex-1 overflow-y-auto px-6 py-4",
|
||||
"flex-1 overflow-y-auto px-6 py-4 pt-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -6,9 +6,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { FileText, Tag, Folder, AlignLeft, Copy, Loader2 } from 'lucide-react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { createDefinition, updateDefinition } from '../service';
|
||||
import { getEnabledCategories } from '../../../Category/service';
|
||||
@ -42,7 +40,6 @@ const FormBasicInfoModal: React.FC<FormBasicInfoModalProps> = ({
|
||||
key: '',
|
||||
categoryId: undefined as number | undefined,
|
||||
description: '',
|
||||
isTemplate: false,
|
||||
});
|
||||
|
||||
// 加载分类列表和初始化数据
|
||||
@ -58,7 +55,6 @@ const FormBasicInfoModal: React.FC<FormBasicInfoModalProps> = ({
|
||||
key: record.key,
|
||||
categoryId: record.categoryId,
|
||||
description: record.description || '',
|
||||
isTemplate: record.isTemplate || false,
|
||||
});
|
||||
} else if (mode === 'create') {
|
||||
resetForm();
|
||||
@ -81,7 +77,6 @@ const FormBasicInfoModal: React.FC<FormBasicInfoModalProps> = ({
|
||||
key: '',
|
||||
categoryId: undefined,
|
||||
description: '',
|
||||
isTemplate: false,
|
||||
});
|
||||
};
|
||||
|
||||
@ -132,7 +127,7 @@ const FormBasicInfoModal: React.FC<FormBasicInfoModalProps> = ({
|
||||
key: formData.key,
|
||||
categoryId: formData.categoryId,
|
||||
description: formData.description,
|
||||
isTemplate: formData.isTemplate,
|
||||
isTemplate: false,
|
||||
status: 'DRAFT',
|
||||
schema: {
|
||||
version: '1.0',
|
||||
@ -166,7 +161,7 @@ const FormBasicInfoModal: React.FC<FormBasicInfoModalProps> = ({
|
||||
description: formData.description,
|
||||
schema: record.schema,
|
||||
status: record.status,
|
||||
isTemplate: formData.isTemplate,
|
||||
isTemplate: record.isTemplate,
|
||||
version: record.version, // 乐观锁版本号
|
||||
};
|
||||
|
||||
@ -204,14 +199,36 @@ const FormBasicInfoModal: React.FC<FormBasicInfoModalProps> = ({
|
||||
</DialogHeader>
|
||||
|
||||
<DialogBody>
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 第一行:表单名称 + 表单标识 */}
|
||||
{/* 第一行:分类 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="form-category">
|
||||
表单分类
|
||||
</Label>
|
||||
<Select
|
||||
value={formData.categoryId?.toString() || undefined}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, categoryId: Number(value) }))}
|
||||
>
|
||||
<SelectTrigger id="form-category" className="h-10">
|
||||
<SelectValue placeholder="选择表单所属分类" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map(cat => (
|
||||
<SelectItem key={cat.id} value={cat.id.toString()}>
|
||||
{cat.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
帮助用户快速找到相关表单
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 第二行:表单名称 + 表单标识 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="form-name" className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
<Label htmlFor="form-name">
|
||||
表单名称
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
@ -228,8 +245,7 @@ const FormBasicInfoModal: React.FC<FormBasicInfoModalProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="form-key" className="flex items-center gap-2">
|
||||
<Tag className="h-4 w-4 text-muted-foreground" />
|
||||
<Label htmlFor="form-key">
|
||||
表单标识
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
@ -246,58 +262,9 @@ const FormBasicInfoModal: React.FC<FormBasicInfoModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 第二行:分类 + 设为模板 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="form-category" className="flex items-center gap-2">
|
||||
<Folder className="h-4 w-4 text-muted-foreground" />
|
||||
表单分类
|
||||
</Label>
|
||||
<Select
|
||||
value={formData.categoryId?.toString() || undefined}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, categoryId: Number(value) }))}
|
||||
>
|
||||
<SelectTrigger id="form-category" className="h-10">
|
||||
<SelectValue placeholder="选择表单所属分类" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map(cat => (
|
||||
<SelectItem key={cat.id} value={cat.id.toString()}>
|
||||
{cat.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
帮助用户快速找到相关表单
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="form-isTemplate" className="flex items-center gap-2">
|
||||
<Copy className="h-4 w-4 text-muted-foreground" />
|
||||
设为表单模板
|
||||
</Label>
|
||||
<div className="flex items-center h-10 rounded-lg border px-4 bg-muted/30">
|
||||
<div className="flex-1">
|
||||
<span className="text-sm">启用模板功能</span>
|
||||
</div>
|
||||
<Switch
|
||||
id="form-isTemplate"
|
||||
checked={formData.isTemplate}
|
||||
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isTemplate: checked }))}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
模板表单可以被其他表单引用和复制
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 第三行:描述 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="form-description" className="flex items-center gap-2">
|
||||
<AlignLeft className="h-4 w-4 text-muted-foreground" />
|
||||
<Label htmlFor="form-description">
|
||||
表单描述
|
||||
</Label>
|
||||
<Textarea
|
||||
|
||||
@ -0,0 +1,339 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableCell,
|
||||
} from '@/components/ui/table';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { Plus, Edit, Trash2, Loader2 } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { DataTablePagination } from '@/components/ui/pagination';
|
||||
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||
import type { MenuResponse } from '../types';
|
||||
import {
|
||||
getPermissions,
|
||||
createPermission,
|
||||
updatePermission,
|
||||
deletePermission,
|
||||
} from '@/pages/System/Permission/List/service';
|
||||
import type {
|
||||
PermissionResponse,
|
||||
PermissionRequest,
|
||||
} from '@/pages/System/Permission/List/types';
|
||||
import PermissionEditDialog, { type FormValues } from './PermissionEditDialog';
|
||||
|
||||
interface PermissionDialogProps {
|
||||
open: boolean;
|
||||
menu: MenuResponse;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const PermissionDialog: React.FC<PermissionDialogProps> = ({
|
||||
open,
|
||||
menu,
|
||||
onOpenChange,
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [permissions, setPermissions] = useState<PermissionResponse[]>([]);
|
||||
const [pagination, setPagination] = useState({
|
||||
pageNum: DEFAULT_CURRENT,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
totalElements: 0,
|
||||
});
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [editRecord, setEditRecord] = useState<PermissionResponse | undefined>();
|
||||
|
||||
// 加载权限列表
|
||||
const loadPermissions = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getPermissions({
|
||||
menuId: menu.id,
|
||||
pageNum: pagination.pageNum - 1,
|
||||
pageSize: pagination.pageSize,
|
||||
});
|
||||
if (response) {
|
||||
setPermissions(response.content || []);
|
||||
setPagination({
|
||||
...pagination,
|
||||
totalElements: response.totalElements || 0,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载权限失败:', error);
|
||||
toast({
|
||||
title: '加载失败',
|
||||
description: '获取权限列表失败',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// 每次打开都重置到初始状态
|
||||
setPagination({
|
||||
pageNum: DEFAULT_CURRENT,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
totalElements: 0,
|
||||
});
|
||||
setPermissions([]);
|
||||
setEditDialogOpen(false);
|
||||
setEditRecord(undefined);
|
||||
}
|
||||
}, [open, menu.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
loadPermissions();
|
||||
}
|
||||
}, [open, pagination.pageNum, pagination.pageSize]);
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
setEditRecord(undefined);
|
||||
setEditDialogOpen(true);
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record: PermissionResponse) => {
|
||||
setEditRecord(record);
|
||||
setEditDialogOpen(true);
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleFormSubmit = async (formData: FormValues) => {
|
||||
try {
|
||||
const data: PermissionRequest = {
|
||||
menuId: menu.id,
|
||||
code: formData.code,
|
||||
name: formData.name,
|
||||
type: formData.type,
|
||||
sort: formData.sort,
|
||||
};
|
||||
|
||||
if (editRecord) {
|
||||
await updatePermission(editRecord.id, data);
|
||||
toast({
|
||||
title: '更新成功',
|
||||
description: `权限 "${formData.name}" 已更新`,
|
||||
});
|
||||
} else {
|
||||
await createPermission(data);
|
||||
toast({
|
||||
title: '创建成功',
|
||||
description: `权限 "${formData.name}" 已创建`,
|
||||
});
|
||||
}
|
||||
|
||||
setEditDialogOpen(false);
|
||||
|
||||
// 保存后重置分页到第一页
|
||||
if (!editRecord && pagination.pageNum !== 1) {
|
||||
setPagination({ ...pagination, pageNum: 1 });
|
||||
} else {
|
||||
loadPermissions();
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: editRecord ? '更新失败' : '创建失败',
|
||||
description: error.response?.data?.message || '操作失败',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (record: PermissionResponse) => {
|
||||
if (!confirm(`确定要删除权限 "${record.name}" 吗?`)) return;
|
||||
|
||||
try {
|
||||
await deletePermission(record.id);
|
||||
toast({
|
||||
title: '删除成功',
|
||||
description: `权限 "${record.name}" 已删除`,
|
||||
});
|
||||
loadPermissions();
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: '删除失败',
|
||||
description: error.response?.data?.message || '删除失败',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 获取权限类型Badge
|
||||
const getTypeBadge = (type: string) => {
|
||||
const typeMap: Record<
|
||||
string,
|
||||
{ text: string; variant: 'default' | 'secondary' | 'outline' }
|
||||
> = {
|
||||
MENU: { text: '菜单', variant: 'default' },
|
||||
BUTTON: { text: '按钮', variant: 'secondary' },
|
||||
API: { text: '接口', variant: 'outline' },
|
||||
};
|
||||
const info = typeMap[type] || { text: type, variant: 'outline' };
|
||||
return <Badge variant={info.variant}>{info.text}</Badge>;
|
||||
};
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setPagination({ ...pagination, pageNum: newPage + 1 });
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>权限管理:{menu.name}</DialogTitle>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
管理该菜单下的权限点
|
||||
</p>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogBody>
|
||||
{/* 权限列表 */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium">权限列表</h3>
|
||||
<Button size="sm" onClick={handleAdd}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
新增权限
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead width="200px">权限编码</TableHead>
|
||||
<TableHead width="150px">权限名称</TableHead>
|
||||
<TableHead width="100px">类型</TableHead>
|
||||
<TableHead width="80px" className="text-center">
|
||||
排序
|
||||
</TableHead>
|
||||
<TableHead width="120px" className="text-right">
|
||||
操作
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} 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>
|
||||
) : permissions.length > 0 ? (
|
||||
permissions.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell width="200px">
|
||||
<code className="text-xs bg-muted px-2 py-0.5 rounded">
|
||||
{item.code}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell width="150px" className="font-medium">
|
||||
{item.name}
|
||||
</TableCell>
|
||||
<TableCell width="100px">
|
||||
{getTypeBadge(item.type)}
|
||||
</TableCell>
|
||||
<TableCell width="80px" className="text-center">
|
||||
{item.sort}
|
||||
</TableCell>
|
||||
<TableCell width="120px" className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(item)}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(item)}
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="h-24 text-center">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
暂无权限数据
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="flex justify-end border-t border-border bg-muted/40">
|
||||
<DataTablePagination
|
||||
pageIndex={pagination.pageNum - 1}
|
||||
pageSize={pagination.pageSize}
|
||||
pageCount={Math.ceil(pagination.totalElements / pagination.pageSize)}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogBody>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
{/* 编辑对话框 */}
|
||||
<PermissionEditDialog
|
||||
open={editDialogOpen}
|
||||
record={editRecord}
|
||||
menuId={menu.id}
|
||||
menuName={menu.name}
|
||||
onOpenChange={setEditDialogOpen}
|
||||
onSubmit={handleFormSubmit}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermissionDialog;
|
||||
|
||||
@ -0,0 +1,177 @@
|
||||
import React, { useEffect } 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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
import type { PermissionResponse } from '@/pages/System/Permission/List/types';
|
||||
|
||||
interface PermissionEditDialogProps {
|
||||
open: boolean;
|
||||
record?: PermissionResponse;
|
||||
menuId: number;
|
||||
menuName: string;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSubmit: (data: FormValues) => void;
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
code: z.string().min(1, '请输入权限编码'),
|
||||
name: z.string().min(1, '请输入权限名称'),
|
||||
type: z.string().min(1, '请选择权限类型'),
|
||||
sort: z.number().min(0, '排序必须大于等于0'),
|
||||
});
|
||||
|
||||
export type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
const PermissionEditDialog: React.FC<PermissionEditDialogProps> = ({
|
||||
open,
|
||||
record,
|
||||
menuId,
|
||||
menuName,
|
||||
onOpenChange,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
reset,
|
||||
setValue,
|
||||
watch,
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
code: '',
|
||||
name: '',
|
||||
type: 'BUTTON',
|
||||
sort: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const selectedType = watch('type');
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (record) {
|
||||
// 编辑模式
|
||||
reset({
|
||||
code: record.code,
|
||||
name: record.name,
|
||||
type: record.type,
|
||||
sort: record.sort,
|
||||
});
|
||||
} else {
|
||||
// 新建模式
|
||||
reset({
|
||||
code: '',
|
||||
name: '',
|
||||
type: 'BUTTON',
|
||||
sort: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [open, record, reset]);
|
||||
|
||||
const handleFormSubmit = (data: FormValues) => {
|
||||
onSubmit(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{record ? '编辑权限' : '新建权限'}</DialogTitle>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
菜单:{menuName}
|
||||
</p>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="grid gap-4 px-6 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="code">权限编码 *</Label>
|
||||
<Input
|
||||
id="code"
|
||||
placeholder="例如: user:create"
|
||||
{...register('code')}
|
||||
/>
|
||||
{errors.code && (
|
||||
<p className="text-sm text-destructive">{errors.code.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">权限名称 *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder="例如: 创建用户"
|
||||
{...register('name')}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-sm text-destructive">{errors.name.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="type">权限类型 *</Label>
|
||||
<Select
|
||||
value={selectedType}
|
||||
onValueChange={(value) => setValue('type', value)}
|
||||
>
|
||||
<SelectTrigger id="type">
|
||||
<SelectValue placeholder="选择权限类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="MENU">菜单权限</SelectItem>
|
||||
<SelectItem value="BUTTON">按钮权限</SelectItem>
|
||||
<SelectItem value="API">接口权限</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.type && (
|
||||
<p className="text-sm text-destructive">{errors.type.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="sort">排序 *</Label>
|
||||
<Input
|
||||
id="sort"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
{...register('sort', { valueAsNumber: true })}
|
||||
/>
|
||||
{errors.sort && (
|
||||
<p className="text-sm text-destructive">{errors.sort.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit">确定</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermissionEditDialog;
|
||||
|
||||
@ -7,7 +7,7 @@ import { useToast } from '@/components/ui/use-toast';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import {
|
||||
Loader2, Plus, Edit, Trash2, ChevronRight, ChevronDown,
|
||||
FolderTree, Menu as MenuIcon, EyeOff
|
||||
FolderTree, Menu as MenuIcon, EyeOff, Key
|
||||
} from 'lucide-react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setMenus } from '@/store/userSlice';
|
||||
@ -16,6 +16,7 @@ import type { MenuResponse } from './types';
|
||||
import { getIconComponent } from '@/config/icons.tsx';
|
||||
import EditDialog from './components/EditDialog';
|
||||
import DeleteDialog from './components/DeleteDialog';
|
||||
import PermissionDialog from './components/PermissionDialog';
|
||||
|
||||
/**
|
||||
* 菜单管理页面
|
||||
@ -30,6 +31,8 @@ const MenuPage: React.FC = () => {
|
||||
const [editRecord, setEditRecord] = useState<MenuResponse>();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deleteRecord, setDeleteRecord] = useState<MenuResponse | null>(null);
|
||||
const [permissionDialogOpen, setPermissionDialogOpen] = useState(false);
|
||||
const [permissionMenu, setPermissionMenu] = useState<MenuResponse | null>(null);
|
||||
|
||||
// 加载菜单树
|
||||
const loadMenuTree = async () => {
|
||||
@ -93,6 +96,12 @@ const MenuPage: React.FC = () => {
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
// 权限管理
|
||||
const handlePermission = (record: MenuResponse) => {
|
||||
setPermissionMenu(record);
|
||||
setPermissionDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (!deleteRecord) return;
|
||||
try {
|
||||
@ -151,6 +160,9 @@ const MenuPage: React.FC = () => {
|
||||
key={menu.id}
|
||||
className={`hover:bg-muted/50 ${menu.hidden ? 'bg-muted/30' : ''}`}
|
||||
>
|
||||
<TableCell width="80px" className="text-muted-foreground">
|
||||
{menu.id}
|
||||
</TableCell>
|
||||
<TableCell width="250px">
|
||||
<div className="flex items-center gap-2" style={{ paddingLeft: `${level * 24}px` }}>
|
||||
{hasChildren ? (
|
||||
@ -214,6 +226,14 @@ const MenuPage: React.FC = () => {
|
||||
<TableCell width="100px" className="text-center">{menu.sort}</TableCell>
|
||||
<TableCell width="200px" sticky>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handlePermission(menu)}
|
||||
title="权限"
|
||||
>
|
||||
<Key className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@ -337,9 +357,10 @@ const MenuPage: React.FC = () => {
|
||||
<CardContent>
|
||||
{/* 表格 */}
|
||||
<div className="rounded-md border">
|
||||
<Table minWidth="1280px">
|
||||
<Table minWidth="1360px">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead width="80px">ID</TableHead>
|
||||
<TableHead width="250px">菜单名称</TableHead>
|
||||
<TableHead width="80px">图标</TableHead>
|
||||
<TableHead width="200px">路由地址</TableHead>
|
||||
@ -353,7 +374,7 @@ const MenuPage: React.FC = () => {
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="h-24 text-center">
|
||||
<TableCell colSpan={9} 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>
|
||||
@ -364,7 +385,7 @@ const MenuPage: React.FC = () => {
|
||||
menuTree.map(menu => renderMenuRow(menu))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="h-24 text-center">
|
||||
<TableCell colSpan={9} className="h-24 text-center">
|
||||
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||
<FolderTree className="w-16 h-16 mb-4 text-muted-foreground/50" />
|
||||
<div className="text-lg font-semibold mb-2">暂无菜单数据</div>
|
||||
@ -398,6 +419,15 @@ const MenuPage: React.FC = () => {
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onConfirm={confirmDelete}
|
||||
/>
|
||||
|
||||
{/* 权限管理对话框 */}
|
||||
{permissionMenu && (
|
||||
<PermissionDialog
|
||||
open={permissionDialogOpen}
|
||||
menu={permissionMenu}
|
||||
onOpenChange={setPermissionDialogOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
30
frontend/src/pages/System/Permission/List/service.ts
Normal file
30
frontend/src/pages/System/Permission/List/service.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import request from '@/utils/request';
|
||||
import type { PermissionResponse, PermissionRequest, PermissionQuery } from './types';
|
||||
import { Page } from '@/types/base';
|
||||
|
||||
const BASE_URL = '/api/v1/permission';
|
||||
|
||||
// 获取权限列表(分页)
|
||||
export const getPermissions = (params?: PermissionQuery) =>
|
||||
request.get<Page<PermissionResponse>>(`${BASE_URL}/page`, { params });
|
||||
|
||||
// 获取权限列表(不分页,按菜单ID查询)
|
||||
export const getPermissionsByMenuId = (menuId: number) =>
|
||||
request.get<PermissionResponse[]>(`${BASE_URL}/list`, { params: { menuId } });
|
||||
|
||||
// 获取单个权限
|
||||
export const getPermission = (id: number) =>
|
||||
request.get<PermissionResponse>(`${BASE_URL}/${id}`);
|
||||
|
||||
// 创建权限
|
||||
export const createPermission = (data: PermissionRequest) =>
|
||||
request.post<PermissionResponse>(`${BASE_URL}/create`, data);
|
||||
|
||||
// 更新权限
|
||||
export const updatePermission = (id: number, data: PermissionRequest) =>
|
||||
request.put<PermissionResponse>(`${BASE_URL}/${id}`, data);
|
||||
|
||||
// 删除权限
|
||||
export const deletePermission = (id: number) =>
|
||||
request.delete(`${BASE_URL}/${id}`);
|
||||
|
||||
36
frontend/src/pages/System/Permission/List/types.ts
Normal file
36
frontend/src/pages/System/Permission/List/types.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { BaseQuery, BaseRequest, BaseResponse } from '@/types/base';
|
||||
import type { MenuResponse } from '@/pages/System/Menu/List/types';
|
||||
|
||||
/**
|
||||
* 权限查询参数
|
||||
*/
|
||||
export interface PermissionQuery extends BaseQuery {
|
||||
menuId?: number;
|
||||
code?: string;
|
||||
name?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限请求参数
|
||||
*/
|
||||
export interface PermissionRequest extends BaseRequest {
|
||||
menuId: number;
|
||||
code: string;
|
||||
name: string;
|
||||
type: string;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限响应数据
|
||||
*/
|
||||
export interface PermissionResponse extends BaseResponse {
|
||||
menuId: number;
|
||||
code: string;
|
||||
name: string;
|
||||
type: string;
|
||||
sort: number;
|
||||
menu?: MenuResponse;
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { DataTablePagination } from '@/components/ui/pagination';
|
||||
import {
|
||||
Loader2, Plus, Search, Edit, Trash2, KeyRound, Tag as TagIcon,
|
||||
ShieldCheck, Settings
|
||||
ShieldCheck, Settings, Shield
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { getRoleList, deleteRole, getRolePermissions, assignPermissions } from './service';
|
||||
@ -246,11 +246,12 @@ const RolePage: React.FC = () => {
|
||||
|
||||
{/* 表格 */}
|
||||
<div className="rounded-md border">
|
||||
<Table minWidth="1180px">
|
||||
<Table minWidth="1280px">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead width="150px">角色编码</TableHead>
|
||||
<TableHead width="150px">角色名称</TableHead>
|
||||
<TableHead width="100px">类型</TableHead>
|
||||
<TableHead width="200px">标签</TableHead>
|
||||
<TableHead width="100px">排序</TableHead>
|
||||
<TableHead width="200px">描述</TableHead>
|
||||
@ -261,7 +262,7 @@ const RolePage: React.FC = () => {
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="h-24 text-center">
|
||||
<TableCell colSpan={8} 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>
|
||||
@ -270,13 +271,22 @@ const RolePage: React.FC = () => {
|
||||
</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 width="150px" className="font-medium">
|
||||
<code className="text-sm">{record.code}</code>
|
||||
</TableCell>
|
||||
<TableCell width="150px">{record.name}</TableCell>
|
||||
<TableCell width="100px">
|
||||
{record.isAdmin ? (
|
||||
<Badge variant="default" className="bg-amber-500 hover:bg-amber-600">
|
||||
<Shield className="h-3 w-3 mr-1" />
|
||||
管理员
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">普通角色</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell width="200px">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{record.tags && record.tags.length > 0 ? (
|
||||
@ -330,7 +340,7 @@ const RolePage: React.FC = () => {
|
||||
>
|
||||
<TagIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
{!isAdmin && (
|
||||
{!record.isAdmin && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@ -348,7 +358,7 @@ const RolePage: React.FC = () => {
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="h-24 text-center">
|
||||
<TableCell colSpan={8} 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>
|
||||
|
||||
@ -27,6 +27,7 @@ export interface RoleRequest extends BaseRequest{
|
||||
name: string;
|
||||
description?: string;
|
||||
sort?: number;
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
// 角色响应数据
|
||||
@ -35,6 +36,7 @@ export interface RoleResponse extends BaseResponse {
|
||||
name: string;
|
||||
description?: string;
|
||||
sort: number;
|
||||
isAdmin: boolean;
|
||||
tags?: RoleTagResponse[];
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user