import type { EpisodeMappingPreview, EpisodeSuggestion } from "@umt/contracts";
import { Wand2 } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { useLocation, useSearchParams } from "react-router";
import { useEntityWords, useEpisodeMappingPreview } from "~/api/episode-mapping.ts";
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { WordsEditor } from "~/components/identify-words/words-editor.tsx";
import { Chip, Panel } from "~/components/library/parts.tsx";
import { Button } from "~/components/ui/button.tsx";
import { EpisodeIssuesList } from "./issues.tsx";
import { groupMethodsText, lineFromSearch, MAPPING_ANCHOR } from "./model.ts";
import { PreviewTable } from "./preview-table.tsx";
// 作品页「集数映射」分区:季表摘要、作品识别词(与规则页「识别词」同一个编辑器、同一份数据)、
// 按发布组的统计与建议、换算不了的资源,以及近期资源按已保存的识别词的判定。
function SeasonSummary({ p }: { p: EpisodeMappingPreview["seasons"] }) {
const main = p.seasons.filter((s) => s.season >= 1);
if (!main.length) return
还没有季表,刷新元数据后出现。
;
return (
{main.map((s) => (
第 {s.season} 季 · {s.total != null ? `${s.total} 集` : "总集数未知"}
{s.season === p.latestSeason ? " · 最新" : ""}
))}
{p.ended ? "已完结" : "未完结"}
{p.structure ? ` · 按剧集组「${p.structure.name}」分季` : ""}
);
}
const pad = (n: number) => String(n).padStart(2, "0");
/** 建议规则写成作品识别词的一行 */
export function suggestionLine(s: EpisodeSuggestion): string {
const scope = `${s.season == null ? "*" : `S${pad(s.season)}`}${s.releaseGroup ? ` @${s.releaseGroup}` : ""}`;
return `${scope} >> S${pad(s.targetSeason)} ${s.expression}`;
}
function GroupStats({
groups,
onUseSuggestion,
}: {
groups: EpisodeMappingPreview["groups"];
onUseSuggestion: (line: string) => void;
}) {
if (!groups.length) return null;
return (
{groups.map((g) => (
-
{groupMethodsText(g)}
{g.pending ? 待确认 {g.pending} 条 : null}
{g.suggestion ? (
) : null}
))}
);
}
export function EpisodeMappingPanel({ entityId }: { entityId: string }) {
const [sp] = useSearchParams();
const location = useLocation();
const words = useEntityWords(entityId);
const preview = useEpisodeMappingPreview(entityId, {});
const [draft, setDraft] = useState(null);
const ref = useRef(null);
const saved = words.data?.block.text ?? "";
const text = draft ?? saved;
/** 把一行追加进草稿(建议、集数待确认的「修改」、从别处跳来带的一行) */
const append = (line: string) => {
setDraft((cur) => {
const base = (cur ?? saved).replace(/\n*$/, "");
if (base.split("\n").some((l) => l.trim() === line.trim())) return cur ?? saved;
return `${base ? `${base}\n` : ""}${line}\n`;
});
ref.current?.scrollIntoView({ block: "start", behavior: "smooth" });
};
// 从集数待确认「修改」跳来:把那一行放进草稿并滚到这里
const fromLink = lineFromSearch(sp);
// biome-ignore lint/correctness/useExhaustiveDependencies: 识别词加载好之后只处理一次地址里带的行
useEffect(() => {
if (fromLink && words.isSuccess) append(fromLink);
if (location.hash === `#${MAPPING_ANCHOR}`) ref.current?.scrollIntoView({ block: "start" });
}, [fromLink, words.isSuccess, location.hash]);
// 保存后草稿归零(编辑器已拿到新版本)
useEffect(() => {
if (draft !== null && draft === saved) setDraft(null);
}, [draft, saved]);
return (
{preview.data ? : null}
{words.isPending ? (
) : words.isError ? (
words.refetch()} />
) : (
)}
{preview.data ? : null}
{preview.isPending ? (
) : preview.isError ? (
preview.refetch()} />
) : (
)}
);
}