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: {