更新依赖项和配置以支持新版本的pnpm,优化PostCSS和Tailwind配置,添加延迟输入功能到EffectsPage,提升用户体验

This commit is contained in:
eafonyang
2026-06-08 17:12:14 +08:00
parent 56da3e0d1f
commit 09ddaf958c
9 changed files with 2359 additions and 1357 deletions
-12
View File
@@ -1,12 +0,0 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
<title>Luxsin X9 Controller</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+5 -4
View File
@@ -43,10 +43,6 @@ body,
height: auto !important;
}
.min-h-dvh {
min-height: 0;
}
.x9-app-shell-outer {
min-height: 100vh;
min-height: -webkit-fill-available;
@@ -90,6 +86,11 @@ body {
}
@layer components {
.flex {
min-height: 0;
min-width: 0;
}
.x9-app-shell-outer {
min-height: 100vh;
min-height: -webkit-fill-available;
+162 -5
View File
@@ -7,7 +7,7 @@
All dropdowns → navigate to /select page
============================================================ */
import { useDevice } from "@/contexts/DeviceContext";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { ChevronLeft, ChevronRight, X } from "lucide-react";
import { useLocation } from "wouter";
import { useState, useEffect, useRef, useMemo } from "react";
import BottomNav from "@/components/BottomNav";
@@ -18,6 +18,7 @@ import { parseFirmwareVersion } from "@/lib/luxsinApi";
import localeZh from "@/locales/data-zh.json";
import localeZhHK from "@/locales/data-zh-HK.json";
import localeEn from "@/locales/data-en.json";
import { toast } from "sonner";
type EffectLocale = NonNullable<(typeof localeZh)["effect"]>;
@@ -48,6 +49,44 @@ function formatSubwooferDelayValue(value: number) {
return `${ms.toFixed(2)}ms(${cm.toFixed(1)}cm)`;
}
function parseSubwooferDelayInput(raw: string, english: boolean) {
const trimmed = raw.trim().toLowerCase();
if (!trimmed) {
return {
ok: false as const,
message: english ? "Enter a value" : "请输入数值",
};
}
const cmMatch = trimmed.match(/^([\d.]+)\s*cm$/);
if (cmMatch) {
const cm = Number(cmMatch[1]);
if (!Number.isFinite(cm)) {
return { ok: false as const, message: english ? "Invalid value" : "无效数值" };
}
const value = Math.round((cm * SUBWOOFER_DELAY_MS_DIVISOR) / SUBWOOFER_DELAY_CM_NUMERATOR);
return { ok: true as const, value: clampSubwooferDelayValue(value) };
}
const msMatch = trimmed.match(/^([\d.]+)\s*ms?$/);
const plainMatch = trimmed.match(/^([\d.]+)$/);
const msStr = msMatch?.[1] ?? plainMatch?.[1];
if (msStr) {
const ms = Number(msStr);
if (!Number.isFinite(ms)) {
return { ok: false as const, message: english ? "Invalid value" : "无效数值" };
}
const value = Math.round(ms * SUBWOOFER_DELAY_MS_DIVISOR);
return { ok: true as const, value: clampSubwooferDelayValue(value) };
}
return {
ok: false as const,
message: english ? "Use e.g. 10.5, 10.5ms, or 7.4cm" : "格式如 10.5、10.5ms 或 7.4cm",
};
}
type DelayInputTarget = "main" | "sub";
type DelayChannel = "L" | "R";
function DelaySpeakerIcon({ direction }: { direction: "left" | "right" }) {
@@ -292,6 +331,48 @@ export default function EffectsPage() {
const isDraggingSubDelayMain = useRef(false);
const isDraggingSubDelaySub = useRef(false);
const [delayInputTarget, setDelayInputTarget] = useState<DelayInputTarget | null>(null);
const [delayInputValue, setDelayInputValue] = useState("");
const delayInputEnglish = deviceState?.language === 0;
const maxDelayMs = SUBWOOFER_DELAY_MAX / SUBWOOFER_DELAY_MS_DIVISOR;
const openDelayInput = (target: DelayInputTarget) => {
const current = target === "main" ? localSubDelayMain : localSubDelaySub;
setDelayInputValue(subwooferDelayToMs(current).toFixed(2));
setDelayInputTarget(target);
};
const closeDelayInput = () => {
setDelayInputTarget(null);
setDelayInputValue("");
};
const applyDelayInput = () => {
if (!delayInputTarget) return;
const parsed = parseSubwooferDelayInput(delayInputValue, delayInputEnglish);
if (!parsed.ok) {
toast.error(parsed.message);
return;
}
const delayVal = parsed.value;
if (delayInputTarget === "main") {
setLocalSubDelayMain(delayVal);
updateSetting({ [subwooferDelayMainField]: delayVal });
} else {
setLocalSubDelaySub(delayVal);
updateSetting({ [subwooferDelaySubField]: delayVal });
}
closeDelayInput();
};
const delayInputTitle =
delayInputTarget === "main"
? subwooferDelayText?.mainSpeaker ?? "主机箱"
: delayInputTarget === "sub"
? subwooferDelayText?.subwoofer ?? "低音炮"
: "";
useEffect(() => {
if (!isDragging.current) setLocalWidth(Math.round(widthVal));
}, [widthVal]);
@@ -727,9 +808,14 @@ export default function EffectsPage() {
<span className="text-[14px] text-white/60">
{subwooferDelayText?.mainSpeaker ?? "主机箱"}
</span>
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>
<button
type="button"
className="text-[14px] font-bold active:opacity-70 transition-opacity"
style={{ color: "#00FFF6" }}
onClick={() => openDelayInput("main")}
>
{formatSubwooferDelayValue(localSubDelayMain)}
</span>
</button>
</div>
<CyanSlider
value={localSubDelayMain}
@@ -754,9 +840,14 @@ export default function EffectsPage() {
<span className="text-[14px] text-white/60">
{subwooferDelayText?.subwoofer ?? "低音炮"}
</span>
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>
<button
type="button"
className="text-[14px] font-bold active:opacity-70 transition-opacity"
style={{ color: "#00FFF6" }}
onClick={() => openDelayInput("sub")}
>
{formatSubwooferDelayValue(localSubDelaySub)}
</span>
</button>
</div>
<CyanSlider
value={localSubDelaySub}
@@ -782,6 +873,72 @@ export default function EffectsPage() {
</FeatureGate>
</div>
{delayInputTarget && (
<div
className="fixed inset-0 z-[120] flex items-center justify-center bg-black/65 px-4"
onClick={closeDelayInput}
>
<div
className="w-full max-w-[420px] rounded-[14px] p-5"
style={{
background: "linear-gradient(180deg, rgba(34,36,40,0.98) 0%, rgba(24,26,30,0.98) 100%)",
border: "1px solid rgba(255,255,255,0.12)",
boxShadow: "0 18px 48px rgba(0,0,0,0.55)",
}}
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-2">
<h3 className="text-[18px] font-semibold leading-tight text-white/90">
{delayInputTitle} · {subwooferDelayText?.label ?? "延时"}
</h3>
<button
type="button"
className="rounded-full p-1.5 text-white/45 transition-colors hover:bg-white/10 hover:text-white/80"
onClick={closeDelayInput}
aria-label={effectText.enableConfirmCancel ?? "取消"}
>
<X size={20} />
</button>
</div>
<p className="mb-4 text-[13px] leading-relaxed text-white/45">
{delayInputEnglish
? `Enter delay in ms (0${maxDelayMs.toFixed(2)}), or cm, e.g. 10.5, 10.5ms, 7.4cm`
: `输入延时(ms0${maxDelayMs.toFixed(2)}),也可输入 cm,如 10.5、10.5ms、7.4cm`}
</p>
<input
value={delayInputValue}
onChange={(e) => setDelayInputValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
applyDelayInput();
}
}}
inputMode="decimal"
placeholder={delayInputEnglish ? "e.g. 10.50" : "如 10.50"}
className="h-11 w-full rounded-[10px] border border-white/12 bg-[#15171b] px-3 text-[15px] text-white/90 outline-none placeholder:text-white/30 focus:border-[#00FFF6]/40"
autoFocus
/>
<div className="mt-6 flex items-center justify-center gap-4 sm:gap-8">
<button
type="button"
className="min-w-[112px] rounded-full px-5 py-2.5 text-[15px] font-medium text-white/85 transition-colors bg-[#3f4349] hover:bg-[#4a4f56] active:scale-[0.98]"
onClick={closeDelayInput}
>
{effectText.enableConfirmCancel ?? "取消"}
</button>
<button
type="button"
className="min-w-[112px] rounded-full px-5 py-2.5 text-[15px] font-semibold text-black transition-all bg-[#00FFF6] hover:brightness-95 active:scale-[0.98]"
onClick={applyDelayInput}
>
{delayInputEnglish ? "OK" : "确定"}
</button>
</div>
</div>
</div>
)}
<BottomNav />
</div>
);
+1 -9
View File
@@ -94,13 +94,5 @@
"vite-plugin-manus-runtime": "^0.0.57",
"vitest": "^2.1.4"
},
"packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af",
"pnpm": {
"patchedDependencies": {
"wouter@3.7.1": "patches/wouter@3.7.1.patch"
},
"overrides": {
"tailwindcss>nanoid": "3.3.7"
}
}
"packageManager": "pnpm@11.5.2+sha512.71c631e382066efc25625d5cf029075de07b61b37f6e27350fbd84b1bda5864c8c1967adc280776b45c30a715c0359a3be08fef42d5bb09e2b99029979692916"
}
+2133 -1313
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
packages:
- .
allowBuilds:
esbuild: true
overrides:
tailwindcss>nanoid: 3.3.7
+9 -1
View File
@@ -1,6 +1,14 @@
/**
* Legacy PostCSS is configured inline in vite.legacy.config.ts (same as x8).
* This file is kept for reference / tooling that expects a postcss config path.
*/
const path = require("node:path");
const tailwindcss3 = require("tailwindcss3");
const autoprefixer = require("autoprefixer");
module.exports = {
plugins: [tailwindcss3({ config: "./tailwind.legacy.config.cjs" }), autoprefixer()],
plugins: [
tailwindcss3({ config: path.join(__dirname, "tailwind.legacy.config.cjs") }),
autoprefixer(),
],
};
+6
View File
@@ -28,4 +28,10 @@ module.exports = {
},
},
plugins: [],
safelist: [
"x9-app-shell-outer",
"x9-app-shell-column",
"x9-bottom-nav-outer",
"x9-bottom-nav-inner",
],
};
+34 -12
View File
@@ -4,6 +4,7 @@ import path from "node:path";
import { createRequire } from "node:module";
import { defineConfig, type Plugin } from "vite";
const PROJECT_ROOT = import.meta.dirname;
const require = createRequire(import.meta.url);
const tailwindcss3 = require("tailwindcss3");
const autoprefixer = require("autoprefixer");
@@ -14,8 +15,8 @@ function legacyIndexRewritePlugin(): Plugin {
apply: "serve",
configureServer(server) {
server.middlewares.use((req, _res, next) => {
const url = req.url ?? "";
if (url === "/" || url === "/index.html") {
const url = (req.url ?? "").split("?")[0];
if (url === "/" || url === "/index.html" || url === "/i.modern.html") {
req.url = "/i.legacy.html";
}
next();
@@ -24,38 +25,59 @@ function legacyIndexRewritePlugin(): Plugin {
};
}
/** Dev MPA may discover modern CSS — stub them so Tailwind v3 postcss does not choke. */
function legacySkipModernCssPlugin(): Plugin {
return {
name: "legacy-skip-modern-css",
enforce: "pre",
apply: "serve",
load(id) {
const norm = id.replace(/\\/g, "/");
if (norm.endsWith("/client/src/index.css")) {
return "/* legacy dev: skip modern index.css */";
}
if (/node_modules\/tailwindcss\/index\.css/.test(norm)) {
return "/* legacy dev: skip tailwind v4 */";
}
},
};
}
export default defineConfig({
plugins: [legacyIndexRewritePlugin(), react(), jsxLocPlugin()],
plugins: [legacySkipModernCssPlugin(), legacyIndexRewritePlugin(), react(), jsxLocPlugin()],
define: {
"import.meta.env.VITE_X9_LEGACY": JSON.stringify("1"),
},
base: process.env.NODE_ENV === "production" ? "/x9/v2/legacy/" : "/",
resolve: {
alias: {
"@": path.resolve(import.meta.dirname, "client", "src"),
"@shared": path.resolve(import.meta.dirname, "shared"),
"@assets": path.resolve(import.meta.dirname, "attached_assets"),
"@": path.resolve(PROJECT_ROOT, "client", "src"),
"@shared": path.resolve(PROJECT_ROOT, "shared"),
"@assets": path.resolve(PROJECT_ROOT, "attached_assets"),
},
dedupe: ["react", "react-dom"],
},
envDir: path.resolve(import.meta.dirname),
root: path.resolve(import.meta.dirname, "client"),
publicDir: path.resolve(import.meta.dirname, "client/public"),
envDir: path.resolve(PROJECT_ROOT),
root: path.resolve(PROJECT_ROOT, "client"),
publicDir: path.resolve(PROJECT_ROOT, "client/public"),
css: {
transformer: "postcss",
postcss: {
plugins: [tailwindcss3({ config: "./tailwind.legacy.config.cjs" }), autoprefixer()],
plugins: [
tailwindcss3({ config: path.join(PROJECT_ROOT, "tailwind.legacy.config.cjs") }),
autoprefixer(),
],
},
},
build: {
target: ["chrome91", "edge91"],
cssTarget: "chrome91",
outDir: path.resolve(import.meta.dirname, "dist/legacy"),
outDir: path.resolve(PROJECT_ROOT, "dist/legacy"),
emptyOutDir: true,
minify: "terser",
rollupOptions: {
input: {
i: path.resolve(import.meta.dirname, "client", "i.legacy.html"),
i: path.resolve(PROJECT_ROOT, "client", "i.legacy.html"),
},
},
},