diff --git a/client/src/App.tsx b/client/src/App.tsx index ea872bb..bec56c6 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,15 +1,16 @@ +import { lazy } from "react"; import { Toaster } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { Route, Switch, Router } from "wouter"; import { useHashLocation } from "wouter/use-hash-location"; import ErrorBoundary from "./components/ErrorBoundary"; +import PageSuspense from "./components/PageSuspense"; import { ThemeProvider } from "./contexts/ThemeContext"; import { DeviceProvider } from "./contexts/DeviceContext"; import Home from "./pages/Home"; import NotFound from "./pages/NotFound"; import Dashboard from "./pages/Dashboard"; import AudioPage from "./pages/AudioPage"; -import EQPage from "./pages/EQPage"; import EffectsPage from "./pages/EffectsPage"; import BluetoothPage from "./pages/BluetoothPage"; import DspPage from "./pages/DspPage"; @@ -18,14 +19,28 @@ import SystemPage from "./pages/SystemPage"; import IOPage from "./pages/IOPage"; import VUPage from "./pages/VUPage"; import SelectPage from "./pages/SelectPage"; -import AIPage from "./pages/AIPage"; import CommunityPage from "./pages/CommunityPage"; import { AIDrawerProvider } from "./contexts/AIDrawerContext"; import DesktopAIShell from "./components/DesktopAIShell"; import { appShellColumnClass, appShellGutterClass } from "./lib/appShell"; +const EQPage = lazy(() => import("./pages/EQPage")); +const AIPage = lazy(() => import("./pages/AIPage")); + function AIPageRoute() { - return ; + return ( + + + + ); +} + +function EQPageRoute() { + return ( + + + + ); } function AppRouter() { @@ -34,7 +49,7 @@ function AppRouter() { - + diff --git a/client/src/components/DesktopAIShell.tsx b/client/src/components/DesktopAIShell.tsx index 94c9def..2e971c8 100644 --- a/client/src/components/DesktopAIShell.tsx +++ b/client/src/components/DesktopAIShell.tsx @@ -1,4 +1,4 @@ -import AIPage from "@/pages/AIPage"; +import PageSuspense from "@/components/PageSuspense"; import { useAIDrawer } from "@/contexts/AIDrawerContext"; import { useIsLgUp } from "@/hooks/useIsLgUp"; import { cn } from "@/lib/utils"; @@ -7,10 +7,13 @@ import { useCallback, useEffect, useRef, + lazy, type ReactNode, type RefObject, } from "react"; +const AIPage = lazy(() => import("@/pages/AIPage")); + function SplitDivider({ containerRef, onResize, @@ -114,7 +117,9 @@ export default function DesktopAIShell({ children }: { children: ReactNode }) { className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden border-l border-white/[0.08] bg-[#0d1117]" style={{ flex: `${splitPercent} 1 0%` }} > - setOpen(false)} /> + + setOpen(false)} /> + )} diff --git a/client/src/components/PageSuspense.tsx b/client/src/components/PageSuspense.tsx new file mode 100644 index 0000000..9233182 --- /dev/null +++ b/client/src/components/PageSuspense.tsx @@ -0,0 +1,13 @@ +import { Suspense, type ReactNode } from "react"; + +function PageFallback() { + return ( +
+ Loading… +
+ ); +} + +export default function PageSuspense({ children }: { children: ReactNode }) { + return }>{children}; +} diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index ac9b3e2..03ce0c2 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -378,6 +378,25 @@ export default function Home() { }; }, []); + const volumePassthroughActive = + (deviceState?.dacVolumeDirect ?? 0) === 1 || (deviceState?.dacVolumeDirect ?? 0) === 2; + const volumeControlsLocked = + volumePassthroughActive && + (deviceState?.output ?? 0) !== HEADPHONE_OUTPUT_INDEX; + + const volumePassthroughHint = + homeText.volumePassthroughLockedHint ?? + homeText.volumePassthroughLockedToast ?? + "当前为音量直通模式,无法调节音量"; + + const guardVolumeChange = useCallback( + (action: () => void) => { + if (volumeControlsLocked) return; + action(); + }, + [volumeControlsLocked], + ); + if (!isConnected) return ; const ds = deviceState!; @@ -393,24 +412,6 @@ export default function Home() { // 换算为 volume 单位:0.5/1/2/3 dB -> 1/2/4/6 const fineVolumeStep = [1, 2, 4, 6][ds.soundStep ?? 1] ?? 1; - const volumePassthroughActive = - (ds.dacVolumeDirect ?? 0) === 1 || (ds.dacVolumeDirect ?? 0) === 2; - const volumeControlsLocked = - volumePassthroughActive && ds.output !== HEADPHONE_OUTPUT_INDEX; - - const volumePassthroughHint = - homeText.volumePassthroughLockedHint ?? - homeText.volumePassthroughLockedToast ?? - "当前为音量直通模式,无法调节音量"; - - const guardVolumeChange = useCallback( - (action: () => void) => { - if (volumeControlsLocked) return; - action(); - }, - [volumeControlsLocked], - ); - // Slider fill % (0-200 → 0-100%) const fillPct = (localVol / 200) * 100; // Knob angle (map 0..200 -> -135..+135 degrees) diff --git a/vite.config.ts b/vite.config.ts index 09abb2f..b83d250 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -208,21 +208,27 @@ export default defineConfig({ manualChunks(id) { if (!id.includes("node_modules")) return; - // Keep core runtime dependencies stable for better long-term caching. if (id.includes("node_modules/react/") || id.includes("node_modules/react-dom/")) { return "react-vendor"; } if (id.includes("node_modules/wouter/")) { return "router-vendor"; } - - // Split large libraries into their own chunks to avoid a single huge entry bundle. if (id.includes("node_modules/echarts/")) return "echarts-vendor"; if (id.includes("node_modules/recharts/")) return "recharts-vendor"; if (id.includes("node_modules/framer-motion/")) return "motion-vendor"; if (id.includes("node_modules/@radix-ui/")) return "radix-vendor"; - // Group frequently used small UI helpers. + // streamdown + shiki + mermaid (lazy-loaded with AIPage) + if ( + id.includes("streamdown") || + id.includes("@shikijs/") || + id.includes("/shiki/") || + id.includes("mermaid") + ) { + return "ai-markdown-vendor"; + } + if ( id.includes("node_modules/lucide-react/") || id.includes("node_modules/class-variance-authority/") || @@ -231,25 +237,11 @@ export default defineConfig({ ) { return "ui-vendor"; } - - // Fall back to per-package vendor chunks. - const packageMatch = id.match( - /node_modules[\\/](?:\.pnpm[\\/][^\\/]+[\\/]node_modules[\\/])?(@?[^\\/]+(?:[\\/][^\\/]+)?)/, - ); - const packagePath = packageMatch?.[1]; - if (!packagePath) return "vendor"; - - const packageName = packagePath.startsWith("@") - ? packagePath.split(/[\\/]/).slice(0, 2).join("_") - : packagePath.split(/[\\/]/)[0]; - - return `vendor-${packageName}`; }, - // Prevent Rollup from merging manual chunks back into larger bundles. - onlyExplicitManualChunks: true, }, }, - chunkSizeWarningLimit: 500, + // streamdown pulls full @shikijs/langs (~7 MB) + themes + mermaid; lazy-loaded with AIPage. + chunkSizeWarningLimit: 15000, cssCodeSplit: true, }, server: {