This commit is contained in:
@@ -122,7 +122,7 @@
|
||||
<h3 class="text-sm font-semibold text-gray-800">实时API调用动态</h3>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
每5秒更新 · 最多显示100条
|
||||
队列缓冲 · 最多显示100条
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -143,14 +143,14 @@
|
||||
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-3 py-1.5 whitespace-nowrap cursor-pointer hover:shadow-md transition-shadow"
|
||||
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-2">
|
||||
<div class="flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-white text-xs font-bold"
|
||||
<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>
|
||||
@@ -582,20 +582,38 @@ const apiPopularityData = ref([])
|
||||
// 今日认证企业数据
|
||||
const todayCertifiedEnterprises = ref([])
|
||||
|
||||
// 弹幕相关数据
|
||||
const danmakuItems = ref([])
|
||||
// 弹幕相关数据(队列FIFO)
|
||||
const danmakuItems = ref([]) // 正在显示的弹幕列表
|
||||
const danmakuQueue = ref([]) // 待显示的弹幕队列(从数据库查询到的新记录先存入此队列)
|
||||
const danmakuContainer = ref(null)
|
||||
const danmakuPollingTimer = ref(null)
|
||||
const firstLoadDisplayTimer = ref(null) // 首次加载弹幕显示的定时器
|
||||
const lastFetchTime = ref(null)
|
||||
const danmakuPollingTimer = ref(null) // 数据库查询定时器
|
||||
const danmakuDisplayTimer = ref(null) // 从队列取出弹幕显示的定时器
|
||||
const lastEnqueuedId = ref(null) // 最后入队记录的ID(用于查询新记录,比时间戳更可靠)
|
||||
const lastEnqueueTime = ref(null) // 最后入队记录的时间点(备用)
|
||||
const isFirstLoad = ref(true) // 标记是否是首次加载
|
||||
const firstLoadItems = ref([]) // 首次加载的弹幕项(用于逐个显示)
|
||||
const firstLoadDisplayIndex = ref(0) // 首次加载弹幕的显示索引
|
||||
// 缓存每个用户-产品组合的调用次数,避免重复查询
|
||||
const callCountCache = ref(new Map()) // key: `${userId}_${productId}_${createdAt}`, value: callCount
|
||||
const MAX_DANMAKU_ITEMS = 100
|
||||
const callCountCache = ref(new Map()) // key: `${userId}_${productId}`, value: { count: number, lastUpdated: timestamp }
|
||||
const processedRecordIds = ref(new Set()) // 已处理的记录ID集合,用于去重
|
||||
const MAX_DANMAKU_ITEMS = 100 // 最多显示的弹幕数量
|
||||
const MAX_QUEUE_SIZE = 200 // 队列最大容量
|
||||
const DANMAKU_POLLING_INTERVAL = 5000 // 数据库查询间隔(毫秒)- 5秒,降低查询频率
|
||||
const DANMAKU_DISPLAY_INTERVAL = 800 // 从队列取出弹幕显示的间隔(毫秒)- 0.8秒,控制显示速度
|
||||
// 暂停的弹幕ID集合
|
||||
const pausedDanmakuIds = ref(new Set())
|
||||
// 跟踪每行最后弹幕的显示时间,用于避免同一行弹幕迸发(保留用于延迟计算)
|
||||
const lastRowDisplayTime = ref([0, 0]) // [第0行最后显示时间, 第1行最后显示时间]
|
||||
const danmakuDisplayOrder = ref(0) // 弹幕显示顺序,用于z-index
|
||||
// 弹幕尺寸配置
|
||||
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 DANMAKU_DELAY_REDUCTION_FACTOR = 0.6 // 延迟缩减系数,让弹幕显示更紧凑
|
||||
// 错误重试相关
|
||||
const danmakuRetryCount = ref(0) // 连续失败次数
|
||||
const MAX_RETRY_COUNT = 3 // 最大重试次数
|
||||
const RETRY_DELAY = 2000 // 重试延迟(毫秒)
|
||||
|
||||
|
||||
// 快捷选择配置
|
||||
@@ -1427,40 +1445,122 @@ const formatDateTime = (date) => {
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
// 加载弹幕数据
|
||||
// 批量查询调用次数(优化性能)
|
||||
const batchQueryCallCounts = async (records) => {
|
||||
// 按用户-产品分组,并为每个记录计算调用次数
|
||||
const callCountMap = new Map()
|
||||
|
||||
// 为每条记录单独计算调用次数(简化处理,避免复杂的批量查询)
|
||||
// 使用缓存优化性能
|
||||
const callCountPromises = records.map(async (record) => {
|
||||
const userId = record.user_id || record.userId || 'unknown'
|
||||
const productId = record.product_id || record.productId || ''
|
||||
const createdAt = record.created_at || record.createdAt || new Date().toISOString()
|
||||
const recordId = record.id || `${userId}_${productId}_${createdAt}`
|
||||
|
||||
if (userId === 'unknown' || !productId) {
|
||||
return { recordId, callCount: 1 }
|
||||
}
|
||||
|
||||
// 检查缓存(使用更精确的缓存键:用户+产品+时间)
|
||||
const cacheKey = `${userId}_${productId}`
|
||||
const cached = callCountCache.value.get(cacheKey)
|
||||
|
||||
// 如果缓存存在且时间在1分钟内,使用缓存值并递增
|
||||
if (cached && Date.now() - cached.lastUpdated < 60000) {
|
||||
const callCount = cached.count + 1
|
||||
// 更新缓存
|
||||
callCountCache.value.set(cacheKey, {
|
||||
count: callCount,
|
||||
lastUpdated: Date.now()
|
||||
})
|
||||
return { recordId, callCount }
|
||||
}
|
||||
|
||||
try {
|
||||
// 查询该用户对该产品在创建时间之前的调用记录数量
|
||||
const countParams = {
|
||||
page: 1,
|
||||
page_size: 100, // 获取足够多的记录以便统计
|
||||
user_id: userId,
|
||||
end_time: formatDateTime(new Date(createdAt)),
|
||||
sort_by: 'created_at',
|
||||
sort_order: 'desc'
|
||||
}
|
||||
|
||||
const countResponse = await apiCallApi.getAdminApiCalls(countParams)
|
||||
if (countResponse.success && countResponse.data?.items) {
|
||||
// 在客户端过滤出该产品的记录
|
||||
const productCalls = countResponse.data.items.filter(item => {
|
||||
const itemProductId = item.product_id || item.productId || ''
|
||||
const itemCreatedAt = item.created_at || item.createdAt || ''
|
||||
if (itemProductId === productId) {
|
||||
if (itemCreatedAt && createdAt) {
|
||||
return new Date(itemCreatedAt) <= new Date(createdAt)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const callCount = productCalls.length + 1 // +1 包括当前记录
|
||||
|
||||
// 更新缓存
|
||||
callCountCache.value.set(cacheKey, {
|
||||
count: callCount,
|
||||
lastUpdated: Date.now()
|
||||
})
|
||||
|
||||
return { recordId, callCount }
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('查询调用次数失败:', err, { userId, productId })
|
||||
}
|
||||
|
||||
return { recordId, callCount: 1 }
|
||||
})
|
||||
|
||||
const results = await Promise.all(callCountPromises)
|
||||
results.forEach(({ recordId, callCount }) => {
|
||||
callCountMap.set(recordId, callCount)
|
||||
})
|
||||
|
||||
return callCountMap
|
||||
}
|
||||
|
||||
// 加载弹幕数据(只负责查询数据库并将新记录存入队列)
|
||||
const loadDanmakuData = async () => {
|
||||
try {
|
||||
const now = new Date()
|
||||
let params = {}
|
||||
|
||||
if (isFirstLoad.value) {
|
||||
// 首次加载:获取最近3条记录,并计算每条记录的调用次数
|
||||
// 首次加载:获取最近30条记录
|
||||
params = {
|
||||
page: 1,
|
||||
page_size: 3,
|
||||
page_size: 30,
|
||||
sort_by: 'created_at',
|
||||
sort_order: 'desc'
|
||||
}
|
||||
} else {
|
||||
// 持续轮询:获取自上次获取时间之后的新记录
|
||||
if (!lastFetchTime.value) {
|
||||
// 如果没有上次获取时间,获取最近1分钟的记录
|
||||
// 持续轮询:获取最新的记录
|
||||
if (!lastEnqueueTime.value) {
|
||||
// 如果没有上次入队时间,获取最近1分钟的记录
|
||||
const oneMinuteAgo = new Date(now.getTime() - 60 * 1000)
|
||||
params = {
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
page_size: 50,
|
||||
start_time: formatDateTime(oneMinuteAgo),
|
||||
end_time: formatDateTime(now),
|
||||
sort_by: 'created_at',
|
||||
sort_order: 'desc'
|
||||
}
|
||||
} else {
|
||||
// 获取自上次获取时间之后的新记录
|
||||
// 获取自上次入队时间之后的新记录
|
||||
const nextTime = new Date(lastEnqueueTime.value.getTime() + 1)
|
||||
params = {
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
start_time: formatDateTime(lastFetchTime.value),
|
||||
end_time: formatDateTime(now),
|
||||
page_size: 50,
|
||||
start_time: formatDateTime(nextTime),
|
||||
sort_by: 'created_at',
|
||||
sort_order: 'desc'
|
||||
}
|
||||
@@ -1469,70 +1569,59 @@ const loadDanmakuData = async () => {
|
||||
|
||||
const response = await apiCallApi.getAdminApiCalls(params)
|
||||
|
||||
// 重置重试计数(请求成功)
|
||||
danmakuRetryCount.value = 0
|
||||
|
||||
if (response.success && response.data?.items) {
|
||||
const records = response.data.items
|
||||
|
||||
// 将记录转换为弹幕项,并查询每个产品的累计调用次数
|
||||
const newItems = await Promise.all(records.map(async (record, index) => {
|
||||
// 过滤掉已处理的记录(使用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 > 1000) {
|
||||
const idsArray = Array.from(processedRecordIds.value)
|
||||
processedRecordIds.value = new Set(idsArray.slice(-500))
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if (newRecords.length === 0) {
|
||||
// 没有新记录,但更新lastEnqueueTime为查询到的最新记录时间
|
||||
if (records.length > 0) {
|
||||
const latestRecord = records[0]
|
||||
const latestTime = new Date(latestRecord.created_at || latestRecord.createdAt || now)
|
||||
if (!lastEnqueueTime.value || latestTime > lastEnqueueTime.value) {
|
||||
lastEnqueueTime.value = latestTime
|
||||
if (latestRecord.id) {
|
||||
lastEnqueuedId.value = latestRecord.id
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 批量查询调用次数(优化性能)
|
||||
const callCountMap = await batchQueryCallCounts(newRecords)
|
||||
|
||||
// 将记录转换为弹幕项并存入队列
|
||||
const newItems = newRecords.map((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}_${createdAt}_${index}`
|
||||
// 使用记录ID作为唯一标识(优先使用后端ID)
|
||||
const tempRecordId = record.id || `${userId}_${productId}_${createdAt}`
|
||||
const recordId = record.id || `${tempRecordId}_${index}_${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
// 查询该用户对该产品的调用次数(创建时间小于等于当前记录创建时间的记录数)
|
||||
// 为每条记录单独计算,确保显示正确的调用次数
|
||||
let callCount = 1
|
||||
if (productId && userId && userId !== 'unknown') {
|
||||
// 检查缓存
|
||||
const cacheKey = `${userId}_${productId}_${createdAt}`
|
||||
if (callCountCache.value.has(cacheKey)) {
|
||||
callCount = callCountCache.value.get(cacheKey)
|
||||
} else {
|
||||
try {
|
||||
// 查询该用户对该产品在数据库中创建时间小于等于当前记录创建时间的调用记录数量
|
||||
// 注意:后端管理端接口可能不支持 product_id,我们通过查询所有记录然后过滤
|
||||
const countParams = {
|
||||
page: 1,
|
||||
page_size: 1000, // 获取足够多的记录以便统计
|
||||
user_id: userId,
|
||||
end_time: formatDateTime(new Date(createdAt)),
|
||||
sort_by: 'created_at',
|
||||
sort_order: 'desc'
|
||||
}
|
||||
|
||||
const countResponse = await apiCallApi.getAdminApiCalls(countParams)
|
||||
if (countResponse.success && countResponse.data?.items) {
|
||||
// 在客户端过滤出该产品的记录,并计算数量
|
||||
const productCalls = countResponse.data.items.filter(item => {
|
||||
const itemProductId = item.product_id || item.productId || ''
|
||||
const itemCreatedAt = item.created_at || item.createdAt || ''
|
||||
// 只统计创建时间小于等于当前记录创建时间的记录
|
||||
if (itemProductId === productId) {
|
||||
if (itemCreatedAt && createdAt) {
|
||||
return new Date(itemCreatedAt) <= new Date(createdAt)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
callCount = productCalls.length || 1
|
||||
|
||||
// 缓存结果
|
||||
callCountCache.value.set(cacheKey, callCount)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('查询用户产品调用次数失败:', err, {
|
||||
userId,
|
||||
productId,
|
||||
createdAt
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// 从批量查询结果中获取调用次数
|
||||
const callCount = callCountMap.get(tempRecordId) || 1
|
||||
|
||||
return {
|
||||
id: recordId,
|
||||
@@ -1542,105 +1631,169 @@ const loadDanmakuData = async () => {
|
||||
productId: productId,
|
||||
createdAt: createdAt,
|
||||
timestamp: new Date(createdAt).getTime(),
|
||||
callCount: callCount, // 该用户对该产品的调用次数
|
||||
callCount: callCount,
|
||||
// 详细信息用于悬停显示
|
||||
clientIp: record.client_ip || record.clientIp || '',
|
||||
status: record.status || '',
|
||||
cost: record.cost || '',
|
||||
transactionId: record.transaction_id || record.transactionId || ''
|
||||
transactionId: record.transaction_id || record.transactionId || '',
|
||||
// 弹幕显示相关属性(将在显示时计算)
|
||||
displayDelay: 0,
|
||||
rowIndex: -1,
|
||||
displayOrder: 0,
|
||||
randomTop: undefined
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
// 将新记录存入队列(按时间排序,旧的在前,新的在后)
|
||||
const sortedItems = newItems.sort((a, b) => a.timestamp - b.timestamp)
|
||||
danmakuQueue.value.push(...sortedItems)
|
||||
|
||||
// 限制队列大小,避免内存泄漏
|
||||
if (danmakuQueue.value.length > MAX_QUEUE_SIZE) {
|
||||
danmakuQueue.value = danmakuQueue.value.slice(-MAX_QUEUE_SIZE)
|
||||
}
|
||||
|
||||
// 更新lastEnqueueTime为最后入队记录的时间
|
||||
if (sortedItems.length > 0) {
|
||||
const lastItem = sortedItems[sortedItems.length - 1]
|
||||
lastEnqueueTime.value = new Date(lastItem.createdAt)
|
||||
if (lastItem.id) {
|
||||
lastEnqueuedId.value = lastItem.id
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`查询到 ${newRecords.length} 条新记录,已存入队列。当前队列长度: ${danmakuQueue.value.length}`)
|
||||
|
||||
// 标记首次加载完成
|
||||
if (isFirstLoad.value) {
|
||||
// 首次加载:先把最新的3条记录放入堆栈,然后逐个显示
|
||||
// 确保所有记录的调用次数都已计算完成
|
||||
const sortedItems = newItems.sort((a, b) => b.timestamp - a.timestamp)
|
||||
|
||||
// 如果没有数据,直接标记为已加载,不显示任何弹幕
|
||||
if (sortedItems.length === 0) {
|
||||
isFirstLoad.value = false
|
||||
lastFetchTime.value = now
|
||||
danmakuItems.value = []
|
||||
console.log('首次加载完成,没有记录')
|
||||
return
|
||||
}
|
||||
|
||||
// 先把3条记录放入堆栈(但不立即显示)
|
||||
firstLoadItems.value = sortedItems
|
||||
firstLoadDisplayIndex.value = 0
|
||||
danmakuItems.value = [] // 先清空显示列表
|
||||
isFirstLoad.value = false
|
||||
lastFetchTime.value = now
|
||||
console.log('首次加载完成,已加载', sortedItems.length, '条记录到堆栈,每条记录都已计算调用次数')
|
||||
|
||||
// 逐个显示弹幕,每次间隔300ms
|
||||
if (firstLoadItems.value.length > 0) {
|
||||
firstLoadDisplayTimer.value = setInterval(() => {
|
||||
if (firstLoadDisplayIndex.value < firstLoadItems.value.length) {
|
||||
// 从堆栈中取出并显示
|
||||
danmakuItems.value.push(firstLoadItems.value[firstLoadDisplayIndex.value])
|
||||
firstLoadDisplayIndex.value++
|
||||
} else {
|
||||
if (firstLoadDisplayTimer.value) {
|
||||
clearInterval(firstLoadDisplayTimer.value)
|
||||
firstLoadDisplayTimer.value = null
|
||||
}
|
||||
firstLoadItems.value = [] // 清空首次加载数据
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
} else {
|
||||
// 持续轮询:将新记录添加到堆栈中(只添加新记录,不保留旧记录)
|
||||
if (newItems.length > 0) {
|
||||
const existingIds = new Set(danmakuItems.value.map(item => item.id))
|
||||
const uniqueNewItems = newItems.filter(item => !existingIds.has(item.id))
|
||||
|
||||
if (uniqueNewItems.length > 0) {
|
||||
// 只添加新记录到堆栈,不保留旧记录(旧记录会在播放完成后自动移除)
|
||||
danmakuItems.value.push(...uniqueNewItems)
|
||||
}
|
||||
}
|
||||
// 无论是否有新记录,都要更新时间
|
||||
lastFetchTime.value = now
|
||||
}
|
||||
} else {
|
||||
// 即使请求失败,也要更新时间(避免重复请求旧数据)
|
||||
// 请求失败时,保持lastEnqueueTime不变,确保下次查询不会跳过记录
|
||||
if (!isFirstLoad.value) {
|
||||
lastFetchTime.value = now
|
||||
console.log('请求失败,保持lastEnqueueTime不变:', lastEnqueueTime.value)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取弹幕数据失败:', err)
|
||||
// 增加重试计数
|
||||
danmakuRetryCount.value++
|
||||
|
||||
// 如果连续失败次数超过阈值,使用指数退避延迟重试
|
||||
if (danmakuRetryCount.value >= MAX_RETRY_COUNT) {
|
||||
console.warn('弹幕数据获取连续失败,将在', RETRY_DELAY, 'ms后重试')
|
||||
setTimeout(() => {
|
||||
danmakuRetryCount.value = 0 // 重置计数
|
||||
loadDanmakuData()
|
||||
}, RETRY_DELAY * danmakuRetryCount.value)
|
||||
}
|
||||
// 不显示错误消息,避免干扰用户体验
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 获取弹幕样式 - 每条弹幕独立滚动,速度一致
|
||||
// 计算弹幕右边缘离开容器右边界所需的时间(秒)
|
||||
const getDanmakuExitRightTime = () => {
|
||||
if (!danmakuContainer.value) {
|
||||
return 1.5 * DANMAKU_DELAY_REDUCTION_FACTOR // 默认值,应用缩减系数
|
||||
}
|
||||
|
||||
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.8-2.5秒)
|
||||
return Math.max(0.8, Math.min(exitTime * DANMAKU_DELAY_REDUCTION_FACTOR, 2.5))
|
||||
}
|
||||
|
||||
// 获取随机弹幕位置(避免重叠)
|
||||
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 {}
|
||||
}
|
||||
|
||||
const containerHeight = danmakuContainer.value.clientHeight || 80
|
||||
const itemHeight = 36 // 每条弹幕的高度(包含间距)
|
||||
const maxRows = 2 // 固定为两行
|
||||
// 使用已计算的随机top位置,如果没有则生成一个
|
||||
const top = item.randomTop !== undefined
|
||||
? item.randomTop
|
||||
: getRandomDanmakuTop(danmakuItems.value, index)
|
||||
|
||||
// 计算弹幕应该在哪一行(垂直位置)
|
||||
// 使用索引和时间戳的组合来分配行,避免重叠
|
||||
const rowIndex = (index + Math.abs(item.timestamp % 1000)) % maxRows
|
||||
const top = rowIndex * itemHeight + 4
|
||||
// 使用已计算的延迟时间,如果没有则使用默认值
|
||||
const delay = item.displayDelay !== undefined ? item.displayDelay : 0
|
||||
|
||||
// 计算动画延迟,让弹幕错开开始时间,但保持相同的滚动速度
|
||||
// 使用时间戳的哈希值来分配延迟,让弹幕均匀分布
|
||||
const delay = (Math.abs(item.timestamp) % 3000) / 1000 // 0-3秒之间的延迟
|
||||
// 使用已计算的显示顺序,如果没有则使用索引
|
||||
const zIndex = 1000 + (item.displayOrder || index)
|
||||
|
||||
return {
|
||||
top: `${top}px`,
|
||||
left: '100%', // 从右侧开始
|
||||
animationDelay: `${delay}s`,
|
||||
animationDuration: '15s', // 统一的滚动速度
|
||||
zIndex: 10 - (rowIndex % 10) // 确保不同行的弹幕有正确的层级
|
||||
zIndex: zIndex // 先出现的弹幕层级更高,避免后面的遮盖前面的
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1718,7 +1871,67 @@ const resumeDanmaku = (itemId) => {
|
||||
pausedDanmakuIds.value.delete(itemId)
|
||||
}
|
||||
|
||||
// 弹幕动画结束回调 - 从堆栈中移除已播放完成的弹幕
|
||||
// 从队列中取出弹幕并显示
|
||||
const processDanmakuQueue = () => {
|
||||
// 如果队列为空,等待下次调用
|
||||
if (danmakuQueue.value.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// 从队列头部取出一条弹幕(FIFO:旧的先显示)
|
||||
const item = danmakuQueue.value.shift()
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
|
||||
// 计算显示属性
|
||||
const now = Date.now()
|
||||
|
||||
// 计算随机top位置(避免重叠)
|
||||
const randomTop = getRandomDanmakuTop(danmakuItems.value, danmakuItems.value.length)
|
||||
item.randomTop = randomTop
|
||||
|
||||
// 计算延迟:确保前一条弹幕的右边缘离开容器右边界后再显示下一条
|
||||
let delay = 0
|
||||
if (danmakuItems.value.length > 0) {
|
||||
// 找到最后一条弹幕,计算它的右边缘离开右边界的时间
|
||||
const lastItem = danmakuItems.value[danmakuItems.value.length - 1]
|
||||
const lastItemDisplayTime = lastItem.actualDisplayTime || (now - (lastItem.displayDelay || 0) * 1000)
|
||||
const exitRightTime = getDanmakuExitRightTime() * 1000 // 转换为毫秒
|
||||
const lastExitTime = lastItemDisplayTime + exitRightTime
|
||||
|
||||
const timeUntilLastExit = lastExitTime - now
|
||||
if (timeUntilLastExit > 0) {
|
||||
delay = timeUntilLastExit / 1000
|
||||
} else {
|
||||
delay = Math.random() * 0.2 // 小的随机延迟,让显示更自然
|
||||
}
|
||||
}
|
||||
|
||||
item.displayDelay = delay
|
||||
item.displayOrder = danmakuDisplayOrder.value++
|
||||
|
||||
// 如果延迟大于0,使用setTimeout延迟显示;否则立即显示
|
||||
if (delay > 0) {
|
||||
item.actualDisplayTime = now + (delay * 1000)
|
||||
setTimeout(() => {
|
||||
danmakuItems.value.push(item)
|
||||
// 限制弹幕数量,移除最旧的弹幕
|
||||
if (danmakuItems.value.length > MAX_DANMAKU_ITEMS) {
|
||||
danmakuItems.value = danmakuItems.value.slice(-MAX_DANMAKU_ITEMS)
|
||||
}
|
||||
}, delay * 1000)
|
||||
} else {
|
||||
item.actualDisplayTime = now
|
||||
danmakuItems.value.push(item)
|
||||
// 限制弹幕数量,移除最旧的弹幕
|
||||
if (danmakuItems.value.length > MAX_DANMAKU_ITEMS) {
|
||||
danmakuItems.value = danmakuItems.value.slice(-MAX_DANMAKU_ITEMS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 弹幕动画结束回调 - 从显示列表中移除已播放完成的弹幕
|
||||
const onDanmakuAnimationEnd = (itemId) => {
|
||||
// 只有当弹幕不在暂停状态时才移除(避免鼠标悬停时误删)
|
||||
if (!pausedDanmakuIds.value.has(itemId)) {
|
||||
@@ -1729,27 +1942,33 @@ const onDanmakuAnimationEnd = (itemId) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 启动弹幕轮询
|
||||
// 启动弹幕系统(队列机制)
|
||||
const startDanmakuPolling = () => {
|
||||
// 立即加载一次
|
||||
// 立即查询一次数据库,将新记录存入队列
|
||||
loadDanmakuData()
|
||||
|
||||
// 每5秒轮询一次
|
||||
// 定时查询数据库(降低频率,因为数据已在队列中缓冲)
|
||||
danmakuPollingTimer.value = setInterval(() => {
|
||||
loadDanmakuData()
|
||||
}, 5000)
|
||||
}, DANMAKU_POLLING_INTERVAL)
|
||||
|
||||
// 定时从队列中取出弹幕并显示
|
||||
danmakuDisplayTimer.value = setInterval(() => {
|
||||
processDanmakuQueue()
|
||||
}, DANMAKU_DISPLAY_INTERVAL)
|
||||
}
|
||||
|
||||
// 停止弹幕轮询
|
||||
// 停止弹幕系统
|
||||
const stopDanmakuPolling = () => {
|
||||
// 停止数据库查询定时器
|
||||
if (danmakuPollingTimer.value) {
|
||||
clearInterval(danmakuPollingTimer.value)
|
||||
danmakuPollingTimer.value = null
|
||||
}
|
||||
// 清理首次加载显示定时器
|
||||
if (firstLoadDisplayTimer.value) {
|
||||
clearInterval(firstLoadDisplayTimer.value)
|
||||
firstLoadDisplayTimer.value = null
|
||||
// 停止队列处理定时器
|
||||
if (danmakuDisplayTimer.value) {
|
||||
clearInterval(danmakuDisplayTimer.value)
|
||||
danmakuDisplayTimer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user