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/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/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 - + 实时预览 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/system-settings/api.ts b/frontend-web/src/core/system-settings/api.ts index 71f5036d..3b3e76b7 100644 --- a/frontend-web/src/core/system-settings/api.ts +++ b/frontend-web/src/core/system-settings/api.ts @@ -291,6 +291,8 @@ export type CustomThemeBaseMode = "light" | "dark"; export interface CustomThemeColorOverrides { button_hover?: string | null; + navigation_gradient_start?: string | null; + navigation_gradient_end?: string | null; selected_background?: string | null; border?: string | null; muted?: string | null; diff --git a/frontend-web/src/core/theme/custom-theme.ts b/frontend-web/src/core/theme/custom-theme.ts index e1dde872..161b02c6 100644 --- a/frontend-web/src/core/theme/custom-theme.ts +++ b/frontend-web/src/core/theme/custom-theme.ts @@ -13,6 +13,8 @@ const BLACK: Rgb = { r: 0, g: 0, b: 0 }; export const CUSTOM_THEME_ADVANCED_KEYS = [ "button_hover", + "navigation_gradient_start", + "navigation_gradient_end", "selected_background", "border", "muted", @@ -146,6 +148,18 @@ export function getCustomThemeTokens(theme: CustomThemeTokenInput): Record +const iconButtonClass = (isDark: boolean, isPaleLightTheme: boolean, isCustomTheme: boolean) => cn( "size-8 shrink-0 rounded-sm", - isDark + isCustomTheme + ? "text-[var(--navigation-foreground)] hover:bg-white/20 hover:text-[var(--navigation-foreground)] active:bg-white/30" + : isDark ? "text-slate-400 hover:bg-slate-800/50 hover:text-white" : isPaleLightTheme ? "text-[var(--foreground)] hover:bg-white/55 hover:text-[var(--foreground)] active:bg-white/75" @@ -48,9 +50,22 @@ type HeaderProps = { }; const Header = ({ collapsed }: HeaderProps) => { - const { theme } = useTheme(); - const isDark = theme === "dark"; + const { theme, baseMode } = useTheme(); + const isDark = baseMode === "dark"; + const isCustomTheme = theme.startsWith("custom-theme-"); const isPaleLightTheme = theme === "iceblue" || theme === "lightyellow"; + const headerTone = isCustomTheme + ? "banner-custom" + : isDark + ? "banner-dark" + : isPaleLightTheme + ? "content" + : "banner-light"; + const headerTextClass = isCustomTheme + ? "text-[var(--navigation-foreground)]" + : isPaleLightTheme + ? "text-[var(--foreground)]" + : "text-white"; const darkLogoPosition = readRuntimeEnv("VITE_DARK_LOGO_POSITION") === "left" ? "left" : "center"; const isDarkLogoOnLeft = darkLogoPosition === "left"; @@ -152,11 +167,11 @@ const Header = ({ collapsed }: HeaderProps) => { // 右侧操作区(换肤/通知/用户)—— 深浅两套布局共用 const actionCluster = ( <> - - + + @@ -165,7 +180,7 @@ const Header = ({ collapsed }: HeaderProps) => { variant="ghost" size="icon-sm" onClick={() => setNotificationOpen(true)} - className={cn(iconButtonClass(isDark, isPaleLightTheme), "relative")} + className={cn(iconButtonClass(isDark, isPaleLightTheme, isCustomTheme), "relative")} title="通知" > @@ -186,7 +201,13 @@ const Header = ({ collapsed }: HeaderProps) => {
@@ -196,7 +217,9 @@ const Header = ({ collapsed }: HeaderProps) => { variant="ghost" className={cn( "h-8 gap-2 rounded-sm px-2", - isDark + isCustomTheme + ? "text-[var(--navigation-foreground)] hover:bg-white/20 active:bg-white/30" + : isDark ? "text-slate-200 hover:bg-slate-800/50" : isPaleLightTheme ? "text-[var(--foreground)] hover:bg-white/55 active:bg-white/75" @@ -206,7 +229,9 @@ const Header = ({ collapsed }: HeaderProps) => {
{
-

+

{displayName}

{positionName && ( -

+

{positionName}

)} @@ -231,7 +256,7 @@ const Header = ({ collapsed }: HeaderProps) => { @@ -323,7 +348,7 @@ const Header = ({ collapsed }: HeaderProps) => { onError={handleBrandLogoError} />
- + {desensitizeText("CM业务中心")}
@@ -342,7 +367,9 @@ const Header = ({ collapsed }: HeaderProps) => {
{ />
{!collapsed && ( - + {desensitizeText("CM业务中心")} )} diff --git a/frontend-web/src/styles/globals.css b/frontend-web/src/styles/globals.css index 0db166ff..d4e9e639 100644 --- a/frontend-web/src/styles/globals.css +++ b/frontend-web/src/styles/globals.css @@ -1849,6 +1849,16 @@ html.lightyellow .gq-history-item--active .bg-primary { /* Runtime-created themes deliberately use semantic variables rather than a generated class per colour. These compatibility rules keep the selected, hovered and compact-navigation states readable on Chrome 109 as well. */ +html[data-custom-theme="true"] .strategy-app-header { + background-color: var(--primary); + background-image: linear-gradient( + 105deg, + var(--navigation-gradient-start) 0%, + var(--primary) 52%, + var(--navigation-gradient-end) 100% + ); +} + html[data-custom-theme="true"] .agent-avatar { background-color: var(--accent); color: var(--accent-foreground); diff --git a/offline-backend-20260512/backend/CLAUDE.md b/offline-backend-20260512/backend/CLAUDE.md index 42ed2b04..5f2a15f5 100644 --- a/offline-backend-20260512/backend/CLAUDE.md +++ b/offline-backend-20260512/backend/CLAUDE.md @@ -1009,7 +1009,10 @@ When using `make dev` from root, the frontend automatically connects through ngi the built-in default theme. A row is `CustomThemePreset` (`id`, `name`, six-digit `primary`, optional primary-button `text_color`, `mode`, `overrides`, `enabled`, `sort_order`). `overrides` is intentionally a short interaction-only list: -`button_hover`, `selected_background`, `border`, `muted`, `muted_text`, `destructive`, `brand_title`, and `primary_foreground`. +`button_hover`, `navigation_gradient_start`, `navigation_gradient_end`, +`selected_background`, `border`, `muted`, `muted_text`, `destructive`, +`brand_title`, and `primary_foreground`. The navigation gradient endpoints +default from `primary` and are only stored when an administrator overrides them. All values must be six-digit hex strings; the frontend derives every remaining semantic CSS/TDesign token so admins do not maintain a full raw token sheet. Ids must be `custom-theme-*`, names and ids are unique, and the router limits diff --git a/offline-backend-20260512/backend/README.md b/offline-backend-20260512/backend/README.md index 795883e9..56b715ed 100644 --- a/offline-backend-20260512/backend/README.md +++ b/offline-backend-20260512/backend/README.md @@ -422,8 +422,11 @@ themes through `PUT /api/system-settings/appearance`. Each theme stores a safe six-digit `primary` colour, an optional primary-button `text_color`, `light`/`dark` mode, name, enabled flag and sort order. The frontend derives the rest of the visual tokens; the optional `overrides` object only exposes -eight interaction colours (`button_hover`, `selected_background`, `border`, -`muted`, `muted_text`, `destructive`, `brand_title`, `primary_foreground`) for exceptional brand requirements. A custom theme id + ten interaction colours (`button_hover`, `navigation_gradient_start`, + `navigation_gradient_end`, `selected_background`, `border`, `muted`, + `muted_text`, `destructive`, `brand_title`, `primary_foreground`) for + exceptional brand requirements. The two navigation values override the + automatically derived top-bar gradient. A custom theme id must use the `custom-theme-` prefix. Disabled or removed custom themes cannot remain the system default and automatically fall back to `light`. diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/config/system_settings.py b/offline-backend-20260512/backend/packages/harness/deerflow/config/system_settings.py index 10219c77..92e1d25e 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/config/system_settings.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/config/system_settings.py @@ -57,6 +57,14 @@ class CustomThemeColorOverrides(BaseModel): """ button_hover: str | None = Field(default=None, description="Primary button hover colour") + navigation_gradient_start: str | None = Field( + default=None, + description="Left colour for the configurable top-navigation gradient", + ) + navigation_gradient_end: str | None = Field( + default=None, + description="Right colour for the configurable top-navigation gradient", + ) selected_background: str | None = Field(default=None, description="Selected/active item background") border: str | None = Field(default=None, description="Card and input border colour") muted: str | None = Field(default=None, description="Muted/secondary surface colour") @@ -71,6 +79,8 @@ class CustomThemeColorOverrides(BaseModel): @field_validator( "button_hover", + "navigation_gradient_start", + "navigation_gradient_end", "selected_background", "border", "muted",