import { settingsSectionForLegacyTab } from "@umt/contracts"; import { useEffect } from "react"; import { Link, redirect, useLocation, useNavigate } from "react-router"; import { PageHeader } from "~/components/console/page-header.tsx"; import { parseSettingsHash, SECTION_GROUPS, settingsHash } from "~/components/settings/sections.tsx"; import { anchorId } from "~/components/settings/settings-block.tsx"; import { SettingsSearch } from "~/components/settings/settings-search.tsx"; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from "~/components/ui/select.tsx"; import { cn } from "~/lib/utils.ts"; export const meta = () => [{ title: "设置 · UHD 媒体自动化控制台" }]; /** 旧链接 `?tab=xxx` → `#分区[/节]`(console-ia.md §3.3) */ export function clientLoader({ request }: { request: Request }) { const url = new URL(request.url); const tab = url.searchParams.get("tab"); if (!tab) return null; const hit = settingsSectionForLegacyTab(tab); url.searchParams.delete("tab"); const rest = url.searchParams.toString(); return redirect(`${url.pathname}${rest ? `?${rest}` : ""}${hit ? settingsHash(hit.section, hit.anchor) : ""}`); } /** 搜索选中字段时经 location.state 带过来,对齐到节后聚焦这个字段 */ type NavState = { field?: string } | null; export default function SettingsPage() { const location = useLocation(); const navigate = useNavigate(); const { section, anchor, moved } = parseSettingsHash(location.hash); const field = (location.state as NavState)?.field ?? null; // 旧锚点(重排前的 #automation/cache 等)换成新位置,不留历史记录 useEffect(() => { if (moved) navigate({ hash: settingsHash(section.id, anchor) }, { replace: true, state: location.state }); }, [moved, section.id, anchor, navigate, location.state]); // 切分区回到顶部;带节锚点时对齐到该节(有字段时对齐到字段并聚焦)。节内容多是异步加载的(上游列表、能力清单), // 上方内容变高会把目标挤下去,所以在 2.5 秒内反复校正,直到用户自己滚动为止。 // biome-ignore lint/correctness/useExhaustiveDependencies: location.key 让重复点同一锚点也能再跳一次 useEffect(() => { if (moved) return; if (!anchor) { window.scrollTo({ top: 0 }); return; } const started = performance.now(); let stopped = false; let focused = false; let timer = 0; const stop = () => { stopped = true; }; const events = ["wheel", "touchstart", "keydown", "pointerdown"] as const; for (const e of events) window.addEventListener(e, stop, { passive: true }); const align = () => { if (stopped) return; const fieldEl = field ? document.getElementById(`f-${field}`) : null; if (fieldEl) { const r = fieldEl.getBoundingClientRect(); if (r.top < 80 || r.bottom > window.innerHeight - 40) fieldEl.scrollIntoView({ block: "center" }); if (!focused) { focused = true; fieldEl.focus({ preventScroll: true }); } } else { const el = document.getElementById(anchorId(section.id, anchor)); if (el) { const margin = Number.parseFloat(getComputedStyle(el).scrollMarginTop) || 0; if (Math.abs(el.getBoundingClientRect().top - margin) > 2) el.scrollIntoView({ block: "start" }); } } if (performance.now() - started < 2500) timer = window.setTimeout(align, 120); }; timer = window.setTimeout(align, 0); return () => { window.clearTimeout(timer); for (const e of events) window.removeEventListener(e, stop); }; }, [section.id, anchor, field, moved, location.key]); return ( <>
{/* 窄屏:搜索 + 分区下拉 + 节横排 */}

{section.description}

{section.anchors.length > 1 ? ( ) : null}
{/* 宽屏:搜索 + 左侧分组导航 */}
{/* 分区页头:标题 + 一句说明;窄屏由上方下拉承担,标题只留给读屏 */}

{section.label}

{section.description}

{section.render()}
); }