完善仪表盘任务与证书版本展示
This commit is contained in:
parent
db793b723f
commit
15f31040a2
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user