feat: 增强知识库索引与设置页面模块化拆分

扩展知识库索引任务和 RAG 检索支持增量入库和文档去重,优
化本体检测和规则匹配精度,前端设置页面拆分为 LLM、邮件
和 Hermes 员工同步子面板并重构样式,新增日志详情组件和
知识入库日志模型,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-22 23:47:28 +08:00
parent 88ff04bef8
commit 5b388d08c0
84 changed files with 10170 additions and 2599 deletions

View File

@@ -5,7 +5,7 @@
<div class="settings-nav-head">
<span class="nav-kicker">Settings</span>
<h2>系统设置</h2>
<p>已完成 {{ completedSectionCount }} / {{ sections.length }} 项配置敏感字段不会保存在浏览器草稿中</p>
<p>已完成 {{ completedSectionCount }} / {{ sections.length }} 项配置</p>
</div>
<nav class="settings-nav-list">
@@ -47,18 +47,26 @@
<template v-if="activeSection === 'profile'">
<section class="settings-card">
<div class="card-head">
<div>
<h4>系统基本信息</h4>
<p>统一维护企业名称系统显示名称和版权信息保存后会同步更新当前系统品牌名称</p>
<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">
<i class="mdi mdi-domain"></i>
<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">
@@ -96,9 +104,14 @@
<template v-else-if="activeSection === 'admin'">
<section class="settings-card">
<div class="card-head">
<div>
<h4>管理员账号</h4>
<p>维护最高权限管理员的登录账户密码和安全通知邮箱</p>
<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>
@@ -137,9 +150,14 @@
<section class="settings-card">
<div class="card-head">
<div>
<h4>登录安全策略</h4>
<p>控制会话超时登录提醒和管理员高风险操作的基础安全策略</p>
<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>
@@ -161,7 +179,7 @@
<strong>开启双因素验证</strong>
<small>要求管理员使用附加验证步骤登录后台</small>
</span>
<span class="switch" :class="{ active: pageState.adminForm.mfaEnabled }"><i></i></span>
<span class="switch-btn" :class="{ active: pageState.adminForm.mfaEnabled }"><i></i></span>
</button>
<button class="switch-row" type="button" @click="toggleBoolean('adminForm', 'strongPassword')">
@@ -169,7 +187,7 @@
<strong>启用强密码策略</strong>
<small>管理员密码修改时需要满足强度要求</small>
</span>
<span class="switch" :class="{ active: pageState.adminForm.strongPassword }"><i></i></span>
<span class="switch-btn" :class="{ active: pageState.adminForm.strongPassword }"><i></i></span>
</button>
<button class="switch-row" type="button" @click="toggleBoolean('adminForm', 'loginAlertEnabled')">
@@ -177,356 +195,154 @@
<strong>异常登录提醒</strong>
<small>检测到高风险登录时向安全通知邮箱发送告警</small>
</span>
<span class="switch" :class="{ active: pageState.adminForm.loginAlertEnabled }"><i></i></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>
<h4>会话保留策略</h4>
<p>控制智能体会话在系统中的保留时长超过保留期的历史会话会自动清理</p>
</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 === 'llm'">
<div class="model-grid">
<section class="settings-card">
<div class="card-head">
<div>
<h4>主模型配置</h4>
<p>用于 AI 助手和主业务链路的默认模型接入</p>
</div>
<div class="card-head-actions">
<button class="test-button" type="button" :disabled="isModelTesting('main')" @click="testModelConnection('main')">
<i :class="isModelTesting('main') ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-connection'"></i>
<span>{{ isModelTesting('main') ? '测试中...' : '测试模型' }}</span>
</button>
</div>
</div>
</template>
<div class="form-grid">
<div class="field field-full">
<small class="secret-bound-state">
<i class="mdi mdi-source-branch"></i>
<span>保存后会同步写入 Hermes 配置外部 Hermes agent 也可通过后端共享接口读取这里的主模型配置</span>
</small>
</div>
<label class="field">
<span><em>*</em> 供应商</span>
<select v-model="pageState.llmForm.mainProvider" @change="applyProviderPreset('main')">
<option v-for="option in providerOptions" :key="option" :value="option">{{ option }}</option>
</select>
</label>
<label class="field">
<span><em>*</em> 模型名称</span>
<input v-model="pageState.llmForm.mainModel" type="text" placeholder="请输入主模型名称" />
</label>
<label class="field field-full">
<span><em>*</em> 接口地址</span>
<input v-model="pageState.llmForm.mainEndpoint" type="text" placeholder="请输入模型接口地址" />
</label>
<label class="field field-full">
<span>API Key</span>
<input
v-model="pageState.llmForm.mainApiKey"
type="password"
autocomplete="off"
@focus="clearModelSecretMask('main')"
:placeholder="pageState.llmForm.mainApiKeyConfigured ? '已配置,如需修改请重新输入' : '保存后不会保留在草稿中'"
/>
<small v-if="pageState.llmForm.mainApiKeyConfigured" class="secret-bound-state">
<i class="mdi mdi-database-lock"></i>
<span>已从数据库加密加载测试会使用已保存密钥</span>
</small>
</label>
</div>
<div v-if="getModelTestState('main').message" class="test-feedback" :class="`is-${getModelTestState('main').status}`">
<i :class="getModelTestState('main').status === 'success' ? 'mdi mdi-check-circle' : getModelTestState('main').status === 'testing' ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-alert-circle'"></i>
<span>{{ getModelTestState('main').message }}</span>
</div>
</section>
<section class="settings-card">
<div class="card-head">
<div>
<h4>备份模型配置</h4>
<p>主模型不可用时用于兜底切换的备用模型接入</p>
</div>
<div class="card-head-actions">
<button class="test-button" type="button" :disabled="isModelTesting('backup')" @click="testModelConnection('backup')">
<i :class="isModelTesting('backup') ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-connection'"></i>
<span>{{ isModelTesting('backup') ? '测试中...' : '测试模型' }}</span>
</button>
</div>
</div>
<div class="form-grid">
<label class="field">
<span><em>*</em> 供应商</span>
<select v-model="pageState.llmForm.backupProvider" @change="applyProviderPreset('backup')">
<option v-for="option in providerOptions" :key="option" :value="option">{{ option }}</option>
</select>
</label>
<label class="field">
<span><em>*</em> 模型名称</span>
<input v-model="pageState.llmForm.backupModel" type="text" placeholder="请输入备份模型名称" />
</label>
<label class="field field-full">
<span><em>*</em> 接口地址</span>
<input v-model="pageState.llmForm.backupEndpoint" type="text" placeholder="请输入模型接口地址" />
</label>
<label class="field field-full">
<span>API Key</span>
<input
v-model="pageState.llmForm.backupApiKey"
type="password"
autocomplete="off"
@focus="clearModelSecretMask('backup')"
:placeholder="pageState.llmForm.backupApiKeyConfigured ? '已配置,如需修改请重新输入' : '保存后不会保留在草稿中'"
/>
<small v-if="pageState.llmForm.backupApiKeyConfigured" class="secret-bound-state">
<i class="mdi mdi-database-lock"></i>
<span>已从数据库加密加载测试会使用已保存密钥</span>
</small>
</label>
</div>
<div v-if="getModelTestState('backup').message" class="test-feedback" :class="`is-${getModelTestState('backup').status}`">
<i :class="getModelTestState('backup').status === 'success' ? 'mdi mdi-check-circle' : getModelTestState('backup').status === 'testing' ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-alert-circle'"></i>
<span>{{ getModelTestState('backup').message }}</span>
</div>
</section>
<section class="settings-card">
<div class="card-head">
<div>
<h4>Embedding 模型配置</h4>
<p>用于向量检索知识库召回和语义匹配的嵌入模型设置</p>
</div>
<div class="card-head-actions">
<button class="test-button" type="button" :disabled="isModelTesting('embedding')" @click="testModelConnection('embedding')">
<i :class="isModelTesting('embedding') ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-connection'"></i>
<span>{{ isModelTesting('embedding') ? '测试中...' : '测试模型' }}</span>
</button>
</div>
</div>
<div class="form-grid">
<label class="field">
<span><em>*</em> 供应商</span>
<select v-model="pageState.llmForm.embeddingProvider" @change="applyProviderPreset('embedding')">
<option v-for="option in providerOptions" :key="option" :value="option">{{ option }}</option>
</select>
</label>
<label class="field">
<span><em>*</em> 模型名称</span>
<input v-model="pageState.llmForm.embeddingModel" type="text" placeholder="请输入 Embedding 模型名称" />
</label>
<label class="field field-full">
<span><em>*</em> 接口地址</span>
<input v-model="pageState.llmForm.embeddingEndpoint" type="text" placeholder="请输入模型接口地址" />
</label>
<label class="field field-full">
<span>API Key</span>
<input
v-model="pageState.llmForm.embeddingApiKey"
type="password"
autocomplete="off"
@focus="clearModelSecretMask('embedding')"
:placeholder="pageState.llmForm.embeddingApiKeyConfigured ? '已配置,如需修改请重新输入' : '保存后不会保留在草稿中'"
/>
<small v-if="pageState.llmForm.embeddingApiKeyConfigured" class="secret-bound-state">
<i class="mdi mdi-database-lock"></i>
<span>已从数据库加密加载测试会使用已保存密钥</span>
</small>
</label>
</div>
<div
v-if="getModelTestState('embedding').message"
class="test-feedback"
:class="`is-${getModelTestState('embedding').status}`"
>
<i :class="getModelTestState('embedding').status === 'success' ? 'mdi mdi-check-circle' : getModelTestState('embedding').status === 'testing' ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-alert-circle'"></i>
<span>{{ getModelTestState('embedding').message }}</span>
</div>
</section>
<section class="settings-card">
<div class="card-head">
<div>
<h4>Reranker 模型配置</h4>
<p>用于检索结果重排和语义精排的 Reranker 模型设置</p>
</div>
<div class="card-head-actions">
<button class="test-button" type="button" :disabled="isModelTesting('reranker')" @click="testModelConnection('reranker')">
<i :class="isModelTesting('reranker') ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-connection'"></i>
<span>{{ isModelTesting('reranker') ? '测试中...' : '测试模型' }}</span>
</button>
</div>
</div>
<div class="form-grid">
<label class="field">
<span><em>*</em> 供应商</span>
<select v-model="pageState.llmForm.rerankerProvider" @change="applyProviderPreset('reranker')">
<option v-for="option in providerOptions" :key="option" :value="option">{{ option }}</option>
</select>
</label>
<label class="field">
<span><em>*</em> 模型名称</span>
<input v-model="pageState.llmForm.rerankerModel" type="text" placeholder="请输入 Reranker 模型名称" />
</label>
<label class="field field-full">
<span><em>*</em> 接口地址</span>
<input v-model="pageState.llmForm.rerankerEndpoint" type="text" placeholder="请输入模型接口地址" />
</label>
<label class="field field-full">
<span>API Key</span>
<input
v-model="pageState.llmForm.rerankerApiKey"
type="password"
autocomplete="off"
@focus="clearModelSecretMask('reranker')"
:placeholder="pageState.llmForm.rerankerApiKeyConfigured ? '已配置,如需修改请重新输入' : '保存后不会保留在草稿中'"
/>
<small v-if="pageState.llmForm.rerankerApiKeyConfigured" class="secret-bound-state">
<i class="mdi mdi-database-lock"></i>
<span>已从数据库加密加载测试会使用已保存密钥</span>
</small>
</label>
</div>
<div
v-if="getModelTestState('reranker').message"
class="test-feedback"
:class="`is-${getModelTestState('reranker').status}`"
>
<i :class="getModelTestState('reranker').status === 'success' ? 'mdi mdi-check-circle' : getModelTestState('reranker').status === 'testing' ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-alert-circle'"></i>
<span>{{ getModelTestState('reranker').message }}</span>
</div>
</section>
</div>
</template>
<template v-else-if="activeSection === 'rendering'">
<section class="settings-card rendering-settings-card">
<div class="card-head">
<div>
<h4>ONLYOFFICE 服务配置</h4>
</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" :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">
<template v-else-if="activeSection === 'session'">
<section class="settings-card">
<div class="card-head">
<div>
<h4>日志级别与留存</h4>
<p>定义系统记录粒度归档周期和告警接收人方便后续审计与排障</p>
<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>
@@ -572,9 +388,14 @@
<section class="settings-card">
<div class="card-head">
<div>
<h4>审计策略</h4>
<p>决定是否记录关键操作登录行为以及是否对敏感字段进行脱敏处理</p>
<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>
@@ -584,7 +405,7 @@
<strong>记录关键操作日志</strong>
<small>保存配置修改审批动作和账户管理等重要事件</small>
</span>
<span class="switch" :class="{ active: pageState.logForm.operationAudit }"><i></i></span>
<span class="switch-btn" :class="{ active: pageState.logForm.operationAudit }"><i></i></span>
</button>
<button class="switch-row" type="button" @click="toggleBoolean('logForm', 'loginAudit')">
@@ -592,7 +413,7 @@
<strong>记录登录审计</strong>
<small>追踪登录来源登录结果和异常登录行为</small>
</span>
<span class="switch" :class="{ active: pageState.logForm.loginAudit }"><i></i></span>
<span class="switch-btn" :class="{ active: pageState.logForm.loginAudit }"><i></i></span>
</button>
<button class="switch-row" type="button" @click="toggleBoolean('logForm', 'maskSensitive')">
@@ -600,106 +421,14 @@
<strong>敏感字段脱敏</strong>
<small>日志写入时自动隐藏密码密钥与认证令牌</small>
</span>
<span class="switch" :class="{ active: pageState.logForm.maskSensitive }"><i></i></span>
<span class="switch-btn" :class="{ active: pageState.logForm.maskSensitive }"><i></i></span>
</button>
</div>
</section>
</template>
<template v-else>
<section class="settings-card">
<div class="card-head">
<div>
<h4>SMTP 基础配置</h4>
<p>维护系统发信地址认证账号和加密方式用于审批提醒与系统通知</p>
</div>
</div>
<div class="form-grid">
<label class="field">
<span><em>*</em> SMTP Host</span>
<input v-model="pageState.mailForm.smtpHost" type="text" placeholder="请输入 SMTP Host" />
</label>
<label class="field">
<span><em>*</em> 端口</span>
<input v-model.number="pageState.mailForm.port" type="number" min="1" max="65535" />
</label>
<label class="field">
<span>加密方式</span>
<select v-model="pageState.mailForm.encryption">
<option value="SSL/TLS">SSL/TLS</option>
<option value="STARTTLS">STARTTLS</option>
<option value="None"></option>
</select>
</label>
<label class="field">
<span>发件人名称</span>
<input v-model="pageState.mailForm.senderName" type="text" placeholder="请输入发件人名称" />
</label>
<label class="field">
<span><em>*</em> 发件人邮箱</span>
<input v-model="pageState.mailForm.senderAddress" type="email" placeholder="请输入发件人邮箱" />
</label>
<label class="field">
<span><em>*</em> 登录账号</span>
<input v-model="pageState.mailForm.username" type="text" placeholder="请输入 SMTP 登录账号" />
</label>
<label class="field field-full">
<span>SMTP 密码</span>
<input
v-model="pageState.mailForm.password"
type="password"
autocomplete="off"
:placeholder="pageState.mailForm.passwordConfigured ? '已配置,如需修改请重新输入' : '保存后不会保留在草稿中'"
/>
</label>
</div>
</section>
<section class="settings-card">
<div class="card-head">
<div>
<h4>通知策略</h4>
<p>控制是否启用邮件通知日报摘要以及默认接收邮箱</p>
</div>
</div>
<div class="switch-group">
<button class="switch-row" type="button" @click="toggleBoolean('mailForm', 'alertEnabled')">
<span class="switch-copy">
<strong>启用系统通知</strong>
<small>审批异常告警和系统事件可通过邮件触达用户</small>
</span>
<span class="switch" :class="{ active: pageState.mailForm.alertEnabled }"><i></i></span>
</button>
<button class="switch-row" type="button" @click="toggleBoolean('mailForm', 'digestEnabled')">
<span class="switch-copy">
<strong>启用日报摘要</strong>
<small>按固定时间发送系统运行与待办摘要</small>
</span>
<span class="switch" :class="{ active: pageState.mailForm.digestEnabled }"><i></i></span>
</button>
</div>
<div class="form-grid compact-grid">
<label class="field">
<span>摘要发送时间</span>
<input v-model="pageState.mailForm.digestTime" type="time" />
</label>
<label class="field">
<span>默认接收邮箱</span>
<input v-model="pageState.mailForm.defaultReceiver" type="email" placeholder="请输入默认接收邮箱" />
</label>
</div>
</section>
<template v-else-if="activeSection === 'mail'">
<MailSettingsPanel :mail-form="pageState.mailForm" />
</template>
</div>
</div>
@@ -709,4 +438,5 @@
<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>