's'
This commit is contained in:
@@ -191,8 +191,15 @@ function Button({
|
||||
);
|
||||
}
|
||||
|
||||
export type LiveConsoleWorkspaceProps = {
|
||||
/** 嵌入门户「网络」页:只渲染 ShellCrash 面板,不改写地址栏、不显示顶栏/Tab/底栏 */
|
||||
embeddedShellCrash?: boolean;
|
||||
/** 与门户首页语言同步 */
|
||||
portalLang?: Lang;
|
||||
};
|
||||
|
||||
/** 全功能控制台:默认从门户 `/` 进入后打开 `/console`。 */
|
||||
export function LiveConsoleWorkspace() {
|
||||
export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }: LiveConsoleWorkspaceProps) {
|
||||
const base = apiBase();
|
||||
const [lang, setLang] = useState<Lang>("zh");
|
||||
const [theme, setTheme] = useState<ThemeMode>("dark");
|
||||
@@ -448,6 +455,16 @@ export function LiveConsoleWorkspace() {
|
||||
}, [fetchJson, refreshAndroidDevices, refreshServices]);
|
||||
|
||||
useEffect(() => {
|
||||
if (embeddedShellCrash) {
|
||||
setTab("shellcrash");
|
||||
if (portalLang === "zh" || portalLang === "en") {
|
||||
setLang(portalLang);
|
||||
} else {
|
||||
const hub = window.localStorage.getItem("live-hub-lang");
|
||||
if (hub === "zh" || hub === "en") setLang(hub);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const pathTab = pathnameToTab(window.location.pathname);
|
||||
const nextTab = params.get("tab");
|
||||
@@ -459,16 +476,23 @@ export function LiveConsoleWorkspace() {
|
||||
else if (storedLang === "zh" || storedLang === "en") setLang(storedLang);
|
||||
const storedTheme = window.localStorage.getItem("live-console-theme");
|
||||
if (storedTheme === "light" || storedTheme === "dark") setTheme(storedTheme);
|
||||
}, []);
|
||||
}, [embeddedShellCrash, portalLang]);
|
||||
|
||||
useEffect(() => {
|
||||
if (embeddedShellCrash) return;
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
window.localStorage.setItem("live-console-theme", theme);
|
||||
}, [theme]);
|
||||
}, [theme, embeddedShellCrash]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!embeddedShellCrash) return;
|
||||
if (portalLang === "zh" || portalLang === "en") setLang(portalLang);
|
||||
}, [embeddedShellCrash, portalLang]);
|
||||
|
||||
useEffect(() => {
|
||||
if (embeddedShellCrash) return;
|
||||
syncUrl(tab, lang);
|
||||
}, [lang, syncUrl, tab]);
|
||||
}, [embeddedShellCrash, lang, syncUrl, tab]);
|
||||
|
||||
useEffect(() => {
|
||||
void boot();
|
||||
@@ -483,7 +507,7 @@ export function LiveConsoleWorkspace() {
|
||||
|
||||
useEffect(() => {
|
||||
if (booting || error) return;
|
||||
if (tab !== "overview" && tab !== "modules") return;
|
||||
if (tab !== "overview" && tab !== "modules" && tab !== "shellcrash") return;
|
||||
void refreshServices();
|
||||
const timer = window.setInterval(() => void refreshServices(), 10000);
|
||||
return () => clearInterval(timer);
|
||||
@@ -809,102 +833,116 @@ export function LiveConsoleWorkspace() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-b border-white/[0.08] bg-slate-950/80 backdrop-blur-md">
|
||||
<div className="mx-auto flex max-w-7xl flex-wrap items-center justify-between gap-2 px-4 py-2.5 text-xs sm:px-6 lg:px-10">
|
||||
<a
|
||||
className="font-medium text-amber-400/95 transition hover:text-amber-300"
|
||||
href="/"
|
||||
>
|
||||
← {tr("返回门户首页", "Back to portal")}
|
||||
</a>
|
||||
<span className="text-slate-500">
|
||||
{tr("无人直播 · 本页需与 API 同域(推荐 live.local:8001)", "Unmanned live · same origin as API (live.local:8001)")}
|
||||
</span>
|
||||
{!embeddedShellCrash ? (
|
||||
<div className="border-b border-white/[0.08] bg-slate-950/80 backdrop-blur-md">
|
||||
<div className="mx-auto flex max-w-7xl flex-wrap items-center justify-between gap-2 px-4 py-2.5 text-xs sm:px-6 lg:px-10">
|
||||
<a className="font-medium text-amber-400/95 transition hover:text-amber-300" href="/">
|
||||
← {tr("返回门户首页", "Back to portal")}
|
||||
</a>
|
||||
<span className="text-slate-500">
|
||||
{tr("无人直播 · 本页需与 API 同域(推荐 live.local:8001)", "Unmanned live · same origin as API (live.local:8001)")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<main className="safe-pb-nav mx-auto min-h-screen max-w-7xl px-4 pb-16 pt-6 sm:px-6 lg:px-10">
|
||||
) : null}
|
||||
<main
|
||||
className={
|
||||
embeddedShellCrash
|
||||
? "mx-auto min-h-0 max-w-7xl px-1 pb-4 pt-1 sm:px-3"
|
||||
: "safe-pb-nav mx-auto min-h-screen max-w-7xl px-4 pb-16 pt-6 sm:px-6 lg:px-10"
|
||||
}
|
||||
>
|
||||
{toast ? (
|
||||
<div className="animate-toast-in fixed bottom-24 left-1/2 z-50 max-w-[min(92vw,28rem)] -translate-x-1/2 rounded-2xl border border-cyan-500/25 bg-slate-950/95 px-5 py-3 text-center text-sm text-cyan-50 shadow-lg md:bottom-6 [html[data-theme=light]_&]:border-cyan-600/25 [html[data-theme=light]_&]:bg-white/95 [html[data-theme=light]_&]:text-cyan-950">
|
||||
{toast}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<header className="mb-8 flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-cyan-400/90">
|
||||
{tr("控制平面 · ARM Linux 开发板", "Control plane · ARM Linux SBC")}
|
||||
</p>
|
||||
<h1 className="mt-2 text-3xl font-bold text-[color:var(--text)] sm:text-4xl">
|
||||
{tr("无人直播控制中心", "Unmanned live control center")}
|
||||
</h1>
|
||||
<p className="mt-3 max-w-3xl text-sm text-slate-400">
|
||||
{tr(
|
||||
"ShellCrash 负责代理上网;本机 FFmpeg 处理推流或 HDMI 采集;Neko / Chromium 负责浏览器侧;开发板通过 USB 双头线 ADB 控制 AOSP,采集卡可走视频进 Linux。各模块独立,可分别启停。",
|
||||
"ShellCrash for egress; FFmpeg for stream or HDMI; Neko/Chromium for browser; USB ADB to AOSP plus optional capture card. Modules are isolated.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="meta-chip max-w-full">
|
||||
{(stack?.mdns_host || "live.local") +
|
||||
" · " +
|
||||
(stack?.machine || "unknown") +
|
||||
" · " +
|
||||
(stack?.kernel || "unknown")}
|
||||
{!embeddedShellCrash ? (
|
||||
<header className="mb-8 flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-cyan-400/90">
|
||||
{tr("控制平面 · ARM Linux 开发板", "Control plane · ARM Linux SBC")}
|
||||
</p>
|
||||
<h1 className="mt-2 text-3xl font-bold text-[color:var(--text)] sm:text-4xl">
|
||||
{tr("无人直播控制中心", "Unmanned live control center")}
|
||||
</h1>
|
||||
<p className="mt-3 max-w-3xl text-sm text-slate-400">
|
||||
{tr(
|
||||
"ShellCrash 负责代理上网;本机 FFmpeg 处理推流或 HDMI 采集;Neko / Chromium 负责浏览器侧;开发板通过 USB 双头线 ADB 控制 AOSP,采集卡可走视频进 Linux。各模块独立,可分别启停。",
|
||||
"ShellCrash for egress; FFmpeg for stream or HDMI; Neko/Chromium for browser; USB ADB to AOSP plus optional capture card. Modules are isolated.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="meta-chip max-w-full">
|
||||
{(stack?.mdns_host || "live.local") +
|
||||
" · " +
|
||||
(stack?.machine || "unknown") +
|
||||
" · " +
|
||||
(stack?.kernel || "unknown")}
|
||||
</div>
|
||||
<Button
|
||||
className={lang === "zh" ? "border border-cyan-400/50 bg-cyan-500/10" : "border border-white/12 bg-black/20"}
|
||||
onClick={() => setLang("zh")}
|
||||
>
|
||||
中文
|
||||
</Button>
|
||||
<Button
|
||||
className={lang === "en" ? "border border-cyan-400/50 bg-cyan-500/10" : "border border-white/12 bg-black/20"}
|
||||
onClick={() => setLang("en")}
|
||||
>
|
||||
EN
|
||||
</Button>
|
||||
<Button
|
||||
className={theme === "light" ? "border border-amber-400/50 bg-amber-500/15" : "border border-white/12 bg-black/20"}
|
||||
onClick={() => setTheme("light")}
|
||||
title={tr("日间主题", "Light theme")}
|
||||
>
|
||||
{tr("☀️ 日间", "☀️ Light")}
|
||||
</Button>
|
||||
<Button
|
||||
className={theme === "dark" ? "border border-indigo-400/50 bg-indigo-500/15" : "border border-white/12 bg-black/20"}
|
||||
onClick={() => setTheme("dark")}
|
||||
title={tr("夜间主题", "Dark theme")}
|
||||
>
|
||||
{tr("🌙 夜间", "🌙 Dark")}
|
||||
</Button>
|
||||
<Button className="border border-white/12 bg-black/20" onClick={() => void boot()}>
|
||||
{tr("刷新", "Refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
className={lang === "zh" ? "border border-cyan-400/50 bg-cyan-500/10" : "border border-white/12 bg-black/20"}
|
||||
onClick={() => setLang("zh")}
|
||||
>
|
||||
中文
|
||||
</Button>
|
||||
<Button
|
||||
className={lang === "en" ? "border border-cyan-400/50 bg-cyan-500/10" : "border border-white/12 bg-black/20"}
|
||||
onClick={() => setLang("en")}
|
||||
>
|
||||
EN
|
||||
</Button>
|
||||
<Button
|
||||
className={theme === "light" ? "border border-amber-400/50 bg-amber-500/15" : "border border-white/12 bg-black/20"}
|
||||
onClick={() => setTheme("light")}
|
||||
title={tr("日间主题", "Light theme")}
|
||||
>
|
||||
{tr("☀️ 日间", "☀️ Light")}
|
||||
</Button>
|
||||
<Button
|
||||
className={theme === "dark" ? "border border-indigo-400/50 bg-indigo-500/15" : "border border-white/12 bg-black/20"}
|
||||
onClick={() => setTheme("dark")}
|
||||
title={tr("夜间主题", "Dark theme")}
|
||||
>
|
||||
{tr("🌙 夜间", "🌙 Dark")}
|
||||
</Button>
|
||||
<Button className="border border-white/12 bg-black/20" onClick={() => void boot()}>
|
||||
{tr("刷新", "Refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="tab-strip" aria-label={tr("主导航", "Primary navigation")}>
|
||||
{TABS.map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
className={`tab-btn ${tab === item ? "tab-btn-active" : ""}`}
|
||||
onClick={() => setTab(item)}
|
||||
>
|
||||
{{
|
||||
overview: tr("总览", "Overview"),
|
||||
business: tr("直播业务", "Live ops"),
|
||||
modules: tr("系统服务", "Services"),
|
||||
shellcrash: "ShellCrash",
|
||||
android: tr("安卓设备", "Android"),
|
||||
config: tr("配置中心", "Config"),
|
||||
}[item]}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</header>
|
||||
<nav className="tab-strip" aria-label={tr("主导航", "Primary navigation")}>
|
||||
{TABS.map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
className={`tab-btn ${tab === item ? "tab-btn-active" : ""}`}
|
||||
onClick={() => setTab(item)}
|
||||
>
|
||||
{{
|
||||
overview: tr("总览", "Overview"),
|
||||
business: tr("直播业务", "Live ops"),
|
||||
modules: tr("系统服务", "Services"),
|
||||
shellcrash: "ShellCrash",
|
||||
android: tr("安卓设备", "Android"),
|
||||
config: tr("配置中心", "Config"),
|
||||
}[item]}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</header>
|
||||
) : (
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2 border-b border-white/[0.08] pb-3">
|
||||
<h2 className="text-base font-semibold text-[color:var(--text)]">ShellCrash</h2>
|
||||
<Button className="border border-white/12 bg-black/20" onClick={() => void boot()}>
|
||||
{tr("刷新", "Refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{booting ? (
|
||||
<div className="glass rounded-3xl py-24 text-center text-slate-400">
|
||||
@@ -1546,36 +1584,38 @@ export function LiveConsoleWorkspace() {
|
||||
) : null}
|
||||
</main>
|
||||
|
||||
<nav
|
||||
className="fixed bottom-0 left-0 right-0 z-40 flex justify-around border-t border-white/15 bg-black/75 py-1.5 backdrop-blur-lg [html[data-theme=light]_&]:border-slate-200/60 [html[data-theme=light]_&]:bg-white/90 md:hidden"
|
||||
style={{ paddingBottom: "max(0.35rem, env(safe-area-inset-bottom))" }}
|
||||
aria-label={tr("主导航", "Main nav")}
|
||||
>
|
||||
{TABS.map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
className={`btn-focus flex min-w-0 flex-1 flex-col items-center gap-0.5 rounded-xl px-1 py-1 text-[10px] font-medium ${
|
||||
tab === item ? "text-cyan-300 [html[data-theme=light]_&]:text-cyan-700" : "text-slate-400 [html[data-theme=light]_&]:text-slate-600"
|
||||
}`}
|
||||
onClick={() => setTab(item)}
|
||||
>
|
||||
<span className="text-lg leading-none" aria-hidden>
|
||||
{tabEmoji[item]}
|
||||
</span>
|
||||
<span className="truncate">
|
||||
{{
|
||||
overview: tr("总览", "Home"),
|
||||
business: tr("业务", "Biz"),
|
||||
modules: tr("模块", "Mods"),
|
||||
shellcrash: "SC",
|
||||
android: tr("安卓", "Droid"),
|
||||
config: tr("配置", "Cfg"),
|
||||
}[item]}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
{!embeddedShellCrash ? (
|
||||
<nav
|
||||
className="fixed bottom-0 left-0 right-0 z-40 flex justify-around border-t border-white/15 bg-black/75 py-1.5 backdrop-blur-lg [html[data-theme=light]_&]:border-slate-200/60 [html[data-theme=light]_&]:bg-white/90 md:hidden"
|
||||
style={{ paddingBottom: "max(0.35rem, env(safe-area-inset-bottom))" }}
|
||||
aria-label={tr("主导航", "Main nav")}
|
||||
>
|
||||
{TABS.map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
className={`btn-focus flex min-w-0 flex-1 flex-col items-center gap-0.5 rounded-xl px-1 py-1 text-[10px] font-medium ${
|
||||
tab === item ? "text-cyan-300 [html[data-theme=light]_&]:text-cyan-700" : "text-slate-400 [html[data-theme=light]_&]:text-slate-600"
|
||||
}`}
|
||||
onClick={() => setTab(item)}
|
||||
>
|
||||
<span className="text-lg leading-none" aria-hidden>
|
||||
{tabEmoji[item]}
|
||||
</span>
|
||||
<span className="truncate">
|
||||
{{
|
||||
overview: tr("总览", "Home"),
|
||||
business: tr("业务", "Biz"),
|
||||
modules: tr("模块", "Mods"),
|
||||
shellcrash: "SC",
|
||||
android: tr("安卓", "Droid"),
|
||||
config: tr("配置", "Cfg"),
|
||||
}[item]}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user