Files
cn-rdms-web/src/views/infra/log-management/api-access-log/index.vue

239 lines
7.7 KiB
Vue
Raw Normal View History

<script setup lang="tsx">
import { computed, reactive, ref } from 'vue';
import { INFRA_OPERATE_TYPE_DICT_CODE } from '@/constants/dict';
import { fetchExportApiAccessLog, fetchGetApiAccessLog, fetchGetApiAccessLogPage } 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,
formatDuration,
getLogExportFileName
} from '../shared';
import ApiAccessLogSearch from './modules/search.vue';
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
defineOptions({ name: 'ApiAccessLogTab' });
type ApiAccessLogPageResponse = Awaited<ReturnType<typeof fetchGetApiAccessLogPage>>;
function createSearchParams(): Api.SystemLog.ApiAccess.SearchParams {
return {
pageNo: 1,
pageSize: 10,
userId: undefined,
userType: undefined,
applicationName: undefined,
requestUrl: undefined,
beginTime: undefined,
duration: undefined,
resultCode: undefined
};
}
function transformPageResult(response: ApiAccessLogPageResponse, 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.ApiAccess.Log | null>(null);
const exporting = ref(false);
const canExport = computed(() => hasAuth(LogPermission.ApiAccessExport));
const detailSections: LogDetailSection[] = [
{
title: '请求信息',
fields: [
{ label: '日志编号', key: 'id' },
{ label: '链路追踪编号', key: 'traceId' },
{ label: '应用名', key: 'applicationName' },
{ label: '请求方式', key: 'requestMethod' },
{ label: '请求地址', key: 'requestUrl', span: 2 },
{ label: '开始时间', key: 'beginTime', type: 'datetime' },
{ label: '结束时间', key: 'endTime', type: 'datetime' },
{ label: '执行时长', formatter: detail => formatDuration(detail.duration) },
{ label: '结果码', key: 'resultCode' },
{ label: '结果提示', key: 'resultMsg', span: 2 }
]
},
{
title: '业务上下文',
fields: [
{ label: '用户编号', key: 'userId' },
{ label: '操作模块', key: 'operateModule' },
{ label: '操作名', key: 'operateName' },
{ label: '操作分类', key: 'operateType', type: 'dict', dictCode: INFRA_OPERATE_TYPE_DICT_CODE },
{ label: '用户IP', key: 'userIp' },
{ label: '浏览器UA', key: 'userAgent', type: 'multiline' }
]
},
{
title: '报文内容',
fields: [
{ label: '请求参数', key: 'requestParams', type: 'multiline' },
{ label: '响应结果', key: 'responseBody', type: 'multiline' }
]
}
];
const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagination } = useUIPaginatedTable<
ApiAccessLogPageResponse,
Api.SystemLog.ApiAccess.Log
>({
paginationProps: {
currentPage: searchParams.pageNo,
pageSize: searchParams.pageSize
},
api: () => fetchGetApiAccessLogPage(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 },
{ prop: 'requestMethod', label: '请求方式', width: 100, align: 'center' },
{ prop: 'requestUrl', label: '请求地址', minWidth: 220, showOverflowTooltip: true },
{ prop: 'operateModule', label: '操作模块', minWidth: 140, showOverflowTooltip: true },
{ prop: 'operateName', label: '操作名', minWidth: 140, showOverflowTooltip: true },
{
prop: 'operateType',
label: '操作分类',
minWidth: 120,
formatter: row => <DictText dictCode={INFRA_OPERATE_TYPE_DICT_CODE} value={row.operateType} />
},
{ prop: 'resultCode', label: '结果码', width: 100, align: 'center' },
{ prop: 'duration', label: '执行时长', width: 120, formatter: row => formatDuration(row.duration) },
{ prop: 'beginTime', label: '请求时间', minWidth: 180, formatter: row => formatDateTime(row.beginTime) },
{
prop: 'operate',
label: '操作',
width: 90,
align: 'center',
fixed: 'right',
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
}
]
});
function getRowActions(row: Api.SystemLog.ApiAccess.Log): BusinessTableAction[] {
return [
{
key: 'view',
label: '查看',
buttonType: 'primary',
icon: IconMdiEyeOutline,
onClick: () => openDetail(row)
}
];
}
function openDetail(row: Api.SystemLog.ApiAccess.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 fetchExportApiAccessLog(searchParams);
exporting.value = false;
if (error || !blob) {
return;
}
downloadBlob(blob, getLogExportFileName('API访问日志'));
}
</script>
<template>
<div class="flex-col-stretch gap-16px overflow-hidden">
<ApiAccessLogSearch 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="fetchGetApiAccessLog"
/>
</div>
</template>