feat(web): 主题皮肤系统与 LLM 设置面板重构
- useThemeSkin 重构主题皮肤应用逻辑,支持企业 AI 风格主题切换 - settingsModelHelper 新增主题与模型表字段映射,useSettings 适配 - LlmSettingsPanel/SettingsView 面板重构,支持多模型行编辑与主题区块 - settings-view.css 适配主题样式,新增 settings-theme-section 测试,更新 settings-llm-section 测试
This commit is contained in:
@@ -2,7 +2,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { useSystemState } from './useSystemState.js'
|
||||
import { useThemeSkin } from './useThemeSkin.js'
|
||||
import { normalizeThemeMode, useThemeSkin } from './useThemeSkin.js'
|
||||
import { clearSystemCaches, fetchSettings, saveSettings } from '../services/settings.js'
|
||||
import { useToast } from './useToast.js'
|
||||
import {
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
maskConfiguredModelSecrets,
|
||||
maskConfiguredRenderSecret,
|
||||
mergeState,
|
||||
normalizeLlmModelRows,
|
||||
normalizeValue,
|
||||
persistSettings,
|
||||
readStoredSettings
|
||||
@@ -61,6 +62,8 @@ export function useSettings() {
|
||||
const cacheClearMessage = ref('')
|
||||
const cacheClearFailed = ref(false)
|
||||
|
||||
pageState.value.appearanceForm.themeSkin = setThemeSkin(pageState.value.appearanceForm.themeSkin)
|
||||
|
||||
const sections = SECTION_DEFINITIONS
|
||||
const logLevels = LOG_LEVELS
|
||||
const providerOptions = PROVIDER_OPTIONS
|
||||
@@ -108,6 +111,13 @@ export function useSettings() {
|
||||
nextState.llmForm.backupApiKey = currentState.llmForm.backupApiKey
|
||||
nextState.llmForm.embeddingApiKey = currentState.llmForm.embeddingApiKey
|
||||
nextState.llmForm.rerankerApiKey = currentState.llmForm.rerankerApiKey
|
||||
const modelApiKeysBySlot = new Map(
|
||||
normalizeLlmModelRows(currentState.llmForm.models).map((row) => [row.slot, row.apiKey])
|
||||
)
|
||||
nextState.llmForm.models = normalizeLlmModelRows(nextState.llmForm.models).map((row) => ({
|
||||
...row,
|
||||
apiKey: modelApiKeysBySlot.get(row.slot) ?? row.apiKey
|
||||
}))
|
||||
}
|
||||
|
||||
if (preserveAdminPasswords) {
|
||||
@@ -123,13 +133,16 @@ export function useSettings() {
|
||||
nextState.mailForm.password = currentState.mailForm.password
|
||||
}
|
||||
|
||||
const normalizedThemeMode = normalizeThemeMode(nextState.appearanceForm?.themeSkin)
|
||||
nextState.appearanceForm = {
|
||||
...nextState.appearanceForm,
|
||||
themeSkin: normalizedThemeMode
|
||||
}
|
||||
|
||||
pageState.value = maskConfiguredRenderSecret(maskConfiguredModelSecrets(nextState))
|
||||
persistSettings(pageState.value)
|
||||
updateBrandPreviewFromState(pageState.value)
|
||||
|
||||
if (nextState.appearanceForm?.themeSkin) {
|
||||
setThemeSkin(nextState.appearanceForm.themeSkin)
|
||||
}
|
||||
setThemeSkin(normalizedThemeMode)
|
||||
}
|
||||
|
||||
async function loadSettingsSnapshot() {
|
||||
@@ -358,12 +371,12 @@ export function useSettings() {
|
||||
}
|
||||
|
||||
function selectThemeSkin(skinId) {
|
||||
setThemeSkin(skinId)
|
||||
pageState.value.appearanceForm.themeSkin = skinId
|
||||
pageState.value.appearanceForm.themeSkin = setThemeSkin(skinId)
|
||||
}
|
||||
|
||||
async function saveAppearanceSection() {
|
||||
await persistRemoteSettings('界面皮肤已保存并应用到企业配置。', {
|
||||
pageState.value.appearanceForm.themeSkin = normalizeThemeMode(pageState.value.appearanceForm.themeSkin)
|
||||
await persistRemoteSettings('主题设置已保存并应用到企业配置。', {
|
||||
preserveModelApiKeys: true,
|
||||
preserveAdminPasswords: true,
|
||||
preserveRenderSecret: true,
|
||||
@@ -373,16 +386,16 @@ export function useSettings() {
|
||||
|
||||
async function saveLlmSection() {
|
||||
const llmForm = pageState.value.llmForm
|
||||
const modelConfigs = [
|
||||
['主模型', llmForm.mainProvider, llmForm.mainModel, llmForm.mainEndpoint],
|
||||
['备份模型', llmForm.backupProvider, llmForm.backupModel, llmForm.backupEndpoint],
|
||||
['Embedding 模型', llmForm.embeddingProvider, llmForm.embeddingModel, llmForm.embeddingEndpoint],
|
||||
['Reranker 模型', llmForm.rerankerProvider, llmForm.rerankerModel, llmForm.rerankerEndpoint]
|
||||
]
|
||||
const modelRows = normalizeLlmModelRows(llmForm.models)
|
||||
|
||||
for (const [label, provider, model, endpoint] of modelConfigs) {
|
||||
if (!isModelConfigReady(provider, model, endpoint)) {
|
||||
toast(`请完整填写${label}的供应商、模型名称和接口地址。`)
|
||||
if (modelRows.length === 0) {
|
||||
toast('请至少添加一个模型配置。')
|
||||
return
|
||||
}
|
||||
|
||||
for (const row of modelRows) {
|
||||
if (!isModelConfigReady(row.provider, row.modelId, row.url)) {
|
||||
toast('请完整填写每个模型的供应商、model_id 和接口地址。')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const THEME_SKIN_STORAGE_KEY = 'x-financial-theme-skin'
|
||||
const DEFAULT_THEME_SKIN_ID = 'sky'
|
||||
const DEFAULT_THEME_SKIN_ID = 'enterprise'
|
||||
|
||||
const DEFAULT_SEMANTIC_COLORS = {
|
||||
success: '#2f855a',
|
||||
@@ -28,112 +28,45 @@ const DEFAULT_SEMANTIC_COLORS = {
|
||||
|
||||
export const THEME_SKIN_OPTIONS = [
|
||||
{
|
||||
id: 'sky',
|
||||
label: '浅蓝企业',
|
||||
desc: '默认皮肤,降低蓝色饱和度,适合财务 SaaS 和审批后台。',
|
||||
primary: '#3a7ca5',
|
||||
primaryHover: '#2f6d95',
|
||||
primaryActive: '#255b7d',
|
||||
primarySoft: '#eaf4fa',
|
||||
primarySoftStrong: '#d4e8f3',
|
||||
secondary: '#4f6f9f',
|
||||
chartBlue: '#4f6f9f',
|
||||
chartPurple: '#6e7fa6',
|
||||
chartAmber: '#b58b4c'
|
||||
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: 'blue',
|
||||
label: '湖蓝灰',
|
||||
desc: '偏灰的湖蓝色,弱化科技感,更适合高密度运营页面。',
|
||||
primary: '#477c9e',
|
||||
primaryHover: '#3a6a89',
|
||||
primaryActive: '#305873',
|
||||
primarySoft: '#edf5f8',
|
||||
primarySoftStrong: '#d8e8ef',
|
||||
secondary: '#5d7288',
|
||||
chartBlue: '#477c9e',
|
||||
chartPurple: '#77799c',
|
||||
chartAmber: '#b28a54'
|
||||
},
|
||||
{
|
||||
id: 'navy',
|
||||
label: '稳健蓝',
|
||||
desc: '偏金融和管理驾驶舱的稳重蓝,适合长时间办公查看。',
|
||||
primary: '#4b6f95',
|
||||
primaryHover: '#405f80',
|
||||
primaryActive: '#354e69',
|
||||
primarySoft: '#eef3f8',
|
||||
primarySoftStrong: '#dbe6f0',
|
||||
secondary: '#6b7280',
|
||||
chartBlue: '#4b6f95',
|
||||
chartPurple: '#69769d',
|
||||
chartAmber: '#aa8a55'
|
||||
},
|
||||
{
|
||||
id: 'teal',
|
||||
label: '雾青',
|
||||
desc: '保留绿色倾向但降低鲜艳度,比旧绿色更克制。',
|
||||
primary: '#3f827c',
|
||||
primaryHover: '#36706b',
|
||||
primaryActive: '#2d5c58',
|
||||
primarySoft: '#eef8f6',
|
||||
primarySoftStrong: '#d8ebe8',
|
||||
secondary: '#4f6f9f',
|
||||
chartBlue: '#4f7f9f',
|
||||
chartPurple: '#708099',
|
||||
chartAmber: '#b18a53'
|
||||
},
|
||||
{
|
||||
id: 'legacy-green',
|
||||
label: '经典绿',
|
||||
desc: '保留旧版系统绿色,适合继续沿用原有品牌记忆。',
|
||||
primary: '#10b981',
|
||||
primaryHover: '#059669',
|
||||
primaryActive: '#047857',
|
||||
primarySoft: '#ecfdf5',
|
||||
primarySoftStrong: '#d1fae5',
|
||||
secondary: '#2563eb',
|
||||
chartBlue: '#2563eb',
|
||||
chartPurple: '#6d6a9f',
|
||||
chartAmber: '#b88a44'
|
||||
},
|
||||
{
|
||||
id: 'sage',
|
||||
label: '鼠尾草绿',
|
||||
desc: '低饱和灰绿色,比经典绿更安静,适合企业内控场景。',
|
||||
primary: '#5f8d72',
|
||||
primaryHover: '#517b62',
|
||||
primaryActive: '#436653',
|
||||
primarySoft: '#f0f7f2',
|
||||
primarySoftStrong: '#dcebe0',
|
||||
secondary: '#4f6f9f',
|
||||
chartBlue: '#4f748f',
|
||||
chartPurple: '#7a7898',
|
||||
chartAmber: '#a98753'
|
||||
},
|
||||
{
|
||||
id: 'slate',
|
||||
label: '石板灰蓝',
|
||||
desc: '弱主色方案,适合审计、规则和报表密集页面。',
|
||||
primary: '#64748b',
|
||||
primaryHover: '#526174',
|
||||
primaryActive: '#3f4a5a',
|
||||
id: 'enterprise',
|
||||
label: '企业沉稳',
|
||||
desc: '低饱和、轻描边、少渲染,适合正式生产环境和企业级财务 SaaS。',
|
||||
keywords: ['克制', '结构化', '低噪声'],
|
||||
primary: '#475569',
|
||||
primaryHover: '#3f4a5a',
|
||||
primaryActive: '#334155',
|
||||
primarySoft: '#f1f5f9',
|
||||
primarySoftStrong: '#e2e8f0',
|
||||
secondary: '#3a7ca5',
|
||||
secondary: '#64748b',
|
||||
chartBlue: '#5d7590',
|
||||
chartPurple: '#77748f',
|
||||
chartAmber: '#a88955'
|
||||
chartPurple: '#6b7280',
|
||||
chartAmber: '#9a7a45'
|
||||
},
|
||||
{
|
||||
id: 'soft-violet',
|
||||
label: '灰紫蓝',
|
||||
desc: '保留一点智能系统气质,但用灰度压低 AI 感和饱和度。',
|
||||
primary: '#6d6a9f',
|
||||
primaryHover: '#5f5b8c',
|
||||
primaryActive: '#504c78',
|
||||
primarySoft: '#f2f1f8',
|
||||
primarySoftStrong: '#e2e0ef',
|
||||
id: 'intelligent',
|
||||
label: '专业智能',
|
||||
desc: '保留少量智能识别感,同时控制饱和度,适合稳定办公和 AI 辅助并重的团队。',
|
||||
keywords: ['智能', '专业', '轻点缀'],
|
||||
primary: '#5f6f9f',
|
||||
primaryHover: '#53618b',
|
||||
primaryActive: '#465275',
|
||||
primarySoft: '#f3f4fb',
|
||||
primarySoftStrong: '#e2e5f4',
|
||||
secondary: '#477c9e',
|
||||
chartBlue: '#4f7495',
|
||||
chartPurple: '#6d6a9f',
|
||||
@@ -142,9 +75,36 @@ export const THEME_SKIN_OPTIONS = [
|
||||
]
|
||||
|
||||
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) {
|
||||
return THEME_SKIN_OPTIONS.find((skin) => skin.id === id) || THEME_SKIN_OPTIONS[0]
|
||||
const themeMode = normalizeThemeMode(id)
|
||||
return THEME_SKIN_OPTIONS.find((skin) => skin.id === themeMode) || THEME_SKIN_OPTIONS[0]
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
@@ -185,6 +145,7 @@ function applyThemeSkin(skin) {
|
||||
const infoRgb = hexToRgb(DEFAULT_SEMANTIC_COLORS.info)
|
||||
|
||||
root.dataset.themeSkin = skin.id
|
||||
root.dataset.themeMode = skin.id
|
||||
|
||||
setVariables(root, {
|
||||
'--primary': skin.primary,
|
||||
@@ -270,6 +231,8 @@ export function setThemeSkin(id) {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(THEME_SKIN_STORAGE_KEY, skin.id)
|
||||
}
|
||||
|
||||
return skin.id
|
||||
}
|
||||
|
||||
export function useThemeSkin() {
|
||||
|
||||
Reference in New Issue
Block a user