feat(roundtable): 深链会商页引入底部快捷跳转栏(去编辑/弹框查看)

抽出 TaskJumpBar 为独立组件,单智能体问答与多智能体会商共用;会商深链
taskId 模式按业务渲染按钮管理配置,并隐藏「编辑」与「弹框查看」按钮。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
fengxiangdebaoziandClaude Opus 4.8 committed 2026-06-18 19:17:45 +08:00
1 parent bd5623ebc5
commit 3a315798e9
3 files changed
+137 -88

No files matched your search

@@ -66,6 +66,8 @@ import { useModels } from "@/core/models/hooks";
import { getAvatar, Man1Avatar, RobotAvatar } from "@/roundtable-planning/components/Avatars";
import { ImportRoundtableDraftDialog } from "@/components/workspace/roundtable-draft-share-dialogs";
import { TaskPurposeDialog } from "@/strategy-components/components/task-components/TaskPurposeDialog";
import { TaskJumpBar } from "@/strategy-components/components/task-components/TaskJumpBar";
import type { TaskBusiness } from "@/core/auth/task-buttons";
import {
derivePurposeLevels,
fetchTaskPurposeDetail,
@@ -133,6 +135,16 @@ import { listBusinessMappings } from "@/strategy-components/api/business-mapping
import { emitWujieEvent, isWujieEnv, onWujieEvent, readWujieProps } from "@/core/embed/wujie";
import { collectRoundtableDeliverables } from "@/roundtable-planning/lib/roundtable-deliverables";
/**
* 业务码 → 任务工作区业务码(``TaskBusiness``),用于深链 taskId 模式底部「快捷跳转」栏
* (与单智能体问答共用 ``TaskJumpBar``)。与 task-deeplink.ts 的 GOPATH_BUSINESS_CODE 反向一致。
*/
const BUSINESS_CODE_TO_TASK_BUSINESS: Record<string, TaskBusiness> = {
"3Q": "3qfx",
"6BF": "rwfx",
"7BF": "xdfx",
};
/** 从任务页带进来的预填(经 navigation state 传递)。见会商自动化串联(模块 C)。 */
interface RoundtablePrefill {
/** 任务名称(注入 Step 1 首条用户消息)。 */
@@ -1911,6 +1923,17 @@ export default function RoundtablePlanningPage() {
onSaveIntent={handleSaveIntent}
/>
</main>
{/* 深链 taskId 模式:底部「快捷跳转」栏(与单智能体问答共用,按业务取按钮管理配置)。 */}
{prefillRef.current?.taskId &&
prefillRef.current?.businessCode &&
BUSINESS_CODE_TO_TASK_BUSINESS[prefillRef.current.businessCode] && (
<TaskJumpBar
business={BUSINESS_CODE_TO_TASK_BUSINESS[prefillRef.current.businessCode]}
taskId={prefillRef.current.taskId}
roundtable
/>
)}
</div>
);
}
@@ -16,7 +16,7 @@
import { useEffect, useMemo, useState } from "react";
import { Outlet, useLocation, useNavigate, useParams } from "react-router-dom";
import { ArrowRight, ExternalLink, Loader2, Maximize2, MessageSquare, PlusSquare, Trash2 } from "lucide-react";
import { Loader2, MessageSquare, PlusSquare, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
@@ -30,26 +30,14 @@ import {
} from "@/components/ui/dialog";
import { deleteTaskPurpose, readTaskContext } from "@/core/auth/task-purpose";
import { readCopTaskDetail } from "@/core/auth/task-deeplink";
import {
DEFAULT_TASK_DEEPLINK_CONFIG,
fetchTaskDeeplinkConfig,
type TaskDeeplinkConfig,
} from "@/core/auth/task-deeplink-config";
import {
TASK_BUSINESSES,
buttonsForBusiness,
resolveTaskButtons,
runTaskButton,
type TaskBusiness,
} from "@/core/auth/task-buttons";
import { useTaskButtons } from "@/strategy-components/api/task-buttons";
import { TASK_BUSINESSES, type TaskBusiness } from "@/core/auth/task-buttons";
import { useTaskThreads } from "@/core/threads/use-task-threads";
import { titleOfThread } from "@/core/threads/utils";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
import AgentChatPage from "@/pages/AgentChatPage";
import { ChatRuntime } from "@/pages/WorkspaceRoutes";
import { TaskPurposeDialog } from "@/strategy-components/components/task-components/TaskPurposePanel";
import { TaskJumpBar } from "@/strategy-components/components/task-components/TaskJumpBar";
function useChatsBase(taskBase: string): { taskId: string; agentId: string; chatsBase: string } {
const { agent_id } = useParams<{ agent_id: string }>();
@@ -97,79 +85,6 @@ function parseThreadId(pathname: string, chatsBase: string): string | null {
return seg && seg !== "new" ? seg : null;
}
/**
* 底部「快捷跳转」栏:横跨整个工作区底部,按配置顺序渲染该业务启用的跳转按钮(按钮管理里排序)。
* 后端为准,整张表为空才回退种子默认;地址依赖实时深链配置,故把配置拉进 state。见 [[task-buttons]]。
*/
function TaskJumpBar({ business, taskId }: { business: TaskBusiness; taskId: string }) {
const navigate = useNavigate();
const [cfg, setCfg] = useState<TaskDeeplinkConfig>(() => DEFAULT_TASK_DEEPLINK_CONFIG);
useEffect(() => {
let alive = true;
void fetchTaskDeeplinkConfig().then((c) => {
if (alive) setCfg(c);
});
return () => {
alive = false;
};
}, []);
const { data: storedButtons = [] } = useTaskButtons();
const jumpButtons = useMemo(
() => buttonsForBusiness(resolveTaskButtons(storedButtons, cfg), business),
[storedButtons, cfg, business],
);
// 目的树弹框(purpose 类型按钮)由组件层打开。
const [purposeOpen, setPurposeOpen] = useState(false);
// xdfx:深链原始「行动id」(与解析后的任务id 不同才算真行动id),弹框据此换回任务id 再查目的。
const ctxActionId = readTaskContext()?.actionId;
const purposeActionId =
business === "xdfx" && ctxActionId && ctxActionId !== taskId ? ctxActionId : undefined;
if (jumpButtons.length === 0) return null;
const handleRun = (btn: (typeof jumpButtons)[number]) => {
if (btn.linkType === "purpose") {
setPurposeOpen(true);
return;
}
void runTaskButton(btn, {
taskId,
actionId: readTaskContext()?.actionId,
navigate,
onError: (m) => toast.error(m),
});
};
return (
<div className="border-border/70 bg-background/90 flex shrink-0 flex-wrap items-center justify-end gap-3 border-t px-5 py-3 backdrop-blur-xl dark:border-[#1b477e]/70 dark:bg-[#061b35]/85">
{jumpButtons.map((btn) => (
<Button
key={btn.id}
className="h-11 gap-2 border-transparent bg-[var(--color-main)] px-6 text-base text-white shadow-sm hover:bg-[var(--brand-950)]"
onClick={() => handleRun(btn)}
title={desensitize(btn.label)}
>
<span className="truncate">{desensitize(btn.label)}</span>
{btn.linkType === "url" ? (
<ExternalLink className="h-5 w-5 shrink-0" />
) : btn.linkType === "purpose" ? (
<Maximize2 className="h-5 w-5 shrink-0" />
) : (
<ArrowRight className="h-5 w-5 shrink-0" />
)}
</Button>
))}
<TaskPurposeDialog
taskId={taskId}
open={purposeOpen}
onOpenChange={setPurposeOpen}
resolveFromActionId={purposeActionId}
/>
</div>
);
}
/**
* 左侧任务对话列表:顶部「当前任务」卡片 +「新建对话」;下方对话列表(1:N)。
* 跳转按钮已移到工作区底部「快捷跳转」栏(见 ``TaskJumpBar``)。
@@ -0,0 +1,111 @@
/**
* 任务深链的底部「快捷跳转」栏:横跨整个工作区底部,按配置顺序渲染该业务启用的跳转
* 按钮(按钮管理里排序)。单智能体问答(``TaskFxWorkspace``)与多智能体会商
* (``RoundtablePlanningPage`` 深链 taskId 模式)共用。
*
* 后端为准,整张表为空才回退种子默认;地址依赖实时深链配置,故把配置拉进 state。
* 见 [[task-buttons]]。
*/
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { ArrowRight, ExternalLink, Maximize2 } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { readTaskContext } from "@/core/auth/task-purpose";
import {
DEFAULT_TASK_DEEPLINK_CONFIG,
fetchTaskDeeplinkConfig,
type TaskDeeplinkConfig,
} from "@/core/auth/task-deeplink-config";
import {
buttonsForBusiness,
resolveTaskButtons,
runTaskButton,
type TaskBusiness,
} from "@/core/auth/task-buttons";
import { useTaskButtons } from "@/strategy-components/api/task-buttons";
import { desensitize } from "@/lib/desensitize";
import { TaskPurposeDialog } from "@/strategy-components/components/task-components/TaskPurposePanel";
export function TaskJumpBar({
business,
taskId,
roundtable,
}: {
business: TaskBusiness;
taskId: string;
/** 多智能体会商深链模式:不展示「编辑」与「弹框查看」(purpose)按钮。 */
roundtable?: boolean;
}) {
const navigate = useNavigate();
const [cfg, setCfg] = useState<TaskDeeplinkConfig>(() => DEFAULT_TASK_DEEPLINK_CONFIG);
useEffect(() => {
let alive = true;
void fetchTaskDeeplinkConfig().then((c) => {
if (alive) setCfg(c);
});
return () => {
alive = false;
};
}, []);
const { data: storedButtons = [] } = useTaskButtons();
const jumpButtons = useMemo(() => {
const list = buttonsForBusiness(resolveTaskButtons(storedButtons, cfg), business);
// 会商页:去掉「编辑」与「弹框查看」(会商页头部已有「任务目的」入口)。
return roundtable
? list.filter((b) => b.linkType !== "purpose" && b.label.trim() !== "编辑")
: list;
}, [storedButtons, cfg, business, roundtable]);
// 目的树弹框(purpose 类型按钮)由组件层打开。
const [purposeOpen, setPurposeOpen] = useState(false);
// xdfx:深链原始「行动id」(与解析后的任务id 不同才算真行动id),弹框据此换回任务id 再查目的。
const ctxActionId = readTaskContext()?.actionId;
const purposeActionId =
business === "xdfx" && ctxActionId && ctxActionId !== taskId ? ctxActionId : undefined;
if (jumpButtons.length === 0) return null;
const handleRun = (btn: (typeof jumpButtons)[number]) => {
if (btn.linkType === "purpose") {
setPurposeOpen(true);
return;
}
void runTaskButton(btn, {
taskId,
actionId: readTaskContext()?.actionId,
navigate,
onError: (m) => toast.error(m),
});
};
return (
<div className="border-border/70 bg-background/90 flex shrink-0 flex-wrap items-center justify-end gap-3 border-t px-5 py-3 backdrop-blur-xl dark:border-[#1b477e]/70 dark:bg-[#061b35]/85">
{jumpButtons.map((btn) => (
<Button
key={btn.id}
className="h-11 gap-2 border-transparent bg-[var(--color-main)] px-6 text-base text-white shadow-sm hover:bg-[var(--brand-950)]"
onClick={() => handleRun(btn)}
title={desensitize(btn.label)}
>
<span className="truncate">{desensitize(btn.label)}</span>
{btn.linkType === "url" ? (
<ExternalLink className="h-5 w-5 shrink-0" />
) : btn.linkType === "purpose" ? (
<Maximize2 className="h-5 w-5 shrink-0" />
) : (
<ArrowRight className="h-5 w-5 shrink-0" />
)}
</Button>
))}
<TaskPurposeDialog
taskId={taskId}
open={purposeOpen}
onOpenChange={setPurposeOpen}
resolveFromActionId={purposeActionId}
/>
</div>
);
}