Files
UHD_Media_Tool/apps/web/app/components/settings/settings-search.tsx
T
rootandClaude Opus 5.5 8071ad33f9 feat(web): 设置页按新分类重排,加搜索、缓存分区与旧锚点跳转
- 左栏三组分区 + 当前分区的节;窄屏为搜索框、分组下拉和节的横排胶囊
- 搜索框按分区、节、节别名和字段名定位,选中字段时滚到该字段并聚焦
- 旧 hash(#automation/cache、#api-keys、#library/scrape 等)replace 到新位置
- 下载器分区放下载盘水位与保种缺省值,名字与实例抽屉统一(暂停线 / 恢复线 / 保种天数)
- 新增获取、整理与刮削、系统、缓存分区;API Key 并入账户与安全,快捷键移到界面
- 补上缺的入口:最小视频体积、子会话并发与层数、搜索站点;日志保留天数(已废弃)不再显示
- 缓存分区按区列出条数、占用、最近写入,单区清空二次确认;TMDB 与图片缓存并入
- 保存提示统一为「已保存「节名」」;按入库口径把相关文案改成「整理」

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-06 18:01:26 +08:00

117 lines
3.9 KiB
TypeScript

import { Search, X } from "lucide-react";
import { useId, useMemo, useRef, useState } from "react";
import { useNavigate } from "react-router";
import { Input } from "~/components/ui/input.tsx";
import { cn } from "~/lib/utils.ts";
import { type SettingsSearchHit, searchSettings } from "./search-index.ts";
import { settingsHash } from "./sections.tsx";
/** 设置页搜索框:按设置名、节名找位置;选中后跳到该节,字段会被聚焦 */
export function SettingsSearch({ className }: { className?: string }) {
const navigate = useNavigate();
const listId = useId();
const [q, setQ] = useState("");
const [open, setOpen] = useState(false);
const [active, setActive] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
const hits = useMemo(() => searchSettings(q), [q]);
const show = open && q.trim() !== "";
const pick = (h: SettingsSearchHit) => {
setOpen(false);
setQ("");
navigate({ hash: settingsHash(h.section, h.anchor) }, { state: h.field ? { field: h.field } : null });
};
return (
<div className={cn("relative", className)}>
<Search
className="pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground"
aria-hidden
/>
<Input
ref={inputRef}
type="text"
enterKeyHint="search"
role="combobox"
aria-expanded={show}
aria-controls={listId}
aria-autocomplete="list"
aria-label="搜索设置"
placeholder="搜索设置"
value={q}
className="pr-8 pl-8"
onChange={(e) => {
setQ(e.target.value);
setActive(0);
setOpen(true);
}}
onFocus={() => setOpen(true)}
onBlur={() => window.setTimeout(() => setOpen(false), 120)}
onKeyDown={(e) => {
if (e.key === "ArrowDown") {
e.preventDefault();
setActive((i) => Math.min(i + 1, hits.length - 1));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setActive((i) => Math.max(i - 1, 0));
} else if (e.key === "Enter") {
const h = hits[active];
if (h) {
e.preventDefault();
pick(h);
}
} else if (e.key === "Escape") {
setQ("");
setOpen(false);
}
}}
/>
{q ? (
<button
type="button"
aria-label="清除搜索"
className="absolute top-1/2 right-2 -translate-y-1/2 rounded-full p-0.5 text-muted-foreground hover:text-foreground"
onClick={() => {
setQ("");
inputRef.current?.focus();
}}
>
<X className="size-3.5" />
</button>
) : null}
{show ? (
<div
id={listId}
role="listbox"
aria-label="搜索结果"
className="pg-float absolute inset-x-0 top-full z-30 mt-1.5 max-h-80 animate-pg-pop overflow-y-auto rounded-xl p-1"
>
{hits.length === 0 ? (
<p className="px-3 py-2 text-caption text-muted-foreground">没有匹配的设置</p>
) : (
hits.map((h, i) => (
<button
key={h.key}
type="button"
role="option"
aria-selected={i === active}
onMouseDown={(e) => e.preventDefault()}
onMouseEnter={() => setActive(i)}
onClick={() => pick(h)}
className={cn(
"block w-full rounded-lg px-3 py-1.5 text-left transition-colors duration-micro",
i === active ? "bg-foreground/[0.06]" : "",
)}
>
<span className="block truncate text-sm text-foreground">{h.label}</span>
<span className="block truncate text-meta text-muted-foreground">{h.where}</span>
</button>
))
)}
</div>
) : null}
</div>
);
}