feat: initial commit for HookPilot

This commit is contained in:
吴璨
2026-09-27 20:09:10 +08:00
commit e998d4defc
73 changed files with 10782 additions and 0 deletions
+158
View File
@@ -0,0 +1,158 @@
<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>