mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
feat(admin): add detailed send log viewer with extended fields
This commit is contained in:
parent
3a6a2cbbc5
commit
dcbe93be91
14 changed files with 460 additions and 42 deletions
|
|
@ -911,6 +911,98 @@ main {
|
|||
padding: 56px 0;
|
||||
}
|
||||
|
||||
.log-entry.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.log-status {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
border-radius: 999px;
|
||||
padding: 2px 9px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.log-status.ok {
|
||||
color: var(--ok);
|
||||
background: var(--ok-dim);
|
||||
}
|
||||
|
||||
.log-status.bad {
|
||||
color: var(--bad);
|
||||
background: var(--bad-dim);
|
||||
}
|
||||
|
||||
.log-detail {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 22px;
|
||||
max-width: 720px;
|
||||
width: calc(100vw - 48px);
|
||||
max-height: 82vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 12px 40px rgba(15, 23, 42, 0.25);
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 60;
|
||||
background: var(--scrim);
|
||||
backdrop-filter: blur(2px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.detail-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.detail-head h3 {
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 30%) 1fr;
|
||||
gap: 6px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.detail-grid dt {
|
||||
color: var(--faint);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.6px;
|
||||
padding-top: 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.detail-grid dd {
|
||||
margin: 0;
|
||||
padding-top: 6px;
|
||||
color: var(--text);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.detail-grid code {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
background: var(--surface-2);
|
||||
padding: 2px 7px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.detail-grid dd code {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.filter-row {
|
||||
grid-template-columns: 100px 1fr auto;
|
||||
|
|
|
|||
|
|
@ -11,11 +11,18 @@
|
|||
<p v-else-if="!loading && !logs.length" class="empty-log">{{ t("logs.empty") }}</p>
|
||||
|
||||
<div class="log-list">
|
||||
<article v-for="l in logs" :key="l.ts + l.routeId" class="log-entry" :class="{ fail: !l.ok }">
|
||||
<article
|
||||
v-for="l in logs"
|
||||
:key="l.id ?? l.ts + l.routeId"
|
||||
class="log-entry"
|
||||
:class="{ fail: !l.ok, clickable: true }"
|
||||
@click="openDetail(l)"
|
||||
>
|
||||
<div class="log-head">
|
||||
<span class="dot" :class="l.ok ? 'ok' : 'bad'"></span>
|
||||
<span class="log-route">{{ l.routeId }}</span>
|
||||
<span class="log-event">{{ l.event }}</span>
|
||||
<span v-if="l.status" class="log-status" :class="l.ok ? 'ok' : 'bad'">{{ l.status }}</span>
|
||||
<span class="log-time">{{ fmtTime(l.ts) }}</span>
|
||||
</div>
|
||||
<div class="log-meta">
|
||||
|
|
@ -31,6 +38,30 @@
|
|||
<div v-if="!l.ok && l.error" class="log-error">{{ l.error }}</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-if="detailOpen" class="modal-overlay" @click.self="closeDetail">
|
||||
<div class="log-detail">
|
||||
<div class="detail-head">
|
||||
<h3>{{ t("logs.detailTitle") }}</h3>
|
||||
<button class="icon-btn" @click="closeDetail">✕</button>
|
||||
</div>
|
||||
|
||||
<p v-if="detailLoading" class="empty-log">{{ t("status.loading") }}</p>
|
||||
<p v-else-if="detailError" class="err">{{ detailError }}</p>
|
||||
|
||||
<dl v-else-if="detail" class="detail-grid">
|
||||
<template v-for="row in detailRows" :key="row.label">
|
||||
<dt>{{ row.label }}</dt>
|
||||
<dd>
|
||||
<code v-if="row.code">{{ row.value }}</code>
|
||||
<span v-else>{{ row.value }}</span>
|
||||
</dd>
|
||||
</template>
|
||||
</dl>
|
||||
|
||||
<p v-else class="err">{{ t("logs.detailMissing") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -38,10 +69,48 @@
|
|||
import type { SendRecord } from "~/types";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { loadById } = useSendLogs();
|
||||
|
||||
const props = defineProps<{ logs: SendRecord[]; loading: boolean }>();
|
||||
const emit = defineEmits<{ (e: "refresh"): void }>();
|
||||
|
||||
const detailOpen = ref(false);
|
||||
const detail = ref<SendRecord | null>(null);
|
||||
const detailLoading = ref(false);
|
||||
const detailError = ref("");
|
||||
|
||||
const detailRows = computed(() => {
|
||||
const l = detail.value;
|
||||
if (!l) return [] as Array<{ label: string; value: string; code?: boolean }>;
|
||||
const rows: Array<{ label: string; value: string; code?: boolean }> = [
|
||||
{ label: t("logs.id"), value: String(l.id ?? "-"), code: true },
|
||||
{ label: t("logs.time"), value: fmtTime(l.ts) },
|
||||
{ label: t("logs.route"), value: l.routeId, code: true },
|
||||
{ label: t("logs.event"), value: l.event, code: true },
|
||||
{ label: t("logs.action"), value: l.action || "-", code: true },
|
||||
{ label: t("logs.actor"), value: l.actor || "-", code: true },
|
||||
{ label: t("logs.deliveryId"), value: l.deliveryId || "-", code: true },
|
||||
{ label: t("logs.repo"), value: l.repo || "-", code: true },
|
||||
{ label: t("logs.target"), value: l.target, code: true },
|
||||
{ label: t("logs.platform"), value: l.platform || "-", code: true },
|
||||
{ label: t("logs.status"), value: l.status != null ? String(l.status) : "-", code: true },
|
||||
{ label: t("logs.messageId"), value: l.messageId || "-", code: true },
|
||||
{ label: t("logs.errorCode"), value: l.errorCode || "-", code: true },
|
||||
{ label: t("logs.duration"), value: l.durationMs != null ? `${l.durationMs} ms` : "-" },
|
||||
{ label: t("logs.attempts"), value: l.attempts != null ? String(l.attempts) : "-" },
|
||||
{ label: t("logs.ok"), value: l.ok ? t("logs.yes") : t("logs.no") },
|
||||
{ label: t("logs.error"), value: l.error || "-" },
|
||||
];
|
||||
if (l.detail && Object.keys(l.detail).length > 0) {
|
||||
rows.push({
|
||||
label: t("logs.detail"),
|
||||
value: JSON.stringify(l.detail, null, 2),
|
||||
code: true,
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
});
|
||||
|
||||
function fmtTime(ts: number): string {
|
||||
return new Date(ts).toLocaleString();
|
||||
}
|
||||
|
|
@ -49,4 +118,24 @@ function fmtTime(ts: number): string {
|
|||
function refresh(): void {
|
||||
emit("refresh");
|
||||
}
|
||||
|
||||
async function openDetail(l: SendRecord): Promise<void> {
|
||||
if (l.id == null) return;
|
||||
detailOpen.value = true;
|
||||
detail.value = null;
|
||||
detailError.value = "";
|
||||
detailLoading.value = true;
|
||||
try {
|
||||
detail.value = await loadById(l.id);
|
||||
} catch (err) {
|
||||
detailError.value = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
detailLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function closeDetail(): void {
|
||||
detailOpen.value = false;
|
||||
detail.value = null;
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -98,6 +98,26 @@ const en: Dict = {
|
|||
"logs.empty": "No send records yet — trigger a webhook to see results.",
|
||||
"logs.repo": "REPO",
|
||||
"logs.target": "TARGET",
|
||||
"logs.detailTitle": "Send Detail",
|
||||
"logs.detailMissing": "Could not load this entry.",
|
||||
"logs.id": "ID",
|
||||
"logs.time": "Time",
|
||||
"logs.route": "Route",
|
||||
"logs.event": "Event",
|
||||
"logs.action": "Action",
|
||||
"logs.actor": "Actor",
|
||||
"logs.deliveryId": "Delivery ID",
|
||||
"logs.platform": "Platform",
|
||||
"logs.status": "HTTP Status",
|
||||
"logs.messageId": "Message ID",
|
||||
"logs.errorCode": "Error Code",
|
||||
"logs.duration": "Duration",
|
||||
"logs.attempts": "Attempts",
|
||||
"logs.ok": "Result",
|
||||
"logs.yes": "OK",
|
||||
"logs.no": "Failed",
|
||||
"logs.error": "Error",
|
||||
"logs.detail": "Extra Info",
|
||||
"toast.routesSaved": "Routes saved",
|
||||
"toast.routeDeleted": "Route deleted",
|
||||
"toast.groupSaved": "Groups saved",
|
||||
|
|
@ -205,6 +225,26 @@ const zh: Dict = {
|
|||
"logs.empty": "还没有发送记录 —— 触发一次 webhook 即可查看结果。",
|
||||
"logs.repo": "仓库",
|
||||
"logs.target": "目标",
|
||||
"logs.detailTitle": "发送详情",
|
||||
"logs.detailMissing": "无法加载该条记录。",
|
||||
"logs.id": "ID",
|
||||
"logs.time": "时间",
|
||||
"logs.route": "路由",
|
||||
"logs.event": "事件",
|
||||
"logs.action": "动作",
|
||||
"logs.actor": "操作者",
|
||||
"logs.deliveryId": "Delivery ID",
|
||||
"logs.platform": "平台",
|
||||
"logs.status": "HTTP 状态码",
|
||||
"logs.messageId": "消息 ID",
|
||||
"logs.errorCode": "错误码",
|
||||
"logs.duration": "耗时",
|
||||
"logs.attempts": "尝试次数",
|
||||
"logs.ok": "结果",
|
||||
"logs.yes": "成功",
|
||||
"logs.no": "失败",
|
||||
"logs.error": "错误信息",
|
||||
"logs.detail": "附加信息",
|
||||
"toast.routesSaved": "路由已保存",
|
||||
"toast.routeDeleted": "路由已删除",
|
||||
"toast.groupSaved": "分组已保存",
|
||||
|
|
|
|||
|
|
@ -29,5 +29,19 @@ export function useSendLogs() {
|
|||
}
|
||||
}
|
||||
|
||||
return { logs, loading, needLogin, error, load };
|
||||
async function loadById(id: number): Promise<SendRecord | null> {
|
||||
const res = await fetch(`/admin/api/logs/${id}`, {
|
||||
headers: { accept: "application/json" },
|
||||
credentials: "same-origin",
|
||||
});
|
||||
if (res.status === 401) {
|
||||
needLogin.value = true;
|
||||
return null;
|
||||
}
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = (await res.json()) as { log?: SendRecord };
|
||||
return data.log ?? null;
|
||||
}
|
||||
|
||||
return { logs, loading, needLogin, error, load, loadById };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export function fmtMatch(match: string | string[]): string {
|
|||
}
|
||||
|
||||
export interface SendRecord {
|
||||
id?: number;
|
||||
ts: number;
|
||||
routeId: string;
|
||||
event: string;
|
||||
|
|
@ -57,4 +58,14 @@ export interface SendRecord {
|
|||
target: string;
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
status?: number;
|
||||
messageId?: string;
|
||||
deliveryId?: string;
|
||||
platform?: string;
|
||||
actor?: string;
|
||||
action?: string;
|
||||
durationMs?: number;
|
||||
errorCode?: string;
|
||||
attempts?: number;
|
||||
detail?: Record<string, unknown>;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue