feat(web): merge search filters into one row and tidy episode versions (POLISH-1)

- H&R becomes a quick-filter facet (yes/no/unknown, with counts) on search
  results and the site resources tab; drop the old free/H&R selects, keep
  size range and sort on the same row (mobile: drawer), single clear button
- legacy free/hr URL params migrate into quick filters; stored selections
  keep the same localStorage key
- search timing and query terms move into TechInfo
- library episode versions: click the whole row to expand, versions listed
  as sibling rows, details only under the selected version, raw values in
  TechInfo
- slot comparison gaps get reason codes (matchGapText in contracts
  reasons.ts): "可换成 2160p", "缺 HDR 版本"

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 22:06:32 +08:00
1 parent 12bc4664b6
commit 71b129d85a
19 files changed
+668 -290

No files matched your search

+19 -1
View File
@@ -145,6 +145,7 @@ describe("与版本目标比对", () => {
state: "missing",
assetId: null,
reasons: [],
codes: [],
});
});
@@ -158,12 +159,29 @@ describe("与版本目标比对", () => {
expect(m.state).toBe("upgradeable");
expect(m.assetId).toBe("bd");
expect(m.reasons.join(" ")).toMatch(/1080p/);
expect(m.codes).toEqual(["version_resolution:2160p"]);
});
it("差距原因码:HDR / 字幕 / 来源带目标参数", () => {
const hdr: SlotSpec = { id: "h", name: "h", hard: { hdr: { anyOf: ["DV", "HDR10"] } } };
expect(matchVersions(hdr, [v("bd", BD_FILE)], { isEpisode: true }).codes).toEqual(["version_hdr"]);
const dvOnly: SlotSpec = { id: "d", name: "d", hard: { hdr: { anyOf: ["DV"] } } };
expect(matchVersions(dvOnly, [v("bd", BD_FILE)], { isEpisode: true }).codes).toEqual(["version_hdr:DV"]);
const remux: SlotSpec = { id: "r", name: "r", hard: { sources: ["REMUX", "BluRay-Disc"] } };
expect(matchVersions(remux, [v("dv", DV_FILE, PROBE_DV)], { isEpisode: true }).codes).toEqual([
"version_source:REMUX",
]);
});
it("达标但升级策略还有余量 = upgradeable;到顶 = satisfied", () => {
const up: SlotSpec = { id: "s", name: "x", hard: {}, upgrade: { enabled: true } };
const web = matchVersions(up, [v("dv", DV_FILE, PROBE_DV)], { isEpisode: true });
expect(web).toMatchObject({ state: "upgradeable", assetId: "dv", reasons: ["来源还能升级"] });
expect(web).toMatchObject({
state: "upgradeable",
assetId: "dv",
reasons: ["来源还能升级"],
codes: ["version_source_upgrade"],
});
const capped: SlotSpec = { ...up, upgrade: { enabled: true, ceilingTier: 2 } };
expect(matchVersions(capped, [v("dv", DV_FILE, PROBE_DV)], { isEpisode: true }).state).toBe("satisfied");
const onlyWeb: SlotSpec = { ...up, hard: { sources: ["WEB-DL"] } };
@@ -100,6 +100,19 @@ describe("release-facets", () => {
expect(ids(run({ promotion: ["2x"] }))).toEqual(["4"]);
});
it("H&R 三态分面:缺省按未知;未知档只在有资源时列出", () => {
const hrItems = [mk("y", { hr: true }), mk("n", { hr: false }), mk("n2", { hr: false }), mk("u")];
const r = applyFacets(hrItems, (x) => x, {}, { now: NOW });
expect(r.facets.hr).toEqual([
{ value: "yes", label: "有", count: 1 },
{ value: "no", label: "无", count: 2 },
{ value: "unknown", label: "未知", count: 1 },
]);
expect(ids(applyFacets(hrItems, (x) => x, { hr: ["no", "unknown"] }, { now: NOW }))).toEqual(["n", "n2", "u"]);
const known = applyFacets([mk("y", { hr: true })], (x) => x, {}, { now: NOW });
expect(known.facets.hr.map((o) => o.value)).toEqual(["yes", "no"]);
});
it("已选但没有结果的值仍列出(计数 0)", () => {
const r = run({ releaseGroup: ["NoSuch"], site: ["zzz"] });
expect(r.items).toEqual([]);