| null>(null);
+ const autoConnectStartedRef = useRef(false);
const saveIp = useCallback((newIp: string) => {
setIp(newIp);
@@ -202,6 +204,17 @@ export function DeviceProvider({ children }: { children: React.ReactNode }) {
if (!isDemoMode && api) api.setBalance(v).catch(console.error);
}, [api, isDemoMode]);
+ // 启动时根据 ?ip= 或已存 IP 自动连接(默认进入首页,无需连接页)
+ useEffect(() => {
+ if (autoConnectStartedRef.current) return;
+ autoConnectStartedRef.current = true;
+
+ const urlIp = getDeviceIpFromUrl();
+ const targetIp = urlIp ?? ip;
+ if (urlIp) saveIp(urlIp);
+ void connect(false, targetIp);
+ }, [connect, ip, saveIp]);
+
// Polling for changes
useEffect(() => {
if (!isConnected || isDemoMode || !api) return;
diff --git a/client/src/lib/deviceIp.ts b/client/src/lib/deviceIp.ts
new file mode 100644
index 0000000..8a9fb7a
--- /dev/null
+++ b/client/src/lib/deviceIp.ts
@@ -0,0 +1,11 @@
+/** 从地址栏 ?ip= 读取设备 IP(hash 路由下 query 仍在 search 上) */
+export function getDeviceIpFromUrl(): string | null {
+ if (typeof window === "undefined") return null;
+ const ip = new URLSearchParams(window.location.search).get("ip");
+ const trimmed = ip?.trim();
+ return trimmed || null;
+}
+
+export function resolveInitialDeviceIp(): string {
+ return getDeviceIpFromUrl() ?? localStorage.getItem("luxsin_ip") ?? "192.168.2.30";
+}
diff --git a/client/src/locales/data-en.json b/client/src/locales/data-en.json
index 6fbed9f..7950b4e 100644
--- a/client/src/locales/data-en.json
+++ b/client/src/locales/data-en.json
@@ -12,7 +12,8 @@
"powerOffConfirmCancel": "Cancel",
"powerOffToastDemo": "Demo mode: disconnected from the device.",
"powerOffToastSent": "Shutdown command sent.",
- "doubleClickHint": "Double-click to enter"
+ "doubleClickHint": "Double-click to enter",
+ "backToLegacy": "Legacy version"
},
"vuPage": {
"title": "VU meter gallery"
@@ -587,7 +588,7 @@
"deviceName": "Device name",
"firmwareVersion": "Firmware version",
"macAddress": "MAC address",
- "disconnect": "Disconnect"
+ "deviceIp": "Device IP"
}
},
"prompt": {
diff --git a/client/src/locales/data-zh-HK.json b/client/src/locales/data-zh-HK.json
index 701bd2e..2aa0e8f 100644
--- a/client/src/locales/data-zh-HK.json
+++ b/client/src/locales/data-zh-HK.json
@@ -12,7 +12,8 @@
"powerOffConfirmCancel": "取消",
"powerOffToastDemo": "演示模式:已中斷與裝置的連線",
"powerOffToastSent": "已傳送關機指令",
- "doubleClickHint": "按兩下進入"
+ "doubleClickHint": "按兩下進入",
+ "backToLegacy": "返回舊版"
},
"vuPage": {
"title": "VU 表庫"
@@ -403,7 +404,7 @@
"deviceName": "裝置名稱",
"firmwareVersion": "韌體版本",
"macAddress": "MAC 位址",
- "disconnect": "中斷連線"
+ "deviceIp": "裝置 IP"
}
},
"prompt": {
diff --git a/client/src/locales/data-zh.json b/client/src/locales/data-zh.json
index 0c11587..b75dcdd 100644
--- a/client/src/locales/data-zh.json
+++ b/client/src/locales/data-zh.json
@@ -12,7 +12,8 @@
"powerOffConfirmCancel": "取消",
"powerOffToastDemo": "演示模式:已断开与设备的连接",
"powerOffToastSent": "已发送关机指令",
- "doubleClickHint": "双击进入"
+ "doubleClickHint": "双击进入",
+ "backToLegacy": "返回旧版"
},
"vuPage": {
"title": "VU 表库"
@@ -404,7 +405,7 @@
"deviceName": "设备名称",
"firmwareVersion": "固件版本",
"macAddress": "MAC 地址",
- "disconnect": "断开连接"
+ "deviceIp": "设备 IP"
}
},
"prompt": {
diff --git a/client/src/pages/AIPage.tsx b/client/src/pages/AIPage.tsx
index 81faeaa..ad8fdc0 100644
--- a/client/src/pages/AIPage.tsx
+++ b/client/src/pages/AIPage.tsx
@@ -7,7 +7,7 @@
Backend: /sse/question (SSE), /sse/messages, /sse/chats, /sse/clear
============================================================ */
import BottomNav from "@/components/BottomNav";
-import ConnectScreen from "@/components/ConnectScreen";
+import ConnectionPlaceholder from "@/components/ConnectionPlaceholder";
import { useDevice } from "@/contexts/DeviceContext";
import {
AI_SPLIT_RESTORE_PATH_KEY,
@@ -337,7 +337,7 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {})
})();
}, [mac, loadChat]);
- // 没有 mac 时(设备未识别完毕)不应该停在 initializing;ConnectScreen 会拦截
+ // 没有 mac 时(设备未识别完毕)不应该停在 initializing;未连接时 ConnectionPlaceholder 会拦截
useEffect(() => {
if (!mac && initializing) {
// 给 mac 一段缓冲期,否则即便最终没有 mac 也要释放欢迎页
@@ -692,7 +692,7 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {})
const headerTitle = aiText.title;
- if (!isConnected) return ;
+ if (!isConnected) return ;
return (
{
const dist = Math.abs(i - lead);
- let amp = 0.14;
- if (dist === 0) amp = 0.96;
- else if (dist === 1) amp = 0.74;
- else if (dist === 2) amp = 0.5;
- else if (dist === 3) amp = 0.3;
- return Math.min(1, amp * (0.82 + pulse * 0.18));
+ let peak = 0.06;
+ if (dist === 0) peak = 1;
+ else if (dist === 1) peak = 0.82;
+ else if (dist === 2) peak = 0.62;
+ else if (dist === 3) peak = 0.42;
+ else if (dist === 4) peak = 0.26;
+
+ const ripple = 0.35 * Math.max(0, Math.sin(t * 2.75 + i * 0.62));
+ const sway = 0.1 * Math.sin(t * 1.4 + i * 0.35);
+ const h = 0.05 + peak * kick + ripple * (0.55 + kick * 0.45) + sway;
+ return Math.min(1, Math.max(0.05, h));
+ });
+}
+
+/** 彩蛋律动二:海浪形波从左→右,再右→左 */
+function vuHeightsForWave(phaseElapsedMs: number): number[] {
+ const half = EGG_WAVE_HALF_MS;
+ const ltr = phaseElapsedMs < half;
+ const localMs = ltr ? phaseElapsedMs : phaseElapsedMs - half;
+ const t = localMs / 1000;
+ const direction = ltr ? 1 : -1;
+ const speed = 0.75;
+
+ return Array.from({ length: VU_BAR_COUNT }, (_, i) => {
+ const phase = t * speed * Math.PI * 2 - direction * i * 0.72;
+ const wave = (Math.sin(phase) + 1) / 2;
+ const foam = 0.06 + 0.05 * Math.sin(i * 0.28 + t * 0.5);
+ return Math.min(1, Math.max(0.06, foam + wave * 0.9));
});
}
@@ -131,7 +164,7 @@ function ListRow({
}
export default function Home() {
- const { isConnected, deviceState, setVolume, updateSetting, api, disconnect, isDemoMode } = useDevice();
+ const { isConnected, deviceState, setVolume, updateSetting, api, disconnect, isDemoMode, ip } = useDevice();
const [, setLocation] = useLocation();
const powerActionRef = useRef(false);
const hpEqLastTapRef = useRef(null);
@@ -144,6 +177,8 @@ export default function Home() {
const jackProgressRef = useRef(0);
const jackResetTimerRef = useRef | null>(null);
const eggRafRef = useRef(null);
+ /** 下次三连击成功时播放的律动:1=旋律,2=海浪(各需单独三连击触发) */
+ const jackEggNextRef = useRef<1 | 2>(1);
const homeText = useMemo((): HomeLocale => {
if (!deviceState) return {} as HomeLocale;
@@ -257,28 +292,33 @@ export default function Home() {
};
}, []);
- const triggerJackEasterEgg = useCallback(() => {
- setEasterEggActive(true);
- jackProgressRef.current = 0;
- const start = performance.now();
+ const runJackEggAnimation = useCallback(
+ (mode: "dance" | "wave") => {
+ setEasterEggActive(true);
+ setVuHeights(mode === "dance" ? vuHeightsForDance(0) : vuHeightsForWave(0));
+ jackProgressRef.current = 0;
+ const duration = mode === "dance" ? EGG_DANCE_DURATION_MS : EGG_WAVE_DURATION_MS;
+ const start = performance.now();
- const tick = (now: number) => {
- const elapsed = now - start;
- if (elapsed >= EGG_DURATION_MS) {
- setEasterEggActive(false);
- setVuHeights(idleVuBarHeights());
- eggRafRef.current = null;
- return;
- }
- const beat = Math.floor(elapsed / EGG_BEAT_MS) % EGG_MELODY_LEAD.length;
- const pulse = 0.5 + 0.5 * Math.sin(elapsed * 0.02);
- setVuHeights(vuHeightsForMelodyBeat(EGG_MELODY_LEAD[beat], pulse));
+ const tick = (now: number) => {
+ const elapsed = now - start;
+ if (elapsed >= duration) {
+ setEasterEggActive(false);
+ setVuHeights(idleVuBarHeights());
+ eggRafRef.current = null;
+ return;
+ }
+ setVuHeights(
+ mode === "dance" ? vuHeightsForDance(elapsed) : vuHeightsForWave(elapsed),
+ );
+ eggRafRef.current = requestAnimationFrame(tick);
+ };
+
+ if (eggRafRef.current !== null) cancelAnimationFrame(eggRafRef.current);
eggRafRef.current = requestAnimationFrame(tick);
- };
-
- if (eggRafRef.current !== null) cancelAnimationFrame(eggRafRef.current);
- eggRafRef.current = requestAnimationFrame(tick);
- }, []);
+ },
+ [],
+ );
const onJackClick = useCallback(
(jack: JackId) => {
@@ -288,7 +328,13 @@ export default function Home() {
if (jack === expected) {
jackProgressRef.current += 1;
if (jackProgressRef.current >= JACK_EGG_SEQUENCE.length) {
- triggerJackEasterEgg();
+ if (jackEggNextRef.current === 1) {
+ runJackEggAnimation("dance");
+ jackEggNextRef.current = 2;
+ } else {
+ runJackEggAnimation("wave");
+ jackEggNextRef.current = 1;
+ }
}
} else {
jackProgressRef.current = jack === JACK_EGG_SEQUENCE[0] ? 1 : 0;
@@ -299,7 +345,7 @@ export default function Home() {
jackProgressRef.current = 0;
}, JACK_SEQUENCE_TIMEOUT_MS);
},
- [easterEggActive, triggerJackEasterEgg],
+ [easterEggActive, runJackEggAnimation],
);
useEffect(() => {
@@ -309,7 +355,7 @@ export default function Home() {
};
}, []);
- if (!isConnected) return ;
+ if (!isConnected) return ;
const ds = deviceState!;
const inputLabel = INPUT_LABELS[ds.input] ?? "USB-C";
@@ -471,8 +517,8 @@ export default function Home() {
background: isHot ? "#ef4444" : i > 13 ? "#f59e0b" : "#00FFF6",
opacity: easterEggActive ? 0.95 : 0.7,
transition: easterEggActive
- ? "height 90ms ease-out, opacity 120ms ease-out"
- : "height 200ms ease-out",
+ ? "none"
+ : "height 200ms ease-out, opacity 200ms ease-out",
}}
/>
);
@@ -766,6 +812,15 @@ export default function Home() {
value={vuLabel}
onClick={() => setLocation("/vu")}
/>
+ }
+ label={homeText.backToLegacy ?? "返回旧版"}
+ onClick={() => {
+ const target = ip.trim();
+ if (!target) return;
+ window.location.href = `https://am.luxsinaudio.com/x8/i.html?ip=${encodeURIComponent(target)}#/home`;
+ }}
+ />
diff --git a/client/src/pages/SystemPage.tsx b/client/src/pages/SystemPage.tsx
index e0563be..818501f 100644
--- a/client/src/pages/SystemPage.tsx
+++ b/client/src/pages/SystemPage.tsx
@@ -90,7 +90,7 @@ type LocaleSettings = {
deviceName: string;
firmwareVersion: string;
macAddress: string;
- disconnect: string;
+ deviceIp: string;
};
};
};
@@ -109,7 +109,7 @@ function optionLabels(items: Array<{ index: number; label: string }> | undefined
export default function SystemPage() {
const [, setLocation] = useLocation();
- const { deviceState, updateSetting, disconnect, refresh, isConnected, api } = useDevice();
+ const { deviceState, updateSetting, refresh, isConnected, api, ip } = useDevice();
const ds = deviceState;
// 页面加载且已连接时刷新数据
@@ -204,16 +204,10 @@ export default function SystemPage() {
{ui.macAddress}
{ds?.mac ?? "—"}
-
-
- {/* ── 断开连接 ── */}
-
-
+
+ {ui.deviceIp ?? "设备 IP"}
+ {ip || "—"}
+
diff --git a/package.json b/package.json
index cc53c93..6425238 100644
--- a/package.json
+++ b/package.json
@@ -5,8 +5,8 @@
"license": "MIT",
"scripts": {
"dev": "vite --host",
- "build": "vite build && esbuild server/index.ts --platform=node --packages=external --bundle --format=esm --outdir=dist",
- "start": "NODE_ENV=production node dist/index.js",
+ "build": "vite build",
+ "start": "NODE_ENV=production tsx server/index.ts",
"preview": "vite preview --host",
"check": "tsc --noEmit",
"format": "prettier --write ."
diff --git a/server/index.ts b/server/index.ts
index 70704f7..0de24f0 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -10,17 +10,13 @@ async function startServer() {
const app = express();
const server = createServer(app);
- // Serve static files from dist/public in production
- const staticPath =
- process.env.NODE_ENV === "production"
- ? path.resolve(__dirname, "public")
- : path.resolve(__dirname, "..", "dist", "public");
+ const staticPath = path.resolve(__dirname, "..", "dist");
app.use(express.static(staticPath));
- // Handle client-side routing - serve index.html for all routes
+ // Handle client-side routing — entry is i.html (see vite rollup input)
app.get("*", (_req, res) => {
- res.sendFile(path.join(staticPath, "index.html"));
+ res.sendFile(path.join(staticPath, "i.html"));
});
const port = process.env.PORT || 3000;
diff --git a/vite.config.ts b/vite.config.ts
index 1735015..09abb2f 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -150,11 +150,33 @@ function vitePluginManusDebugCollector(): Plugin {
};
}
-const plugins = [react(), tailwindcss(), jsxLocPlugin(), vitePluginManusRuntime(), vitePluginManusDebugCollector()];
+/** 生产构建时剔除 public 中的 __manus__(仅开发调试使用) */
+function vitePluginExcludeManusFromDist(): Plugin {
+ const distDir = path.resolve(import.meta.dirname, "dist");
+ return {
+ name: "exclude-manus-from-dist",
+ closeBundle() {
+ if (process.env.NODE_ENV !== "production") return;
+ const manusDir = path.join(distDir, "__manus__");
+ if (fs.existsSync(manusDir)) {
+ fs.rmSync(manusDir, { recursive: true, force: true });
+ }
+ },
+ };
+}
+
+const plugins = [
+ react(),
+ tailwindcss(),
+ jsxLocPlugin(),
+ vitePluginManusRuntime(),
+ vitePluginManusDebugCollector(),
+ vitePluginExcludeManusFromDist(),
+];
export default defineConfig({
plugins,
- base: process.env.NODE_ENV === 'production' ? '/x8/v2/public/' : '/',
+ base: process.env.NODE_ENV === "production" ? "/x8/v2/" : "/",
resolve: {
alias: {
"@": path.resolve(import.meta.dirname, "client", "src"),
@@ -169,7 +191,7 @@ export default defineConfig({
build: {
target: 'esnext',
cssTarget: 'esnext',
- outDir: path.resolve(import.meta.dirname, "dist/public"),
+ outDir: path.resolve(import.meta.dirname, "dist"),
emptyOutDir: true,
minify: 'terser',
terserOptions: {