<script lang="ts" setup>
|
import type { AuditOperation } from './types';
|
|
import { computed, reactive, toRefs } from 'vue';
|
|
import { usePermission } from '@jnpf/hooks';
|
import { BasicDrawer, useDrawerInner } from '@jnpf/ui/drawer';
|
import { formatToDateTime } from '@jnpf/utils';
|
|
import { Empty, Result, Timeline, TimelineItem } from 'ant-design-vue';
|
|
import { getAuditActionLabel, getAuditEventTypeLabel } from './auditOperation';
|
import { auditOperationQuery } from './auditOperationQuery';
|
import { AUDIT_DETAIL_PERMISSION } from './types';
|
|
const emit = defineEmits<{
|
viewDetail: [operation: AuditOperation];
|
viewDiffs: [operation: AuditOperation];
|
}>();
|
|
const state = reactive({
|
bizCode: '',
|
error: false,
|
list: [] as AuditOperation[],
|
targetId: '',
|
});
|
const { bizCode, error, list, targetId } = toRefs(state);
|
const { hasBtnP } = usePermission();
|
const [registerDrawer, { changeLoading }] = useDrawerInner(init);
|
|
const hasDetailPermission = computed(() => hasBtnP(AUDIT_DETAIL_PERMISSION, false));
|
const isTruncated = computed(() => state.list.length >= 500);
|
|
async function init(data: { bizCode?: string; targetId: string }) {
|
state.bizCode = data.bizCode?.trim() || '';
|
state.targetId = data.targetId?.trim() || '';
|
state.list = [];
|
state.error = false;
|
await loadTimeline();
|
}
|
|
async function loadTimeline() {
|
if (!state.targetId) {
|
state.error = true;
|
return;
|
}
|
changeLoading(true);
|
state.error = false;
|
try {
|
state.list = await auditOperationQuery.timeline(state.targetId);
|
} catch {
|
state.list = [];
|
state.error = true;
|
} finally {
|
changeLoading(false);
|
}
|
}
|
|
function displayTime(value?: string) {
|
return value ? formatToDateTime(value, 'YYYY-MM-DD HH:mm:ss') : '—';
|
}
|
|
function displayOperator(operation: AuditOperation) {
|
return operation.operatorName || '未知用户';
|
}
|
|
function viewDetail(operation: AuditOperation) {
|
emit('viewDetail', operation);
|
}
|
|
function viewDiffs(operation: AuditOperation) {
|
emit('viewDiffs', operation);
|
}
|
</script>
|
|
<template>
|
<BasicDrawer
|
v-bind="$attrs"
|
@register="registerDrawer"
|
:title="`业务时间线 · ${bizCode || targetId}`"
|
width="min(720px, calc(100vw - 16px))"
|
class="audit-timeline-drawer"
|
:keyboard="true"
|
destroy-on-close>
|
<div class="audit-timeline-body">
|
<a-alert v-if="isTruncated" class="audit-timeline-alert" type="warning" show-icon message="仅展示前 500 条记录" />
|
|
<Result v-if="error" status="error" title="业务时间线加载失败" sub-title="请稍后重试。">
|
<template #extra>
|
<a-button type="primary" @click="loadTimeline">重试</a-button>
|
</template>
|
</Result>
|
<Empty v-else-if="!list.length" description="该业务记录暂无审计日志" />
|
|
<Timeline v-else class="audit-timeline">
|
<TimelineItem v-for="operation in list" :key="operation.correlationKey" color="blue">
|
<time class="audit-timeline-time">{{ displayTime(operation.eventTime) }}</time>
|
<div class="audit-timeline-content">
|
<div class="audit-timeline-heading">
|
<strong>{{ getAuditActionLabel(operation) }}</strong>
|
<a-tag :bordered="false">{{ getAuditEventTypeLabel(operation.eventType) }}</a-tag>
|
</div>
|
<div class="audit-timeline-meta">
|
<span>{{ displayOperator(operation) }}</span>
|
<a-button
|
v-if="operation.diffCount && hasDetailPermission"
|
class="audit-timeline-diff-link"
|
type="link"
|
size="small"
|
@click.stop="viewDiffs(operation)">
|
{{ operation.diffCount }} 处字段变更
|
</a-button>
|
<span v-else>{{ operation.diffCount ? `${operation.diffCount} 处字段变更` : '无字段明细' }}</span>
|
<span>{{ operation.sourceSummary }}</span>
|
</div>
|
<a-collapse ghost expand-icon-position="end">
|
<a-collapse-panel key="operation" header="操作信息">
|
<div class="audit-timeline-more">
|
<div><span>业务对象:</span>{{ operation.bizType || '—' }}</div>
|
<div><span>业务模块:</span>{{ operation.bizModule || '—' }}</div>
|
<div><span>本次操作事件:</span>{{ operation.eventIds.length }} 条</div>
|
<a-button v-if="hasDetailPermission" type="link" size="small" @click="viewDetail(operation)">查看详情</a-button>
|
</div>
|
</a-collapse-panel>
|
</a-collapse>
|
</div>
|
</TimelineItem>
|
</Timeline>
|
</div>
|
</BasicDrawer>
|
</template>
|
|
<style lang="scss" scoped>
|
.audit-timeline-body {
|
min-width: 0;
|
padding: 20px 24px 28px;
|
}
|
|
.audit-timeline-alert {
|
margin-bottom: 20px;
|
}
|
|
.audit-timeline {
|
margin-top: 4px;
|
}
|
|
.audit-timeline-time {
|
display: block;
|
margin-bottom: 6px;
|
color: var(--text-color-secondary);
|
}
|
|
.audit-timeline-content {
|
min-width: 0;
|
padding: 0 0 20px;
|
}
|
|
.audit-timeline-heading {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px;
|
align-items: center;
|
margin-bottom: 8px;
|
}
|
|
.audit-timeline-meta {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 6px 16px;
|
color: var(--text-color-secondary);
|
}
|
|
.audit-timeline-diff-link {
|
height: auto;
|
padding: 0;
|
font-size: inherit;
|
}
|
|
.audit-timeline-more {
|
display: grid;
|
gap: 8px;
|
min-width: 0;
|
overflow-wrap: anywhere;
|
|
span {
|
color: var(--text-color-secondary);
|
}
|
|
:deep(.ant-btn-link) {
|
justify-self: start;
|
height: auto;
|
padding: 0;
|
}
|
}
|
|
:deep(.ant-collapse-header) {
|
padding-inline: 0 !important;
|
}
|
|
:deep(.ant-collapse-content-box) {
|
padding-inline: 0 !important;
|
}
|
|
@media (max-width: 640px) {
|
.audit-timeline-body {
|
padding: 16px 12px 20px;
|
}
|
|
.audit-timeline-meta {
|
display: grid;
|
gap: 4px;
|
}
|
}
|
</style>
|