feat: 支持主题系统Logo与欢迎页开关
This commit is contained in:
1 parent
b97aa77adc
commit
82dd9fb08f
12 files changed
+178
-22
No files matched your search
@@ -16,7 +16,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>CM 助手</title>
|
<title>CM 助手</title>
|
||||||
<script src="/runtime-config.js"></script>
|
<script src="/runtime-config.js" data-runtime-config></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<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 |
@@ -7,11 +7,30 @@
|
|||||||
// 4. 这里的默认值来自 .env.network;如某个环境不用某项,可以留空字符串。
|
// 4. 这里的默认值来自 .env.network;如某个环境不用某项,可以留空字符串。
|
||||||
// 5. 布尔开关也用字符串形式填写:"true" / "false"。
|
// 5. 布尔开关也用字符串形式填写:"true" / "false"。
|
||||||
window.__CM_RUNTIME_CONFIG__ = {
|
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 等。
|
// 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 流式对话接口地址:普通问答、智能体对话等流式能力使用。
|
// 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。
|
// Canvas/AI 写作独立 LangGraph 地址:为空时前端会回退使用 VITE_LANGGRAPH_BASE_URL。
|
||||||
VITE_CANVAS_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";
|
"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 { cn } from "@/lib/utils";
|
||||||
import { useDisplayDesensitizer } from "@/core/sensitive-words/use-display-desensitizer";
|
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 }) {
|
export function CompactWelcome({ className }: { className?: string }) {
|
||||||
const desensitizeText = useDisplayDesensitizer();
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -19,6 +31,8 @@ export function CompactWelcome({ className }: { className?: string }) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* CM logo:浅底圆环 + 主色渐变圆角方块 */}
|
{/* CM logo:浅底圆环 + 主色渐变圆角方块 */}
|
||||||
|
{/* Controlled by VITE_WELCOME_LOGO_VISIBLE in runtime-config.js. */}
|
||||||
|
{isWelcomeLogoVisible && (
|
||||||
<div
|
<div
|
||||||
className="mb-3.5 inline-flex items-center justify-center rounded-full p-3"
|
className="mb-3.5 inline-flex items-center justify-center rounded-full p-3"
|
||||||
style={{
|
style={{
|
||||||
@@ -28,18 +42,29 @@ export function CompactWelcome({ className }: { className?: string }) {
|
|||||||
"1px solid rgb(var(--legacy-workspace-brand-rgb) / 12%)",
|
"1px solid rgb(var(--legacy-workspace-brand-rgb) / 12%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
{shouldShowWelcomeLogo && welcomeLogo ? (
|
||||||
className="flex size-14 items-center justify-center rounded-2xl shadow-md"
|
<img
|
||||||
style={{
|
src={welcomeLogo}
|
||||||
backgroundImage:
|
alt=""
|
||||||
"linear-gradient(to top right, var(--workspace-brand), var(--brand-200))",
|
aria-hidden="true"
|
||||||
}}
|
className="size-16 object-contain"
|
||||||
>
|
onError={() => setFailedWelcomeLogo(welcomeLogo)}
|
||||||
<span className="text-2xl font-black tracking-wide text-white italic">
|
/>
|
||||||
{desensitizeText("CM")}
|
) : (
|
||||||
</span>
|
<div
|
||||||
</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>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<h1 className="flex items-center justify-center text-2xl font-bold tracking-tight text-foreground/90">
|
<h1 className="flex items-center justify-center text-2xl font-bold tracking-tight text-foreground/90">
|
||||||
{desensitizeText("欢迎使用CM助手")}
|
{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 { useState } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { NotificationDialog } from "@/components/workspace/notification-dialog";
|
import { NotificationDialog } from "@/components/workspace/notification-dialog";
|
||||||
import { getStoredAuth } from "@/core/auth";
|
import { getStoredAuth } from "@/core/auth";
|
||||||
import { useLiveAccount } from "@/core/auth/use-live-account";
|
import { useLiveAccount } from "@/core/auth/use-live-account";
|
||||||
|
import { getHeaderBrandAsset, getHeaderBrandFallback } from "@/core/branding/brand-assets";
|
||||||
import { isWujieEnv } from "@/core/embed/wujie";
|
import { isWujieEnv } from "@/core/embed/wujie";
|
||||||
import { useUnreadCount } from "@/core/notifications";
|
import { useUnreadCount } from "@/core/notifications";
|
||||||
import { useMyPosition } from "@/core/positions";
|
import { useMyPosition } from "@/core/positions";
|
||||||
@@ -46,6 +47,17 @@ type HeaderProps = {
|
|||||||
const Header = ({ collapsed }: HeaderProps) => {
|
const Header = ({ collapsed }: HeaderProps) => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const isDark = theme === "dark";
|
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 [notificationOpen, setNotificationOpen] = useState(false);
|
||||||
const { data: unreadCount = 0 } = useUnreadCount();
|
const { data: unreadCount = 0 } = useUnreadCount();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -227,7 +239,19 @@ const Header = ({ collapsed }: HeaderProps) => {
|
|||||||
{showTopNav ? (
|
{showTopNav ? (
|
||||||
<>
|
<>
|
||||||
{/* 左分区(flex-1):菜单靠右贴近中间基座 */}
|
{/* 左分区(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
|
<nav
|
||||||
className="top-submenu-bar top-submenu-bar--group"
|
className="top-submenu-bar top-submenu-bar--group"
|
||||||
aria-label="左侧菜单"
|
aria-label="左侧菜单"
|
||||||
@@ -238,8 +262,12 @@ const Header = ({ collapsed }: HeaderProps) => {
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 中:CM助手(落在背景图发光基座上,纯文字无图标) */}
|
{/* 中:系统 logo + 名称,落在背景图发光基座上。 */}
|
||||||
<div className="header-center-title">{desensitizeText("CM业务中心")}</div>
|
<div className="header-center-title">
|
||||||
|
<span className="header-center-title__label">
|
||||||
|
{desensitizeText("CM业务中心")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 右分区(flex-1):菜单靠左贴近中间基座,操作区靠最右 */}
|
{/* 右分区(flex-1):菜单靠左贴近中间基座,操作区靠最右 */}
|
||||||
<div className="flex h-full min-w-0 flex-1 items-center justify-between">
|
<div className="flex h-full min-w-0 flex-1 items-center justify-between">
|
||||||
@@ -261,10 +289,16 @@ const Header = ({ collapsed }: HeaderProps) => {
|
|||||||
{/* 浅色顶栏:logo + 一级导航横排(白字落在 banner 上) */}
|
{/* 浅色顶栏:logo + 一级导航横排(白字落在 banner 上) */}
|
||||||
<div className="flex h-full min-w-0 flex-1 items-center">
|
<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 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">
|
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-white/20">
|
||||||
<FileText className="h-4 w-4 text-white" />
|
<img
|
||||||
|
src={brandLogo}
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-[35px] w-[35px] object-contain"
|
||||||
|
onError={handleBrandLogoError}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="whitespace-nowrap text-base font-semibold text-white">
|
<span className="whitespace-nowrap text-lg font-semibold text-white">
|
||||||
{desensitizeText("CM业务中心")}
|
{desensitizeText("CM业务中心")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -288,7 +322,13 @@ const Header = ({ collapsed }: HeaderProps) => {
|
|||||||
: "bg-white/20",
|
: "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>
|
</div>
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<span className="text-sm font-semibold text-white">
|
<span className="text-sm font-semibold text-white">
|
||||||
|
|||||||
@@ -1179,6 +1179,9 @@ html.dark .strategy-app-header {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-center-title__label {
|
||||||
/* 白→浅蓝→白 金属质感渐变文字 */
|
/* 白→浅蓝→白 金属质感渐变文字 */
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
|
|||||||
Reference in new issue
Block a user