@@ -101,147 +122,148 @@ export function SettingsPage() {
)
}
-// 应用主题色的辅助函数
-function applyAccentColor(color: string) {
- const root = document.documentElement
+// 辅助函数:将 HSL 字符串转换为 HEX
+function hslToHex(hsl: string): string {
+ if (!hsl) return '#000000'
- // 预设颜色配置
- const colors = {
- // 单色
- blue: {
- hsl: '221.2 83.2% 53.3%',
- darkHsl: '217.2 91.2% 59.8%',
- gradient: null
- },
- purple: {
- hsl: '271 91% 65%',
- darkHsl: '270 95% 75%',
- gradient: null
- },
- green: {
- hsl: '142 71% 45%',
- darkHsl: '142 76% 36%',
- gradient: null
- },
- orange: {
- hsl: '25 95% 53%',
- darkHsl: '20 90% 48%',
- gradient: null
- },
- pink: {
- hsl: '330 81% 60%',
- darkHsl: '330 85% 70%',
- gradient: null
- },
- red: {
- hsl: '0 84% 60%',
- darkHsl: '0 90% 70%',
- gradient: null
- },
-
- // 渐变色
- 'gradient-sunset': {
- hsl: '15 95% 60%',
- darkHsl: '15 95% 65%',
- gradient: 'linear-gradient(135deg, hsl(25 95% 53%) 0%, hsl(330 81% 60%) 100%)'
- },
- 'gradient-ocean': {
- hsl: '200 90% 55%',
- darkHsl: '200 90% 60%',
- gradient: 'linear-gradient(135deg, hsl(221.2 83.2% 53.3%) 0%, hsl(189 94% 43%) 100%)'
- },
- 'gradient-forest': {
- hsl: '150 70% 45%',
- darkHsl: '150 75% 40%',
- gradient: 'linear-gradient(135deg, hsl(142 71% 45%) 0%, hsl(158 64% 52%) 100%)'
- },
- 'gradient-aurora': {
- hsl: '310 85% 65%',
- darkHsl: '310 90% 70%',
- gradient: 'linear-gradient(135deg, hsl(271 91% 65%) 0%, hsl(330 81% 60%) 100%)'
- },
- 'gradient-fire': {
- hsl: '15 95% 55%',
- darkHsl: '15 95% 60%',
- gradient: 'linear-gradient(135deg, hsl(0 84% 60%) 0%, hsl(25 95% 53%) 100%)'
- },
- 'gradient-twilight': {
- hsl: '250 90% 60%',
- darkHsl: '250 95% 65%',
- gradient: 'linear-gradient(135deg, hsl(239 84% 67%) 0%, hsl(271 91% 65%) 100%)'
- },
- }
-
- const selectedColor = colors[color as keyof typeof colors]
- if (selectedColor) {
- // 设置主色
- root.style.setProperty('--primary', selectedColor.hsl)
-
- // 设置渐变(如果有)
- if (selectedColor.gradient) {
- root.style.setProperty('--primary-gradient', selectedColor.gradient)
- root.classList.add('has-gradient')
- } else {
- root.style.removeProperty('--primary-gradient')
- root.classList.remove('has-gradient')
- }
- } else if (color.startsWith('#')) {
- // 自定义颜色 - 将 HEX 转换为 HSL
- const hexToHsl = (hex: string) => {
- // 移除 # 号
- hex = hex.replace('#', '')
-
- // 转换为 RGB
- const r = parseInt(hex.substring(0, 2), 16) / 255
- const g = parseInt(hex.substring(2, 4), 16) / 255
- const b = parseInt(hex.substring(4, 6), 16) / 255
-
- const max = Math.max(r, g, b)
- const min = Math.min(r, g, b)
- let h = 0
- let s = 0
- const l = (max + min) / 2
-
- if (max !== min) {
- const d = max - min
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
-
- switch (max) {
- case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break
- case g: h = ((b - r) / d + 2) / 6; break
- case b: h = ((r - g) / d + 4) / 6; break
- }
- }
-
- return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`
- }
-
- root.style.setProperty('--primary', hexToHsl(color))
- root.style.removeProperty('--primary-gradient')
- root.classList.remove('has-gradient')
+ // 解析 "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 (h >= 0 && h < 60) { r = c; g = x; b = 0 }
+ else if (h >= 60 && h < 120) { r = x; g = c; b = 0 }
+ else if (h >= 120 && h < 180) { r = 0; g = c; b = x }
+ else if (h >= 180 && h < 240) { r = 0; g = x; b = c }
+ else if (h >= 240 && h < 300) { r = x; g = 0; b = c }
+ else if (h >= 300 && 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, resetTheme } = useTheme()
const { enableAnimations, setEnableAnimations, enableWavesBackground, setEnableWavesBackground } = useAnimation()
- const [accentColor, setAccentColor] = useState(() => {
- return localStorage.getItem('accent-color') || 'blue'
- })
+ const { toast } = useToast()
+
+ const [localCSS, setLocalCSS] = useState(themeConfig.customCSS || '')
+ const [cssWarnings, setCssWarnings] = useState
([])
+ const cssDebounceRef = useRef | null>(null)
+ const fileInputRef = useRef(null)
- // 页面加载时应用保存的主题色
- useEffect(() => {
- const savedColor = localStorage.getItem('accent-color') || 'blue'
- applyAccentColor(savedColor)
- }, [])
+ const updateTokenSection = useCallback(
+ (section: K, partial: Partial) => {
+ updateThemeConfig({
+ tokenOverrides: {
+ ...themeConfig.tokenOverrides,
+ [section]: {
+ ...defaultLightTokens[section],
+ ...themeConfig.tokenOverrides?.[section],
+ ...partial,
+ } as ThemeTokens[K],
+ },
+ })
+ },
+ [themeConfig.tokenOverrides, updateThemeConfig]
+ )
- const handleAccentColorChange = (color: string) => {
- setAccentColor(color)
- localStorage.setItem('accent-color', color)
- applyAccentColor(color)
+ const resetTokenSection = useCallback(
+ (section: keyof ThemeTokens) => {
+ const newOverrides: Partial = { ...themeConfig.tokenOverrides }
+ delete newOverrides[section]
+ updateThemeConfig({ tokenOverrides: newOverrides })
+ },
+ [themeConfig.tokenOverrides, updateThemeConfig]
+ )
+
+ 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) {
+ return hslToHex(themeConfig.accentColor)
+ }
+ return '#3b82f6' // 默认蓝色
+ }, [themeConfig.accentColor])
+
+ const handleAccentColorChange = (e: React.ChangeEvent) => {
+ const hex = e.target.value
+ const hsl = hexToHSL(hex)
+ updateThemeConfig({ accentColor: hsl })
}
+ const handleResetAccent = () => {
+ updateThemeConfig({ accentColor: '' })
+ }
+
+ const handleExport = () => {
+ const json = exportThemeJSON()
+ const blob = new Blob([json], { type: 'application/json' })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = `maibot-theme-${Date.now()}.json`
+ a.click()
+ URL.revokeObjectURL(url)
+ }
+
+ const handleImport = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0]
+ if (!file) return
+ const reader = new FileReader()
+ reader.onload = (ev) => {
+ const json = ev.target?.result as string
+ const result = importThemeJSON(json)
+ if (result.success) {
+ // 导入成功后需要刷新页面使配置生效(因为 ThemeProvider 需要重新读取 localStorage)
+ toast({ title: '导入成功', description: '主题配置已导入,页面将自动刷新' })
+ setTimeout(() => window.location.reload(), 1000)
+ } else {
+ toast({ title: '导入失败', description: result.errors.join('; '), variant: 'destructive' })
+ }
+ }
+ reader.readAsText(file)
+ // 重置 input,允许重复选择同一文件
+ e.target.value = ''
+ }
+
+ const handleResetTheme = () => {
+ resetTheme()
+ setLocalCSS('')
+ setCssWarnings([])
+ toast({ title: '重置成功', description: '主题已重置为默认值' })
+ }
+
+ const previewTokens = useMemo(() => {
+ return getComputedTokens(themeConfig, resolvedTheme === 'dark').color
+ }, [themeConfig, resolvedTheme])
+
return (
{/* 主题模式 */}
@@ -272,136 +294,438 @@ function AppearanceTab() {
- {/* 主题色 */}
+ {/* 主题色配置 */}