diff --git a/frontend-web/src/components/page-sidebar.tsx b/frontend-web/src/components/page-sidebar.tsx index 2d5375ec..eb1f0bd1 100644 --- a/frontend-web/src/components/page-sidebar.tsx +++ b/frontend-web/src/components/page-sidebar.tsx @@ -10,6 +10,7 @@ import { Gauge, ListTree, MousePointerClick, + BriefcaseBusiness, Settings, ShieldAlert, ShieldCheck, @@ -235,6 +236,14 @@ function SecondaryNavSidebar({ headerHidden }: { headerHidden: boolean }) { 按钮管理 )} + {isAdmin && ( + navigate("/page/strategy/admin/position-roles")} + > + + 岗位角色配置 + + )} {isAdmin && ( navigate("/page/strategy/admin/login-whitelist")} @@ -540,6 +549,12 @@ function LegacyAccordionSidebar({ 按钮管理 )} + {isAdmin && ( + navigate("/page/strategy/admin/position-roles")}> + + 岗位角色配置 + + )} {isAdmin && ( navigate("/page/strategy/admin/login-whitelist")}> diff --git a/frontend-web/src/components/workspace/workspace-nav-menu.tsx b/frontend-web/src/components/workspace/workspace-nav-menu.tsx index 69f95724..8bc0e1a1 100644 --- a/frontend-web/src/components/workspace/workspace-nav-menu.tsx +++ b/frontend-web/src/components/workspace/workspace-nav-menu.tsx @@ -234,6 +234,14 @@ export function WorkspaceNavMenu() { 岗位管理 )} + {isAdmin && ( + navigate("/page/workspace/admin/position-roles")} + > + + 岗位角色配置 + + )} {isAdmin && ( diff --git a/frontend-web/src/core/auth/route-key-login.ts b/frontend-web/src/core/auth/route-key-login.ts index 7c7601ff..0b8b157a 100644 --- a/frontend-web/src/core/auth/route-key-login.ts +++ b/frontend-web/src/core/auth/route-key-login.ts @@ -5,11 +5,25 @@ import { readRuntimeEnv } from "@/env"; * * Add a new entry here when an external system needs a short ``routeKey``. * Targets must remain internal SPA routes; LoginPage validates them again - * before navigating. + * before navigating. The optional display settings are converted into the + * existing route parameters on the destination URL. */ -export const ROUTE_KEY_MAPPINGS: Readonly> = { - a: "/page/canvas/ai-writing", - "8bf": "/page/strategy/qa/8bf-roundtable", +export interface RouteKeyMapping { + /** Internal SPA route to open after sign-in. */ + path: string; + /** Add `hideChrome=1` to hide the global header and page sidebar. */ + hideChrome?: boolean; + /** Add the existing `theme` route parameter and apply it during sign-in. */ + theme?: string; +} + +export const ROUTE_KEY_MAPPINGS: Readonly> = { + a: { path: "/page/canvas/ai-writing" }, + "8bf": { + path: "/page/strategy/qa/8bf-roundtable", + hideChrome: true, + theme: "dark-blue", + }, }; /** Login URL parameter that selects a route from ``ROUTE_KEY_MAPPINGS``. */ @@ -26,8 +40,23 @@ export function getRouteKeyLoginPassword(): string { return readRuntimeEnv("VITE_ROUTE_LOGIN_PASSWORD") ?? ""; } -export function resolveRouteKey(routeKey: string | null | undefined): string { +export function resolveRouteKeyMapping( + routeKey: string | null | undefined, +): RouteKeyMapping | undefined { const key = routeKey?.trim(); - if (!key) return ""; - return ROUTE_KEY_MAPPINGS[key] ?? ""; + if (!key) return undefined; + return ROUTE_KEY_MAPPINGS[key]; +} + +export function resolveRouteKey(routeKey: string | null | undefined): string { + const mapping = resolveRouteKeyMapping(routeKey); + if (!mapping) return ""; + + const [path, ownQuery = ""] = mapping.path.split("?"); + const query = new URLSearchParams(ownQuery); + if (mapping.hideChrome) query.set("hideChrome", "1"); + if (mapping.theme) query.set("theme", mapping.theme); + + const serializedQuery = query.toString(); + return serializedQuery ? `${path}?${serializedQuery}` : path; } diff --git a/frontend-web/src/core/embed/frame-context.ts b/frontend-web/src/core/embed/frame-context.ts new file mode 100644 index 00000000..3223cc2a --- /dev/null +++ b/frontend-web/src/core/embed/frame-context.ts @@ -0,0 +1,24 @@ +/** + * Whether the application is currently hosted by another page. + * + * This intentionally covers both a native `