|
|
@@ -66,7 +66,7 @@ async function optionalRuntimeFile(runId: string, filename: string): Promise<Run
|
|
|
}
|
|
|
|
|
|
export function RunDashboardPage({ runId }: { runId: string }) {
|
|
|
- const [activeStage, setActiveStage] = useState("source");
|
|
|
+ const [activeStage, setActiveStage] = useState("walk");
|
|
|
const [data, setData] = useState<DashboardData | null>(null);
|
|
|
const [runtimeFile, setRuntimeFile] = useState<RuntimeFileResponse | null>(null);
|
|
|
const [drawer, setDrawer] = useState<DrawerContent>(null);
|
|
|
@@ -141,12 +141,8 @@ export function RunDashboardPage({ runId }: { runId: string }) {
|
|
|
|
|
|
{data ? (
|
|
|
<section className="detail-panel business-page">
|
|
|
- <StageNavigationCards
|
|
|
- dashboard={data.dashboard}
|
|
|
- stages={data.dashboard.stage_conclusions}
|
|
|
- activeStage={activeStage}
|
|
|
- onSelect={setActiveStage}
|
|
|
- />
|
|
|
+ <FunnelStrip dashboard={data.dashboard} stages={data.dashboard.stage_conclusions} onSelect={setActiveStage} />
|
|
|
+ <PanelNav dashboard={data.dashboard} stages={data.dashboard.stage_conclusions} activeStage={activeStage} onSelect={setActiveStage} />
|
|
|
<TimelineSummaryStrip runId={runId} timeline={data.timeline} contentItems={data.contentItems} />
|
|
|
<StagePanel
|
|
|
activeStage={activeStage}
|
|
|
@@ -211,7 +207,53 @@ function TimelineSummaryStrip({
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-function StageNavigationCards({
|
|
|
+// 激进合一:被折叠的 query/platform/judge 漏斗段点击后跳「发现旅程」。
|
|
|
+function funnelTarget(stageId: string): string {
|
|
|
+ if (stageId === "query" || stageId === "platform" || stageId === "judge") return "walk";
|
|
|
+ return stageId;
|
|
|
+}
|
|
|
+
|
|
|
+// 第一层:漏斗总览条(7 段,只读 + 点击跳对应面板)。复用 stage_conclusions 与 stageCount,零新增取数。
|
|
|
+function FunnelStrip({
|
|
|
+ dashboard,
|
|
|
+ stages,
|
|
|
+ onSelect
|
|
|
+}: {
|
|
|
+ dashboard: DashboardResponse;
|
|
|
+ stages: StageConclusion[];
|
|
|
+ onSelect: (stageId: string) => void;
|
|
|
+}) {
|
|
|
+ return (
|
|
|
+ <div className="funnel-strip" aria-label="运行漏斗总览">
|
|
|
+ {stages.map((stage, i) => {
|
|
|
+ const count = stageCount(dashboard, stage.stage_id);
|
|
|
+ return (
|
|
|
+ <span className="funnel-seg-wrap" key={stage.stage_id}>
|
|
|
+ {i > 0 ? <ChevronRight size={14} className="funnel-arrow" /> : null}
|
|
|
+ <button type="button" className={`funnel-seg ${stage.status}`} onClick={() => onSelect(funnelTarget(stage.stage_id))}>
|
|
|
+ <span className="funnel-label">
|
|
|
+ <span className={`funnel-dot ${stage.status}`} />
|
|
|
+ {stage.label}
|
|
|
+ </span>
|
|
|
+ <strong className="funnel-count">{count !== null ? count : "—"}</strong>
|
|
|
+ </button>
|
|
|
+ </span>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+// 第二层:5 面板导航(前端常量驱动)。发现旅程卡副行挂 Query/内容/判定 三计数。
|
|
|
+const PANELS: Array<{ id: string; label: string }> = [
|
|
|
+ { id: "source", label: "需求" },
|
|
|
+ { id: "walk", label: "发现旅程" },
|
|
|
+ { id: "asset", label: "内容资产" },
|
|
|
+ { id: "learning", label: "学习复盘" },
|
|
|
+ { id: "config", label: "策略配置" }
|
|
|
+];
|
|
|
+
|
|
|
+function PanelNav({
|
|
|
dashboard,
|
|
|
stages,
|
|
|
activeStage,
|
|
|
@@ -222,27 +264,34 @@ function StageNavigationCards({
|
|
|
activeStage: string;
|
|
|
onSelect: (stageId: string) => void;
|
|
|
}) {
|
|
|
+ const statusOf = (id: string) => stages.find((s) => s.stage_id === id)?.status || "";
|
|
|
+ const headlineOf = (id: string) => stages.find((s) => s.stage_id === id)?.headline || "";
|
|
|
return (
|
|
|
- <nav className="stage-nav-grid" aria-label="运行阶段">
|
|
|
- {stages.map((stage) => (
|
|
|
- <button
|
|
|
- className={`stage-nav-card ${stage.status} ${activeStage === stage.stage_id ? "active" : ""}`}
|
|
|
- key={stage.stage_id}
|
|
|
- onClick={() => onSelect(stage.stage_id)}
|
|
|
- type="button"
|
|
|
- >
|
|
|
- <div className="stage-nav-top">
|
|
|
- <strong>{stage.label}</strong>
|
|
|
- {stageCount(dashboard, stage.stage_id) !== null ? (
|
|
|
- <span className="stage-nav-count">{stageCount(dashboard, stage.stage_id)}</span>
|
|
|
- ) : null}
|
|
|
- </div>
|
|
|
- <div className="stage-nav-body">
|
|
|
- <span>{stage.headline}</span>
|
|
|
- <small>{stage.metric}</small>
|
|
|
- </div>
|
|
|
- </button>
|
|
|
- ))}
|
|
|
+ <nav className="panel-nav" aria-label="工作台面板">
|
|
|
+ {PANELS.map((p) => {
|
|
|
+ const active = activeStage === p.id;
|
|
|
+ return (
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ key={p.id}
|
|
|
+ className={`panel-nav-card ${statusOf(p.id)} ${active ? "active" : ""}`}
|
|
|
+ onClick={() => onSelect(p.id)}
|
|
|
+ >
|
|
|
+ <div className="panel-nav-top">
|
|
|
+ <strong>{p.label}</strong>
|
|
|
+ </div>
|
|
|
+ {p.id === "walk" ? (
|
|
|
+ <div className="panel-nav-sub">
|
|
|
+ Query {stageCount(dashboard, "query")} · 内容 {stageCount(dashboard, "platform")} · 判定 {stageCount(dashboard, "judge")}
|
|
|
+ </div>
|
|
|
+ ) : p.id === "config" ? (
|
|
|
+ <div className="panel-nav-sub">当前生效的全局策略配置</div>
|
|
|
+ ) : (
|
|
|
+ <div className="panel-nav-sub">{headlineOf(p.id)}</div>
|
|
|
+ )}
|
|
|
+ </button>
|
|
|
+ );
|
|
|
+ })}
|
|
|
</nav>
|
|
|
);
|
|
|
}
|