import { ChevronDown, ChevronLeft, ChevronRight, LogOut, Menu, RefreshCw, Shield, X, } from "lucide-react"; import { createContext, useContext, useEffect, useMemo, useState } from "react"; import { Link, Outlet, useLocation, useParams } from "react-router-dom"; import { PermissionBoundary, useAppMenus, useRuntimeAuth, useRuntimeBootstrap, type PermissionBoundaryFallbackState, } from "openxiangda/runtime/react"; import { buildStarterAdminNavigation, filterNavigationByMenuCodes, type StarterNavigationGroup, } from "@/app/navigation"; import { starterBrand } from "@/app/starter-content"; import { PrimaryButton, SecondaryButton, StatePage, cn, } from "@/shared/ui"; type PlatformMenuLike = { children?: PlatformMenuLike[]; code?: string | null; formUuid?: string | null; isHidden?: boolean | null; resourceCode?: string | null; routeCode?: string | null; type?: string | null; }; type AdminPageMeta = { breadcrumbs?: string[]; title?: string; }; type NavigationState = { activePath?: string; breadcrumbs: string[]; title: string; }; const AdminPageMetaContext = createContext<((meta: AdminPageMeta | null) => void) | null>(null); const noopSetAdminPageMeta = () => undefined; export function useAdminPageMetaController() { return useContext(AdminPageMetaContext) ?? noopSetAdminPageMeta; } export function AdminShell() { const { appType = "" } = useParams(); const location = useLocation(); const bootstrap = useRuntimeBootstrap(); const menus = useAppMenus(); const auth = useRuntimeAuth(); const [collapsedGroups, setCollapsedGroups] = useState>({}); const [mobileOpen, setMobileOpen] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [userMenuOpen, setUserMenuOpen] = useState(false); const [loggingOut, setLoggingOut] = useState(false); const [pageMeta, setPageMeta] = useState(null); const userName = String( bootstrap.data?.user?.name || bootstrap.data?.user?.nickName || bootstrap.data?.user?.id || "当前用户", ); const userAvatar = String( bootstrap.data?.user?.avatar || bootstrap.data?.user?.avatarUrl || bootstrap.data?.user?.photoUrl || "", ); const userRole = String( bootstrap.data?.user?.roleName || bootstrap.data?.user?.title || bootstrap.data?.user?.position || bootstrap.data?.user?.departmentName || "平台用户", ); const menuCodes = useMemo(() => collectMenuCodes(menus.data), [menus.data]); const groups = useMemo( () => filterNavigationByMenuCodes( buildStarterAdminNavigation({ appType, }), menuCodes, ), [appType, menuCodes], ); const navigationState = useMemo( () => resolveNavigationState(groups, location.pathname, location.search, appType), [appType, groups, location.pathname, location.search], ); const headerTitle = pageMeta?.title || navigationState.title || starterBrand.fallbackName; const breadcrumbs = pageMeta?.breadcrumbs && pageMeta.breadcrumbs.length > 0 ? pageMeta.breadcrumbs : navigationState.breadcrumbs; const handleLogout = async () => { setLoggingOut(true); await auth.logoutAndRedirect({ replace: true }); }; const sidebar = (compact = false) => ( ); return ( } loadingFallback={} menuCode="admin_dashboard" path={`/view/${appType}/admin`} routeCode="admin.dashboard" >
{mobileOpen ? (
{sidebar(false)}
) : null}
{breadcrumbs.join(" / ")}
{headerTitle}
{userMenuOpen ? (
{userName}
{userRole}
) : null}
); } function UserAvatar({ name, size = "md", src, }: { name: string; size?: "md" | "lg"; src?: string; }) { const className = cn( "shrink-0 overflow-hidden rounded-full bg-[linear-gradient(135deg,#2563eb_0%,#38bdf8_100%)] text-white shadow-sm ring-2 ring-white", size === "lg" ? "h-11 w-11" : "h-9 w-9", ); if (src) { return ( ); } return ( {name.slice(0, 1).toUpperCase()} ); } function OpenXiangdaMark() { return ( ); } function AdminPermissionState({ state }: { state: PermissionBoundaryFallbackState }) { const auth = useRuntimeAuth(); useEffect(() => { if (state.errorType === "unauthenticated") { void auth.redirectToLogin({ replace: true }); } }, [auth, state.errorType]); if (state.errorType === "unauthenticated") { return ( void auth.redirectToLogin({ replace: true })}> 立即登录 } description="当前浏览器没有有效登录态,正在为你跳转到登录页。" fullScreen icon={} status="401" title="需要登录" /> ); } return ( window.history.back()}>返回上一页 void auth.redirectToLogin({ replace: true })}> 切换账号 } description={state.message || "请切换到有权限的账号,或联系管理员开通后台访问权限。"} fullScreen icon={} status="403" title="无权访问管理后台" /> ); } function AdminLoadingState() { return ( } status="LOADING" title="正在进入应用" /> ); } function collectMenuCodes(items: PlatformMenuLike[]): Set { const codes = new Set(); const visit = (item: PlatformMenuLike) => { if (item.isHidden) return; for (const value of [item.code, item.resourceCode, item.routeCode]) { if (value) codes.add(String(value)); } for (const child of item.children || []) visit(child); }; for (const item of items) visit(item); return codes; } function isMenuActive(pathname: string, search: string, target: string) { const normalize = (value: string) => value .replace(/[?#].*$/, "") .replace(/\/+$/, "") .replace(/\/{2,}/g, "/"); if (target.includes("?")) return `${pathname}${search}` === target; return normalize(pathname) === normalize(target); } function resolveNavigationState( groups: StarterNavigationGroup[], pathname: string, search: string, appType: string, ): NavigationState { const fallback: NavigationState = { breadcrumbs: ["首页"], title: starterBrand.fallbackName, }; const entries = groups.flatMap(group => group.items.map(item => ({ group, item }))); const exact = entries.find(({ item }) => isMenuActive(pathname, search, item.path)); if (exact) { return { activePath: exact.item.path, breadcrumbs: ["首页", exact.group.title, exact.item.name], title: exact.item.name, }; } const route = parseAdminRoute(pathname, appType); if (!route) return fallback; const related = findRelatedNavigationEntry(entries, route); if (related) { const title = route.kind === "form-detail" || route.kind === "process-detail" ? `${related.item.name}详情` : related.item.name; return { activePath: related.item.path, breadcrumbs: ["首页", related.group.title, related.item.name], title, }; } if (route.kind === "data") { return { breadcrumbs: ["首页", "数据管理"], title: "业务数据" }; } if (route.kind === "form-new") { return { breadcrumbs: ["首页", "业务办理"], title: "发起申请" }; } if (route.kind === "form-detail") { return { breadcrumbs: ["首页", "业务记录"], title: "记录详情" }; } return { breadcrumbs: ["首页", "流程办理"], title: "流程详情" }; } function parseAdminRoute(pathname: string, appType: string) { const escapedAppType = appType.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const prefix = new RegExp(`^/view/${escapedAppType}/admin/`); if (!prefix.test(pathname)) return null; const rest = pathname.replace(prefix, ""); const [kind, formUuid, tail] = rest.split("/"); if (kind === "data" && formUuid) return { formUuid, kind: "data" as const }; if (kind === "forms" && formUuid && tail === "new") return { formUuid, kind: "form-new" as const }; if (kind === "forms" && formUuid) return { formUuid, kind: "form-detail" as const }; if (kind === "process" && formUuid) return { formUuid, kind: "process-detail" as const }; return null; } function findRelatedNavigationEntry( entries: Array<{ group: StarterNavigationGroup; item: StarterNavigationGroup["items"][number] }>, route: NonNullable>, ) { const formPathNeedle = `/forms/${route.formUuid}/new`; const dataPathNeedle = `/data/${route.formUuid}`; if (route.kind === "data") return entries.find(({ item }) => item.path.includes(dataPathNeedle)); if (route.kind === "form-new") return entries.find(({ item }) => item.path.includes(formPathNeedle)); return ( entries.find(({ item }) => item.path.includes(dataPathNeedle)) || entries.find(({ item }) => item.path.includes(formPathNeedle)) ); }