feat(web): 实现菜单管理功能

- 添加菜单列表页面和管理组件
- 实现菜单 CRUD 操作和树形结构展示
- 添加菜单表单(支持创建/编辑)
- 实现图标选择器和动态图标映射

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
charilezhou
2026-01-17 14:06:15 +08:00
parent ed228860f6
commit 3cfc6bf15c
7 changed files with 1234 additions and 0 deletions

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

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

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

View File

@@ -0,0 +1,2 @@
export { MenusTable } from './MenusTable';
export { MenuEditDialog } from './MenuEditDialog';

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

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