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.description}