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
+ }
+ }
+}