227 lines
5.7 KiB
Vue
227 lines
5.7 KiB
Vue
<script setup lang="ts">
|
||
import { onMounted, reactive, ref } from "vue";
|
||
import apiClient, { getApiErrorMessage } from "@/api/client";
|
||
import { useViewport } from "@/composables/useViewport";
|
||
import type { SystemLog } from "@/types";
|
||
import { logLevelLabelMap } from "@/types";
|
||
|
||
const loading = ref(false);
|
||
const logs = ref<SystemLog[]>([]);
|
||
const loadError = ref("");
|
||
const { isMobile } = useViewport();
|
||
|
||
const filters = reactive({
|
||
liveRoomId: "",
|
||
level: undefined as number | undefined,
|
||
recordTaskId: "",
|
||
take: 200
|
||
});
|
||
|
||
const logLevelOptions = Object.entries(logLevelLabelMap).map(([value, label]) => ({
|
||
value: Number(value),
|
||
label
|
||
}));
|
||
|
||
async function loadLogs() {
|
||
loading.value = true;
|
||
loadError.value = "";
|
||
|
||
try {
|
||
const { data } = await apiClient.get<SystemLog[]>("/logs", {
|
||
params: {
|
||
liveRoomId: filters.liveRoomId || undefined,
|
||
level: typeof filters.level === "number" ? filters.level : undefined,
|
||
recordTaskId: filters.recordTaskId || undefined,
|
||
take: filters.take
|
||
}
|
||
});
|
||
|
||
logs.value = data;
|
||
} catch (error) {
|
||
loadError.value = getApiErrorMessage(error, "系统日志加载失败,请稍后重试。");
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function levelTagType(level: number) {
|
||
if (level === 3) {
|
||
return "danger";
|
||
}
|
||
|
||
if (level === 2) {
|
||
return "warning";
|
||
}
|
||
|
||
return "info";
|
||
}
|
||
|
||
function formatDate(value?: string) {
|
||
return value ? new Date(value).toLocaleString() : "-";
|
||
}
|
||
|
||
onMounted(loadLogs);
|
||
</script>
|
||
|
||
<template>
|
||
<div class="page-stack">
|
||
<div class="page-header">
|
||
<div>
|
||
<h1 class="page-title">系统日志</h1>
|
||
<p class="page-subtitle">
|
||
支持按直播间、任务和日志级别筛选,便于快速定位解析失败、巡检异常和 ffmpeg 输出问题。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<el-alert v-if="loadError" class="page-error-alert" type="error" :closable="false" show-icon :title="loadError" />
|
||
|
||
<el-card class="surface-card filter-card" shadow="never">
|
||
<h3 class="section-title">日志过滤</h3>
|
||
<p class="section-subtitle">默认拉取最近 200 条日志,可按直播间 ID、任务 ID 和级别缩小范围。</p>
|
||
|
||
<el-form class="filter-form">
|
||
<el-form-item label="直播间 ID">
|
||
<el-input v-model="filters.liveRoomId" placeholder="可选" />
|
||
</el-form-item>
|
||
<el-form-item label="日志级别">
|
||
<el-select v-model="filters.level" clearable placeholder="全部级别">
|
||
<el-option
|
||
v-for="option in logLevelOptions"
|
||
:key="option.value"
|
||
:label="option.label"
|
||
:value="option.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="任务 ID">
|
||
<el-input v-model="filters.recordTaskId" placeholder="可选" />
|
||
</el-form-item>
|
||
<el-form-item label="数量">
|
||
<el-input-number v-model="filters.take" :min="1" :max="500" />
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="loadLogs">查询</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
|
||
<el-card class="surface-card table-card" shadow="never">
|
||
<h3 class="section-title">日志列表</h3>
|
||
<p class="section-subtitle">按时间倒序排列,优先展示最近发生的事件。</p>
|
||
|
||
<div class="table-scroll-shell logs-table-shell">
|
||
<el-table
|
||
:data="logs"
|
||
v-loading="loading"
|
||
:height="isMobile ? undefined : 720"
|
||
class="premium-table logs-table"
|
||
table-layout="auto"
|
||
>
|
||
<el-table-column label="级别" width="100">
|
||
<template #default="{ row }">
|
||
<el-tag :type="levelTagType(row.level)">
|
||
{{ logLevelLabelMap[row.level] }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="分类" width="128" prop="category" />
|
||
<el-table-column label="消息" min-width="240" prop="message" />
|
||
<el-table-column label="详情" min-width="360">
|
||
<template #default="{ row }">
|
||
<span class="monospace log-detail">{{ row.detail || "-" }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="时间" width="180">
|
||
<template #default="{ row }">
|
||
<span class="log-date-text">{{ formatDate(row.createdAt) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.page-stack {
|
||
display: grid;
|
||
gap: 24px;
|
||
}
|
||
|
||
.page-error-alert {
|
||
border-radius: 14px;
|
||
}
|
||
|
||
.filter-card :deep(.el-card__body),
|
||
.table-card :deep(.el-card__body) {
|
||
padding-top: 20px;
|
||
}
|
||
|
||
.filter-form {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||
gap: 8px 12px;
|
||
align-items: end;
|
||
}
|
||
|
||
.filter-form :deep(.el-form-item:last-child) {
|
||
align-self: end;
|
||
}
|
||
|
||
.log-detail {
|
||
font-size: 12px;
|
||
color: #6d665b;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.log-date-text {
|
||
white-space: nowrap;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.logs-table-shell {
|
||
margin-inline: -4px;
|
||
padding-inline: 4px;
|
||
}
|
||
|
||
.logs-table {
|
||
min-width: 980px;
|
||
}
|
||
|
||
.logs-table :deep(.el-table__cell) {
|
||
vertical-align: top;
|
||
}
|
||
|
||
.logs-table :deep(.cell) {
|
||
overflow: visible;
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.filter-form {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
}
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.filter-form {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.filter-form :deep(.el-form-item:last-child) {
|
||
margin-left: 0;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.filter-form {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.filter-form :deep(.el-button) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
</style>
|