1111
This commit is contained in:
@@ -102,69 +102,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 - 紧凑布局 -->
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="flex flex-col xl:flex-row gap-4">
|
||||
|
||||
<!-- 左侧图表区域 -->
|
||||
<div class="flex-1">
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
<!-- 数据弹幕区域 - 独占2列 -->
|
||||
<div class="xl:col-span-2">
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 flex items-center justify-center rounded-lg bg-indigo-500 text-white mr-2">
|
||||
<el-icon size="12">
|
||||
<TrendCharts />
|
||||
</el-icon>
|
||||
</div>
|
||||
<h3 class="text-sm font-semibold text-gray-800">实时API调用动态</h3>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
实时更新 · 最多显示100条
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 弹幕容器 -->
|
||||
<div ref="danmakuContainer" class="relative h-20 overflow-hidden bg-gradient-to-b from-gray-50 to-white rounded-lg border border-gray-100">
|
||||
<!-- 无数据提示 -->
|
||||
<div v-if="danmakuItems.length === 0" class="absolute inset-0 flex items-center justify-center text-gray-400 text-sm">
|
||||
<div class="text-center">
|
||||
<el-icon size="24" class="mb-2 animate-spin">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<p>正在加载数据...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 弹幕项 - 每条独立滚动 -->
|
||||
<div
|
||||
v-for="(item, index) in danmakuItems"
|
||||
:key="item.id"
|
||||
:style="getDanmakuStyle(item, index)"
|
||||
class="absolute danmaku-item bg-white rounded-lg shadow-sm border px-2.5 py-1 whitespace-nowrap cursor-pointer hover:shadow-md transition-shadow"
|
||||
:class="[getDanmakuClass(item), { 'danmaku-paused': pausedDanmakuIds.has(item.id) }]"
|
||||
@mouseenter="pauseDanmaku(item.id)"
|
||||
@mouseleave="resumeDanmaku(item.id)"
|
||||
@animationend="onDanmakuAnimationEnd(item.id)"
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center text-white text-xs font-bold"
|
||||
:class="getUserAvatarClass(item.userId)">
|
||||
{{ getUsernameInitial(item.username) }}
|
||||
</div>
|
||||
<span class="text-xs font-medium text-gray-800">{{ item.username }}</span>
|
||||
<span class="text-xs text-gray-400">·</span>
|
||||
<span class="text-xs text-purple-600 font-medium">{{ item.apiName }}</span>
|
||||
<span class="text-xs text-gray-400">·</span>
|
||||
<span class="text-xs text-blue-600 font-semibold">第{{ item.callCount || 1 }}次</span>
|
||||
<span class="text-xs text-gray-400">·</span>
|
||||
<span class="text-xs text-gray-500">{{ formatRelativeTime(item.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 弹幕系统区域 - 占据两列宽度 -->
|
||||
<div class="col-span-1 xl:col-span-2">
|
||||
<DanmakuBar />
|
||||
</div>
|
||||
<!-- 用户注册与认证趋势 -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
|
||||
@@ -523,7 +470,6 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { apiCallApi } from '@/api'
|
||||
import {
|
||||
adminGetApiDomainStatistics,
|
||||
adminGetApiPopularityRanking,
|
||||
@@ -534,6 +480,7 @@ import {
|
||||
adminGetUserCallRanking,
|
||||
adminGetUserDomainStatistics
|
||||
} from '@/api/statistics'
|
||||
import DanmakuBar from '@/components/common/DanmakuBar.vue'
|
||||
import { Check, Loading, Money, Refresh, TrendCharts, User } from '@element-plus/icons-vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -582,27 +529,6 @@ const apiPopularityData = ref([])
|
||||
// 今日认证企业数据
|
||||
const todayCertifiedEnterprises = ref([])
|
||||
|
||||
// 弹幕相关数据 - 重构为实时显示模式
|
||||
const danmakuItems = ref([]) // 正在显示的弹幕列表
|
||||
const danmakuContainer = ref(null)
|
||||
const danmakuPollingTimer = ref(null) // 数据库查询定时器
|
||||
const lastQueryTime = ref(null) // 最后查询的时间点
|
||||
const processedRecordIds = ref(new Set()) // 已处理的记录ID集合,用于去重
|
||||
const pausedDanmakuIds = ref(new Set()) // 暂停的弹幕ID集合
|
||||
const danmakuDisplayOrder = ref(0) // 弹幕显示顺序,用于z-index
|
||||
const isProcessing = ref(false) // 是否正在处理新记录,避免并发
|
||||
|
||||
// 配置常量
|
||||
const MAX_DANMAKU_ITEMS = 100 // 最多显示的弹幕数量
|
||||
const DANMAKU_POLLING_INTERVAL = 1500 // 数据库查询间隔(毫秒)- 1.5秒,提高实时性
|
||||
const DANMAKU_ITEM_HEIGHT = 28 // 每条弹幕的高度
|
||||
const DANMAKU_CONTAINER_HEIGHT = 80 // 容器高度
|
||||
const DANMAKU_MIN_SPACING = 32 // 最小垂直间距(避免重叠)
|
||||
const DANMAKU_ANIMATION_DURATION = 15 // 动画持续时间(秒)
|
||||
const DANMAKU_ESTIMATED_WIDTH = 250 // 估算的弹幕宽度(像素)
|
||||
const MAX_PROCESSED_IDS = 1000 // 最大已处理ID数量,避免内存泄漏
|
||||
|
||||
|
||||
// 快捷选择配置
|
||||
const shortcuts = [
|
||||
{
|
||||
@@ -1396,437 +1322,10 @@ const getNoDataMessage = (periodRef) => {
|
||||
|
||||
|
||||
|
||||
// 获取用户名显示文本
|
||||
const getDisplayUsername = (record) => {
|
||||
// 尝试多种可能的字段名(兼容不同的命名风格)
|
||||
if (record.company_name || record.companyName) {
|
||||
return record.company_name || record.companyName
|
||||
}
|
||||
if (record.user?.company_name || record.user?.companyName) {
|
||||
return record.user.company_name || record.user.companyName
|
||||
}
|
||||
if (record.user?.phone) {
|
||||
return record.user.phone
|
||||
}
|
||||
// 如果都没有,使用用户ID的前8位作为显示
|
||||
if (record.user_id || record.userId) {
|
||||
const userId = record.user_id || record.userId
|
||||
return `用户${userId.substring(0, 8)}`
|
||||
}
|
||||
return '未知用户'
|
||||
}
|
||||
|
||||
// 获取API名称
|
||||
const getApiName = (record) => {
|
||||
return record.product_name || record.productName || '未知API'
|
||||
}
|
||||
|
||||
// 格式化日期时间为后端期望的格式
|
||||
const formatDateTime = (date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
// 获取调用次数(简化版本,实时显示不需要精确计数)
|
||||
const getCallCount = (record) => {
|
||||
// 实时弹幕中,调用次数可以简化为1,或者从记录中获取
|
||||
// 如果需要精确计数,可以后续优化
|
||||
return record.call_count || 1
|
||||
}
|
||||
|
||||
// 加载弹幕数据 - 实时模式:查询到新记录立即显示
|
||||
const loadDanmakuData = async () => {
|
||||
// 如果正在处理,跳过本次查询
|
||||
if (isProcessing.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isProcessing.value = true
|
||||
const now = new Date()
|
||||
let params = {}
|
||||
|
||||
// 构建查询参数
|
||||
if (!lastQueryTime.value) {
|
||||
// 首次加载:获取最近30条记录
|
||||
params = {
|
||||
page: 1,
|
||||
page_size: 30,
|
||||
sort_by: 'created_at',
|
||||
sort_order: 'desc'
|
||||
}
|
||||
} else {
|
||||
// 获取自上次查询时间之后的新记录
|
||||
params = {
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
start_time: formatDateTime(lastQueryTime.value),
|
||||
sort_by: 'created_at',
|
||||
sort_order: 'desc'
|
||||
}
|
||||
}
|
||||
|
||||
const response = await apiCallApi.getAdminApiCalls(params)
|
||||
|
||||
if (response.success && response.data?.items) {
|
||||
const records = response.data.items
|
||||
|
||||
// 过滤掉已处理的记录(使用ID去重)
|
||||
const newRecords = records.filter(record => {
|
||||
const recordId = record.id || `${record.user_id || record.userId}_${record.created_at || record.createdAt}`
|
||||
if (processedRecordIds.value.has(recordId)) {
|
||||
return false
|
||||
}
|
||||
processedRecordIds.value.add(recordId)
|
||||
|
||||
// 限制已处理ID集合的大小,避免内存泄漏
|
||||
if (processedRecordIds.value.size > MAX_PROCESSED_IDS) {
|
||||
const idsArray = Array.from(processedRecordIds.value)
|
||||
processedRecordIds.value = new Set(idsArray.slice(-MAX_PROCESSED_IDS / 2))
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// 更新查询时间
|
||||
if (records.length > 0) {
|
||||
const latestRecord = records.reduce((latest, current) => {
|
||||
const latestTime = new Date(latest.created_at || latest.createdAt || 0).getTime()
|
||||
const currentTime = new Date(current.created_at || current.createdAt || 0).getTime()
|
||||
return currentTime > latestTime ? current : latest
|
||||
})
|
||||
const latestTime = new Date(latestRecord.created_at || latestRecord.createdAt || now)
|
||||
if (!lastQueryTime.value || latestTime > lastQueryTime.value) {
|
||||
lastQueryTime.value = latestTime
|
||||
}
|
||||
} else {
|
||||
// 如果没有记录,更新时间到当前时间减1秒
|
||||
const oneSecondAgo = new Date(now.getTime() - 1000)
|
||||
if (!lastQueryTime.value || oneSecondAgo > lastQueryTime.value) {
|
||||
lastQueryTime.value = oneSecondAgo
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有新记录,立即处理并显示
|
||||
if (newRecords.length > 0) {
|
||||
// 按时间排序(旧的在前,新的在后)
|
||||
const sortedRecords = newRecords.sort((a, b) => {
|
||||
const timeA = new Date(a.created_at || a.createdAt || 0).getTime()
|
||||
const timeB = new Date(b.created_at || b.createdAt || 0).getTime()
|
||||
return timeA - timeB
|
||||
})
|
||||
|
||||
// 立即处理并显示新记录
|
||||
await processAndDisplayRecords(sortedRecords)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取弹幕数据失败:', err)
|
||||
} finally {
|
||||
isProcessing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 计算弹幕右边缘离开容器右边界所需的时间(秒)
|
||||
const getDanmakuExitRightTime = () => {
|
||||
if (!danmakuContainer.value) {
|
||||
return 1.0 // 默认值
|
||||
}
|
||||
|
||||
const containerWidth = danmakuContainer.value.clientWidth || 1200
|
||||
const danmakuWidth = DANMAKU_ESTIMATED_WIDTH
|
||||
|
||||
// 弹幕从 left: 100% 开始,移动到完全离开左边界
|
||||
// 总移动距离 = 容器宽度 + 弹幕宽度
|
||||
const totalDistance = containerWidth + danmakuWidth
|
||||
|
||||
// 弹幕右边缘离开右边界需要移动的距离 = 弹幕宽度
|
||||
// 时间 = (弹幕宽度 / 总移动距离) * 动画持续时间
|
||||
const exitTime = (danmakuWidth / totalDistance) * DANMAKU_ANIMATION_DURATION
|
||||
|
||||
// 限制在合理范围内(0.5-2秒)
|
||||
return Math.max(0.5, Math.min(exitTime, 2.0))
|
||||
}
|
||||
|
||||
// 获取随机弹幕位置(避免重叠)
|
||||
const getRandomDanmakuTop = (existingItems, currentIndex) => {
|
||||
// 使用实际容器高度,如果没有则使用默认值
|
||||
const containerHeight = danmakuContainer.value?.clientHeight || DANMAKU_CONTAINER_HEIGHT
|
||||
const itemHeight = DANMAKU_ITEM_HEIGHT
|
||||
const minSpacing = DANMAKU_MIN_SPACING
|
||||
const padding = 4 // 上下边距
|
||||
|
||||
// 可用空间:容器高度减去上下边距和弹幕高度
|
||||
const availableHeight = Math.max(0, containerHeight - itemHeight - padding * 2)
|
||||
|
||||
if (availableHeight <= 0) {
|
||||
// 如果可用空间不足,返回中间位置
|
||||
return containerHeight / 2 - itemHeight / 2
|
||||
}
|
||||
|
||||
// 获取当前正在显示的弹幕位置(用于碰撞检测)
|
||||
// 只考虑最近显示的弹幕,因为旧的弹幕已经滚出屏幕
|
||||
const activeTops = existingItems
|
||||
.filter(item => item.randomTop !== undefined)
|
||||
.slice(-5) // 只考虑最近5条弹幕,避免过度限制
|
||||
.map(item => item.randomTop)
|
||||
.sort((a, b) => a - b)
|
||||
|
||||
// 尝试找到不重叠的位置
|
||||
let attempts = 0
|
||||
let top = 0
|
||||
|
||||
while (attempts < 20) {
|
||||
// 在可用空间内随机生成位置
|
||||
top = padding + Math.random() * availableHeight
|
||||
|
||||
// 检查是否与现有弹幕重叠
|
||||
const hasOverlap = activeTops.some(existingTop => {
|
||||
const distance = Math.abs(top - existingTop)
|
||||
return distance < minSpacing
|
||||
})
|
||||
|
||||
if (!hasOverlap) {
|
||||
break
|
||||
}
|
||||
|
||||
attempts++
|
||||
}
|
||||
|
||||
// 如果20次尝试都失败,使用简单的随机位置(但尽量分散)
|
||||
if (attempts >= 20) {
|
||||
// 将空间分成多个区域,随机选择一个区域
|
||||
const zones = 4 // 分成4个区域
|
||||
const zoneHeight = availableHeight / zones
|
||||
const selectedZone = Math.floor(Math.random() * zones)
|
||||
top = padding + selectedZone * zoneHeight + Math.random() * zoneHeight * 0.6
|
||||
}
|
||||
|
||||
return Math.max(padding, Math.min(top, containerHeight - itemHeight - padding))
|
||||
}
|
||||
|
||||
// 获取弹幕样式 - 每条弹幕独立滚动,速度一致,位置随机
|
||||
const getDanmakuStyle = (item, index) => {
|
||||
if (!danmakuContainer.value) {
|
||||
return {}
|
||||
}
|
||||
|
||||
// 使用已计算的随机top位置,如果没有则生成一个
|
||||
const top = item.randomTop !== undefined
|
||||
? item.randomTop
|
||||
: getRandomDanmakuTop(danmakuItems.value, index)
|
||||
|
||||
// 使用已计算的延迟时间,如果没有则使用默认值
|
||||
const delay = item.displayDelay !== undefined ? item.displayDelay : 0
|
||||
|
||||
// 使用已计算的显示顺序,如果没有则使用索引
|
||||
const zIndex = 1000 + (item.displayOrder || index)
|
||||
|
||||
return {
|
||||
top: `${top}px`,
|
||||
left: '100%', // 从右侧开始
|
||||
animationDelay: `${delay}s`,
|
||||
animationDuration: '15s', // 统一的滚动速度
|
||||
zIndex: zIndex // 先出现的弹幕层级更高,避免后面的遮盖前面的
|
||||
}
|
||||
}
|
||||
|
||||
// 获取弹幕类名
|
||||
const getDanmakuClass = (item) => {
|
||||
// 根据用户ID生成一致的颜色
|
||||
const colors = [
|
||||
'border-purple-300 bg-purple-50',
|
||||
'border-blue-300 bg-blue-50',
|
||||
'border-green-300 bg-green-50',
|
||||
'border-yellow-300 bg-yellow-50',
|
||||
'border-red-300 bg-red-50',
|
||||
'border-indigo-300 bg-indigo-50',
|
||||
'border-pink-300 bg-pink-50',
|
||||
'border-teal-300 bg-teal-50'
|
||||
]
|
||||
const index = (item.userId?.charCodeAt(0) || 0) % colors.length
|
||||
return colors[index]
|
||||
}
|
||||
|
||||
// 获取用户头像类名
|
||||
const getUserAvatarClass = (userId) => {
|
||||
// 根据用户ID生成一致的颜色
|
||||
const colors = [
|
||||
'bg-purple-500',
|
||||
'bg-blue-500',
|
||||
'bg-green-500',
|
||||
'bg-yellow-500',
|
||||
'bg-red-500',
|
||||
'bg-indigo-500',
|
||||
'bg-pink-500',
|
||||
'bg-teal-500'
|
||||
]
|
||||
const index = (userId?.charCodeAt(0) || 0) % colors.length
|
||||
return colors[index]
|
||||
}
|
||||
|
||||
// 获取用户名首字母
|
||||
const getUsernameInitial = (username) => {
|
||||
if (!username) return '?'
|
||||
// 如果是中文,取第一个字符;如果是英文,取首字母大写
|
||||
const firstChar = username.charAt(0)
|
||||
if (/[\u4e00-\u9fa5]/.test(firstChar)) {
|
||||
return firstChar
|
||||
}
|
||||
return firstChar.toUpperCase()
|
||||
}
|
||||
|
||||
// 格式化相对时间
|
||||
const formatRelativeTime = (timeStr) => {
|
||||
if (!timeStr) return '刚刚'
|
||||
|
||||
const now = new Date()
|
||||
const time = new Date(timeStr)
|
||||
const diff = Math.floor((now - time) / 1000) // 秒数差
|
||||
|
||||
if (diff < 60) {
|
||||
return '刚刚'
|
||||
} else if (diff < 3600) {
|
||||
return `${Math.floor(diff / 60)}分钟前`
|
||||
} else if (diff < 86400) {
|
||||
return `${Math.floor(diff / 3600)}小时前`
|
||||
} else {
|
||||
return formatTime(timeStr)
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停弹幕滚动
|
||||
const pauseDanmaku = (itemId) => {
|
||||
pausedDanmakuIds.value.add(itemId)
|
||||
}
|
||||
|
||||
// 恢复弹幕滚动
|
||||
const resumeDanmaku = (itemId) => {
|
||||
pausedDanmakuIds.value.delete(itemId)
|
||||
}
|
||||
|
||||
// 处理并显示记录 - 实时模式:立即显示新记录
|
||||
const processAndDisplayRecords = async (records) => {
|
||||
if (!records || records.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
let nextDisplayTime = now
|
||||
|
||||
// 处理每条记录
|
||||
records.forEach((record, index) => {
|
||||
const username = getDisplayUsername(record)
|
||||
const apiName = getApiName(record)
|
||||
const userId = record.user_id || record.userId || 'unknown'
|
||||
const createdAt = record.created_at || record.createdAt || new Date().toISOString()
|
||||
const productId = record.product_id || record.productId || ''
|
||||
|
||||
// 使用记录ID作为唯一标识
|
||||
const recordId = record.id || `${userId}_${productId}_${createdAt}_${index}_${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
// 计算调用次数
|
||||
const callCount = getCallCount(record)
|
||||
|
||||
// 计算随机top位置(避免重叠)
|
||||
const randomTop = getRandomDanmakuTop(danmakuItems.value, danmakuItems.value.length + index)
|
||||
|
||||
// 计算显示延迟:确保弹幕不会重叠
|
||||
let displayDelay = 0
|
||||
if (danmakuItems.value.length > 0) {
|
||||
// 计算最后一条弹幕的退出时间
|
||||
const lastItem = danmakuItems.value[danmakuItems.value.length - 1]
|
||||
const lastItemStartTime = lastItem.actualDisplayTime || now
|
||||
const exitRightTime = getDanmakuExitRightTime() * 1000
|
||||
const lastItemExitTime = lastItemStartTime + exitRightTime
|
||||
|
||||
// 如果最后一条弹幕还没退出,需要等待
|
||||
if (lastItemExitTime > now) {
|
||||
displayDelay = (lastItemExitTime - now) / 1000
|
||||
} else {
|
||||
// 否则可以立即显示,但添加小随机延迟避免重叠
|
||||
displayDelay = Math.random() * 0.3
|
||||
}
|
||||
}
|
||||
|
||||
// 创建弹幕项
|
||||
const danmakuItem = {
|
||||
id: recordId,
|
||||
userId: userId,
|
||||
username: username,
|
||||
apiName: apiName,
|
||||
productId: productId,
|
||||
createdAt: createdAt,
|
||||
timestamp: new Date(createdAt).getTime(),
|
||||
callCount: callCount,
|
||||
clientIp: record.client_ip || record.clientIp || '',
|
||||
status: record.status || '',
|
||||
cost: record.cost || '',
|
||||
transactionId: record.transaction_id || record.transactionId || '',
|
||||
displayDelay: displayDelay,
|
||||
displayOrder: danmakuDisplayOrder.value++,
|
||||
randomTop: randomTop,
|
||||
actualDisplayTime: now + (displayDelay * 1000)
|
||||
}
|
||||
|
||||
// 延迟显示或立即显示
|
||||
if (displayDelay > 0) {
|
||||
setTimeout(() => {
|
||||
danmakuItems.value.push(danmakuItem)
|
||||
// 限制弹幕数量
|
||||
if (danmakuItems.value.length > MAX_DANMAKU_ITEMS) {
|
||||
danmakuItems.value = danmakuItems.value.slice(-MAX_DANMAKU_ITEMS)
|
||||
}
|
||||
}, displayDelay * 1000)
|
||||
} else {
|
||||
danmakuItems.value.push(danmakuItem)
|
||||
// 限制弹幕数量
|
||||
if (danmakuItems.value.length > MAX_DANMAKU_ITEMS) {
|
||||
danmakuItems.value = danmakuItems.value.slice(-MAX_DANMAKU_ITEMS)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 弹幕动画结束回调 - 从显示列表中移除已播放完成的弹幕
|
||||
const onDanmakuAnimationEnd = (itemId) => {
|
||||
// 只有当弹幕不在暂停状态时才移除(避免鼠标悬停时误删)
|
||||
if (!pausedDanmakuIds.value.has(itemId)) {
|
||||
const index = danmakuItems.value.findIndex(item => item.id === itemId)
|
||||
if (index !== -1) {
|
||||
danmakuItems.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 启动弹幕系统 - 实时模式
|
||||
const startDanmakuPolling = () => {
|
||||
// 立即查询一次数据库
|
||||
loadDanmakuData()
|
||||
|
||||
// 定时查询数据库,查询到新记录立即显示
|
||||
danmakuPollingTimer.value = setInterval(() => {
|
||||
loadDanmakuData()
|
||||
}, DANMAKU_POLLING_INTERVAL)
|
||||
}
|
||||
|
||||
// 停止弹幕系统
|
||||
const stopDanmakuPolling = () => {
|
||||
if (danmakuPollingTimer.value) {
|
||||
clearInterval(danmakuPollingTimer.value)
|
||||
danmakuPollingTimer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initDefaultDateRange()
|
||||
loadSystemStatistics()
|
||||
startDanmakuPolling()
|
||||
})
|
||||
|
||||
// 组件卸载时清理
|
||||
@@ -1838,44 +1337,8 @@ onUnmounted(() => {
|
||||
}
|
||||
})
|
||||
chartInstances.value = []
|
||||
|
||||
// 停止弹幕轮询
|
||||
stopDanmakuPolling()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 弹幕滚动动画 - 从左到右,速度一致,只播放一次 */
|
||||
.danmaku-item {
|
||||
animation: danmakuScroll 15s linear forwards;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* 暂停弹幕动画 */
|
||||
.danmaku-item.danmaku-paused {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@keyframes danmakuScroll {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
opacity: 0;
|
||||
}
|
||||
2% {
|
||||
opacity: 1;
|
||||
}
|
||||
98% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(calc(-100vw - 100%));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹幕容器 */
|
||||
.danmaku-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user