Files
X-Financial/web/src/composables/useThemeSkin.js
caoxiaozhu 5753899eb3 feat(web): 主题皮肤系统与 LLM 设置面板重构
- useThemeSkin 重构主题皮肤应用逻辑,支持企业 AI 风格主题切换
- settingsModelHelper 新增主题与模型表字段映射,useSettings 适配
- LlmSettingsPanel/SettingsView 面板重构,支持多模型行编辑与主题区块
- settings-view.css 适配主题样式,新增 settings-theme-section 测试,更新 settings-llm-section 测试
2026-06-26 22:42:00 +08:00

246 lines
7.7 KiB
JavaScript

import { computed, ref } from 'vue'
const THEME_SKIN_STORAGE_KEY = 'x-financial-theme-skin'
const DEFAULT_THEME_SKIN_ID = 'enterprise'
const DEFAULT_SEMANTIC_COLORS = {
success: '#2f855a',
successHover: '#276749',
successActive: '#22543d',
successSoft: '#f0f7f2',
successLine: '#cde6d5',
warning: '#b7791f',
warningHover: '#975a16',
warningActive: '#7b4514',
warningSoft: '#fff8eb',
warningLine: '#efd9af',
danger: '#dc2626',
dangerHover: '#b91c1c',
dangerActive: '#991b1b',
dangerSoft: '#fef2f2',
dangerLine: '#fecaca',
info: '#475569',
infoHover: '#334155',
infoActive: '#1e293b',
infoSoft: '#f1f5f9',
infoLine: '#cbd5e1'
}
export const THEME_SKIN_OPTIONS = [
{
id: 'vivid',
label: '动感活泼',
desc: '保留当前 AI 助手的明快节奏,适合演示、培训和轻量工作台。',
keywords: ['明快', '渐变', '助手感'],
primary: '#2f7cff',
primaryHover: '#2563eb',
primaryActive: '#1d4ed8',
primarySoft: '#eef6ff',
primarySoftStrong: '#dbeafe',
secondary: '#7c5cff',
chartBlue: '#2f7cff',
chartPurple: '#7c5cff',
chartAmber: '#f59e0b'
},
{
id: 'enterprise',
label: '企业沉稳',
desc: '低饱和、轻描边、少渲染,适合正式生产环境和企业级财务 SaaS。',
keywords: ['克制', '结构化', '低噪声'],
primary: '#475569',
primaryHover: '#3f4a5a',
primaryActive: '#334155',
primarySoft: '#f1f5f9',
primarySoftStrong: '#e2e8f0',
secondary: '#64748b',
chartBlue: '#5d7590',
chartPurple: '#6b7280',
chartAmber: '#9a7a45'
},
{
id: 'intelligent',
label: '专业智能',
desc: '保留少量智能识别感,同时控制饱和度,适合稳定办公和 AI 辅助并重的团队。',
keywords: ['智能', '专业', '轻点缀'],
primary: '#5f6f9f',
primaryHover: '#53618b',
primaryActive: '#465275',
primarySoft: '#f3f4fb',
primarySoftStrong: '#e2e5f4',
secondary: '#477c9e',
chartBlue: '#4f7495',
chartPurple: '#6d6a9f',
chartAmber: '#a98857'
}
]
const activeThemeSkinId = ref(DEFAULT_THEME_SKIN_ID)
const THEME_MODE_IDS = new Set(THEME_SKIN_OPTIONS.map((skin) => skin.id))
const LEGACY_THEME_MODE_MAP = {
sky: 'vivid',
blue: 'vivid',
emerald: 'vivid',
teal: 'vivid',
'legacy-green': 'vivid',
navy: 'enterprise',
slate: 'enterprise',
sage: 'enterprise',
gray: 'enterprise',
grey: 'enterprise',
purple: 'intelligent',
violet: 'intelligent',
'soft-violet': 'intelligent'
}
export function normalizeThemeMode(value) {
const normalized = String(value ?? '').trim()
if (THEME_MODE_IDS.has(normalized)) {
return normalized
}
return LEGACY_THEME_MODE_MAP[normalized] || DEFAULT_THEME_SKIN_ID
}
function findThemeSkin(id) {
const themeMode = normalizeThemeMode(id)
return THEME_SKIN_OPTIONS.find((skin) => skin.id === themeMode) || THEME_SKIN_OPTIONS[0]
}
function hexToRgb(hex) {
const normalized = String(hex || '').replace('#', '')
const value = normalized.length === 3
? normalized.split('').map((item) => item + item).join('')
: normalized
const numberValue = Number.parseInt(value, 16)
if (!Number.isFinite(numberValue) || value.length !== 6) {
return '58, 124, 165'
}
return [
(numberValue >> 16) & 255,
(numberValue >> 8) & 255,
numberValue & 255
].join(', ')
}
function setVariables(root, variables) {
Object.entries(variables).forEach(([key, value]) => {
root.style.setProperty(key, value)
})
}
function applyThemeSkin(skin) {
if (typeof document === 'undefined') {
return
}
const root = document.documentElement
const primaryRgb = hexToRgb(skin.primary)
const secondaryRgb = hexToRgb(skin.secondary)
const successRgb = hexToRgb(DEFAULT_SEMANTIC_COLORS.success)
const warningRgb = hexToRgb(DEFAULT_SEMANTIC_COLORS.warning)
const dangerRgb = hexToRgb(DEFAULT_SEMANTIC_COLORS.danger)
const infoRgb = hexToRgb(DEFAULT_SEMANTIC_COLORS.info)
root.dataset.themeSkin = skin.id
root.dataset.themeMode = skin.id
setVariables(root, {
'--primary': skin.primary,
'--primary-hover': skin.primaryHover,
'--primary-active': skin.primaryActive,
'--primary-soft': skin.primarySoft,
'--primary-soft-strong': skin.primarySoftStrong,
'--primary-rgb': primaryRgb,
'--secondary': skin.secondary,
'--secondary-rgb': secondaryRgb,
'--theme-secondary': skin.secondary,
'--theme-secondary-rgb': secondaryRgb,
'--theme-primary': skin.primary,
'--theme-primary-hover': skin.primaryHover,
'--theme-primary-active': skin.primaryActive,
'--theme-primary-soft': skin.primarySoft,
'--theme-primary-soft-strong': skin.primarySoftStrong,
'--theme-primary-light-5': 'color-mix(in srgb, var(--theme-primary) 46%, white)',
'--theme-primary-light-9': 'color-mix(in srgb, var(--theme-primary) 8%, white)',
'--theme-primary-rgb': primaryRgb,
'--theme-primary-shadow': `rgba(${primaryRgb}, 0.16)`,
'--theme-focus-ring': `rgba(${primaryRgb}, 0.12)`,
'--theme-gradient-primary': `linear-gradient(135deg, ${skin.primary}, ${skin.primaryActive})`,
'--chart-primary': skin.primary,
'--chart-primary-rgb': primaryRgb,
'--chart-blue': skin.chartBlue,
'--chart-purple': skin.chartPurple,
'--chart-amber': skin.chartAmber,
'--success': DEFAULT_SEMANTIC_COLORS.success,
'--success-hover': DEFAULT_SEMANTIC_COLORS.successHover,
'--success-active': DEFAULT_SEMANTIC_COLORS.successActive,
'--success-soft': DEFAULT_SEMANTIC_COLORS.successSoft,
'--success-line': DEFAULT_SEMANTIC_COLORS.successLine,
'--success-rgb': successRgb,
'--warning': DEFAULT_SEMANTIC_COLORS.warning,
'--warning-hover': DEFAULT_SEMANTIC_COLORS.warningHover,
'--warning-active': DEFAULT_SEMANTIC_COLORS.warningActive,
'--warning-soft': DEFAULT_SEMANTIC_COLORS.warningSoft,
'--warning-line': DEFAULT_SEMANTIC_COLORS.warningLine,
'--warning-rgb': warningRgb,
'--danger': DEFAULT_SEMANTIC_COLORS.danger,
'--danger-hover': DEFAULT_SEMANTIC_COLORS.dangerHover,
'--danger-active': DEFAULT_SEMANTIC_COLORS.dangerActive,
'--danger-soft': DEFAULT_SEMANTIC_COLORS.dangerSoft,
'--danger-line': DEFAULT_SEMANTIC_COLORS.dangerLine,
'--danger-rgb': dangerRgb,
'--info': DEFAULT_SEMANTIC_COLORS.info,
'--info-hover': DEFAULT_SEMANTIC_COLORS.infoHover,
'--info-active': DEFAULT_SEMANTIC_COLORS.infoActive,
'--info-soft': DEFAULT_SEMANTIC_COLORS.infoSoft,
'--info-line': DEFAULT_SEMANTIC_COLORS.infoLine,
'--info-rgb': infoRgb,
'--el-color-primary': skin.primary,
'--el-color-primary-dark-2': skin.primaryActive,
'--el-color-primary-light-3': skin.primaryHover,
'--el-color-primary-light-5': 'var(--theme-primary-light-5)',
'--el-color-primary-light-7': skin.primarySoftStrong,
'--el-color-primary-light-8': skin.primarySoft,
'--el-color-primary-light-9': 'var(--theme-primary-light-9)'
})
}
function readStoredThemeSkinId() {
if (typeof window === 'undefined') {
return DEFAULT_THEME_SKIN_ID
}
const stored = window.localStorage.getItem(THEME_SKIN_STORAGE_KEY)
return findThemeSkin(stored).id
}
export function installThemeSkin() {
const skin = findThemeSkin(readStoredThemeSkinId())
activeThemeSkinId.value = skin.id
applyThemeSkin(skin)
}
export function setThemeSkin(id) {
const skin = findThemeSkin(id)
activeThemeSkinId.value = skin.id
applyThemeSkin(skin)
if (typeof window !== 'undefined') {
window.localStorage.setItem(THEME_SKIN_STORAGE_KEY, skin.id)
}
return skin.id
}
export function useThemeSkin() {
return {
activeThemeSkin: computed(() => findThemeSkin(activeThemeSkinId.value)),
activeThemeSkinId,
setThemeSkin,
themeSkinOptions: THEME_SKIN_OPTIONS
}
}