- 左栏三组分区 + 当前分区的节;窄屏为搜索框、分组下拉和节的横排胶囊 - 搜索框按分区、节、节别名和字段名定位,选中字段时滚到该字段并聚焦 - 旧 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
129 lines
4.2 KiB
TypeScript
129 lines
4.2 KiB
TypeScript
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;
|
||
}
|