<script lang="ts" setup>
|
import type { ActionItem, BasicColumn } from '@jnpf/ui/vxeTable';
|
|
import type { AuditOperation } from '../../audit/events/types';
|
|
import type { DmsFolderAuditPageQuery } from '#/api/x/audit/events';
|
|
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 DetailDrawer from '../../audit/events/DetailDrawer.vue';
|
import DiffDrawer from '../../audit/events/DiffDrawer.vue';
|
import TimelineDrawer from '../../audit/events/TimelineDrawer.vue';
|
import { folderAuditOperationQuery } from './folderAuditOperationQuery';
|
|
defineOptions({ name: 'DmsFolderAudit' });
|
|
const QUERY_PERMISSION = 'dms.folder.audit.query';
|
const DETAIL_PERMISSION = 'dms.folder.audit.detail';
|
const DEFAULT_PAGE_SIZE = 20;
|
const listError = ref(false);
|
const { hasBtnP } = usePermission();
|
const hasQueryPermission = computed(() => hasBtnP(QUERY_PERMISSION, false));
|
const hasDetailPermission = computed(() => hasBtnP(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: 'folderName', minWidth: 150, slots: { default: 'folderName' } },
|
{ title: '上级目录', dataIndex: 'parentFolderName', minWidth: 140, slots: { default: 'parentFolderName' } },
|
{ title: '文件夹路径', dataIndex: 'recordTitle', minWidth: 240, slots: { default: 'recordTitle' } },
|
{ title: '操作动作', dataIndex: 'actionLabel', minWidth: 155, slots: { default: 'actionLabel' } },
|
{ title: '变更摘要', dataIndex: 'diffCount', width: 115, slots: { default: 'diffSummary' } },
|
{ title: '签名状态', dataIndex: 'signed', width: 105, slots: { default: 'signed' } },
|
{ 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: 'operatorName',
|
label: '操作人',
|
component: 'Input',
|
componentProps: { placeholder: '请输入操作人', submitOnPressEnter: true },
|
},
|
{
|
field: 'actionCode',
|
label: '操作动作',
|
component: 'Select',
|
componentProps: {
|
allowClear: true,
|
placeholder: '请选择操作动作',
|
options: [
|
{ fullName: '新建', id: 'CREATE' },
|
{ fullName: '重命名或移动', id: 'UPDATE' },
|
{ fullName: '删除', id: 'DELETE' },
|
],
|
},
|
},
|
{
|
field: 'signed',
|
label: '签名状态',
|
component: 'Select',
|
componentProps: {
|
allowClear: true,
|
placeholder: '请选择签名状态',
|
options: [
|
{ fullName: '已签名', id: true },
|
{ fullName: '未签名', id: false },
|
],
|
},
|
},
|
{
|
field: 'keyword',
|
label: '名称或路径',
|
component: 'Input',
|
componentProps: { placeholder: '请输入文件夹名称或路径', submitOnPressEnter: true },
|
},
|
{
|
field: 'folderId',
|
label: '文件夹 ID',
|
component: 'Input',
|
componentProps: { placeholder: '请输入完整文件夹 ID', submitOnPressEnter: true },
|
},
|
],
|
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: DmsFolderAuditPageQuery) {
|
if (!hasQueryPermission.value) throw new Error('DMS_FOLDER_AUDIT_QUERY_FORBIDDEN');
|
return { data: await folderAuditOperationQuery.page(params) };
|
}
|
|
function ensureBoundedQuery(rawQuery: Record<string, unknown>): DmsFolderAuditPageQuery {
|
const query = Object.fromEntries(
|
Object.entries(rawQuery).flatMap(([key, value]) => {
|
const normalized = typeof value === 'string' ? value.trim() : value;
|
return normalized === '' || normalized === undefined || normalized === null ? [] : [[key, normalized]];
|
}),
|
) as unknown as DmsFolderAuditPageQuery;
|
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 getActionLabel(operation: AuditOperation) {
|
return operation.actionLabel || operation.actionCode || '未标记操作';
|
}
|
|
function getActionColor(operation: AuditOperation) {
|
if (operation.actionCode === 'CREATE') return 'green';
|
if (operation.actionCode === 'DELETE') return 'red';
|
return 'blue';
|
}
|
|
function getDiffSummary(operation: AuditOperation) {
|
return operation.diffCount > 0 ? `${operation.diffCount} 处变更` : '无字段明细';
|
}
|
|
function getTableActions(record: AuditOperation): ActionItem[] {
|
return [
|
{
|
auth: DETAIL_PERMISSION,
|
label: '详情',
|
onClick: () => showDetail(record),
|
},
|
{
|
auth: QUERY_PERMISSION,
|
label: '时间线',
|
onClick: () => openTimeline(record),
|
},
|
];
|
}
|
|
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 openTimeline(operation: AuditOperation) {
|
if (!operation.targetId || !hasQueryPermission.value) return;
|
openTimelineDrawer(true, { bizCode: operation.recordTitle || operation.bizCode, targetId: operation.targetId });
|
}
|
|
function showTimelineDetail(operation: AuditOperation) {
|
closeTimelineDrawer();
|
nextTick(() => showDetail(operation));
|
}
|
|
function showTimelineDiffs(operation: AuditOperation) {
|
closeTimelineDrawer();
|
nextTick(() => showDiffs(operation));
|
}
|
|
function retryList() {
|
listError.value = false;
|
reload();
|
}
|
</script>
|
|
<template>
|
<div class="jnpf-content-wrapper folder-audit-page">
|
<div class="jnpf-content-wrapper-center">
|
<Result
|
v-if="!hasQueryPermission"
|
class="folder-audit-permission-state"
|
status="403"
|
title="无权查看文件夹审计日志"
|
sub-title="请联系管理员授予 dms.folder.audit.query 权限。" />
|
|
<div v-else class="jnpf-content-wrapper-content folder-audit-table-content">
|
<a-alert
|
v-if="listError"
|
class="folder-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" @fetch-error="listError = true" @fetch-success="listError = false">
|
<template #operatorName="{ record }">
|
<span :title="record.operatorName || '未知用户'">{{ record.operatorName || '未知用户' }}</span>
|
</template>
|
<template #folderName="{ record }">
|
<span :title="record.folderName || '—'">{{ record.folderName || '—' }}</span>
|
</template>
|
<template #parentFolderName="{ record }">
|
<span :title="record.parentFolderName || '—'">{{ record.parentFolderName || '—' }}</span>
|
</template>
|
<template #recordTitle="{ record }">
|
<div class="folder-audit-primary" :title="record.recordTitle || record.bizCode || '—'">
|
{{ record.recordTitle || record.bizCode || '—' }}
|
</div>
|
<div v-if="record.targetId" class="folder-audit-secondary" :title="record.targetId">{{ record.targetId }}</div>
|
</template>
|
<template #actionLabel="{ record }">
|
<a-tag :bordered="false" :color="getActionColor(record)" :title="getActionLabel(record)">{{ getActionLabel(record) }}</a-tag>
|
</template>
|
<template #diffSummary="{ record }">
|
<a-button
|
v-if="record.diffCount > 0 && hasDetailPermission"
|
class="folder-audit-diff-link"
|
type="link"
|
size="small"
|
@click.stop="showDiffs(record)">
|
{{ getDiffSummary(record) }}
|
</a-button>
|
<span v-else>{{ getDiffSummary(record) }}</span>
|
</template>
|
<template #signed="{ record }">
|
<a-tag :bordered="false" :color="record.signed ? 'green' : 'default'">{{ record.signed ? '已签名' : '未签名' }}</a-tag>
|
</template>
|
<template #ipRegion="{ record }">
|
<div class="folder-audit-primary" :title="record.ip || '—'">{{ record.ip || '—' }}</div>
|
<div v-if="record.ipRegion" class="folder-audit-secondary" :title="record.ipRegion">{{ record.ipRegion }}</div>
|
</template>
|
<template #action="{ record }">
|
<TableAction :actions="getTableActions(record)" stop-button-propagation />
|
</template>
|
</BasicVxeTable>
|
</div>
|
</div>
|
|
<DetailDrawer :detail-loader="folderAuditOperationQuery.detail" :detail-permission="DETAIL_PERMISSION" @register="registerDetailDrawer" />
|
<DiffDrawer :detail-loader="folderAuditOperationQuery.detail" :detail-permission="DETAIL_PERMISSION" @register="registerDiffDrawer" />
|
<TimelineDrawer
|
:detail-permission="DETAIL_PERMISSION"
|
:timeline-loader="folderAuditOperationQuery.timeline"
|
@register="registerTimelineDrawer"
|
@view-detail="showTimelineDetail"
|
@view-diffs="showTimelineDiffs" />
|
</div>
|
</template>
|
|
<style lang="scss" scoped>
|
.folder-audit-page,
|
.folder-audit-table-content {
|
min-width: 0;
|
}
|
|
.folder-audit-table-content {
|
display: flex;
|
flex-direction: column;
|
}
|
|
.folder-audit-list-error {
|
flex: none;
|
margin: 12px 12px 0;
|
}
|
|
.folder-audit-permission-state {
|
margin: auto;
|
}
|
|
.folder-audit-primary,
|
.folder-audit-secondary {
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
|
.folder-audit-secondary {
|
margin-top: 2px;
|
font-size: 12px;
|
color: var(--text-color-secondary);
|
}
|
|
.folder-audit-diff-link {
|
height: auto;
|
padding: 0;
|
font-weight: 500;
|
}
|
|
@media (max-width: 640px) {
|
.folder-audit-list-error {
|
margin: 8px 8px 0;
|
}
|
}
|
</style>
|