|
|
@@ -17,6 +17,9 @@ export type VideoNode = {
|
|
|
key: string;
|
|
|
id: string;
|
|
|
searchQueryId: string;
|
|
|
+ batchKind: string;
|
|
|
+ pageNumber: number;
|
|
|
+ rankInPage: number;
|
|
|
video: RawRecord;
|
|
|
title: string;
|
|
|
author: string;
|
|
|
@@ -28,7 +31,8 @@ export type VideoNode = {
|
|
|
};
|
|
|
export type Forest = { rootVideos: VideoNode[]; queryRoutes: RouteNode[] };
|
|
|
|
|
|
-type QueryGroup = { id: string; label: string; videos: VideoNode[] };
|
|
|
+type BatchGroup = { kind: string; label: string; gate: string; videos: VideoNode[] };
|
|
|
+type QueryGroup = { id: string; label: string; batches: BatchGroup[] };
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// component
|
|
|
@@ -101,17 +105,28 @@ export function WalkTree({
|
|
|
<span className="wt-root-chip">搜索词</span>
|
|
|
<strong>{group.label}</strong>
|
|
|
</div>
|
|
|
- {group.videos.map((node, index) => (
|
|
|
- <VideoRow
|
|
|
- node={node}
|
|
|
- depth={0}
|
|
|
- index={index}
|
|
|
- runId={runId}
|
|
|
- expanded={expanded}
|
|
|
- onToggle={toggle}
|
|
|
- onOpenDrawer={setDrawer}
|
|
|
- key={node.key}
|
|
|
- />
|
|
|
+ {group.batches.map((batch) => (
|
|
|
+ <div className="wt-batch" key={batch.kind || "未分批"}>
|
|
|
+ <div className="wt-batch-head">
|
|
|
+ <span className="wt-batch-label">{batch.label}</span>
|
|
|
+ <span className="wt-batch-count">{batch.videos.length} 条</span>
|
|
|
+ {batch.gate ? <span className="wt-batch-gate">· {batch.gate}</span> : null}
|
|
|
+ </div>
|
|
|
+ <div className="wt-batch-videos">
|
|
|
+ {batch.videos.map((node) => (
|
|
|
+ <VideoRow
|
|
|
+ node={node}
|
|
|
+ depth={0}
|
|
|
+ hideRootIndex
|
|
|
+ runId={runId}
|
|
|
+ expanded={expanded}
|
|
|
+ onToggle={toggle}
|
|
|
+ onOpenDrawer={setDrawer}
|
|
|
+ key={node.key}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
))}
|
|
|
</div>
|
|
|
))
|
|
|
@@ -151,6 +166,7 @@ function VideoRow({
|
|
|
node,
|
|
|
depth,
|
|
|
index,
|
|
|
+ hideRootIndex,
|
|
|
runId,
|
|
|
expanded,
|
|
|
onToggle,
|
|
|
@@ -159,6 +175,7 @@ function VideoRow({
|
|
|
node: VideoNode;
|
|
|
depth: number;
|
|
|
index?: number;
|
|
|
+ hideRootIndex?: boolean;
|
|
|
runId: string;
|
|
|
expanded: Set<string>;
|
|
|
onToggle: (key: string) => void;
|
|
|
@@ -175,7 +192,9 @@ function VideoRow({
|
|
|
<Toggle open={open} disabled={!hasRoutes} onClick={() => onToggle(node.key)} />
|
|
|
<button type="button" className="wt-row-main" onClick={() => onOpenDrawer(videoDrawer(node))}>
|
|
|
<span className={`wt-dot ${tone}`} />
|
|
|
- <span className="wt-kicker">{depth === 0 ? `起点 ${(index ?? 0) + 1}` : "带回"}</span>
|
|
|
+ {depth === 0 && hideRootIndex ? null : (
|
|
|
+ <span className="wt-kicker">{depth === 0 ? `起点 ${(index ?? 0) + 1}` : "带回"}</span>
|
|
|
+ )}
|
|
|
<span className="wt-title">{node.title}</span>
|
|
|
<span className="wt-author">{node.author}</span>
|
|
|
{node.score ? <span className="wt-score">{node.score}</span> : null}
|
|
|
@@ -363,6 +382,9 @@ export function buildForest(actions: RawRecord[]): Forest {
|
|
|
key,
|
|
|
id,
|
|
|
searchQueryId: text(video.search_query_id, ""),
|
|
|
+ batchKind: text(video.progressive_batch_kind, ""),
|
|
|
+ pageNumber: Number(video.progressive_page_number) || 0,
|
|
|
+ rankInPage: Number(video.progressive_item_rank_in_page) || 0,
|
|
|
video,
|
|
|
title: cleanTitle(text(video.title, "无标题视频")),
|
|
|
author: text(video.author, "未知作者"),
|
|
|
@@ -394,10 +416,54 @@ function groupRoots(rootVideos: VideoNode[], queryLabels: Record<string, string>
|
|
|
return [...map.entries()].map(([id, videos]) => ({
|
|
|
id,
|
|
|
label: queryLabels[id] || (id ? id : "其它来源"),
|
|
|
- videos
|
|
|
+ batches: groupBatches(videos)
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
+function groupBatches(videos: VideoNode[]): BatchGroup[] {
|
|
|
+ const map = new Map<string, VideoNode[]>();
|
|
|
+ videos.forEach((node) => {
|
|
|
+ const kind = node.batchKind || "";
|
|
|
+ if (!map.has(kind)) map.set(kind, []);
|
|
|
+ map.get(kind)!.push(node);
|
|
|
+ });
|
|
|
+ return [...map.entries()]
|
|
|
+ .map(([kind, items]) => ({
|
|
|
+ kind,
|
|
|
+ label: batchLabel(kind),
|
|
|
+ gate: batchGate(kind),
|
|
|
+ videos: items.slice().sort((a, b) => a.rankInPage - b.rankInPage),
|
|
|
+ sortKey: batchSortKey(kind)
|
|
|
+ }))
|
|
|
+ .sort((a, b) => a.sortKey - b.sortKey)
|
|
|
+ .map(({ sortKey: _sortKey, ...rest }) => rest);
|
|
|
+}
|
|
|
+
|
|
|
+function batchSortKey(kind: string): number {
|
|
|
+ if (kind === "initial_top3") return 10;
|
|
|
+ if (kind === "page_remainder") return 11;
|
|
|
+ const page = /^page_(\d+)$/.exec(kind);
|
|
|
+ if (page) return Number(page[1]) * 10;
|
|
|
+ if (kind === "author_works") return 9000;
|
|
|
+ return 9999;
|
|
|
+}
|
|
|
+
|
|
|
+function batchLabel(kind: string): string {
|
|
|
+ if (kind === "initial_top3") return "前 3 条";
|
|
|
+ if (kind === "page_remainder") return "同页剩余";
|
|
|
+ const page = /^page_(\d+)$/.exec(kind);
|
|
|
+ if (page) return `第 ${Number(page[1])} 页`;
|
|
|
+ if (kind === "author_works") return "作者作品";
|
|
|
+ return "未分批";
|
|
|
+}
|
|
|
+
|
|
|
+function batchGate(kind: string): string {
|
|
|
+ if (kind === "initial_top3") return "总是看";
|
|
|
+ if (kind === "page_remainder") return "命中才看";
|
|
|
+ if (/^page_\d+$/.test(kind)) return "命中才翻";
|
|
|
+ return "";
|
|
|
+}
|
|
|
+
|
|
|
function collectKeys(forest: Forest): { videoKeys: Set<string>; routeKeys: Set<string>; allKeys: Set<string> } {
|
|
|
const videoKeys = new Set<string>();
|
|
|
const routeKeys = new Set<string>();
|