补充证书历史版本前端恢复入口
保存证书历史版本列表、恢复确认、幂等提交和任务详情跳转的前端实现,作为后续样式与测试优化基线。
This commit is contained in:
parent
3aa1d50251
commit
bd5f31946c
@ -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,
|
||||
|
||||
@ -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
|
||||
<button class="text-button" type="button" @click="openCertificateImport(item)">
|
||||
导入证书
|
||||
</button>
|
||||
<button class="text-button" type="button" @click="openCertificateVersions(item)">
|
||||
历史版本
|
||||
</button>
|
||||
<button class="text-button" type="button" @click="openEdit(item)">
|
||||
编辑</button
|
||||
><button
|
||||
@ -495,6 +507,54 @@ const { activeSection, activeLabel, sections, selectSection } = useDashboardNavi
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div v-if="showCertificateVersions" class="modal-backdrop" @click.self="closeCertificateVersions">
|
||||
<section class="modal-card certificate-history-modal" role="dialog" aria-modal="true" aria-label="证书历史版本">
|
||||
<div class="modal-heading">
|
||||
<div>
|
||||
<p class="eyebrow">证书管理</p>
|
||||
<h2>证书历史版本</h2>
|
||||
<p class="muted">托管域名:{{ versionDomain?.name }}</p>
|
||||
</div>
|
||||
<button class="close-button" type="button" aria-label="关闭" @click="closeCertificateVersions">×</button>
|
||||
</div>
|
||||
<p class="security-hint">
|
||||
这里仅恢复 Scert 本地托管证书。恢复完成不等于远端服务器已部署,请在任务完成后单独确认部署状态。
|
||||
</p>
|
||||
<div v-if="versionsLoading" class="empty-state">正在读取证书历史版本……</div>
|
||||
<p v-else-if="versionsError" class="form-error" role="alert">{{ versionsError }}</p>
|
||||
<div v-else-if="certificateVersions.length === 0" class="empty-state compact-empty">
|
||||
<strong>暂无可显示的历史版本</strong>
|
||||
<span>证书成功保存后,历史版本会出现在这里。</span>
|
||||
</div>
|
||||
<div v-else class="certificate-history-list">
|
||||
<article v-for="version in certificateVersions" :key="version.history_id || version.version" class="certificate-history-row">
|
||||
<div>
|
||||
<div class="domain-title-line">
|
||||
<strong>{{ version.current ? "当前版本" : "历史版本" }}</strong>
|
||||
<span v-if="version.current" class="tag success">正在使用</span>
|
||||
</div>
|
||||
<div class="domain-meta">
|
||||
<span>版本:{{ version.version || "未记录" }}</span>
|
||||
<span>保存时间:{{ formatDate(version.saved_at) }}</span>
|
||||
<span>有效期至:{{ formatDate(version.not_after) }}</span>
|
||||
<span v-if="version.fingerprint_sha256">指纹:{{ version.fingerprint_sha256 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="!version.current"
|
||||
class="secondary-button"
|
||||
type="button"
|
||||
:disabled="!!restoreSubmitting"
|
||||
@click="restoreCertificateVersion(version)"
|
||||
>
|
||||
{{ restoreSubmitting === version.history_id ? "提交中……" : "恢复此版本" }}
|
||||
</button>
|
||||
<span v-else class="muted">当前版本不可重复恢复</span>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<DomainModal v-if="showForm" :state="dashboard" />
|
||||
|
||||
<ServerModal v-if="showServerForm" :state="dashboard" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user