Files
UHD_Media_Tool/apps/web/app/components/settings/search-index.ts
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

129 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { SETTINGS_SECTIONS, type SettingsSectionId } from "@umt/contracts";
import { BUDGET_CARD, IDENTITY_CARD } from "./acquire.tsx";
import { ADVANCED_CARD, BUDGET_CARD as AGENT_BUDGET_CARD, INVESTIGATE_CARD, RUNTIME_CARD } from "./agent.tsx";
import { WEBSEARCH_CARD } from "./agent-websearch.tsx";
import { IMAGES_CARD, TMDB_CACHE_CARD, TTL_CARD } from "./cache.tsx";
import { CACHE_CARD, SEEDING_CARD } from "./downloaders.tsx";
import { LANGUAGE_CARD, TMDB_CARD } from "./metadata.tsx";
import { ORGANIZE_CARD, SCRAPE_CARD } from "./organize.tsx";
import type { CardSpec } from "./settings-card.tsx";
import { DEFAULTS_CARD, NETWORK_CARD } from "./sites.tsx";
import { STORAGE_BUDGET_CARD, STORAGE_RETENTION_CARD, WORKER_LANES_CARD } from "./system.tsx";
// 设置页搜索:分区名、节名、节的别名(contracts keywords)、各节表单字段名。
// 选中字段时页面滚到该节并聚焦字段(控件 id = `f-<字段名>`)。
/** 表单卡片 → 所在分区;新增卡片时在这里登记,测试会核对锚点存在且字段都在页面上 */
export const SETTINGS_CARDS: readonly [SettingsSectionId, CardSpec][] = [
["downloaders", CACHE_CARD],
["downloaders", SEEDING_CARD],
["sites", DEFAULTS_CARD],
["sites", NETWORK_CARD],
["metadata", TMDB_CARD],
["metadata", LANGUAGE_CARD],
["acquire", BUDGET_CARD],
["acquire", IDENTITY_CARD],
["organize", ORGANIZE_CARD],
["organize", SCRAPE_CARD],
["agent", AGENT_BUDGET_CARD],
["agent", INVESTIGATE_CARD],
["agent", RUNTIME_CARD],
["agent", WEBSEARCH_CARD],
["agent", ADVANCED_CARD],
["system", WORKER_LANES_CARD],
["system", STORAGE_BUDGET_CARD],
["system", STORAGE_RETENTION_CARD],
["cache", TMDB_CACHE_CARD],
["cache", IMAGES_CARD],
["cache", TTL_CARD],
];
export type SettingsSearchHit = {
key: string;
/** 命中的名字(字段名 / 节名 / 分区名) */
label: string;
section: SettingsSectionId;
anchor: string | null;
/** 表单字段名(控件 id 为 `f-<field>`) */
field: string | null;
/** 位置:分区 › 节 */
where: string;
};
type Entry = SettingsSearchHit & { terms: string[] };
function buildIndex(): Entry[] {
const out: Entry[] = [];
for (const s of SETTINGS_SECTIONS) {
out.push({
key: s.id,
label: s.label,
section: s.id,
anchor: null,
field: null,
where: s.description,
terms: [s.label],
});
for (const a of s.anchors) {
out.push({
key: `${s.id}/${a.id}`,
label: a.label,
section: s.id,
anchor: a.id,
field: null,
where: s.label,
terms: [a.label, ...(a.keywords ?? [])],
});
}
}
for (const [sid, card] of SETTINGS_CARDS) {
const s = SETTINGS_SECTIONS.find((x) => x.id === sid);
const a = s?.anchors.find((x) => x.id === card.anchor);
for (const f of card.fields) {
out.push({
key: `${sid}/${card.anchor}/${f.name}`,
label: f.label,
section: sid,
anchor: card.anchor,
field: f.name,
where: `${s?.label ?? sid} › ${a?.label ?? card.title}`,
terms: [f.label],
});
}
}
return out;
}
let INDEX: Entry[] | null = null;
const norm = (v: string) => v.toLowerCase().replace(/[\s()()「」::、,,]/g, "");
/** 按名字找设置:名字开头命中排前,其次包含;同一位置只留一条 */
export function searchSettings(query: string, limit = 8): SettingsSearchHit[] {
const q = norm(query);
if (!q) return [];
INDEX ??= buildIndex();
const scored: [number, Entry][] = [];
for (const e of INDEX) {
let best = -1;
for (const [i, t] of e.terms.entries()) {
const n = norm(t);
const score = n === q ? 3 : n.startsWith(q) ? 2 : n.includes(q) ? 1 : -1;
// 别名命中比名字本身命中低一档
const adj = score < 0 ? -1 : i === 0 ? score + 0.5 : score;
best = Math.max(best, adj);
}
if (best >= 0) scored.push([best, e]);
}
scored.sort((a, b) => b[0] - a[0]);
const seen = new Set<string>();
const hits: SettingsSearchHit[] = [];
for (const [, { terms: _t, ...hit }] of scored) {
if (seen.has(hit.key)) continue;
seen.add(hit.key);
hits.push(hit);
if (hits.length >= limit) break;
}
return hits;
}