diff --git a/dashboard/src/index.css b/dashboard/src/index.css index 7a5ca28c..e0968b47 100644 --- a/dashboard/src/index.css +++ b/dashboard/src/index.css @@ -13,60 +13,183 @@ @layer base { :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - --primary: 221.2 83.2% 53.3%; - --primary-foreground: 210 40% 98%; - --primary-gradient: none; /* 默认无渐变 */ - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 221.2 83.2% 53.3%; - --radius: 0.5rem; - --chart-1: 221.2 83.2% 53.3%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; + /* Color Tokens */ + --color-primary: 221.2 83.2% 53.3%; + --color-primary-foreground: 210 40% 98%; + --color-primary-gradient: none; + --color-secondary: 210 40% 96.1%; + --color-secondary-foreground: 222.2 47.4% 11.2%; + --color-muted: 210 40% 96.1%; + --color-muted-foreground: 215.4 16.3% 46.9%; + --color-accent: 210 40% 96.1%; + --color-accent-foreground: 222.2 47.4% 11.2%; + --color-destructive: 0 84.2% 60.2%; + --color-destructive-foreground: 210 40% 98%; + --color-background: 0 0% 100%; + --color-foreground: 222.2 84% 4.9%; + --color-card: 0 0% 100%; + --color-card-foreground: 222.2 84% 4.9%; + --color-popover: 0 0% 100%; + --color-popover-foreground: 222.2 84% 4.9%; + --color-border: 214.3 31.8% 91.4%; + --color-input: 214.3 31.8% 91.4%; + --color-ring: 221.2 83.2% 53.3%; + --color-chart-1: 221.2 83.2% 53.3%; + --color-chart-2: 160 60% 45%; + --color-chart-3: 30 80% 55%; + --color-chart-4: 280 65% 60%; + --color-chart-5: 340 75% 55%; + + /* Typography Tokens */ + --typography-font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --typography-font-family-code: "JetBrains Mono", "Monaco", "Courier New", monospace; + --typography-font-size-xs: 0.75rem; + --typography-font-size-sm: 0.875rem; + --typography-font-size-base: 1rem; + --typography-font-size-lg: 1.125rem; + --typography-font-size-xl: 1.25rem; + --typography-font-size-2xl: 1.5rem; + --typography-font-weight-normal: 400; + --typography-font-weight-medium: 500; + --typography-font-weight-semibold: 600; + --typography-font-weight-bold: 700; + --typography-line-height-tight: 1.2; + --typography-line-height-normal: 1.5; + --typography-line-height-relaxed: 1.75; + --typography-letter-spacing-tight: -0.02em; + --typography-letter-spacing-normal: 0em; + --typography-letter-spacing-wide: 0.02em; + + /* Visual Tokens */ + --visual-radius-sm: 0.25rem; + --visual-radius-md: 0.375rem; + --visual-radius-lg: 0.5rem; + --visual-radius-xl: 0.75rem; + --visual-radius-full: 9999px; + --visual-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --visual-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --visual-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + --visual-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1); + --visual-blur-sm: 4px; + --visual-blur-md: 12px; + --visual-blur-lg: 24px; + --visual-opacity-disabled: 0.5; + --visual-opacity-hover: 0.8; + --visual-opacity-overlay: 0.75; + + /* Layout Tokens */ + --layout-space-unit: 0.25rem; + --layout-space-xs: 0.5rem; + --layout-space-sm: 0.75rem; + --layout-space-md: 1rem; + --layout-space-lg: 1.5rem; + --layout-space-xl: 2rem; + --layout-space-2xl: 3rem; + --layout-sidebar-width: 16rem; + --layout-header-height: 3.5rem; + --layout-max-content-width: 1280px; + + /* Animation Tokens */ + --animation-anim-duration-fast: 150ms; + --animation-anim-duration-normal: 300ms; + --animation-anim-duration-slow: 500ms; + --animation-anim-easing-default: cubic-bezier(0.4, 0, 0.2, 1); + --animation-anim-easing-in: cubic-bezier(0.4, 0, 1, 1); + --animation-anim-easing-out: cubic-bezier(0, 0, 0.2, 1); + --animation-anim-easing-in-out: cubic-bezier(0.4, 0, 0.2, 1); + --animation-transition-colors: color 300ms cubic-bezier(0.4, 0, 0.2, 1); + --animation-transition-transform: transform 300ms cubic-bezier(0.4, 0, 0.2, 1); + --animation-transition-opacity: opacity 300ms cubic-bezier(0.4, 0, 0.2, 1); + + /* Legacy Aliases (backward compatibility) */ + --background: var(--color-background); + --foreground: var(--color-foreground); + --card: var(--color-card); + --card-foreground: var(--color-card-foreground); + --popover: var(--color-popover); + --popover-foreground: var(--color-popover-foreground); + --primary: var(--color-primary); + --primary-foreground: var(--color-primary-foreground); + --primary-gradient: var(--color-primary-gradient); + --secondary: var(--color-secondary); + --secondary-foreground: var(--color-secondary-foreground); + --muted: var(--color-muted); + --muted-foreground: var(--color-muted-foreground); + --accent: var(--color-accent); + --accent-foreground: var(--color-accent-foreground); + --destructive: var(--color-destructive); + --destructive-foreground: var(--color-destructive-foreground); + --border: var(--color-border); + --input: var(--color-input); + --ring: var(--color-ring); + --radius: var(--visual-radius-lg); + --chart-1: var(--color-chart-1); + --chart-2: var(--color-chart-2); + --chart-3: var(--color-chart-3); + --chart-4: var(--color-chart-4); + --chart-5: var(--color-chart-5); } .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 210 40% 98%; - --primary-gradient: none; /* 默认无渐变 */ - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 224.3 76.3% 48%; - --chart-1: 217.2 91.2% 59.8%; - --chart-2: 160 60% 50%; - --chart-3: 30 80% 60%; - --chart-4: 280 65% 65%; - --chart-5: 340 75% 60%; + /* Color Tokens */ + --color-primary: 217.2 91.2% 59.8%; + --color-primary-foreground: 210 40% 98%; + --color-primary-gradient: none; + --color-secondary: 217.2 32.6% 17.5%; + --color-secondary-foreground: 210 40% 98%; + --color-muted: 217.2 32.6% 17.5%; + --color-muted-foreground: 215 20.2% 65.1%; + --color-accent: 217.2 32.6% 17.5%; + --color-accent-foreground: 210 40% 98%; + --color-destructive: 0 62.8% 30.6%; + --color-destructive-foreground: 210 40% 98%; + --color-background: 222.2 84% 4.9%; + --color-foreground: 210 40% 98%; + --color-card: 222.2 84% 4.9%; + --color-card-foreground: 210 40% 98%; + --color-popover: 222.2 84% 4.9%; + --color-popover-foreground: 210 40% 98%; + --color-border: 217.2 32.6% 17.5%; + --color-input: 217.2 32.6% 17.5%; + --color-ring: 224.3 76.3% 48%; + --color-chart-1: 217.2 91.2% 59.8%; + --color-chart-2: 160 60% 50%; + --color-chart-3: 30 80% 60%; + --color-chart-4: 280 65% 65%; + --color-chart-5: 340 75% 60%; + + /* Visual Tokens (dark mode shadows) */ + --visual-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.25); + --visual-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3); + --visual-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4); + --visual-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5); + + /* Legacy Aliases (backward compatibility) */ + --background: var(--color-background); + --foreground: var(--color-foreground); + --card: var(--color-card); + --card-foreground: var(--color-card-foreground); + --popover: var(--color-popover); + --popover-foreground: var(--color-popover-foreground); + --primary: var(--color-primary); + --primary-foreground: var(--color-primary-foreground); + --primary-gradient: var(--color-primary-gradient); + --secondary: var(--color-secondary); + --secondary-foreground: var(--color-secondary-foreground); + --muted: var(--color-muted); + --muted-foreground: var(--color-muted-foreground); + --accent: var(--color-accent); + --accent-foreground: var(--color-accent-foreground); + --destructive: var(--color-destructive); + --destructive-foreground: var(--color-destructive-foreground); + --border: var(--color-border); + --input: var(--color-input); + --ring: var(--color-ring); + --chart-1: var(--color-chart-1); + --chart-2: var(--color-chart-2); + --chart-3: var(--color-chart-3); + --chart-4: var(--color-chart-4); + --chart-5: var(--color-chart-5); } } @@ -92,28 +215,24 @@ } @layer utilities { - /* 渐变色背景工具类 */ .bg-primary-gradient { - background: var(--primary-gradient, hsl(var(--primary))); + background: var(--color-primary-gradient, hsl(var(--color-primary))); } - /* 渐变色文字工具类 - 默认使用普通文字颜色 */ .text-primary-gradient { - color: hsl(var(--primary)); + color: hsl(var(--color-primary)); } - /* 当应用了 has-gradient 类时,使用渐变文字效果 */ .has-gradient .text-primary-gradient { - background: var(--primary-gradient); + background: var(--color-primary-gradient); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: transparent; } - /* 渐变色边框工具类 */ .border-primary-gradient { - border-image: var(--primary-gradient, linear-gradient(to right, hsl(var(--primary)), hsl(var(--primary)))) 1; + border-image: var(--color-primary-gradient, linear-gradient(to right, hsl(var(--color-primary)), hsl(var(--color-primary)))) 1; } } @@ -170,10 +289,9 @@ pointer-events: auto; } -/* 自定义滚动条样式 */ .custom-scrollbar { scrollbar-width: thin; - scrollbar-color: hsl(var(--border)) transparent; + scrollbar-color: hsl(var(--color-border)) transparent; } .custom-scrollbar::-webkit-scrollbar { @@ -187,14 +305,14 @@ } .custom-scrollbar::-webkit-scrollbar-thumb { - background: hsl(var(--border)); + background: hsl(var(--color-border)); border-radius: 4px; border: 2px solid transparent; background-clip: content-box; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { - background: hsl(var(--muted-foreground) / 0.5); + background: hsl(var(--color-muted-foreground) / 0.5); background-clip: content-box; } diff --git a/dashboard/src/routes/settings.tsx b/dashboard/src/routes/settings.tsx index ce3a10a7..3e02e467 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, useEffect, useRef } from 'react' +import { useState, useMemo, useRef } from 'react' import { useNavigate } from '@tanstack/react-router' import { cn } from '@/lib/utils' import { fetchWithAuth } from '@/lib/fetch-with-auth' @@ -47,6 +47,9 @@ import { AlertDialogTrigger, } from '@/components/ui/alert-dialog' +import { getComputedTokens } from '@/lib/theme/pipeline' +import { hexToHSL } from '@/lib/theme/palette' + export function SettingsPage() { return (
@@ -222,26 +225,68 @@ function applyAccentColor(color: string) { } } +// 辅助函数:将 HSL 字符串转换为 HEX +function hslToHex(hsl: string): string { + if (!hsl) return '#000000' + + // 解析 "221.2 83.2% 53.3%" 格式 + const parts = hsl.split(' ').filter(Boolean) + if (parts.length < 3) return '#000000' + + const h = parseFloat(parts[0]) + const s = parseFloat(parts[1].replace('%', '')) + const l = parseFloat(parts[2].replace('%', '')) + + const sDecimal = s / 100 + const lDecimal = l / 100 + + const c = (1 - Math.abs(2 * lDecimal - 1)) * sDecimal + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)) + const m = lDecimal - c / 2 + + let r = 0, g = 0, b = 0 + + if (0 <= h && h < 60) { r = c; g = x; b = 0 } + else if (60 <= h && h < 120) { r = x; g = c; b = 0 } + else if (120 <= h && h < 180) { r = 0; g = c; b = x } + else if (180 <= h && h < 240) { r = 0; g = x; b = c } + else if (240 <= h && h < 300) { r = x; g = 0; b = c } + else if (300 <= h && h < 360) { r = c; g = 0; b = x } + + const toHex = (n: number) => { + const hex = Math.round((n + m) * 255).toString(16) + return hex.length === 1 ? '0' + hex : hex + } + + return `#${toHex(r)}${toHex(g)}${toHex(b)}` +} + // 外观设置标签页 function AppearanceTab() { - const { theme, setTheme } = useTheme() + const { theme, setTheme, themeConfig, updateThemeConfig, resolvedTheme } = useTheme() const { enableAnimations, setEnableAnimations, enableWavesBackground, setEnableWavesBackground } = useAnimation() - const [accentColor, setAccentColor] = useState(() => { - return localStorage.getItem('accent-color') || 'blue' - }) - // 页面加载时应用保存的主题色 - useEffect(() => { - const savedColor = localStorage.getItem('accent-color') || 'blue' - applyAccentColor(savedColor) - }, []) + const currentAccentHex = useMemo(() => { + if (themeConfig.accentColor) { + return hslToHex(themeConfig.accentColor) + } + return '#3b82f6' // 默认蓝色 + }, [themeConfig.accentColor]) - const handleAccentColorChange = (color: string) => { - setAccentColor(color) - localStorage.setItem('accent-color', color) - applyAccentColor(color) + const handleAccentColorChange = (e: React.ChangeEvent) => { + const hex = e.target.value + const hsl = hexToHSL(hex) + updateThemeConfig({ accentColor: hsl }) } + const handleResetAccent = () => { + updateThemeConfig({ accentColor: '' }) + } + + const previewTokens = useMemo(() => { + return getComputedTokens(themeConfig, resolvedTheme === 'dark').color + }, [themeConfig, resolvedTheme]) + return (
{/* 主题模式 */} @@ -272,135 +317,65 @@ function AppearanceTab() {
- {/* 主题色 */} + {/* 主题色配置 */}
-

主题色

+
+

主题色

+ +
- {/* 单色预设 */} -
-
-

单色

-
- - - - - - -
-
- - {/* 渐变色预设 */} -
-

渐变色

-
- - - - - - -
-
- - {/* 自定义颜色选择器 */} -
-

自定义颜色

-
-
+
+ {/* 颜色选择器 */} +
+
+
handleAccentColorChange(e.target.value)} - className="h-10 sm:h-12 w-full rounded-lg border-2 border-border cursor-pointer" - title="选择自定义颜色" + value={currentAccentHex} + onChange={handleAccentColorChange} + className="absolute inset-0 w-[150%] h-[150%] -top-1/4 -left-1/4 cursor-pointer p-0 border-0" />
-
- handleAccentColorChange(e.target.value)} - placeholder="#3b82f6" - className="font-mono text-sm" - /> +
+ +

