diff --git a/web/src/assets/styles/app.css b/web/src/assets/styles/app.css index ccd48f7..d94699b 100644 --- a/web/src/assets/styles/app.css +++ b/web/src/assets/styles/app.css @@ -82,11 +82,15 @@ .main.approval-main, .main.policies-main, .main.audit-main, -.main.employees-main { +.main.employees-main, +.main.settings-main { height: 100dvh; grid-template-rows: auto minmax(0, 1fr); overflow: hidden; } +.main.settings-main { + grid-template-rows: minmax(0, 1fr); +} .workarea { overflow: auto; padding: 24px; } .workarea.chat-workarea { min-height: 0; @@ -96,11 +100,16 @@ .workarea.approval-workarea, .workarea.policies-workarea, .workarea.audit-workarea, -.workarea.employees-workarea { +.workarea.employees-workarea, +.workarea.settings-workarea { min-height: 0; overflow: hidden; padding: 20px 24px; } +.workarea.settings-workarea { + padding: 0; + background: #fff; +} @media (max-width: 1180px) { .app { grid-template-columns: 220px minmax(0, 1fr); } diff --git a/web/src/assets/styles/views/settings-view.css b/web/src/assets/styles/views/settings-view.css new file mode 100644 index 0000000..c64061e --- /dev/null +++ b/web/src/assets/styles/views/settings-view.css @@ -0,0 +1,664 @@ +.settings-page { + height: 100%; + min-height: 0; + animation: fadeUp 220ms var(--ease) both; +} + +.settings-shell { + height: 100%; + min-height: 0; + display: grid; + grid-template-columns: 248px minmax(0, 1fr); + overflow: hidden; + border-radius: 24px; + background: linear-gradient(180deg, #ffffff 0%, #fbfefd 100%); +} + +.settings-nav { + min-width: 0; + min-height: 0; + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + gap: 10px; + padding: 22px 16px 18px; + border-right: 1px solid #e7edf3; + background: linear-gradient(180deg, #fcfffd 0%, #f5fbf8 58%, #ffffff 100%); +} + +.settings-nav-head { + display: grid; + gap: 8px; + padding: 4px 10px 18px; + border-bottom: 1px solid #eef3f7; +} + +.nav-kicker { + color: #10b981; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.settings-nav-head h2 { + color: #0f172a; + font-size: 24px; + font-weight: 860; + line-height: 1.1; +} + +.settings-nav-head p { + color: #64748b; + font-size: 12px; + line-height: 1.6; +} + +.settings-nav-list { + min-height: 0; + display: grid; + align-content: start; + gap: 8px; + overflow: auto; + padding-right: 4px; +} + +.settings-nav-item { + width: 100%; + min-height: 74px; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + padding: 14px 14px 14px 16px; + border: 1px solid transparent; + border-radius: 18px; + background: transparent; + color: #334155; + text-align: left; + transition: + background 180ms var(--ease), + border-color 180ms var(--ease), + box-shadow 180ms var(--ease), + color 180ms var(--ease), + transform 180ms var(--ease); +} + +.settings-nav-item:hover { + transform: translateY(-1px); + border-color: rgba(16, 185, 129, 0.14); + background: rgba(255, 255, 255, 0.9); +} + +.settings-nav-item.active { + border-color: rgba(16, 185, 129, 0.16); + background: linear-gradient(135deg, rgba(16, 185, 129, 0.14), rgba(16, 185, 129, 0.04)); + box-shadow: inset 3px 0 0 #10b981; + color: #047857; +} + +.nav-item-copy { + min-width: 0; + display: grid; + gap: 4px; +} + +.nav-item-copy strong { + color: inherit; + font-size: 14px; + font-weight: 820; + line-height: 1.25; +} + +.nav-item-copy small { + color: #64748b; + font-size: 12px; + line-height: 1.45; +} + +.nav-item-state { + width: 26px; + height: 26px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + background: #f1f5f9; + color: #94a3b8; + font-size: 14px; +} + +.settings-nav-item.complete .nav-item-state { + background: #ecfdf5; + color: #059669; +} + +.settings-nav-foot { + display: grid; + gap: 4px; + padding: 16px 12px 2px; + border-top: 1px solid #eef3f7; +} + +.settings-nav-foot span { + color: #64748b; + font-size: 12px; + font-weight: 700; +} + +.settings-nav-foot strong { + color: #0f172a; + font-size: 16px; + font-weight: 820; +} + +.settings-body { + min-width: 0; + min-height: 0; + display: grid; + grid-template-rows: auto minmax(0, 1fr); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 250, 252, 0.96) 100%); +} + +.settings-toolbar { + position: sticky; + top: 0; + z-index: 2; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 18px; + padding: 24px 28px 20px; + border-bottom: 1px solid #eef2f7; + background: rgba(255, 255, 255, 0.92); + backdrop-filter: blur(14px); +} + +.settings-toolbar-copy { + min-width: 0; +} + +.settings-breadcrumb { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: 0 12px; + border-radius: 999px; + background: #eef8f2; + color: #047857; + font-size: 12px; + font-weight: 800; +} + +.settings-toolbar-copy h3 { + margin-top: 14px; + color: #0f172a; + font-size: 28px; + font-weight: 860; + line-height: 1.15; +} + +.settings-toolbar-copy p { + margin-top: 10px; + max-width: 760px; + color: #64748b; + font-size: 14px; + line-height: 1.7; +} + +.settings-toolbar-actions { + display: grid; + justify-items: end; + gap: 12px; +} + +.section-status { + min-height: 36px; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 0 13px; + border-radius: 999px; + background: #fff7ed; + color: #c2410c; + font-size: 12px; + font-weight: 800; + white-space: nowrap; +} + +.section-status.complete { + background: #ecfdf5; + color: #059669; +} + +.save-button { + min-height: 42px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 0 18px; + border: 0; + border-radius: 14px; + background: linear-gradient(135deg, #13b87b, #0a9d68); + color: #fff; + font-size: 13px; + font-weight: 820; + box-shadow: 0 12px 26px rgba(5, 150, 105, 0.2); + transition: + transform 180ms var(--ease), + box-shadow 180ms var(--ease), + filter 180ms var(--ease); +} + +.save-button:hover { + transform: translateY(-1px); + box-shadow: 0 16px 30px rgba(5, 150, 105, 0.22); + filter: saturate(1.04); +} + +.settings-content { + min-height: 0; + overflow: auto; + display: grid; + align-content: start; + gap: 18px; + padding: 24px 28px 28px; +} + +.settings-card { + padding: 22px 22px 24px; + border: 1px solid #e8eef3; + border-radius: 22px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 251, 255, 0.94)); +} + +.card-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; + margin-bottom: 18px; +} + +.card-head h4 { + color: #0f172a; + font-size: 18px; + font-weight: 840; + line-height: 1.2; +} + +.card-head p { + margin-top: 6px; + color: #64748b; + font-size: 13px; + line-height: 1.65; +} + +.form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px 20px; + align-items: start; +} + +.profile-grid { + grid-template-columns: 96px repeat(2, minmax(0, 1fr)); +} + +.compact-grid { + margin-bottom: 18px; +} + +.field { + display: grid; + gap: 8px; +} + +.field-wide { + grid-column: span 2; +} + +.field-full { + grid-column: 1 / -1; +} + +.field span { + color: #334155; + font-size: 12px; + font-weight: 800; + line-height: 1.2; +} + +.field em { + margin-right: 4px; + color: #ef4444; + font-style: normal; +} + +.field input, +.field select { + width: 100%; + min-height: 44px; + padding: 0 14px; + border: 1px solid #d7e0ea; + border-radius: 16px; + background: #fff; + color: #0f172a; + font-size: 13px; + line-height: 1.45; + transition: + border-color 180ms var(--ease), + box-shadow 180ms var(--ease), + background 180ms var(--ease); +} + +.field input::placeholder { + color: #94a3b8; +} + +.field input:focus, +.field select:focus { + outline: none; + border-color: rgba(16, 185, 129, 0.55); + box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.12); +} + +.logo-field { + align-self: stretch; +} + +.logo-tile { + width: 96px; + height: 96px; + display: grid; + place-items: center; + border: 1px dashed #cbd5e1; + border-radius: 22px; + background: + linear-gradient(45deg, #f8fafc 25%, transparent 25%, transparent 75%, #f8fafc 75%, #f8fafc), + linear-gradient(45deg, #f8fafc 25%, transparent 25%, transparent 75%, #f8fafc 75%, #f8fafc); + background-position: 0 0, 9px 9px; + background-size: 18px 18px; + color: #10b981; + font-size: 36px; +} + +.preview-card { + display: grid; + grid-template-columns: 78px minmax(0, 1fr) auto; + align-items: center; + gap: 18px; + padding: 22px; + border: 1px solid rgba(16, 185, 129, 0.14); + border-radius: 24px; + background: linear-gradient(135deg, rgba(16, 185, 129, 0.08), rgba(59, 130, 246, 0.05)); +} + +.preview-icon { + width: 78px; + height: 78px; + display: grid; + place-items: center; + border-radius: 22px; + background: linear-gradient(135deg, #10b981, #0f766e); + color: #fff; + font-size: 34px; + box-shadow: 0 14px 28px rgba(16, 185, 129, 0.18); +} + +.preview-copy strong { + display: block; + color: #0f172a; + font-size: 18px; + font-weight: 840; +} + +.preview-copy p { + margin-top: 6px; + color: #334155; + font-size: 14px; + font-weight: 700; +} + +.preview-copy small { + display: block; + margin-top: 8px; + color: #64748b; + font-size: 12px; + line-height: 1.55; +} + +.preview-badge { + min-height: 30px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 12px; + border-radius: 999px; + background: #ecfdf5; + color: #059669; + font-size: 12px; + font-weight: 820; + white-space: nowrap; +} + +.chip-row { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-bottom: 18px; +} + +.level-chip { + min-width: 78px; + min-height: 36px; + padding: 0 14px; + border: 1px solid #d7e0ea; + border-radius: 999px; + background: #fff; + color: #475569; + font-size: 12px; + font-weight: 820; + transition: + border-color 160ms ease, + background 160ms ease, + box-shadow 160ms ease, + color 160ms ease; +} + +.level-chip.active { + border-color: #10b981; + background: #10b981; + color: #fff; + box-shadow: 0 8px 18px rgba(16, 185, 129, 0.18); +} + +.range-shell { + min-height: 44px; + display: flex; + align-items: center; + gap: 14px; + padding: 0 14px; + border: 1px solid #d7e0ea; + border-radius: 16px; + background: #fff; +} + +.range-shell input[type='range'] { + flex: 1 1 auto; + accent-color: #10b981; +} + +.range-shell strong { + min-width: 28px; + color: #0f172a; + font-size: 13px; + font-weight: 800; + text-align: right; +} + +.switch-group { + display: grid; + gap: 12px; +} + +.switch-row { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 15px 16px; + border: 1px solid #e5eaf0; + border-radius: 18px; + background: #fbfdff; + text-align: left; + transition: + border-color 180ms var(--ease), + background 180ms var(--ease), + transform 180ms var(--ease); +} + +.switch-row:hover { + transform: translateY(-1px); + border-color: rgba(16, 185, 129, 0.18); + background: #f7fffb; +} + +.switch-copy { + min-width: 0; + display: grid; + gap: 4px; +} + +.switch-copy strong { + color: #0f172a; + font-size: 14px; + font-weight: 800; +} + +.switch-copy small { + color: #64748b; + font-size: 12px; + line-height: 1.55; +} + +.switch { + position: relative; + flex: 0 0 auto; + width: 48px; + height: 28px; + display: inline-flex; + align-items: center; + padding: 3px; + border-radius: 999px; + background: #dbe4ee; + transition: background 180ms var(--ease); +} + +.switch i { + width: 22px; + height: 22px; + border-radius: 999px; + background: #fff; + box-shadow: 0 2px 6px rgba(15, 23, 42, 0.14); + transition: transform 180ms var(--ease); +} + +.switch.active { + background: #10b981; +} + +.switch.active i { + transform: translateX(20px); +} + +@media (max-width: 1260px) { + .settings-shell { + grid-template-columns: 226px minmax(0, 1fr); + } + + .settings-toolbar { + flex-direction: column; + align-items: stretch; + } + + .settings-toolbar-actions { + justify-items: stretch; + } + + .save-button, + .section-status { + justify-content: center; + } +} + +@media (max-width: 960px) { + .settings-shell { + grid-template-columns: 1fr; + } + + .settings-nav { + grid-template-rows: auto auto auto; + border-right: 0; + border-bottom: 1px solid #e7edf3; + } + + .settings-nav-list { + display: flex; + gap: 8px; + overflow-x: auto; + padding-right: 0; + } + + .settings-nav-item { + min-width: 208px; + } + + .settings-toolbar, + .settings-content { + padding-inline: 20px; + } + + .form-grid, + .profile-grid { + grid-template-columns: 1fr; + } + + .field-wide, + .field-full { + grid-column: span 1; + } + + .logo-field { + width: fit-content; + } + + .preview-card { + grid-template-columns: 1fr; + justify-items: start; + } +} + +@media (max-width: 640px) { + .settings-toolbar { + padding: 18px 16px; + } + + .settings-toolbar-copy h3 { + font-size: 24px; + } + + .settings-content { + padding: 16px; + } + + .settings-card { + padding: 18px 16px; + border-radius: 18px; + } + + .settings-nav { + padding: 18px 12px 14px; + } +} diff --git a/web/src/components/layout/SidebarRail.vue b/web/src/components/layout/SidebarRail.vue index 09060e6..4a58528 100644 --- a/web/src/components/layout/SidebarRail.vue +++ b/web/src/components/layout/SidebarRail.vue @@ -7,7 +7,7 @@ - 星海科技 + {{ displayCompanyName }} @@ -54,11 +54,15 @@ import { computed } from 'vue' const props = defineProps({ navItems: { type: Array, required: true }, activeView: { type: String, required: true }, + companyName: { + type: String, + default: '' + }, currentUser: { type: Object, default: () => ({ name: '系统管理员', - role: '财务管理员', + role: '管理员', avatar: '管' }) } @@ -73,8 +77,9 @@ const sidebarMeta = { approval: { label: '审批中心', badge: '12' }, chat: { label: 'AI 助手' }, policies: { label: '知识管理' }, - audit: { label: '技能中心' }, - employees: { label: '员工管理' } + audit: { label: '审计追踪' }, + employees: { label: '员工管理' }, + settings: { label: '系统设置' } } const decoratedNavItems = computed(() => @@ -87,9 +92,11 @@ const decoratedNavItems = computed(() => const displayUser = computed(() => ({ name: props.currentUser?.name || '系统管理员', - role: props.currentUser?.role || '财务管理员', + role: props.currentUser?.role || '管理员', avatar: props.currentUser?.avatar || '管' })) + +const displayCompanyName = computed(() => props.companyName || 'X-Financial') diff --git a/web/src/views/scripts/SettingsView.js b/web/src/views/scripts/SettingsView.js new file mode 100644 index 0000000..55841ca --- /dev/null +++ b/web/src/views/scripts/SettingsView.js @@ -0,0 +1,395 @@ +import { computed, ref } from 'vue' + +import { useSystemState } from '../../composables/useSystemState.js' +import { useToast } from '../../composables/useToast.js' + +const SETTINGS_STORAGE_KEY = 'x-financial-settings-draft' +const CURRENT_YEAR = new Date().getFullYear() + +const SECTION_DEFINITIONS = [ + { + id: 'profile', + label: '企业信息', + title: '系统基本信息', + desc: '公司名称、品牌与版权', + longDesc: '统一维护企业名称、系统显示名和版权信息,保存后会直接同步到当前界面的品牌预览。', + actionLabel: '保存企业信息' + }, + { + id: 'admin', + label: '管理员安全', + title: '管理员账号与安全策略', + desc: '账号、密码与登录安全', + longDesc: '管理最高权限管理员的账号、密码和登录安全策略,密码类字段仅用于本次填写,不会进入浏览器草稿。', + actionLabel: '保存安全设置' + }, + { + id: 'llm', + label: '大语言模型', + title: '模型接入配置', + desc: '供应商、模型与推理策略', + longDesc: '配置 AI 助手与识别流程依赖的大模型接入信息,并维护推理模式、知识检索和输出行为。', + actionLabel: '保存模型配置' + }, + { + id: 'logs', + label: '日志策略', + title: '日志与审计策略', + desc: '日志级别、留存与脱敏', + longDesc: '定义系统日志级别、留存周期和审计策略,保证后续排障、追溯和安全审计有完整依据。', + actionLabel: '保存日志策略' + }, + { + id: 'mail', + label: '邮箱设置', + title: '邮箱通知配置', + desc: 'SMTP 与通知投递策略', + longDesc: '维护系统邮件发送配置和通知投递策略,审批、预警和摘要邮件都会依赖这里的设置。', + actionLabel: '保存邮箱配置' + } +] + +const LOG_LEVELS = ['DEBUG', 'INFO', 'WARN', 'ERROR'] + +function normalizeValue(value) { + return String(value ?? '').trim() +} + +function buildDefaultState(companyProfile, currentUser) { + const companyName = normalizeValue(companyProfile?.name) || 'X-Financial' + const companyCode = normalizeValue(companyProfile?.code) || 'XF-001' + const adminEmail = + normalizeValue(companyProfile?.adminEmail) || + normalizeValue(currentUser?.email) || + 'admin@example.com' + const adminAccount = normalizeValue(currentUser?.username) || 'superadmin' + + return { + companyForm: { + companyName, + displayName: companyName, + companyCode, + recordNumber: '', + environment: '生产环境', + copyright: `Copyright © 2024-${CURRENT_YEAR} ${companyName}. All Rights Reserved.` + }, + adminForm: { + adminAccount, + adminEmail, + newPassword: '', + confirmPassword: '', + sessionTimeout: Number(import.meta.env.VITE_AUTH_IDLE_TIMEOUT_MINUTES || 30), + noticeEmail: adminEmail, + mfaEnabled: true, + strongPassword: true, + loginAlertEnabled: true + }, + llmForm: { + provider: 'OpenAI Compatible', + model: 'gpt-4.1-mini', + endpoint: 'https://api.openai.com/v1', + embeddingModel: 'text-embedding-3-large', + apiKey: '', + reasoningMode: 'balanced', + maxTokens: 4096, + temperature: 0.2, + knowledgeEnabled: true, + citationEnabled: true + }, + logForm: { + level: 'INFO', + retentionDays: 180, + archiveCycle: 'weekly', + logPath: 'server/logs/app.log', + alertEmail: adminEmail, + operationAudit: true, + loginAudit: true, + maskSensitive: true + }, + mailForm: { + smtpHost: 'smtp.exmail.qq.com', + port: 465, + encryption: 'SSL/TLS', + senderName: companyName, + senderAddress: adminEmail, + username: adminEmail, + password: '', + alertEnabled: true, + digestEnabled: false, + digestTime: '09:00', + defaultReceiver: adminEmail + } + } +} + +function readStoredSettings() { + if (typeof window === 'undefined') { + return null + } + + const raw = window.sessionStorage.getItem(SETTINGS_STORAGE_KEY) + if (!raw) { + return null + } + + try { + return JSON.parse(raw) + } catch { + return null + } +} + +function mergeStoredState(defaults, stored) { + return { + companyForm: { ...defaults.companyForm, ...(stored?.companyForm || {}) }, + adminForm: { ...defaults.adminForm, ...(stored?.adminForm || {}) }, + llmForm: { ...defaults.llmForm, ...(stored?.llmForm || {}) }, + logForm: { ...defaults.logForm, ...(stored?.logForm || {}) }, + mailForm: { ...defaults.mailForm, ...(stored?.mailForm || {}) } + } +} + +function sanitizeForStorage(state) { + return { + companyForm: { ...state.companyForm }, + adminForm: { + ...state.adminForm, + newPassword: '', + confirmPassword: '' + }, + llmForm: { + ...state.llmForm, + apiKey: '' + }, + logForm: { ...state.logForm }, + mailForm: { + ...state.mailForm, + password: '' + } + } +} + +function persistSettings(state) { + if (typeof window === 'undefined') { + return + } + + window.sessionStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(sanitizeForStorage(state))) +} + +function computeSectionStatus(state) { + return { + profile: Boolean( + normalizeValue(state.companyForm.companyName) && + normalizeValue(state.companyForm.displayName) && + normalizeValue(state.companyForm.copyright) + ), + admin: Boolean( + normalizeValue(state.adminForm.adminAccount) && + normalizeValue(state.adminForm.adminEmail) && + Number(state.adminForm.sessionTimeout) >= 5 + ), + llm: Boolean( + normalizeValue(state.llmForm.provider) && + normalizeValue(state.llmForm.model) && + normalizeValue(state.llmForm.endpoint) + ), + logs: Boolean( + normalizeValue(state.logForm.level) && + Number(state.logForm.retentionDays) > 0 && + normalizeValue(state.logForm.logPath) + ), + mail: Boolean( + normalizeValue(state.mailForm.smtpHost) && + Number(state.mailForm.port) > 0 && + normalizeValue(state.mailForm.senderAddress) && + normalizeValue(state.mailForm.username) + ) + } +} + +export default { + name: 'SettingsView', + setup() { + const { toast } = useToast() + const { companyProfile, currentUser, updateCompanyProfilePreview } = useSystemState() + + const defaults = buildDefaultState(companyProfile.value, currentUser.value) + const pageState = ref(mergeStoredState(defaults, readStoredSettings())) + const activeSection = ref('profile') + + const sections = SECTION_DEFINITIONS + const logLevels = LOG_LEVELS + + const sectionStatus = computed(() => computeSectionStatus(pageState.value)) + const completedSectionCount = computed(() => Object.values(sectionStatus.value).filter(Boolean).length) + const activeSectionConfig = computed( + () => sections.find((section) => section.id === activeSection.value) || sections[0] + ) + + function activateSection(sectionId) { + activeSection.value = sectionId + } + + function toggleBoolean(formKey, field) { + pageState.value[formKey][field] = !pageState.value[formKey][field] + } + + function saveProfileSection() { + const companyForm = pageState.value.companyForm + + if (!normalizeValue(companyForm.companyName)) { + toast('请输入企业名称。') + return + } + + if (!normalizeValue(companyForm.displayName)) { + toast('请输入系统显示名称。') + return + } + + if (!normalizeValue(companyForm.copyright)) { + toast('请输入版权信息。') + return + } + + updateCompanyProfilePreview({ + name: normalizeValue(companyForm.displayName), + code: normalizeValue(companyForm.companyCode) + }) + + pageState.value.mailForm.senderName = normalizeValue(companyForm.displayName) + persistSettings(pageState.value) + toast('企业信息已保存并应用到当前界面预览。') + } + + function saveAdminSection() { + const adminForm = pageState.value.adminForm + + if (!normalizeValue(adminForm.adminAccount)) { + toast('请输入管理员账号。') + return + } + + if (!normalizeValue(adminForm.adminEmail)) { + toast('请输入管理员邮箱。') + return + } + + if (Number(adminForm.sessionTimeout) < 5) { + toast('会话超时时间不能少于 5 分钟。') + return + } + + if (adminForm.newPassword) { + if (adminForm.newPassword.length < 5) { + toast('管理员密码至少需要 5 位。') + return + } + + if (adminForm.newPassword !== adminForm.confirmPassword) { + toast('两次输入的管理员密码不一致。') + return + } + } + + updateCompanyProfilePreview({ + adminEmail: normalizeValue(adminForm.adminEmail) + }) + + persistSettings(pageState.value) + adminForm.newPassword = '' + adminForm.confirmPassword = '' + toast('管理员安全设置已保存。') + } + + function saveLlmSection() { + const llmForm = pageState.value.llmForm + + if ( + !normalizeValue(llmForm.provider) || + !normalizeValue(llmForm.model) || + !normalizeValue(llmForm.endpoint) + ) { + toast('请完整填写模型供应商、模型名称和接口地址。') + return + } + + persistSettings(pageState.value) + llmForm.apiKey = '' + toast('模型配置已保存。') + } + + function saveLogsSection() { + const logForm = pageState.value.logForm + + if (!normalizeValue(logForm.level) || Number(logForm.retentionDays) <= 0) { + toast('请填写有效的日志级别和留存天数。') + return + } + + if (!normalizeValue(logForm.logPath)) { + toast('请输入日志路径。') + return + } + + persistSettings(pageState.value) + toast('日志策略已保存。') + } + + function saveMailSection() { + const mailForm = pageState.value.mailForm + + if (!normalizeValue(mailForm.smtpHost) || Number(mailForm.port) <= 0) { + toast('请填写有效的 SMTP Host 和端口。') + return + } + + if (!normalizeValue(mailForm.senderAddress) || !normalizeValue(mailForm.username)) { + toast('请填写发件人邮箱和 SMTP 登录账号。') + return + } + + persistSettings(pageState.value) + mailForm.password = '' + toast('邮箱配置已保存。') + } + + function saveActiveSection() { + if (activeSection.value === 'profile') { + saveProfileSection() + return + } + + if (activeSection.value === 'admin') { + saveAdminSection() + return + } + + if (activeSection.value === 'llm') { + saveLlmSection() + return + } + + if (activeSection.value === 'logs') { + saveLogsSection() + return + } + + saveMailSection() + } + + return { + activeSection, + activeSectionConfig, + activateSection, + completedSectionCount, + logLevels, + pageState, + saveActiveSection, + sectionStatus, + sections, + toggleBoolean + } + } +}