避免全量父页清掉老版内层深链仍依赖的 embed.session;深链问答/会商/大屏绘制由 isHostNestedSurfacePath 控制 chrome,并抽取对话历史 Tab 共享行组件。 Co-authored-by: Cursor <cursoragent@cursor.com>
190 lines
8.5 KiB
TypeScript
190 lines
8.5 KiB
TypeScript
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>
|
||
);
|
||
}
|