mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-10 20:36:55 +08:00
feat: add heartbeat monitoring system and disable browser timer throttling
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 // 普通消息使用默认颜色
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user