From ce78cbf936c9b7a689910489b153e5b6ce1dd8f6 Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Thu, 18 Jun 2026 21:09:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend-web/src/core/auth/task-purpose.ts | 13 +- .../pages/RoundtablePlanningPage.tsx | 2 +- .../task-components/TaskJumpBar.tsx | 188 ++++++++++++++++-- .../task-components/TaskPurposeDialog.tsx | 11 +- 4 files changed, 188 insertions(+), 26 deletions(-) diff --git a/frontend-web/src/core/auth/task-purpose.ts b/frontend-web/src/core/auth/task-purpose.ts index 90685a0b..5fe3c5f2 100644 --- a/frontend-web/src/core/auth/task-purpose.ts +++ b/frontend-web/src/core/auth/task-purpose.ts @@ -166,14 +166,18 @@ export function derivePurposeLevels(tree: PurposeTreeNode[]): PurposeLevel[] { * 把归一化后的目的树转换成 Step 3 流程图组件(FlowGraphPanel)认识的 FlowGraphData。 * * - 逐层深度优先遍历,给每个节点分配稳定 id(`p1/p2/…`,按遍历顺序); - * - 父→子建一条边(from=父 id);多个根目的各自成一棵子树(画布支持多根); + * - 父→子建一条边(from=父 id); * - 节点主文本(`text`)作 label,层级名(`label`)作 group(画布顶部小标签), * 推荐理由并进 detail(点节点弹窗里看)。 + * - 传入 ``rootLabel`` 时,在最顶层加一个「任务」根节点,所有「目的」挂到它下面 + * (与 B 系统给的「任务 → 目的 → 行为体 → …」流程一致,单一根节点);不传则保持 + * 旧的「多个目的各自成根」森林(向后兼容)。 * 容错沿用 PurposeTreeNode(normalizeNode 已兜底字段/层级变动),这里不再二次判断。 */ export function purposeTreeToFlowGraph( tree: PurposeTreeNode[], taskId?: string, + rootLabel?: string, ): FlowGraphData { const nodes: FlowGraphNode[] = []; const edges: FlowGraphEdge[] = []; @@ -189,7 +193,12 @@ export function purposeTreeToFlowGraph( if (parentId) edges.push({ from: parentId, to: id }); node.children?.forEach((child) => walk(child, id)); }; - tree.forEach((root) => walk(root, null)); + // 任务为单一根节点(仅在有目的数据且传入 rootLabel 时):每个「目的」根挂到它下面。 + const rootId = rootLabel && rootLabel.trim() && tree.length > 0 ? "task-root" : null; + if (rootId) { + nodes.push({ id: rootId, label: rootLabel!.trim(), type: "start", group: "任务" }); + } + tree.forEach((root) => walk(root, rootId)); return { title: taskId ? `任务目的(任务 ${taskId})` : "任务目的", nodes, diff --git a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx index 85e9bfc2..23fc50fa 100644 --- a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx +++ b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx @@ -3627,7 +3627,7 @@ function PurposeFlowPreview({ > - 策划流程 + 原任务已有结果 {levels.map((lv, i) => { const m = meta[lv.type]; diff --git a/frontend-web/src/strategy-components/components/task-components/TaskJumpBar.tsx b/frontend-web/src/strategy-components/components/task-components/TaskJumpBar.tsx index fa6d86b5..13b5050f 100644 --- a/frontend-web/src/strategy-components/components/task-components/TaskJumpBar.tsx +++ b/frontend-web/src/strategy-components/components/task-components/TaskJumpBar.tsx @@ -9,10 +9,19 @@ import { useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { ArrowRight, ExternalLink, Maximize2 } from "lucide-react"; +import { + ArrowRight, + ExternalLink, + FileSearch, + GitBranch, + Maximize2, + Network, + Workflow, +} from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; import { readTaskContext } from "@/core/auth/task-purpose"; import { DEFAULT_TASK_DEEPLINK_CONFIG, @@ -23,6 +32,7 @@ import { buttonsForBusiness, resolveTaskButtons, runTaskButton, + type TaskButton, type TaskBusiness, } from "@/core/auth/task-buttons"; import { useTaskButtons } from "@/strategy-components/api/task-buttons"; @@ -81,31 +91,171 @@ export function TaskJumpBar({ }); }; + const nextButtons = jumpButtons.filter(isNextTaskButton); + const menuButtons = jumpButtons.filter((btn) => !isNextTaskButton(btn)); + return ( -
- {jumpButtons.map((btn) => ( - - ))} + <> + {menuButtons.length > 0 && } + {nextButtons.length > 0 && ( +
+ {nextButtons.map((btn) => ( + + ))} +
+ )} + + ); +} + +function isNextTaskButton(btn: TaskButton) { + return btn.id.toLowerCase().includes("next") || btn.label.trim() === "下一步"; +} + +function TaskJumpButton({ + button, + onRun, +}: { + button: TaskButton; + onRun: (button: TaskButton) => void; +}) { + return ( + + ); +} + +function TaskSideJumpMenu({ + buttons, + onRun, +}: { + buttons: TaskButton[]; + onRun: (button: TaskButton) => void; +}) { + const [open, setOpen] = useState(false); + const menuWidth = 168; + const collapsedWidth = 64; + + return ( +
setOpen(true)} + onMouseLeave={() => setOpen(false)} + onFocus={() => setOpen(true)} + onBlur={(event) => { + if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false); + }} + > +
+
+ {buttons.map((btn) => { + const tone = getRoundtableMenuTone(btn); + return ( + + ); + })} +
+
); } + +function renderTaskJumpIcon(btn: TaskButton, className: string) { + if (btn.linkType === "url") return ; + if (btn.linkType === "purpose") return ; + return ; +} + +function renderRoundtableMenuIcon(btn: TaskButton) { + const className = "h-4 w-4"; + if (btn.linkType === "purpose") return ; + if (btn.linkType === "url") return ; + if (btn.target === "3qfx") return ; + if (btn.target === "xdfx") return ; + if (btn.target === "rwfx") return ; + return ; +} + +function getRoundtableMenuTone(btn: TaskButton) { + if (btn.linkType === "url") { + return { + item: "hover:text-rose-700 dark:hover:text-rose-100", + icon: "bg-rose-50 text-rose-600 dark:bg-rose-400/10 dark:text-rose-200", + }; + } + if (btn.target === "3qfx") { + return { + item: "hover:text-sky-700 dark:hover:text-sky-100", + icon: "bg-sky-50 text-sky-600 dark:bg-sky-400/10 dark:text-sky-200", + }; + } + if (btn.target === "xdfx") { + return { + item: "hover:text-emerald-700 dark:hover:text-emerald-100", + icon: "bg-emerald-50 text-emerald-600 dark:bg-emerald-400/10 dark:text-emerald-200", + }; + } + if (btn.target === "rwfx") { + return { + item: "hover:text-amber-700 dark:hover:text-amber-100", + icon: "bg-amber-50 text-amber-600 dark:bg-amber-400/10 dark:text-amber-200", + }; + } + return { + item: "hover:text-slate-900 dark:hover:text-white", + icon: "bg-slate-50 text-slate-600 dark:bg-white/10 dark:text-slate-100", + }; +} diff --git a/frontend-web/src/strategy-components/components/task-components/TaskPurposeDialog.tsx b/frontend-web/src/strategy-components/components/task-components/TaskPurposeDialog.tsx index a6b91c7f..732488e3 100644 --- a/frontend-web/src/strategy-components/components/task-components/TaskPurposeDialog.tsx +++ b/frontend-web/src/strategy-components/components/task-components/TaskPurposeDialog.tsx @@ -19,6 +19,8 @@ import { purposeTreeToFlowGraph, type TaskPurposeResult, } from "@/core/auth/task-purpose"; +import { readCopTaskDetail } from "@/core/auth/task-deeplink"; +import { desensitize } from "@/lib/desensitize"; import { FlowGraphPanel } from "@/roundtable-planning/components/FlowGraphPanel"; export function TaskPurposeDialog({ @@ -52,10 +54,11 @@ export function TaskPurposeDialog({ }; }, [open, taskId]); - const flowData = useMemo( - () => purposeTreeToFlowGraph(result?.tree ?? [], taskId), - [result, taskId], - ); + // 任务为流程图的单一根节点:根节点文案用登录深链暂存的任务名称(已脱敏),取不到回退「任务」。 + const flowData = useMemo(() => { + const taskName = desensitize((readCopTaskDetail()?.taskName ?? "").trim()) || "任务"; + return purposeTreeToFlowGraph(result?.tree ?? [], taskId, taskName); + }, [result, taskId]); const hasNodes = flowData.nodes.length > 0;