feat: 支持主题系统Logo与欢迎页开关

This commit is contained in:
fengxiangdebaozi committed 2026-07-16 22:31:22 +08:00
1 parent b97aa77adc
commit 82dd9fb08f
12 files changed
+178 -22

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>CM 助手</title>
<script src="/runtime-config.js"></script>
<script src="/runtime-config.js" data-runtime-config></script>
</head>
<body>
<div id="root"></div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+21 -2
View File
@@ -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: "",
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

@@ -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<string | null>(null);
const shouldShowWelcomeLogo =
Boolean(welcomeLogo) && failedWelcomeLogo !== welcomeLogo;
const isWelcomeLogoVisible = readRuntimeFlag("VITE_WELCOME_LOGO_VISIBLE");
return (
<div
@@ -19,6 +31,8 @@ export function CompactWelcome({ className }: { className?: string }) {
)}
>
{/* CM logo:浅底圆环 + 主色渐变圆角方块 */}
{/* Controlled by VITE_WELCOME_LOGO_VISIBLE in runtime-config.js. */}
{isWelcomeLogoVisible && (
<div
className="mb-3.5 inline-flex items-center justify-center rounded-full p-3"
style={{
@@ -28,18 +42,29 @@ export function CompactWelcome({ className }: { className?: string }) {
"1px solid rgb(var(--legacy-workspace-brand-rgb) / 12%)",
}}
>
<div
className="flex size-14 items-center justify-center rounded-2xl shadow-md"
style={{
backgroundImage:
"linear-gradient(to top right, var(--workspace-brand), var(--brand-200))",
}}
>
<span className="text-2xl font-black tracking-wide text-white italic">
{desensitizeText("CM")}
</span>
</div>
{shouldShowWelcomeLogo && welcomeLogo ? (
<img
src={welcomeLogo}
alt=""
aria-hidden="true"
className="size-16 object-contain"
onError={() => setFailedWelcomeLogo(welcomeLogo)}
/>
) : (
<div
className="flex size-14 items-center justify-center rounded-2xl shadow-md"
style={{
backgroundImage:
"linear-gradient(to top right, var(--workspace-brand), var(--brand-200))",
}}
>
<span className="text-2xl font-black tracking-wide text-white italic">
{desensitizeText("CM")}
</span>
</div>
)}
</div>
)}
<h1 className="flex items-center justify-center text-2xl font-bold tracking-tight text-foreground/90">
{desensitizeText("欢迎使用CM助手")}
@@ -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<BrandTheme, string> = {
light: fallbackLogoLight,
lightblue: fallbackLogoLightblue,
dark: fallbackLogoDark,
};
const headerRuntimeKeys: Record<BrandTheme, string> = {
light: "VITE_BRAND_LOGO_LIGHT_URL",
lightblue: "VITE_BRAND_LOGO_LIGHTBLUE_URL",
dark: "VITE_BRAND_LOGO_DARK_URL",
};
const welcomeRuntimeKeys: Record<BrandTheme, string> = {
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<HTMLScriptElement>("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]);
}
@@ -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<string | null>(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):菜单靠右贴近中间基座 */}
<div className="flex h-full min-w-0 flex-1 items-center justify-end">
<div className="flex h-full min-w-0 flex-1 items-center justify-between">
<div
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-cyan-300/25 bg-blue-950/55"
title="系统 Logo"
>
<img
src={brandLogo}
alt=""
aria-hidden="true"
className="h-[35px] w-[35px] object-contain"
onError={handleBrandLogoError}
/>
</div>
<nav
className="top-submenu-bar top-submenu-bar--group"
aria-label="左侧菜单"
@@ -238,8 +262,12 @@ const Header = ({ collapsed }: HeaderProps) => {
</nav>
</div>
{/* 中:CM助手(落在背景图发光基座上,纯文字无图标) */}
<div className="header-center-title">{desensitizeText("CM业务中心")}</div>
{/* 中:系统 logo + 名称,落在背景图发光基座上。 */}
<div className="header-center-title">
<span className="header-center-title__label">
{desensitizeText("CM业务中心")}
</span>
</div>
{/* 右分区(flex-1):菜单靠左贴近中间基座,操作区靠最右 */}
<div className="flex h-full min-w-0 flex-1 items-center justify-between">
@@ -261,10 +289,16 @@ const Header = ({ collapsed }: HeaderProps) => {
{/* 浅色顶栏:logo + 一级导航横排(白字落在 banner 上) */}
<div className="flex h-full min-w-0 flex-1 items-center">
<div className="flex flex-shrink-0 items-center gap-2 pr-1">
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-white/20">
<FileText className="h-4 w-4 text-white" />
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-white/20">
<img
src={brandLogo}
alt=""
aria-hidden="true"
className="h-[35px] w-[35px] object-contain"
onError={handleBrandLogoError}
/>
</div>
<span className="whitespace-nowrap text-base font-semibold text-white">
<span className="whitespace-nowrap text-lg font-semibold text-white">
{desensitizeText("CM业务中心")}
</span>
</div>
@@ -288,7 +322,13 @@ const Header = ({ collapsed }: HeaderProps) => {
: "bg-white/20",
)}
>
<FileText className="h-4 w-4 text-white" />
<img
src={brandLogo}
alt=""
aria-hidden="true"
className="h-7 w-7 object-contain"
onError={handleBrandLogoError}
/>
</div>
{!collapsed && (
<span className="text-sm font-semibold text-white">
+3
View File
@@ -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,