2026-06-25 21:34:23 +08:00
|
|
|
<script setup lang="tsx">
|
|
|
|
|
import { computed, reactive, ref } from 'vue';
|
|
|
|
|
import { INFRA_API_ERROR_LOG_PROCESS_STATUS_DICT_CODE } from '@/constants/dict';
|
|
|
|
|
import { fetchExportApiErrorLog, fetchGetApiErrorLog, fetchGetApiErrorLogPage } from '@/service/api';
|
|
|
|
|
import { useAuth } from '@/hooks/business/auth';
|
|
|
|
|
import { useUIPaginatedTable } from '@/hooks/common/table';
|
|
|
|
|
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
|
|
|
|
import DictText from '@/components/custom/dict-text.vue';
|
|
|
|
|
import LogDetailDialog from '../shared/log-detail-dialog.vue';
|
|
|
|
|
import { type LogDetailSection, LogPermission, downloadBlob, formatDateTime, getLogExportFileName } from '../shared';
|
|
|
|
|
import ApiErrorLogSearch from './modules/search.vue';
|
|
|
|
|
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
|
|
|
|
|
|
|
|
|
defineOptions({ name: 'ApiErrorLogTab' });
|
|
|
|
|
|
|
|
|
|
type ApiErrorLogPageResponse = Awaited<ReturnType<typeof fetchGetApiErrorLogPage>>;
|
|
|
|
|
|
|
|
|
|
function createSearchParams(): Api.SystemLog.ApiError.SearchParams {
|
|
|
|
|
return {
|
|
|
|
|
pageNo: 1,
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
userId: undefined,
|
|
|
|
|
userType: undefined,
|
|
|
|
|
applicationName: undefined,
|
|
|
|
|
requestUrl: undefined,
|
|
|
|
|
exceptionTime: undefined,
|
|
|
|
|
processStatus: undefined
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function transformPageResult(response: ApiErrorLogPageResponse, pageNo: number, pageSize: number) {
|
|
|
|
|
if (!response.error && response.data) {
|
|
|
|
|
return {
|
|
|
|
|
data: response.data.list,
|
|
|
|
|
pageNum: pageNo,
|
|
|
|
|
pageSize,
|
|
|
|
|
total: response.data.total
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
data: [],
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
pageSize,
|
|
|
|
|
total: 0
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { hasAuth } = useAuth();
|
|
|
|
|
const searchParams = reactive(createSearchParams());
|
|
|
|
|
const detailVisible = ref(false);
|
|
|
|
|
const currentRow = ref<Api.SystemLog.ApiError.Log | null>(null);
|
|
|
|
|
const exporting = ref(false);
|
|
|
|
|
const canExport = computed(() => hasAuth(LogPermission.ApiErrorExport));
|
|
|
|
|
|
|
|
|
|
const detailSections: LogDetailSection[] = [
|
|
|
|
|
{
|
|
|
|
|
title: '异常信息',
|
|
|
|
|
fields: [
|
|
|
|
|
{ label: '编号', key: 'id' },
|
|
|
|
|
{ label: '链路追踪编号', key: 'traceId' },
|
|
|
|
|
{ label: '应用名', key: 'applicationName' },
|
|
|
|
|
{ label: '请求方式', key: 'requestMethod' },
|
|
|
|
|
{ label: '请求地址', key: 'requestUrl', span: 2 },
|
|
|
|
|
{ label: '异常时间', key: 'exceptionTime', type: 'datetime' },
|
|
|
|
|
{ label: '异常名', key: 'exceptionName' },
|
|
|
|
|
{ label: '异常消息', key: 'exceptionMessage', type: 'multiline' },
|
|
|
|
|
{ label: '根因消息', key: 'exceptionRootCauseMessage', type: 'multiline' }
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '上下文',
|
|
|
|
|
fields: [
|
2026-07-01 17:15:12 +08:00
|
|
|
{ label: '用户姓名', key: 'userNickname' },
|
2026-06-25 21:34:23 +08:00
|
|
|
{ label: '用户编号', key: 'userId' },
|
|
|
|
|
{ label: '用户IP', key: 'userIp' },
|
|
|
|
|
{ label: '浏览器UA', key: 'userAgent', type: 'multiline' },
|
|
|
|
|
{ label: '请求参数', key: 'requestParams', type: 'multiline' }
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '堆栈与处理',
|
|
|
|
|
fields: [
|
|
|
|
|
{ label: '异常类名', key: 'exceptionClassName' },
|
|
|
|
|
{ label: '异常文件', key: 'exceptionFileName' },
|
|
|
|
|
{ label: '异常方法', key: 'exceptionMethodName' },
|
|
|
|
|
{ label: '异常行号', key: 'exceptionLineNumber' },
|
|
|
|
|
{ label: '处理状态', key: 'processStatus', type: 'dict', dictCode: INFRA_API_ERROR_LOG_PROCESS_STATUS_DICT_CODE },
|
|
|
|
|
{ label: '处理时间', key: 'processTime', type: 'datetime' },
|
|
|
|
|
{ label: '处理用户编号', key: 'processUserId', span: 2 },
|
|
|
|
|
{ label: '异常栈轨迹', key: 'exceptionStackTrace', type: 'multiline' }
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
|
|
|
|
ApiErrorLogPageResponse,
|
|
|
|
|
Api.SystemLog.ApiError.Log
|
|
|
|
|
>({
|
|
|
|
|
paginationProps: {
|
|
|
|
|
currentPage: searchParams.pageNo,
|
|
|
|
|
pageSize: searchParams.pageSize
|
|
|
|
|
},
|
|
|
|
|
api: () => fetchGetApiErrorLogPage(searchParams),
|
|
|
|
|
transform: response => transformPageResult(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
|
|
|
|
onPaginationParamsChange: params => {
|
|
|
|
|
searchParams.pageNo = params.currentPage ?? 1;
|
|
|
|
|
searchParams.pageSize = params.pageSize ?? 10;
|
|
|
|
|
},
|
|
|
|
|
columns: () => [
|
|
|
|
|
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
|
|
|
|
{ prop: 'applicationName', label: '应用名', minWidth: 140, showOverflowTooltip: true },
|
2026-07-01 17:15:12 +08:00
|
|
|
{
|
|
|
|
|
prop: 'userNickname',
|
|
|
|
|
label: '用户姓名',
|
|
|
|
|
minWidth: 120,
|
|
|
|
|
showOverflowTooltip: true,
|
|
|
|
|
formatter: row => row.userNickname || '--'
|
|
|
|
|
},
|
2026-06-25 21:34:23 +08:00
|
|
|
{ prop: 'requestMethod', label: '请求方式', width: 100, align: 'center' },
|
|
|
|
|
{ prop: 'requestUrl', label: '请求地址', minWidth: 220, showOverflowTooltip: true },
|
|
|
|
|
{ prop: 'exceptionName', label: '异常名', minWidth: 180, showOverflowTooltip: true },
|
|
|
|
|
{
|
|
|
|
|
prop: 'processStatus',
|
|
|
|
|
label: '处理状态',
|
|
|
|
|
minWidth: 120,
|
|
|
|
|
formatter: row => <DictText dictCode={INFRA_API_ERROR_LOG_PROCESS_STATUS_DICT_CODE} value={row.processStatus} />
|
|
|
|
|
},
|
|
|
|
|
{ prop: 'exceptionTime', label: '异常时间', minWidth: 180, formatter: row => formatDateTime(row.exceptionTime) },
|
|
|
|
|
{
|
|
|
|
|
prop: 'operate',
|
|
|
|
|
label: '操作',
|
|
|
|
|
width: 90,
|
|
|
|
|
align: 'center',
|
|
|
|
|
fixed: 'right',
|
|
|
|
|
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function getRowActions(row: Api.SystemLog.ApiError.Log): BusinessTableAction[] {
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
key: 'view',
|
|
|
|
|
label: '查看',
|
|
|
|
|
buttonType: 'primary',
|
|
|
|
|
icon: IconMdiEyeOutline,
|
|
|
|
|
onClick: () => openDetail(row)
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openDetail(row: Api.SystemLog.ApiError.Log) {
|
|
|
|
|
currentRow.value = row;
|
|
|
|
|
detailVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function reloadTable(page = searchParams.pageNo) {
|
|
|
|
|
await getDataByPage(page);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetSearchParams() {
|
|
|
|
|
Object.assign(searchParams, createSearchParams());
|
|
|
|
|
reloadTable(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleSearch() {
|
|
|
|
|
reloadTable(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleExport() {
|
|
|
|
|
exporting.value = true;
|
|
|
|
|
const { error, data: blob } = await fetchExportApiErrorLog(searchParams);
|
|
|
|
|
exporting.value = false;
|
|
|
|
|
|
|
|
|
|
if (error || !blob) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
downloadBlob(blob, getLogExportFileName('API错误日志'));
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-07-01 17:15:12 +08:00
|
|
|
<div class="h-full min-h-0 flex-col-stretch gap-16px overflow-hidden">
|
2026-06-25 21:34:23 +08:00
|
|
|
<ApiErrorLogSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
|
|
|
|
|
|
|
|
|
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="flex items-center justify-between gap-12px">
|
|
|
|
|
<div class="flex items-center gap-10px">
|
|
|
|
|
<p>API错误日志列表</p>
|
|
|
|
|
<ElTag effect="plain">{{ mobilePagination.total || data.length }}</ElTag>
|
|
|
|
|
</div>
|
|
|
|
|
<TableHeaderOperation
|
|
|
|
|
v-model:columns="columnChecks"
|
|
|
|
|
:disabled-delete="true"
|
|
|
|
|
:loading="loading"
|
|
|
|
|
@refresh="getData"
|
|
|
|
|
>
|
|
|
|
|
<template #default>
|
|
|
|
|
<ElButton v-if="canExport" plain type="primary" :loading="exporting" @click="handleExport">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<icon-mdi-download class="text-icon" />
|
|
|
|
|
</template>
|
|
|
|
|
导出
|
|
|
|
|
</ElButton>
|
|
|
|
|
</template>
|
|
|
|
|
</TableHeaderOperation>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<div class="flex-1">
|
|
|
|
|
<ElTable v-loading="loading" height="100%" border row-key="id" :data="data">
|
|
|
|
|
<ElTableColumn v-for="col in columns" :key="String(col.prop)" v-bind="col" />
|
|
|
|
|
</ElTable>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="mt-20px flex justify-end">
|
|
|
|
|
<ElPagination
|
|
|
|
|
v-if="mobilePagination.total"
|
|
|
|
|
layout="total,prev,pager,next,sizes"
|
|
|
|
|
v-bind="mobilePagination"
|
|
|
|
|
@current-change="mobilePagination['current-change']"
|
|
|
|
|
@size-change="mobilePagination['size-change']"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</ElCard>
|
|
|
|
|
|
|
|
|
|
<LogDetailDialog
|
|
|
|
|
v-model:visible="detailVisible"
|
|
|
|
|
title="API错误日志详情"
|
|
|
|
|
:row-data="currentRow"
|
|
|
|
|
:sections="detailSections"
|
|
|
|
|
:fetch-detail="fetchGetApiErrorLog"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|