blob: 4e1d15a8d6c63572e006cdf70d55edf2f2b2e09a [file]
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<template>
<div class="event-timeline-container">
<a-spin :spinning="loading">
<a-timeline mode="left" class="event-timeline">
<a-timeline-item
v-for="(item, index) in events"
:key="index"
:color="item.type === 'warning' ? '#faad14' : '#1890ff'"
>
<!-- Time label on the left -->
<template #label>
<span class="event-time">{{ item.time }}</span>
</template>
<!-- Custom dot -->
<template #dot>
<div class="event-dot" :class="item.type">
<CheckCircleOutlined v-if="item.type !== 'warning'" class="dot-icon normal" />
<WarningOutlined v-else class="dot-icon warning" />
</div>
</template>
<!-- Event card -->
<div class="event-card" :class="item.type">
<span class="event-message">{{ item.message }}</span>
<a-tag :color="item.type === 'warning' ? 'orange' : 'blue'" class="event-source-tag">
{{ item.source }}
</a-tag>
</div>
</a-timeline-item>
<!-- Bottom hint -->
<a-timeline-item>
<template #dot>
<ArrowDownOutlined class="bottom-arrow" />
</template>
<div class="bottom-hint">
<span>{{ $t('eventExpiryHint') || '过期事件不会存储' }}</span>
<a-spin v-if="loadingMore" size="small" class="load-more-spinner" />
</div>
</a-timeline-item>
</a-timeline>
<div ref="loadMoreTriggerRef" class="load-more-trigger" />
</a-spin>
<a-empty v-if="!loading && events.length === 0" description="暂无事件" />
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import type { EventItem } from '@/types/api'
import { CheckCircleOutlined, WarningOutlined, ArrowDownOutlined } from '@ant-design/icons-vue'
const props = defineProps<{
events: EventItem[]
loading: boolean
loadingMore?: boolean
hasMore?: boolean
}>()
const emit = defineEmits<{
(e: 'loadMore'): void
}>()
const loadMoreTriggerRef = ref<HTMLElement>()
let observer: IntersectionObserver | null = null
const tryLoadMore = () => {
if (!props.loading && !props.loadingMore && props.hasMore) {
emit('loadMore')
}
}
onMounted(() => {
if (!window.IntersectionObserver || !loadMoreTriggerRef.value) {
return
}
observer = new IntersectionObserver((entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
tryLoadMore()
}
})
observer.observe(loadMoreTriggerRef.value)
})
onBeforeUnmount(() => {
observer?.disconnect()
observer = null
})
</script>
<style lang="less" scoped>
.event-timeline-container {
padding: 40px 20px 20px;
.event-timeline {
:deep(.ant-timeline-item-label) {
width: 180px;
}
:deep(.ant-timeline-item-tail) {
border-left: 2px solid #e8f4ff;
}
}
.event-time {
font-size: 13px;
color: #8c8c8c;
white-space: nowrap;
display: inline-block;
width: 160px;
text-align: right;
}
.event-dot {
display: flex;
align-items: center;
justify-content: center;
.dot-icon {
font-size: 16px;
&.normal {
color: #1890ff;
}
&.warning {
color: #faad14;
}
}
}
.event-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-radius: 6px;
border-left: 4px solid #1890ff;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.2s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
&.warning {
border-left-color: #faad14;
background: #fff7e6;
}
.event-message {
flex: 1;
font-size: 14px;
color: #262626;
margin-right: 12px;
line-height: 1.5;
}
.event-source-tag {
flex-shrink: 0;
font-size: 12px;
border-radius: 4px;
}
}
.bottom-arrow {
font-size: 14px;
color: #bfbfbf;
}
.bottom-hint {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #bfbfbf;
padding: 8px 0;
}
.load-more-spinner {
flex-shrink: 0;
}
.load-more-trigger {
width: 100%;
height: 1px;
}
}
</style>