feat: 新增异常日志收集功能,mediabot 可提交运行时异常,管理端可查看

This commit is contained in:
mediabot-pt
2026-07-01 00:14:07 +08:00
parent 3972f00e25
commit a45650f480
12 changed files with 439 additions and 1 deletions

View File

@@ -81,5 +81,10 @@ export default {
approve: (configId) => api.post(`/admin/reviews/${configId}/approve`),
reject: (configId, comment) => api.post(`/admin/reviews/${configId}/reject`, null, { params: { comment: comment || '' } }),
},
errorLogs: {
list: () => api.get('/admin/error-logs'),
bySite: (siteId) => api.get(`/admin/error-logs/by-site/${siteId}`),
get: (id) => api.get(`/admin/error-logs/${id}`),
},
health: () => api.get('/health', { baseURL: '' })
}

View File

@@ -52,6 +52,12 @@ const routes = [
component: () => import('../views/ApiLogs.vue'),
meta: { title: '访问日志' }
},
{
path: 'error-logs',
name: 'ErrorLogs',
component: () => import('../views/ErrorLogs.vue'),
meta: { title: '异常日志' }
},
{
path: 'access-results',
name: 'AccessResults',

133
web/src/views/ErrorLogs.vue Normal file
View File

@@ -0,0 +1,133 @@
<template>
<div>
<h2>异常日志</h2>
<el-card style="margin-top: 20px">
<div style="margin-bottom: 12px; display: flex; gap: 10px; align-items: center">
<el-select v-model="filterSiteId" placeholder="按站点筛选" clearable style="width: 200px">
<el-option v-for="s in siteOptions" :key="s" :label="s" :value="s" />
</el-select>
<el-select v-model="filterErrorType" placeholder="按异常类型筛选" clearable style="width: 220px">
<el-option v-for="t in errorTypeOptions" :key="t" :label="t" :value="t" />
</el-select>
<el-button @click="fetchAll" :icon="Refresh">刷新</el-button>
</div>
<el-table :data="filteredList" v-loading="loading" stripe size="small" empty-text="暂无异常日志">
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="siteId" label="站点" width="110">
<template #default="scope">
<span class="text-xs">{{ scope.row.siteId || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="errorType" label="异常类型" width="180">
<template #default="scope">
<el-tag size="small" type="danger">{{ scope.row.errorType }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="message" label="异常消息" min-width="250" show-overflow-tooltip>
<template #default="scope">
<span class="text-xs">{{ scope.row.message || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="configVersion" label="版本" width="80">
<template #default="scope">
<span class="text-xs">{{ scope.row.configVersion || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="url" label="URL" min-width="180" show-overflow-tooltip>
<template #default="scope">
<span class="text-xs" style="word-break:break-all">{{ scope.row.url || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="clientId" label="来源" width="100" show-overflow-tooltip />
<el-table-column prop="createdAt" label="时间" width="160" />
<el-table-column label="操作" width="80" fixed="right">
<template #default="scope">
<el-button size="small" @click="handleView(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 详情弹窗 -->
<el-dialog v-model="detailVisible" title="异常详情" width="850px" :close-on-click-modal="false">
<el-descriptions :column="2" border size="small" style="margin-bottom: 12px">
<el-descriptions-item label="站点" :span="1">{{ detail.siteId || '-' }}</el-descriptions-item>
<el-descriptions-item label="来源" :span="1">{{ detail.clientId || '-' }}</el-descriptions-item>
<el-descriptions-item label="异常类型" :span="2">
<el-tag size="small" type="danger">{{ detail.errorType }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="异常消息" :span="2">{{ detail.message || '-' }}</el-descriptions-item>
<el-descriptions-item label="配置版本">{{ detail.configVersion || '-' }}</el-descriptions-item>
<el-descriptions-item label="URL" :span="1">
<span style="word-break:break-all">{{ detail.url || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="时间" :span="2">{{ detail.createdAt }}</el-descriptions-item>
</el-descriptions>
<div v-if="detail.stackTrace" style="margin-top: 10px">
<div style="font-size:13px;font-weight:500;color:#606266;margin-bottom:4px">堆栈信息:</div>
<pre class="stack-trace">{{ detail.stackTrace }}</pre>
</div>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import api from '../api'
import { Refresh } from '@element-plus/icons-vue'
const loading = ref(false)
const list = ref([])
const filterSiteId = ref('')
const filterErrorType = ref('')
const detailVisible = ref(false)
const detail = ref({})
const siteOptions = computed(() => [...new Set(list.value.map(r => r.siteId).filter(Boolean))].sort())
const errorTypeOptions = computed(() => [...new Set(list.value.map(r => r.errorType).filter(Boolean))].sort())
const filteredList = computed(() => {
let items = list.value
if (filterSiteId.value) items = items.filter(r => r.siteId === filterSiteId.value)
if (filterErrorType.value) items = items.filter(r => r.errorType === filterErrorType.value)
return items
})
const fetchAll = async () => {
loading.value = true
try {
const r = await api.errorLogs.list()
list.value = r.data || []
} catch { ElMessage.error('获取异常日志失败') }
finally { loading.value = false }
}
const handleView = (row) => {
detail.value = { ...row }
detailVisible.value = true
}
onMounted(fetchAll)
</script>
<style scoped>
.stack-trace {
background: #1e1e1e;
color: #d4d4d4;
padding: 12px;
border-radius: 4px;
font-size: 11px;
line-height: 1.5;
margin: 0;
white-space: pre-wrap;
word-break: break-all;
max-height: 450px;
overflow: auto;
}
</style>

View File

@@ -38,6 +38,10 @@
<el-icon><Document /></el-icon>
<span>访问日志</span>
</el-menu-item>
<el-menu-item index="/error-logs">
<el-icon><WarningFilled /></el-icon>
<span>异常日志</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-container>