2026-05-27 09:17:57 +08:00
|
|
|
import { computed, ref } from 'vue'
|
|
|
|
|
|
|
|
|
|
const THEME_SKIN_STORAGE_KEY = 'x-financial-theme-skin'
|
2026-06-26 22:42:00 +08:00
|
|
|
const DEFAULT_THEME_SKIN_ID = 'enterprise'
|
2026-05-27 09:17:57 +08:00
|
|
|
|
|
|
|
|
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 = [
|
|
|
|
|
{
|
2026-06-26 22:42:00 +08:00
|
|
|
id: 'vivid',
|
|
|
|
|
label: '动感活泼',
|
|
|
|
|
desc: '保留当前 AI 助手的明快节奏,适合演示、培训和轻量工作台。',
|
|
|
|
|
keywords: ['明快', '渐变', '助手感'],
|
|
|
|
|
primary: '#2f7cff',
|
|
|
|
|
primaryHover: '#2563eb',
|
|
|
|
|
primaryActive: '#1d4ed8',
|
|
|
|
|
primarySoft: '#eef6ff',
|
|
|
|
|
primarySoftStrong: '#dbeafe',
|
|
|
|
|
secondary: '#7c5cff',
|
|
|
|
|
chartBlue: '#2f7cff',
|
|
|
|
|
chartPurple: '#7c5cff',
|
|
|
|
|
chartAmber: '#f59e0b'
|
2026-05-27 09:17:57 +08:00
|
|
|
},
|
|
|
|
|
{
|
2026-06-26 22:42:00 +08:00
|
|
|
id: 'enterprise',
|
|
|
|
|
label: '企业沉稳',
|
|
|
|
|
desc: '低饱和、轻描边、少渲染,适合正式生产环境和企业级财务 SaaS。',
|
|
|
|
|
keywords: ['克制', '结构化', '低噪声'],
|
|
|
|
|
primary: '#475569',
|
|
|
|
|
primaryHover: '#3f4a5a',
|
|
|
|
|
primaryActive: '#334155',
|
2026-05-27 09:17:57 +08:00
|
|
|
primarySoft: '#f1f5f9',
|
|
|
|
|
primarySoftStrong: '#e2e8f0',
|
2026-06-26 22:42:00 +08:00
|
|
|
secondary: '#64748b',
|
2026-05-27 09:17:57 +08:00
|
|
|
chartBlue: '#5d7590',
|
2026-06-26 22:42:00 +08:00
|
|
|
chartPurple: '#6b7280',
|
|
|
|
|
chartAmber: '#9a7a45'
|
2026-05-27 09:17:57 +08:00
|
|
|
},
|
|
|
|
|
{
|
2026-06-26 22:42:00 +08:00
|
|
|
id: 'intelligent',
|
|
|
|
|
label: '专业智能',
|
|
|
|
|
desc: '保留少量智能识别感,同时控制饱和度,适合稳定办公和 AI 辅助并重的团队。',
|
|
|
|
|
keywords: ['智能', '专业', '轻点缀'],
|
|
|
|
|
primary: '#5f6f9f',
|
|
|
|
|
primaryHover: '#53618b',
|
|
|
|
|
primaryActive: '#465275',
|
|
|
|
|
primarySoft: '#f3f4fb',
|
|
|
|
|
primarySoftStrong: '#e2e5f4',
|
2026-05-27 09:17:57 +08:00
|
|
|
secondary: '#477c9e',
|
|
|
|
|
chartBlue: '#4f7495',
|
|
|
|
|
chartPurple: '#6d6a9f',
|
|
|
|
|
chartAmber: '#a98857'
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const activeThemeSkinId = ref(DEFAULT_THEME_SKIN_ID)
|
2026-06-26 22:42:00 +08:00
|
|
|
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
|
|
|
|
|
}
|
2026-05-27 09:17:57 +08:00
|
|
|
|
|
|
|
|
function findThemeSkin(id) {
|
2026-06-26 22:42:00 +08:00
|
|
|
const themeMode = normalizeThemeMode(id)
|
|
|
|
|
return THEME_SKIN_OPTIONS.find((skin) => skin.id === themeMode) || THEME_SKIN_OPTIONS[0]
|
2026-05-27 09:17:57 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2026-06-26 22:42:00 +08:00
|
|
|
root.dataset.themeMode = skin.id
|
2026-05-27 09:17:57 +08:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-06-26 22:42:00 +08:00
|
|
|
|
|
|
|
|
return skin.id
|
2026-05-27 09:17:57 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useThemeSkin() {
|
|
|
|
|
return {
|
|
|
|
|
activeThemeSkin: computed(() => findThemeSkin(activeThemeSkinId.value)),
|
|
|
|
|
activeThemeSkinId,
|
|
|
|
|
setThemeSkin,
|
|
|
|
|
themeSkinOptions: THEME_SKIN_OPTIONS
|
|
|
|
|
}
|
|
|
|
|
}
|