diff --git a/frontend-web/index.html b/frontend-web/index.html index 26b8f4a6..fab13fa2 100644 --- a/frontend-web/index.html +++ b/frontend-web/index.html @@ -16,7 +16,7 @@ CM 助手 - +
diff --git a/frontend-web/public/brand-assets/brand-logo-dark.png b/frontend-web/public/brand-assets/brand-logo-dark.png new file mode 100644 index 00000000..426f78a9 Binary files /dev/null and b/frontend-web/public/brand-assets/brand-logo-dark.png differ diff --git a/frontend-web/public/brand-assets/brand-logo-light.png b/frontend-web/public/brand-assets/brand-logo-light.png new file mode 100644 index 00000000..53168237 Binary files /dev/null and b/frontend-web/public/brand-assets/brand-logo-light.png differ diff --git a/frontend-web/public/brand-assets/brand-logo-lightblue.png b/frontend-web/public/brand-assets/brand-logo-lightblue.png new file mode 100644 index 00000000..c25098e4 Binary files /dev/null and b/frontend-web/public/brand-assets/brand-logo-lightblue.png differ diff --git a/frontend-web/public/runtime-config.js b/frontend-web/public/runtime-config.js index b3326576..371d1694 100644 --- a/frontend-web/public/runtime-config.js +++ b/frontend-web/public/runtime-config.js @@ -7,6 +7,25 @@ // 4. 这里的默认值来自 .env.network;如某个环境不用某项,可以留空字符串。 // 5. 布尔开关也用字符串形式填写:"true" / "false"。 window.__CM_RUNTIME_CONFIG__ = { + // 品牌图片相对路径会以 runtime-config.js 所在目录为基准解析。 + // Nginx 以子路径部署时不需要修改前端图片路径。 + // 打包后可直接替换 dist/brand-assets/ 内的同名 PNG。 + // 也可以把下方图片地址改为其他 PNG 地址,无需重新打包。 + // 是否显示整块欢迎页图标区域:"true" 显示,"false" 隐藏,默认隐藏。 + VITE_WELCOME_LOGO_VISIBLE: "false", + // 红白主题的顶栏系统 Logo 图片地址。 + VITE_BRAND_LOGO_LIGHT_URL: "brand-assets/brand-logo-light.png", + // 浅蓝主题的顶栏系统 Logo 图片地址。 + VITE_BRAND_LOGO_LIGHTBLUE_URL: "brand-assets/brand-logo-lightblue.png", + // 深色主题的顶栏系统 Logo 图片地址。 + VITE_BRAND_LOGO_DARK_URL: "brand-assets/brand-logo-dark.png", + // 红白主题的欢迎页 Logo 图片地址:为空时显示原有 CM 文字和渐变背景。 + VITE_BRAND_WELCOME_LIGHT_URL: "", + // 浅蓝主题的欢迎页 Logo 图片地址:为空时显示原有 CM 文字和渐变背景。 + VITE_BRAND_WELCOME_LIGHTBLUE_URL: "", + // 深色主题的欢迎页 Logo 图片地址:为空时显示原有 CM 文字和渐变背景。 + VITE_BRAND_WELCOME_DARK_URL: "", + // DeerFlow Gateway 地址:前端自有后端接口,例如 /api/agents、/api/auth、/api/threads 等。 VITE_BACKEND_BASE_URL: "http://10.19.200.5:2026", diff --git a/frontend-web/src/assets/img/brand-logo-dark.png b/frontend-web/src/assets/img/brand-logo-dark.png new file mode 100644 index 00000000..426f78a9 Binary files /dev/null and b/frontend-web/src/assets/img/brand-logo-dark.png differ diff --git a/frontend-web/src/assets/img/brand-logo-light.png b/frontend-web/src/assets/img/brand-logo-light.png new file mode 100644 index 00000000..53168237 Binary files /dev/null and b/frontend-web/src/assets/img/brand-logo-light.png differ diff --git a/frontend-web/src/assets/img/brand-logo-lightblue.png b/frontend-web/src/assets/img/brand-logo-lightblue.png new file mode 100644 index 00000000..c25098e4 Binary files /dev/null and b/frontend-web/src/assets/img/brand-logo-lightblue.png differ diff --git a/frontend-web/src/components/page-sidebar-dark.tsx b/frontend-web/src/components/page-sidebar-dark.tsx index 2670f137..4d409edf 100644 --- a/frontend-web/src/components/page-sidebar-dark.tsx +++ b/frontend-web/src/components/page-sidebar-dark.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from "react"; import { useNavigate, useLocation } from "react-router-dom"; +import { PanelLeftOpen, PanelLeftClose } from "lucide-react"; import { getStoredAuth } from "@/core/auth"; import { isWujieEnv } from "@/core/embed/wujie"; @@ -12,7 +13,13 @@ import { import { useSidebarMenuItems } from "@/core/page-layout/use-sidebar-menu"; import './page-sdebar.css' -export function PageSidebarDark() { +type PageSidebarDarkProps = { + /** 是否收起(true 时只渲染左下角浮动入口,sidebar 主体隐藏) */ + collapsed: boolean; + onToggleCollapsed: () => void; +}; + +export function PageSidebarDark({ collapsed, onToggleCollapsed }: PageSidebarDarkProps) { const isAdmin = getStoredAuth()?.system_role === "admin"; const visibleMenuItems = useSidebarMenuItems(!!isAdmin); const inWujieEnv = isWujieEnv(); @@ -189,6 +196,22 @@ export function PageSidebarDark() { ); + // 收起时主侧栏不占宽度,保留左下角入口以便随时恢复。 + if (collapsed) { + return ( + + ); + } + return ( ); diff --git a/frontend-web/src/components/workspace/compact-welcome.tsx b/frontend-web/src/components/workspace/compact-welcome.tsx index 1b840da3..a40ac422 100644 --- a/frontend-web/src/components/workspace/compact-welcome.tsx +++ b/frontend-web/src/components/workspace/compact-welcome.tsx @@ -1,5 +1,10 @@ "use client"; +import { useState } from "react"; + +import { useTheme } from "@/contexts/ThemeContext"; +import { getWelcomeBrandAsset } from "@/core/branding/brand-assets"; +import { readRuntimeFlag } from "@/env"; import { cn } from "@/lib/utils"; import { useDisplayDesensitizer } from "@/core/sensitive-words/use-display-desensitizer"; @@ -10,6 +15,13 @@ import { useDisplayDesensitizer } from "@/core/sensitive-words/use-display-desen */ export function CompactWelcome({ className }: { className?: string }) { const desensitizeText = useDisplayDesensitizer(); + const { theme } = useTheme(); + const brandTheme = theme; + const welcomeLogo = getWelcomeBrandAsset(brandTheme); + const [failedWelcomeLogo, setFailedWelcomeLogo] = useState(null); + const shouldShowWelcomeLogo = + Boolean(welcomeLogo) && failedWelcomeLogo !== welcomeLogo; + const isWelcomeLogoVisible = readRuntimeFlag("VITE_WELCOME_LOGO_VISIBLE"); return (
{/* CM logo:浅底圆环 + 主色渐变圆角方块 */} + {/* Controlled by VITE_WELCOME_LOGO_VISIBLE in runtime-config.js. */} + {isWelcomeLogoVisible && (
-
- - {desensitizeText("CM")} - -
+ {shouldShowWelcomeLogo && welcomeLogo ? ( + setFailedWelcomeLogo(welcomeLogo)} + /> + ) : ( +
+ + {desensitizeText("CM")} + +
+ )}
+ )}

