Scert/frontend/src/views/Dashboard/SettingsModal.vue
xuans ba6bd80382 收紧全局设置与DNS权限并更新进度清单
限制系统设置修改、DNS在线测试和续期操作的角色权限,补充前后端回归测试与开发进度记录。
2026-09-29 22:33:32 +08:00

397 lines
16 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.

<script setup>
import HelpTip from "../components/HelpTip/HelpTip.vue";
import { useSettingsModal } from "./SettingsModal.js";
import MailNotificationSettings from "./MailNotificationSettings.vue";
const props = defineProps({ state: { type: Object, required: true } });
const {
settingsForm,
showSettings,
settingsSaving,
settingsError,
dnsProviderCatalog,
selectedDNSProvider,
dnsTesting,
dnsTestError,
dnsVerificationElapsedSeconds,
dnsVerificationTimeoutSeconds,
dnsReadyToSave,
canManageSystemSettings,
handleDNSProviderChange,
clearDNSCredentials,
testDNS,
closeSettings,
saveSettings,
dnsVerificationRequired,
} = props.state;
const { activeSection, submitSettings, navigateTabs } = useSettingsModal(saveSettings);
</script>
<template>
<div v-if="showSettings" class="modal-backdrop settings-backdrop" @click.self="closeSettings">
<section class="modal-card settings-modal" role="dialog" aria-modal="true" aria-label="系统设置">
<div class="modal-heading">
<div>
<p class="eyebrow">系统配置</p>
<h2 id="settings-title">系统设置</h2>
</div>
<button class="close-button" type="button" aria-label="关闭" @click="closeSettings">×</button>
</div>
<p v-if="!canManageSystemSettings" class="settings-readonly-notice">
当前账号仅可查看系统设置,修改配置和 DNS 在线验证需要超级管理员权限。
</p>
<div class="settings-tabs" role="tablist" aria-labelledby="settings-title">
<button
id="settings-tab-service"
type="button"
role="tab"
aria-controls="settings-panel-service"
:aria-selected="activeSection === 'service'"
:tabindex="activeSection === 'service' ? 0 : -1"
@click="activeSection = 'service'"
@keydown="navigateTabs"
>
服务监听
</button>
<button
id="settings-tab-acme"
type="button"
role="tab"
aria-controls="settings-panel-acme"
:aria-selected="activeSection === 'acme'"
:tabindex="activeSection === 'acme' ? 0 : -1"
@click="activeSection = 'acme'"
@keydown="navigateTabs"
>
ACME 设置
</button>
<button
id="settings-tab-dns"
type="button"
role="tab"
aria-controls="settings-panel-dns"
:aria-selected="activeSection === 'dns'"
:tabindex="activeSection === 'dns' ? 0 : -1"
@click="activeSection = 'dns'"
@keydown="navigateTabs"
>
DNS API 验证
</button>
<button
id="settings-tab-notification"
type="button"
role="tab"
aria-controls="settings-panel-notification"
:aria-selected="activeSection === 'notification'"
:tabindex="activeSection === 'notification' ? 0 : -1"
@click="activeSection = 'notification'"
@keydown="navigateTabs"
>
邮件通知
</button>
</div>
<form novalidate @submit.prevent="submitSettings">
<fieldset :disabled="!canManageSystemSettings" class="settings-fields">
<section
v-show="activeSection === 'service'"
id="settings-panel-service"
data-settings-section="service"
role="tabpanel"
aria-labelledby="settings-tab-service"
class="settings-panel"
>
<h3>服务监听</h3>
<div class="form-grid">
<label class="field">
<span>监听地址</span>
<input v-model="settingsForm.server.host" required />
</label>
<label class="field">
<span>服务端口</span>
<input v-model.number="settingsForm.server.port" type="number" min="1" max="65535" required />
</label>
</div>
<div class="form-grid">
<label class="field">
<span>HTTPS 模式</span>
<select v-model="settingsForm.server.https_mode">
<option value="direct">直接终止 HTTPS</option>
<option value="proxy">反向代理 HTTPS</option>
</select>
</label>
<label v-if="settingsForm.server.https_mode === 'proxy'" class="field">
<span>代理公开 HTTPS 端口(0 为默认 443)</span>
<input v-model.number="settingsForm.server.proxy_https_port" type="number" min="0" max="65535" required />
</label>
<label class="field">
<span>证书存储路径</span>
<input v-model="settingsForm.paths.certificate" required />
</label>
</div>
<div class="form-grid">
<label class="field">
<span>管理页面域名</span>
<input v-model="settingsForm.web.hostname" placeholder="例如 ssl.example.com" />
</label>
<label class="field">
<span>管理页面公网 IP</span>
<input v-model="settingsForm.web.public_ip" placeholder="例如 203.0.113.10" />
</label>
</div>
<div class="settings-web-controls">
<label class="switch-field settings-switch-field">
<input v-model="settingsForm.web.enabled" type="checkbox" />
<span>
<strong>开启 Web 管理页面</strong>
<HelpTip>关闭后停止网络 Web 管理入口,桌面端本地管理功能不受影响。</HelpTip>
</span>
</label>
<HelpTip>修改此开关后需要重启 Scert 服务,新的监听状态才会生效。</HelpTip>
</div>
</section>
<section
v-show="activeSection === 'acme'"
id="settings-panel-acme"
data-settings-section="acme"
role="tabpanel"
aria-labelledby="settings-tab-acme"
class="settings-panel"
>
<h3>ACME 设置</h3>
<div class="form-grid">
<label class="field">
<span>ACME 环境</span>
<select v-model="settingsForm.acme.environment">
<option value="staging">测试环境</option>
<option value="production">生产环境</option>
</select>
</label>
<label class="field">
<span>自动续期阈值(天)</span>
<input v-model.number="settingsForm.acme.renewal_threshold_days" type="number" min="1" max="365" />
</label>
</div>
<div class="form-grid">
<label class="field">
<span>常规检查间隔(天)</span>
<input v-model.number="settingsForm.acme.renewal_check_interval_days" type="number" min="1" max="30" />
</label>
<label class="field">
<span>临近到期检查阈值(天)</span>
<input v-model.number="settingsForm.acme.renewal_near_expiry_days" type="number" min="1" max="365" />
</label>
</div>
<div class="form-grid">
<label class="field">
<span>紧急续期阈值(天)</span>
<input v-model.number="settingsForm.acme.renewal_urgent_threshold_days" type="number" min="1" max="365" />
</label>
<label class="field">
<span>紧急续期最大尝试次数</span>
<input v-model.number="settingsForm.acme.renewal_max_retries" type="number" min="1" max="10" />
</label>
</div>
<div class="form-grid">
<label class="field">
<span>续期重试间隔(分钟)</span>
<input v-model.number="settingsForm.acme.renewal_retry_interval_mins" type="number" min="1" max="1440" />
</label>
<HelpTip>剩余有效期低于自动续期阈值时触发续期;低于紧急阈值时按配置重试。</HelpTip>
</div>
<div class="form-grid">
<label class="field">
<span>DNS 传播等待(秒)</span>
<input
v-model.number="settingsForm.acme.propagation_timeout_secs"
type="number"
min="1"
/>
</label>
<label class="field">
<span>DNS 检测间隔(秒)</span>
<input
v-model.number="settingsForm.acme.propagation_polling_interval_secs"
type="number"
min="1"
/>
</label>
</div>
<HelpTip>
阿里云 RAM 权限验证最多等待 6 秒:系统会创建、查询并清理临时 TXT
记录。以上 300 秒和 5 秒用于实际申请证书时的公网 DNS-01
传播等待与轮询。
</HelpTip>
</section>
<section
v-show="activeSection === 'dns'"
id="settings-panel-dns"
data-settings-section="dns"
role="tabpanel"
aria-labelledby="settings-tab-dns"
class="settings-panel"
>
<h3>DNS API 验证</h3>
<HelpTip>
<span>输入域名后,系统将使用当前阿里云账号立即创建、查询并清理临时 TXT 记录,以验证该域名的实际 DNS 操作权限。</span>
<span>凭据只会在本次在线测试请求中提交,系统仅保存加密引用。</span>
</HelpTip>
<div class="form-grid">
<label class="field" for="settings-dns-provider">
<span>DNS 服务商</span>
<select
id="settings-dns-provider"
v-model="settingsForm.dns.provider"
@change="handleDNSProviderChange"
>
<option value="">暂不配置</option>
<option
v-for="provider in dnsProviderCatalog"
:key="provider.id"
:value="provider.id"
:disabled="!provider.available"
>
{{ provider.name }}{{ provider.available ? "" : "(暂不可用)" }}
</option>
</select>
</label>
<label class="field" for="settings-dns-test-domain">
<span>在线测试域名</span>
<input
id="settings-dns-test-domain"
v-model="settingsForm.dns.test_domain"
placeholder="例如 example.com"
autocomplete="off"
/>
</label>
</div>
<HelpTip v-if="selectedDNSProvider">
{{ selectedDNSProvider.description }}
</HelpTip>
<p
v-if="selectedDNSProvider && !selectedDNSProvider.available"
class="form-error"
>
当前服务商适配器暂未开放,无法进行在线测试。
</p>
<div v-if="selectedDNSProvider" class="dns-credentials-grid">
<label
v-for="field in selectedDNSProvider.credential_fields"
:key="field.name"
class="field"
:for="`settings-dns-${field.name}`"
>
<span>
{{ field.label }}
<small v-if="field.required && dnsVerificationRequired">必填</small>
</span>
<input
:id="`settings-dns-${field.name}`"
v-model="settingsForm.dns.credentials[field.name]"
:type="field.secret ? 'password' : 'text'"
:placeholder="field.placeholder"
:required="field.required && dnsVerificationRequired"
autocomplete="off"
/>
</label>
</div>
<div v-if="selectedDNSProvider" class="dns-test-actions">
<button
class="secondary-button"
type="button"
:disabled="dnsTesting || settingsSaving || !selectedDNSProvider.available"
@click="testDNS"
>
{{ dnsTesting ? "测试中……" : "在线测试 DNS API" }}
</button>
<button
class="text-button"
type="button"
:disabled="dnsTesting || settingsSaving"
@click="clearDNSCredentials"
>
清空凭据
</button>
<span
class="dns-verification-status"
:class="settingsForm.dns.verification_state"
>
{{ settingsForm.dns.verification_state === "verified" ? "已验证" : "待验证" }}
</span>
</div>
<div
v-if="dnsTesting"
class="dns-verification-progress"
role="status"
aria-live="polite"
>
<strong>正在进行 DNS 验证,请等待</strong>
<span>
已等待 {{ dnsVerificationElapsedSeconds }} 秒,最长等待
{{ dnsVerificationTimeoutSeconds }} 秒。
</span>
<small v-if="selectedDNSProvider?.id === 'aliyun'">
正在通过阿里云 DNS API 创建、查询并清理临时 TXT 记录,不等待公网 DNS 传播,通常数秒即可完成;实际申请证书时,仍可能需要等待公网 DNS-01 记录传播。
</small>
<small v-else>
系统正在创建临时 TXT、查询验证记录并自动清理。这是 DNS API 权限验证;实际申请证书时,仍可能需要等待公网 DNS-01 记录传播。
</small>
</div>
<p v-if="dnsTestError" class="form-error">{{ dnsTestError }}</p>
<div
v-if="settingsForm.dns.test_report?.stages?.length"
class="dns-test-report"
>
<div class="dns-test-report-heading">
<strong>在线测试结果</strong>
<span v-if="settingsForm.dns.test_report.record_name">
临时记录:{{ settingsForm.dns.test_report.record_name }}
</span>
</div>
<div
v-for="stage in settingsForm.dns.test_report.stages"
:key="stage.name"
class="dns-test-stage"
>
<span class="dns-stage-indicator" :class="stage.status">
</span>
<div>
<strong>{{ stage.name }}</strong>
<span>{{ stage.message }}</span>
</div>
</div>
</div>
<HelpTip v-if="!dnsReadyToSave">
DNS 服务商发生变化或尚未配置时,必须先完成 DNS API 在线测试。
</HelpTip>
</section>
<section
v-show="activeSection === 'notification'"
id="settings-panel-notification"
data-settings-section="notification"
role="tabpanel"
aria-labelledby="settings-tab-notification"
class="settings-panel"
>
<h3>邮件通知</h3>
<MailNotificationSettings :notification="settingsForm.notification" />
</section>
<p v-if="settingsError" class="form-error">{{ settingsError }}</p>
<div class="modal-actions">
<button class="secondary-button" type="button" :disabled="settingsSaving" @click="closeSettings">取消</button>
<button
class="primary-button"
type="submit"
:disabled="settingsSaving || !dnsReadyToSave"
>
{{ settingsSaving ? "保存中……" : "保存设置" }}
</button>
</div>
</fieldset>
</form>
</section>
</div>
</template>
<style scoped src="./Dashboard.css"></style>
<style scoped src="./SettingsModal.css"></style>