import React, { useEffect, useMemo, useRef, useState } from "react"; import { Edit, FileBox, FileText, HelpCircle, Layers, LayoutDashboard, MapPin, MoreHorizontal, Package, Plus, Settings, Tag, Trash2, Type, Users, } from "lucide-react"; import { toast } from "sonner"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Label } from "../ui/label"; import { Switch } from "../ui/switch"; import { Textarea } from "../ui/textarea"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../ui/dialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../ui/table"; import { createSystemMenu, deleteSystemMenu, getDirectoryMenusForParentSelect, getSystemMenus, updateSystemMenu, } from "../../services/systemMenuService"; import type { SystemMenuDto, SystemMenuUpsertInput } from "../../types/systemMenu"; type IconKey = | "Settings" | "LayoutDashboard" | "Tag" | "MapPin" | "Users" | "Package" | "FileText" | "HelpCircle" | "Layers" | "Type" | "FileBox"; const ICONS: Record> = { Settings, LayoutDashboard, Tag, MapPin, Users, Package, FileText, HelpCircle, Layers, Type, FileBox, }; function toDisplay(v: string | null | undefined): string { const s = (v ?? "").trim(); return s ? s : "N/A"; } function toIntOrNull(v: string): number | null { const s = v.trim(); if (!s) return null; const n = Number.parseInt(s, 10); return Number.isFinite(n) ? n : null; } export function SystemMenuView() { const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [refreshSeq, setRefreshSeq] = useState(0); const [actionsOpenForId, setActionsOpenForId] = useState(null); const [keyword, setKeyword] = useState(""); const keywordTimerRef = useRef(null); const [debouncedKeyword, setDebouncedKeyword] = useState(""); const [isCreateOpen, setIsCreateOpen] = useState(false); const [isEditOpen, setIsEditOpen] = useState(false); const [isDeleteOpen, setIsDeleteOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleting, setDeleting] = useState(null); const abortRef = useRef(null); useEffect(() => { if (keywordTimerRef.current) window.clearTimeout(keywordTimerRef.current); keywordTimerRef.current = window.setTimeout(() => setDebouncedKeyword(keyword.trim()), 300); return () => { if (keywordTimerRef.current) window.clearTimeout(keywordTimerRef.current); }; }, [keyword]); useEffect(() => { const run = async () => { abortRef.current?.abort(); const ac = new AbortController(); abortRef.current = ac; setLoading(true); try { const res = await getSystemMenus( { // 这里不分页展示:一次性拉大页数据 skipCount: 1, maxResultCount: 5000, keyword: debouncedKeyword || undefined, }, ac.signal, ); setItems(res.items ?? []); } catch (e: any) { if (e?.name === "AbortError") return; toast.error("Failed to load system menus.", { description: e?.message ? String(e.message) : "Please try again.", }); setItems([]); } finally { setLoading(false); } }; run(); return () => abortRef.current?.abort(); }, [debouncedKeyword, refreshSeq]); const refreshList = () => setRefreshSeq((x) => x + 1); const openEdit = (m: SystemMenuDto) => { setActionsOpenForId(null); setEditing(m); setIsEditOpen(true); }; const openDelete = (m: SystemMenuDto) => { setActionsOpenForId(null); setDeleting(m); setIsDeleteOpen(true); }; return (
setKeyword(e.target.value)} style={{ height: 40, boxSizing: "border-box" }} className="border border-gray-300 rounded-md w-40 shrink-0 bg-white placeholder:text-gray-500" />
{/* flex-col + min-h-0:表格区域滚动,底部分页栏始终可见(避免被 overflow-hidden 裁掉) */}
Menu Name Route URL Router Name Type Order Visible Enabled Actions {items.length === 0 ? ( {loading ? "Loading..." : "No data"} ) : ( items.map((m) => ( {toDisplay(m.menuName)} {toDisplay(m.routeUrl)} {toDisplay(m.routerName)} {m.menuType ?? "N/A"} {m.orderNum ?? "N/A"} {m.isShow ? "Yes" : "No"} {m.state ? "Yes" : "No"} setActionsOpenForId(open ? m.id : null)} >
)) )}
); } function SystemMenuDialog({ mode, open, menu, onOpenChange, onSaved, }: { mode: "create" | "edit"; open: boolean; menu: SystemMenuDto | null; onOpenChange: (open: boolean) => void; onSaved: () => void; }) { const isEdit = mode === "edit"; const [submitting, setSubmitting] = useState(false); // 按图二字段(英文展示) const [menuName, setMenuName] = useState(""); const [routerName, setRouterName] = useState(""); const [routeUrl, setRouteUrl] = useState(""); const [menuType, setMenuType] = useState<"directory" | "menu">("menu"); const [permissionCode, setPermissionCode] = useState(""); const [parentId, setParentId] = useState(""); const [parentDirectories, setParentDirectories] = useState([]); const [parentDirsLoading, setParentDirsLoading] = useState(false); const [menuIcon, setMenuIcon] = useState(""); const [orderNum, setOrderNum] = useState(""); const [link, setLink] = useState(""); const [component, setComponent] = useState(""); const [query, setQuery] = useState(""); const [remark, setRemark] = useState(""); const [isCache, setIsCache] = useState(false); const [isShow, setIsShow] = useState(true); const [state, setState] = useState(true); useEffect(() => { if (!open) return; setSubmitting(false); setMenuName(menu?.menuName ?? ""); setRouterName(menu?.routerName ?? ""); setRouteUrl(menu?.routeUrl ?? ""); // menuType: 目录/菜单(默认菜单) // 这里按常见约定:0=Directory, 1=Menu;若后端枚举不同,再按 Swagger 调整 setMenuType(menu?.menuType === 0 ? "directory" : "menu"); setPermissionCode(menu?.permissionCode ?? ""); const rawPid = String(menu?.parentId ?? "").trim(); setParentId( !rawPid || rawPid === "00000000-0000-0000-0000-000000000000" ? "" : rawPid, ); setMenuIcon((menu?.menuIcon as IconKey | null) ?? ""); setOrderNum(menu?.orderNum === null || menu?.orderNum === undefined ? "" : String(menu.orderNum)); setLink(menu?.link ?? ""); setComponent(menu?.component ?? ""); setQuery(menu?.query ?? ""); setRemark(menu?.remark ?? ""); setIsCache(!!menu?.isCache); setIsShow(menu?.isShow ?? true); setState(menu?.state ?? true); }, [open, menu]); const PARENT_ROOT = "__parent_root__"; useEffect(() => { if (!open) return; let cancelled = false; setParentDirsLoading(true); getDirectoryMenusForParentSelect() .then((list) => { if (!cancelled) setParentDirectories(list); }) .catch(() => { if (!cancelled) setParentDirectories([]); }) .finally(() => { if (!cancelled) setParentDirsLoading(false); }); return () => { cancelled = true; }; }, [open]); const isRootParentId = (id: string) => !id.trim() || id === "00000000-0000-0000-0000-000000000000"; const parentSelectOptions = useMemo(() => { const dirs = parentDirectories.filter((d) => d.id && d.id !== menu?.id); const pid = (parentId || "").trim(); if (pid && !isRootParentId(pid) && !dirs.some((d) => d.id === pid)) { return [ ...dirs, { id: pid, menuName: `(Current parent) ${pid}` } as SystemMenuDto, ]; } return dirs; }, [parentDirectories, parentId, menu?.id]); const parentSelectValue = isRootParentId(parentId) ? PARENT_ROOT : parentId; const canSubmit = useMemo(() => { return Boolean(menuName.trim() && routeUrl.trim() && orderNum.trim()); }, [menuName, routeUrl, orderNum]); const submit = async () => { if (!canSubmit) { toast.error("Please fill in required fields.", { description: "Menu Name, Route URL, and Order are required.", }); return; } setSubmitting(true); try { const payload: SystemMenuUpsertInput = { menuName: menuName.trim(), routerName: routerName.trim() ? routerName.trim() : null, routeUrl: routeUrl.trim(), // 0=Directory, 1=Menu menuType: menuType === "directory" ? 0 : 1, permissionCode: permissionCode.trim() ? permissionCode.trim() : null, parentId: isRootParentId(parentId) ? null : parentId.trim(), menuIcon: menuIcon ? menuIcon : null, orderNum: toIntOrNull(orderNum), link: link.trim() ? link.trim() : null, component: component.trim() ? component.trim() : null, query: query.trim() ? query.trim() : null, remark: remark.trim() ? remark.trim() : null, isCache, isShow, state, }; if (isEdit) { if (!menu?.id) throw new Error("Missing id."); await updateSystemMenu(menu.id, payload); toast.success("Menu updated.", { description: "Changes have been saved successfully." }); } else { await createSystemMenu(payload); toast.success("Menu created.", { description: "A new menu has been created successfully." }); } onOpenChange(false); onSaved(); } catch (e: any) { toast.error(isEdit ? "Failed to update menu." : "Failed to create menu.", { description: e?.message ? String(e.message) : "Please try again.", }); } finally { setSubmitting(false); } }; return ( {isEdit ? "Edit System Menu" : "New System Menu"} {isEdit ? "Update system menu fields and save changes." : "Fill out the form to create a new system menu."}
setMenuName(e.target.value)} placeholder="e.g. Location Manager" />
setRouteUrl(e.target.value)} placeholder="e.g. /location" />
setRouterName(e.target.value)} placeholder="e.g. location" />
setPermissionCode(e.target.value)} placeholder="e.g. sys:menu" />
setOrderNum(e.target.value)} placeholder="e.g. 10" />
setLink(e.target.value)} placeholder="Optional" />
setComponent(e.target.value)} placeholder="Optional" />
setQuery(e.target.value)} placeholder="Optional" />