feat(web): subscriptions overview groups, coverage matrix view and v2 form (FE-5)
- Overview: kind tabs, five groups with server counts, card/list views, slot badges, progress (unknown total = "?"), filters in URL, batch actions - Detail /console/subscriptions/🆔 season tabs, episode x version-target matrix with cell sheet (manual fill, plan link), drag slot order, auto-fill switch and fill budget, refresh coverage - Create/edit dialog (basic/advanced): work picker via global search, season/range/total, sites, rule groups, include/exclude, templates, upgrade strategy, downloader/save path; 409 links to existing subscription Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
4deaaab6fc
commit
b7a7fd3958
13 files changed
+3241
-726
No files matched your search
@@ -0,0 +1,156 @@
|
||||
import type { CoverageCell, Subscription, SubscriptionSlotSummary } from "@umt/contracts";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
cellKind,
|
||||
draftFromSubscription,
|
||||
draftToParams,
|
||||
emptyDraft,
|
||||
foldNames,
|
||||
moveItem,
|
||||
progressText,
|
||||
seasonLabel,
|
||||
slotBadge,
|
||||
upgradeOf,
|
||||
} from "./model.ts";
|
||||
|
||||
const slot = (x: Partial<SubscriptionSlotSummary>): SubscriptionSlotSummary => ({
|
||||
slotId: "s",
|
||||
name: "4K",
|
||||
primary: true,
|
||||
status: "empty",
|
||||
have: 0,
|
||||
inflight: 0,
|
||||
missing: 0,
|
||||
total: null,
|
||||
upgrading: false,
|
||||
minPri: null,
|
||||
targetPri: null,
|
||||
autoFill: true,
|
||||
ruleGroupIds: [],
|
||||
...x,
|
||||
});
|
||||
const cell = (x: Partial<CoverageCell>): CoverageCell => ({
|
||||
unitKey: "s1e1",
|
||||
state: "missing",
|
||||
assetId: null,
|
||||
quality: null,
|
||||
pri: null,
|
||||
planId: null,
|
||||
planStage: null,
|
||||
reason: null,
|
||||
upgradable: false,
|
||||
deferred: false,
|
||||
...x,
|
||||
});
|
||||
|
||||
describe("订阅总览文案", () => {
|
||||
it("进度总未知显示 ?", () => {
|
||||
expect(progressText({ have: 3, total: null, inflight: 0 })).toBe("3/?");
|
||||
expect(progressText({ have: 3, total: 10, inflight: 1 })).toBe("3/10");
|
||||
});
|
||||
it("季标签:特别篇 / 接续 / 电影", () => {
|
||||
expect(seasonLabel({ mediaKind: "series", seasonNumber: 0, seasonScope: "current_season" })).toBe("特别篇");
|
||||
expect(seasonLabel({ mediaKind: "series", seasonNumber: 2, seasonScope: "following_seasons" })).toBe("S2 起");
|
||||
expect(seasonLabel({ mediaKind: "series", seasonNumber: 1, seasonScope: "current_season" })).toBe("S1");
|
||||
expect(seasonLabel({ mediaKind: "movie", seasonNumber: null, seasonScope: "current_season" })).toBeNull();
|
||||
});
|
||||
it("版本徽标按 洗版 → 在途 → 缺 → 已齐 判定", () => {
|
||||
expect(slotBadge(slot({ upgrading: true, minPri: 97, targetPri: 100, missing: 2 })).label).toBe("洗版中 97→100");
|
||||
expect(slotBadge(slot({ inflight: 2, missing: 3 }))).toEqual({ label: "在途 2", tone: "neutral" });
|
||||
expect(slotBadge(slot({ missing: 3 })).tone).toBe("destructive");
|
||||
expect(slotBadge(slot({ have: 10, total: 10 }))).toEqual({ label: "已齐", tone: "success" });
|
||||
expect(slotBadge(slot({})).label).toBe("未开始");
|
||||
});
|
||||
it("名称折叠", () => {
|
||||
expect(foldNames([])).toBe("全部站点");
|
||||
expect(foldNames(["a", "b", "c", "d"])).toBe("a、b +2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("覆盖矩阵格", () => {
|
||||
it("state + 附带字段 → 显示类别", () => {
|
||||
expect(cellKind(cell({ state: "have" }))).toBe("have");
|
||||
expect(cellKind(cell({ state: "have", upgradable: true }))).toBe("upgradable");
|
||||
expect(cellKind(cell({ state: "missing", reason: "suspected" }))).toBe("suspected");
|
||||
expect(cellKind(cell({ state: "missing", deferred: true }))).toBe("deferred");
|
||||
expect(cellKind(cell({ state: "skipped", reason: "not_aired" }))).toBe("not_aired");
|
||||
expect(cellKind(cell({ state: "skipped", reason: "out_of_range" }))).toBe("skipped");
|
||||
expect(cellKind(cell({ state: "inflight" }))).toBe("inflight");
|
||||
});
|
||||
it("拖动排序", () => {
|
||||
expect(moveItem(["a", "b", "c"], 2, 0)).toEqual(["c", "a", "b"]);
|
||||
expect(moveItem(["a", "b"], 0, 5)).toEqual(["a", "b"]);
|
||||
});
|
||||
it("升级策略取自版本目标", () => {
|
||||
expect(upgradeOf([{ upgradeThreshold: {} }], "series")).toBe("off");
|
||||
expect(upgradeOf([{ upgradeThreshold: { enabled: true, mode: "full_pack" } }], "series")).toBe("full_pack");
|
||||
expect(upgradeOf([{ upgradeThreshold: { enabled: true } }], "movie")).toBe("movie");
|
||||
});
|
||||
});
|
||||
|
||||
describe("表单 ↔ params", () => {
|
||||
it("保留未知键,空值不写,只在改动时带 best_version / total_episode", () => {
|
||||
const d = {
|
||||
...emptyDraft("series"),
|
||||
siteIds: ["x"],
|
||||
episodeStart: "3",
|
||||
include: "中字\n\n 4K ",
|
||||
autoFill: "off" as const,
|
||||
unitsPerDay: "2",
|
||||
gbPerDay: "1.5",
|
||||
total: "12",
|
||||
upgrade: "full_pack" as const,
|
||||
};
|
||||
const { params, errors } = draftToParams(
|
||||
d,
|
||||
{ custom: 1, keywords: ["旧"] },
|
||||
{ upgradeChanged: true, totalChanged: true },
|
||||
);
|
||||
expect(errors).toEqual({});
|
||||
expect(params).toEqual({
|
||||
custom: 1,
|
||||
siteIds: ["x"],
|
||||
include: ["中字", "4K"],
|
||||
episodeRange: { start: 3 },
|
||||
total_episode: 12,
|
||||
manual_total_episode: false,
|
||||
autoFill: "off",
|
||||
fillBudget: { unitsPerDay: 2, bytesPerDay: Math.round(1.5 * 1024 ** 3) },
|
||||
best_version: true,
|
||||
best_version_full: true,
|
||||
});
|
||||
const same = draftToParams(d, {}, { upgradeChanged: false, totalChanged: false }).params;
|
||||
expect(same.best_version).toBeUndefined();
|
||||
expect(same.total_episode).toBeUndefined();
|
||||
});
|
||||
it("校验:结束集 < 开始集", () => {
|
||||
const { errors } = draftToParams(
|
||||
{ ...emptyDraft("series"), episodeStart: "5", episodeEnd: "2" },
|
||||
{},
|
||||
{ upgradeChanged: false, totalChanged: false },
|
||||
);
|
||||
expect(errors.episodeEnd).toBeTruthy();
|
||||
});
|
||||
it("从订阅回填", () => {
|
||||
const sub = {
|
||||
mediaKind: "series",
|
||||
seasonUnitId: null,
|
||||
seasonScope: "current_season",
|
||||
mode: "continuous",
|
||||
priority: "tracking",
|
||||
policyRevisionId: null,
|
||||
params: {
|
||||
episodeRange: { start: 2 },
|
||||
autoFill: "on",
|
||||
fillBudget: { bytesPerDay: 2 * 1024 ** 3 },
|
||||
keywords: ["a", "b"],
|
||||
},
|
||||
} as unknown as Subscription;
|
||||
const d = draftFromSubscription(sub, "per_unit");
|
||||
expect(d.episodeStart).toBe("2");
|
||||
expect(d.autoFill).toBe("on");
|
||||
expect(d.gbPerDay).toBe("2");
|
||||
expect(d.keywords).toBe("a\nb");
|
||||
expect(d.upgrade).toBe("per_unit");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user