弹幕
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-3 flex flex-col" style="min-height: 120px;">
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-3 flex flex-col" style="min-height: 100px; height: 100px;">
|
||||||
<div class="flex items-center justify-between mb-2 pb-2 border-b border-gray-100">
|
<div class="flex items-center justify-between mb-2 pb-2 border-b border-gray-100">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<el-icon size="16" class="mr-2">
|
<el-icon size="16" class="mr-2">
|
||||||
@@ -7,25 +7,15 @@
|
|||||||
</el-icon>
|
</el-icon>
|
||||||
<h3 class="text-sm font-semibold text-gray-800">实时动态</h3>
|
<h3 class="text-sm font-semibold text-gray-800">实时动态</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<el-switch
|
|
||||||
v-model="enabled"
|
|
||||||
@change="handleToggle"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<span class="text-xs text-gray-500">{{ enabled ? '开启' : '关闭' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div ref="danmakuWrapper" class="relative flex-1 overflow-hidden" style="min-height: 120px; max-height: 140px;">
|
<div ref="danmakuWrapper" class="relative flex-1 overflow-hidden" style="height: 100px;">
|
||||||
<div
|
<div
|
||||||
v-for="danmaku in activeDanmakus"
|
v-for="danmaku in activeDanmakus"
|
||||||
:key="danmaku.id"
|
:key="danmaku.id"
|
||||||
:data-danmaku-id="danmaku.id"
|
:data-danmaku-id="danmaku.id"
|
||||||
:class="['danmaku-item', `danmaku-${danmaku.status}`]"
|
:class="['danmaku-item', `danmaku-${danmaku.status}`]"
|
||||||
:style="{
|
:style="{
|
||||||
top: `${danmaku.top}px`,
|
animationDuration: `${danmaku.duration}ms`
|
||||||
animationDuration: `${danmaku.duration}ms`,
|
|
||||||
animationDelay: `${danmaku.delay}ms`
|
|
||||||
}"
|
}"
|
||||||
@animationend="handleAnimationEnd(danmaku.id)"
|
@animationend="handleAnimationEnd(danmaku.id)"
|
||||||
>
|
>
|
||||||
@@ -40,407 +30,252 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { apiCallApi } from '@/api'
|
import { apiCallApi } from '@/api'
|
||||||
import { TrendCharts } from '@element-plus/icons-vue'
|
import { TrendCharts } from '@element-plus/icons-vue'
|
||||||
import { onMounted, onUnmounted, ref } from 'vue'
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
refreshInterval: {
|
|
||||||
type: Number,
|
|
||||||
default: 5000 // 默认5秒刷新一次
|
|
||||||
},
|
|
||||||
maxDanmakus: {
|
|
||||||
type: Number,
|
|
||||||
default: 20 // 最多显示20条弹幕
|
|
||||||
},
|
|
||||||
danmakuSpeed: {
|
|
||||||
type: Number,
|
|
||||||
default: 15000 // 弹幕滚动速度(毫秒),更慢一些让用户能看到
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const enabled = ref(true)
|
|
||||||
const danmakuWrapper = ref(null)
|
|
||||||
const activeDanmakus = ref([])
|
|
||||||
const danmakuQueue = ref([])
|
|
||||||
const pendingQueue = ref([]) // 待处理的弹幕队列
|
|
||||||
const lastFetchTime = ref(null)
|
|
||||||
const fetchTimer = ref(null)
|
|
||||||
const processedIds = ref(new Set()) // 已处理的记录ID,避免重复显示
|
|
||||||
const isProcessing = ref(false) // 是否正在处理弹幕
|
|
||||||
|
|
||||||
// 计算时间差(多少分钟前)
|
|
||||||
const calculateTimeAgo = (timeStr) => {
|
|
||||||
if (!timeStr) return '刚刚'
|
|
||||||
|
|
||||||
try {
|
const props = defineProps({
|
||||||
const time = new Date(timeStr)
|
refreshInterval: {
|
||||||
const now = new Date()
|
type: Number,
|
||||||
const diff = Math.floor((now - time) / 1000 / 60) // 分钟差
|
default: 3000 // 默认3秒刷新一次
|
||||||
|
},
|
||||||
if (diff < 1) return '刚刚'
|
danmakuSpeed: {
|
||||||
if (diff < 60) return `${diff}分钟前`
|
type: Number,
|
||||||
|
default: 15000 // 弹幕滚动速度(毫秒)
|
||||||
const hours = Math.floor(diff / 60)
|
|
||||||
if (hours < 24) return `${hours}小时前`
|
|
||||||
|
|
||||||
const days = Math.floor(hours / 24)
|
|
||||||
return `${days}天前`
|
|
||||||
} catch (e) {
|
|
||||||
return '刚刚'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取最新API调用记录
|
|
||||||
const fetchLatestApiCalls = async () => {
|
|
||||||
if (!enabled.value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const params = {
|
|
||||||
page: 1,
|
|
||||||
page_size: props.maxDanmakus,
|
|
||||||
sort_by: 'created_at',
|
|
||||||
sort_order: 'desc'
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 固定配置
|
||||||
|
const FETCH_PAGE_SIZE = 50 // 每次获取的记录数
|
||||||
|
const BASE_EMIT_INTERVAL = 5000 // 基础5秒,避免弹幕重叠
|
||||||
|
const RANDOM_EMIT_RANGE = 1000 // 0-1秒随机范围
|
||||||
|
|
||||||
|
const enabled = ref(true)
|
||||||
|
const danmakuWrapper = ref(null)
|
||||||
|
const activeDanmakus = ref([]) // 当前正在显示的弹幕
|
||||||
|
const pendingQueue = ref([]) // 待处理的弹幕队列
|
||||||
|
const lastFetchTime = ref(null)
|
||||||
|
const fetchTimer = ref(null)
|
||||||
|
const processedIds = ref(new Set()) // 已处理的记录ID,确保每条数据只显示一次
|
||||||
|
const emitTimer = ref(null) // 弹幕发射定时器
|
||||||
|
const timeUpdateTimer = ref(null) // 时间更新定时器
|
||||||
|
|
||||||
|
// 计算时间差(多少分钟前)
|
||||||
|
const calculateTimeAgo = (timeStr) => {
|
||||||
|
if (!timeStr) return '刚刚'
|
||||||
|
|
||||||
// 如果上次获取过数据,只获取更新的记录
|
try {
|
||||||
if (lastFetchTime.value) {
|
const time = new Date(timeStr)
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const oneMinuteAgo = new Date(now.getTime() - 60000) // 1分钟前
|
const diff = Math.floor((now - time) / 1000 / 60) // 分钟差
|
||||||
params.start_time = oneMinuteAgo.toISOString().replace('T', ' ').substring(0, 19)
|
|
||||||
|
if (diff < 1) return '刚刚'
|
||||||
|
if (diff < 60) return `${diff}分钟前`
|
||||||
|
|
||||||
|
const hours = Math.floor(diff / 60)
|
||||||
|
if (hours < 24) return `${hours}小时前`
|
||||||
|
|
||||||
|
const days = Math.floor(hours / 24)
|
||||||
|
return `${days}天前`
|
||||||
|
} catch (e) {
|
||||||
|
return '刚刚'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最新API调用记录
|
||||||
|
const fetchLatestApiCalls = async () => {
|
||||||
|
if (!enabled.value) return
|
||||||
|
|
||||||
const response = await apiCallApi.getAdminApiCalls(params)
|
try {
|
||||||
|
const params = {
|
||||||
// 处理响应数据,兼容不同的响应格式
|
page: 1,
|
||||||
let items = []
|
page_size: FETCH_PAGE_SIZE,
|
||||||
if (response && response.data) {
|
sort_by: 'created_at',
|
||||||
if (response.data.items) {
|
sort_order: 'desc'
|
||||||
items = response.data.items
|
|
||||||
} else if (Array.isArray(response.data)) {
|
|
||||||
items = response.data
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (items.length > 0) {
|
|
||||||
|
|
||||||
// 过滤已处理的记录,只添加新记录
|
// 如果上次获取过数据,只获取更新的记录
|
||||||
const newItems = items.filter(item => {
|
if (lastFetchTime.value) {
|
||||||
if (processedIds.value.has(item.id)) {
|
const now = new Date()
|
||||||
return false
|
const oneMinuteAgo = new Date(now.getTime() - 60000) // 1分钟前
|
||||||
|
params.start_time = oneMinuteAgo.toISOString().replace('T', ' ').substring(0, 19)
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await apiCallApi.getAdminApiCalls(params)
|
||||||
|
|
||||||
|
// 处理响应数据,兼容不同的响应格式
|
||||||
|
let items = []
|
||||||
|
if (response && response.data) {
|
||||||
|
if (response.data.items) {
|
||||||
|
items = response.data.items
|
||||||
|
} else if (Array.isArray(response.data)) {
|
||||||
|
items = response.data
|
||||||
}
|
}
|
||||||
processedIds.value.add(item.id)
|
}
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
// 将新记录添加到待处理队列
|
if (items.length > 0) {
|
||||||
newItems.forEach(item => {
|
// 过滤已处理过的记录,确保每条数据只显示一次
|
||||||
addToPendingQueue(item)
|
const newItems = items.filter(item => {
|
||||||
})
|
if (processedIds.value.has(item.id)) {
|
||||||
|
return false
|
||||||
// 开始处理队列
|
}
|
||||||
processPendingQueue()
|
processedIds.value.add(item.id)
|
||||||
|
return true
|
||||||
lastFetchTime.value = new Date()
|
})
|
||||||
|
|
||||||
|
// 按时间顺序排序(从旧到新),确保按顺序显示
|
||||||
|
newItems.sort((a, b) => {
|
||||||
|
const timeA = new Date(a.created_at || a.start_at || 0)
|
||||||
|
const timeB = new Date(b.created_at || b.start_at || 0)
|
||||||
|
return timeA - timeB
|
||||||
|
})
|
||||||
|
|
||||||
|
// 将新记录按顺序添加到待处理队列
|
||||||
|
newItems.forEach(item => {
|
||||||
|
addToPendingQueue(item)
|
||||||
|
})
|
||||||
|
|
||||||
|
lastFetchTime.value = new Date()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取API调用记录失败:', error)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取API调用记录失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加到待处理队列
|
|
||||||
const addToPendingQueue = (item) => {
|
|
||||||
pendingQueue.value.push(item)
|
|
||||||
|
|
||||||
// 如果队列太长,移除最旧的
|
|
||||||
if (pendingQueue.value.length > props.maxDanmakus * 2) {
|
|
||||||
pendingQueue.value.shift()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理待处理队列,每次弹幕间隔1.5秒+随机0-2秒
|
|
||||||
const processPendingQueue = () => {
|
|
||||||
if (isProcessing.value || pendingQueue.value.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isProcessing.value = true
|
// 添加到待处理队列
|
||||||
|
const addToPendingQueue = (item) => {
|
||||||
|
pendingQueue.value.push(item)
|
||||||
|
|
||||||
|
// 限制队列大小,避免内存溢出(最多保留150条)
|
||||||
|
if (pendingQueue.value.length > 150) {
|
||||||
|
pendingQueue.value.shift() // 移除最旧的一条
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const processNext = () => {
|
// 发射弹幕(按固定间隔连续发射,允许多条同时显示)
|
||||||
// 如果有弹幕实体还在当前弹幕区域内,则等待其完全离开后再装载下一条
|
const emitDanmaku = () => {
|
||||||
if (hasDanmakuInside()) {
|
if (!enabled.value) {
|
||||||
setTimeout(processNext, 300)
|
emitTimer.value = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从队列取出一条弹幕
|
||||||
if (pendingQueue.value.length === 0) {
|
if (pendingQueue.value.length === 0) {
|
||||||
isProcessing.value = false
|
// 队列为空,等待一段时间后重试
|
||||||
|
emitTimer.value = setTimeout(() => {
|
||||||
|
emitDanmaku()
|
||||||
|
}, 1000)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = pendingQueue.value.shift()
|
const item = pendingQueue.value.shift()
|
||||||
addDanmakuToQueue(item)
|
if (!item) {
|
||||||
|
emitTimer.value = setTimeout(() => {
|
||||||
// 计算下一个弹幕的延迟:1秒 + 0-1秒随机
|
emitDanmaku()
|
||||||
const delay = 1000 + Math.random() * 1000
|
}, 1000)
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
processNext()
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
processNext()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查弹幕碰撞(基于垂直位置和预计路径)
|
|
||||||
const checkCollision = (newDanmaku) => {
|
|
||||||
if (!danmakuWrapper.value) return false
|
|
||||||
|
|
||||||
const danmakuHeight = 40 // 弹幕高度
|
|
||||||
const minVerticalGap = 45 // 最小垂直间距(确保不重叠)
|
|
||||||
const newTop = newDanmaku.top
|
|
||||||
const newBottom = newTop + danmakuHeight
|
|
||||||
|
|
||||||
// 检查与所有活动弹幕的垂直位置碰撞
|
|
||||||
for (const existingDanmaku of activeDanmakus.value) {
|
|
||||||
const existingTop = existingDanmaku.top
|
|
||||||
const existingBottom = existingTop + danmakuHeight
|
|
||||||
|
|
||||||
// 垂直位置是否太接近(考虑最小间距)
|
|
||||||
const verticalOverlap = !(newBottom < existingTop - minVerticalGap || newTop > existingBottom + minVerticalGap)
|
|
||||||
|
|
||||||
if (verticalOverlap) {
|
|
||||||
return true // 发生碰撞
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加弹幕到显示队列(带碰撞检测)
|
|
||||||
const addDanmakuToQueue = (item) => {
|
|
||||||
const danmaku = {
|
|
||||||
id: item.id || `danmaku-${Date.now()}-${Math.random()}`,
|
|
||||||
companyName: item.company_name || item.user?.company_name || '未知企业',
|
|
||||||
productName: item.product_name || '未知产品',
|
|
||||||
productId: item.product_id || 'N/A',
|
|
||||||
status: item.status || 'pending',
|
|
||||||
startAt: item.start_at || item.created_at,
|
|
||||||
timeAgo: calculateTimeAgo(item.start_at || item.created_at),
|
|
||||||
top: 0,
|
|
||||||
duration: props.danmakuSpeed,
|
|
||||||
delay: 0,
|
|
||||||
width: 0, // 将在DOM渲染后计算
|
|
||||||
currentLeft: 0 // 当前水平位置
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试找到一个不碰撞的位置
|
|
||||||
let attempts = 0
|
|
||||||
let foundPosition = false
|
|
||||||
|
|
||||||
while (attempts < 20 && !foundPosition) {
|
|
||||||
// 计算垂直位置
|
|
||||||
danmaku.top = calculateTopPosition()
|
|
||||||
|
|
||||||
// 检查碰撞
|
|
||||||
if (!checkCollision(danmaku)) {
|
|
||||||
foundPosition = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
attempts++
|
|
||||||
|
|
||||||
// 如果碰撞,稍微调整垂直位置
|
|
||||||
if (attempts < 10) {
|
|
||||||
// 尝试不同的垂直位置
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
// 如果多次尝试都碰撞,延迟生成
|
|
||||||
setTimeout(() => {
|
|
||||||
addDanmakuToQueue(item)
|
|
||||||
}, 500 + Math.random() * 1000)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// 创建弹幕对象(使用唯一ID确保不重复)
|
||||||
if (!foundPosition) {
|
const danmaku = {
|
||||||
// 如果找不到位置,延迟生成
|
id: `danmaku-${item.id}-${Date.now()}-${Math.random()}`,
|
||||||
setTimeout(() => {
|
companyName: item.company_name || item.user?.company_name || '未知企业',
|
||||||
addDanmakuToQueue(item)
|
productName: item.product_name || '未知产品',
|
||||||
}, 1000)
|
status: item.status || 'pending',
|
||||||
return
|
startAt: item.start_at || item.created_at,
|
||||||
}
|
timeAgo: calculateTimeAgo(item.start_at || item.created_at),
|
||||||
|
duration: props.danmakuSpeed
|
||||||
// 暂存队列(保持逻辑完整,便于未来扩展)
|
}
|
||||||
danmakuQueue.value.push(danmaku)
|
|
||||||
|
// 添加到活跃弹幕列表(立即开始动画)
|
||||||
// 如果队列中的弹幕太多,移除最旧的
|
|
||||||
if (danmakuQueue.value.length > props.maxDanmakus * 2) {
|
|
||||||
danmakuQueue.value.shift()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 仅在右侧区域空闲时添加
|
|
||||||
if (!hasDanmakuInside()) {
|
|
||||||
activeDanmakus.value.push(danmaku)
|
activeDanmakus.value.push(danmaku)
|
||||||
} else {
|
|
||||||
// 如果右侧区域有弹幕,稍后再尝试
|
// 安排下一条弹幕的发射(随机间隔:5s + 0-1s)
|
||||||
const tryAdd = () => {
|
const interval = BASE_EMIT_INTERVAL + Math.random() * RANDOM_EMIT_RANGE
|
||||||
if (!hasDanmakuInside()) {
|
emitTimer.value = setTimeout(() => {
|
||||||
activeDanmakus.value.push(danmaku)
|
emitDanmaku()
|
||||||
} else {
|
}, interval)
|
||||||
setTimeout(tryAdd, 200)
|
}
|
||||||
|
|
||||||
|
// 启动弹幕发射系统
|
||||||
|
const startEmissionSystem = () => {
|
||||||
|
if (emitTimer.value) {
|
||||||
|
clearTimeout(emitTimer.value)
|
||||||
|
}
|
||||||
|
emitDanmaku() // 开始第一次发射
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理动画结束事件(弹幕移出屏幕时移除)
|
||||||
|
const handleAnimationEnd = (id) => {
|
||||||
|
const index = activeDanmakus.value.findIndex(d => d.id === id)
|
||||||
|
if (index > -1) {
|
||||||
|
activeDanmakus.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新所有弹幕的时间显示
|
||||||
|
const updateTimeAgo = () => {
|
||||||
|
activeDanmakus.value.forEach(danmaku => {
|
||||||
|
if (danmaku.startAt) {
|
||||||
|
danmaku.timeAgo = calculateTimeAgo(danmaku.startAt)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
setTimeout(tryAdd, 200)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 弹幕会在动画结束时通过 animationend 事件自动移除
|
// 开始获取数据
|
||||||
}
|
const startFetching = () => {
|
||||||
|
// 立即获取一次
|
||||||
// 处理动画结束事件
|
fetchLatestApiCalls()
|
||||||
const handleAnimationEnd = (id) => {
|
|
||||||
// 动画结束后,弹幕已经完全移出左侧,可以安全移除
|
|
||||||
removeDanmaku(id)
|
|
||||||
// 释放占用,继续处理待排队的弹幕
|
|
||||||
isProcessing.value = false
|
|
||||||
processPendingQueue()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断当前弹幕区域内是否还有弹幕实体(交叠到可视区域)
|
|
||||||
const hasDanmakuInside = () => {
|
|
||||||
if (!danmakuWrapper.value) return false
|
|
||||||
const wrapperRect = danmakuWrapper.value.getBoundingClientRect()
|
|
||||||
const elements = danmakuWrapper.value.querySelectorAll('.danmaku-item')
|
|
||||||
for (const el of elements) {
|
|
||||||
const rect = el.getBoundingClientRect()
|
|
||||||
// 判断是否与可视区域有交叠(水平和垂直都需有重叠)
|
|
||||||
const horizontalOverlap = rect.right > wrapperRect.left && rect.left < wrapperRect.right
|
|
||||||
const verticalOverlap = rect.bottom > wrapperRect.top && rect.top < wrapperRect.bottom
|
|
||||||
if (horizontalOverlap && verticalOverlap) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算弹幕的垂直位置(随机分散,避免重叠)
|
|
||||||
const calculateTopPosition = () => {
|
|
||||||
const containerHeight = 80 // 容器高度80px
|
|
||||||
const minTop = 10 // 最小顶部距离
|
|
||||||
const maxTop = containerHeight - 40 // 最大顶部距离(留出弹幕高度空间)
|
|
||||||
|
|
||||||
// 获取当前活动的弹幕位置
|
|
||||||
const activePositions = activeDanmakus.value.map(d => d.top).filter(pos => pos > 0)
|
|
||||||
|
|
||||||
// 尝试找到一个不重叠的位置
|
|
||||||
let newTop
|
|
||||||
let attempts = 0
|
|
||||||
const minGap = 35 // 最小间距35px
|
|
||||||
|
|
||||||
do {
|
|
||||||
// 在容器高度范围内随机生成位置
|
|
||||||
newTop = minTop + Math.random() * (maxTop - minTop)
|
|
||||||
attempts++
|
|
||||||
|
|
||||||
// 检查是否与现有弹幕重叠
|
// 设置定时器定期获取数据
|
||||||
const tooClose = activePositions.some(pos => Math.abs(pos - newTop) < minGap)
|
if (fetchTimer.value) {
|
||||||
|
clearInterval(fetchTimer.value)
|
||||||
|
}
|
||||||
|
fetchTimer.value = setInterval(() => {
|
||||||
|
fetchLatestApiCalls()
|
||||||
|
}, props.refreshInterval)
|
||||||
|
|
||||||
if (!tooClose || attempts > 10) {
|
// 启动弹幕发射系统
|
||||||
break
|
startEmissionSystem()
|
||||||
|
|
||||||
|
// 定时更新"多少分钟前"
|
||||||
|
if (timeUpdateTimer.value) {
|
||||||
|
clearInterval(timeUpdateTimer.value)
|
||||||
}
|
}
|
||||||
} while (attempts < 10)
|
timeUpdateTimer.value = setInterval(() => {
|
||||||
|
if (enabled.value) {
|
||||||
|
updateTimeAgo()
|
||||||
|
}
|
||||||
|
}, 60000) // 每分钟更新一次时间显示
|
||||||
|
}
|
||||||
|
|
||||||
// 如果尝试10次还是重叠,就使用一个固定但分散的位置
|
// 停止获取数据
|
||||||
if (attempts >= 10) {
|
const stopFetching = () => {
|
||||||
const positions = [20, 50] // 两行固定位置
|
if (fetchTimer.value) {
|
||||||
const usedPositions = activePositions.filter(pos =>
|
clearInterval(fetchTimer.value)
|
||||||
positions.some(fixedPos => Math.abs(pos - fixedPos) < minGap)
|
fetchTimer.value = null
|
||||||
)
|
}
|
||||||
if (usedPositions.length < positions.length) {
|
if (emitTimer.value) {
|
||||||
newTop = positions.find(pos =>
|
clearTimeout(emitTimer.value)
|
||||||
!activePositions.some(used => Math.abs(used - pos) < minGap)
|
emitTimer.value = null
|
||||||
) || positions[0]
|
}
|
||||||
} else {
|
if (timeUpdateTimer.value) {
|
||||||
newTop = minTop + Math.random() * (maxTop - minTop)
|
clearInterval(timeUpdateTimer.value)
|
||||||
|
timeUpdateTimer.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.round(newTop)
|
onMounted(() => {
|
||||||
}
|
if (enabled.value) {
|
||||||
|
startFetching()
|
||||||
// 移除弹幕
|
|
||||||
const removeDanmaku = (id) => {
|
|
||||||
const index = activeDanmakus.value.findIndex(d => d.id === id)
|
|
||||||
if (index > -1) {
|
|
||||||
activeDanmakus.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const queueIndex = danmakuQueue.value.findIndex(d => d.id === id)
|
|
||||||
if (queueIndex > -1) {
|
|
||||||
danmakuQueue.value.splice(queueIndex, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新所有弹幕的时间显示
|
|
||||||
const updateTimeAgo = () => {
|
|
||||||
activeDanmakus.value.forEach(danmaku => {
|
|
||||||
if (danmaku.startAt) {
|
|
||||||
danmaku.timeAgo = calculateTimeAgo(danmaku.startAt)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
onUnmounted(() => {
|
||||||
// 处理开关切换
|
|
||||||
const handleToggle = (value) => {
|
|
||||||
if (value) {
|
|
||||||
startFetching()
|
|
||||||
} else {
|
|
||||||
stopFetching()
|
stopFetching()
|
||||||
}
|
})
|
||||||
}
|
</script>
|
||||||
|
|
||||||
// 开始获取数据
|
|
||||||
const startFetching = () => {
|
|
||||||
// 立即获取一次
|
|
||||||
fetchLatestApiCalls()
|
|
||||||
|
|
||||||
// 设置定时器
|
|
||||||
if (fetchTimer.value) {
|
|
||||||
clearInterval(fetchTimer.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchTimer.value = setInterval(() => {
|
|
||||||
fetchLatestApiCalls()
|
|
||||||
}, props.refreshInterval)
|
|
||||||
|
|
||||||
// 定时更新"多少分钟前"
|
|
||||||
setInterval(() => {
|
|
||||||
if (enabled.value) {
|
|
||||||
updateTimeAgo()
|
|
||||||
}
|
|
||||||
}, 60000) // 每分钟更新一次时间显示
|
|
||||||
}
|
|
||||||
|
|
||||||
// 停止获取数据
|
|
||||||
const stopFetching = () => {
|
|
||||||
if (fetchTimer.value) {
|
|
||||||
clearInterval(fetchTimer.value)
|
|
||||||
fetchTimer.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (enabled.value) {
|
|
||||||
startFetching()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
stopFetching()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.danmaku-item {
|
.danmaku-item {
|
||||||
@@ -450,16 +285,16 @@ onUnmounted(() => {
|
|||||||
animation: danmaku-scroll linear forwards;
|
animation: danmaku-scroll linear forwards;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
/* 弹幕从右侧隐藏区域开始(容器外),由动画控制位置 */
|
top: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes danmaku-scroll {
|
@keyframes danmaku-scroll {
|
||||||
from {
|
from {
|
||||||
transform: translateX(100%);
|
transform: translate(100%, -50%);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
transform: translateX(-300%);
|
transform: translate(-200%, -50%);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user