This commit is contained in:
eric
2026-03-29 01:54:10 -05:00
parent 784a4468b8
commit c3ee09674f
13 changed files with 4347 additions and 70 deletions

View File

@@ -26,10 +26,12 @@ import {
} from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { LiveStudioScene3D } from "@/components/LiveStudioScene3D";
import OverviewCharts from "@/components/OverviewCharts";
import { LiveConsoleWorkspace } from "@/components/console/LiveConsoleWorkspace";
import { LiveAndroidWorkstation } from "@/components/live-product/LiveAndroidWorkstation";
import { LiveTiktokHdmiView } from "@/components/live-product/LiveTiktokHdmiView";
import type { PortalNavTarget } from "@/components/live-product/LivePipelineStrip";
import { LiveYoutubeUnmannedView } from "@/components/live-product/LiveYoutubeUnmannedView";
import { normalizeBrowserReachableHttpUrl } from "@/lib/panelUrls";
@@ -665,6 +667,23 @@ export default function LiveControlApp() {
))}
</div>
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.05 }}
className="rounded-2xl border border-white/[0.07] bg-zinc-950/30 p-2 backdrop-blur-xl"
>
<OverviewCharts snapshot={dash?.snapshot ?? null} theme="dark" tr={t} />
</motion.div>
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.09 }}
className="rounded-2xl border border-fuchsia-500/20 bg-gradient-to-br from-fuchsia-500/10 via-zinc-950/60 to-cyan-500/10 p-3 shadow-lg shadow-fuchsia-900/10"
>
<LiveStudioScene3D tr={t} />
</motion.div>
{dash?.hardware && typeof dash.hardware === "object" ? (
<div className="rounded-2xl border border-indigo-500/25 bg-gradient-to-br from-indigo-500/[0.1] via-zinc-950/70 to-violet-500/[0.08] p-6 shadow-2xl shadow-indigo-500/5">
<h3 className="text-base font-semibold text-white">
@@ -797,6 +816,12 @@ export default function LiveControlApp() {
{dash.stack.neko_login.note_zh ||
t("Chromium :9200 与 Firefox :9201(若启用)共用下列账号策略。", "Chromium :9200 and Firefox :9201 share the same policy.")}
</p>
<p className="mt-2 text-[11px] text-amber-200/85">
{t(
"重要:两栏分开填——昵称随意,密码必须是环境变量里的口令(默认 12345678)。用错密码会像一直加载;勿把「用户名/密码」写在一行。",
"Use two fields: display name is free-form; password must match NEKO_* (default 12345678). Wrong password looks like infinite loading.",
)}
</p>
<div className="mt-4 grid gap-3 sm:grid-cols-2 font-mono text-sm">
<div className="rounded-xl border border-white/10 bg-black/40 px-4 py-3">
<p className="text-[10px] uppercase text-zinc-500">{t("用户", "User")}</p>
@@ -880,6 +905,55 @@ export default function LiveControlApp() {
</div>
) : null}
<div className="rounded-2xl border border-violet-500/20 bg-gradient-to-br from-violet-500/[0.12] via-zinc-950/50 to-fuchsia-500/[0.06] p-5 shadow-lg shadow-violet-500/5">
<div className="flex items-center justify-between gap-2">
<h3 className="text-sm font-semibold text-white">{t("直播进程", "Live processes")}</h3>
<span className="rounded-full bg-white/10 px-2 py-0.5 text-[10px] font-medium text-zinc-300">
{dash?.live?.backend_mode || "—"}
</span>
</div>
<ul className="mt-4 space-y-2.5">
{(dash?.live?.processes || []).slice(0, 10).map((p) => (
<li
key={p.pm2}
className="group flex items-center gap-3 rounded-xl border border-white/[0.08] bg-black/35 px-3 py-2.5 transition hover:border-violet-400/25 hover:bg-black/45"
>
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500/30 to-fuchsia-500/20 text-[10px] font-bold text-violet-100 ring-1 ring-white/10">
LIVE
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-zinc-100">{p.label || p.pm2}</p>
<p className="truncate font-mono text-[10px] text-zinc-600">{p.script || p.pm2}</p>
</div>
<span className={`shrink-0 rounded-lg px-2.5 py-1 text-[10px] font-semibold ${statusPill(p.process_status || "")}`}>
{p.process_status || "—"}
</span>
</li>
))}
{!dash?.live?.processes?.length ? (
<li className="rounded-xl border border-dashed border-white/10 py-8 text-center text-xs text-zinc-600">
{t("暂无直播进程", "No live processes")}
</li>
) : null}
</ul>
<div className="mt-5 grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => setView("live_youtube")}
className="rounded-xl border border-violet-400/35 bg-violet-500/15 py-2.5 text-[11px] font-semibold text-violet-100 shadow-inner shadow-black/20 transition hover:bg-violet-500/25"
>
YouTube
</button>
<button
type="button"
onClick={() => setView("live_tiktok")}
className="rounded-xl border border-cyan-400/35 bg-cyan-500/15 py-2.5 text-[11px] font-semibold text-cyan-100 shadow-inner shadow-black/20 transition hover:bg-cyan-500/25"
>
TikTok
</button>
</div>
</div>
{dash?.probes && showStreamProbes ? (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{dash.probes.srs ? (
@@ -997,67 +1071,6 @@ export default function LiveControlApp() {
) : null}
</div>
) : null}
<div className="grid gap-6 xl:grid-cols-3">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.15 }}
className="xl:col-span-2 rounded-2xl border border-white/[0.07] bg-zinc-950/30 p-2 backdrop-blur-xl"
>
<OverviewCharts snapshot={dash?.snapshot ?? null} theme="dark" tr={t} />
</motion.div>
<div className="space-y-4">
<div className="rounded-2xl border border-violet-500/20 bg-gradient-to-br from-violet-500/[0.12] via-zinc-950/50 to-fuchsia-500/[0.06] p-5 shadow-lg shadow-violet-500/5">
<div className="flex items-center justify-between gap-2">
<h3 className="text-sm font-semibold text-white">{t("直播进程", "Live processes")}</h3>
<span className="rounded-full bg-white/10 px-2 py-0.5 text-[10px] font-medium text-zinc-300">
{dash?.live?.backend_mode || "—"}
</span>
</div>
<ul className="mt-4 space-y-2.5">
{(dash?.live?.processes || []).slice(0, 10).map((p) => (
<li
key={p.pm2}
className="group flex items-center gap-3 rounded-xl border border-white/[0.08] bg-black/35 px-3 py-2.5 transition hover:border-violet-400/25 hover:bg-black/45"
>
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500/30 to-fuchsia-500/20 text-[10px] font-bold text-violet-100 ring-1 ring-white/10">
LIVE
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-zinc-100">{p.label || p.pm2}</p>
<p className="truncate font-mono text-[10px] text-zinc-600">{p.script || p.pm2}</p>
</div>
<span className={`shrink-0 rounded-lg px-2.5 py-1 text-[10px] font-semibold ${statusPill(p.process_status || "")}`}>
{p.process_status || "—"}
</span>
</li>
))}
{!dash?.live?.processes?.length ? (
<li className="rounded-xl border border-dashed border-white/10 py-8 text-center text-xs text-zinc-600">
{t("暂无直播进程", "No live processes")}
</li>
) : null}
</ul>
<div className="mt-5 grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => setView("live_youtube")}
className="rounded-xl border border-violet-400/35 bg-violet-500/15 py-2.5 text-[11px] font-semibold text-violet-100 shadow-inner shadow-black/20 transition hover:bg-violet-500/25"
>
YouTube
</button>
<button
type="button"
onClick={() => setView("live_tiktok")}
className="rounded-xl border border-cyan-400/35 bg-cyan-500/15 py-2.5 text-[11px] font-semibold text-cyan-100 shadow-inner shadow-black/20 transition hover:bg-cyan-500/25"
>
TikTok
</button>
</div>
</div>
</div>
</div>
</div>
) : null}
@@ -1098,6 +1111,14 @@ export default function LiveControlApp() {
)}
</p>
) : null}
{s.service_id === "neko_firefox" ? (
<p className="mt-2 text-[11px] leading-snug text-fuchsia-200/80">
{t(
"第二套 Neko 桌面(Firefox);ENABLE_NEKO_FF=1。登录:昵称任意,密码 = NEKO_USER_PASS / NEKO_ADMIN_PASS(与 Chromium 相同,默认 12345678)。",
"Second Neko desktop (Firefox). Login: any display name; password = NEKO_USER_PASS / NEKO_ADMIN_PASS (same as Chromium).",
)}
</p>
) : null}
<div className="mt-4 flex flex-wrap items-center gap-2">
{s.url ? (
<a
@@ -1177,6 +1198,7 @@ export default function LiveControlApp() {
fetchJson={fetchJson}
liveProcesses={dash?.live?.processes ?? []}
notify={notify}
onNavigate={(target: PortalNavTarget) => setView(target)}
/>
) : null}
@@ -1193,6 +1215,7 @@ export default function LiveControlApp() {
onSaveUrlConfig={(c) => saveUrlConfig(c)}
onCopy={copyText}
fetchJson={fetchJson}
onNavigate={(target: PortalNavTarget) => setView(target)}
/>
) : null}