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 f44dd78f..371d1694 100644 --- a/frontend-web/public/runtime-config.js +++ b/frontend-web/public/runtime-config.js @@ -7,11 +7,30 @@ // 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", + VITE_BACKEND_BASE_URL: "http://10.19.200.5:2026", // LangGraph 流式对话接口地址:普通问答、智能体对话等流式能力使用。 - // VITE_LANGGRAPH_BASE_URL: "http://10.19.200.5:2026/api", + VITE_LANGGRAPH_BASE_URL: "http://10.19.200.5:2026/api", // Canvas/AI 写作独立 LangGraph 地址:为空时前端会回退使用 VITE_LANGGRAPH_BASE_URL。 VITE_CANVAS_LANGGRAPH_BASE_URL: "", 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/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/strategy-components/Header.tsx b/frontend-web/src/strategy-components/Header.tsx index 3ca08ddc..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(); @@ -227,7 +239,19 @@ const Header = ({ collapsed }: HeaderProps) => { {showTopNav ? ( <> {/* 左分区(flex-1):菜单靠右贴近中间基座 */} -
+
+
+ +
- {/* 中:CM助手(落在背景图发光基座上,纯文字无图标) */} -
{desensitizeText("CM业务中心")}
+ {/* 中:系统 logo + 名称,落在背景图发光基座上。 */} +
+ + {desensitizeText("CM业务中心")} + +
{/* 右分区(flex-1):菜单靠左贴近中间基座,操作区靠最右 */}
@@ -261,10 +289,16 @@ const Header = ({ collapsed }: HeaderProps) => { {/* 浅色顶栏:logo + 一级导航横排(白字落在 banner 上) */}
-
- +
+
- + {desensitizeText("CM业务中心")}
@@ -288,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,