import type { CoverageCell, SubscriptionCoverage } from "@umt/contracts"; import { Clock, Download, ExternalLink, Wand2 } from "lucide-react"; import { useState } from "react"; import { Link } from "react-router"; import { toast } from "sonner"; import { useFillSlot } from "~/api/subscriptions.ts"; import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx"; import { KvList } from "~/components/console/kv-list.tsx"; import { consolePath } from "~/components/console/nav.ts"; import { Button } from "~/components/ui/button.tsx"; import { errorMessage } from "~/lib/api.ts"; import { cn } from "~/lib/utils.ts"; import { CELL_LABEL, type CellKind, type CoverageColumn, cellKind, columnLabel, PLAN_STAGE, SKIP_REASON, seasonSlices, seasonTitle, } from "./model.ts"; // 覆盖矩阵(设计 §2.1):列 = 集(电影 = 内容版本),行 = 收录版本,首行"库内任一版本"。 // 底色无彩,只有状态用状态色:已入库 success、缺失 destructive、可升级/延后 warning;在途用斜纹。 type Row = SubscriptionCoverage["rows"][number]; /** 格样式:每种都配文字(悬浮 + 侧栏),不只靠颜色 */ const CELL_CLS: Record = { have: "border-success/50 bg-success/70", upgradable: "border-success/50 bg-success/70 after:absolute after:top-0 after:right-0 after:h-0 after:w-0 after:border-t-[7px] after:border-l-[7px] after:border-t-warning after:border-l-transparent", inflight: "border-foreground/40 bg-[repeating-linear-gradient(135deg,color-mix(in_oklab,var(--color-foreground)_45%,transparent)_0_2px,transparent_2px_5px)]", deferred: "border-warning/70 bg-transparent", missing: "border-destructive/70 bg-transparent", suspected: "border-dashed border-destructive/60 bg-transparent", not_aired: "border-transparent bg-foreground/[0.10]", skipped: "border-transparent bg-foreground/[0.04]", }; const LEGEND: CellKind[] = [ "have", "upgradable", "inflight", "missing", "suspected", "deferred", "not_aired", "skipped", ]; function CellSwatch({ k, className }: { k: CellKind; className?: string }) { return ( {k === "deferred" ? : null} ); } export function CoverageLegend() { return ( ); } function cellTitle(row: Row, col: CoverageColumn, c: CoverageCell) { const k = cellKind(c); const parts = [`${row.name} · ${columnLabel(col)}`, CELL_LABEL[k]]; if (c.quality?.label) parts.push(c.quality.label); if (c.pri !== null) parts.push(`pri ${c.pri}`); if (c.planStage) parts.push(PLAN_STAGE[c.planStage] ?? c.planStage); if (c.reason && SKIP_REASON[c.reason]) parts.push(SKIP_REASON[c.reason]); return parts.join(" · "); } export type SelectedCell = { row: Row; col: CoverageColumn; cell: CoverageCell; season: string }; export function CoverageMatrix({ cov, seasonIndex, onSelect, }: { cov: SubscriptionCoverage; seasonIndex: number; onSelect: (s: SelectedCell) => void; }) { const season = cov.seasons[seasonIndex]; const slice = seasonSlices(cov)[seasonIndex]; if (!season || !slice) return null; const cols = season.columns; if (cols.length === 0) return (

这一季还没有集信息。设置总集数后按集显示。

); const sTitle = seasonTitle(season, cov.mediaKind); const wide = cov.mediaKind === "movie"; return (
{cols.map((c) => ( ))} {cov.rows.map((row) => { const cells = row.cells.slice(slice.start, slice.end); const any = row.slotId === null; return ( {cells.map((c, i) => { const col = cols[i]!; const k = cellKind(c); return ( ); })} ); })}
收录版本 {wide ? columnLabel(c) : (c.episode ?? columnLabel(c))}
{row.name} {row.primary ? 主 : null}
); } /** 点格打开:资产 / 质量 / pri / 计划 / 原因 / 可升级 / 延后 → 手动补全、跳下载计划 */ export function CellSheet({ sel, subscriptionId, mediaEntityId, canFill, onClose, }: { sel: SelectedCell | null; subscriptionId: string; mediaEntityId: string | null; canFill: boolean; onClose: () => void; }) { const fill = useFillSlot(); const [done, setDone] = useState(null); const c = sel?.cell; const k = c ? cellKind(c) : null; const slotId = sel?.row.slotId ?? null; const fillable = !!slotId && !!c && (c.state === "missing" || c.upgradable); const doFill = () => slotId && c && fill.mutate( { id: subscriptionId, slotId, unitKeys: [c.unitKey] }, { onSuccess: (r) => { setDone(c.unitKey); toast.success(r.deduplicated ? "补全任务已在排队" : "已开始补全这一集"); }, onError: (e) => toast.error(errorMessage(e, "补全失败")), }, ); return ( { if (!o) { setDone(null); onClose(); } }} title={sel ? `${sel.season} · ${columnLabel(sel.col)}` : ""} description={sel ? `${sel.row.name}${sel.row.slotId === null ? "(作品全部文件)" : ""}` : null} footer={ sel && c ? ( <> {c.planId ? ( ) : null} {fillable ? ( ) : null} ) : null } > {sel && c && k ? ( <>
{CELL_LABEL[k]}
{c.assetId.slice(0, 8)} ) : ( {c.assetId} ) ) : null, }, { label: "下载计划", value: c.planId ? `${PLAN_STAGE[c.planStage ?? ""] ?? c.planStage ?? "在途"} · ${c.planId.slice(0, 8)}` : null, }, { label: "原因", value: c.reason ? (SKIP_REASON[c.reason] ?? c.reason) : null }, { label: "延后", value: c.deferred ? "有资源在等待重评(预算或自动补全关闭)" : null }, ]} /> {fillable ? (

手动补全不受自动补全开关和补全预算限制,仍受全局每日预算与缓存卷阈值约束。

) : null} {sel.row.slotId === null && c.state === "missing" ? (

这一行汇总全部版本。要补全,点具体收录版本那一行的格。

) : null} ) : null}
); }