175 lines
5.2 KiB
Vue
175 lines
5.2 KiB
Vue
<template>
|
||
<div class="stack">
|
||
<div class="dashboard-toolbar">
|
||
<el-button plain :loading="loading" @click="loadOverview">刷新概览</el-button>
|
||
<el-switch v-model="autoRefresh" inline-prompt active-text="自动刷新" inactive-text="手动" />
|
||
<span class="updated-at">最近刷新:{{ lastUpdatedAt || "暂无" }}</span>
|
||
</div>
|
||
|
||
<el-alert
|
||
v-if="loadError"
|
||
title="读取概览失败,已保留上一次成功结果。"
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
/>
|
||
|
||
<div class="stats-grid">
|
||
<PageCard v-for="item in stats" :key="item.label">
|
||
<div class="stat-item">
|
||
<span>{{ item.label }}</span>
|
||
<strong>{{ item.value }}</strong>
|
||
<small>{{ item.note }}</small>
|
||
</div>
|
||
</PageCard>
|
||
</div>
|
||
|
||
<PageCard title="第一期目标" description="先把运营最需要的控制面搬到 Web,再逐步把检测执行和运维能力完全迁到 Linux。">
|
||
<el-timeline>
|
||
<el-timeline-item timestamp="当前阶段" placement="top">已创建 Web 后台、API、运行中心和导入/导出/筛选主链路。</el-timeline-item>
|
||
<el-timeline-item timestamp="下一步" placement="top">继续补 Linux Worker 控制闭环和更完整的运行诊断。</el-timeline-item>
|
||
<el-timeline-item timestamp="后续阶段" placement="top">把检测端逐步迁为 Linux Worker,前后端彻底分离。</el-timeline-item>
|
||
</el-timeline>
|
||
</PageCard>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||
import PageCard from "@/components/PageCard.vue";
|
||
import { dashboardApi } from "@/api/modules";
|
||
|
||
const loading = ref(false);
|
||
const loadError = ref(false);
|
||
const autoRefresh = ref(true);
|
||
const lastUpdatedAt = ref("");
|
||
let timer: number | null = null;
|
||
|
||
const stats = ref([
|
||
{ label: "总域名", value: "--", note: "数据库域名总量" },
|
||
{ label: "待检测", value: "--", note: "当前待处理域名" },
|
||
{ label: "黑名单", value: "--", note: "命中风险域名" },
|
||
{ label: "API", value: "离线", note: "当前 API 运行状态" },
|
||
{ label: "本机 Worker", value: "离线", note: "当前节点本机 Worker 运行状态" },
|
||
{ label: "有效执行节点", value: "离线", note: "当前集群可承担检测任务的在线节点" },
|
||
{ label: "模式", value: "--", note: "当前 Worker 运行模式" }
|
||
]);
|
||
|
||
const clearRefreshTimer = () => {
|
||
if (timer) {
|
||
window.clearInterval(timer);
|
||
timer = null;
|
||
}
|
||
};
|
||
|
||
const ensureRefreshTimer = () => {
|
||
clearRefreshTimer();
|
||
if (autoRefresh.value) {
|
||
timer = window.setInterval(() => {
|
||
loadOverview(false);
|
||
}, 10000);
|
||
}
|
||
};
|
||
|
||
const formatNow = () =>
|
||
new Date().toLocaleString("zh-CN", {
|
||
hour12: false
|
||
});
|
||
|
||
const loadOverview = async (showError = true) => {
|
||
loading.value = true;
|
||
try {
|
||
const { data } = await dashboardApi.overview();
|
||
const localWorkerValue =
|
||
data.local_worker_status === "not-applicable"
|
||
? "不承载"
|
||
: data.local_worker_status || data.worker_status;
|
||
const localWorkerNote =
|
||
data.local_worker_status === "not-applicable"
|
||
? `当前节点 ${data.node_region || "-"} / ${data.node_role || "-"},默认不承载本机 Worker`
|
||
: "当前节点本机 Worker 运行状态";
|
||
stats.value = [
|
||
{ label: "总域名", value: String(data.domains_total), note: "数据库域名总量" },
|
||
{ label: "待检测", value: String(data.pending_total), note: "当前待处理域名" },
|
||
{ label: "黑名单", value: String(data.blacklist_total), note: "命中风险域名" },
|
||
{ label: "API", value: data.api_status, note: "当前 API 运行状态" },
|
||
{ label: "本机 Worker", value: localWorkerValue, note: localWorkerNote },
|
||
{
|
||
label: "有效执行节点",
|
||
value: `${data.cluster_worker_status || "offline"} / ${data.cluster_online_worker_nodes || 0}`,
|
||
note: `可执行检测的在线节点,独立 Worker ${data.cluster_dedicated_online_worker_nodes || 0},控制面 ${data.cluster_online_control_nodes || 0}`
|
||
},
|
||
{ label: "模式", value: data.worker_mode, note: "当前 Worker 运行模式" }
|
||
];
|
||
loadError.value = false;
|
||
lastUpdatedAt.value = formatNow();
|
||
} catch {
|
||
loadError.value = true;
|
||
if (showError) {
|
||
lastUpdatedAt.value = lastUpdatedAt.value || "读取失败";
|
||
}
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
watch(autoRefresh, ensureRefreshTimer);
|
||
|
||
onMounted(async () => {
|
||
await loadOverview();
|
||
ensureRefreshTimer();
|
||
});
|
||
|
||
onBeforeUnmount(clearRefreshTimer);
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.stack {
|
||
display: grid;
|
||
gap: 20px;
|
||
}
|
||
|
||
.dashboard-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.updated-at {
|
||
color: #64748b;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 18px;
|
||
}
|
||
|
||
.stat-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.stat-item span {
|
||
color: #64748b;
|
||
}
|
||
|
||
.stat-item strong {
|
||
font-size: 32px;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.stat-item small {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
@media (max-width: 960px) {
|
||
.stats-grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
</style>
|