485 lines
20 KiB
Vue
485 lines
20 KiB
Vue
<script setup>
|
||
import HelpTip from "../components/HelpTip/HelpTip.vue";
|
||
import { useSettingsModal } from "./SettingsModal.js";
|
||
|
||
const props = defineProps({ state: { type: Object, required: true } });
|
||
const {
|
||
settingsForm,
|
||
showSettings,
|
||
settingsSaving,
|
||
settingsError,
|
||
dnsProviderCatalog,
|
||
selectedDNSProvider,
|
||
dnsTesting,
|
||
dnsTestError,
|
||
dnsVerificationElapsedSeconds,
|
||
dnsVerificationTimeoutSeconds,
|
||
dnsReadyToSave,
|
||
handleDNSProviderChange,
|
||
clearDNSCredentials,
|
||
testDNS,
|
||
closeSettings,
|
||
saveSettings,
|
||
dnsVerificationRequired,
|
||
} = props.state;
|
||
const { activeSection, submitSettings, navigateTabs } = useSettingsModal(saveSettings);
|
||
</script>
|
||
|
||
<template>
|
||
<div v-if="showSettings" class="modal-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>
|
||
<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-http"
|
||
type="button"
|
||
role="tab"
|
||
aria-controls="settings-panel-http"
|
||
:aria-selected="activeSection === 'http'"
|
||
:tabindex="activeSection === 'http' ? 0 : -1"
|
||
@click="activeSection = 'http'"
|
||
@keydown="navigateTabs"
|
||
>
|
||
HTTP 通知
|
||
</button>
|
||
<button
|
||
id="settings-tab-smtp"
|
||
type="button"
|
||
role="tab"
|
||
aria-controls="settings-panel-smtp"
|
||
:aria-selected="activeSection === 'smtp'"
|
||
:tabindex="activeSection === 'smtp' ? 0 : -1"
|
||
@click="activeSection = 'smtp'"
|
||
@keydown="navigateTabs"
|
||
>
|
||
SMTP 通知
|
||
</button>
|
||
</div>
|
||
<form novalidate @submit.prevent="submitSettings">
|
||
<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 === 'http'"
|
||
id="settings-panel-http"
|
||
data-settings-section="http"
|
||
role="tabpanel"
|
||
aria-labelledby="settings-tab-http"
|
||
class="settings-panel"
|
||
>
|
||
<h3>HTTP 通知</h3>
|
||
<div class="form-grid">
|
||
<label class="switch-field">
|
||
<input v-model="settingsForm.notification.http_enabled" type="checkbox" />
|
||
<span>
|
||
<strong>启用 HTTP 通知</strong>
|
||
<HelpTip>通过外部邮件或消息服务 API 发送通知</HelpTip>
|
||
</span>
|
||
</label>
|
||
<label class="field">
|
||
<span>通知服务商</span>
|
||
<input v-model="settingsForm.notification.provider" placeholder="例如 temporary-mail" />
|
||
</label>
|
||
</div>
|
||
<div class="form-grid">
|
||
<label class="field">
|
||
<span>通知接口地址</span>
|
||
<input v-model="settingsForm.notification.endpoint" placeholder="https://..." />
|
||
</label>
|
||
<label class="field">
|
||
<span>发件人邮箱</span>
|
||
<input v-model="settingsForm.notification.sender_email" type="email" />
|
||
</label>
|
||
</div>
|
||
<label class="field">
|
||
<span>收件人</span>
|
||
<input :value="settingsForm.notification.recipients.join(', ')" @input="settingsForm.notification.recipients = $event.target.value.split(/[,,]/).map((item) => item.trim()).filter(Boolean)" placeholder="多个地址用逗号分隔" />
|
||
</label>
|
||
</section>
|
||
<section
|
||
v-show="activeSection === 'smtp'"
|
||
id="settings-panel-smtp"
|
||
data-settings-section="smtp"
|
||
role="tabpanel"
|
||
aria-labelledby="settings-tab-smtp"
|
||
class="settings-panel"
|
||
>
|
||
<h3>SMTP 通知</h3>
|
||
<label class="switch-field">
|
||
<input v-model="settingsForm.notification.smtp_enabled" type="checkbox" />
|
||
<span>
|
||
<strong>启用 SMTP 通知</strong>
|
||
<HelpTip>使用外部 SMTP 服务发送证书任务通知,不会实现邮件服务器端</HelpTip>
|
||
</span>
|
||
</label>
|
||
<div class="form-grid">
|
||
<label class="field">
|
||
<span>SMTP 主机</span>
|
||
<input v-model="settingsForm.notification.smtp_host" placeholder="例如 smtp.example.com" />
|
||
</label>
|
||
<label class="field">
|
||
<span>SMTP 端口</span>
|
||
<input v-model.number="settingsForm.notification.smtp_port" type="number" min="1" max="65535" />
|
||
</label>
|
||
</div>
|
||
<div class="form-grid">
|
||
<label class="field">
|
||
<span>SMTP 用户名</span>
|
||
<input v-model="settingsForm.notification.smtp_username" autocomplete="username" />
|
||
</label>
|
||
<label class="field">
|
||
<span>SMTP 密码</span>
|
||
<input v-model="settingsForm.notification.smtp_password" type="password" autocomplete="new-password" placeholder="留空表示不更新已保存密码" />
|
||
</label>
|
||
</div>
|
||
<div class="form-grid">
|
||
<label class="field">
|
||
<span>连接模式</span>
|
||
<select v-model="settingsForm.notification.smtp_mode">
|
||
<option value="plain">明文连接</option>
|
||
<option value="starttls">STARTTLS</option>
|
||
<option value="implicit_tls">隐式 TLS</option>
|
||
</select>
|
||
</label>
|
||
<label class="switch-field">
|
||
<input v-model="settingsForm.notification.smtp_allow_plaintext_auth" type="checkbox" />
|
||
<span>
|
||
<strong>允许明文认证</strong>
|
||
<HelpTip>仅在明确确认 SMTP 网络受保护时开启</HelpTip>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
<HelpTip>
|
||
{{ settingsForm.notification.smtp_credential_configured ? "SMTP 凭据已配置;密码不会回显。填写新密码后保存即可更新。" : "SMTP 凭据尚未配置;启用 SMTP 时请填写用户名和密码。" }}
|
||
</HelpTip>
|
||
</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>
|
||
</form>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped src="./Dashboard.css"></style>
|
||
<style scoped src="./SettingsModal.css"></style>
|