Smail/frontend/src/views/components/ConfigSettings/ConfigEditor/ConfigEditor.vue
smail b61c4c9479
Some checks are pending
Security Checks / security (push) Waiting to run
精简配置表单重复标题说明并完成浏览器验收
2026-09-06 14:48:42 +08:00

203 lines
7.9 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>
<div class="config-page" :data-config-page="selectedSection" v-loading="loading">
<header class="config-overview" aria-labelledby="config-page-title">
<div class="overview-heading">
<div class="overview-copy">
<div class="overview-title">
<h3 id="config-page-title">{{ currentSection.label }}</h3>
<el-tag v-if="!loading && !availabilityError" :type="stateTagType" effect="light" size="small">
<span class="sr-only">运行状态:</span>{{ stateLabel }}
</el-tag>
</div>
<p class="settings-desc">{{ currentSection.description }}</p>
</div>
<div class="overview-actions">
<el-tag class="access-tag" :type="canWrite ? 'info' : 'warning'" effect="plain" size="small">
{{ canWrite ? '敏感凭据已脱敏' : '只读模式' }}
</el-tag>
<el-button
v-if="!availabilityError"
size="small"
plain
:loading="loading"
:disabled="!!working || sectionLoading || loading"
@click="refreshPage"
>
刷新状态
</el-button>
</div>
</div>
<template v-if="!loading && !availabilityError">
<dl class="overview-meta" aria-label="当前配置状态">
<div class="overview-meta-item">
<dt>生效代次</dt>
<dd>{{ status.activeGeneration || '-' }}</dd>
</div>
<div class="overview-meta-item">
<dt>最近校验</dt>
<dd>{{ formatTime(status.lastValidAt) }}</dd>
</div>
<div class="overview-meta-item" :class="{ 'overview-meta-warning': status.pendingRestart?.length }">
<dt>待重启服务</dt>
<dd v-if="status.pendingRestart?.length" class="tag-list">
<el-tag v-for="service in status.pendingRestart" :key="service" type="warning" effect="plain" size="small">
{{ serviceLabel(service) }}
</el-tag>
</dd>
<dd v-else>无需重启</dd>
</div>
</dl>
<el-alert
v-for="issue in status.degradedReasons || []"
:key="`${issue.code}-${issue.message}`"
:title="`${issue.code}:${issue.message}`"
type="warning"
:closable="false"
show-icon
class="degraded-alert"
/>
</template>
</header>
<el-alert
v-if="!canWrite"
title="当前账号仅能查看配置,保存、应用和回滚操作已隐藏。"
type="info"
:closable="false"
show-icon
class="config-alert"
/>
<div v-if="availabilityError" class="config-availability" role="status">
<el-alert :title="availabilityError" type="warning" :closable="false" show-icon />
<el-button :loading="loading" @click="refreshPage">重新检查</el-button>
</div>
<div v-else-if="!loading" class="config-page-body">
<section class="editor-panel" :aria-busy="sectionLoading">
<p v-if="selectedSection === 'ssl' && status.tlsMode === 'proxy'" class="heading-note" role="note">
当前由反向代理提供 HTTPS,Smail 不自动申请或续期 SSL 证书。下方配置仅用于本地邮件服务证书;外部续期后请重新导入并应用,不能仅更新反代服务器上的证书。
</p>
<el-alert
v-if="sectionError"
:title="sectionError"
type="error"
:closable="false"
show-icon
class="config-alert"
/>
<ConfigForm
v-if="!sectionLoading && !sectionError"
:section="selectedSection"
:config="formConfig"
:errors="formErrors"
:disabled="!canWrite || !!working || !!pendingCandidate"
@change="updateField"
/>
<p id="config-sensitive-hint" class="editor-hint">{{ sensitiveHint }}。</p>
<div v-if="secretField" class="secret-editor">
<div>
<label for="config-secret-input">{{ secretField.label }}</label>
<p>{{ secretField.description }}</p>
</div>
<el-input
id="config-secret-input"
:model-value="secretValue"
type="password"
show-password
clearable
:disabled="!canWrite || sectionLoading || !!working || !!sectionError || !!pendingCandidate"
:placeholder="secretField.placeholder"
autocomplete="new-password"
@update:model-value="updateSecret"
/>
</div>
<div v-if="canWrite" class="editor-footer">
<el-alert
v-if="pendingCandidate"
:title="`服务器存在待应用候选,代次 ${pendingCandidate.generation}。已锁定编辑;如需修改,请先取消候选。刷新页面不会丢失候选。`"
type="warning"
:closable="false"
show-icon
/>
<el-checkbox v-model="confirmed" class="confirmation-check" :disabled="sectionLoading || !!working || !!sectionError">
我已确认这是受控变更,并允许使用最近认证完成高风险配置操作
</el-checkbox>
<div class="action-row">
<el-button :loading="working === 'validate'" :disabled="sectionLoading || !!working || !!sectionError || !!pendingCandidate" @click="validateConfig">
只验证
</el-button>
<el-button type="primary" :loading="working === 'save'" :disabled="sectionLoading || !!working || !!sectionError || !!pendingCandidate" @click="saveConfig">
保存候选配置
</el-button>
<el-button v-if="pendingCandidate && !pendingCandidate.sameSession" :disabled="!!working || !confirmed" @click="managePending('resume')">
恢复候选到当前会话
</el-button>
<el-button v-if="pendingCandidate" type="warning" plain :disabled="!!working || pendingCandidate.committed" @click="managePending('cancel')">
取消候选配置
</el-button>
<el-button type="success" plain :loading="working === 'apply'" :disabled="sectionLoading || !!working || !savedGeneration" @click="applyConfig">
{{ savedGeneration ? `应用代次 ${savedGeneration}` : '应用配置' }}
</el-button>
</div>
<p class="action-hint">保存不会立即生效;需要继续修改时先取消候选。跨会话恢复或取消需要近期认证并勾选确认。</p>
</div>
<div v-if="validationResult" class="result-box" :class="validationResult.valid ? 'result-success' : 'result-error'" role="status">
<strong>{{ validationResult.valid ? '验证通过' : '验证未通过' }}</strong>
<span v-if="validationResult.generation">候选代次:{{ validationResult.generation }}</span>
<ul v-if="validationResult.issues?.length">
<li v-for="issue in validationResult.issues" :key="`${issue.code}-${issue.message}`">
{{ issue.code }}:{{ issue.message }}
</li>
</ul>
</div>
</section>
</div>
</div>
</template>
<script setup>
import { inject } from 'vue'
import ConfigForm from '../ConfigForm/ConfigForm.vue'
const {
availabilityError,
loading,
working,
status,
stateLabel,
stateTagType,
selectedSection,
currentSection,
sectionLoading,
formConfig,
formErrors,
updateField,
updateSecret,
sectionError,
sensitiveHint,
secretField,
secretValue,
confirmed,
savedGeneration,
pendingCandidate,
managePending,
validationResult,
canWrite,
refreshPage,
validateConfig,
saveConfig,
applyConfig,
formatTime,
serviceLabel
} = inject('smail-config-page')
</script>
<style scoped src="../ConfigSettings.css"></style>
<style scoped src="./ConfigEditor.css"></style>