feat(web): 实现角色管理功能
- 添加角色列表页面和管理组件 - 实现角色 CRUD 操作 - 支持角色权限和菜单分配 - 添加角色表单和权限选择器 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
33
apps/web/src/app/(dashboard)/roles/page.tsx
Normal file
33
apps/web/src/app/(dashboard)/roles/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import { RolesTable } from '@/components/roles/RolesTable';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
|
||||
export default function RolesPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">角色管理</h1>
|
||||
<p className="text-muted-foreground">管理系统中的角色及其权限。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>角色列表</CardTitle>
|
||||
<CardDescription>查看和管理所有角色,可分配权限和菜单。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RolesTable />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
347
apps/web/src/components/roles/RoleEditDialog.tsx
Normal file
347
apps/web/src/components/roles/RoleEditDialog.tsx
Normal file
@@ -0,0 +1,347 @@
|
||||
'use client';
|
||||
|
||||
import type { RoleResponse, PermissionResponse } from '@seclusion/shared';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useAllPermissions, useCreateRole, useRole, useUpdateRole } from '@/hooks/useRoles';
|
||||
|
||||
interface RoleEditDialogProps {
|
||||
role: RoleResponse | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
interface RoleFormValues {
|
||||
code: string;
|
||||
name: string;
|
||||
description: string;
|
||||
isEnabled: boolean;
|
||||
sort: number;
|
||||
permissionIds: string[];
|
||||
}
|
||||
|
||||
export function RoleEditDialog({ role, open, onOpenChange }: RoleEditDialogProps) {
|
||||
const isEditing = !!role;
|
||||
const [activeTab, setActiveTab] = useState('basic');
|
||||
|
||||
// 获取角色详情(包含权限)
|
||||
const { data: roleDetail, isLoading: isLoadingRole } = useRole(role?.id || '');
|
||||
|
||||
// 获取所有权限
|
||||
const { data: permissions = [], isLoading: isLoadingPermissions } = useAllPermissions();
|
||||
|
||||
// 创建/更新
|
||||
const createRole = useCreateRole();
|
||||
const updateRole = useUpdateRole();
|
||||
|
||||
const form = useForm<RoleFormValues>({
|
||||
defaultValues: {
|
||||
code: '',
|
||||
name: '',
|
||||
description: '',
|
||||
isEnabled: true,
|
||||
sort: 0,
|
||||
permissionIds: [],
|
||||
},
|
||||
});
|
||||
|
||||
// 当对话框打开或角色数据变化时,重置表单
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (roleDetail) {
|
||||
form.reset({
|
||||
code: roleDetail.code,
|
||||
name: roleDetail.name,
|
||||
description: roleDetail.description || '',
|
||||
isEnabled: roleDetail.isEnabled,
|
||||
sort: roleDetail.sort,
|
||||
permissionIds: roleDetail.permissions?.map((p) => p.id) || [],
|
||||
});
|
||||
} else if (!role) {
|
||||
form.reset({
|
||||
code: '',
|
||||
name: '',
|
||||
description: '',
|
||||
isEnabled: true,
|
||||
sort: 0,
|
||||
permissionIds: [],
|
||||
});
|
||||
}
|
||||
setActiveTab('basic');
|
||||
}
|
||||
}, [open, roleDetail, role, form]);
|
||||
|
||||
const onSubmit = async (values: RoleFormValues) => {
|
||||
try {
|
||||
if (isEditing && role) {
|
||||
await updateRole.mutateAsync({
|
||||
id: role.id,
|
||||
data: {
|
||||
name: values.name,
|
||||
description: values.description || undefined,
|
||||
isEnabled: values.isEnabled,
|
||||
sort: values.sort,
|
||||
permissionIds: values.permissionIds,
|
||||
},
|
||||
});
|
||||
toast.success('角色更新成功');
|
||||
} else {
|
||||
await createRole.mutateAsync({
|
||||
code: values.code,
|
||||
name: values.name,
|
||||
description: values.description || undefined,
|
||||
isEnabled: values.isEnabled,
|
||||
sort: values.sort,
|
||||
permissionIds: values.permissionIds,
|
||||
});
|
||||
toast.success('角色创建成功');
|
||||
}
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : '操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 按资源分组权限
|
||||
const groupedPermissions = permissions.reduce<Record<string, PermissionResponse[]>>(
|
||||
(acc, permission) => {
|
||||
const resource = permission.resource;
|
||||
if (!acc[resource]) {
|
||||
acc[resource] = [];
|
||||
}
|
||||
acc[resource].push(permission);
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const isLoading = isLoadingRole || isLoadingPermissions;
|
||||
const isMutating = createRole.isPending || updateRole.isPending;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEditing ? '编辑角色' : '新建角色'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEditing ? '修改角色信息和权限配置。' : '创建新角色并配置权限。'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-muted-foreground">加载中...</div>
|
||||
</div>
|
||||
) : (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="basic">基本信息</TabsTrigger>
|
||||
<TabsTrigger value="permissions">权限配置</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="basic" className="space-y-4 mt-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="code"
|
||||
rules={{
|
||||
required: '请输入角色编码',
|
||||
pattern: {
|
||||
value: /^[a-z][a-z0-9_]*$/,
|
||||
message: '编码只能包含小写字母、数字和下划线,且以字母开头',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>角色编码</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="如:manager"
|
||||
{...field}
|
||||
disabled={isEditing || role?.isSystem}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>唯一标识,创建后不可修改</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
rules={{ required: '请输入角色名称' }}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>角色名称</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="如:项目经理" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>描述</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="角色描述(可选)"
|
||||
className="resize-none"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>排序</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isEnabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>启用状态</FormLabel>
|
||||
<FormDescription>禁用后该角色将不生效</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="permissions" className="space-y-4 mt-4">
|
||||
<div className="text-sm text-muted-foreground mb-4">
|
||||
选择该角色拥有的权限:
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="permissionIds"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-4">
|
||||
{Object.entries(groupedPermissions).map(([resource, perms]) => (
|
||||
<div key={resource} className="rounded-lg border p-4">
|
||||
<div className="font-medium mb-3 capitalize">{resource}</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{perms.map((permission) => (
|
||||
<div
|
||||
key={permission.id}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Checkbox
|
||||
id={permission.id}
|
||||
checked={field.value.includes(permission.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
field.onChange([...field.value, permission.id]);
|
||||
} else {
|
||||
field.onChange(
|
||||
field.value.filter((id) => id !== permission.id)
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor={permission.id}
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
{permission.name}
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
({permission.code})
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{Object.keys(groupedPermissions).length === 0 && (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
暂无可分配的权限
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isMutating}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit" disabled={isMutating}>
|
||||
{isMutating ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
312
apps/web/src/components/roles/RolesTable.tsx
Normal file
312
apps/web/src/components/roles/RolesTable.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
'use client';
|
||||
|
||||
import type { RoleResponse } from '@seclusion/shared';
|
||||
import { formatDate } from '@seclusion/shared';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { MoreHorizontal, Trash2, Pencil, Plus, Shield } from 'lucide-react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { RoleEditDialog } from './RoleEditDialog';
|
||||
|
||||
import {
|
||||
DataTable,
|
||||
DataTableColumnHeader,
|
||||
type PaginationState,
|
||||
type SortingParams,
|
||||
type SearchConfig,
|
||||
type SearchParams,
|
||||
} from '@/components/shared/DataTable';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { PAGINATION } from '@/config/constants';
|
||||
import { useRoles, useDeleteRole } from '@/hooks/useRoles';
|
||||
|
||||
interface RoleActionsProps {
|
||||
role: RoleResponse;
|
||||
onEdit: (role: RoleResponse) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
function RoleActions({ role, onEdit, onDelete }: RoleActionsProps) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">打开菜单</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>操作</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onEdit(role)}>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
{!role.isSystem && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDelete(role.id)}
|
||||
className="text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export function RolesTable() {
|
||||
// 分页状态
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
page: PAGINATION.DEFAULT_PAGE,
|
||||
pageSize: PAGINATION.DEFAULT_PAGE_SIZE,
|
||||
});
|
||||
|
||||
// 排序状态
|
||||
const [sorting, setSorting] = useState<SortingParams>({});
|
||||
|
||||
// 搜索状态
|
||||
const [searchParams, setSearchParams] = useState<SearchParams>({});
|
||||
|
||||
// 对话框状态
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [roleToDelete, setRoleToDelete] = useState<string | null>(null);
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [roleToEdit, setRoleToEdit] = useState<RoleResponse | null>(null);
|
||||
|
||||
// 查询
|
||||
const { data: rolesData, isLoading, refetch } = useRoles({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
search: typeof searchParams.search === 'string' ? searchParams.search : undefined,
|
||||
...sorting,
|
||||
});
|
||||
|
||||
// 变更
|
||||
const deleteRole = useDeleteRole();
|
||||
|
||||
const handleDelete = useCallback((id: string) => {
|
||||
setRoleToDelete(id);
|
||||
setDeleteDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const confirmDelete = useCallback(async () => {
|
||||
if (!roleToDelete) return;
|
||||
|
||||
try {
|
||||
await deleteRole.mutateAsync(roleToDelete);
|
||||
toast.success('角色已删除');
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : '删除失败');
|
||||
} finally {
|
||||
setDeleteDialogOpen(false);
|
||||
setRoleToDelete(null);
|
||||
}
|
||||
}, [roleToDelete, deleteRole]);
|
||||
|
||||
const handleEdit = useCallback((role: RoleResponse) => {
|
||||
setRoleToEdit(role);
|
||||
setEditDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
setRoleToEdit(null);
|
||||
setEditDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
// 分页变化处理
|
||||
const handlePaginationChange = useCallback((newPagination: PaginationState) => {
|
||||
setPagination(newPagination);
|
||||
}, []);
|
||||
|
||||
// 排序变化处理
|
||||
const handleSortingChange = useCallback((newSorting: SortingParams) => {
|
||||
setSorting(newSorting);
|
||||
setPagination((prev) => ({ ...prev, page: 1 }));
|
||||
}, []);
|
||||
|
||||
// 搜索变化处理
|
||||
const handleSearchChange = useCallback((params: SearchParams) => {
|
||||
setSearchParams(params);
|
||||
setPagination((prev) => ({ ...prev, page: 1 }));
|
||||
}, []);
|
||||
|
||||
// 搜索配置
|
||||
const searchConfig: SearchConfig<RoleResponse> = {
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
key: 'search',
|
||||
label: '关键词',
|
||||
placeholder: '搜索角色编码或名称',
|
||||
colSpan: 2,
|
||||
},
|
||||
],
|
||||
columns: 4,
|
||||
};
|
||||
|
||||
// 列定义
|
||||
const columns: ColumnDef<RoleResponse>[] = [
|
||||
{
|
||||
accessorKey: 'code',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
title="角色编码"
|
||||
sortKey={column.id}
|
||||
sorting={sorting}
|
||||
onSortingChange={handleSortingChange}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="h-4 w-4 text-muted-foreground" />
|
||||
<code className="text-sm">{row.original.code}</code>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
title="角色名称"
|
||||
sortKey={column.id}
|
||||
sorting={sorting}
|
||||
onSortingChange={handleSortingChange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: '描述',
|
||||
cell: ({ row }) => row.original.description || '-',
|
||||
},
|
||||
{
|
||||
accessorKey: 'isSystem',
|
||||
header: '类型',
|
||||
cell: ({ row }) => (
|
||||
<Badge variant={row.original.isSystem ? 'secondary' : 'outline'}>
|
||||
{row.original.isSystem ? '系统内置' : '自定义'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'isEnabled',
|
||||
header: '状态',
|
||||
cell: ({ row }) => (
|
||||
<Badge variant={row.original.isEnabled ? 'default' : 'secondary'}>
|
||||
{row.original.isEnabled ? '启用' : '禁用'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
title="创建时间"
|
||||
sortKey={column.id}
|
||||
sorting={sorting}
|
||||
onSortingChange={handleSortingChange}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
formatDate(new Date(row.original.createdAt), 'YYYY-MM-DD HH:mm'),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '操作',
|
||||
cell: ({ row }) => (
|
||||
<RoleActions
|
||||
role={row.original}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 工具栏 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" onClick={handleCreate}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新建角色
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 数据表格 */}
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={rolesData?.items ?? []}
|
||||
pagination={pagination}
|
||||
paginationInfo={
|
||||
rolesData ? { total: rolesData.total, totalPages: rolesData.totalPages } : undefined
|
||||
}
|
||||
onPaginationChange={handlePaginationChange}
|
||||
manualPagination
|
||||
sorting={sorting}
|
||||
onSortingChange={handleSortingChange}
|
||||
manualSorting
|
||||
searchConfig={searchConfig}
|
||||
searchParams={searchParams}
|
||||
onSearchChange={handleSearchChange}
|
||||
isLoading={isLoading}
|
||||
emptyMessage="暂无角色"
|
||||
/>
|
||||
|
||||
{/* 删除确认弹窗 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除该角色吗?删除后将无法恢复,且会影响关联的用户权限。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 编辑/新建角色弹窗 */}
|
||||
<RoleEditDialog
|
||||
role={roleToEdit}
|
||||
open={editDialogOpen}
|
||||
onOpenChange={setEditDialogOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
2
apps/web/src/components/roles/index.ts
Normal file
2
apps/web/src/components/roles/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { RolesTable } from './RolesTable';
|
||||
export { RoleEditDialog } from './RoleEditDialog';
|
||||
90
apps/web/src/hooks/useRoles.ts
Normal file
90
apps/web/src/hooks/useRoles.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { CreateRoleDto, UpdateRoleDto } from '@seclusion/shared';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { roleService, type GetRolesParams } from '@/services/role.service';
|
||||
import { useIsAuthenticated } from '@/stores';
|
||||
|
||||
// Query Keys
|
||||
export const roleKeys = {
|
||||
all: ['roles'] as const,
|
||||
lists: () => [...roleKeys.all, 'list'] as const,
|
||||
list: (params: GetRolesParams) => [...roleKeys.lists(), params] as const,
|
||||
details: () => [...roleKeys.all, 'detail'] as const,
|
||||
detail: (id: string) => [...roleKeys.details(), id] as const,
|
||||
};
|
||||
|
||||
export const permissionKeys = {
|
||||
all: ['permissions'] as const,
|
||||
list: () => [...permissionKeys.all, 'list'] as const,
|
||||
};
|
||||
|
||||
// 获取角色列表
|
||||
export function useRoles(params: GetRolesParams = {}) {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
return useQuery({
|
||||
queryKey: roleKeys.list(params),
|
||||
queryFn: () => roleService.getRoles(params),
|
||||
enabled: isAuthenticated,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取单个角色详情
|
||||
export function useRole(id: string) {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
return useQuery({
|
||||
queryKey: roleKeys.detail(id),
|
||||
queryFn: () => roleService.getRole(id),
|
||||
enabled: isAuthenticated && !!id,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取所有权限
|
||||
export function useAllPermissions() {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
return useQuery({
|
||||
queryKey: permissionKeys.list(),
|
||||
queryFn: () => roleService.getAllPermissions(),
|
||||
enabled: isAuthenticated,
|
||||
});
|
||||
}
|
||||
|
||||
// 创建角色
|
||||
export function useCreateRole() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateRoleDto) => roleService.createRole(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: roleKeys.lists() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 更新角色
|
||||
export function useUpdateRole() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: UpdateRoleDto }) =>
|
||||
roleService.updateRole(id, data),
|
||||
onSuccess: (_, { id }) => {
|
||||
queryClient.invalidateQueries({ queryKey: roleKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: roleKeys.detail(id) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 删除角色
|
||||
export function useDeleteRole() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => roleService.deleteRole(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: roleKeys.lists() });
|
||||
},
|
||||
});
|
||||
}
|
||||
55
apps/web/src/services/role.service.ts
Normal file
55
apps/web/src/services/role.service.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type {
|
||||
CreateRoleDto,
|
||||
PaginatedResponse,
|
||||
PermissionResponse,
|
||||
RoleDetailResponse,
|
||||
RoleResponse,
|
||||
UpdateRoleDto,
|
||||
} from '@seclusion/shared';
|
||||
|
||||
import { API_ENDPOINTS } from '@/config/constants';
|
||||
import { http } from '@/lib/http';
|
||||
|
||||
export interface GetRolesParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export const roleService = {
|
||||
// 获取角色列表
|
||||
getRoles: (params: GetRolesParams = {}): Promise<PaginatedResponse<RoleResponse>> => {
|
||||
return http.get<PaginatedResponse<RoleResponse>>(API_ENDPOINTS.ROLES, {
|
||||
params,
|
||||
});
|
||||
},
|
||||
|
||||
// 获取单个角色详情(包含权限和菜单)
|
||||
getRole: (id: string): Promise<RoleDetailResponse> => {
|
||||
return http.get<RoleDetailResponse>(`${API_ENDPOINTS.ROLES}/${id}`);
|
||||
},
|
||||
|
||||
// 创建角色
|
||||
createRole: (data: CreateRoleDto): Promise<RoleResponse> => {
|
||||
return http.post<RoleResponse>(API_ENDPOINTS.ROLES, data);
|
||||
},
|
||||
|
||||
// 更新角色
|
||||
updateRole: (id: string, data: UpdateRoleDto): Promise<RoleResponse> => {
|
||||
return http.patch<RoleResponse>(`${API_ENDPOINTS.ROLES}/${id}`, data);
|
||||
},
|
||||
|
||||
// 删除角色
|
||||
deleteRole: (id: string): Promise<void> => {
|
||||
return http.delete<void>(`${API_ENDPOINTS.ROLES}/${id}`);
|
||||
},
|
||||
|
||||
// 获取所有权限列表(用于分配权限,获取全部不分页)
|
||||
getAllPermissions: async (): Promise<PermissionResponse[]> => {
|
||||
const response = await http.get<PaginatedResponse<PermissionResponse>>(
|
||||
API_ENDPOINTS.PERMISSIONS,
|
||||
{ params: { pageSize: 1000 } }
|
||||
);
|
||||
return response.items;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user