完善 DNS 验证等待提示
This commit is contained in:
parent
90ab0abb5e
commit
ddbf3b2d29
@ -55,15 +55,16 @@ func TestAdminDNSTestReturnsVerificationResult(t *testing.T) {
|
||||
t.Fatalf("DNS 在线测试失败:%d %s", response.Code, response.Body.String())
|
||||
}
|
||||
var payload struct {
|
||||
Verified bool `json:"verified"`
|
||||
CredentialRef string `json:"credential_ref"`
|
||||
VerificationToken string `json:"verification_token"`
|
||||
TestReport setup.DNSVerificationReport `json:"test_report"`
|
||||
Verified bool `json:"verified"`
|
||||
CredentialRef string `json:"credential_ref"`
|
||||
VerificationToken string `json:"verification_token"`
|
||||
TestReport setup.DNSVerificationReport `json:"test_report"`
|
||||
VerificationTimeoutSecs int `json:"verification_timeout_secs"`
|
||||
}
|
||||
if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("解析 DNS 在线测试响应失败:%v", err)
|
||||
}
|
||||
if !payload.Verified || payload.CredentialRef == "" || payload.VerificationToken == "" || payload.TestReport.RecordName == "" {
|
||||
if !payload.Verified || payload.CredentialRef == "" || payload.VerificationToken == "" || payload.TestReport.RecordName == "" || payload.VerificationTimeoutSecs <= 0 {
|
||||
t.Fatalf("DNS 在线测试响应内容不正确:%+v", payload)
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,6 +157,7 @@ func (s *Server) testAdminDNS(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (s *Server) testDNS(c *gin.Context) {
|
||||
verificationTimeoutSecs := int(s.dnsVerificationTimeout().Seconds())
|
||||
if s.setupDNS == nil {
|
||||
s.writeError(c, http.StatusServiceUnavailable, "DNS 在线验证服务尚未初始化", nil)
|
||||
return
|
||||
@ -173,7 +174,10 @@ func (s *Server) testDNS(c *gin.Context) {
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, dnsprovider.ErrProviderUnavailable) {
|
||||
s.writeError(c, http.StatusNotImplemented, "该 DNS 服务商在线验证适配器尚未开放", nil)
|
||||
c.JSON(http.StatusNotImplemented, gin.H{
|
||||
"error": "该 DNS 服务商在线验证适配器尚未开放",
|
||||
"verification_timeout_secs": verificationTimeoutSecs,
|
||||
})
|
||||
return
|
||||
}
|
||||
report := result.TestReport
|
||||
@ -184,16 +188,21 @@ func (s *Server) testDNS(c *gin.Context) {
|
||||
if s.logger != nil {
|
||||
s.logger.Error("DNS API 在线验证失败", "provider", request.ProviderID, "test_domain", request.TestDomain)
|
||||
}
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "DNS API 在线验证失败,请检查凭据、测试域名和 DNS 权限", "test_report": report})
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{
|
||||
"error": "DNS API 在线验证失败,请检查凭据、测试域名和 DNS 权限",
|
||||
"test_report": report,
|
||||
"verification_timeout_secs": verificationTimeoutSecs,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"verified": true,
|
||||
"credential_ref": result.CredentialRef,
|
||||
"verification_token": result.VerificationToken,
|
||||
"expires_at": result.ExpiresAt,
|
||||
"test_report": result.TestReport,
|
||||
"message": "DNS API 在线验证通过,凭据已加密保存。",
|
||||
"verified": true,
|
||||
"credential_ref": result.CredentialRef,
|
||||
"verification_token": result.VerificationToken,
|
||||
"expires_at": result.ExpiresAt,
|
||||
"test_report": result.TestReport,
|
||||
"verification_timeout_secs": verificationTimeoutSecs,
|
||||
"message": "DNS API 在线验证通过,凭据已加密保存。",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -239,6 +239,9 @@ func TestSetupDNSProviderCatalogAndOnlineTest(t *testing.T) {
|
||||
!strings.Contains(testResponse.Body.String(), "清理记录") {
|
||||
t.Fatalf("DNS 在线测试响应应包含阶段报告:%s", testResponse.Body.String())
|
||||
}
|
||||
if !strings.Contains(testResponse.Body.String(), `"verification_timeout_secs":930`) {
|
||||
t.Fatalf("DNS 在线测试响应应包含当前最大等待时间:%s", testResponse.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupDNSTestUsesConfiguredRequestTimeout(t *testing.T) {
|
||||
@ -262,6 +265,9 @@ func TestSetupDNSTestUsesConfiguredRequestTimeout(t *testing.T) {
|
||||
if response.Code != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("模拟 DNS 验证失败应返回 422,实际为 %d:%s", response.Code, response.Body.String())
|
||||
}
|
||||
if !strings.Contains(response.Body.String(), `"verification_timeout_secs":31`) {
|
||||
t.Fatalf("DNS 验证失败响应应包含当前最大等待时间:%s", response.Body.String())
|
||||
}
|
||||
if !verifier.verifyHasDeadline {
|
||||
t.Fatal("DNS 在线测试应收到带截止时间的上下文")
|
||||
}
|
||||
|
||||
@ -28,9 +28,22 @@ export function verifiedResponse(token = "test-token") {
|
||||
|
||||
// 运行真实组合式逻辑和生命周期,不加载浏览器,也不连接 DNS 或业务数据库。
|
||||
export async function mountComposable(t, factory, fetcher) {
|
||||
const intervals = new Map();
|
||||
let nextIntervalID = 1;
|
||||
const originals = new Map();
|
||||
for (const [key, value] of Object.entries({
|
||||
window: { setInterval: () => 1, clearInterval() {}, location: { assign() {} } },
|
||||
window: {
|
||||
setInterval(callback) {
|
||||
const intervalID = nextIntervalID;
|
||||
nextIntervalID += 1;
|
||||
intervals.set(intervalID, callback);
|
||||
return intervalID;
|
||||
},
|
||||
clearInterval(intervalID) {
|
||||
intervals.delete(intervalID);
|
||||
},
|
||||
location: { assign() {} },
|
||||
},
|
||||
document: { body: { style: { overflow: "" } } },
|
||||
})) {
|
||||
originals.set(key, Object.getOwnPropertyDescriptor(globalThis, key));
|
||||
@ -66,5 +79,11 @@ export async function mountComposable(t, factory, fetcher) {
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
await nextTick();
|
||||
return { state, unmount };
|
||||
return {
|
||||
state,
|
||||
unmount,
|
||||
runIntervals() {
|
||||
for (const callback of [...intervals.values()]) callback();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -1091,6 +1091,28 @@ code {
|
||||
background: #e7f8ee;
|
||||
}
|
||||
|
||||
.dns-verification-progress {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin: -4px 0 16px;
|
||||
padding: 12px 13px;
|
||||
border: 1px solid #cbdcf2;
|
||||
border-radius: 11px;
|
||||
color: #2d5b99;
|
||||
background: #f0f6ff;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.dns-verification-progress strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dns-verification-progress small {
|
||||
color: #6d7b91;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dns-test-report {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
|
||||
const defaultDNSVerificationTimeoutSeconds = 45;
|
||||
const dnsVerificationCleanupBufferSeconds = 30;
|
||||
|
||||
export function useDashboard() {
|
||||
const scertLogo = "/Scert.png";
|
||||
|
||||
@ -37,8 +40,11 @@ export function useDashboard() {
|
||||
const settingsError = ref("");
|
||||
const dnsTesting = ref(false);
|
||||
const dnsTestError = ref("");
|
||||
const dnsVerificationElapsedSeconds = ref(0);
|
||||
const dnsVerificationTimeoutSeconds = ref(defaultDNSVerificationTimeoutSeconds);
|
||||
const verificationClock = ref(Date.now());
|
||||
let verificationTimer;
|
||||
let dnsVerificationTimer;
|
||||
let dnsRequestVersion = 0;
|
||||
const settingsForm = ref(createEmptySettingsForm());
|
||||
const showCertificateImport = ref(false);
|
||||
@ -212,6 +218,47 @@ export function useDashboard() {
|
||||
new Date(dns.expires_at).getTime() > Date.now();
|
||||
});
|
||||
|
||||
function resolveDNSVerificationTimeoutSeconds(value) {
|
||||
const propagationTimeout = Number(value);
|
||||
if (!Number.isFinite(propagationTimeout) || propagationTimeout <= 0) {
|
||||
return defaultDNSVerificationTimeoutSeconds;
|
||||
}
|
||||
return Math.floor(propagationTimeout) + dnsVerificationCleanupBufferSeconds;
|
||||
}
|
||||
|
||||
function updateDNSVerificationTimeoutSeconds(value) {
|
||||
const timeout = Number(value);
|
||||
if (Number.isFinite(timeout) && timeout > 0) {
|
||||
dnsVerificationTimeoutSeconds.value = Math.floor(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
function stopDNSVerificationTimer(requestVersion) {
|
||||
if (requestVersion !== undefined && requestVersion !== dnsRequestVersion) return;
|
||||
if (dnsVerificationTimer) {
|
||||
window.clearInterval(dnsVerificationTimer);
|
||||
dnsVerificationTimer = undefined;
|
||||
}
|
||||
dnsVerificationElapsedSeconds.value = 0;
|
||||
}
|
||||
|
||||
function startDNSVerificationTimer(requestVersion) {
|
||||
stopDNSVerificationTimer();
|
||||
dnsVerificationTimeoutSeconds.value = resolveDNSVerificationTimeoutSeconds(
|
||||
settingsForm.value.acme.propagation_timeout_secs,
|
||||
);
|
||||
const startedAt = Date.now();
|
||||
const updateElapsedSeconds = () => {
|
||||
if (requestVersion !== dnsRequestVersion) return;
|
||||
dnsVerificationElapsedSeconds.value = Math.max(
|
||||
0,
|
||||
Math.floor((Date.now() - startedAt) / 1000),
|
||||
);
|
||||
};
|
||||
updateElapsedSeconds();
|
||||
dnsVerificationTimer = window.setInterval(updateElapsedSeconds, 1000);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [settingsForm.value.dns.provider, settingsForm.value.dns.test_domain, settingsForm.value.dns.credentials],
|
||||
handleDNSProviderChange,
|
||||
@ -713,6 +760,7 @@ export function useDashboard() {
|
||||
|
||||
function handleDNSProviderChange() {
|
||||
dnsRequestVersion += 1;
|
||||
stopDNSVerificationTimer();
|
||||
dnsTesting.value = false;
|
||||
const dns = settingsForm.value.dns;
|
||||
const saved = settings.value?.dns;
|
||||
@ -761,9 +809,10 @@ export function useDashboard() {
|
||||
dnsTestError.value = `请填写${missingField.label}`;
|
||||
return;
|
||||
}
|
||||
const requestVersion = dnsRequestVersion;
|
||||
dnsTesting.value = true;
|
||||
dns.verification_state = "pending";
|
||||
const requestVersion = dnsRequestVersion;
|
||||
startDNSVerificationTimer(requestVersion);
|
||||
try {
|
||||
const response = await fetchAdmin("/api/v1/dns/test", {
|
||||
method: "POST",
|
||||
@ -776,6 +825,7 @@ export function useDashboard() {
|
||||
});
|
||||
const result = await response.json().catch(() => ({}));
|
||||
if (requestVersion !== dnsRequestVersion) return;
|
||||
updateDNSVerificationTimeoutSeconds(result.verification_timeout_secs);
|
||||
dns.test_report = result.test_report || null;
|
||||
if (!response.ok) {
|
||||
throw new Error(result.error || "DNS API 在线测试失败");
|
||||
@ -793,7 +843,10 @@ export function useDashboard() {
|
||||
dns.verification_state = "pending";
|
||||
dnsTestError.value = err.message || "DNS API 在线测试失败";
|
||||
} finally {
|
||||
if (requestVersion === dnsRequestVersion) dnsTesting.value = false;
|
||||
if (requestVersion === dnsRequestVersion) {
|
||||
stopDNSVerificationTimer(requestVersion);
|
||||
dnsTesting.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1100,6 +1153,7 @@ export function useDashboard() {
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
dnsRequestVersion += 1;
|
||||
stopDNSVerificationTimer();
|
||||
if (verificationTimer) window.clearInterval(verificationTimer);
|
||||
document.body.style.overflow = previousBodyOverflow;
|
||||
});
|
||||
@ -1119,6 +1173,8 @@ export function useDashboard() {
|
||||
selectedDNSProvider,
|
||||
dnsTesting,
|
||||
dnsTestError,
|
||||
dnsVerificationElapsedSeconds,
|
||||
dnsVerificationTimeoutSeconds,
|
||||
dnsReadyToSave,
|
||||
handleDNSProviderChange,
|
||||
clearDNSCredentials,
|
||||
|
||||
@ -60,6 +60,8 @@ const {
|
||||
selectedDNSProvider,
|
||||
dnsTesting,
|
||||
dnsTestError,
|
||||
dnsVerificationElapsedSeconds,
|
||||
dnsVerificationTimeoutSeconds,
|
||||
dnsReadyToSave,
|
||||
handleDNSProviderChange,
|
||||
clearDNSCredentials,
|
||||
@ -570,6 +572,21 @@ const {
|
||||
{{ 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>
|
||||
系统正在创建临时 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"
|
||||
|
||||
@ -431,6 +431,7 @@ label > small {
|
||||
|
||||
.provider-description,
|
||||
.dns-verification-success,
|
||||
.dns-verification-progress,
|
||||
.dns-verification-report {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
@ -452,6 +453,21 @@ label > small {
|
||||
background: #fff8e8;
|
||||
}
|
||||
|
||||
.dns-verification-progress {
|
||||
border: 1px solid #cbdcf2;
|
||||
color: #2d5b99;
|
||||
background: #f0f6ff;
|
||||
}
|
||||
|
||||
.dns-verification-progress strong {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.dns-verification-progress small {
|
||||
color: #53647b;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.provider-description strong,
|
||||
.dns-verification-success strong {
|
||||
font-size: 11px;
|
||||
@ -831,6 +847,7 @@ button:disabled {
|
||||
|
||||
.provider-description,
|
||||
.dns-verification-success,
|
||||
.dns-verification-progress,
|
||||
.dns-test-actions {
|
||||
margin-top: 6px;
|
||||
}
|
||||
@ -984,13 +1001,15 @@ button:disabled {
|
||||
}
|
||||
|
||||
.provider-description,
|
||||
.dns-verification-success {
|
||||
.dns-verification-success,
|
||||
.dns-verification-progress {
|
||||
padding: 6px 8px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.provider-description span,
|
||||
.dns-verification-success span,
|
||||
.dns-verification-progress small,
|
||||
.dns-test-actions > span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
|
||||
const defaultDNSVerificationTimeoutSeconds = 45;
|
||||
const dnsVerificationCleanupBufferSeconds = 30;
|
||||
|
||||
const commonEmailDomains = [
|
||||
{ label: "常用邮箱", value: "" },
|
||||
{ label: "@163.com", value: "163.com" },
|
||||
@ -25,6 +28,8 @@ export function useSetupWizard(props, emit) {
|
||||
const dnsCatalogError = ref("");
|
||||
const dnsTesting = ref(false);
|
||||
const dnsTestError = ref("");
|
||||
const dnsVerificationElapsedSeconds = ref(0);
|
||||
const dnsVerificationTimeoutSeconds = ref(defaultDNSVerificationTimeoutSeconds);
|
||||
const dnsProviders = ref([]);
|
||||
const dnsCredentials = ref({});
|
||||
const dnsCredentialsVisible = ref(true);
|
||||
@ -41,6 +46,7 @@ export function useSetupWizard(props, emit) {
|
||||
const emailAutofillElement = ref(null);
|
||||
let emailVisibleEdited = false;
|
||||
let verificationTimer;
|
||||
let dnsVerificationTimer;
|
||||
let dnsRequestVersion = 0;
|
||||
let emailAutofillTimer;
|
||||
const targetType = ref(props.status.web_public_ip ? "ip" : "hostname");
|
||||
@ -143,11 +149,53 @@ export function useSetupWizard(props, emit) {
|
||||
|
||||
function invalidateDNSVerification() {
|
||||
dnsRequestVersion += 1;
|
||||
stopDNSVerificationTimer();
|
||||
dnsTesting.value = false;
|
||||
dnsVerification.value = null;
|
||||
dnsTestError.value = "";
|
||||
}
|
||||
|
||||
function resolveDNSVerificationTimeoutSeconds(value) {
|
||||
const propagationTimeout = Number(value);
|
||||
if (!Number.isFinite(propagationTimeout) || propagationTimeout <= 0) {
|
||||
return defaultDNSVerificationTimeoutSeconds;
|
||||
}
|
||||
return Math.floor(propagationTimeout) + dnsVerificationCleanupBufferSeconds;
|
||||
}
|
||||
|
||||
function updateDNSVerificationTimeoutSeconds(value) {
|
||||
const timeout = Number(value);
|
||||
if (Number.isFinite(timeout) && timeout > 0) {
|
||||
dnsVerificationTimeoutSeconds.value = Math.floor(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
function stopDNSVerificationTimer(requestVersion) {
|
||||
if (requestVersion !== undefined && requestVersion !== dnsRequestVersion) return;
|
||||
if (dnsVerificationTimer) {
|
||||
window.clearInterval(dnsVerificationTimer);
|
||||
dnsVerificationTimer = undefined;
|
||||
}
|
||||
dnsVerificationElapsedSeconds.value = 0;
|
||||
}
|
||||
|
||||
function startDNSVerificationTimer(requestVersion) {
|
||||
stopDNSVerificationTimer();
|
||||
dnsVerificationTimeoutSeconds.value = resolveDNSVerificationTimeoutSeconds(
|
||||
form.value.propagation_timeout_secs,
|
||||
);
|
||||
const startedAt = Date.now();
|
||||
const updateElapsedSeconds = () => {
|
||||
if (requestVersion !== dnsRequestVersion) return;
|
||||
dnsVerificationElapsedSeconds.value = Math.max(
|
||||
0,
|
||||
Math.floor((Date.now() - startedAt) / 1000),
|
||||
);
|
||||
};
|
||||
updateElapsedSeconds();
|
||||
dnsVerificationTimer = window.setInterval(updateElapsedSeconds, 1000);
|
||||
}
|
||||
|
||||
function formatVerificationExpiry(value) {
|
||||
if (!value) return "";
|
||||
return new Intl.DateTimeFormat("zh-CN", {
|
||||
@ -417,8 +465,9 @@ export function useSetupWizard(props, emit) {
|
||||
}
|
||||
}
|
||||
|
||||
dnsTesting.value = true;
|
||||
const requestVersion = dnsRequestVersion;
|
||||
dnsTesting.value = true;
|
||||
startDNSVerificationTimer(requestVersion);
|
||||
try {
|
||||
const response = await fetch("/api/v1/setup/dns/test", {
|
||||
method: "POST",
|
||||
@ -431,6 +480,7 @@ export function useSetupWizard(props, emit) {
|
||||
});
|
||||
const result = await response.json().catch(() => ({}));
|
||||
if (requestVersion !== dnsRequestVersion) return;
|
||||
updateDNSVerificationTimeoutSeconds(result.verification_timeout_secs);
|
||||
if (result.test_report) {
|
||||
dnsVerification.value = { test_report: result.test_report };
|
||||
}
|
||||
@ -440,7 +490,10 @@ export function useSetupWizard(props, emit) {
|
||||
if (requestVersion !== dnsRequestVersion) return;
|
||||
dnsTestError.value = err.message || "DNS API 在线验证失败";
|
||||
} finally {
|
||||
if (requestVersion === dnsRequestVersion) dnsTesting.value = false;
|
||||
if (requestVersion === dnsRequestVersion) {
|
||||
stopDNSVerificationTimer(requestVersion);
|
||||
dnsTesting.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -810,6 +863,8 @@ export function useSetupWizard(props, emit) {
|
||||
dnsCatalogError,
|
||||
dnsTesting,
|
||||
dnsTestError,
|
||||
dnsVerificationElapsedSeconds,
|
||||
dnsVerificationTimeoutSeconds,
|
||||
dnsProviders,
|
||||
dnsCredentials,
|
||||
dnsCredentialsVisible,
|
||||
|
||||
@ -20,6 +20,8 @@ const {
|
||||
dnsCatalogError,
|
||||
dnsTesting,
|
||||
dnsTestError,
|
||||
dnsVerificationElapsedSeconds,
|
||||
dnsVerificationTimeoutSeconds,
|
||||
dnsProviders,
|
||||
dnsCredentials,
|
||||
dnsCredentialsVisible,
|
||||
@ -797,6 +799,21 @@ const {
|
||||
记录并自动清理,明文凭据不会写入普通配置文件。</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="dnsTesting"
|
||||
class="dns-verification-progress"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<strong>正在进行 DNS 验证,请等待</strong>
|
||||
<span>
|
||||
已等待 {{ dnsVerificationElapsedSeconds }} 秒,最长等待
|
||||
{{ dnsVerificationTimeoutSeconds }} 秒。
|
||||
</span>
|
||||
<small>
|
||||
系统正在创建临时 TXT、查询验证记录并自动清理。这是 DNS API 权限验证;实际申请证书时,仍可能需要等待公网 DNS-01 记录传播。
|
||||
</small>
|
||||
</div>
|
||||
<p v-if="dnsTestError" class="inline-error">{{ dnsTestError }}</p>
|
||||
<div v-if="dnsVerificationValid" class="dns-verification-success">
|
||||
<strong>DNS API 在线验证通过</strong>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user