feat(日志管理): 开发日志管理功能。
fix(项目任务): 1、任务完成后需要依然能够修改工作日志,但是只能修改工作内容和上传附件。2、任务完成后,协办人的工作日志不应该能删除、所有任务里的成员不能新增工作日志,前端不显示新增、删除按钮。3、团队成员的面板,在成员排序时,让有下属的成员提前。4、在任务弹出框有个快速用执行的信息填充的icon。
This commit is contained in:
238
src/views/infra/log-management/api-access-log/index.vue
Normal file
238
src/views/infra/log-management/api-access-log/index.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<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>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { SYSTEM_USER_TYPE_DICT_CODE } from '@/constants/dict';
|
||||
import TableSearchFields, { type SearchField } from '@/components/custom/table-search-fields.vue';
|
||||
|
||||
defineOptions({ name: 'ApiAccessLogSearch' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
reset: [];
|
||||
search: [];
|
||||
}>();
|
||||
|
||||
const model = defineModel<Api.SystemLog.ApiAccess.SearchParams>('model', { required: true });
|
||||
|
||||
const fields = computed<SearchField[]>(() => [
|
||||
{
|
||||
key: 'applicationName',
|
||||
label: '应用名',
|
||||
type: 'input',
|
||||
placeholder: '请输入应用名'
|
||||
},
|
||||
{
|
||||
key: 'requestUrl',
|
||||
label: '请求地址',
|
||||
type: 'input',
|
||||
placeholder: '请输入请求地址'
|
||||
},
|
||||
{
|
||||
key: 'resultCode',
|
||||
label: '结果码',
|
||||
type: 'input',
|
||||
placeholder: '请输入结果码',
|
||||
transformValue: value => {
|
||||
const text = String(value ?? '').trim();
|
||||
if (!text) return undefined;
|
||||
const resultCode = Number(text);
|
||||
return Number.isFinite(resultCode) ? resultCode : undefined;
|
||||
},
|
||||
resolveValue: value => (value === null || value === undefined ? '' : String(value))
|
||||
},
|
||||
{
|
||||
key: 'duration',
|
||||
label: '最小时长(ms)',
|
||||
type: 'input',
|
||||
placeholder: '请输入执行时长下限',
|
||||
transformValue: value => {
|
||||
const text = String(value ?? '').trim();
|
||||
if (!text) return undefined;
|
||||
const duration = Number(text);
|
||||
return Number.isFinite(duration) ? duration : undefined;
|
||||
},
|
||||
resolveValue: value => (value === null || value === undefined ? '' : String(value))
|
||||
},
|
||||
{
|
||||
key: 'userId',
|
||||
label: '用户编号',
|
||||
type: 'input',
|
||||
placeholder: '请输入用户编号'
|
||||
},
|
||||
{
|
||||
key: 'userType',
|
||||
label: '用户类型',
|
||||
type: 'dict',
|
||||
placeholder: '请选择用户类型',
|
||||
dictCode: SYSTEM_USER_TYPE_DICT_CODE,
|
||||
filterable: true
|
||||
},
|
||||
{
|
||||
key: 'beginTime',
|
||||
label: '请求时间',
|
||||
type: 'dateRange',
|
||||
placeholder: '请选择请求时间',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
format: 'YYYY-MM-DD HH:mm:ss'
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableSearchFields v-model="model" :fields="fields" :columns="4" @reset="emit('reset')" @search="emit('search')" />
|
||||
</template>
|
||||
Reference in New Issue
Block a user