Files
X-Financial/web/src/views/SettingsView.vue
caoxiaozhu 2dcc72102d style: 全局 UI 主题皮肤重构与样式模块化
引入 Element Plus 主题定制和主题皮肤 composable,将全局
样式拆分为组件级独立 CSS 文件(侧边栏、顶栏、工作台等),
统一色彩变量和间距规范,重构所有视图和组件样式以适配新
主题系统,优化图表和知识图谱组件视觉表现,提取审计和差
旅报销相关子组件。
2026-05-27 09:17:57 +08:00

490 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<section class="settings-page">
<div class="settings-shell panel">
<aside class="settings-nav" aria-label="系统设置分类">
<div class="settings-nav-head">
<span class="nav-kicker">Settings</span>
<h2>系统设置</h2>
<p>已完成 {{ completedSectionCount }} / {{ sections.length }} 项配置</p>
</div>
<nav class="settings-nav-list">
<button
v-for="section in sections"
:key="section.id"
class="settings-nav-item"
:class="{
active: activeSection === section.id
}"
type="button"
@click="activateSection(section.id)"
>
<span class="nav-item-copy">
<strong>{{ section.label }}</strong>
<small>{{ section.desc }}</small>
</span>
</button>
</nav>
</aside>
<div class="settings-body">
<header class="settings-toolbar">
<div class="settings-toolbar-copy">
<span class="settings-breadcrumb">首页 / 系统设置 / {{ activeSectionConfig.label }}</span>
<h3>{{ activeSectionConfig.title }}</h3>
<p>{{ activeSectionConfig.longDesc }}</p>
</div>
<div class="settings-toolbar-actions">
<button class="save-button" type="button" @click="saveActiveSection">
<i class="mdi mdi-content-save-outline"></i>
<span>{{ activeSectionConfig.actionLabel }}</span>
</button>
</div>
</header>
<div class="settings-content">
<template v-if="activeSection === 'profile'">
<section class="settings-card">
<div class="card-head">
<div class="card-title-with-icon">
<div class="model-icon-box slate">
<i class="mdi mdi-domain"></i>
</div>
<div>
<h4>系统基本信息</h4>
<p>统一维护企业名称系统显示名称和版权信息保存后会同步更新当前系统品牌名称</p>
</div>
</div>
</div>
<div class="form-grid profile-grid">
<label class="field logo-field">
<span><em>*</em> 系统图标</span>
<div class="logo-tile" aria-hidden="true" @click="triggerLogoUpload" style="cursor: pointer; overflow: hidden;" title="点击上传图片">
<img v-if="pageState.companyForm.logo" :src="pageState.companyForm.logo" style="width:100%;height:100%;object-fit:contain;" />
<i v-else class="mdi mdi-domain"></i>
</div>
<input type="file" ref="logoInputRef" accept="image/*" style="display: none;" @change="handleLogoUpload" />
<small style="color:#64748b; font-size:12px; margin-top:4px;">建议尺寸 64x64PNG/JPG 格式</small>
</label>
<label class="field">
<span><em>*</em> 企业名称</span>
<input v-model="pageState.companyForm.companyName" type="text" placeholder="请输入企业法定名称" />
</label>
<label class="field">
<span>企业编码</span>
<input v-model="pageState.companyForm.companyCode" type="text" placeholder="例如 XF-001" />
</label>
<label class="field field-wide">
<span><em>*</em> 系统显示名称</span>
<input v-model="pageState.companyForm.displayName" type="text" placeholder="请输入系统对内展示名称" />
</label>
<label class="field">
<span>备案号</span>
<input v-model="pageState.companyForm.recordNumber" type="text" placeholder="请输入备案号" />
</label>
<label class="field field-full">
<span><em>*</em> 版权信息</span>
<input
v-model="pageState.companyForm.copyright"
type="text"
placeholder="例如 Copyright © 2024-2026 X-Financial. All Rights Reserved."
/>
</label>
</div>
</section>
</template>
<template v-else-if="activeSection === 'appearance'">
<section class="settings-card">
<div class="card-head">
<div class="card-title-with-icon">
<div class="model-icon-box slate">
<i class="mdi mdi-palette-outline"></i>
</div>
<div>
<h4>界面皮肤与企业主色</h4>
<p>只调整整体主色焦点态按钮和 Element Plus 控件颜色不改变业务布局</p>
</div>
</div>
</div>
<div class="skin-option-grid">
<button
v-for="skin in themeSkinOptions"
:key="skin.id"
class="skin-option"
:class="{ active: activeThemeSkinId === skin.id }"
type="button"
@click="selectThemeSkin(skin.id)"
>
<span class="skin-swatch" aria-hidden="true">
<i :style="{ background: skin.primary }"></i>
<i :style="{ background: skin.primarySoftStrong }"></i>
<i :style="{ background: skin.secondary }"></i>
<i :style="{ background: skin.chartAmber }"></i>
</span>
<span class="skin-copy">
<strong>{{ skin.label }}</strong>
<small>{{ skin.desc }}</small>
</span>
<span v-if="activeThemeSkinId === skin.id" class="skin-current">当前</span>
</button>
</div>
<div class="skin-preview-panel">
<div>
<strong>{{ activeThemeSkin.label }}</strong>
<span>当前主色会同步到全局按钮焦点环下拉浮层和表单控件</span>
</div>
<button class="skin-preview-action" type="button">主按钮</button>
</div>
</section>
</template>
<template v-else-if="activeSection === 'admin'">
<section class="settings-card">
<div class="card-head">
<div class="card-title-with-icon">
<div class="model-icon-box slate">
<i class="mdi mdi-account-cog-outline"></i>
</div>
<div>
<h4>管理员账号</h4>
<p>维护最高权限管理员的登录账户密码和安全通知邮箱</p>
</div>
</div>
</div>
<div class="form-grid">
<label class="field">
<span><em>*</em> 管理员账号</span>
<input v-model="pageState.adminForm.adminAccount" type="text" placeholder="请输入管理员账号" />
</label>
<label class="field">
<span><em>*</em> 管理员邮箱</span>
<input v-model="pageState.adminForm.adminEmail" type="email" placeholder="请输入管理员邮箱" />
</label>
<label class="field">
<span>新密码</span>
<input
v-model="pageState.adminForm.newPassword"
type="password"
autocomplete="new-password"
placeholder="至少 5 位"
/>
</label>
<label class="field">
<span>确认密码</span>
<input
v-model="pageState.adminForm.confirmPassword"
type="password"
autocomplete="new-password"
placeholder="再次输入管理员密码"
/>
</label>
</div>
</section>
<section class="settings-card">
<div class="card-head">
<div class="card-title-with-icon">
<div class="model-icon-box slate">
<i class="mdi mdi-shield-lock-outline"></i>
</div>
<div>
<h4>登录安全策略</h4>
<p>控制会话超时登录提醒和管理员高风险操作的基础安全策略</p>
</div>
</div>
</div>
<div class="form-grid compact-grid">
<label class="field">
<span><em>*</em> 会话超时分钟</span>
<input v-model.number="pageState.adminForm.sessionTimeout" type="number" min="5" max="240" />
</label>
<label class="field">
<span>安全通知邮箱</span>
<input v-model="pageState.adminForm.noticeEmail" type="email" placeholder="用于接收安全提醒" />
</label>
</div>
<div class="switch-group">
<button class="switch-row" type="button" @click="toggleBoolean('adminForm', 'mfaEnabled')">
<span class="switch-copy">
<strong>开启双因素验证</strong>
<small>要求管理员使用附加验证步骤登录后台</small>
</span>
<span class="switch-btn" :class="{ active: pageState.adminForm.mfaEnabled }"><i></i></span>
</button>
<button class="switch-row" type="button" @click="toggleBoolean('adminForm', 'strongPassword')">
<span class="switch-copy">
<strong>启用强密码策略</strong>
<small>管理员密码修改时需要满足强度要求</small>
</span>
<span class="switch-btn" :class="{ active: pageState.adminForm.strongPassword }"><i></i></span>
</button>
<button class="switch-row" type="button" @click="toggleBoolean('adminForm', 'loginAlertEnabled')">
<span class="switch-copy">
<strong>异常登录提醒</strong>
<small>检测到高风险登录时向安全通知邮箱发送告警</small>
</span>
<span class="switch-btn" :class="{ active: pageState.adminForm.loginAlertEnabled }"><i></i></span>
</button>
</div>
</section>
</template>
<template v-else-if="activeSection === 'session'">
<section class="settings-card">
<div class="card-head">
<div class="card-title-with-icon">
<div class="model-icon-box slate">
<i class="mdi mdi-clock-time-three-outline"></i>
</div>
<div>
<h4>会话保留策略</h4>
<p>控制智能体会话在系统中的保留时长超过保留期的历史会话会自动清理</p>
</div>
</div>
</div>
<div class="form-grid compact-grid">
<label class="field">
<span><em>*</em> 保留会话天数</span>
<div
ref="sessionRetentionPickerRef"
class="session-picker-filter"
:class="{ open: sessionRetentionPickerOpen }"
>
<button
class="session-picker-trigger"
type="button"
:aria-expanded="sessionRetentionPickerOpen"
aria-haspopup="dialog"
@click="toggleSessionRetentionPicker"
>
<span class="session-picker-label">{{ pageState.sessionForm.conversationRetentionDays }} </span>
<i class="mdi mdi-chevron-down"></i>
</button>
<div
v-if="sessionRetentionPickerOpen"
class="session-picker-popover"
role="dialog"
aria-label="选择会话保留天数"
>
<header>
<strong>选择会话保留天数</strong>
<button type="button" aria-label="关闭会话保留天数选择" @click="closeSessionRetentionPicker">
<i class="mdi mdi-close"></i>
</button>
</header>
<div class="session-picker-option-list">
<button
v-for="option in sessionRetentionOptions"
:key="option.value"
type="button"
class="session-picker-option"
:class="{ active: pageState.sessionForm.conversationRetentionDays === option.value }"
@click="selectSessionRetentionDays(option.value)"
>
{{ option.label }}
</button>
</div>
</div>
</div>
<small>最小 1 最大 10 按会话最后活跃时间计算</small>
</label>
</div>
</section>
</template>
<template v-else-if="activeSection === 'hermes'">
<HermesEmployeeSettingsPanel
:hermes-form="pageState.hermesForm"
@toggle-master="toggleHermesMaster"
@toggle-flag="toggleHermesFlag"
@toggle-task="toggleHermesTask"
@update-task-time="updateHermesTaskTime"
/>
</template>
<template v-else-if="activeSection === 'llm'">
<LlmSettingsPanel :llm-form="pageState.llmForm" :provider-options="providerOptions" />
</template>
<template v-else-if="activeSection === 'rendering'">
<section class="settings-card rendering-settings-card">
<div class="card-head">
<div class="card-title-with-icon">
<div class="model-icon-box slate">
<i class="mdi mdi-file-document-edit-outline"></i>
</div>
<div>
<h4>ONLYOFFICE 服务配置</h4>
</div>
</div>
</div>
<div class="switch-group">
<button class="switch-row" type="button" @click="toggleBoolean('renderForm', 'enabled')">
<span class="switch-copy">
<strong>启用 ONLYOFFICE 文件渲染</strong>
<small>启用后知识库中的 Office 文件将优先走 ONLYOFFICE 在线预览</small>
</span>
<span class="switch-btn" :class="{ active: pageState.renderForm.enabled }"><i></i></span>
</button>
</div>
<div class="form-grid">
<label class="field field-full">
<span><em>*</em> ONLYOFFICE 服务地址</span>
<input
v-model="pageState.renderForm.publicUrl"
type="text"
placeholder="例如 http://10.10.10.122:8082"
/>
</label>
<label class="field field-full">
<span><em>*</em> JWT 密钥</span>
<input
v-model="pageState.renderForm.jwtSecret"
type="password"
autocomplete="off"
@focus="clearRenderSecretMask"
:placeholder="pageState.renderForm.jwtSecretConfigured ? '已配置,如需修改请重新输入' : '保存后不会保留在草稿中'"
/>
<small v-if="pageState.renderForm.jwtSecretConfigured" class="secret-bound-state">
<i class="mdi mdi-database-lock"></i>
<span>已从数据库加密加载预览签名会使用已保存密钥</span>
</small>
</label>
</div>
</section>
</template>
<template v-else-if="activeSection === 'logs'">
<section class="settings-card">
<div class="card-head">
<div class="card-title-with-icon">
<div class="model-icon-box slate">
<i class="mdi mdi-text-box-search-outline"></i>
</div>
<div>
<h4>日志级别与留存</h4>
<p>定义系统记录粒度归档周期和告警接收人方便后续审计与排障</p>
</div>
</div>
</div>
<div class="chip-row">
<button
v-for="level in logLevels"
:key="level"
class="level-chip"
:class="{ active: pageState.logForm.level === level }"
type="button"
@click="pageState.logForm.level = level"
>
{{ level }}
</button>
</div>
<div class="form-grid">
<label class="field">
<span><em>*</em> 留存天数</span>
<input v-model.number="pageState.logForm.retentionDays" type="number" min="7" />
</label>
<label class="field">
<span>归档周期</span>
<EnterpriseSelect
v-model="pageState.logForm.archiveCycle"
:options="archiveCycleOptions"
placeholder="选择归档周期"
/>
</label>
<label class="field field-full">
<span><em>*</em> 日志路径</span>
<input v-model="pageState.logForm.logPath" type="text" placeholder="例如 server/logs/app.log" />
</label>
<label class="field field-full">
<span>告警邮箱</span>
<input v-model="pageState.logForm.alertEmail" type="email" placeholder="用于接收日志异常提醒" />
</label>
</div>
</section>
<section class="settings-card">
<div class="card-head">
<div class="card-title-with-icon">
<div class="model-icon-box slate">
<i class="mdi mdi-eye-check-outline"></i>
</div>
<div>
<h4>审计策略</h4>
<p>决定是否记录关键操作登录行为以及是否对敏感字段进行脱敏处理</p>
</div>
</div>
</div>
<div class="switch-group">
<button class="switch-row" type="button" @click="toggleBoolean('logForm', 'operationAudit')">
<span class="switch-copy">
<strong>记录关键操作日志</strong>
<small>保存配置修改审批动作和账户管理等重要事件</small>
</span>
<span class="switch-btn" :class="{ active: pageState.logForm.operationAudit }"><i></i></span>
</button>
<button class="switch-row" type="button" @click="toggleBoolean('logForm', 'loginAudit')">
<span class="switch-copy">
<strong>记录登录审计</strong>
<small>追踪登录来源登录结果和异常登录行为</small>
</span>
<span class="switch-btn" :class="{ active: pageState.logForm.loginAudit }"><i></i></span>
</button>
<button class="switch-row" type="button" @click="toggleBoolean('logForm', 'maskSensitive')">
<span class="switch-copy">
<strong>敏感字段脱敏</strong>
<small>日志写入时自动隐藏密码密钥与认证令牌</small>
</span>
<span class="switch-btn" :class="{ active: pageState.logForm.maskSensitive }"><i></i></span>
</button>
</div>
</section>
</template>
<template v-else-if="activeSection === 'mail'">
<MailSettingsPanel :mail-form="pageState.mailForm" />
</template>
</div>
</div>
</div>
</section>
</template>
<script src="./scripts/SettingsView.js"></script>
<style scoped src="../assets/styles/views/settings-view-form.css"></style>
<style scoped src="../assets/styles/views/settings-view.css"></style>