增加权限弹窗

This commit is contained in:
dengqichen 2025-11-01 15:09:47 +08:00
parent 95e66480a2
commit 774523696a
9 changed files with 667 additions and 76 deletions

View File

@ -99,7 +99,7 @@ const DialogBody = ({
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn( className={cn(
"flex-1 overflow-y-auto px-6 py-4", "flex-1 overflow-y-auto px-6 py-4 pt-6",
className className
)} )}
{...props} {...props}

View File

@ -6,9 +6,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch'; import { Loader2 } from 'lucide-react';
import { Separator } from '@/components/ui/separator';
import { FileText, Tag, Folder, AlignLeft, Copy, Loader2 } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
import { createDefinition, updateDefinition } from '../service'; import { createDefinition, updateDefinition } from '../service';
import { getEnabledCategories } from '../../../Category/service'; import { getEnabledCategories } from '../../../Category/service';
@ -42,7 +40,6 @@ const FormBasicInfoModal: React.FC<FormBasicInfoModalProps> = ({
key: '', key: '',
categoryId: undefined as number | undefined, categoryId: undefined as number | undefined,
description: '', description: '',
isTemplate: false,
}); });
// 加载分类列表和初始化数据 // 加载分类列表和初始化数据
@ -58,7 +55,6 @@ const FormBasicInfoModal: React.FC<FormBasicInfoModalProps> = ({
key: record.key, key: record.key,
categoryId: record.categoryId, categoryId: record.categoryId,
description: record.description || '', description: record.description || '',
isTemplate: record.isTemplate || false,
}); });
} else if (mode === 'create') { } else if (mode === 'create') {
resetForm(); resetForm();
@ -81,7 +77,6 @@ const FormBasicInfoModal: React.FC<FormBasicInfoModalProps> = ({
key: '', key: '',
categoryId: undefined, categoryId: undefined,
description: '', description: '',
isTemplate: false,
}); });
}; };
@ -132,7 +127,7 @@ const FormBasicInfoModal: React.FC<FormBasicInfoModalProps> = ({
key: formData.key, key: formData.key,
categoryId: formData.categoryId, categoryId: formData.categoryId,
description: formData.description, description: formData.description,
isTemplate: formData.isTemplate, isTemplate: false,
status: 'DRAFT', status: 'DRAFT',
schema: { schema: {
version: '1.0', version: '1.0',
@ -166,7 +161,7 @@ const FormBasicInfoModal: React.FC<FormBasicInfoModalProps> = ({
description: formData.description, description: formData.description,
schema: record.schema, schema: record.schema,
status: record.status, status: record.status,
isTemplate: formData.isTemplate, isTemplate: record.isTemplate,
version: record.version, // 乐观锁版本号 version: record.version, // 乐观锁版本号
}; };
@ -204,14 +199,36 @@ const FormBasicInfoModal: React.FC<FormBasicInfoModalProps> = ({
</DialogHeader> </DialogHeader>
<DialogBody> <DialogBody>
<Separator />
<div className="space-y-6"> <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="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="form-name" className="flex items-center gap-2"> <Label htmlFor="form-name">
<FileText className="h-4 w-4 text-muted-foreground" />
<span className="text-destructive">*</span> <span className="text-destructive">*</span>
</Label> </Label>
@ -228,8 +245,7 @@ const FormBasicInfoModal: React.FC<FormBasicInfoModalProps> = ({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="form-key" className="flex items-center gap-2"> <Label htmlFor="form-key">
<Tag className="h-4 w-4 text-muted-foreground" />
<span className="text-destructive">*</span> <span className="text-destructive">*</span>
</Label> </Label>
@ -246,58 +262,9 @@ const FormBasicInfoModal: React.FC<FormBasicInfoModalProps> = ({
</div> </div>
</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"> <div className="space-y-2">
<Label htmlFor="form-description" className="flex items-center gap-2"> <Label htmlFor="form-description">
<AlignLeft className="h-4 w-4 text-muted-foreground" />
</Label> </Label>
<Textarea <Textarea

View File

@ -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;

View File

@ -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;

View File

@ -7,7 +7,7 @@ import { useToast } from '@/components/ui/use-toast';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { import {
Loader2, Plus, Edit, Trash2, ChevronRight, ChevronDown, Loader2, Plus, Edit, Trash2, ChevronRight, ChevronDown,
FolderTree, Menu as MenuIcon, EyeOff FolderTree, Menu as MenuIcon, EyeOff, Key
} from 'lucide-react'; } from 'lucide-react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { setMenus } from '@/store/userSlice'; import { setMenus } from '@/store/userSlice';
@ -16,6 +16,7 @@ import type { MenuResponse } from './types';
import { getIconComponent } from '@/config/icons.tsx'; import { getIconComponent } from '@/config/icons.tsx';
import EditDialog from './components/EditDialog'; import EditDialog from './components/EditDialog';
import DeleteDialog from './components/DeleteDialog'; import DeleteDialog from './components/DeleteDialog';
import PermissionDialog from './components/PermissionDialog';
/** /**
* *
@ -30,6 +31,8 @@ const MenuPage: React.FC = () => {
const [editRecord, setEditRecord] = useState<MenuResponse>(); const [editRecord, setEditRecord] = useState<MenuResponse>();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteRecord, setDeleteRecord] = useState<MenuResponse | null>(null); const [deleteRecord, setDeleteRecord] = useState<MenuResponse | null>(null);
const [permissionDialogOpen, setPermissionDialogOpen] = useState(false);
const [permissionMenu, setPermissionMenu] = useState<MenuResponse | null>(null);
// 加载菜单树 // 加载菜单树
const loadMenuTree = async () => { const loadMenuTree = async () => {
@ -93,6 +96,12 @@ const MenuPage: React.FC = () => {
setDeleteDialogOpen(true); setDeleteDialogOpen(true);
}; };
// 权限管理
const handlePermission = (record: MenuResponse) => {
setPermissionMenu(record);
setPermissionDialogOpen(true);
};
const confirmDelete = async () => { const confirmDelete = async () => {
if (!deleteRecord) return; if (!deleteRecord) return;
try { try {
@ -151,6 +160,9 @@ const MenuPage: React.FC = () => {
key={menu.id} key={menu.id}
className={`hover:bg-muted/50 ${menu.hidden ? 'bg-muted/30' : ''}`} className={`hover:bg-muted/50 ${menu.hidden ? 'bg-muted/30' : ''}`}
> >
<TableCell width="80px" className="text-muted-foreground">
{menu.id}
</TableCell>
<TableCell width="250px"> <TableCell width="250px">
<div className="flex items-center gap-2" style={{ paddingLeft: `${level * 24}px` }}> <div className="flex items-center gap-2" style={{ paddingLeft: `${level * 24}px` }}>
{hasChildren ? ( {hasChildren ? (
@ -214,6 +226,14 @@ const MenuPage: React.FC = () => {
<TableCell width="100px" className="text-center">{menu.sort}</TableCell> <TableCell width="100px" className="text-center">{menu.sort}</TableCell>
<TableCell width="200px" sticky> <TableCell width="200px" sticky>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handlePermission(menu)}
title="权限"
>
<Key className="h-4 w-4" />
</Button>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@ -337,9 +357,10 @@ const MenuPage: React.FC = () => {
<CardContent> <CardContent>
{/* 表格 */} {/* 表格 */}
<div className="rounded-md border"> <div className="rounded-md border">
<Table minWidth="1280px"> <Table minWidth="1360px">
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead width="80px">ID</TableHead>
<TableHead width="250px"></TableHead> <TableHead width="250px"></TableHead>
<TableHead width="80px"></TableHead> <TableHead width="80px"></TableHead>
<TableHead width="200px"></TableHead> <TableHead width="200px"></TableHead>
@ -353,7 +374,7 @@ const MenuPage: React.FC = () => {
<TableBody> <TableBody>
{loading ? ( {loading ? (
<TableRow> <TableRow>
<TableCell colSpan={8} className="h-24 text-center"> <TableCell colSpan={9} className="h-24 text-center">
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin mr-2" /> <Loader2 className="h-6 w-6 animate-spin mr-2" />
<span className="text-sm text-muted-foreground">...</span> <span className="text-sm text-muted-foreground">...</span>
@ -364,7 +385,7 @@ const MenuPage: React.FC = () => {
menuTree.map(menu => renderMenuRow(menu)) menuTree.map(menu => renderMenuRow(menu))
) : ( ) : (
<TableRow> <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"> <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" /> <FolderTree className="w-16 h-16 mb-4 text-muted-foreground/50" />
<div className="text-lg font-semibold mb-2"></div> <div className="text-lg font-semibold mb-2"></div>
@ -398,6 +419,15 @@ const MenuPage: React.FC = () => {
onOpenChange={setDeleteDialogOpen} onOpenChange={setDeleteDialogOpen}
onConfirm={confirmDelete} onConfirm={confirmDelete}
/> />
{/* 权限管理对话框 */}
{permissionMenu && (
<PermissionDialog
open={permissionDialogOpen}
menu={permissionMenu}
onOpenChange={setPermissionDialogOpen}
/>
)}
</div> </div>
); );
}; };

View 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}`);

View 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;
}

View File

@ -7,7 +7,7 @@ import { Input } from '@/components/ui/input';
import { DataTablePagination } from '@/components/ui/pagination'; import { DataTablePagination } from '@/components/ui/pagination';
import { import {
Loader2, Plus, Search, Edit, Trash2, KeyRound, Tag as TagIcon, Loader2, Plus, Search, Edit, Trash2, KeyRound, Tag as TagIcon,
ShieldCheck, Settings ShieldCheck, Settings, Shield
} from 'lucide-react'; } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
import { getRoleList, deleteRole, getRolePermissions, assignPermissions } from './service'; import { getRoleList, deleteRole, getRolePermissions, assignPermissions } from './service';
@ -246,11 +246,12 @@ const RolePage: React.FC = () => {
{/* 表格 */} {/* 表格 */}
<div className="rounded-md border"> <div className="rounded-md border">
<Table minWidth="1180px"> <Table minWidth="1280px">
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead width="150px"></TableHead> <TableHead width="150px"></TableHead>
<TableHead width="150px"></TableHead> <TableHead width="150px"></TableHead>
<TableHead width="100px"></TableHead>
<TableHead width="200px"></TableHead> <TableHead width="200px"></TableHead>
<TableHead width="100px"></TableHead> <TableHead width="100px"></TableHead>
<TableHead width="200px"></TableHead> <TableHead width="200px"></TableHead>
@ -261,7 +262,7 @@ const RolePage: React.FC = () => {
<TableBody> <TableBody>
{loading ? ( {loading ? (
<TableRow> <TableRow>
<TableCell colSpan={7} className="h-24 text-center"> <TableCell colSpan={8} className="h-24 text-center">
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin mr-2" /> <Loader2 className="h-6 w-6 animate-spin mr-2" />
<span className="text-sm text-muted-foreground">...</span> <span className="text-sm text-muted-foreground">...</span>
@ -270,13 +271,22 @@ const RolePage: React.FC = () => {
</TableRow> </TableRow>
) : data?.content && data.content.length > 0 ? ( ) : data?.content && data.content.length > 0 ? (
data.content.map((record) => { data.content.map((record) => {
const isAdmin = record.code === 'admin';
return ( return (
<TableRow key={record.id} className="hover:bg-muted/50"> <TableRow key={record.id} className="hover:bg-muted/50">
<TableCell width="150px" className="font-medium"> <TableCell width="150px" className="font-medium">
<code className="text-sm">{record.code}</code> <code className="text-sm">{record.code}</code>
</TableCell> </TableCell>
<TableCell width="150px">{record.name}</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"> <TableCell width="200px">
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{record.tags && record.tags.length > 0 ? ( {record.tags && record.tags.length > 0 ? (
@ -330,7 +340,7 @@ const RolePage: React.FC = () => {
> >
<TagIcon className="h-4 w-4" /> <TagIcon className="h-4 w-4" />
</Button> </Button>
{!isAdmin && ( {!record.isAdmin && (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@ -348,7 +358,7 @@ const RolePage: React.FC = () => {
}) })
) : ( ) : (
<TableRow> <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"> <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" /> <ShieldCheck className="w-16 h-16 mb-4 text-muted-foreground/50" />
<div className="text-lg font-semibold mb-2"></div> <div className="text-lg font-semibold mb-2"></div>

View File

@ -27,6 +27,7 @@ export interface RoleRequest extends BaseRequest{
name: string; name: string;
description?: string; description?: string;
sort?: number; sort?: number;
isAdmin?: boolean;
} }
// 角色响应数据 // 角色响应数据
@ -35,6 +36,7 @@ export interface RoleResponse extends BaseResponse {
name: string; name: string;
description?: string; description?: string;
sort: number; sort: number;
isAdmin: boolean;
tags?: RoleTagResponse[]; tags?: RoleTagResponse[];
createTime: string; createTime: string;
updateTime: string; updateTime: string;