Files
deerflow-code/frontend-web/src/pages/PageRoutes.tsx
T
董宏凯andCursor 37517c2778 fix: 嵌套深链改为按路由去 chrome,顶层不再清除共用 embed.session
避免全量父页清掉老版内层深链仍依赖的 embed.session;深链问答/会商/大屏绘制由 isHostNestedSurfacePath 控制 chrome,并抽取对话历史 Tab 共享行组件。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 13:01:46 +08:00

190 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo, useState } from "react";
import { Navigate, Route, Routes } from "react-router-dom";
import { PageSidebar } from "@/components/page-sidebar";
import { PageSidebarDark } from "@/components/page-sidebar-dark";
import { CompactToolbox } from "@/components/workspace/compact-toolbox";
import { isChatsIframeWorkspacePath } from "@/core/page-layout/chats-iframe-route";
import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled";
import { isWujieEnv } from "@/core/embed/wujie";
import {
isEmbedActive,
isChromeHidden,
isHeaderHidden,
isHideChromeActive,
readEmbedParam,
readHideHeaderParam,
readHideChromeParam,
} from "@/core/embed/embed-session";
import { isHostNestedSurfacePath } from "@/core/page-layout/host-nested-surface";
import { useTheme } from "@/contexts/ThemeContext";
import { useLocation } from "react-router-dom";
import { useActiveThirdLevel } from "@/core/page-layout/use-sidebar-menu";
import { useLayoutMode } from "@/core/system-settings/hooks";
import { env } from "@/env";
import WorkspaceRoutes from "./WorkspaceRoutes";
import StrategyRoutes from "./StrategyRoutes";
import Header from "@/strategy-components/Header";
import OpenCanvasRoutes from "@/open-canvas/routes/OpenCanvasRoutes";
import RoundtableRoutes from "./RoundtableRoutes";
import NotebookRoutes from "./NotebookRoutes";
import { RoundtableJobNotifier } from "@/roundtable-planning/components/RoundtableJobNotifier";
function PageLayout() {
const [collapsed, setCollapsed] = useState(false);
const forceCollapsedSidebar = env.FORCE_COLLAPSED_SIDEBAR;
const sidebarEnabled = useMemo(() => readSidebarEnabled(), []);
const location = useLocation();
const { theme } = useTheme();
const isDark = theme === "dark";
// 浅色非嵌入:侧边栏为新版「二级导航图标卡片」窄栏(96px);wujie 嵌入仍走旧版手风琴(64/256px)。
const inWujie = useMemo(() => isWujieEnv(), []);
const lightNarrowSidebar = !isDark && !inWujie;
// 浅色窄栏在右侧展开「三级列」时整体变宽(96→236px),主内容左内边距随之过渡。
const thirdLevel = useActiveThirdLevel();
const lightExpanded = lightNarrowSidebar && thirdLevel.length > 0;
// 界面模式(外观设置):用户显式选「全量/简洁」时以其为准;否则跟随管理员系统默认
// (default_layout_mode);两者都没有才回退到会话级 sidebarEnabled(外部门户/yUserId
// 登录时置位)这一旧默认。这样无论是否走门户登录,「全量↔简洁」都能真正控制顶栏+侧边栏。
const [layoutMode] = useLayoutMode();
const chromeByPreference =
forceCollapsedSidebar
? false
: layoutMode === "full"
? true
: layoutMode === "compact"
? false
: sidebarEnabled;
// iframe 嵌入模式(外部系统 `&embed=1`,按标签页 sessionStorage):去掉全局导航栏 + 侧边栏,
// 只留问答/会商工作区本体。按 location 重算以跟随页内导航;非嵌入标签页恒为 false,不受影响。
const embedActive = useMemo(
() => isEmbedActive() || readEmbedParam(location.search) === "on",
[location.pathname, location.search],
);
// `?hideChrome=1`:仅去全局导航栏 + 侧边栏(不强制主题/不隔离鉴权),用于「大屏绘制」等想要
// 干净全屏的页面。与 embed 一样按标签页 sessionStorage 持久化,跨登录跳转/页内导航存活;非置位
// 标签页恒为 false,不受影响。按 location 重算以跟随页内导航。
const chromeHidden = useMemo(
() => isChromeHidden() || readHideChromeParam(location.search) === "on",
[location.pathname, location.search],
);
const hideChromeActive = useMemo(
() => isHideChromeActive() || readHideChromeParam(location.search) === "on",
[location.pathname, location.search],
);
// `?hideHeader=1` only removes the global top navigation. Unlike
// `hideChrome=1`, PageSidebar remains available in the full layout.
const headerHidden = useMemo(
() => isHeaderHidden() || readHideHeaderParam(location.search) === "on",
[location.pathname, location.search],
);
// `chatMode=full` is the embed-friendly standard chat view: hide the global
// Header/PageSidebar when `hideChrome=1`, but keep WorkspaceSidebar so users
// can still pick an agent and access their common agents/history.
const fullChatMode = useMemo(() => {
const mode = new URLSearchParams(location.search)
.get("chatMode")
?.trim()
.toLowerCase();
return (
mode === "full" &&
location.pathname.startsWith("/page/workspace/chats/")
);
}, [location.pathname, location.search]);
// 深链嵌套页(单智能体问答 / 多智能体会商 / 大屏绘制)只给宿主 iframe 套用,
// 路径上就不带全局 Header / PageSidebar,不靠改写共用 embed.session。
const hostNestedSurface = isHostNestedSurfacePath(location.pathname);
const pageSidebarAllowed =
chromeByPreference &&
!isChatsIframeWorkspacePath(location.pathname) &&
!embedActive &&
!chromeHidden &&
!hostNestedSurface;
const compactToolboxAllowed =
!chromeByPreference &&
!embedActive &&
!chromeHidden &&
!hostNestedSurface &&
!isChatsIframeWorkspacePath(location.pathname);
// h-full(100%)而非 h-screen(100vh):配合 globals.css 的 html/body/#root height:100%
// 高度链,根布局跟随容器高度。全屏下 100% 解析到视口,与 h-screen 等价;无界等嵌套
// 下容器不占满整屏时,高度才能正确收缩而非锁死整屏。
return (
<RoundtableJobNotifier>
{compactToolboxAllowed && <CompactToolbox />}
<div className="h-full flex flex-col bg-muted/20">
{pageSidebarAllowed && !headerHidden && <Header collapsed={collapsed} />}
<div className="flex flex-1 overflow-hidden">
{pageSidebarAllowed && (
isDark ? (
<PageSidebarDark
collapsed={collapsed}
onToggleCollapsed={() => setCollapsed((prev) => !prev)}
headerHidden={headerHidden}
/>
) : (
<PageSidebar
collapsed={collapsed}
onToggleCollapsed={() => setCollapsed((prev) => !prev)}
headerHidden={headerHidden}
/>
)
)}
<main
className={`flex-1 overflow-hidden ${
!pageSidebarAllowed
? ""
: isDark
// 深色 sidebar:展开 118px(10 + 98 + 10 gap),收起时主内容不留左内边距。
? (collapsed ? "pl-0" : "pl-[118px]")
: lightNarrowSidebar
// 浅色新版窄栏:展开三级列时 96→236px,主内容左内边距随之过渡
? `transition-[padding] duration-300 ${lightExpanded ? "pl-[236px]" : "pl-[96px]"}`
// 浅色旧版手风琴(wujie 嵌入):pl-16 ↔ pl-64 平滑收展
: `transition-[padding] duration-300 ${collapsed ? "pl-16" : "pl-64"}`
}`}
>
<Routes>
<Route index element={<Navigate to="workspace" replace />} />
<Route
path="workspace/*"
element={
<WorkspaceRoutes
pageSidebarCollapsed={collapsed}
pageSidebarHidden={!pageSidebarAllowed}
workspaceSidebarHidden={hideChromeActive && !fullChatMode}
headerHidden={!pageSidebarAllowed || headerHidden}
// Full-chat embedding has no global PageSidebar, but it
// still needs the full GeneralQaSidebar (common agents and
// conversation history) inside the workspace.
compactWorkspaceSidebar={pageSidebarAllowed || fullChatMode}
compactActionsHidden={hideChromeActive || headerHidden}
pageSidebarDark={isDark}
forceWorkspaceSidebarCollapsed={
forceCollapsedSidebar && !fullChatMode
}
/>
}
/>
<Route path="strategy/*" element={<StrategyRoutes />} />
<Route path="canvas/*" element={<OpenCanvasRoutes />} />
<Route path="roundtable/*" element={<RoundtableRoutes />} />
<Route path="notebook/*" element={<NotebookRoutes />} />
</Routes>
</main>
</div>
</div>
</RoundtableJobNotifier>
);
}
export default function PageRoutes() {
return (
<Routes>
<Route path="*" element={<PageLayout />} />
</Routes>
);
}