|
@@ -14,7 +14,8 @@ import {
|
|
|
UserSearch,
|
|
UserSearch,
|
|
|
CornerDownRight,
|
|
CornerDownRight,
|
|
|
ArrowUpLeft,
|
|
ArrowUpLeft,
|
|
|
- Database
|
|
|
|
|
|
|
+ Database,
|
|
|
|
|
+ RotateCw
|
|
|
} from "lucide-react";
|
|
} from "lucide-react";
|
|
|
import { getConfigRulePacks, getRuntimeFile } from "@/lib/api/client";
|
|
import { getConfigRulePacks, getRuntimeFile } from "@/lib/api/client";
|
|
|
import type { ContentItemsResponse, WalkGraphEdge } from "@/lib/api/types";
|
|
import type { ContentItemsResponse, WalkGraphEdge } from "@/lib/api/types";
|
|
@@ -70,6 +71,7 @@ type VideoJourney = {
|
|
|
reason: string;
|
|
reason: string;
|
|
|
score: unknown;
|
|
score: unknown;
|
|
|
downstreamCn: string[];
|
|
downstreamCn: string[];
|
|
|
|
|
+ page?: number; // 这条视频来自该搜索词的第几页(翻页结构化用)
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
function buildVideos(items: AnyRec[], actionsByFrom: Map<string, Set<string>>): VideoJourney[] {
|
|
function buildVideos(items: AnyRec[], actionsByFrom: Map<string, Set<string>>): VideoJourney[] {
|
|
@@ -132,7 +134,7 @@ export function WalkJourney({
|
|
|
const [walkActions, setWalkActions] = useState<AnyRec[] | null>(null);
|
|
const [walkActions, setWalkActions] = useState<AnyRec[] | null>(null);
|
|
|
const [searchQueries, setSearchQueries] = useState<AnyRec[] | null>(null);
|
|
const [searchQueries, setSearchQueries] = useState<AnyRec[] | null>(null);
|
|
|
const [rulePack, setRulePack] = useState<AnyRec | null>(null);
|
|
const [rulePack, setRulePack] = useState<AnyRec | null>(null);
|
|
|
- const [view, setView] = useState<"map" | "tree">("map");
|
|
|
|
|
|
|
+ const [view, setView] = useState<"map" | "tree">("tree");
|
|
|
const [openVideo, setOpenVideo] = useState<string | null>(null);
|
|
const [openVideo, setOpenVideo] = useState<string | null>(null);
|
|
|
const [openBlocks, setOpenBlocks] = useState<Set<string>>(new Set());
|
|
const [openBlocks, setOpenBlocks] = useState<Set<string>>(new Set());
|
|
|
const [highlight, setHighlight] = useState<string | null>(null);
|
|
const [highlight, setHighlight] = useState<string | null>(null);
|
|
@@ -241,12 +243,17 @@ export function WalkJourney({
|
|
|
topMetas.forEach((m) => rootByBlock.set(m.qid, []));
|
|
topMetas.forEach((m) => rootByBlock.set(m.qid, []));
|
|
|
videos.forEach((v) => {
|
|
videos.forEach((v) => {
|
|
|
if (v.prevStep === "author_works") return;
|
|
if (v.prevStep === "author_works") return;
|
|
|
- // 该视频的 queryId 可能是翻页块(q_xxx_page_yyy),归到其父顶层块
|
|
|
|
|
|
|
+ // 该视频的 queryId 可能是翻页块(q_xxx_page_yyy),归到其父顶层块;页码从后缀解析
|
|
|
let qid = v.queryId;
|
|
let qid = v.queryId;
|
|
|
|
|
+ let page = 1;
|
|
|
const vm = metaByQid.get(qid);
|
|
const vm = metaByQid.get(qid);
|
|
|
- if (vm && vm.prevStep === "query_next_page" && vm.parent) qid = vm.parent;
|
|
|
|
|
|
|
+ if (vm && vm.prevStep === "query_next_page" && vm.parent) {
|
|
|
|
|
+ qid = vm.parent;
|
|
|
|
|
+ const mt = v.queryId.match(/_page_0*(\d+)$/);
|
|
|
|
|
+ page = mt ? Number(mt[1]) : 2;
|
|
|
|
|
+ }
|
|
|
if (rootByBlock.has(qid)) {
|
|
if (rootByBlock.has(qid)) {
|
|
|
- rootByBlock.get(qid)!.push(v);
|
|
|
|
|
|
|
+ rootByBlock.get(qid)!.push({ ...v, page });
|
|
|
blockOfVideo[v.id] = qid;
|
|
blockOfVideo[v.id] = qid;
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
@@ -310,7 +317,7 @@ export function WalkJourney({
|
|
|
text: m.text,
|
|
text: m.text,
|
|
|
pageCount: pageCountByParent[m.qid] || 0,
|
|
pageCount: pageCountByParent[m.qid] || 0,
|
|
|
lineage,
|
|
lineage,
|
|
|
- rootVideos: rootByBlock.get(m.qid) || []
|
|
|
|
|
|
|
+ rootVideos: (rootByBlock.get(m.qid) || []).slice().sort((a, b) => (a.page || 1) - (b.page || 1))
|
|
|
};
|
|
};
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -340,12 +347,12 @@ export function WalkJourney({
|
|
|
<div className="walk-journey">
|
|
<div className="walk-journey">
|
|
|
<div className="wj-viewbar">
|
|
<div className="wj-viewbar">
|
|
|
<div className="wj-toggle">
|
|
<div className="wj-toggle">
|
|
|
- <button type="button" className={view === "map" ? "active" : ""} onClick={() => setView("map")}>
|
|
|
|
|
- <Network size={15} /> 地图
|
|
|
|
|
- </button>
|
|
|
|
|
<button type="button" className={view === "tree" ? "active" : ""} onClick={() => setView("tree")}>
|
|
<button type="button" className={view === "tree" ? "active" : ""} onClick={() => setView("tree")}>
|
|
|
<ListTree size={15} /> 路径树
|
|
<ListTree size={15} /> 路径树
|
|
|
</button>
|
|
</button>
|
|
|
|
|
+ <button type="button" className={view === "map" ? "active" : ""} onClick={() => setView("map")}>
|
|
|
|
|
+ <Network size={15} /> 地图
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
<button type="button" className="wj-pack-btn" onClick={() => setPackOpen((v) => !v)}>
|
|
<button type="button" className="wj-pack-btn" onClick={() => setPackOpen((v) => !v)}>
|
|
|
<ShieldCheck size={15} /> 内容发现判断规则包 {packOpen ? <ChevronDown size={15} /> : <ChevronRight size={15} />}
|
|
<ShieldCheck size={15} /> 内容发现判断规则包 {packOpen ? <ChevronDown size={15} /> : <ChevronRight size={15} />}
|
|
@@ -648,10 +655,6 @@ function WalkTree({
|
|
|
<button type="button" className="wj-block-head" onClick={() => toggleBlock(b.qid)}>
|
|
<button type="button" className="wj-block-head" onClick={() => toggleBlock(b.qid)}>
|
|
|
{open ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
|
{open ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
|
|
<span className="wj-seq">{b.seq}</span>
|
|
<span className="wj-seq">{b.seq}</span>
|
|
|
- <Search size={15} />
|
|
|
|
|
- <strong className="wj-block-title">搜索「{b.text}」</strong>
|
|
|
|
|
- {b.pageCount > 0 ? <span className="wj-q-chip">翻页 {b.pageCount} 次</span> : null}
|
|
|
|
|
- <span className="wj-q-count">{b.rootVideos.length} 条视频</span>
|
|
|
|
|
<span
|
|
<span
|
|
|
className={`wj-origin-chip ${b.lineage.kind}${b.lineage.jumpAnchor ? " clickable" : ""}`}
|
|
className={`wj-origin-chip ${b.lineage.kind}${b.lineage.jumpAnchor ? " clickable" : ""}`}
|
|
|
onClick={(e) => {
|
|
onClick={(e) => {
|
|
@@ -663,11 +666,30 @@ function WalkTree({
|
|
|
>
|
|
>
|
|
|
<ArrowUpLeft size={12} /> {b.lineage.chip}
|
|
<ArrowUpLeft size={12} /> {b.lineage.chip}
|
|
|
</span>
|
|
</span>
|
|
|
|
|
+ <Search size={15} />
|
|
|
|
|
+ <strong className="wj-block-title">搜索「{b.text}」</strong>
|
|
|
|
|
+ <span className="wj-q-count">{b.rootVideos.length} 条视频</span>
|
|
|
</button>
|
|
</button>
|
|
|
<div className="wj-crumbs">{b.lineage.crumbs.map(crumb)}</div>
|
|
<div className="wj-crumbs">{b.lineage.crumbs.map(crumb)}</div>
|
|
|
{open ? (
|
|
{open ? (
|
|
|
<div className="wj-block-body">
|
|
<div className="wj-block-body">
|
|
|
- {b.rootVideos.length ? b.rootVideos.map((v) => renderVideo(v, new Set())) : <div className="empty-state">这条搜索没有发现视频。</div>}
|
|
|
|
|
|
|
+ {b.rootVideos.length
|
|
|
|
|
+ ? b.rootVideos.map((v, i) => {
|
|
|
|
|
+ const prevPage = i > 0 ? b.rootVideos[i - 1].page || 1 : 1;
|
|
|
|
|
+ const page = v.page || 1;
|
|
|
|
|
+ const divider = page > 1 && page !== prevPage ? (
|
|
|
|
|
+ <div className="wj-page-divider" key={`pd-${page}`}>
|
|
|
|
|
+ <RotateCw size={13} /> 翻页 → 第 {page} 页
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : null;
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div key={v.id || v.title} className={page > 1 ? "wj-page-group" : undefined}>
|
|
|
|
|
+ {divider}
|
|
|
|
|
+ {renderVideo(v, new Set())}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ })
|
|
|
|
|
+ : <div className="empty-state">这条搜索没有发现视频。</div>}
|
|
|
</div>
|
|
</div>
|
|
|
) : null}
|
|
) : null}
|
|
|
</div>
|
|
</div>
|