diff --git a/apps/web/src/pages/admin.tsx b/apps/web/src/pages/admin.tsx index 0205d65..01fafb8 100644 --- a/apps/web/src/pages/admin.tsx +++ b/apps/web/src/pages/admin.tsx @@ -1128,7 +1128,7 @@ function DomainDNSDialog({ domain }: { domain: Domain }) { - + {domain.name} DNS @@ -2590,27 +2590,77 @@ function DNSPanel({ domain, embedded = false }: { domain?: Domain; embedded?: bo const check = useMutation({ mutationFn: () => api.checkDns(domain!.id), onSuccess: (res) => { qc.invalidateQueries({ queryKey: ["admin", "domains"] }); toast({ title: res.status === "ok" ? "DNS 检测通过" : "DNS 检测未通过", description: Object.values(res.checks).map((c) => c.message).join(";") }) }, onError: (error) => toast({ title: "DNS 检测失败", description: error.message }) }) if (!domain) return 请选择域名 const content = <> -

以下为需要在域名 DNS 管理中添加的记录:

- {records.isError ? { void records.refetch() }} compact /> :
{records.data?.items.map((r) => )}
} - {check.data && <> - -
检测结果
-
{Object.entries(check.data.checks).map(([k, v]) => )}
- } - const checkButton = canCheckDNS ? : null +

以下内容可直接填写到常见 DNS 控制台,根域名的主机记录使用 @。

+ {check.isPending && } + {check.isError && } + {check.data && !check.isPending && } + {records.isError ? { void records.refetch() }} compact /> :
{records.data?.items.map((r) => )}
} + + const checkButton = canCheckDNS ? : null const header =
DNS 记录{checkButton}
- if (embedded) return
DNS 记录
{checkButton}
{content}
+ if (embedded) return
DNS 记录
{checkButton}
{content}
return {header}{content} } +const dnsCheckMeta: Record = { + mx: { label: "MX", description: "收信地址" }, + spf: { label: "SPF", description: "发信授权" }, + dkim: { label: "DKIM", description: "邮件签名" }, + dmarc: { label: "DMARC", description: "防伪策略" }, +} + +function DNSCheckPending() { + return
+
+
正在检测 DNS

正在查询最新解析结果,请稍候...

+
+} + +function DNSCheckFailure({ error }: { error: Error }) { + return
+
+
DNS 检测失败

{error.message || "暂时无法查询 DNS,请稍后重新检测。"}

+
+} + +function DNSCheckSummary({ checks }: { checks: Record }) { + const entries = Object.entries(checks).sort(([left], [right]) => { + const order = ["mx", "spf", "dkim", "dmarc"] + return order.indexOf(left) - order.indexOf(right) + }) + const passed = entries.filter(([, item]) => item.ok).length + const allPassed = entries.length > 0 && passed === entries.length + return
+
+
+ {allPassed ? : } +
+
+
+

{allPassed ? "DNS 配置全部通过" : "DNS 配置需要处理"}

+ {passed}/{entries.length} 项通过 +
+

{allPassed ? "所有邮件相关记录均已正确解析,可以正常使用。" : "请处理下面标红的项目,修改 DNS 后再重新检测。"}

+
+
+
+ {entries.map(([name, item]) => )} +
+
+} + function DNSCheckRow({ name, check }: { name: string; check: { ok: boolean; message: string; found?: string[] } }) { const visibleRecords = check.found?.filter(Boolean) ?? [] - return
-
- -
{name.toUpperCase()}: {check.message}
+ const meta = dnsCheckMeta[name.toLowerCase()] || { label: name.toUpperCase(), description: "DNS 记录" } + return
+
+ {check.ok ? : } +
+
{meta.label}({meta.description})
+
{check.message}
+
- {!check.ok && visibleRecords.length > 0 &&
+ {!check.ok && visibleRecords.length > 0 &&
当前解析
{visibleRecords.map((record, index) =>
{record}
)}
} @@ -2621,36 +2671,61 @@ function dnsDescription(record: DNSRecord): string { if (record.type === "TXT" && record.name.startsWith("_dmarc")) return "声明域名的 DMARC 策略(如何处理未通过 SPF/DKIM 验证的邮件)。" if (record.type === "TXT" && record.value.includes("DKIM1")) return "DKIM 公钥。收件服务器用此密钥验证邮件是否由你发出。" if (record.type === "TXT" && record.value.includes("spf1")) return "声明哪些服务器有权使用你的域名发件,防止伪造。" - if (record.type === "MX") return `确保 ${record.name} 的 A 记录已指向你的服务器 IP,邮件才能到达。` + if (record.type === "MX") { + const mx = mxRecordParts(record.value) + return `邮件由 ${mx.target} 接收,请确保它的 A 记录指向服务器 IP。优先级 ${mx.priority},数值越小越优先。` + } return "" } -function DNSRecordRow({ record }: { record: DNSRecord }) { +function mxRecordParts(value: string) { + const [rawPriority = "10", ...rawTarget] = value.trim().split(/\s+/) + const priority = /^\d+$/.test(rawPriority) ? rawPriority : "10" + const target = (rawTarget.length > 0 ? rawTarget.join(" ") : value).replace(/\.$/, "") + return { priority, target } +} + +function dnsHostForProvider(recordName: string, domainName: string) { + const name = recordName.replace(/\.$/, "") + const domain = domainName.replace(/\.$/, "") + if (name.toLowerCase() === domain.toLowerCase()) return "@" + const suffix = `.${domain}` + if (name.toLowerCase().endsWith(suffix.toLowerCase())) return name.slice(0, -suffix.length) + return name +} + +function DNSRecordRow({ record, domainName }: { record: DNSRecord; domainName: string }) { const { toast } = useToast() const desc = dnsDescription(record) + const longValue = record.value.length > 180 + const mx = record.type === "MX" ? mxRecordParts(record.value) : null + const displayHost = dnsHostForProvider(record.name, domainName) + const displayValue = mx?.target || record.value + const [valueExpanded, setValueExpanded] = React.useState(false) async function copyField(label: string, value: string) { await navigator.clipboard.writeText(value) toast({ title: `${label}已复制` }) } - return
-
+ return
+
{record.type} + {longValue && }
- {desc &&

{desc}

} -
+ {desc &&

{desc}

} +
主机记录 - {record.name} - + {displayHost} +
记录值 - {record.value} - + {displayValue} +
-
- TTL - {record.ttl} 秒 +
+ {mx &&
优先级{mx.priority}
} +
TTL{record.ttl} 秒