<script lang="ts" setup>
|
import type { ActionItem, BasicColumn } from '@jnpf/ui/vxeTable';
|
|
import type { AuditOperation, AuditPageQuery } from './types';
|
|
import { computed, nextTick, ref } from 'vue';
|
|
import { usePermission } from '@jnpf/hooks';
|
import { useDrawer } from '@jnpf/ui/drawer';
|
import { BasicVxeTable, TableAction, useVxeTable } from '@jnpf/ui/vxeTable';
|
|
import { Result } from 'ant-design-vue';
|
import dayjs from 'dayjs';
|
|
import { getAuditActionLabel, normalizeAuditPageQuery } from './auditOperation';
|
import { auditOperationQuery } from './auditOperationQuery';
|
import DetailDrawer from './DetailDrawer.vue';
|
import DiffDrawer from './DiffDrawer.vue';
|
import TimelineDrawer from './TimelineDrawer.vue';
|
import { AUDIT_DETAIL_PERMISSION, AUDIT_QUERY_PERMISSION } from './types';
|
|
defineOptions({ name: 'AuditEvents' });
|
|
const DEFAULT_PAGE_SIZE = 20;
|
const listError = ref(false);
|
const { hasBtnP } = usePermission();
|
const hasQueryPermission = computed(() => hasBtnP(AUDIT_QUERY_PERMISSION, false));
|
const hasDetailPermission = computed(() => hasBtnP(AUDIT_DETAIL_PERMISSION, false));
|
|
const columns: BasicColumn[] = [
|
{ title: '操作时间', dataIndex: 'eventTime', width: 165, format: 'date|YYYY-MM-DD HH:mm:ss' },
|
{ title: '操作人', dataIndex: 'operatorName', minWidth: 110, slots: { default: 'operatorName' } },
|
{ title: '操作记录标题', dataIndex: 'recordTitle', minWidth: 240, slots: { default: 'recordTitle' } },
|
{ title: '模块名称', dataIndex: 'entryName', minWidth: 140, slots: { default: 'entryName' } },
|
{ title: '操作动作', dataIndex: 'actionLabel', minWidth: 150, slots: { default: 'actionLabel' } },
|
{ title: '原因', dataIndex: 'reason', minWidth: 140, slots: { default: 'reason' } },
|
{ title: '变更摘要', dataIndex: 'diffCount', minWidth: 120, slots: { default: 'diffSummary' } },
|
{ title: 'IP / 地区', dataIndex: 'ip', minWidth: 145, slots: { default: 'ipRegion' } },
|
];
|
|
const defaultRange = getDefaultRange();
|
const [registerTable, { reload }] = useVxeTable({
|
api: fetchOperations,
|
beforeFetch: ensureBoundedQuery,
|
columns,
|
immediate: hasQueryPermission.value,
|
emptyText: '当前条件下没有审计记录',
|
rowKey: 'id',
|
showIndexColumn: false,
|
useSearchForm: true,
|
formConfig: {
|
baseColProps: { xs: 24, sm: 12, md: 8, xl: 6 },
|
compact: true,
|
showAdvancedButton: false,
|
schemas: [
|
{
|
field: 'pickerVal',
|
label: '操作时间',
|
component: 'DateRange',
|
defaultValue: defaultRange,
|
componentProps: {
|
allowClear: false,
|
format: 'YYYY-MM-DD HH:mm:ss',
|
placeholder: ['开始时间', '结束时间'],
|
showTime: { defaultValue: [dayjs('00:00:00', 'HH:mm:ss'), dayjs('23:59:59', 'HH:mm:ss')] },
|
},
|
},
|
{
|
field: 'bizModule',
|
label: '业务模块',
|
component: 'Input',
|
helpMessage: '按业务模块精确筛选;列表显示的「模块名称」是操作入口菜单,两者不同',
|
componentProps: { placeholder: '请输入业务模块', submitOnPressEnter: true },
|
},
|
{
|
field: 'operatorName',
|
label: '操作人',
|
component: 'Input',
|
componentProps: { placeholder: '请输入操作人', submitOnPressEnter: true },
|
},
|
{
|
field: 'bizCode',
|
label: '业务单号',
|
component: 'Input',
|
helpMessage: '精确查询',
|
componentProps: { placeholder: '请输入完整业务单号', submitOnPressEnter: true },
|
},
|
{
|
field: 'eventType',
|
label: '事件类型',
|
component: 'Select',
|
componentProps: {
|
allowClear: true,
|
placeholder: '请选择事件类型',
|
options: [
|
{ fullName: '数据变更', id: 'DATA_CHANGE' },
|
{ fullName: '业务动作', id: 'BIZ_ACTION' },
|
{ fullName: '电子签名', id: 'E_SIGNATURE' },
|
],
|
},
|
},
|
],
|
fieldMapToTime: [['pickerVal', ['startTime', 'endTime'], 'YYYY-MM-DD HH:mm:ss']],
|
},
|
pagination: {
|
pageSize: DEFAULT_PAGE_SIZE,
|
pageSizeOptions: ['20', '50', '100'],
|
showQuickJumper: true,
|
showSizeChanger: true,
|
showTotal: (total) => `共 ${total} 次操作`,
|
},
|
actionColumn: {
|
dataIndex: 'action',
|
fixed: 'right',
|
title: '操作',
|
width: 145,
|
},
|
});
|
|
const [registerDetailDrawer, { openDrawer: openDetailDrawer }] = useDrawer();
|
const [registerDiffDrawer, { openDrawer: openDiffDrawer }] = useDrawer();
|
const [registerTimelineDrawer, { closeDrawer: closeTimelineDrawer, openDrawer: openTimelineDrawer }] = useDrawer();
|
|
async function fetchOperations(params: AuditPageQuery) {
|
if (!hasQueryPermission.value) throw new Error('AUDIT_QUERY_FORBIDDEN');
|
return { data: await auditOperationQuery.page(params) };
|
}
|
|
function ensureBoundedQuery(rawQuery: Record<string, unknown>) {
|
const query = normalizeAuditPageQuery(rawQuery);
|
if (!query.startTime || !query.endTime) {
|
const [startTime, endTime] = getDefaultRange().map((value) => dayjs(value).format('YYYY-MM-DD HH:mm:ss'));
|
query.startTime = startTime;
|
query.endTime = endTime;
|
}
|
query.currentPage = Number(query.currentPage) || 1;
|
query.pageSize = Math.min(Number(query.pageSize) || DEFAULT_PAGE_SIZE, 100);
|
return query;
|
}
|
|
function getDefaultRange() {
|
return [dayjs().subtract(6, 'day').startOf('day').valueOf(), dayjs().endOf('day').valueOf()];
|
}
|
|
function handleFetchSuccess() {
|
listError.value = false;
|
}
|
|
function handleFetchError() {
|
listError.value = true;
|
}
|
|
function retryList() {
|
listError.value = false;
|
reload();
|
}
|
|
function getTableActions(record: AuditOperation): ActionItem[] {
|
return [
|
{
|
auth: AUDIT_DETAIL_PERMISSION,
|
label: '详情',
|
onClick: () => showDetail(record),
|
},
|
{
|
ifShow: !!record.targetId,
|
label: '时间线',
|
onClick: () => showTimeline(record),
|
},
|
];
|
}
|
|
function handleCellClick({ column, row }: { column?: { field?: string }; row: AuditOperation }) {
|
if (!hasDetailPermission.value || column?.field === 'action' || column?.field === 'diffCount') return;
|
showDetail(row);
|
}
|
|
function showDetail(operation: AuditOperation) {
|
if (!hasDetailPermission.value) return;
|
openDetailDrawer(true, { operation });
|
}
|
|
function showDiffs(operation: AuditOperation) {
|
if (!hasDetailPermission.value || operation.diffCount <= 0) return;
|
openDiffDrawer(true, { operation });
|
}
|
|
function showTimeline(operation: AuditOperation) {
|
if (!operation.targetId || !hasQueryPermission.value) return;
|
openTimelineDrawer(true, { bizCode: operation.bizCode, targetId: operation.targetId });
|
}
|
|
function showTimelineDetail(operation: AuditOperation) {
|
closeTimelineDrawer();
|
nextTick(() => showDetail(operation));
|
}
|
|
function showTimelineDiffs(operation: AuditOperation) {
|
closeTimelineDrawer();
|
nextTick(() => showDiffs(operation));
|
}
|
|
function getDiffSummary(operation: AuditOperation) {
|
if (operation.diffCount > 0) return `${operation.diffCount} 处变更`;
|
return operation.eventType === 'BIZ_ACTION' ? '仅业务动作' : '无字段明细';
|
}
|
</script>
|
|
<template>
|
<div class="jnpf-content-wrapper audit-events-page">
|
<div class="jnpf-content-wrapper-center">
|
<Result v-if="!hasQueryPermission" class="audit-permission-state" status="403" title="无权查看审计日志" sub-title="请联系管理员授予 audit.query 权限。" />
|
|
<div v-else class="jnpf-content-wrapper-content audit-table-content">
|
<a-alert v-if="listError" class="audit-list-error" type="error" show-icon message="审计记录加载失败" description="筛选条件已保留,请稍后重试。">
|
<template #action>
|
<a-button size="small" @click="retryList">重试</a-button>
|
</template>
|
</a-alert>
|
|
<BasicVxeTable @register="registerTable" @cell-click="handleCellClick" @fetch-error="handleFetchError" @fetch-success="handleFetchSuccess">
|
<template #operatorName="{ record }">
|
<span :title="record.operatorName || '未知用户'">{{ record.operatorName || '未知用户' }}</span>
|
</template>
|
<template #recordTitle="{ record }">
|
<div class="audit-table-primary" :title="record.recordTitle || record.bizCode || '—'">
|
{{ record.recordTitle || record.bizCode || '—' }}
|
</div>
|
<div v-for="(line, i) in record.subTitles || []" :key="i" class="audit-table-secondary" :title="line">
|
{{ line }}
|
</div>
|
<div v-if="(record.subTitleCount || 0) > (record.subTitles || []).length" class="audit-table-secondary">
|
共 {{ record.subTitleCount }} 条
|
</div>
|
</template>
|
<template #entryName="{ record }">
|
<span :title="record.entryName || record.bizModule || '—'">{{ record.entryName || record.bizModule || '—' }}</span>
|
</template>
|
<template #actionLabel="{ record }">
|
<a-tag :bordered="false" color="blue" :title="getAuditActionLabel(record)">{{ getAuditActionLabel(record) }}</a-tag>
|
<a-tag v-if="record.signed" :bordered="false" color="green" title="本次操作含电子签名">🖊 已签名</a-tag>
|
</template>
|
<template #reason="{ record }">
|
<span :title="record.reason || '—'">{{ record.reason || '—' }}</span>
|
</template>
|
<template #diffSummary="{ record }">
|
<a-button v-if="record.diffCount > 0 && hasDetailPermission" class="audit-diff-link" type="link" size="small" @click.stop="showDiffs(record)">
|
{{ getDiffSummary(record) }}
|
</a-button>
|
<span v-else>{{ getDiffSummary(record) }}</span>
|
</template>
|
<template #ipRegion="{ record }">
|
<div class="audit-table-primary" :title="record.ip || '—'">{{ record.ip || '—' }}</div>
|
<div class="audit-table-secondary" v-if="record.ipRegion" :title="record.ipRegion">{{ record.ipRegion }}</div>
|
</template>
|
<template #action="{ record }">
|
<TableAction :actions="getTableActions(record)" stop-button-propagation />
|
</template>
|
</BasicVxeTable>
|
</div>
|
</div>
|
|
<DetailDrawer @register="registerDetailDrawer" />
|
<DiffDrawer @register="registerDiffDrawer" />
|
<TimelineDrawer @register="registerTimelineDrawer" @view-detail="showTimelineDetail" @view-diffs="showTimelineDiffs" />
|
</div>
|
</template>
|
|
<style lang="scss" scoped>
|
.audit-events-page {
|
min-width: 0;
|
}
|
|
.audit-table-content {
|
display: flex;
|
flex-direction: column;
|
min-width: 0;
|
}
|
|
.audit-list-error {
|
flex: none;
|
margin: 12px 12px 0;
|
}
|
|
.audit-permission-state {
|
margin: auto;
|
}
|
|
.audit-table-primary,
|
.audit-table-secondary {
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
|
.audit-table-secondary {
|
margin-top: 2px;
|
font-size: 12px;
|
color: var(--text-color-secondary);
|
}
|
|
.audit-diff-link {
|
height: auto;
|
padding: 0;
|
font-weight: 500;
|
}
|
|
@media (max-width: 640px) {
|
.audit-list-error {
|
margin: 8px 8px 0;
|
}
|
}
|
</style>
|