feat: add heartbeat monitoring system and disable browser timer throttling

This commit is contained in:
Megghy
2025-10-04 01:27:47 +08:00
parent fc465a8e0d
commit 389515bc5b
14 changed files with 246 additions and 532 deletions

View File

@@ -50,6 +50,7 @@ import {
NAlert,
NButton,
NCard,
NDataTable,
NDescriptions,
NDescriptionsItem,
NDivider,
@@ -65,7 +66,6 @@ import {
NQrCode,
NRadioButton,
NRadioGroup,
NScrollbar,
NSpin,
NStatistic,
NTabPane,
@@ -75,7 +75,8 @@ import {
NTooltip,
useMessage,
} from 'naive-ui'
import { computed, nextTick, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
import type { DataTableColumns } from 'naive-ui'
import { computed, h, nextTick, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
import VChart from 'vue-echarts'
import { useAccount } from '@/api/account'
import { useWebFetcher } from '@/store/useWebFetcher'
@@ -238,6 +239,60 @@ const sortedTodayTypes = computed(() => {
.sort(([, countA], [, countB]) => countB - countA)
})
type TodayTypeRow = {
key: string
rank: number
type: string
count: number
}
const todayTypeColumns: DataTableColumns<TodayTypeRow> = [
{
title: '排名',
key: 'rank',
align: 'center',
width: 72,
},
{
title: '类型',
key: 'type',
minWidth: 160,
ellipsis: {
tooltip: true,
},
render: row => h(
NTag,
{
size: 'small',
type: 'info',
bordered: false,
style: 'max-width: 100%; justify-content: flex-start;',
},
{
default: () => row.type,
},
),
},
{
title: '事件数',
key: 'count',
align: 'right',
width: 120,
render: row => row.count.toLocaleString(),
},
]
const todayTypeTableData = computed<TodayTypeRow[]>(() => {
return sortedTodayTypes.value.map(([type, count], index) => ({
key: type,
rank: index + 1,
type,
count,
}))
})
const todayTypeRowKey = (row: TodayTypeRow) => row.key
// Login Status (Computed from original snippet)
const loginStatusString = computed(() => {
switch (loginStatus.value) {
@@ -1039,6 +1094,7 @@ onUnmounted(() => {
<VChart
ref="gaugeChart"
:option="gaugeOption"
:manual-update="true"
autoresize
/>
</div>
@@ -1055,6 +1111,7 @@ onUnmounted(() => {
<VChart
ref="typeDistributionChart"
:option="typeDistributionOption"
:manual-update="true"
autoresize
/>
</div>
@@ -1123,36 +1180,26 @@ onUnmounted(() => {
<NText strong>
类型明细:
</NText>
<NScrollbar style="max-height: 200px; margin-top: 8px;">
<div v-if="sortedTodayTypes.length > 0">
<NFlex
vertical
spacing="small"
>
<NFlex
v-for="[type, count] in sortedTodayTypes"
:key="type"
justify="space-between"
align="center"
>
<NTag
size="small"
:bordered="false"
type="info"
style="max-width: 70%;"
>
<NEllipsis>{{ type }}</NEllipsis>
</NTag>
<NText>{{ count.toLocaleString() }}</NText>
</NFlex>
</NFlex>
</div>
<div style="margin-top: 8px;">
<NDataTable
v-if="todayTypeTableData.length > 0"
:columns="todayTypeColumns"
:data="todayTypeTableData"
:row-key="todayTypeRowKey"
size="small"
:bordered="false"
striped
:pagination="false"
:max-height="220"
single-line
:scrollbar-props="{ size: 6 }"
/>
<NEmpty
v-else
description="今日暂无数据"
size="small"
/>
</NScrollbar>
</div>
</NGi>
</NGrid>
</div>
@@ -1174,6 +1221,7 @@ onUnmounted(() => {
v-if="historicalData.length > 0"
ref="historyChart"
:option="historyOption"
:manual-update="true"
autoresize
/>
<NEmpty

View File

@@ -19,6 +19,7 @@ import { initAll, OnClientUnmounted } from './data/initialize'
import { useDanmakuWindow } from './store/useDanmakuWindow'
// 引入子组件
import WindowBar from './WindowBar.vue'
import { BASE_URL } from '@/data/constants'
// --- 响应式状态 ---
@@ -166,7 +167,7 @@ onMounted(() => {
<template #trigger>
<NA
class="token-get-link"
@click="openUrl('https://vtsuru.suki.club/manage')"
@click="openUrl(`https://${BASE_URL}/manage`)"
>
前往获取
</NA>

View File

@@ -22,6 +22,7 @@ const filterTypeOptions = [
{ label: 'SC', value: 'SC' },
{ label: '舰长', value: 'Guard' },
{ label: '进场', value: 'Enter' },
{ label: '点赞', value: 'Like' },
]
// 分组预设
@@ -211,8 +212,6 @@ const separatorOptions = [
<NGi>
<NFormItem label="背景颜色">
<NColorPicker
v-model:value="danmakuWindow.danmakuWindowSetting.backgroundColor"
:show-alpha="true"
/>
</NFormItem>
</NGi>

View File

@@ -133,6 +133,9 @@ const {
<template v-if="item.type === EventDataTypes.Enter">
<span class="enter-badge">进入了直播间</span>
</template>
<template v-else-if="item.type === EventDataTypes.Like">
<span class="like-badge"> 点赞了</span>
</template>
</div>
<div
v-if="item.type === EventDataTypes.Message && (item?.msg || parsedMessage.length > 0 || item.emoji)"
@@ -450,4 +453,15 @@ const {
word-break: break-all;
white-space: normal;
}
.like-badge {
color: #F56C6C;
font-size: 0.85em;
font-weight: 500;
padding: 1px 6px;
background-color: rgba(245, 108, 108, 0.1);
border-radius: 4px;
margin-left: auto;
white-space: nowrap;
}
</style>

View File

@@ -92,6 +92,7 @@ export function useDanmakuUtils(
case EventDataTypes.SC: return `sc-item ${scColorClass.value}`
case EventDataTypes.Guard: return 'guard-item'
case EventDataTypes.Enter: return 'enter-item'
case EventDataTypes.Like: return 'like-item'
default: return ''
}
})
@@ -171,6 +172,7 @@ export function useDanmakuUtils(
case EventDataTypes.SC: return '【SC】'
case EventDataTypes.Guard: return '【舰长】'
case EventDataTypes.Enter: return '【进场】'
case EventDataTypes.Like: return '【点赞】'
default: return ''
}
})
@@ -202,6 +204,8 @@ export function useDanmakuUtils(
return props.item.msg || '开通了舰长'
case EventDataTypes.Enter:
return '进入了直播间'
case EventDataTypes.Like:
return '点赞了'
default:
return ''
}
@@ -217,6 +221,8 @@ export function useDanmakuUtils(
return guardColor.value // 舰长消息使用舰长颜色
} else if (props.item.type === EventDataTypes.Enter) {
return '#67C23A' // 入场消息绿色
} else if (props.item.type === EventDataTypes.Like) {
return '#F56C6C' // 点赞消息红色
}
return undefined // 普通消息使用默认颜色
})

View File

@@ -143,23 +143,7 @@ export function recordEvent(eventType: string) {
* (需要根据实际接收到的数据结构调整)
*/
export function getEventType(command: any): string {
if (typeof command === 'string') {
try {
command = JSON.parse(command)
} catch (e) {
return 'UNKNOWN_FORMAT'
}
}
if (command && typeof command === 'object') {
// 优先使用 'cmd' 字段 (常见于 Web 或 OpenLive)
if (command.cmd) return command.cmd
// 备选 'command' 字段
if (command.command) return command.command
// 备选 'type' 字段
if (command.type) return command.type
}
return 'UNKNOWN' // 未知类型
return command.cmd
}
/**

View File

@@ -39,7 +39,7 @@ async function sendHeartbeat() {
}
}
function startHeartbeat() {
export function startHeartbeat() {
// 立即发送一次,确保后端在加载后快速收到心跳
void sendHeartbeat()
@@ -50,7 +50,7 @@ function startHeartbeat() {
info('[心跳] 定时器已启动,间隔 2 秒')
}
function stopHeartbeat() {
export function stopHeartbeat() {
if (heartbeatTimer !== null) {
clearInterval(heartbeatTimer)
heartbeatTimer = null
@@ -179,7 +179,7 @@ export async function initAll(isOnBoot: boolean) {
useAutoAction().init()
useBiliFunction().init()
startHeartbeat()
//startHeartbeat()
clientInited.value = true
}
export function OnClientUnmounted() {