点击色环选择或输入 HEX 值

-

- 点击色块选择颜色,或手动输入 HEX 颜色代码 -

+ +
+ +
+
+ + {/* 实时色板预览 */} +
+

实时色板预览

+
+ + + + + + + + +
@@ -452,6 +427,22 @@ function AppearanceTab() { ) } +function ColorTokenPreview({ name, value, foreground, border }: { name: string, value: string, foreground?: string, border?: boolean }) { + return ( +
+
+ Aa +
+
+ {name} +
+
+ ) +} + // 安全设置标签页 function SecurityTab() { const navigate = useNavigate() diff --git a/dashboard/tailwind.config.js b/dashboard/tailwind.config.js index d8d73d5b..38a487c0 100644 --- a/dashboard/tailwind.config.js +++ b/dashboard/tailwind.config.js @@ -5,40 +5,61 @@ export default { theme: { extend: { colors: { - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', + border: 'hsl(var(--color-border))', + input: 'hsl(var(--color-input))', + ring: 'hsl(var(--color-ring))', + background: 'hsl(var(--color-background))', + foreground: 'hsl(var(--color-foreground))', primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', + DEFAULT: 'hsl(var(--color-primary))', + foreground: 'hsl(var(--color-primary-foreground))', }, secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', + DEFAULT: 'hsl(var(--color-secondary))', + foreground: 'hsl(var(--color-secondary-foreground))', }, muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', + DEFAULT: 'hsl(var(--color-muted))', + foreground: 'hsl(var(--color-muted-foreground))', }, accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', + DEFAULT: 'hsl(var(--color-accent))', + foreground: 'hsl(var(--color-accent-foreground))', }, card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', + DEFAULT: 'hsl(var(--color-card))', + foreground: 'hsl(var(--color-card-foreground))', }, popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', + DEFAULT: 'hsl(var(--color-popover))', + foreground: 'hsl(var(--color-popover-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--color-destructive))', + foreground: 'hsl(var(--color-destructive-foreground))', }, }, borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)', + lg: 'var(--visual-radius-lg)', + md: 'var(--visual-radius-md)', + sm: 'var(--visual-radius-sm)', + xl: 'var(--visual-radius-xl)', + full: 'var(--visual-radius-full)', + }, + fontFamily: { + sans: 'var(--typography-font-family-base)', + mono: 'var(--typography-font-family-code)', + }, + boxShadow: { + sm: 'var(--visual-shadow-sm)', + md: 'var(--visual-shadow-md)', + lg: 'var(--visual-shadow-lg)', + xl: 'var(--visual-shadow-xl)', + }, + transitionDuration: { + fast: 'var(--animation-anim-duration-fast)', + normal: 'var(--animation-anim-duration-normal)', + slow: 'var(--animation-anim-duration-slow)', }, keyframes: { 'slide-in-from-right': {