From bb556dc7ae92a53339d056ea835ef50cfcaf5679 Mon Sep 17 00:00:00 2001 From: DrSmoothl <1787882683@qq.com> Date: Thu, 19 Feb 2026 18:22:35 +0800 Subject: [PATCH] feat(theme): extend settings UI with token controls and custom CSS editor --- dashboard/package.json | 1 + dashboard/src/components/CodeEditor.tsx | 7 +- dashboard/src/routes/settings.tsx | 485 +++++++++++++++++++++++- 3 files changed, 490 insertions(+), 3 deletions(-) diff --git a/dashboard/package.json b/dashboard/package.json index e57cd1bf..30177556 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -13,6 +13,7 @@ "test:ui": "vitest --ui" }, "dependencies": { + "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-javascript": "^6.2.4", "@codemirror/lang-json": "^6.0.2", "@codemirror/lang-python": "^6.2.1", diff --git a/dashboard/src/components/CodeEditor.tsx b/dashboard/src/components/CodeEditor.tsx index 224046ec..00438655 100644 --- a/dashboard/src/components/CodeEditor.tsx +++ b/dashboard/src/components/CodeEditor.tsx @@ -1,7 +1,8 @@ import { useEffect, useState } from 'react' import CodeMirror from '@uiw/react-codemirror' -import { python } from '@codemirror/lang-python' +import { css } from '@codemirror/lang-css' import { json, jsonParseLinter } from '@codemirror/lang-json' +import { python } from '@codemirror/lang-python' import { oneDark } from '@codemirror/theme-one-dark' import { EditorView } from '@codemirror/view' import { StreamLanguage } from '@codemirror/language' @@ -9,10 +10,11 @@ import { toml as tomlMode } from '@codemirror/legacy-modes/mode/toml' import { useTheme } from '@/components/use-theme' -export type Language = 'python' | 'json' | 'toml' | 'text' +export type Language = 'python' | 'json' | 'toml' | 'css' | 'text' interface CodeEditorProps { value: string + onChange?: (value: string) => void language?: Language readOnly?: boolean @@ -29,6 +31,7 @@ const languageExtensions: Record = { python: [python()], json: [json(), jsonParseLinter()], toml: [StreamLanguage.define(tomlMode)], + css: [css()], text: [], } diff --git a/dashboard/src/routes/settings.tsx b/dashboard/src/routes/settings.tsx index b9feb805..74d98380 100644 --- a/dashboard/src/routes/settings.tsx +++ b/dashboard/src/routes/settings.tsx @@ -1,7 +1,7 @@ import { Palette, Info, Shield, Eye, EyeOff, Copy, RefreshCw, Check, CheckCircle2, XCircle, AlertTriangle, Settings, RotateCcw, Database, Download, Upload, Trash2, HardDrive } from 'lucide-react' import { useTheme } from '@/components/use-theme' import { useAnimation } from '@/hooks/use-animation' -import { useState, useMemo, useRef } from 'react' +import { useState, useMemo, useRef, useCallback, useEffect } from 'react' import { useNavigate } from '@tanstack/react-router' import { cn } from '@/lib/utils' import { fetchWithAuth } from '@/lib/fetch-with-auth' @@ -49,6 +49,23 @@ import { import { getComputedTokens } from '@/lib/theme/pipeline' import { hexToHSL } from '@/lib/theme/palette' +import { defaultDarkTokens, defaultLightTokens } from '@/lib/theme/tokens' +import type { ThemeTokens } from '@/lib/theme/tokens' +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion' +import { CodeEditor } from '@/components/CodeEditor' +import { sanitizeCSS } from '@/lib/theme/sanitizer' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' export function SettingsPage() { return ( @@ -265,6 +282,25 @@ function hslToHex(hsl: string): string { function AppearanceTab() { const { theme, setTheme, themeConfig, updateThemeConfig, resolvedTheme } = useTheme() const { enableAnimations, setEnableAnimations, enableWavesBackground, setEnableWavesBackground } = useAnimation() + + const [localCSS, setLocalCSS] = useState(themeConfig.customCSS || '') + const [cssWarnings, setCssWarnings] = useState([]) + const cssDebounceRef = useRef | null>(null) + + useEffect(() => { + setLocalCSS(themeConfig.customCSS || '') + }, [themeConfig.customCSS]) + + const handleCSSChange = useCallback((val: string) => { + setLocalCSS(val) + const result = sanitizeCSS(val) + setCssWarnings(result.warnings) + + if (cssDebounceRef.current) clearTimeout(cssDebounceRef.current) + cssDebounceRef.current = setTimeout(() => { + updateThemeConfig({ customCSS: val }) + }, 500) + }, [updateThemeConfig]) const currentAccentHex = useMemo(() => { if (themeConfig.accentColor) { @@ -380,6 +416,453 @@ function AppearanceTab() { + {/* 样式微调 */} +
+

界面样式微调

+ + {/* 1. 字体排版 (Typography) */} + + 字体排版 (Typography) + +
+
+ +
+ +
+ + +
+ +
+
+ + + {parseFloat((themeConfig.tokenOverrides?.typography as any)?.['font-size-base'] || '1') * 16}px + +
+ { + updateThemeConfig({ + tokenOverrides: { + ...themeConfig.tokenOverrides, + typography: { + ...themeConfig.tokenOverrides?.typography, + 'font-size-base': `${vals[0] / 16}rem` + } + } + }) + }} + /> +
+ +
+ + +
+
+
+
+ + {/* 2. 视觉效果 (Visual) */} + + 视觉效果 (Visual) + +
+
+ +
+ +
+
+ + + {Math.round(parseFloat((themeConfig.tokenOverrides?.visual as any)?.['radius-md'] || '0.375') * 16)}px + +
+ { + updateThemeConfig({ + tokenOverrides: { + ...themeConfig.tokenOverrides, + visual: { + ...themeConfig.tokenOverrides?.visual, + 'radius-md': `${vals[0] / 16}rem` + } + } + }) + }} + /> +
+ +
+ + +
+ +
+ + { + updateThemeConfig({ + tokenOverrides: { + ...themeConfig.tokenOverrides, + visual: { + ...themeConfig.tokenOverrides?.visual, + 'blur-md': checked ? defaultLightTokens.visual['blur-md'] : '0px' + } + } + }) + }} + /> +
+
+
+
+ + {/* 3. 布局 (Layout) */} + + 布局 (Layout) + +
+
+ +
+ +
+
+ + + {(themeConfig.tokenOverrides?.layout as any)?.['sidebar-width'] || '16rem'} + +
+ { + updateThemeConfig({ + tokenOverrides: { + ...themeConfig.tokenOverrides, + layout: { + ...themeConfig.tokenOverrides?.layout, + 'sidebar-width': `${vals[0]}rem` + } + } + }) + }} + /> +
+ +
+
+ + + {(themeConfig.tokenOverrides?.layout as any)?.['max-content-width'] || '1280px'} + +
+ { + updateThemeConfig({ + tokenOverrides: { + ...themeConfig.tokenOverrides, + layout: { + ...themeConfig.tokenOverrides?.layout, + 'max-content-width': `${vals[0]}px` + } + } + }) + }} + /> +
+ +
+
+ + + {(themeConfig.tokenOverrides?.layout as any)?.['space-unit'] || '0.25rem'} + +
+ { + updateThemeConfig({ + tokenOverrides: { + ...themeConfig.tokenOverrides, + layout: { + ...themeConfig.tokenOverrides?.layout, + 'space-unit': `${vals[0]}rem` + } + } + }) + }} + /> +
+
+
+
+ + {/* 4. 动画 (Animation) */} + + 动画 (Animation) + +
+
+ +
+ +
+ + +
+
+
+
+
+
+ +
+
+
+

自定义 CSS

+

+ 编写自定义 CSS 来进一步个性化界面。危险的 CSS(如 @import、url())将被自动过滤。 +

+
+ +
+ +
+ + + {cssWarnings.length > 0 && ( +
+
+ + 以下内容已被安全过滤: +
+
    + {cssWarnings.map((w, i) =>
  • {w}
  • )} +
+
+ )} +
+
+ {/* 动效设置 */}

动画效果