From bd5f31946c59f090ce16bf66c379d8da563df4cd Mon Sep 17 00:00:00 2001 From: xuans Date: Sat, 3 Oct 2026 02:24:48 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E8=AF=81=E4=B9=A6=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E7=89=88=E6=9C=AC=E5=89=8D=E7=AB=AF=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 保存证书历史版本列表、恢复确认、幂等提交和任务详情跳转的前端实现,作为后续样式与测试优化基线。 --- frontend/src/views/Dashboard/Dashboard.js | 74 ++++++++++++++++++++++ frontend/src/views/Dashboard/Dashboard.vue | 60 ++++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/frontend/src/views/Dashboard/Dashboard.js b/frontend/src/views/Dashboard/Dashboard.js index ff9aac6..00f7fcd 100644 --- a/frontend/src/views/Dashboard/Dashboard.js +++ b/frontend/src/views/Dashboard/Dashboard.js @@ -65,6 +65,12 @@ export function useDashboard() { const importSaving = ref(false); const importError = ref(""); const importResult = ref(null); + const showCertificateVersions = ref(false); + const versionDomain = ref(null); + const certificateVersions = ref([]); + const versionsLoading = ref(false); + const versionsError = ref(""); + const restoreSubmitting = ref(""); let importIdempotencyKey = ""; const importFiles = ref({ certificate: null, @@ -92,6 +98,7 @@ export function useDashboard() { showServerForm.value || showSettings.value || showCertificateImport.value || + showCertificateVersions.value || deploymentResult.value !== null || showTaskDetails.value, ); @@ -847,6 +854,64 @@ export function useDashboard() { if (!importSaving.value) showCertificateImport.value = false; } + async function openCertificateVersions(item) { + versionDomain.value = item; + certificateVersions.value = []; + versionsError.value = ""; + versionsLoading.value = true; + showCertificateVersions.value = true; + try { + const response = await fetchAdmin( + `/api/v1/domains/${item.id}/certificate/versions`, + ); + const result = await response.json().catch(() => ({})); + if (!response.ok) throw new Error(result.error || "读取证书历史版本失败"); + certificateVersions.value = Array.isArray(result.items) ? result.items : []; + } catch (err) { + versionsError.value = err.message || "读取证书历史版本失败"; + } finally { + versionsLoading.value = false; + } + } + + function closeCertificateVersions() { + if (!restoreSubmitting.value) showCertificateVersions.value = false; + } + + async function restoreCertificateVersion(version) { + if (!version?.history_id || version.current || restoreSubmitting.value) return; + if (!window.confirm("确定恢复这个证书历史版本吗?恢复只更新 Scert 本地托管证书,不代表所有远端服务器已经部署。")) { + return; + } + const domain = versionDomain.value; + if (!domain) return; + const suffix = crypto.randomUUID ? crypto.randomUUID() : createRandomSuffix(); + restoreSubmitting.value = version.history_id; + versionsError.value = ""; + try { + const response = await fetchAdmin( + `/api/v1/domains/${domain.id}/certificate/versions/${encodeURIComponent(version.history_id)}/restore`, + { + method: "POST", + headers: { "Idempotency-Key": `certificate-restore-${domain.id}-${suffix}` }, + }, + ); + const result = await response.json().catch(() => ({})); + const replayed = response.status === 409 && result.replayed; + if (!(response.ok || replayed) || !result.task?.id) { + throw new Error(result.error || "提交证书历史版本恢复失败"); + } + notice.value = `证书历史版本恢复任务已提交:${result.task.id},恢复完成后请另行确认远端部署状态`; + showCertificateVersions.value = false; + await loadData(); + await openTaskDetails(result.task); + } catch (err) { + versionsError.value = err.message || "提交证书历史版本恢复失败"; + } finally { + restoreSubmitting.value = ""; + } + } + function selectCertificateImportFile(kind, event) { importFiles.value[kind] = event.target.files?.[0] || null; importError.value = ""; @@ -1127,6 +1192,15 @@ export function useDashboard() { closeCertificateImport, selectCertificateImportFile, importCertificate, + showCertificateVersions, + versionDomain, + certificateVersions, + versionsLoading, + versionsError, + restoreSubmitting, + openCertificateVersions, + closeCertificateVersions, + restoreCertificateVersion, saveServer, deleteServer, saveDomain, diff --git a/frontend/src/views/Dashboard/Dashboard.vue b/frontend/src/views/Dashboard/Dashboard.vue index 45110f5..8fbd614 100644 --- a/frontend/src/views/Dashboard/Dashboard.vue +++ b/frontend/src/views/Dashboard/Dashboard.vue @@ -74,6 +74,15 @@ const { closeCertificateImport, selectCertificateImportFile, importCertificate, + showCertificateVersions, + versionDomain, + certificateVersions, + versionsLoading, + versionsError, + restoreSubmitting, + openCertificateVersions, + closeCertificateVersions, + restoreCertificateVersion, settingsSaving, settingsError, dnsProviderCatalog, @@ -384,6 +393,9 @@ const { activeSection, activeLabel, sections, selectSection } = useDashboardNavi + + +

+ 这里仅恢复 Scert 本地托管证书。恢复完成不等于远端服务器已部署,请在任务完成后单独确认部署状态。 +

+
正在读取证书历史版本……
+ +
+ 暂无可显示的历史版本 + 证书成功保存后,历史版本会出现在这里。 +
+
+
+
+
+ {{ version.current ? "当前版本" : "历史版本" }} + 正在使用 +
+
+ 版本:{{ version.version || "未记录" }} + 保存时间:{{ formatDate(version.saved_at) }} + 有效期至:{{ formatDate(version.not_after) }} + 指纹:{{ version.fingerprint_sha256 }} +
+
+ + 当前版本不可重复恢复 +
+
+ + +