@@ -1,6 +1,6 @@
< script setup lang = "ts" >
import { NButton , NDataTable , NModal , type DataTableColumns } from 'naive-ui'
import { computed , h , onMounted , reactive , ref } from 'vue'
import { NButton , NDataTable , NInput , NInputGroup , NInputGroupLabel , NModal , type DataTableColumns } from 'naive-ui'
import { computed , h , onMounted , reactive , ref , watch } from 'vue'
import { getAuditEventDetail , listAuditEvents } from '@/api/tenant'
import DetailFieldList , { type DetailRow } from '@/components/DetailFieldList.vue'
@@ -23,23 +23,74 @@ const rows = ref<AuditEvent[]>([])
// 服务端分窗回溯游标:空且 exhausted 表示已到 365 天保留期尽头
const cursor = ref ( '' )
const exhausted = ref ( false )
// 服务端全文检索关键字;变化即重置信息流
const q = ref ( '' )
// 已完整回溯到的时刻(比它更新的时段已扫完),检索命中稀疏时给用户方位感
const scannedThrough = ref ( '' )
// 信息流代次:刷新/换关键字自增,用于丢弃仍在途的过期响应
let gen = 0
let qTimer : ReturnType < typeof setTimeout > | undefined
// 当前信息流对应的关键字;回车/防抖只在关键字实际变化时才重查,避免重复请求
let loadedQ : string | null = null
// 连续自动补批计数:搜索命中稀疏时封顶,不能无人值守扫完整个保留期
let autoFills = 0
const maxAutoFills = 2
// 关键字防抖:停止输入 500ms 后按新关键字重查;回车/清空即时生效
watch ( q , ( ) => {
clearTimeout ( qTimer )
if ( q . value . trim ( ) === loadedQ ) return
qTimer = setTimeout ( ( ) => void load ( ) , 500 )
} )
/** 回车/清空的即时提交:关键字未变则不发起重复搜索 */
function submitSearch ( ) {
clearTimeout ( qTimer )
if ( q . value . trim ( ) !== loadedQ ) void load ( )
}
const pageCount = computed ( ( ) => Math . max ( 1 , Math . ceil ( rows . value . length / pagination . pageSize ) ) )
const hasMore = computed ( ( ) => cursor . value !== '' && ! exhausted . value )
/** 回溯进度 = min(游标窗上界, 已加载最旧事件):窗内续翻时游标上界还停在窗口右缘,
* 直接展示会显示成当前时刻;两者取更早才反映真实扫描位置 */
const scanProgress = computed ( ( ) => {
const oldest = rows . value . length ? ( rows . value [ rows . value . length - 1 ] . eventTime ? ? '' ) : ''
if ( ! scannedThrough . value || ! oldest ) return scannedThrough . value || oldest
return scannedThrough . value < oldest ? scannedThrough . value : oldest
} )
/** 受控分页:字面量对象会在重渲染时被重建导致页大小切换失效 */
const pagination = reactive ( {
page : 1 ,
pageSize : 20 ,
showSizePicker : true ,
pageSizes : [ 20 , 50 , 100 ] ,
prefix : ( ) =>
` 已加载 ${ rows . value . length } 条 · ` +
( loadingMore . value ? '正在加载更早…' : exhausted . value ? '已到 365 天保留期尽头' : '翻到末页自动加载更早' ) ,
/** 状态行:加载中/尽头给文案;停驻且还有更早数据时给「加载更早」按钮,
* 自动补批封顶后由用户显式接力,不会无人值守扫完整个保留期 */
prefix : ( ) => {
const parts = [ ` 已加载 ${ rows . value . length } 条 ` ]
if ( scanProgress . value && ! exhausted . value ) parts . push ( ` 已回溯至 ${ fmtTime ( scanProgress . value ) } ` )
if ( exhausted . value ) parts . push ( '已到 365 天保留期尽头' )
const text = parts . join ( ' · ' )
if ( loadingMore . value ) return ` ${ text } · 正在加载更早… `
if ( ! hasMore . value ) return text
return h ( 'span' , { class : 'inline-flex items-center gap-1.5' } , [
` ${ text } · ` ,
h (
NButton ,
{ size : 'tiny' , quaternary : true , type : 'primary' , onClick : ( ) => { autoFills = 0 ; void fetchBatch ( ) } } ,
{ default : ( ) => '加载更早' } ,
) ,
] )
} ,
onUpdatePage : ( p : number ) => {
pagination . page = p
// 懒加载触发点:翻到已加载数据的最后一页即向后端续取一批
if ( p >= pageCount . value ) void fetchBatch ( )
// 懒加载触发点:翻到已加载数据的最后一页即向后端续取一批(用户动作,重置自动补批额度)
if ( p >= pageCount . value ) {
autoFills = 0
void fetchBatch ( )
}
} ,
onUpdatePageSize : ( ps : number ) => {
pagination . pageSize = ps
@@ -47,35 +98,45 @@ const pagination = reactive({
} ,
} )
/** 重置信息流:回到最新一批 */
/** 重置信息流:回到最新一批(刷新按钮与关键字变化共用) */
async function load ( ) {
clearTimeout ( qTimer )
const g = ++ gen
loadedQ = q . value . trim ( )
autoFills = 0
loading . value = true
error . value = ''
rows . value = [ ]
cursor . value = ''
exhausted . value = false
scannedThrough . value = ''
pagination . page = 1
try {
await fetchBatch ( true )
} finally {
loading . value = false
if ( g === gen ) loading . value = false
}
}
/** 向更早方向续取一批(~1 00 条):追加去重后按时间重排;
* 末页不满且还有更早数据时自动补一批,避免首屏残页 */
/** 向更早方向续取一批(~2 00 条):追加去重后按时间重排;
* 末页不满且还有更早数据时自动补一批,避免首屏残页;
* 代次不符的在途响应直接丢弃,防止旧关键字结果混入新信息流 */
async function fetchBatch ( first = false ) {
if ( loadingMore . value || ( ! first && ! hasMore . value ) ) return
if ( ! first && ( loadingMore . value || ! hasMore . value ) ) return
const g = gen
loadingMore . value = true
try {
const r = await listAuditEvents ( props . cfgId , { cursor : cursor . value || undefined } )
const r = await listAuditEvents ( props . cfgId , { cursor : cursor . value || undefined , q : q . value . trim ( ) || undefined } )
if ( g !== gen ) return
const seen = new Set ( rows . value . map ( ( e ) => e . eventId ) )
rows . value = [ ... rows . value , ... r . items . filter ( ( e ) => ! seen . has ( e . eventId ) ) ] . sort ( ( a , b ) =>
( b . eventTime ? ? '' ) . localeCompare ( a . eventTime ? ? '' ) ,
)
cursor . value = r . cursor ? ? ''
exhausted . value = r . exhausted
scannedThrough . value = r . scannedThrough ? ? ''
} catch ( e ) {
if ( g !== gen ) return
if ( first ) {
error . value = e instanceof Error ? e . message : '查询失败'
} else {
@@ -83,9 +144,13 @@ async function fetchBatch(first = false) {
message . error ( e instanceof Error ? e . message : '继续加载失败,请刷新重新查询' )
}
} finally {
loadingMore . value = false
if ( g === gen ) loadingMore . value = false
}
// 首屏残页自动补批:封顶 maxAutoFills 次,命中稀疏时停驻等用户点「加载更早」
if ( g === gen && hasMore . value && rows . value . length < pagination . pageSize && autoFills < maxAutoFills ) {
autoFills ++
void fetchBatch ( )
}
if ( hasMore . value && rows . value . length < pagination . pageSize ) void fetchBatch ( )
}
onMounted ( ( ) => void load ( ) )
@@ -206,6 +271,17 @@ const columns: DataTableColumns<AuditEvent> = [
< div class = "text-sm font-semibold" > 审计日志 < / div >
< StatusBadge v-if = "exhausted" kind="run" label="已加载全部保留期数据" :dot="false" / >
< div class = "flex-1" / >
< NInputGroup class = "!w-76 max-lg:!w-full" >
< NInputGroupLabel size = "small" > 搜索 < / NInputGroupLabel >
< NInput
v -model :value = "q"
size = "small"
clearable
placeholder = "事件 / 资源 / 操作者,支持 *"
@keydown.enter ="submitSearch()"
@clear ="submitSearch()"
/ >
< / NInputGroup >
< NButton size = "small" :loading = "loading" @click ="load()" > 刷新 < / NButton >
< / div >
@@ -228,7 +304,8 @@ const columns: DataTableColumns<AuditEvent> = [
OCI Audit 免费且默认开启 , 事件保留 365 天 , 无需在云端做任何配置 ·
实时查询不入库 , 事件通常延迟数分钟可见 ·
已默认过滤遥测噪声 ( SummarizeMetricsData ) 与内网地址发起的服务互调 ·
从最新事件起每批约 100 条 , 翻到末页自动向更早加载 , 空档期自动扩窗回溯 · 点击行查看完整详情
从最新事件起每批约 200 条 , 翻到末页自动向更早加载 , 空档期自动扩窗回溯 ·
搜索匹配事件 / 资源 / 操作者等可见字段 ( 不区分大小写 , 支持 * 通配 ) , 命中较少时自动补两批后停驻 , 点 「 加载更早 」 继续回溯 · 点击行查看完整详情
< / FootNote >
< NModal v -model :show = "showDetail" preset = "card" closable : style = "{ width: '720px', maxWidth: 'calc(100vw - 24px)' }" >
@@ -254,7 +331,7 @@ const columns: DataTableColumns<AuditEvent> = [
v-if = "detailRaw"
:value = "detailRaw"
title = "原始 JSON"
meta = "来自 Audit SDK "
meta = "来自 OCI 审计日志 "
max -height = " 36vh "
wide
/ >