完善仪表盘DNS在线测试界面

This commit is contained in:
xuans 2026-08-30 23:14:44 +08:00
parent f1c7fc34bd
commit 21dad55b45
3 changed files with 298 additions and 2 deletions

View File

@ -1020,4 +1020,155 @@ code {
.version-row {
flex-direction: column;
}
}
}
.settings-modal {
width: min(100%, 1080px);
max-height: calc(100vh - 24px);
padding: 30px 34px;
}
.settings-section-note {
display: grid;
gap: 4px;
margin: -2px 0 16px;
padding: 11px 13px;
border: 1px solid #d8e6f3;
border-radius: 10px;
color: #637895;
background: #f7fbff;
font-size: 12px;
line-height: 1.55;
}
.dns-provider-description {
margin: -3px 0 15px;
padding: 0 2px;
line-height: 1.55;
}
.dns-credentials-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0 14px;
}
.dns-test-actions {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin: 2px 0 15px;
padding: 12px 13px;
border: 1px solid #d8e6f3;
border-radius: 11px;
background: #fbfdff;
}
.dns-test-actions .text-button {
padding: 5px 2px;
}
.dns-verification-status {
margin-left: auto;
padding: 5px 10px;
border-radius: 999px;
color: #8b681e;
background: #fff4d9;
font-size: 12px;
font-weight: 800;
}
.dns-verification-status.verified {
color: #24734b;
background: #e7f8ee;
}
.dns-test-report {
display: grid;
gap: 8px;
margin: 8px 0 16px;
padding: 13px 14px;
border: 1px solid #d9e6f3;
border-radius: 11px;
background: #f8fbff;
}
.dns-test-report-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
padding-bottom: 4px;
}
.dns-test-report-heading strong {
color: #32405a;
font-size: 13px;
}
.dns-test-report-heading span,
.dns-test-stage span {
color: #6d7b91;
font-size: 12px;
line-height: 1.5;
}
.dns-test-stage {
display: grid;
grid-template-columns: 8px minmax(0, 1fr);
align-items: start;
gap: 9px;
padding: 8px 0;
border-top: 1px solid #e6eef6;
}
.dns-test-stage > div {
display: grid;
gap: 3px;
min-width: 0;
}
.dns-test-stage strong {
color: #32405a;
font-size: 12px;
}
.dns-stage-indicator {
width: 8px;
height: 8px;
margin-top: 4px;
border-radius: 50%;
background: #f0b44c;
}
.dns-stage-indicator.success {
background: #45ad72;
}
.dns-stage-indicator.failed,
.dns-stage-indicator.error {
background: #c55c5c;
}
@media (min-width: 1100px) {
.settings-modal {
width: min(100%, 1080px);
}
}
@media (max-width: 700px) {
.settings-modal {
max-height: calc(100vh - 16px);
padding: 24px 20px;
}
.dns-credentials-grid {
grid-template-columns: 1fr;
}
.dns-verification-status {
margin-left: 0;
}
}

View File

@ -1029,6 +1029,7 @@ export function useDashboard() {
taskResponse,
auditResponse,
settingsResponse,
dnsProviderResponse,
] = await Promise.all([
fetchAdmin("/api/v1/status"),
fetchAdmin("/api/v1/domains"),
@ -1038,6 +1039,7 @@ export function useDashboard() {
fetchAdmin("/api/v1/tasks?limit=20"),
fetchAdmin("/api/v1/audit-logs?limit=20"),
fetchAdmin("/api/v1/settings"),
fetchAdmin("/api/v1/dns/providers"),
]);
const failedResponse = [
statusResponse,
@ -1048,6 +1050,7 @@ export function useDashboard() {
taskResponse,
auditResponse,
settingsResponse,
dnsProviderResponse,
].find((response) => !response.ok);
if (failedResponse) {
throw new Error(await responseError(failedResponse, "读取管理数据失败"));
@ -1060,6 +1063,12 @@ export function useDashboard() {
tasks.value = (await taskResponse.json()).items || [];
auditLogs.value = (await auditResponse.json()).items || [];
settings.value = await settingsResponse.json();
const providerResult = await dnsProviderResponse.json();
dnsProviderCatalog.value = (providerResult.items || []).map((item) => ({
...item,
value: item.id,
label: item.name,
}));
settingsForm.value = normalizeSettings(settings.value);
const associations = await Promise.all(
domains.value.map(async (item) => {
@ -1096,6 +1105,14 @@ export function useDashboard() {
auditLogs,
settings,
settingsForm,
dnsProviderCatalog,
selectedDNSProvider,
dnsTesting,
dnsTestError,
dnsReadyToSave,
handleDNSProviderChange,
clearDNSCredentials,
testDNS,
domainServers,
loading,
saving,

View File

@ -55,6 +55,14 @@ const {
showSettings,
settingsSaving,
settingsError,
dnsProviderCatalog,
selectedDNSProvider,
dnsTesting,
dnsTestError,
dnsReadyToSave,
handleDNSProviderChange,
clearDNSCredentials,
testDNS,
sshKeyCommands,
sshPublicKeyCommands,
defaultCertificatePath,
@ -464,6 +472,120 @@ const {
<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>
<h3>DNS API 验证</h3>
<div class="settings-section-note">
<span>证书申请前会使用 DNS API 创建临时验证记录。</span>
<span>凭据只会在本次在线测试请求中提交,系统仅保存加密引用。</span>
</div>
<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>
<p v-if="selectedDNSProvider" class="field-hint dns-provider-description">
{{ selectedDNSProvider.description }}
</p>
<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">必填</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"
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>
<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>
<p v-if="!dnsReadyToSave" class="security-hint">
DNS 服务商发生变化或尚未配置时,必须先完成 DNS API 在线测试。
</p>
<h3>HTTP 通知</h3>
<div class="form-grid">
<label class="switch-field">
@ -503,7 +625,13 @@ const {
<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">{{ settingsSaving ? "保存中……" : "保存设置" }}</button>
<button
class="primary-button"
type="submit"
:disabled="settingsSaving || !dnsReadyToSave"
>
{{ settingsSaving ? "保存中……" : "保存设置" }}
</button>
</div>
</form>
</section>