From 1555d5344b15c14fcddaaebb9318bcbd3149d1cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Mon, 27 Jul 2026 21:30:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=968BF=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E9=93=BE=E6=B5=81=E7=A8=8B=E5=9B=BE=E4=B8=8E=E6=B7=B1=E8=89=B2?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/EightBfRoundtablePage.tsx | 31 +- .../src/eightbf-roundtable/styles/step1.css | 6 + .../src/eightbf-roundtable/styles/step3.css | 45 +++ .../components/PositionBusinessChainFlow.tsx | 305 ++++++++++++++++++ .../pages/RoundtablePlanningPage.tsx | 21 +- 5 files changed, 395 insertions(+), 13 deletions(-) create mode 100644 frontend-web/src/position-roundtable/components/PositionBusinessChainFlow.tsx diff --git a/frontend-web/src/eightbf-roundtable/pages/EightBfRoundtablePage.tsx b/frontend-web/src/eightbf-roundtable/pages/EightBfRoundtablePage.tsx index a1e2a1b3..3737d960 100644 --- a/frontend-web/src/eightbf-roundtable/pages/EightBfRoundtablePage.tsx +++ b/frontend-web/src/eightbf-roundtable/pages/EightBfRoundtablePage.tsx @@ -1,7 +1,8 @@ import { useCallback, useEffect, useMemo, useState } from "react"; -import { Check, Loader2, Save, Settings2, X } from "lucide-react"; +import { Loader2, Save, Settings2, X } from "lucide-react"; import { getStoredAuth } from "@/core/auth"; +import { PositionBusinessChainFlow } from "@/position-roundtable/components/PositionBusinessChainFlow"; import RoundtablePlanningPage from "@/roundtable-planning/pages/RoundtablePlanningPage"; import { listChains } from "@/roundtable-planning/api/chains"; import type { BusinessChain } from "@/roundtable-planning/lib/business-chain"; @@ -34,6 +35,10 @@ export default function EightBfRoundtablePage({ compactChrome = false }: { compa const [selectedChainId, setSelectedChainId] = useState(""); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); + const configuredChain = useMemo( + () => chains.find((chain) => chain.id === mapping?.chainId) ?? null, + [chains, mapping?.chainId], + ); const loadConfiguration = useCallback(async () => { setIsLoading(true); @@ -61,6 +66,10 @@ export default function EightBfRoundtablePage({ compactChrome = false }: { compa }, [loadConfiguration]); const handleSave = useCallback(async () => { + if (!isAdmin) { + setError("仅管理员可以配置 8BF 业务链条"); + return; + } const selected = chains.find((chain) => chain.id === selectedChainId); if (!selected) { setError("请选择一条已发布的业务链"); @@ -81,16 +90,10 @@ export default function EightBfRoundtablePage({ compactChrome = false }: { compa } finally { setIsSaving(false); } - }, [chains, selectedChainId]); + }, [chains, isAdmin, selectedChainId]); const toolbarSlot = ( <> -
- 8BF 业务链 - - {isLoading ? "加载中…" : mapping?.chainTitle || "待管理员配置"} - -
{isAdmin && ( + )} + + + {!chain ? ( +
+ {loading ? "正在加载业务链条…" : "管理员尚未配置 8BF 业务链条"} +
+ ) : ( + <> + + + + + {chain.title} · 业务链条流程图 + +
+ +
+
+
+ + )} + + ); +} + +function BusinessChainCanvas({ + chain, + expanded = false, + className, +}: { + chain: BusinessChain; + expanded?: boolean; + className?: string; +}) { + const [view, setView] = useState({ tx: 0, ty: 0 }); + const [panning, setPanning] = useState(false); + const dragRef = useRef<{ x: number; y: number; tx: number; ty: number } | null>(null); + const stages = getFlowStages(chain); + const nodeWidth = expanded ? 250 : FLOW_NODE_WIDTH; + const nodeGap = expanded ? 18 : FLOW_NODE_GAP; + let colorIndex = 0; + + const endPan = (event: React.PointerEvent) => { + if (!dragRef.current) return; + dragRef.current = null; + setPanning(false); + try { + event.currentTarget.releasePointerCapture(event.pointerId); + } catch { + // The browser may have already released the pointer capture. + } + }; + + return ( +
setView({ tx: 0, ty: 0 })} + onPointerDown={(event) => { + if (event.button !== 0) return; + dragRef.current = { x: event.clientX, y: event.clientY, tx: view.tx, ty: view.ty }; + setPanning(true); + event.currentTarget.setPointerCapture(event.pointerId); + }} + onPointerMove={(event) => { + const drag = dragRef.current; + if (!drag) return; + setView({ tx: drag.tx + event.clientX - drag.x, ty: drag.ty + event.clientY - drag.y }); + }} + onPointerUp={endPan} + onPointerCancel={endPan} + > +
+ + {stages.map((stage, stageIndex) => { + const stageWidth = Math.max( + nodeWidth, + stage.seats.length * (nodeWidth + nodeGap) - nodeGap, + ); + return ( +
+ +
+ {stage.seats.map((seat) => { + const accent = NODE_COLORS[(colorIndex++ + 1) % NODE_COLORS.length]!; + return ( + + ); + })} +
+
+ ); + })} + + + + +
+
+ ); +} + +function FlowConnector({ + branches, + width, + nodeWidth, + nodeGap, +}: { + branches: number; + width: number; + nodeWidth: number; + nodeGap: number; +}) { + const connectorStyle = { backgroundColor: "rgba(148,163,184,.88)" }; + if (branches <= 1) { + return ( + + + + ); + } + + const nodeCenter = nodeWidth / 2; + return ( +
+ + + {Array.from({ length: branches }).map((_, index) => ( + + + + ))} +
+ ); +} + +function FlowNode({ + title, + description, + accent, + completed = false, + width, +}: { + title: string; + description: string; + accent: string; + completed?: boolean; + width: number; +}) { + const StatusIcon = completed ? CheckCircle2 : Circle; + return ( +
+ + + +

+ {title} +

+

+ {description} +

+
+ ); +} diff --git a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx index b0193548..95a00491 100644 --- a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx +++ b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx @@ -296,6 +296,8 @@ export default function RoundtablePlanningPage({ storageTaskId, /** 供独立业务页在原页头部插入自己的操作入口。 */ toolbarSlot, + /** 独立业务页在第一步左上角替换默认业务链选择区的内容。 */ + entryRailBusinessChainSlot, /** 独立业务页的根节点样式类,避免修改原会商样式。 */ rootClassName, }: { @@ -304,6 +306,7 @@ export default function RoundtablePlanningPage({ lockBusinessChain?: boolean; storageTaskId?: string | null; toolbarSlot?: ReactNode; + entryRailBusinessChainSlot?: ReactNode; rootClassName?: string; }) { const { theme } = useTheme(); @@ -2170,6 +2173,7 @@ export default function RoundtablePlanningPage({ entryRailBusinessCode={prefillRef.current?.businessCode ?? null} lockedChainId={lockedChainId} lockBusinessChain={lockBusinessChain} + entryRailBusinessChainSlot={entryRailBusinessChainSlot} /> {/* MIDDLE VIEWPORT */} @@ -2729,6 +2733,8 @@ interface LeftSidebarProps { lockBusinessChain?: boolean; /** 独立页面锁定显示的业务链。 */ lockedChainId?: string | null; + /** 第一阶段替换默认业务链选择区的自定义内容。 */ + entryRailBusinessChainSlot?: ReactNode; } function Step2SandboxSidebarSync({ @@ -2779,6 +2785,7 @@ function LeftSidebar({ entryRailBusinessCode, lockBusinessChain, lockedChainId, + entryRailBusinessChainSlot, }: LeftSidebarProps) { // 整片脱敏:左侧面板(任务概览/参与角色/人工参与等)的所有文本——标题、动态内容、 // 嵌套子组件文本一并脱敏。消息列表与沙箱是另外的容器,不挂此 Hook,保持原文。 @@ -2921,6 +2928,7 @@ function LeftSidebar({ businessCode={entryRailBusinessCode ?? null} lockBusinessChain={lockBusinessChain} lockedChainId={lockedChainId ?? null} + businessChainSlot={entryRailBusinessChainSlot} /> )} @@ -2975,6 +2983,7 @@ function RoundtableEntryRail({ businessCode, lockedChainId, lockBusinessChain = false, + businessChainSlot, }: { selectedChainId: string | null; onSelectChain: (chain: BusinessChain) => void; @@ -2989,6 +2998,7 @@ function RoundtableEntryRail({ /** 固定业务链时,左侧只展示这条链的流程图及放大入口。 */ lockedChainId?: string | null; lockBusinessChain?: boolean; + businessChainSlot?: ReactNode; }) { const [chains, setChains] = useState([]); const [chainSectionExpanded, setChainSectionExpanded] = useState(true); @@ -3035,7 +3045,7 @@ function RoundtableEntryRail({ useEffect(() => { // 深链接模式不展示业务链选择 → 不必拉公共链条。 - if (deeplink) { + if (deeplink || lockBusinessChain) { void refresh(); return; } @@ -3068,7 +3078,12 @@ function RoundtableEntryRail({ }; return ( -
+
{/* 深链接(taskId)模式:左上角展示「当前任务」(rwfx/3qfx)或「当前行动」(xdfx),替代业务链选择。 */} {deeplink ? ( isXdfx ? ( @@ -3110,6 +3125,8 @@ function RoundtableEntryRail({
) + ) : lockBusinessChain && businessChainSlot ? ( + businessChainSlot ) : ( /* 业务链(公共列表,内部滚动) */