完善仪表盘任务与证书版本展示

This commit is contained in:
xuans 2026-08-30 22:29:04 +08:00
parent db793b723f
commit 15f31040a2
3 changed files with 378 additions and 3 deletions

View File

@ -891,4 +891,133 @@ code {
.settings-modal {
width: min(100%, 980px);
}
}
.page-shell {
max-width: 1380px;
}
.task-details-modal,
.certificate-versions-modal {
width: min(100%, 860px);
}
.task-detail-summary {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-bottom: 18px;
}
.task-detail-summary > div {
display: grid;
gap: 7px;
min-width: 0;
padding: 13px 14px;
border: 1px solid #e0e8f2;
border-radius: 12px;
background: #f8fbff;
}
.task-detail-summary span,
.task-detail-message > span {
color: #718198;
font-size: 12px;
font-weight: 700;
}
.task-detail-summary strong {
min-width: 0;
color: #243653;
font-size: 13px;
overflow-wrap: anywhere;
}
.task-detail-summary strong.tag {
width: fit-content;
color: inherit;
}
.task-detail-message {
margin-bottom: 20px;
padding: 14px 16px;
border: 1px solid #e0e8f2;
border-radius: 12px;
background: #fbfdff;
}
.task-detail-message p {
margin: 8px 0 0;
color: #3f506b;
line-height: 1.65;
overflow-wrap: anywhere;
}
.modal-section-title {
margin: 0 0 10px;
color: #32405a;
font-size: 15px;
}
.attempt-list,
.version-list {
display: grid;
gap: 9px;
}
.attempt-row,
.version-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
padding: 13px 14px;
border: 1px solid #e1e8f1;
border-radius: 12px;
background: #ffffff;
}
.attempt-row > div,
.version-row > div {
display: grid;
gap: 5px;
min-width: 0;
}
.attempt-row strong,
.version-row strong {
color: #2d4568;
font-size: 13px;
overflow-wrap: anywhere;
}
.attempt-error {
color: #b14e4e;
font-size: 12px;
line-height: 1.5;
overflow-wrap: anywhere;
}
.version-row .muted {
overflow-wrap: anywhere;
}
@media (max-width: 900px) {
.task-detail-summary {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.page-shell {
padding: 28px 16px 52px;
}
.task-detail-summary {
grid-template-columns: minmax(0, 1fr);
}
.attempt-row,
.version-row {
flex-direction: column;
}
}

View File

@ -45,6 +45,16 @@ export function useDashboard() {
private_key: null,
issuer_bundle: null,
});
const showTaskDetails = ref(false);
const taskDetails = ref(null);
const taskAttempts = ref([]);
const taskDetailsLoading = ref(false);
const taskDetailsError = ref("");
const showCertificateVersions = ref(false);
const certificateVersions = ref([]);
const certificateVersionsDomain = ref(null);
const certificateVersionsLoading = ref(false);
const certificateVersionsError = ref("");
const hasOpenModal = computed(
() =>
showForm.value ||
@ -100,6 +110,49 @@ export function useDashboard() {
failed: "失败",
cancelled: "已取消",
};
const taskTypeLabels = {
certificate_issue: "申请证书",
certificate_import: "导入证书",
certificate_deploy: "部署证书",
certificate_validate: "校验证书",
certificate_restore: "恢复证书版本",
renewal_run: "自动续期",
dns_test: "DNS 在线测试",
};
const auditActionLabels = {
certificate_issue: "申请证书",
certificate_import: "导入证书",
certificate_deploy: "部署证书",
certificate_validate: "校验证书",
certificate_version_restore: "恢复证书版本",
task_cancel: "取消任务",
settings_update: "更新系统设置",
domain_create: "创建域名",
domain_update: "更新域名",
domain_delete: "删除域名",
server_create: "创建服务器",
server_update: "更新服务器",
server_delete: "删除服务器",
ssh_key_create: "添加 SSH 私钥",
ssh_key_delete: "删除 SSH 私钥",
renewal_run: "执行自动续期",
};
const taskStageLabels = {
queued: "排队中",
certificate_issue: "申请证书",
certificate_validate: "校验证书",
certificate_deploy: "部署证书",
deployment: "远程部署",
verification: "部署后校验",
rollback: "回滚处理",
completed: "已完成",
};
const attemptStatusLabels = {
running: "执行中",
success: "已完成",
failed: "失败",
cancelled: "已取消",
};
const domainStatusLabels = {
pending: "等待申请",
active: "正常",
@ -159,6 +212,22 @@ export function useDashboard() {
return fallback;
}
function taskTypeLabel(value) {
return displayLabel(taskTypeLabels, value);
}
function auditActionLabel(value) {
return displayLabel(auditActionLabels, value);
}
function taskStageLabel(value) {
return displayLabel(taskStageLabels, value);
}
function attemptStatusLabel(value) {
return displayLabel(attemptStatusLabels, value);
}
function createEmptyForm() {
return {
name: "",
@ -523,6 +592,65 @@ export function useDashboard() {
return runDomainAction(item, "deploy", `已提交 ${item.name} 的证书部署`);
}
async function openTaskDetails(item) {
showTaskDetails.value = true;
taskDetails.value = null;
taskAttempts.value = [];
taskDetailsError.value = "";
taskDetailsLoading.value = true;
try {
const [taskResponse, attemptsResponse] = await Promise.all([
fetchAdmin(`/api/v1/tasks/${item.id}`),
fetchAdmin(`/api/v1/tasks/${item.id}/attempts`),
]);
if (!taskResponse.ok) {
throw new Error(await responseError(taskResponse, "读取任务详情失败"));
}
if (!attemptsResponse.ok) {
throw new Error(await responseError(attemptsResponse, "读取任务尝试记录失败"));
}
taskDetails.value = await taskResponse.json();
taskAttempts.value = (await attemptsResponse.json()).items || [];
} catch (err) {
taskDetailsError.value = err.message || "读取任务详情失败";
} finally {
taskDetailsLoading.value = false;
}
}
function closeTaskDetails() {
showTaskDetails.value = false;
taskDetails.value = null;
taskAttempts.value = [];
taskDetailsError.value = "";
}
async function openCertificateVersions(item) {
showCertificateVersions.value = true;
certificateVersionsDomain.value = item;
certificateVersions.value = [];
certificateVersionsError.value = "";
certificateVersionsLoading.value = true;
try {
const response = await fetchAdmin(`/api/v1/domains/${item.id}/certificate/versions`);
if (!response.ok) {
throw new Error(await responseError(response, "查询证书版本失败"));
}
certificateVersions.value = (await response.json()).items || [];
} catch (err) {
certificateVersionsError.value = err.message || "查询证书版本失败";
} finally {
certificateVersionsLoading.value = false;
}
}
function closeCertificateVersions() {
showCertificateVersions.value = false;
certificateVersions.value = [];
certificateVersionsDomain.value = null;
certificateVersionsError.value = "";
}
async function cancelTask(item) {
if (!window.confirm("确定取消这项正在执行的证书任务吗?")) return;
error.value = "";
@ -851,6 +979,10 @@ export function useDashboard() {
certificateTypeLabels,
domainStatusLabels,
taskStatusLabels,
taskTypeLabels,
auditActionLabels,
taskStageLabels,
attemptStatusLabels,
httpsModeLabels,
environmentLabels,
webServerLabels,
@ -904,6 +1036,24 @@ export function useDashboard() {
validateDomain,
deployDomain,
cancelTask,
openTaskDetails,
closeTaskDetails,
showTaskDetails,
taskDetails,
taskAttempts,
taskDetailsLoading,
taskDetailsError,
openCertificateVersions,
closeCertificateVersions,
showCertificateVersions,
certificateVersions,
certificateVersionsDomain,
certificateVersionsLoading,
certificateVersionsError,
taskTypeLabel,
auditActionLabel,
taskStageLabel,
attemptStatusLabel,
runRenewalNow,
openSettings,
closeSettings,

View File

@ -31,7 +31,12 @@ const {
certificateTypeLabels,
domainStatusLabels,
taskStatusLabels,
taskTypeLabel,
auditActionLabel,
taskStageLabel,
attemptStatusLabel,
httpsModeLabels,
dnsProviders,
environmentLabels,
webServerLabels,
isEditing,
@ -80,6 +85,20 @@ const {
deployDomain,
runRenewalNow,
cancelTask,
openTaskDetails,
closeTaskDetails,
showTaskDetails,
taskDetails,
taskAttempts,
taskDetailsLoading,
taskDetailsError,
openCertificateVersions,
closeCertificateVersions,
showCertificateVersions,
certificateVersions,
certificateVersionsDomain,
certificateVersionsLoading,
certificateVersionsError,
displayLabel,
formatDate,
daysRemaining,
@ -198,11 +217,14 @@ const {
<div v-else class="record-list">
<article v-for="item in tasks" :key="item.id" class="record-row">
<div>
<strong>{{ item.task_type }}</strong>
<strong>{{ taskTypeLabel(item.task_type) }}</strong>
<span class="muted">{{ item.message || "无附加信息" }}</span>
</div>
<div class="record-row-actions">
<span class="tag" :class="statusClass(item.status)">{{ displayLabel(taskStatusLabels, item.status) }}</span>
<button class="text-button" type="button" @click="openTaskDetails(item)">
查看详情
</button>
<button
v-if="item.status === 'queued' || item.status === 'running'"
class="text-button"
@ -221,10 +243,10 @@ const {
<div v-else class="record-list">
<article v-for="item in auditLogs" :key="item.id" class="record-row">
<div>
<strong>{{ item.action }}</strong>
<strong>{{ auditActionLabel(item.action) }}</strong>
<span class="muted">{{ item.message || item.resource_type }}</span>
</div>
<span class="tag" :class="item.status === 'success' ? 'success' : 'danger'">{{ item.status }}</span>
<span class="tag" :class="item.status === 'success' ? 'success' : 'danger'">{{ item.status === "success" ? "成功" : "失败" }}</span>
</article>
</div>
</div>
@ -293,6 +315,9 @@ const {
<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
@ -1026,7 +1051,78 @@ const {
</div>
</form>
</section>
<div v-if="showTaskDetails" class="modal-backdrop" @click.self="closeTaskDetails">
<section class="modal-card task-details-modal" role="dialog" aria-modal="true" aria-label="任务详情">
<div class="modal-heading">
<div>
<p class="eyebrow">任务中心</p>
<h2>任务详情</h2>
<p v-if="taskDetails" class="muted">{{ taskTypeLabel(taskDetails.task_type) }} · {{ taskDetails.id }}</p>
</div>
<button class="close-button" type="button" aria-label="关闭" @click="closeTaskDetails">×</button>
</div>
<div v-if="taskDetailsLoading" class="empty-state">正在读取任务详情……</div>
<p v-else-if="taskDetailsError" class="form-error">{{ taskDetailsError }}</p>
<template v-else-if="taskDetails">
<div class="task-detail-summary">
<div><span>当前状态</span><strong class="tag" :class="statusClass(taskDetails.status)">{{ displayLabel(taskStatusLabels, taskDetails.status) }}</strong></div>
<div><span>执行阶段</span><strong>{{ taskStageLabel(taskDetails.stage) }}</strong></div>
<div><span>创建时间</span><strong>{{ formatDate(taskDetails.created_at) }}</strong></div>
<div><span>完成时间</span><strong>{{ taskDetails.finished_at ? formatDate(taskDetails.finished_at) : "未完成" }}</strong></div>
</div>
<div class="task-detail-message">
<span>任务信息</span>
<p>{{ taskDetails.message || "无附加信息" }}</p>
</div>
<h3 class="modal-section-title">执行尝试</h3>
<div v-if="taskAttempts.length === 0" class="empty-state compact-empty">暂无执行尝试记录</div>
<div v-else class="attempt-list">
<article v-for="attempt in taskAttempts" :key="attempt.id" class="attempt-row">
<div>
<strong>第 {{ attempt.attempt_number }} 次尝试 · {{ taskStageLabel(attempt.stage) }}</strong>
<span class="muted">{{ attempt.message || "无附加信息" }}</span>
<span v-if="attempt.error" class="attempt-error">{{ attempt.error }}</span>
</div>
<span class="tag" :class="statusClass(attempt.status)">{{ attemptStatusLabel(attempt.status) }}</span>
</article>
</div>
</template>
<div class="modal-actions">
<button class="secondary-button" type="button" @click="closeTaskDetails">关闭</button>
</div>
</section>
</div>
<div v-if="showCertificateVersions" class="modal-backdrop" @click.self="closeCertificateVersions">
<section class="modal-card certificate-versions-modal" role="dialog" aria-modal="true" aria-label="证书版本">
<div class="modal-heading">
<div>
<p class="eyebrow">证书管理</p>
<h2>当前证书版本</h2>
<p class="muted">{{ certificateVersionsDomain?.name }}</p>
</div>
<button class="close-button" type="button" aria-label="关闭" @click="closeCertificateVersions">×</button>
</div>
<div v-if="certificateVersionsLoading" class="empty-state">正在读取证书版本……</div>
<p v-else-if="certificateVersionsError" class="form-error">{{ certificateVersionsError }}</p>
<div v-else-if="certificateVersions.length === 0" class="empty-state">当前域名还没有已保存的证书版本。</div>
<div v-else class="version-list">
<article v-for="version in certificateVersions" :key="version.version" class="version-row">
<div>
<strong>{{ version.version }}</strong>
<span class="muted">保存时间:{{ formatDate(version.saved_at || version.created_at) }}</span>
<span class="muted">证书目录:{{ version.directory }}</span>
</div>
<span class="tag success">当前使用</span>
</article>
</div>
<p class="security-hint">当前版本策略只保留正在使用的版本;新证书申请成功后会生成新的日期与 Vx.x 目录。</p>
<div class="modal-actions">
<button class="secondary-button" type="button" @click="closeCertificateVersions">关闭</button>
</div>
</section>
</div> </div>
</main>
</template>
<style scoped src="./Dashboard.css"></style>