From 3cfc6bf15c5e524419e2864c84cd1890aa41c217 Mon Sep 17 00:00:00 2001 From: charilezhou Date: Sat, 17 Jan 2026 14:06:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=AE=9E=E7=8E=B0=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加菜单列表页面和管理组件 - 实现菜单 CRUD 操作和树形结构展示 - 添加菜单表单(支持创建/编辑) - 实现图标选择器和动态图标映射 Co-Authored-By: Claude Opus 4.5 --- apps/web/src/app/(dashboard)/menus/page.tsx | 33 ++ .../src/components/menus/MenuEditDialog.tsx | 398 ++++++++++++++++++ apps/web/src/components/menus/MenusTable.tsx | 375 +++++++++++++++++ apps/web/src/components/menus/index.ts | 2 + apps/web/src/hooks/useMenus.ts | 89 ++++ apps/web/src/lib/icons.ts | 287 +++++++++++++ apps/web/src/services/menu.service.ts | 50 +++ 7 files changed, 1234 insertions(+) create mode 100644 apps/web/src/app/(dashboard)/menus/page.tsx create mode 100644 apps/web/src/components/menus/MenuEditDialog.tsx create mode 100644 apps/web/src/components/menus/MenusTable.tsx create mode 100644 apps/web/src/components/menus/index.ts create mode 100644 apps/web/src/hooks/useMenus.ts create mode 100644 apps/web/src/lib/icons.ts create mode 100644 apps/web/src/services/menu.service.ts diff --git a/apps/web/src/app/(dashboard)/menus/page.tsx b/apps/web/src/app/(dashboard)/menus/page.tsx new file mode 100644 index 0000000..8437e4c --- /dev/null +++ b/apps/web/src/app/(dashboard)/menus/page.tsx @@ -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 ( +
+
+
+

菜单管理

+

管理系统菜单和路由配置。

+
+
+ + + + 菜单列表 + 查看和管理系统菜单,支持多级菜单结构。 + + + + + +
+ ); +} diff --git a/apps/web/src/components/menus/MenuEditDialog.tsx b/apps/web/src/components/menus/MenuEditDialog.tsx new file mode 100644 index 0000000..28fce4f --- /dev/null +++ b/apps/web/src/components/menus/MenuEditDialog.tsx @@ -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( + + {prefix}{menu.name} + + ); + + 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({ + 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 ( + + + + {isEditing ? '编辑菜单' : '新建菜单'} + + {isEditing ? '修改菜单配置。' : '创建新的菜单项。'} + + + +
+ + ( + + 父级菜单 + + + + )} + /> + +
+ ( + + 菜单编码 + + + + + + )} + /> + + ( + + 菜单名称 + + + + + + )} + /> +
+ +
+ ( + + 菜单类型 + + + + )} + /> + + ( + + 排序 + + field.onChange(parseInt(e.target.value) || 0)} + /> + + + + )} + /> +
+ + {menuType !== MenuType.BUTTON && ( +
+ ( + + 路由路径 + + + + + + )} + /> + + ( + + 图标 + + + + + 参考 Lucide Icons + + + + )} + /> +
+ )} + + ( + + 权限编码 + + + + + 关联的权限编码,格式为 资源:操作 + + + + )} + /> + +
+ ( + + + + + 隐藏菜单 + + )} + /> + + ( + + + + + 启用 + + )} + /> +
+ + + + + + + +
+
+ ); +} diff --git a/apps/web/src/components/menus/MenusTable.tsx b/apps/web/src/components/menus/MenusTable.tsx new file mode 100644 index 0000000..6fd31db --- /dev/null +++ b/apps/web/src/components/menus/MenusTable.tsx @@ -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 ; + case MenuType.MENU: + return ; + case MenuType.BUTTON: + return ; + default: + return ; + } +} + +// 获取菜单类型文本 +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 ( + + + + + + 操作 + + onEdit(menu)}> + + 编辑 + + {!menu.isStatic && ( + onDelete(menu.id)} + className="text-destructive" + > + + 删除 + + )} + + + ); +} + +export function MenusTable() { + // 分页状态 + const [pagination, setPagination] = useState({ + page: PAGINATION.DEFAULT_PAGE, + pageSize: PAGINATION.DEFAULT_PAGE_SIZE, + }); + + // 排序状态 + const [sorting, setSorting] = useState({}); + + // 搜索状态 + const [searchParams, setSearchParams] = useState({}); + + // 对话框状态 + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [menuToDelete, setMenuToDelete] = useState(null); + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [menuToEdit, setMenuToEdit] = useState(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 = { + fields: [ + { + type: 'text', + key: 'search', + label: '关键词', + placeholder: '搜索菜单编码或名称', + colSpan: 2, + }, + ], + columns: 4, + }; + + // 列定义 + const columns: ColumnDef[] = [ + { + accessorKey: 'name', + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {getMenuTypeIcon(row.original.type)} + {row.original.name} +
+ ), + }, + { + accessorKey: 'code', + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + {row.original.code} + ), + }, + { + accessorKey: 'type', + header: '类型', + cell: ({ row }) => ( + {getMenuTypeText(row.original.type)} + ), + }, + { + accessorKey: 'path', + header: '路由路径', + cell: ({ row }) => ( + + {row.original.path || '-'} + + ), + }, + { + accessorKey: 'permission', + header: '权限编码', + cell: ({ row }) => ( + + {row.original.permission || '-'} + + ), + }, + { + accessorKey: 'isStatic', + header: '来源', + cell: ({ row }) => ( + + {row.original.isStatic ? '静态' : '动态'} + + ), + }, + { + accessorKey: 'isEnabled', + header: '状态', + cell: ({ row }) => ( + + {row.original.isEnabled ? '启用' : '禁用'} + + ), + }, + { + accessorKey: 'sort', + header: ({ column }) => ( + + ), + }, + { + accessorKey: 'createdAt', + header: ({ column }) => ( + + ), + cell: ({ row }) => + formatDate(new Date(row.original.createdAt), 'YYYY-MM-DD HH:mm'), + }, + { + id: 'actions', + header: '操作', + cell: ({ row }) => ( + + ), + }, + ]; + + return ( +
+ {/* 工具栏 */} +
+
+ +
+ +
+ + {/* 数据表格 */} + + + {/* 删除确认弹窗 */} + + + + 确认删除 + + 确定要删除该菜单吗?删除后将无法恢复,子菜单也将一并删除。 + + + + 取消 + + 删除 + + + + + + {/* 编辑/新建菜单弹窗 */} + +
+ ); +} diff --git a/apps/web/src/components/menus/index.ts b/apps/web/src/components/menus/index.ts new file mode 100644 index 0000000..734e05f --- /dev/null +++ b/apps/web/src/components/menus/index.ts @@ -0,0 +1,2 @@ +export { MenusTable } from './MenusTable'; +export { MenuEditDialog } from './MenuEditDialog'; diff --git a/apps/web/src/hooks/useMenus.ts b/apps/web/src/hooks/useMenus.ts new file mode 100644 index 0000000..a2ccf40 --- /dev/null +++ b/apps/web/src/hooks/useMenus.ts @@ -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() }); + }, + }); +} diff --git a/apps/web/src/lib/icons.ts b/apps/web/src/lib/icons.ts new file mode 100644 index 0000000..cd211c3 --- /dev/null +++ b/apps/web/src/lib/icons.ts @@ -0,0 +1,287 @@ +import * as LucideIcons from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; + +/** + * 图标映射 + * 将字符串图标名称映射到 Lucide 图标组件 + */ +export const iconMap: Record = { + // 常用图标 + 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; +} diff --git a/apps/web/src/services/menu.service.ts b/apps/web/src/services/menu.service.ts new file mode 100644 index 0000000..6c4e5ce --- /dev/null +++ b/apps/web/src/services/menu.service.ts @@ -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> => { + return http.get>(API_ENDPOINTS.MENUS, { + params, + }); + }, + + // 获取菜单树 + getMenuTree: (): Promise => { + return http.get(`${API_ENDPOINTS.MENUS}/tree`); + }, + + // 获取单个菜单 + getMenu: (id: string): Promise => { + return http.get(`${API_ENDPOINTS.MENUS}/${id}`); + }, + + // 创建菜单 + createMenu: (data: CreateMenuDto): Promise => { + return http.post(API_ENDPOINTS.MENUS, data); + }, + + // 更新菜单 + updateMenu: (id: string, data: UpdateMenuDto): Promise => { + return http.patch(`${API_ENDPOINTS.MENUS}/${id}`, data); + }, + + // 删除菜单 + deleteMenu: (id: string): Promise => { + return http.delete(`${API_ENDPOINTS.MENUS}/${id}`); + }, +};