feat(web): 实现菜单管理功能
- 添加菜单列表页面和管理组件 - 实现菜单 CRUD 操作和树形结构展示 - 添加菜单表单(支持创建/编辑) - 实现图标选择器和动态图标映射 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
33
apps/web/src/app/(dashboard)/menus/page.tsx
Normal file
33
apps/web/src/app/(dashboard)/menus/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import { MenusTable } from '@/components/menus/MenusTable';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
|
||||
export default function MenusPage() {
|
||||
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>
|
||||
<MenusTable />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
398
apps/web/src/components/menus/MenuEditDialog.tsx
Normal file
398
apps/web/src/components/menus/MenuEditDialog.tsx
Normal file
@@ -0,0 +1,398 @@
|
||||
'use client';
|
||||
|
||||
import type { MenuResponse, MenuTreeNode } from '@seclusion/shared';
|
||||
import { MenuType } from '@seclusion/shared';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { useCreateMenu, useMenuTree, useUpdateMenu } from '@/hooks/useMenus';
|
||||
|
||||
// 用于表示无父级菜单的特殊值(因为 Select 不允许空字符串)
|
||||
const NO_PARENT_VALUE = '__root__';
|
||||
|
||||
interface MenuEditDialogProps {
|
||||
menu: MenuResponse | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
interface MenuFormValues {
|
||||
parentId: string;
|
||||
code: string;
|
||||
name: string;
|
||||
type: string;
|
||||
path: string;
|
||||
icon: string;
|
||||
permission: string;
|
||||
isHidden: boolean;
|
||||
isEnabled: boolean;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
// 递归渲染菜单树选项
|
||||
function renderMenuOptions(menus: MenuTreeNode[], level = 0): React.ReactNode[] {
|
||||
const options: React.ReactNode[] = [];
|
||||
|
||||
for (const menu of menus) {
|
||||
// 只显示目录类型作为父级选项
|
||||
if (menu.type === MenuType.DIR || menu.type === MenuType.MENU) {
|
||||
const prefix = '\u00A0\u00A0'.repeat(level);
|
||||
options.push(
|
||||
<SelectItem key={menu.id} value={menu.id}>
|
||||
{prefix}{menu.name}
|
||||
</SelectItem>
|
||||
);
|
||||
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
options.push(...renderMenuOptions(menu.children, level + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function MenuEditDialog({ menu, open, onOpenChange }: MenuEditDialogProps) {
|
||||
const isEditing = !!menu;
|
||||
|
||||
// 获取菜单树(用于选择父级)
|
||||
const { data: menuTree = [], isLoading: isLoadingTree } = useMenuTree();
|
||||
|
||||
// 创建/更新
|
||||
const createMenu = useCreateMenu();
|
||||
const updateMenu = useUpdateMenu();
|
||||
|
||||
const form = useForm<MenuFormValues>({
|
||||
defaultValues: {
|
||||
parentId: NO_PARENT_VALUE,
|
||||
code: '',
|
||||
name: '',
|
||||
type: MenuType.MENU,
|
||||
path: '',
|
||||
icon: '',
|
||||
permission: '',
|
||||
isHidden: false,
|
||||
isEnabled: true,
|
||||
sort: 0,
|
||||
},
|
||||
});
|
||||
|
||||
// 当对话框打开或菜单数据变化时,重置表单
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (menu) {
|
||||
form.reset({
|
||||
parentId: menu.parentId || NO_PARENT_VALUE,
|
||||
code: menu.code,
|
||||
name: menu.name,
|
||||
type: menu.type,
|
||||
path: menu.path || '',
|
||||
icon: menu.icon || '',
|
||||
permission: menu.permission || '',
|
||||
isHidden: menu.isHidden,
|
||||
isEnabled: menu.isEnabled,
|
||||
sort: menu.sort,
|
||||
});
|
||||
} else {
|
||||
form.reset({
|
||||
parentId: NO_PARENT_VALUE,
|
||||
code: '',
|
||||
name: '',
|
||||
type: MenuType.MENU,
|
||||
path: '',
|
||||
icon: '',
|
||||
permission: '',
|
||||
isHidden: false,
|
||||
isEnabled: true,
|
||||
sort: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [open, menu, form]);
|
||||
|
||||
const onSubmit = async (values: MenuFormValues) => {
|
||||
try {
|
||||
const data = {
|
||||
...values,
|
||||
parentId: values.parentId === NO_PARENT_VALUE ? undefined : values.parentId,
|
||||
path: values.path || undefined,
|
||||
icon: values.icon || undefined,
|
||||
permission: values.permission || undefined,
|
||||
};
|
||||
|
||||
if (isEditing && menu) {
|
||||
const { code: _code, ...updateData } = data;
|
||||
await updateMenu.mutateAsync({
|
||||
id: menu.id,
|
||||
data: updateData,
|
||||
});
|
||||
toast.success('菜单更新成功');
|
||||
} else {
|
||||
await createMenu.mutateAsync(data);
|
||||
toast.success('菜单创建成功');
|
||||
}
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : '操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const isMutating = createMenu.isPending || updateMenu.isPending;
|
||||
const menuType = form.watch('type');
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEditing ? '编辑菜单' : '新建菜单'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEditing ? '修改菜单配置。' : '创建新的菜单项。'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="parentId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>父级菜单</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
disabled={isLoadingTree}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择父级菜单(可选)" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value={NO_PARENT_VALUE}>无(顶级菜单)</SelectItem>
|
||||
{renderMenuOptions(menuTree)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-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="如:user-management"
|
||||
{...field}
|
||||
disabled={isEditing}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
rules={{ required: '请输入菜单名称' }}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>菜单名称</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="如:用户管理" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>菜单类型</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value={MenuType.DIR}>目录</SelectItem>
|
||||
<SelectItem value={MenuType.MENU}>菜单</SelectItem>
|
||||
<SelectItem value={MenuType.BUTTON}>按钮</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{menuType !== MenuType.BUTTON && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="path"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>路由路径</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="如:/users" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="icon"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>图标</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Lucide 图标名,如:Users" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
参考 <a href="https://lucide.dev/icons" target="_blank" rel="noreferrer" className="text-primary hover:underline">Lucide Icons</a>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="permission"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>权限编码</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="如:user:read(可选)" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
关联的权限编码,格式为 资源:操作
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isHidden"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center gap-2">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">隐藏菜单</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isEnabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center gap-2">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">启用</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isMutating}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit" disabled={isMutating}>
|
||||
{isMutating ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
375
apps/web/src/components/menus/MenusTable.tsx
Normal file
375
apps/web/src/components/menus/MenusTable.tsx
Normal file
@@ -0,0 +1,375 @@
|
||||
'use client';
|
||||
|
||||
import type { MenuResponse } from '@seclusion/shared';
|
||||
import { MenuType } from '@seclusion/shared';
|
||||
import { formatDate } from '@seclusion/shared';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { MoreHorizontal, Trash2, Pencil, Plus, Folder, File, MousePointer } from 'lucide-react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { MenuEditDialog } from './MenuEditDialog';
|
||||
|
||||
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 { useMenus, useDeleteMenu } from '@/hooks/useMenus';
|
||||
|
||||
// 获取菜单类型图标
|
||||
function getMenuTypeIcon(type: string) {
|
||||
switch (type) {
|
||||
case MenuType.DIR:
|
||||
return <Folder className="h-4 w-4 text-amber-500" />;
|
||||
case MenuType.MENU:
|
||||
return <File className="h-4 w-4 text-blue-500" />;
|
||||
case MenuType.BUTTON:
|
||||
return <MousePointer className="h-4 w-4 text-green-500" />;
|
||||
default:
|
||||
return <File className="h-4 w-4" />;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取菜单类型文本
|
||||
function getMenuTypeText(type: string) {
|
||||
switch (type) {
|
||||
case MenuType.DIR:
|
||||
return '目录';
|
||||
case MenuType.MENU:
|
||||
return '菜单';
|
||||
case MenuType.BUTTON:
|
||||
return '按钮';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
interface MenuActionsProps {
|
||||
menu: MenuResponse;
|
||||
onEdit: (menu: MenuResponse) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
function MenuActions({ menu, onEdit, onDelete }: MenuActionsProps) {
|
||||
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(menu)}>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
{!menu.isStatic && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDelete(menu.id)}
|
||||
className="text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export function MenusTable() {
|
||||
// 分页状态
|
||||
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 [menuToDelete, setMenuToDelete] = useState<string | null>(null);
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [menuToEdit, setMenuToEdit] = useState<MenuResponse | null>(null);
|
||||
|
||||
// 查询
|
||||
const { data: menusData, isLoading, refetch } = useMenus({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
search: typeof searchParams.search === 'string' ? searchParams.search : undefined,
|
||||
...sorting,
|
||||
});
|
||||
|
||||
// 变更
|
||||
const deleteMenu = useDeleteMenu();
|
||||
|
||||
const handleDelete = useCallback((id: string) => {
|
||||
setMenuToDelete(id);
|
||||
setDeleteDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const confirmDelete = useCallback(async () => {
|
||||
if (!menuToDelete) return;
|
||||
|
||||
try {
|
||||
await deleteMenu.mutateAsync(menuToDelete);
|
||||
toast.success('菜单已删除');
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : '删除失败');
|
||||
} finally {
|
||||
setDeleteDialogOpen(false);
|
||||
setMenuToDelete(null);
|
||||
}
|
||||
}, [menuToDelete, deleteMenu]);
|
||||
|
||||
const handleEdit = useCallback((menu: MenuResponse) => {
|
||||
setMenuToEdit(menu);
|
||||
setEditDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
setMenuToEdit(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<MenuResponse> = {
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
key: 'search',
|
||||
label: '关键词',
|
||||
placeholder: '搜索菜单编码或名称',
|
||||
colSpan: 2,
|
||||
},
|
||||
],
|
||||
columns: 4,
|
||||
};
|
||||
|
||||
// 列定义
|
||||
const columns: ColumnDef<MenuResponse>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
title="菜单名称"
|
||||
sortKey={column.id}
|
||||
sorting={sorting}
|
||||
onSortingChange={handleSortingChange}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{getMenuTypeIcon(row.original.type)}
|
||||
<span>{row.original.name}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'code',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
title="编码"
|
||||
sortKey={column.id}
|
||||
sorting={sorting}
|
||||
onSortingChange={handleSortingChange}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<code className="text-sm text-muted-foreground">{row.original.code}</code>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'type',
|
||||
header: '类型',
|
||||
cell: ({ row }) => (
|
||||
<Badge variant="outline">{getMenuTypeText(row.original.type)}</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'path',
|
||||
header: '路由路径',
|
||||
cell: ({ row }) => (
|
||||
<code className="text-sm text-muted-foreground">
|
||||
{row.original.path || '-'}
|
||||
</code>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'permission',
|
||||
header: '权限编码',
|
||||
cell: ({ row }) => (
|
||||
<code className="text-sm text-muted-foreground">
|
||||
{row.original.permission || '-'}
|
||||
</code>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'isStatic',
|
||||
header: '来源',
|
||||
cell: ({ row }) => (
|
||||
<Badge variant={row.original.isStatic ? 'secondary' : 'outline'}>
|
||||
{row.original.isStatic ? '静态' : '动态'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'isEnabled',
|
||||
header: '状态',
|
||||
cell: ({ row }) => (
|
||||
<Badge variant={row.original.isEnabled ? 'default' : 'secondary'}>
|
||||
{row.original.isEnabled ? '启用' : '禁用'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'sort',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
title="排序"
|
||||
sortKey={column.id}
|
||||
sorting={sorting}
|
||||
onSortingChange={handleSortingChange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
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 }) => (
|
||||
<MenuActions
|
||||
menu={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={menusData?.items ?? []}
|
||||
pagination={pagination}
|
||||
paginationInfo={
|
||||
menusData ? { total: menusData.total, totalPages: menusData.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>
|
||||
|
||||
{/* 编辑/新建菜单弹窗 */}
|
||||
<MenuEditDialog
|
||||
menu={menuToEdit}
|
||||
open={editDialogOpen}
|
||||
onOpenChange={setEditDialogOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
2
apps/web/src/components/menus/index.ts
Normal file
2
apps/web/src/components/menus/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { MenusTable } from './MenusTable';
|
||||
export { MenuEditDialog } from './MenuEditDialog';
|
||||
89
apps/web/src/hooks/useMenus.ts
Normal file
89
apps/web/src/hooks/useMenus.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import type { CreateMenuDto, UpdateMenuDto } from '@seclusion/shared';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { menuService, type GetMenusParams } from '@/services/menu.service';
|
||||
import { useIsAuthenticated } from '@/stores';
|
||||
|
||||
// Query Keys
|
||||
export const menuKeys = {
|
||||
all: ['menus'] as const,
|
||||
lists: () => [...menuKeys.all, 'list'] as const,
|
||||
list: (params: GetMenusParams) => [...menuKeys.lists(), params] as const,
|
||||
tree: () => [...menuKeys.all, 'tree'] as const,
|
||||
details: () => [...menuKeys.all, 'detail'] as const,
|
||||
detail: (id: string) => [...menuKeys.details(), id] as const,
|
||||
};
|
||||
|
||||
// 获取菜单列表
|
||||
export function useMenus(params: GetMenusParams = {}) {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
return useQuery({
|
||||
queryKey: menuKeys.list(params),
|
||||
queryFn: () => menuService.getMenus(params),
|
||||
enabled: isAuthenticated,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取菜单树
|
||||
export function useMenuTree() {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
return useQuery({
|
||||
queryKey: menuKeys.tree(),
|
||||
queryFn: () => menuService.getMenuTree(),
|
||||
enabled: isAuthenticated,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取单个菜单
|
||||
export function useMenu(id: string) {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
return useQuery({
|
||||
queryKey: menuKeys.detail(id),
|
||||
queryFn: () => menuService.getMenu(id),
|
||||
enabled: isAuthenticated && !!id,
|
||||
});
|
||||
}
|
||||
|
||||
// 创建菜单
|
||||
export function useCreateMenu() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateMenuDto) => menuService.createMenu(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: menuKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: menuKeys.tree() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 更新菜单
|
||||
export function useUpdateMenu() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: UpdateMenuDto }) =>
|
||||
menuService.updateMenu(id, data),
|
||||
onSuccess: (_, { id }) => {
|
||||
queryClient.invalidateQueries({ queryKey: menuKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: menuKeys.tree() });
|
||||
queryClient.invalidateQueries({ queryKey: menuKeys.detail(id) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 删除菜单
|
||||
export function useDeleteMenu() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => menuService.deleteMenu(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: menuKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: menuKeys.tree() });
|
||||
},
|
||||
});
|
||||
}
|
||||
287
apps/web/src/lib/icons.ts
Normal file
287
apps/web/src/lib/icons.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* 图标映射
|
||||
* 将字符串图标名称映射到 Lucide 图标组件
|
||||
*/
|
||||
export const iconMap: Record<string, LucideIcon> = {
|
||||
// 常用图标
|
||||
LayoutDashboard: LucideIcons.LayoutDashboard,
|
||||
Users: LucideIcons.Users,
|
||||
User: LucideIcons.User,
|
||||
Settings: LucideIcons.Settings,
|
||||
Shield: LucideIcons.Shield,
|
||||
Menu: LucideIcons.Menu,
|
||||
Home: LucideIcons.Home,
|
||||
FileText: LucideIcons.FileText,
|
||||
FolderOpen: LucideIcons.FolderOpen,
|
||||
Lock: LucideIcons.Lock,
|
||||
Key: LucideIcons.Key,
|
||||
Bell: LucideIcons.Bell,
|
||||
Mail: LucideIcons.Mail,
|
||||
Calendar: LucideIcons.Calendar,
|
||||
Clock: LucideIcons.Clock,
|
||||
Search: LucideIcons.Search,
|
||||
Filter: LucideIcons.Filter,
|
||||
Plus: LucideIcons.Plus,
|
||||
Minus: LucideIcons.Minus,
|
||||
Edit: LucideIcons.Edit,
|
||||
Trash: LucideIcons.Trash,
|
||||
Eye: LucideIcons.Eye,
|
||||
EyeOff: LucideIcons.EyeOff,
|
||||
Check: LucideIcons.Check,
|
||||
X: LucideIcons.X,
|
||||
ChevronDown: LucideIcons.ChevronDown,
|
||||
ChevronUp: LucideIcons.ChevronUp,
|
||||
ChevronLeft: LucideIcons.ChevronLeft,
|
||||
ChevronRight: LucideIcons.ChevronRight,
|
||||
MoreHorizontal: LucideIcons.MoreHorizontal,
|
||||
MoreVertical: LucideIcons.MoreVertical,
|
||||
ExternalLink: LucideIcons.ExternalLink,
|
||||
Download: LucideIcons.Download,
|
||||
Upload: LucideIcons.Upload,
|
||||
Refresh: LucideIcons.RefreshCw,
|
||||
Copy: LucideIcons.Copy,
|
||||
Share: LucideIcons.Share2,
|
||||
Heart: LucideIcons.Heart,
|
||||
Star: LucideIcons.Star,
|
||||
Flag: LucideIcons.Flag,
|
||||
Tag: LucideIcons.Tag,
|
||||
Bookmark: LucideIcons.Bookmark,
|
||||
Archive: LucideIcons.Archive,
|
||||
Inbox: LucideIcons.Inbox,
|
||||
Send: LucideIcons.Send,
|
||||
MessageSquare: LucideIcons.MessageSquare,
|
||||
Phone: LucideIcons.Phone,
|
||||
Video: LucideIcons.Video,
|
||||
Image: LucideIcons.Image,
|
||||
Music: LucideIcons.Music,
|
||||
Film: LucideIcons.Film,
|
||||
Map: LucideIcons.Map,
|
||||
Globe: LucideIcons.Globe,
|
||||
Link: LucideIcons.Link,
|
||||
Paperclip: LucideIcons.Paperclip,
|
||||
Printer: LucideIcons.Printer,
|
||||
Save: LucideIcons.Save,
|
||||
Database: LucideIcons.Database,
|
||||
Server: LucideIcons.Server,
|
||||
Cloud: LucideIcons.Cloud,
|
||||
Wifi: LucideIcons.Wifi,
|
||||
Bluetooth: LucideIcons.Bluetooth,
|
||||
Battery: LucideIcons.Battery,
|
||||
Cpu: LucideIcons.Cpu,
|
||||
HardDrive: LucideIcons.HardDrive,
|
||||
Monitor: LucideIcons.Monitor,
|
||||
Smartphone: LucideIcons.Smartphone,
|
||||
Tablet: LucideIcons.Tablet,
|
||||
Watch: LucideIcons.Watch,
|
||||
Camera: LucideIcons.Camera,
|
||||
Mic: LucideIcons.Mic,
|
||||
Volume2: LucideIcons.Volume2,
|
||||
VolumeX: LucideIcons.VolumeX,
|
||||
Play: LucideIcons.Play,
|
||||
Pause: LucideIcons.Pause,
|
||||
SkipBack: LucideIcons.SkipBack,
|
||||
SkipForward: LucideIcons.SkipForward,
|
||||
Repeat: LucideIcons.Repeat,
|
||||
Shuffle: LucideIcons.Shuffle,
|
||||
Activity: LucideIcons.Activity,
|
||||
AlertCircle: LucideIcons.AlertCircle,
|
||||
AlertTriangle: LucideIcons.AlertTriangle,
|
||||
Info: LucideIcons.Info,
|
||||
HelpCircle: LucideIcons.HelpCircle,
|
||||
CircleDollarSign: LucideIcons.CircleDollarSign,
|
||||
CreditCard: LucideIcons.CreditCard,
|
||||
Wallet: LucideIcons.Wallet,
|
||||
ShoppingCart: LucideIcons.ShoppingCart,
|
||||
ShoppingBag: LucideIcons.ShoppingBag,
|
||||
Package: LucideIcons.Package,
|
||||
Truck: LucideIcons.Truck,
|
||||
Building: LucideIcons.Building,
|
||||
Building2: LucideIcons.Building2,
|
||||
Briefcase: LucideIcons.Briefcase,
|
||||
GraduationCap: LucideIcons.GraduationCap,
|
||||
Award: LucideIcons.Award,
|
||||
Trophy: LucideIcons.Trophy,
|
||||
Target: LucideIcons.Target,
|
||||
Zap: LucideIcons.Zap,
|
||||
Flame: LucideIcons.Flame,
|
||||
Sparkles: LucideIcons.Sparkles,
|
||||
Sun: LucideIcons.Sun,
|
||||
Moon: LucideIcons.Moon,
|
||||
CloudSun: LucideIcons.CloudSun,
|
||||
Thermometer: LucideIcons.Thermometer,
|
||||
Droplet: LucideIcons.Droplet,
|
||||
Wind: LucideIcons.Wind,
|
||||
Umbrella: LucideIcons.Umbrella,
|
||||
Snowflake: LucideIcons.Snowflake,
|
||||
Leaf: LucideIcons.Leaf,
|
||||
Flower: LucideIcons.Flower,
|
||||
Tree: LucideIcons.TreeDeciduous,
|
||||
Mountain: LucideIcons.Mountain,
|
||||
Waves: LucideIcons.Waves,
|
||||
Anchor: LucideIcons.Anchor,
|
||||
Compass: LucideIcons.Compass,
|
||||
Navigation: LucideIcons.Navigation,
|
||||
Plane: LucideIcons.Plane,
|
||||
Car: LucideIcons.Car,
|
||||
Bus: LucideIcons.Bus,
|
||||
Train: LucideIcons.Train,
|
||||
Ship: LucideIcons.Ship,
|
||||
Bike: LucideIcons.Bike,
|
||||
Footprints: LucideIcons.Footprints,
|
||||
Coffee: LucideIcons.Coffee,
|
||||
Pizza: LucideIcons.Pizza,
|
||||
Apple: LucideIcons.Apple,
|
||||
Cake: LucideIcons.Cake,
|
||||
Wine: LucideIcons.Wine,
|
||||
Beer: LucideIcons.Beer,
|
||||
Utensils: LucideIcons.Utensils,
|
||||
Bed: LucideIcons.Bed,
|
||||
Bath: LucideIcons.Bath,
|
||||
Sofa: LucideIcons.Sofa,
|
||||
Lamp: LucideIcons.Lamp,
|
||||
Tv: LucideIcons.Tv,
|
||||
Radio: LucideIcons.Radio,
|
||||
Headphones: LucideIcons.Headphones,
|
||||
Speaker: LucideIcons.Speaker,
|
||||
Gamepad: LucideIcons.Gamepad2,
|
||||
Puzzle: LucideIcons.Puzzle,
|
||||
Dice: LucideIcons.Dice1,
|
||||
Gift: LucideIcons.Gift,
|
||||
PartyPopper: LucideIcons.PartyPopper,
|
||||
Balloon: LucideIcons.Sparkles,
|
||||
Ribbon: LucideIcons.Ribbon,
|
||||
Crown: LucideIcons.Crown,
|
||||
Gem: LucideIcons.Gem,
|
||||
Diamond: LucideIcons.Diamond,
|
||||
Palette: LucideIcons.Palette,
|
||||
Brush: LucideIcons.Brush,
|
||||
Pen: LucideIcons.Pen,
|
||||
Pencil: LucideIcons.Pencil,
|
||||
Highlighter: LucideIcons.Highlighter,
|
||||
Eraser: LucideIcons.Eraser,
|
||||
Ruler: LucideIcons.Ruler,
|
||||
Scissors: LucideIcons.Scissors,
|
||||
Sticker: LucideIcons.Sticker,
|
||||
Stamp: LucideIcons.Stamp,
|
||||
Layers: LucideIcons.Layers,
|
||||
Layout: LucideIcons.Layout,
|
||||
Grid: LucideIcons.Grid3x3,
|
||||
List: LucideIcons.List,
|
||||
Table: LucideIcons.Table,
|
||||
Columns: LucideIcons.Columns3,
|
||||
Rows: LucideIcons.Rows3,
|
||||
SplitSquare: LucideIcons.SplitSquareHorizontal,
|
||||
Sidebar: LucideIcons.PanelLeft,
|
||||
PanelLeft: LucideIcons.PanelLeft,
|
||||
PanelRight: LucideIcons.PanelRight,
|
||||
PanelTop: LucideIcons.PanelTop,
|
||||
PanelBottom: LucideIcons.PanelBottom,
|
||||
Maximize: LucideIcons.Maximize,
|
||||
Minimize: LucideIcons.Minimize,
|
||||
Fullscreen: LucideIcons.Maximize2,
|
||||
ZoomIn: LucideIcons.ZoomIn,
|
||||
ZoomOut: LucideIcons.ZoomOut,
|
||||
Move: LucideIcons.Move,
|
||||
Grab: LucideIcons.Grab,
|
||||
Hand: LucideIcons.Hand,
|
||||
Pointer: LucideIcons.Pointer,
|
||||
MousePointer: LucideIcons.MousePointer,
|
||||
Crosshair: LucideIcons.Crosshair,
|
||||
Code: LucideIcons.Code,
|
||||
Terminal: LucideIcons.Terminal,
|
||||
Command: LucideIcons.Command,
|
||||
Keyboard: LucideIcons.Keyboard,
|
||||
Type: LucideIcons.Type,
|
||||
Hash: LucideIcons.Hash,
|
||||
AtSign: LucideIcons.AtSign,
|
||||
Asterisk: LucideIcons.Asterisk,
|
||||
Percent: LucideIcons.Percent,
|
||||
Binary: LucideIcons.Binary,
|
||||
Braces: LucideIcons.Braces,
|
||||
Brackets: LucideIcons.Brackets,
|
||||
Function: LucideIcons.FunctionSquare,
|
||||
Variable: LucideIcons.Variable,
|
||||
Regex: LucideIcons.Regex,
|
||||
Bug: LucideIcons.Bug,
|
||||
TestTube: LucideIcons.TestTube,
|
||||
Flask: LucideIcons.FlaskConical,
|
||||
Beaker: LucideIcons.Beaker,
|
||||
Microscope: LucideIcons.Microscope,
|
||||
Dna: LucideIcons.Dna,
|
||||
Atom: LucideIcons.Atom,
|
||||
Orbit: LucideIcons.Orbit,
|
||||
Rocket: LucideIcons.Rocket,
|
||||
Satellite: LucideIcons.Satellite,
|
||||
Telescope: LucideIcons.Telescope,
|
||||
Binoculars: LucideIcons.Binoculars,
|
||||
Glasses: LucideIcons.Glasses,
|
||||
Scan: LucideIcons.Scan,
|
||||
QrCode: LucideIcons.QrCode,
|
||||
Barcode: LucideIcons.Barcode,
|
||||
Fingerprint: LucideIcons.Fingerprint,
|
||||
ScanFace: LucideIcons.ScanFace,
|
||||
Bot: LucideIcons.Bot,
|
||||
Brain: LucideIcons.Brain,
|
||||
CircuitBoard: LucideIcons.CircuitBoard,
|
||||
Chip: LucideIcons.Cpu,
|
||||
PlugZap: LucideIcons.PlugZap,
|
||||
Power: LucideIcons.Power,
|
||||
PowerOff: LucideIcons.PowerOff,
|
||||
ToggleLeft: LucideIcons.ToggleLeft,
|
||||
ToggleRight: LucideIcons.ToggleRight,
|
||||
SlidersHorizontal: LucideIcons.SlidersHorizontal,
|
||||
SlidersVertical: LucideIcons.SlidersVertical,
|
||||
Gauge: LucideIcons.Gauge,
|
||||
Speedometer: LucideIcons.Gauge,
|
||||
Timer: LucideIcons.Timer,
|
||||
Hourglass: LucideIcons.Hourglass,
|
||||
History: LucideIcons.History,
|
||||
Undo: LucideIcons.Undo,
|
||||
Redo: LucideIcons.Redo,
|
||||
RotateCcw: LucideIcons.RotateCcw,
|
||||
RotateCw: LucideIcons.RotateCw,
|
||||
FlipHorizontal: LucideIcons.FlipHorizontal,
|
||||
FlipVertical: LucideIcons.FlipVertical,
|
||||
ArrowUp: LucideIcons.ArrowUp,
|
||||
ArrowDown: LucideIcons.ArrowDown,
|
||||
ArrowLeft: LucideIcons.ArrowLeft,
|
||||
ArrowRight: LucideIcons.ArrowRight,
|
||||
ArrowUpRight: LucideIcons.ArrowUpRight,
|
||||
ArrowUpLeft: LucideIcons.ArrowUpLeft,
|
||||
ArrowDownRight: LucideIcons.ArrowDownRight,
|
||||
ArrowDownLeft: LucideIcons.ArrowDownLeft,
|
||||
MoveUp: LucideIcons.MoveUp,
|
||||
MoveDown: LucideIcons.MoveDown,
|
||||
MoveLeft: LucideIcons.MoveLeft,
|
||||
MoveRight: LucideIcons.MoveRight,
|
||||
ChevronsUp: LucideIcons.ChevronsUp,
|
||||
ChevronsDown: LucideIcons.ChevronsDown,
|
||||
ChevronsLeft: LucideIcons.ChevronsLeft,
|
||||
ChevronsRight: LucideIcons.ChevronsRight,
|
||||
Expand: LucideIcons.Expand,
|
||||
Shrink: LucideIcons.Shrink,
|
||||
Focus: LucideIcons.Focus,
|
||||
Circle: LucideIcons.Circle,
|
||||
Square: LucideIcons.Square,
|
||||
Triangle: LucideIcons.Triangle,
|
||||
Pentagon: LucideIcons.Pentagon,
|
||||
Hexagon: LucideIcons.Hexagon,
|
||||
Octagon: LucideIcons.Octagon,
|
||||
// 默认图标
|
||||
Default: LucideIcons.Circle,
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取图标组件
|
||||
* @param iconName 图标名称
|
||||
* @returns 图标组件,如果不存在则返回默认图标
|
||||
*/
|
||||
export function getIcon(iconName: string | null | undefined): LucideIcon {
|
||||
if (!iconName) {
|
||||
return iconMap.Default;
|
||||
}
|
||||
return iconMap[iconName] || iconMap.Default;
|
||||
}
|
||||
50
apps/web/src/services/menu.service.ts
Normal file
50
apps/web/src/services/menu.service.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type {
|
||||
CreateMenuDto,
|
||||
MenuResponse,
|
||||
MenuTreeNode,
|
||||
PaginatedResponse,
|
||||
UpdateMenuDto,
|
||||
} from '@seclusion/shared';
|
||||
|
||||
import { API_ENDPOINTS } from '@/config/constants';
|
||||
import { http } from '@/lib/http';
|
||||
|
||||
export interface GetMenusParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export const menuService = {
|
||||
// 获取菜单列表
|
||||
getMenus: (params: GetMenusParams = {}): Promise<PaginatedResponse<MenuResponse>> => {
|
||||
return http.get<PaginatedResponse<MenuResponse>>(API_ENDPOINTS.MENUS, {
|
||||
params,
|
||||
});
|
||||
},
|
||||
|
||||
// 获取菜单树
|
||||
getMenuTree: (): Promise<MenuTreeNode[]> => {
|
||||
return http.get<MenuTreeNode[]>(`${API_ENDPOINTS.MENUS}/tree`);
|
||||
},
|
||||
|
||||
// 获取单个菜单
|
||||
getMenu: (id: string): Promise<MenuResponse> => {
|
||||
return http.get<MenuResponse>(`${API_ENDPOINTS.MENUS}/${id}`);
|
||||
},
|
||||
|
||||
// 创建菜单
|
||||
createMenu: (data: CreateMenuDto): Promise<MenuResponse> => {
|
||||
return http.post<MenuResponse>(API_ENDPOINTS.MENUS, data);
|
||||
},
|
||||
|
||||
// 更新菜单
|
||||
updateMenu: (id: string, data: UpdateMenuDto): Promise<MenuResponse> => {
|
||||
return http.patch<MenuResponse>(`${API_ENDPOINTS.MENUS}/${id}`, data);
|
||||
},
|
||||
|
||||
// 删除菜单
|
||||
deleteMenu: (id: string): Promise<void> => {
|
||||
return http.delete<void>(`${API_ENDPOINTS.MENUS}/${id}`);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user