159 lines
5.0 KiB
Vue
159 lines
5.0 KiB
Vue
<script setup lang="ts">
|
|
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import { api, errorMessage } from '../api/client';
|
|
import type { Overview } from '../api/types';
|
|
import StatusBadge from '../components/StatusBadge.vue';
|
|
import { toast } from '../composables/toast';
|
|
import { formatDuration, formatRelative, TRIGGER_LABELS } from '../utils/format';
|
|
|
|
const router = useRouter();
|
|
const data = ref<Overview | null>(null);
|
|
let timer: number | undefined;
|
|
|
|
async function load() {
|
|
try {
|
|
data.value = await api.overview();
|
|
} catch (err) {
|
|
toast.error(errorMessage(err));
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
void load();
|
|
timer = window.setInterval(load, 5000);
|
|
});
|
|
onBeforeUnmount(() => window.clearInterval(timer));
|
|
</script>
|
|
|
|
<template>
|
|
<div class="page-header">
|
|
<div>
|
|
<h1>概览</h1>
|
|
<div class="subtitle">接口与任务的整体运行状态</div>
|
|
</div>
|
|
</div>
|
|
|
|
<template v-if="data">
|
|
<div class="stat-grid">
|
|
<div class="card stat-card">
|
|
<div class="label">接口数量</div>
|
|
<div class="value">{{ data.hooks.total }}</div>
|
|
<div class="sub">已启用 {{ data.hooks.enabled }} 个</div>
|
|
</div>
|
|
<div class="card stat-card">
|
|
<div class="label">运行中任务</div>
|
|
<div class="value" :style="{ color: data.running ? 'var(--primary)' : undefined }">{{ data.running }}</div>
|
|
<div class="sub">排队中 {{ data.queued }} 个</div>
|
|
</div>
|
|
<div class="card stat-card">
|
|
<div class="label">近 24 小时成功</div>
|
|
<div class="value" style="color: var(--success)">{{ data.last24h.success }}</div>
|
|
<div class="sub">共 {{ data.last24h.total }} 次执行</div>
|
|
</div>
|
|
<div class="card stat-card">
|
|
<div class="label">近 24 小时失败</div>
|
|
<div class="value" :style="{ color: data.last24h.failed ? 'var(--danger)' : undefined }">{{ data.last24h.failed }}</div>
|
|
<div class="sub">含超时、终止与中断</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overview-grid">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2>近期执行</h2>
|
|
<span class="spacer" />
|
|
<RouterLink :to="{ name: 'executions' }" class="small">查看全部</RouterLink>
|
|
</div>
|
|
<div class="card-body flush table-wrap">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>接口</th>
|
|
<th>来源</th>
|
|
<th>状态</th>
|
|
<th>耗时</th>
|
|
<th>时间</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="e in data.recent"
|
|
:key="e.id"
|
|
class="clickable"
|
|
@click="router.push({ name: 'execution-detail', params: { id: e.id } })"
|
|
>
|
|
<td class="mono">#{{ e.id }}</td>
|
|
<td>{{ e.hookName }}</td>
|
|
<td class="muted">{{ TRIGGER_LABELS[e.trigger] }}</td>
|
|
<td><StatusBadge :status="e.status" /></td>
|
|
<td class="nowrap">{{ formatDuration(e.durationMs) }}</td>
|
|
<td class="muted nowrap">{{ formatRelative(e.createdAt) }}</td>
|
|
</tr>
|
|
<tr v-if="!data.recent.length">
|
|
<td colspan="6" class="empty">暂无执行记录</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header"><h2>近期失败</h2></div>
|
|
<div class="card-body stack">
|
|
<RouterLink
|
|
v-for="e in data.recentFailures"
|
|
:key="e.id"
|
|
:to="{ name: 'execution-detail', params: { id: e.id } }"
|
|
class="failure-item"
|
|
>
|
|
<div class="row">
|
|
<strong>{{ e.hookName }}</strong>
|
|
<span class="spacer" />
|
|
<StatusBadge :status="e.status" />
|
|
</div>
|
|
<div class="small muted">
|
|
#{{ e.id }} · {{ formatRelative(e.createdAt) }}
|
|
<template v-if="e.exitCode !== null"> · 退出码 {{ e.exitCode }}</template>
|
|
</div>
|
|
<div v-if="e.error" class="small failure-error">{{ e.error }}</div>
|
|
</RouterLink>
|
|
<div v-if="!data.recentFailures.length" class="empty" style="padding: 20px">暂无失败记录</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.overview-grid {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
|
|
gap: 16px;
|
|
margin-top: 16px;
|
|
align-items: start;
|
|
}
|
|
.failure-item {
|
|
display: block;
|
|
color: inherit;
|
|
padding: 10px 12px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
}
|
|
.failure-item:hover {
|
|
border-color: var(--border-strong);
|
|
background: var(--surface-2);
|
|
text-decoration: none;
|
|
}
|
|
.failure-error {
|
|
color: var(--danger);
|
|
margin-top: 2px;
|
|
}
|
|
@media (max-width: 1000px) {
|
|
.overview-grid {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
}
|
|
}
|
|
</style>
|