{desensitizeText("欢迎使用CM助手")} diff --git a/frontend-web/src/core/branding/brand-assets.ts b/frontend-web/src/core/branding/brand-assets.ts new file mode 100644 index 00000000..b78cebb9 --- /dev/null +++ b/frontend-web/src/core/branding/brand-assets.ts @@ -0,0 +1,69 @@ +import fallbackLogoDark from "@/assets/img/brand-logo-dark.png"; +import fallbackLogoLight from "@/assets/img/brand-logo-light.png"; +import fallbackLogoLightblue from "@/assets/img/brand-logo-lightblue.png"; +import { readRuntimeEnv } from "@/env"; + +export type BrandTheme = "light" | "lightblue" | "dark"; + +const headerFallbackAssets: Record = { + light: fallbackLogoLight, + lightblue: fallbackLogoLightblue, + dark: fallbackLogoDark, +}; + +const headerRuntimeKeys: Record = { + light: "VITE_BRAND_LOGO_LIGHT_URL", + lightblue: "VITE_BRAND_LOGO_LIGHTBLUE_URL", + dark: "VITE_BRAND_LOGO_DARK_URL", +}; + +const welcomeRuntimeKeys: Record = { + light: "VITE_BRAND_WELCOME_LIGHT_URL", + lightblue: "VITE_BRAND_WELCOME_LIGHTBLUE_URL", + dark: "VITE_BRAND_WELCOME_DARK_URL", +}; + +function runtimeConfigBaseUrl(): string | undefined { + if (typeof document === "undefined") { + return undefined; + } + + return document.querySelector("script[data-runtime-config]")?.src; +} + +/** + * Resolves an optional post-build brand override from runtime-config.js. + * Relative paths are anchored to runtime-config.js, which keeps the URL valid + * when the whole SPA is mounted below an Nginx location prefix. + */ +function readRuntimeBrandAsset(key: string): string | undefined { + const configuredUrl = readRuntimeEnv(key); + if (!configuredUrl) { + return undefined; + } + + try { + const baseUrl = + runtimeConfigBaseUrl() ?? + (typeof window !== "undefined" ? window.location.href : undefined); + return baseUrl ? new URL(configuredUrl, baseUrl).href : configuredUrl; + } catch { + return configuredUrl; + } +} + +export function getHeaderBrandAsset(theme: BrandTheme): string { + return readRuntimeBrandAsset(headerRuntimeKeys[theme]) ?? headerFallbackAssets[theme]; +} + +export function getHeaderBrandFallback(theme: BrandTheme): string { + return headerFallbackAssets[theme]; +} + +/** + * Welcome content deliberately has no bundled image fallback. An empty + * runtime URL keeps the original CM text + gradient treatment. + */ +export function getWelcomeBrandAsset(theme: BrandTheme): string | undefined { + return readRuntimeBrandAsset(welcomeRuntimeKeys[theme]); +} diff --git a/frontend-web/src/pages/PageRoutes.tsx b/frontend-web/src/pages/PageRoutes.tsx index 54b98a5c..f76e80ad 100644 --- a/frontend-web/src/pages/PageRoutes.tsx +++ b/frontend-web/src/pages/PageRoutes.tsx @@ -31,8 +31,6 @@ function PageLayout() { const location = useLocation(); const { theme } = useTheme(); const isDark = theme === "dark"; - // 深色导航侧栏不提供收起入口,始终保持展开,避免切换主题后沿用浅色模式的收起状态。 - const sidebarCollapsed = isDark ? false : collapsed; // 浅色非嵌入:侧边栏为新版「二级导航图标卡片」窄栏(96px);wujie 嵌入仍走旧版手风琴(64/256px)。 const inWujie = useMemo(() => isWujieEnv(), []); const lightNarrowSidebar = !isDark && !inWujie; @@ -76,14 +74,17 @@ function PageLayout() { return (
- {pageSidebarAllowed &&
} + {pageSidebarAllowed &&
}
{pageSidebarAllowed && ( isDark ? ( - + setCollapsed((prev) => !prev)} + /> ) : ( setCollapsed((prev) => !prev)} /> ) @@ -93,8 +94,8 @@ function PageLayout() { !pageSidebarAllowed ? "" : isDark - // 深色 sidebar 固定展开 118px(10 + 98 + 10 gap)。 - ? "pl-[118px]" + // 深色 sidebar:展开 118px(10 + 98 + 10 gap),收起时主内容不留左内边距。 + ? (collapsed ? "pl-0" : "pl-[118px]") : lightNarrowSidebar // 浅色新版窄栏:展开三级列时 96→236px,主内容左内边距随之过渡 ? `transition-[padding] duration-300 ${lightExpanded ? "pl-[236px]" : "pl-[96px]"}` @@ -108,7 +109,7 @@ function PageLayout() { path="workspace/*" element={ (
diff --git a/frontend-web/src/roundtable-planning/styles/roundtable-planning.css b/frontend-web/src/roundtable-planning/styles/roundtable-planning.css index bf583d12..a9060f24 100644 --- a/frontend-web/src/roundtable-planning/styles/roundtable-planning.css +++ b/frontend-web/src/roundtable-planning/styles/roundtable-planning.css @@ -10,10 +10,10 @@ --rt-primary-active: var(--brand-950, #a8071a); --rt-primary-soft: var(--brand-200, #feeded); --rt-page-bg: #fef6f6; - /* 圆桌全页最小可读字号(含 Step 3 方案展示 / 结果绘制) */ - --roundtable-chat-font-size: 16px; - --roundtable-chat-meta-font-size: 16px; - font-size: 16px; + /* 圆桌全页最小可读字号(含 Step 3 方案展示 / 结果绘制),跟随「设置 → 字号」。 */ + --roundtable-chat-font-size: calc(16px * var(--ui-font-scale, 1)); + --roundtable-chat-meta-font-size: calc(16px * var(--ui-font-scale, 1)); + font-size: calc(16px * var(--ui-font-scale, 1)); } /* 会商对话区:气泡正文、步骤卡、Markdown 块 */ @@ -29,10 +29,11 @@ /* * 抬升页内显式写死的小字号类(text-xs / text-sm / text-[10–14px] / calc), - * 避免 Step 3、侧栏、按钮条等遗漏逐文件改 class。text-base 及以上标题保持不变。 + * 并让它们跟随设置页字号倍率,避免 Step 3、左右记录区和对话列表遗漏。 + * text-base 及以上标题由全局 --text-* token 处理。 */ .roundtable-planning :where(.text-xs, .text-sm) { - font-size: 16px; + font-size: calc(16px * var(--ui-font-scale, 1)); } .roundtable-planning :where( @@ -46,7 +47,7 @@ [class*="text-[14px"], [class*="text-[calc"] ):not(.w-5) { - font-size: 16px; + font-size: calc(16px * var(--ui-font-scale, 1)); } .roundtable-planning ::-webkit-scrollbar-thumb { @@ -343,6 +344,17 @@ color: var(--muted-foreground) !important; } +/* ---- Step1 右侧「澄清记录」:覆盖 hover:bg-slate-50/80,避免深色下泛灰白 ---- */ +:is(.dark .roundtable-planning, .roundtable-planning--dark) + .roundtable-clarification-entry:hover { + background-color: rgb(22 119 255 / 18%) !important; +} + +:is(.dark .roundtable-planning, .roundtable-planning--dark) + .roundtable-clarification-entry:hover h4 { + color: #e8edf5 !important; +} + /* ---- focus ring:brand-200 → 主色淡环 ---- */ :is(.dark .roundtable-planning, .roundtable-planning--dark) :is( diff --git a/frontend-web/src/strategy-components/Header.tsx b/frontend-web/src/strategy-components/Header.tsx index 3ebee2a5..e132de1e 100644 --- a/frontend-web/src/strategy-components/Header.tsx +++ b/frontend-web/src/strategy-components/Header.tsx @@ -1,4 +1,4 @@ -import { Bell, ChevronDown, FileText, User } from "lucide-react"; +import { Bell, ChevronDown, User } from "lucide-react"; import { useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; @@ -12,6 +12,7 @@ import { import { NotificationDialog } from "@/components/workspace/notification-dialog"; import { getStoredAuth } from "@/core/auth"; import { useLiveAccount } from "@/core/auth/use-live-account"; +import { getHeaderBrandAsset, getHeaderBrandFallback } from "@/core/branding/brand-assets"; import { isWujieEnv } from "@/core/embed/wujie"; import { useUnreadCount } from "@/core/notifications"; import { useMyPosition } from "@/core/positions"; @@ -46,6 +47,17 @@ type HeaderProps = { const Header = ({ collapsed }: HeaderProps) => { const { theme } = useTheme(); const isDark = theme === "dark"; + const brandTheme = theme; + const configuredBrandLogo = getHeaderBrandAsset(brandTheme); + const fallbackBrandLogo = getHeaderBrandFallback(brandTheme); + const [failedBrandLogo, setFailedBrandLogo] = useState(null); + const brandLogo = + failedBrandLogo === configuredBrandLogo ? fallbackBrandLogo : configuredBrandLogo; + const handleBrandLogoError = () => { + if (brandLogo !== fallbackBrandLogo) { + setFailedBrandLogo(brandLogo); + } + }; const [notificationOpen, setNotificationOpen] = useState(false); const { data: unreadCount = 0 } = useUnreadCount(); const navigate = useNavigate(); @@ -59,7 +71,8 @@ const Header = ({ collapsed }: HeaderProps) => { const positionName = myPosition?.name?.trim() || ""; const displayName = liveAccount.displayName; - const showTopNav = isDark && !collapsed && !isWujieEnv(); + // 深色侧栏收起只影响左侧导航,不应让顶栏回退到浅色的图标布局。 + const showTopNav = isDark && !isWujieEnv(); // 浅色(红/蓝)顶栏:一级导航正常横排进顶栏,不做深色那种围绕基座的特殊处理。 // 与 collapsed 解耦(折叠只影响侧边栏,一级导航始终可见);wujie 嵌入下隐藏, // 此时一级导航回落到侧边栏(见 page-sidebar.tsx)。 @@ -226,7 +239,19 @@ const Header = ({ collapsed }: HeaderProps) => { {showTopNav ? ( <> {/* 左分区(flex-1):菜单靠右贴近中间基座 */} -
+
+
+ +
- {/* 中:CM助手(落在背景图发光基座上,纯文字无图标) */} -
{desensitizeText("CM业务中心")}
+ {/* 中:系统 logo + 名称,落在背景图发光基座上。 */} +
+ + {desensitizeText("CM业务中心")} + +
{/* 右分区(flex-1):菜单靠左贴近中间基座,操作区靠最右 */}
@@ -260,10 +289,16 @@ const Header = ({ collapsed }: HeaderProps) => { {/* 浅色顶栏:logo + 一级导航横排(白字落在 banner 上) */}
-
- +
+
- + {desensitizeText("CM业务中心")}
@@ -287,7 +322,13 @@ const Header = ({ collapsed }: HeaderProps) => { : "bg-white/20", )} > - +
{!collapsed && ( diff --git a/frontend-web/src/styles/globals.css b/frontend-web/src/styles/globals.css index 097e616c..9725479f 100644 --- a/frontend-web/src/styles/globals.css +++ b/frontend-web/src/styles/globals.css @@ -1179,6 +1179,9 @@ html.dark .strategy-app-header { white-space: nowrap; pointer-events: none; user-select: none; +} + +.header-center-title__label { /* 白→浅蓝→白 金属质感渐变文字 */ background: linear-gradient( 180deg,