Просмотр исходного кода

feat(web-v3): 路径树微调(翻页结构化分隔+来历前置+默认路径树)

- 翻页视频按页码分组,块内插'翻页→第N页'分隔行+淡底,结构化体现
- 来历 chip 从靠右移到搜索名前面(序号→来历→搜索词)
- 视图切换换位:路径树在前,默认进来即路径树

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sam Lee 2 дней назад
Родитель
Сommit
5a340dae07
2 измененных файлов с 46 добавлено и 15 удалено
  1. 10 1
      web/app/globals.css
  2. 36 14
      web/features/runs/WalkJourney.tsx

+ 10 - 1
web/app/globals.css

@@ -1636,7 +1636,7 @@ a {
 .wj-block-head { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; width: 100%; text-align: left; padding: 10px 12px; background: transparent; border: 0; cursor: pointer; }
 .wj-seq { width: 20px; height: 20px; border-radius: 50%; background: #eaf3ff; color: #2360ad; font-size: 11px; font-weight: 600; display: inline-flex; align-items: center; justify-content: center; }
 .wj-block-title { font-size: 14px; color: #172033; }
-.wj-origin-chip { margin-left: auto; font-size: 11.5px; padding: 2px 9px; border-radius: 999px; display: inline-flex; align-items: center; gap: 3px; }
+.wj-origin-chip { font-size: 11.5px; padding: 2px 9px; border-radius: 999px; display: inline-flex; align-items: center; gap: 3px; }
 .wj-origin-chip.seed { background: #eef0f4; color: #5b6678; }
 .wj-origin-chip.llm { background: #eaf3ff; color: #2360ad; }
 .wj-origin-chip.tag { background: #f0eaff; color: #6a4ea3; }
@@ -1659,3 +1659,12 @@ a {
 .wj-act.author { display: block; }
 .wj-act-head { display: flex; align-items: center; gap: 5px; font-size: 12px; color: #155a86; }
 .wj-author-works { border-left: 2px solid #cfe0f0; margin-left: 7px; padding-left: 12px; margin-top: 5px; display: flex; flex-direction: column; gap: 5px; }
+
+/* 翻页结构化:分隔行 + 翻页页视频淡底 */
+.wj-page-divider {
+  display: flex; align-items: center; gap: 6px;
+  margin: 4px 0 2px; padding: 3px 10px;
+  font-size: 11.5px; color: #6b7689;
+  background: #f1f4f9; border-radius: 6px; width: fit-content;
+}
+.wj-page-group > .wj-vid > .wj-vid-head { background: #f7f9fc; border-color: #e4e8f0; }

+ 36 - 14
web/features/runs/WalkJourney.tsx

@@ -14,7 +14,8 @@ import {
   UserSearch,
   CornerDownRight,
   ArrowUpLeft,
-  Database
+  Database,
+  RotateCw
 } from "lucide-react";
 import { getConfigRulePacks, getRuntimeFile } from "@/lib/api/client";
 import type { ContentItemsResponse, WalkGraphEdge } from "@/lib/api/types";
@@ -70,6 +71,7 @@ type VideoJourney = {
   reason: string;
   score: unknown;
   downstreamCn: string[];
+  page?: number; // 这条视频来自该搜索词的第几页(翻页结构化用)
 };
 
 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 [searchQueries, setSearchQueries] = 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 [openBlocks, setOpenBlocks] = useState<Set<string>>(new Set());
   const [highlight, setHighlight] = useState<string | null>(null);
@@ -241,12 +243,17 @@ export function WalkJourney({
     topMetas.forEach((m) => rootByBlock.set(m.qid, []));
     videos.forEach((v) => {
       if (v.prevStep === "author_works") return;
-      // 该视频的 queryId 可能是翻页块(q_xxx_page_yyy),归到其父顶层块
+      // 该视频的 queryId 可能是翻页块(q_xxx_page_yyy),归到其父顶层块;页码从后缀解析
       let qid = v.queryId;
+      let page = 1;
       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)) {
-        rootByBlock.get(qid)!.push(v);
+        rootByBlock.get(qid)!.push({ ...v, page });
         blockOfVideo[v.id] = qid;
       }
     });
@@ -310,7 +317,7 @@ export function WalkJourney({
         text: m.text,
         pageCount: pageCountByParent[m.qid] || 0,
         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="wj-viewbar">
         <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")}>
             <ListTree size={15} /> 路径树
           </button>
+          <button type="button" className={view === "map" ? "active" : ""} onClick={() => setView("map")}>
+            <Network size={15} /> 地图
+          </button>
         </div>
         <button type="button" className="wj-pack-btn" onClick={() => setPackOpen((v) => !v)}>
           <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)}>
                 {open ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
                 <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
                   className={`wj-origin-chip ${b.lineage.kind}${b.lineage.jumpAnchor ? " clickable" : ""}`}
                   onClick={(e) => {
@@ -663,11 +666,30 @@ function WalkTree({
                 >
                   <ArrowUpLeft size={12} /> {b.lineage.chip}
                 </span>
+                <Search size={15} />
+                <strong className="wj-block-title">搜索「{b.text}」</strong>
+                <span className="wj-q-count">{b.rootVideos.length} 条视频</span>
               </button>
               <div className="wj-crumbs">{b.lineage.crumbs.map(crumb)}</div>
               {open ? (
                 <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>
               ) : null}
             </div>