diff --git a/frontend-web/public/runtime-config.js b/frontend-web/public/runtime-config.js index 3a21c0aa..f18623c5 100644 --- a/frontend-web/public/runtime-config.js +++ b/frontend-web/public/runtime-config.js @@ -99,7 +99,7 @@ window.__CM_RUNTIME_CONFIG__ = { VITE_AIPI_KNOWES_URL: "http://ch1.b.uat.4.cn/knowes", // MaxKey 鉴权地址:onlineTicket 换 token 使用。 - VITE_AUTH_BASE_URL: "https://auth.y.sit.4.cn", + VITE_AUTH_BASE_URL: "https://auth.y.uat.4.cn", // PG 中任务 iframe 地址。 VITE_AIPI_PGLINKTO_URL: "https://pg.b.uat.4.cn", diff --git a/frontend-web/src/components/page-sdebar.css b/frontend-web/src/components/page-sdebar.css index 0093a30a..73f6a7e2 100644 --- a/frontend-web/src/components/page-sdebar.css +++ b/frontend-web/src/components/page-sdebar.css @@ -607,6 +607,27 @@ html.lightyellow .light-topnav-item--active::after { } /* ── 侧边栏:二级图标卡片列(96px)+ 右侧三级列(展开时整体变宽,带过渡)──────── */ +/* Runtime themes use their configurable header gradient rather than the + red/blue banner artwork. Navigation copy follows an automatically chosen + contrasting foreground so both pale and deep brand colours remain legible. */ +html[data-custom-theme="true"] .light-topnav-item, +html[data-custom-theme="true"] .top-submenu-item { + color: var(--navigation-foreground); +} + +html[data-custom-theme="true"] .light-topnav-item:hover, +html[data-custom-theme="true"] .light-topnav-item--active, +html[data-custom-theme="true"] .light-topnav-item--active:hover, +html[data-custom-theme="true"] .top-submenu-item:hover, +html[data-custom-theme="true"] .top-submenu-item--active { + color: var(--navigation-foreground); + text-shadow: none; +} + +html[data-custom-theme="true"] .light-topnav-item--active::after { + background: var(--navigation-foreground); +} + .light-sidebar { position: fixed; left: 0; 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/appearance-controls.tsx b/frontend-web/src/components/workspace/appearance-controls.tsx index 03f8f81d..742eeba2 100644 --- a/frontend-web/src/components/workspace/appearance-controls.tsx +++ b/frontend-web/src/components/workspace/appearance-controls.tsx @@ -26,7 +26,7 @@ import { buildLightAppUrl } from "@/strategy-components/lib/light-app"; * - "banner-dark" / "banner-light":Header 落在主题 banner 上(白字 / 浅色 hover)。 * - "content":简洁模式聊天页浅底顶栏,走 foreground / muted-foreground。 */ -export type AppearanceControlTone = "banner-dark" | "banner-light" | "content"; +export type AppearanceControlTone = "banner-dark" | "banner-light" | "banner-custom" | "content"; // 换肤色板:红色=红白主题(light),蓝色=浅蓝主题(lightblue),冰川蓝=柔和浅蓝主题(iceblue),浅黄色=柔和黄主题(lightyellow),深蓝色=深色主题(dark) export const THEME_SWATCHES: { id: ThemeType; label: string; color: string }[] = [ @@ -43,6 +43,8 @@ const triggerToneClass = (tone: AppearanceControlTone) => ? "text-slate-400 hover:bg-slate-800/50 hover:text-slate-400" : tone === "banner-light" ? "text-white hover:bg-[var(--brand-950)] hover:text-white active:bg-[#8a0618]" + : tone === "banner-custom" + ? "text-[var(--navigation-foreground)] hover:bg-white/20 hover:text-[var(--navigation-foreground)] active:bg-white/30" : "text-muted-foreground hover:bg-muted hover:text-foreground"; export function CompactShortcutButtons({ tone }: { tone: AppearanceControlTone }) { diff --git a/frontend-web/src/components/workspace/general-qa-sidebar.tsx b/frontend-web/src/components/workspace/general-qa-sidebar.tsx index 23bdfbab..51f47ad8 100644 --- a/frontend-web/src/components/workspace/general-qa-sidebar.tsx +++ b/frontend-web/src/components/workspace/general-qa-sidebar.tsx @@ -83,6 +83,7 @@ import { sortThreadsPinnedFirst, titleOfThread, } from "@/core/threads/utils"; +import { buildFullChatNavigationPath } from "@/core/page-layout/chats-iframe-route"; import { env } from "@/env"; import { desensitize } from "@/lib/desensitize"; import { isIMEComposing } from "@/lib/ime"; @@ -185,15 +186,24 @@ export function GeneralQaSidebar() { // 当前嵌入的智能体(URL 上的 ?agent=):用于高亮对应卡片。 const searchParams = useSearchParams(); const activeAgentId = searchParams.get("agent"); + const currentSearch = searchParams.toString(); + const newChatHref = buildFullChatNavigationPath(NEW_CHAT_PATH, currentSearch); + const moreAgentsHref = buildFullChatNavigationPath(MORE_AGENTS_HREF, currentSearch); + const moreChatsHref = buildFullChatNavigationPath(MORE_CHATS_HREF, currentSearch); // 点击常用智能体 = 在通用问答右侧**原地嵌入**该智能体的对话页:停留在 /chats/new, // 仅给 URL 加 ?agent=,由 NewChatContent 把右侧问答区换成 AgentChatPage。 // 不跳转独立页面、左侧栏不变。每次点击都重开一个干净会话(NewChatContent 的 key)。 const handleSelectAgent = useCallback( (agentId: string) => { - navigate(`/page/workspace/chats/new?agent=${encodeURIComponent(agentId)}`); + navigate( + buildFullChatNavigationPath( + `${NEW_CHAT_PATH}?agent=${encodeURIComponent(agentId)}`, + currentSearch, + ), + ); }, - [navigate], + [currentSearch, navigate], ); // ── 最近的对话取数 + 搜索 ── @@ -241,7 +251,7 @@ export function GeneralQaSidebar() { const handleNewChatClick = (event: React.MouseEvent) => { if (event.metaKey || event.ctrlKey || event.shiftKey || event.button === 1) return; event.preventDefault(); - navigate(NEW_CHAT_PATH, { replace: true, state: { resetAt: Date.now() } }); + navigate(newChatHref, { replace: true, state: { resetAt: Date.now() } }); }; const handleDelete = useCallback( @@ -257,10 +267,17 @@ export function GeneralQaSidebar() { nextThreadPath = listPathOfThread(threads[threadIndex - 1]!); } } - void router.push(nextThreadPath); + void router.push(buildFullChatNavigationPath(nextThreadPath, currentSearch)); } }, - [agentIdFromPath, deleteThread, router, threadIdFromPath, threads], + [ + agentIdFromPath, + currentSearch, + deleteThread, + router, + threadIdFromPath, + threads, + ], ); const handleRenameClick = useCallback((threadId: string, currentTitle: string) => { @@ -314,7 +331,7 @@ export function GeneralQaSidebar() { 底色用「主色混入透明」=主色低透明度叠在当前侧栏底色上,浅/红/蓝/深蓝主题都自适应 (不要混 white,否则深蓝主题下会变成浅色块)。 */} {t.common.more} @@ -454,7 +471,10 @@ export function GeneralQaSidebar() { ) : ( filteredThreads.map((thread) => { - const href = listPathOfThread(thread); + const href = buildFullChatNavigationPath( + listPathOfThread(thread), + currentSearch, + ); const isActive = href === pathname; const isBoot = isBootstrapThread(thread); const pinned = isThreadPinned(thread); @@ -576,7 +596,7 @@ export function GeneralQaSidebar() { )} {!query && ( 更多最近对话 diff --git a/frontend-web/src/components/workspace/settings/custom-theme-library-settings.tsx b/frontend-web/src/components/workspace/settings/custom-theme-library-settings.tsx index be2dbec0..e7f30e0d 100644 --- a/frontend-web/src/components/workspace/settings/custom-theme-library-settings.tsx +++ b/frontend-web/src/components/workspace/settings/custom-theme-library-settings.tsx @@ -70,6 +70,18 @@ const ADVANCED_COLOR_FIELDS: Array<{ description: "主按钮悬停时的背景色", token: "--theme-button-hover", }, + { + key: "navigation_gradient_start", + label: "导航渐变起点", + description: "全量模式顶部导航栏左侧的渐变色", + token: "--navigation-gradient-start", + }, + { + key: "navigation_gradient_end", + label: "导航渐变终点", + description: "全量模式顶部导航栏右侧的渐变色", + token: "--navigation-gradient-end", + }, { key: "selected_background", label: "选中背景", @@ -268,14 +280,20 @@ function CustomThemePreview({ theme }: { theme: CustomThemePreset }) { style={tokens} aria-label={`${theme.name} 的颜色预览`} > -
+
C - + 实时预览 )} + {isAdmin && ( + navigate("/page/workspace/admin/position-roles")} + > + + 岗位角色配置 + + )} {isAdmin && ( diff --git a/frontend-web/src/contexts/ThemeContext.tsx b/frontend-web/src/contexts/ThemeContext.tsx index 9ef844a8..c126046f 100644 --- a/frontend-web/src/contexts/ThemeContext.tsx +++ b/frontend-web/src/contexts/ThemeContext.tsx @@ -229,6 +229,8 @@ function markExplicitChoice(): void { // 主题上下文 interface ThemeContextType { theme: ThemeType; + /** Resolved visual mode; custom themes keep their own light/dark setting. */ + baseMode: 'light' | 'dark'; themeConfig: ThemeConfig; toggleTheme: () => void; setTheme: (theme: ThemeType) => void; @@ -309,6 +311,7 @@ export const ThemeProvider: React.FC = ({ children }) => { const theme: ThemeType = isBuiltinTheme(requestedTheme) ? requestedTheme : customTheme?.id ?? 'light'; + const baseMode: 'light' | 'dark' = customTheme?.mode ?? (theme === 'dark' ? 'dark' : 'light'); useEffect(() => { if (typeof document === 'undefined') return; @@ -377,7 +380,7 @@ export const ThemeProvider: React.FC = ({ children }) => { return ( {children} 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 `