import { ArrowDownOutlined, ArrowUpOutlined, CheckCircleOutlined, FlagOutlined, LoginOutlined, LogoutOutlined, PlusCircleOutlined, UserOutlined, WalletOutlined, } from "@ant-design/icons"; import { useEffect, useMemo, useRef, useState } from "react"; import type { ReactNode } from "react"; import type { ServerConnectionHealth } from "../api/serverConnection"; import { canManageCommunityCases, canReviewCommunity } from "../features/community-review/communityPermissions"; import type { WebNavItem, WebNotification, WebUsageSummary, WebUserSession, WebViewKey } from "../types"; import NotificationCenter from "./NotificationCenter"; import { RechargeModal } from "./RechargeModal/RechargeModal"; import { AnimatedPanel } from "./AnimatedPanel"; interface AppShellProps { activeView: WebViewKey; navItems: WebNavItem[]; session: WebUserSession | null; usage: WebUsageSummary; notifications: WebNotification[]; backendHealth: ServerConnectionHealth; workspaceExpanded: boolean; onSelectView: (view: WebViewKey) => void; onLogout: () => void; onOpenLogin: () => void; onMarkNotificationRead?: (id: string, isRead?: boolean) => void; onMarkAllNotificationsRead?: () => void; children: ReactNode; } const BRAND_LOGO_URL = "https://stringtest.oss-cn-hangzhou.aliyuncs.com/logo.png"; function formatBalance(cents: number): string { const value = Math.max(0, cents) / 100; return `${value.toFixed(2)} 积分`; } function AppShell({ activeView, navItems, session, usage, notifications, backendHealth, workspaceExpanded, onSelectView, onLogout, onOpenLogin, onMarkNotificationRead, onMarkAllNotificationsRead, children, }: AppShellProps) { const activePackage = session?.user.activePackages?.[0]; const profileRef = useRef(null); const submenuHideTimerRef = useRef(null); const [profileOpen, setProfileOpen] = useState(false); const [rechargeOpen, setRechargeOpen] = useState(false); const [openSubmenuKey, setOpenSubmenuKey] = useState(null); const prevActiveViewRef = useRef(activeView); const [navJustActivated, setNavJustActivated] = useState(null); const isAuthView = activeView === "login"; const isImmersiveView = activeView === "agent" || activeView === "avatarConsole"; const showFloatingNav = !isAuthView && !isImmersiveView && activeView !== "home"; const toolSurfaceViews = [ "workbench", "canvas", "more", "scriptTokens", "tokenUsage", "ecommerceTemplates", "sizeTemplate", "imageWorkbench", "resolutionUpscale", "digitalHuman", "avatarConsole", "characterMix", ] as WebViewKey[]; const showPageScrollActions = showFloatingNav && !toolSurfaceViews.includes(activeView); const visibleNavItems = useMemo( () => { const orderedKeys: WebViewKey[] = [ "workbench", "ecommerce", "sizeTemplate", "canvas", "scriptTokens", "tokenUsage", "community", "assets", "more", ]; return orderedKeys .map((key) => navItems.find((item) => item.key === key)) .filter((item): item is WebNavItem => Boolean(item)); }, [navItems], ); useEffect(() => { if (activeView !== prevActiveViewRef.current) { setNavJustActivated(activeView); prevActiveViewRef.current = activeView; const timer = window.setTimeout(() => setNavJustActivated(null), 320); return () => window.clearTimeout(timer); } }, [activeView]); useEffect(() => { if (typeof document === "undefined") { return; } document.documentElement.dataset.theme = "dark"; document.documentElement.dataset.uiTheme = "dark-green"; document.documentElement.style.colorScheme = "dark"; const metaThemeColor = document.querySelector("meta[name='theme-color']"); if (metaThemeColor) { metaThemeColor.content = "#0d0d0f"; } }, []); useEffect(() => { if (!profileOpen) return; const handlePointerDown = (event: PointerEvent) => { if (!profileRef.current?.contains(event.target as Node)) { setProfileOpen(false); } }; document.addEventListener("pointerdown", handlePointerDown); return () => document.removeEventListener("pointerdown", handlePointerDown); }, [profileOpen]); useEffect(() => { if (!session) { setProfileOpen(false); } }, [session]); useEffect(() => { return () => { if (submenuHideTimerRef.current) { window.clearTimeout(submenuHideTimerRef.current); } }; }, []); const showSubmenu = (key: WebViewKey) => { if (submenuHideTimerRef.current) { window.clearTimeout(submenuHideTimerRef.current); submenuHideTimerRef.current = null; } setOpenSubmenuKey(key); }; const scheduleHideSubmenu = () => { if (submenuHideTimerRef.current) { window.clearTimeout(submenuHideTimerRef.current); } submenuHideTimerRef.current = window.setTimeout(() => { setOpenSubmenuKey(null); submenuHideTimerRef.current = null; }, 1500); }; const scrollActivePage = (direction: "top" | "bottom") => { if (typeof document === "undefined") return; const targets = [ ...Array.from(document.querySelectorAll(".web-shell__page")), ...Array.from( document.querySelectorAll( ".workbench-landing-page, .ecommerce-landing-page, .workspace-page-shell__content, .community-page", ), ), document.scrollingElement as HTMLElement | null, ].filter((target): target is HTMLElement => Boolean(target)); targets.forEach((target) => { const top = direction === "top" ? 0 : target.scrollHeight; target.scrollTo({ top, behavior: "smooth" }); }); }; const displayName = session?.user.displayName || session?.user.username || "预览创作者"; const avatarLabel = displayName.trim().slice(0, 1).toUpperCase() || "创"; const avatarUrl = session?.user.avatarUrl || null; const isEnterpriseAccount = Boolean(session?.user.enterpriseId || session?.user.accountType === "enterprise"); const displayedBalanceCents = session && isEnterpriseAccount ? (usage.enterpriseBalanceCents ?? session.user.enterpriseBalanceCents ?? usage.balanceCents) : usage.balanceCents; const displayedBalanceLabel = session ? formatBalance(displayedBalanceCents) : "0 积分"; const isPreviewSession = session?.source === "mock-fallback"; const showCommunityReview = canReviewCommunity(session); const showCommunityCaseAdd = canManageCommunityCases(session); return (
{showFloatingNav ? ( ) : null} {showPageScrollActions ? (
) : null}
{!isImmersiveView ? (
{session && ( onSelectView(view)} onMarkRead={onMarkNotificationRead} onMarkAllRead={onMarkAllNotificationsRead} /> )}
{avatarUrl ? {displayName} : avatarLabel}
{displayName} {session ? session.user.role || "已登录" : "预览模式"}
UID
{session?.user.id || "preview"}
{isEnterpriseAccount ? "企业积分" : "积分"}
{displayedBalanceLabel}
图片
{usage.imageUsed}
视频
{usage.videoUsed}
{import.meta.env.VITE_KEY_SERVER_URL || "使用预览数据"}
{showCommunityReview ? ( <> ) : null} {showCommunityCaseAdd ? ( <> ) : null}
) : null}
{children}
setRechargeOpen(false)} currentBalance={displayedBalanceCents} />
); } export default AppShell;