mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
feat: add heartbeat monitoring system and disable browser timer throttling
This commit is contained in:
@@ -1,425 +0,0 @@
|
||||
import DefaultIndexTemplateVue from '@/views/view/indexTemplate/DefaultIndexTemplate.vue';
|
||||
import { defineAsyncComponent, ref, markRaw } from 'vue';
|
||||
|
||||
const debugAPI =
|
||||
import.meta.env.VITE_API == 'dev'
|
||||
? import.meta.env.VITE_DEBUG_DEV_API
|
||||
: import.meta.env.VITE_DEBUG_RELEASE_API;
|
||||
const releseAPI = `https://vtsuru.suki.club/`;
|
||||
const failoverAPI = `https://failover-api.vtsuru.suki.club/`;
|
||||
|
||||
export const isBackendUsable = ref(true);
|
||||
export const isDev = import.meta.env.MODE === 'development';
|
||||
// @ts-ignore
|
||||
export const isTauri = () => window.__TAURI__ !== undefined || window.__TAURI_INTERNAL__ !== undefined || '__TAURI__' in window;
|
||||
|
||||
export const AVATAR_URL = 'https://workers.vrp.moe/api/bilibili/avatar/';
|
||||
export const FILE_BASE_URL = 'https://files.vtsuru.suki.club';
|
||||
export const IMGUR_URL = FILE_BASE_URL + '/imgur/';
|
||||
export const THINGS_URL = FILE_BASE_URL + '/things/';
|
||||
export const apiFail = ref(false);
|
||||
|
||||
export const BASE_URL =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? debugAPI
|
||||
: apiFail.value
|
||||
? failoverAPI
|
||||
: releseAPI;
|
||||
export const BASE_API_URL = BASE_URL + 'api/';
|
||||
export const FETCH_API = 'https://fetch.vtsuru.live/';
|
||||
export const BASE_HUB_URL =
|
||||
(process.env.NODE_ENV === 'development'
|
||||
? debugAPI
|
||||
: apiFail.value
|
||||
? failoverAPI
|
||||
: releseAPI) + 'hub/';
|
||||
|
||||
export const TURNSTILE_KEY = '0x4AAAAAAAETUSAKbds019h0';
|
||||
|
||||
export const CURRENT_HOST = `${window.location.protocol}//${window.location.host}/`;
|
||||
export const CN_HOST = 'https://vtsuru.suki.club/';
|
||||
|
||||
export const USER_API_URL = BASE_API_URL + 'user/';
|
||||
export const ACCOUNT_API_URL = BASE_API_URL + 'account/';
|
||||
export const BILI_API_URL = BASE_API_URL + 'bili/';
|
||||
export const SONG_API_URL = BASE_API_URL + 'song-list/';
|
||||
export const NOTIFACTION_API_URL = BASE_API_URL + 'notification/';
|
||||
export const QUESTION_API_URL = BASE_API_URL + 'qa/';
|
||||
export const LOTTERY_API_URL = BASE_API_URL + 'lottery/';
|
||||
export const HISTORY_API_URL = BASE_API_URL + 'history/';
|
||||
export const SCHEDULE_API_URL = BASE_API_URL + 'schedule/';
|
||||
export const VIDEO_COLLECT_API_URL = BASE_API_URL + 'video-collect/';
|
||||
export const OPEN_LIVE_API_URL = BASE_API_URL + 'open-live/';
|
||||
export const SONG_REQUEST_API_URL = BASE_API_URL + 'live-request/';
|
||||
export const QUEUE_API_URL = BASE_API_URL + 'queue/';
|
||||
export const EVENT_API_URL = BASE_API_URL + 'event/';
|
||||
export const LIVE_API_URL = BASE_API_URL + 'live/';
|
||||
export const FEEDBACK_API_URL = BASE_API_URL + 'feedback/';
|
||||
export const MUSIC_REQUEST_API_URL = BASE_API_URL + 'music-request/';
|
||||
export const VTSURU_API_URL = BASE_API_URL + 'vtsuru/';
|
||||
export const POINT_API_URL = BASE_API_URL + 'point/';
|
||||
export const BILI_AUTH_API_URL = BASE_API_URL + 'bili-auth/';
|
||||
export const FORUM_API_URL = BASE_API_URL + 'forum/';
|
||||
export const USER_INDEX_API_URL = BASE_API_URL + 'user-index/';
|
||||
export const ANALYZE_API_URL = BASE_API_URL + 'analyze/';
|
||||
|
||||
export type TemplateMapType = {
|
||||
[key: string]: {
|
||||
name: string;
|
||||
settingName?: string;
|
||||
component: any;
|
||||
};
|
||||
};
|
||||
export const ScheduleTemplateMap: TemplateMapType = {
|
||||
'': {
|
||||
name: '默认',
|
||||
//settingName: 'Template.Schedule.Default',
|
||||
component: markRaw(defineAsyncComponent(
|
||||
() => import('@/views/view/scheduleTemplate/DefaultScheduleTemplate.vue')
|
||||
))
|
||||
},
|
||||
pinky: {
|
||||
name: '粉粉',
|
||||
//settingName: 'Template.Schedule.Pinky',
|
||||
component: markRaw(defineAsyncComponent(
|
||||
() => import('@/views/view/scheduleTemplate/PinkySchedule.vue')
|
||||
))
|
||||
},
|
||||
kawaii: {
|
||||
name: '可爱<E58FAF><E788B1>?,
|
||||
//settingName: 'Template.Schedule.Kawaii',
|
||||
component: markRaw(defineAsyncComponent(
|
||||
() => import('@/views/view/scheduleTemplate/KawaiiScheduleTemplate.vue')
|
||||
))
|
||||
}
|
||||
};
|
||||
export const SongListTemplateMap: TemplateMapType = {
|
||||
'': {
|
||||
name: '默认',
|
||||
//settingName: 'Template.SongList.Default',
|
||||
component: markRaw(defineAsyncComponent(
|
||||
() => import('@/views/view/songListTemplate/DefaultSongListTemplate.vue')
|
||||
))
|
||||
},
|
||||
simple: {
|
||||
name: '简<><E7AE80>?,
|
||||
//settingName: 'Template.SongList.Simple',
|
||||
component: markRaw(defineAsyncComponent(
|
||||
() => import('@/views/view/songListTemplate/SimpleSongListTemplate.vue')
|
||||
))
|
||||
},
|
||||
traditional: {
|
||||
name: '列表',
|
||||
settingName: 'Template.SongList.Traditional',
|
||||
component: markRaw(defineAsyncComponent(
|
||||
() =>
|
||||
import('@/views/view/songListTemplate/TraditionalSongListTemplate.vue')
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
export const IndexTemplateMap: TemplateMapType = {
|
||||
'': {
|
||||
name: '默认',
|
||||
//settingName: 'Template.Index.Default',
|
||||
component: markRaw(DefaultIndexTemplateVue)
|
||||
}
|
||||
};
|
||||
|
||||
export const defaultDanmujiCss = `@import url("https://fonts.googleapis.com/css?family=Changa%20One");
|
||||
@import url("https://fonts.googleapis.com/css?family=Imprima");
|
||||
|
||||
/* Transparent background */
|
||||
yt-live-chat-renderer {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-renderer {
|
||||
background-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-author-chip #author-name {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
yt-live-chat-item-list-renderer #item-scroller {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
yt-live-chat-interact-message-renderer #content,
|
||||
yt-live-chat-text-message-renderer #content,
|
||||
yt-live-chat-membership-item-renderer #content {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Hide header and input */
|
||||
yt-live-chat-header-renderer,
|
||||
yt-live-chat-message-input-renderer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide unimportant messages */
|
||||
yt-live-chat-interact-message-renderer[is-deleted],
|
||||
yt-live-chat-text-message-renderer[is-deleted],
|
||||
yt-live-chat-membership-item-renderer[is-deleted] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-mode-change-message-renderer,
|
||||
yt-live-chat-viewer-engagement-message-renderer,
|
||||
yt-live-chat-restricted-participation-renderer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer a,
|
||||
yt-live-chat-membership-item-renderer a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
/* Global Setting */
|
||||
yt-live-chat-renderer {
|
||||
|
||||
}
|
||||
#item-scroller {
|
||||
|
||||
}
|
||||
|
||||
/* Reduce side padding */
|
||||
yt-live-chat-interact-message-renderer,
|
||||
yt-live-chat-text-message-renderer {
|
||||
padding-left: 4px !important;
|
||||
padding-right: 4px !important;
|
||||
}
|
||||
|
||||
/* Outlines */
|
||||
yt-live-chat-renderer * {
|
||||
text-shadow: -2px -2px #000000, -2px -1px #000000, -2px 0px #000000, -2px 1px #000000, -2px 2px #000000, -1px -2px #000000, -1px -1px #000000, -1px 0px #000000, -1px 1px #000000, -1px 2px #000000, 0px -2px #000000, 0px -1px #000000, 0px 0px #000000, 0px 1px #000000, 0px 2px #000000, 1px -2px #000000, 1px -1px #000000, 1px 0px #000000, 1px 1px #000000, 1px 2px #000000, 2px -2px #000000, 2px -1px #000000, 2px 0px #000000, 2px 1px #000000, 2px 2px #000000;
|
||||
font-family: "Imprima", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", SimHei, Arial, sans-serif;
|
||||
font-size: 18px !important;
|
||||
line-height: 20px !important;
|
||||
}
|
||||
|
||||
/* Avatars */
|
||||
yt-live-chat-interact-message-renderer #author-photo,
|
||||
yt-live-chat-interact-message-renderer #author-photo img,
|
||||
yt-live-chat-text-message-renderer #author-photo,
|
||||
yt-live-chat-text-message-renderer #author-photo img,
|
||||
yt-live-chat-paid-message-renderer #author-photo,
|
||||
yt-live-chat-paid-message-renderer #author-photo img,
|
||||
yt-live-chat-membership-item-renderer #author-photo,
|
||||
yt-live-chat-membership-item-renderer #author-photo img {
|
||||
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
border-radius: 24px !important;
|
||||
margin-right: 6px !important;
|
||||
}
|
||||
|
||||
/* Channel names */
|
||||
yt-live-chat-interact-message-renderer #content #author-name,
|
||||
yt-live-chat-text-message-renderer #content #author-name {
|
||||
|
||||
}
|
||||
yt-live-chat-interact-message-renderer #author-name[type="owner"],
|
||||
yt-live-chat-interact-message-renderer yt-live-chat-author-badge-renderer[type="owner"],
|
||||
yt-live-chat-text-message-renderer #author-name[type="owner"],
|
||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="owner"] {
|
||||
color: #ffd600 !important;
|
||||
}
|
||||
yt-live-chat-interact-message-renderer #author-name[type="moderator"],
|
||||
yt-live-chat-interact-message-renderer yt-live-chat-author-badge-renderer[type="moderator"],
|
||||
yt-live-chat-text-message-renderer #author-name[type="moderator"],
|
||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="moderator"] {
|
||||
color: #5e84f1 !important;
|
||||
}
|
||||
yt-live-chat-interact-message-renderer #author-name[type="member"],
|
||||
yt-live-chat-interact-message-renderer yt-live-chat-author-badge-renderer[type="member"],
|
||||
yt-live-chat-text-message-renderer #author-name[type="member"],
|
||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="member"] {
|
||||
color: #0f9d58 !important;
|
||||
}
|
||||
|
||||
yt-live-chat-interact-message-renderer #author-name,
|
||||
yt-live-chat-text-message-renderer #author-name {
|
||||
|
||||
color: #cccccc !important;
|
||||
font-family: "Changa One", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", SimHei, Arial, sans-serif;
|
||||
font-size: 20px !important;
|
||||
line-height: 20px !important;
|
||||
}
|
||||
|
||||
/* Show colon */
|
||||
yt-live-chat-text-message-renderer #author-name::after {
|
||||
content: ":";
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/* Hide badges */
|
||||
yt-live-chat-interact-message-renderer #chat-badges,
|
||||
yt-live-chat-text-message-renderer #chat-badges {
|
||||
|
||||
vertical-align: text-top !important;
|
||||
}
|
||||
img.yt-live-chat-author-badge-renderer, yt-icon.yt-live-chat-author-badge-renderer {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Medal */
|
||||
yt-live-chat-author-medal-renderer {
|
||||
display: none;
|
||||
|
||||
}
|
||||
yt-live-chat-author-medal-renderer[is-fan-group] {
|
||||
display: flex;
|
||||
}
|
||||
#medal-name.yt-live-chat-author-medal-renderer {
|
||||
|
||||
font-size: 14px !important;
|
||||
line-height: 14px !important;
|
||||
}
|
||||
|
||||
#medal-level.yt-live-chat-author-medal-renderer {
|
||||
|
||||
font-size: 14px !important;
|
||||
line-height: 14px !important;
|
||||
}
|
||||
|
||||
|
||||
/* Messages */
|
||||
yt-live-chat-interact-message-renderer #message,
|
||||
yt-live-chat-interact-message-renderer #message *,
|
||||
yt-live-chat-text-message-renderer #message,
|
||||
yt-live-chat-text-message-renderer #message * {
|
||||
color: #ffffff !important;
|
||||
font-family: "Imprima", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", SimHei, Arial, sans-serif;
|
||||
font-size: 18px !important;
|
||||
line-height: 18px !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer #image-and-message {
|
||||
display: inline !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
yt-live-chat-text-message-renderer #message {
|
||||
display: inline !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer #image-and-message .emoji {
|
||||
width: auto !important;
|
||||
height: 48px !important;
|
||||
}
|
||||
|
||||
#image-and-message img[display="block"] {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#image-and-message img[display="inline"] {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
/* Timestamps */
|
||||
|
||||
|
||||
|
||||
/* Background colors */
|
||||
body {
|
||||
overflow: hidden;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer,
|
||||
yt-live-chat-text-message-renderer[is-highlighted] {
|
||||
background-color: rgba(204, 204, 204, 0) !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[author-type="owner"],
|
||||
yt-live-chat-text-message-renderer[author-type="owner"][is-highlighted] {
|
||||
background-color: rgba(255, 214, 0, 0) !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[author-type="moderator"],
|
||||
yt-live-chat-text-message-renderer[author-type="moderator"][is-highlighted] {
|
||||
background-color: rgba(94, 132, 241, 0) !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[author-type="member"],
|
||||
yt-live-chat-text-message-renderer[author-type="member"][is-highlighted] {
|
||||
background-color: rgba(15, 157, 88, 0) !important;
|
||||
}
|
||||
|
||||
/* SuperChat/Fan Funding Messages */
|
||||
yt-live-chat-paid-message-renderer {
|
||||
margin: 4px 0 !important;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer #author-name,
|
||||
yt-live-chat-paid-message-renderer #author-name *,
|
||||
yt-live-chat-membership-item-renderer #header-content-inner-column,
|
||||
yt-live-chat-membership-item-renderer #header-content-inner-column * {
|
||||
color: #ffffff !important;
|
||||
font-family: "Changa One", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", SimHei, Arial, sans-serif;
|
||||
font-size: 20px !important;
|
||||
line-height: 20px !important;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer #purchase-amount,
|
||||
yt-live-chat-paid-message-renderer #purchase-amount *,
|
||||
yt-live-chat-membership-item-renderer #header-subtext,
|
||||
yt-live-chat-membership-item-renderer #header-subtext * {
|
||||
color: #ffffff !important;
|
||||
font-family: "Imprima", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", SimHei, Arial, sans-serif;
|
||||
font-size: 18px !important;
|
||||
line-height: 18px !important;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer #content,
|
||||
yt-live-chat-paid-message-renderer #content * {
|
||||
color: #ffffff !important;
|
||||
font-family: "Imprima", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", SimHei, Arial, sans-serif;
|
||||
font-size: 18px !important;
|
||||
line-height: 18px !important;
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer #card,
|
||||
yt-live-chat-membership-item-renderer #header {
|
||||
background-color: #0f9d58 !important;
|
||||
margin: 4px 0 !important;
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-renderer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* SuperChat Ticker */
|
||||
yt-live-chat-ticker-paid-message-item-renderer,
|
||||
yt-live-chat-ticker-paid-message-item-renderer *,
|
||||
yt-live-chat-ticker-sponsor-item-renderer,
|
||||
yt-live-chat-ticker-sponsor-item-renderer * {
|
||||
color: #ffffff !important;
|
||||
font-family: "Imprima", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", SimHei, Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Animation */
|
||||
@keyframes anim {
|
||||
|
||||
}
|
||||
|
||||
yt-live-chat-interact-message-renderer,
|
||||
yt-live-chat-text-message-renderer,
|
||||
yt-live-chat-membership-item-renderer,
|
||||
yt-live-chat-paid-message-renderer {
|
||||
animation: anim 0ms;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
`
|
||||
@@ -38,7 +38,7 @@
|
||||
"@vueuse/router": "^13.9.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"bilibili-live-ws": "^6.3.1",
|
||||
"bilibili-live-danmaku": "^0.7.14",
|
||||
"cropperjs": "^2.0.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
@@ -55,7 +55,7 @@
|
||||
"md5": "^2.3.0",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.53.0",
|
||||
"naive-ui": "^2.43.1",
|
||||
"naive-ui": "2.42.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"peerjs": "^1.5.5",
|
||||
"pinia": "^3.0.3",
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { KeepLiveWS } from 'bilibili-live-ws/browser' // 导入 bilibili-live-ws 库
|
||||
import { LiveWS } from "bilibili-live-danmaku";
|
||||
// BaseDanmakuClient.ts
|
||||
import type { EventModel } from '@/api/api-models'
|
||||
// 导入事件模型和类型枚举
|
||||
@@ -13,7 +13,7 @@ export default abstract class BaseDanmakuClient {
|
||||
}
|
||||
|
||||
// WebSocket 客户端实例
|
||||
public client: KeepLiveWS | null
|
||||
public client: LiveWS | null
|
||||
|
||||
// 客户端连接状态
|
||||
public state: 'padding' | 'connected' | 'connecting' | 'disconnected'
|
||||
@@ -35,6 +35,7 @@ export default abstract class BaseDanmakuClient {
|
||||
scDel: ((arg1: EventModel, arg2?: any) => void)[] // 新增: SC 删除事件
|
||||
all: ((arg1: any) => void)[] // 'all' 事件监听器接收原始消息或特定事件包
|
||||
follow: ((arg1: EventModel, arg2?: any) => void)[] // 新增: 关注事件
|
||||
like: ((arg1: EventModel, arg2?: any) => void)[] // 新增: 点赞事件
|
||||
}
|
||||
|
||||
// --- 事件系统 2: 使用原始数据类型 ---
|
||||
@@ -48,6 +49,7 @@ export default abstract class BaseDanmakuClient {
|
||||
scDel: ((arg1: any, arg2?: any) => void)[] // 新增: SC 删除事件
|
||||
all: ((arg1: any) => void)[] // 'all' 事件监听器接收原始消息或特定事件包
|
||||
follow: ((arg1: any, arg2?: any) => void)[] // 新增: 关注事件
|
||||
like: ((arg1: any, arg2?: any) => void)[] // 新增: 点赞事件
|
||||
}
|
||||
|
||||
// 创建空的 EventModel 监听器对象
|
||||
@@ -61,6 +63,7 @@ export default abstract class BaseDanmakuClient {
|
||||
scDel: [],
|
||||
all: [],
|
||||
follow: [], // 初始化 follow 事件
|
||||
like: [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +78,7 @@ export default abstract class BaseDanmakuClient {
|
||||
scDel: [],
|
||||
all: [],
|
||||
follow: [], // 初始化 follow 事件
|
||||
like: [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,27 +182,27 @@ export default abstract class BaseDanmakuClient {
|
||||
* @returns Promise<{ success: boolean; message: string }> 连接结果
|
||||
*/
|
||||
protected async initClientInner(
|
||||
chatClient: KeepLiveWS,
|
||||
chatClient: LiveWS,
|
||||
): Promise<{ success: boolean, message: string }> {
|
||||
let isConnected = false // 标记是否连接成功
|
||||
let isError = false // 标记是否发生错误
|
||||
let errorMsg = '' // 存储错误信息
|
||||
|
||||
// 监听错误事件
|
||||
chatClient.on('error', (err: any) => {
|
||||
chatClient.addEventListener('error', (err: any) => {
|
||||
console.error(`[${this.type}] 客户端发生错误:`, err)
|
||||
isError = true
|
||||
errorMsg = err?.message || err?.toString() || '未知错误'
|
||||
})
|
||||
|
||||
// 监听连接成功事件
|
||||
chatClient.on('live', () => {
|
||||
chatClient.addEventListener('CONNECT_SUCCESS', () => {
|
||||
console.log(`[${this.type}] 弹幕客户端连接成功`)
|
||||
isConnected = true
|
||||
})
|
||||
|
||||
// 监听连接关闭事件
|
||||
chatClient.on('close', () => {
|
||||
chatClient.addEventListener('close', () => {
|
||||
console.log(`[${this.type}] 弹幕客户端连接已关闭`)
|
||||
if (this.state !== 'disconnected') {
|
||||
this.state = 'disconnected'
|
||||
@@ -209,7 +213,7 @@ export default abstract class BaseDanmakuClient {
|
||||
|
||||
// 监听原始消息事件 (通用)
|
||||
// 注意: 子类可能也会监听特定事件名, 这里的 'msg' 是备用或处理未被特定监听器捕获的事件
|
||||
chatClient.on('msg', (command: any) => this.onRawMessage(command))
|
||||
chatClient.addEventListener('MESSAGE', (command: any) => this.onRawMessage(command.data))
|
||||
|
||||
this.client = chatClient // 保存客户端实例
|
||||
|
||||
@@ -301,6 +305,12 @@ export default abstract class BaseDanmakuClient {
|
||||
* @param rawCommand - 完整的原始消息对象 (可选, any 类型)
|
||||
*/
|
||||
public abstract onScDel(comand: any): void
|
||||
/**
|
||||
* 处理点赞消息 (子类实现)
|
||||
* @param data - 原始消息数据部分 (any 类型)
|
||||
* @param rawCommand - 完整的原始消息对象 (可选, any 类型)
|
||||
*/
|
||||
public abstract onLike(comand: any): void
|
||||
|
||||
// --- 事件系统 1: on/off (使用 EventModel) ---
|
||||
public onEvent(eventName: 'danmaku', listener: (arg1: EventModel, arg2?: any) => void): this
|
||||
@@ -311,6 +321,7 @@ export default abstract class BaseDanmakuClient {
|
||||
public onEvent(eventName: 'scDel', listener: (arg1: EventModel, arg2?: any) => void): this // 新增
|
||||
public onEvent(eventName: 'all', listener: (arg1: any) => void): this
|
||||
public onEvent(eventName: 'follow', listener: (arg1: EventModel, arg2?: any) => void): this // 新增
|
||||
public onEvent(eventName: 'like', listener: (arg1: EventModel, arg2?: any) => void): this // 新增
|
||||
public onEvent(eventName: keyof BaseDanmakuClient['eventsAsModel'], listener: (...args: any[]) => void): this {
|
||||
if (!this.eventsAsModel[eventName]) {
|
||||
// @ts-ignore
|
||||
@@ -342,6 +353,7 @@ export default abstract class BaseDanmakuClient {
|
||||
public on(eventName: 'scDel', listener: (arg1: any, arg2?: any) => void): this // 新增
|
||||
public on(eventName: 'all', listener: (arg1: any) => void): this
|
||||
public on(eventName: 'follow', listener: (arg1: any, arg2?: any) => void): this // 新增
|
||||
public on(eventName: 'like', listener: (arg1: any, arg2?: any) => void): this // 新增
|
||||
public on(eventName: keyof BaseDanmakuClient['eventsRaw'], listener: (...args: any[]) => void): this {
|
||||
if (!this.eventsRaw[eventName]) {
|
||||
// @ts-ignore
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { KeepLiveWS } from 'bilibili-live-ws/browser'
|
||||
import { DataEvent, LiveWS, MessageData } from 'bilibili-live-danmaku'
|
||||
import { EventDataTypes, GuardLevel } from '@/api/api-models'
|
||||
import { GuidUtils } from '@/Utils'
|
||||
import { AVATAR_URL } from '../constants'
|
||||
import BaseDanmakuClient from './BaseDanmakuClient'
|
||||
import Long from 'long'
|
||||
|
||||
export interface DirectClientAuthInfo {
|
||||
token: string
|
||||
@@ -28,22 +29,34 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
|
||||
protected async initClient(): Promise<{ success: boolean, message: string }> {
|
||||
if (this.authInfo) {
|
||||
const chatClient = new KeepLiveWS(this.authInfo.roomId, {
|
||||
const chatClient = new LiveWS(this.authInfo.roomId, {
|
||||
key: this.authInfo.token,
|
||||
buvid: this.authInfo.buvid,
|
||||
uid: this.authInfo.tokenUserId,
|
||||
protover: 3,
|
||||
})
|
||||
|
||||
chatClient.on('live', () => {
|
||||
chatClient.addEventListener('CONNECT_SUCCESS', () => {
|
||||
console.log(`[direct] 已连接房间: ${this.authInfo.roomId}`)
|
||||
})
|
||||
chatClient.on('DANMU_MSG', data => this.onDanmaku(data))
|
||||
chatClient.on('SEND_GIFT', data => this.onGift(data))
|
||||
chatClient.on('GUARD_BUY', data => this.onGuard(data))
|
||||
chatClient.on('SUPER_CHAT_MESSAGE', data => this.onSC(data))
|
||||
chatClient.on('INTERACT_WORD', data => this.onEnter(data))
|
||||
chatClient.on('SUPER_CHAT_MESSAGE_DELETE', data => this.onScDel(data))
|
||||
chatClient.addEventListener('DANMU_MSG', data => this.onDanmaku(data.data))
|
||||
chatClient.addEventListener('SEND_GIFT', data => this.onGift(data.data))
|
||||
chatClient.addEventListener('GUARD_BUY', data => this.onGuard(data.data))
|
||||
chatClient.addEventListener('SUPER_CHAT_MESSAGE', data => this.onSC(data.data))
|
||||
//chatClient.addEventListener('INTERACT_WORD', data => this.onEnter(data.data))
|
||||
chatClient.addEventListener('MESSAGE', data => {
|
||||
switch (data.data.cmd) {
|
||||
case 'INTERACT_WORD_V2':
|
||||
this.onEnter(data.data)
|
||||
break
|
||||
case 'LIKE_INFO_V3_CLICK':
|
||||
this.onLike(data.data)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
//chatClient.addEventListener('SUPER_CHAT_MESSAGE_DELETE', data => this.onScDel(data))
|
||||
|
||||
return super.initClientInner(chatClient)
|
||||
} else {
|
||||
@@ -55,7 +68,7 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
}
|
||||
}
|
||||
|
||||
public onDanmaku(command: any): void {
|
||||
public onDanmaku(command: MessageData.DANMU_MSG): void {
|
||||
const info = command.info
|
||||
this.eventsRaw?.danmaku?.forEach((d) => {
|
||||
d(info, command)
|
||||
@@ -84,7 +97,7 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
})
|
||||
}
|
||||
|
||||
public onGift(command: any): void {
|
||||
public onGift(command: MessageData.SEND_GIFT): void {
|
||||
const data = command.data
|
||||
this.eventsRaw?.gift?.forEach((d) => {
|
||||
d(data, command)
|
||||
@@ -96,13 +109,13 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
uname: data.uname,
|
||||
uid: data.uid,
|
||||
msg: data.giftName,
|
||||
price: data.price / 1000,
|
||||
price: data.total_coin / 1000,
|
||||
num: data.num,
|
||||
time: Date.now(),
|
||||
guard_level: data.guard_level,
|
||||
fans_medal_level: data.medal_info.medal_level,
|
||||
fans_medal_name: data.medal_info.medal_name,
|
||||
fans_medal_wearing_status: data.medal_info.is_lighted === 1,
|
||||
fans_medal_level: data.fans_medal?.medal_level,
|
||||
fans_medal_name: data.fans_medal?.medal_name,
|
||||
fans_medal_wearing_status: data.fans_medal !== null || data.fans_medal !== undefined,
|
||||
uface: data.face.replace('http://', 'https://'),
|
||||
open_id: '',
|
||||
ouid: GuidUtils.numToGuid(data.uid),
|
||||
@@ -112,7 +125,7 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
})
|
||||
}
|
||||
|
||||
public onSC(command: any): void {
|
||||
public onSC(command: MessageData.SUPER_CHAT_MESSAGE): void {
|
||||
const data = command.data
|
||||
this.eventsRaw?.sc?.forEach((d) => {
|
||||
d(data, command)
|
||||
@@ -130,7 +143,7 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
guard_level: data.user_info.guard_level,
|
||||
fans_medal_level: data.medal_info.medal_level,
|
||||
fans_medal_name: data.medal_info.medal_name,
|
||||
fans_medal_wearing_status: data.medal_info.is_lighted === 1,
|
||||
fans_medal_wearing_status: data.medal_info !== null || data.medal_info !== undefined,
|
||||
uface: data.user_info.face.replace('http://', 'https://'),
|
||||
open_id: '',
|
||||
ouid: GuidUtils.numToGuid(data.uid),
|
||||
@@ -140,7 +153,7 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
})
|
||||
}
|
||||
|
||||
public onGuard(command: any): void {
|
||||
public onGuard(command: MessageData.GUARD_BUY): void {
|
||||
const data = command.data
|
||||
this.eventsRaw?.guard?.forEach((d) => {
|
||||
d(data, command)
|
||||
@@ -168,9 +181,9 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
})
|
||||
}
|
||||
|
||||
public onEnter(command: any): void {
|
||||
const data = command.data
|
||||
const msgType = data.msg_type
|
||||
public onEnter(command: MessageData.INTERACT_WORD_V2): void {
|
||||
const data = command.decoded
|
||||
const msgType = data?.msgType
|
||||
|
||||
if (msgType === 1) {
|
||||
this.eventsRaw?.enter?.forEach((d) => {
|
||||
@@ -180,19 +193,19 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
d(
|
||||
{
|
||||
type: EventDataTypes.Enter,
|
||||
uname: data.uname,
|
||||
uid: data.uid,
|
||||
uname: data?.uname || '',
|
||||
uid: this.convertToNumber(data?.uid) || 0,
|
||||
msg: '',
|
||||
price: 0,
|
||||
num: 1,
|
||||
time: data.timestamp ? data.timestamp * 1000 : Date.now(),
|
||||
guard_level: data.privilege_type || GuardLevel.None,
|
||||
fans_medal_level: data.fans_medal?.medal_level || 0,
|
||||
fans_medal_name: data.fans_medal?.medal_name || '',
|
||||
fans_medal_wearing_status: data.fans_medal?.is_lighted === 1,
|
||||
uface: data.face?.replace('http://', 'https://') || (AVATAR_URL + data.uid),
|
||||
time: data?.timestamp ? this.convertToNumber(data.timestamp) * 1000 : Date.now(),
|
||||
guard_level: this.convertToNumber(data?.privilegeType) || GuardLevel.None,
|
||||
fans_medal_level: this.convertToNumber(data?.fansMedal?.medalLevel) || 0,
|
||||
fans_medal_name: data?.fansMedal?.medalName || '',
|
||||
fans_medal_wearing_status: data?.fansMedal?.isLighted === 1,
|
||||
uface: data?.uinfo?.uheadFrame?.frameImg?.replace('http://', 'https://') || (AVATAR_URL + this.convertToNumber(data?.uid)),
|
||||
open_id: '',
|
||||
ouid: GuidUtils.numToGuid(data.uid),
|
||||
ouid: GuidUtils.numToGuid(this.convertToNumber(data?.uid)),
|
||||
},
|
||||
command,
|
||||
)
|
||||
@@ -205,19 +218,19 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
d(
|
||||
{
|
||||
type: EventDataTypes.Follow,
|
||||
uname: data.uname,
|
||||
uid: data.uid,
|
||||
uname: data?.uname || '',
|
||||
uid: this.convertToNumber(data?.uid),
|
||||
msg: '关注了主播',
|
||||
price: 0,
|
||||
num: 1,
|
||||
time: data.timestamp ? data.timestamp * 1000 : Date.now(),
|
||||
guard_level: data.privilege_type || GuardLevel.None,
|
||||
fans_medal_level: data.fans_medal?.medal_level || 0,
|
||||
fans_medal_name: data.fans_medal?.medal_name || '',
|
||||
fans_medal_wearing_status: data.fans_medal?.is_lighted === 1,
|
||||
uface: data.face?.replace('http://', 'https://') || (AVATAR_URL + data.uid),
|
||||
time: data?.timestamp ? this.convertToNumber(data.timestamp) * 1000 : Date.now(),
|
||||
guard_level: this.convertToNumber(data?.privilegeType) || GuardLevel.None,
|
||||
fans_medal_level: this.convertToNumber(data?.fansMedal?.medalLevel) || 0,
|
||||
fans_medal_name: data?.fansMedal?.medalName || '',
|
||||
fans_medal_wearing_status: data?.fansMedal?.isLighted === 1,
|
||||
uface: data?.uinfo?.uheadFrame?.frameImg?.replace('http://', 'https://') || (AVATAR_URL + data?.uid),
|
||||
open_id: '',
|
||||
ouid: GuidUtils.numToGuid(data.uid),
|
||||
ouid: GuidUtils.numToGuid(this.convertToNumber(data?.uid)),
|
||||
},
|
||||
command,
|
||||
)
|
||||
@@ -225,6 +238,41 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
}
|
||||
}
|
||||
|
||||
convertToNumber(value: number | Long | null | undefined): number {
|
||||
if (value instanceof Long) {
|
||||
return value.toNumber()
|
||||
}
|
||||
return value || 0
|
||||
}
|
||||
|
||||
public onLike(command: any): void {
|
||||
const data = command.data
|
||||
this.eventsRaw?.like?.forEach((d) => {
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.like?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
type: EventDataTypes.Like,
|
||||
uname: data.uname,
|
||||
uid: data.uid,
|
||||
msg: '为直播间点赞',
|
||||
price: 0,
|
||||
num: 1,
|
||||
time: Date.now(),
|
||||
guard_level: 0,
|
||||
fans_medal_level: data.medal_info?.medal_level ?? 0,
|
||||
fans_medal_name: data.medal_info?.medal_name ?? '',
|
||||
fans_medal_wearing_status: data.medal_info?.is_lighted === 1,
|
||||
uface: data.uface.replace('http://', 'https://'),
|
||||
open_id: '',
|
||||
ouid: GuidUtils.numToGuid(data.uid),
|
||||
},
|
||||
command,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public onScDel(command: any): void {
|
||||
const data = command.data
|
||||
this.eventsRaw?.scDel?.forEach((d) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { OpenLiveInfo } from '@/api/api-models'
|
||||
import { KeepLiveWS } from 'bilibili-live-ws/browser'
|
||||
import { LiveWS } from 'bilibili-live-danmaku'
|
||||
import { clearInterval, setInterval } from 'worker-timers'
|
||||
import { EventDataTypes } from '@/api/api-models'
|
||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||
@@ -41,17 +41,35 @@ export default class OpenLiveClient extends BaseDanmakuClient {
|
||||
protected async initClient(): Promise<{ success: boolean, message: string }> {
|
||||
const auth = await this.getAuthInfo()
|
||||
if (auth.data) {
|
||||
const chatClient = new KeepLiveWS(auth.data.anchor_info.room_id, {
|
||||
const chatClient = new LiveWS(auth.data.anchor_info.room_id, {
|
||||
authBody: JSON.parse(auth.data.websocket_info.auth_body),
|
||||
address: auth.data.websocket_info.wss_link[0],
|
||||
})
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_DM', cmd => this.onDanmaku(cmd))
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_GIFT', cmd => this.onGift(cmd))
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_GUARD', cmd => this.onGuard(cmd))
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_SC', cmd => this.onSC(cmd))
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_LIVE_ROOM_ENTER', cmd => this.onEnter(cmd))
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_SUPER_CHAT_DEL', cmd => this.onScDel(cmd))
|
||||
chatClient.on('live', () => {
|
||||
chatClient.addEventListener('MESSAGE', cmd => {
|
||||
switch (cmd.data.cmd as string) {
|
||||
case 'LIVE_OPEN_PLATFORM_DM':
|
||||
this.onDanmaku(cmd.data)
|
||||
break
|
||||
case 'LIVE_OPEN_PLATFORM_GIFT':
|
||||
this.onGift(cmd.data)
|
||||
break
|
||||
case 'LIVE_OPEN_PLATFORM_GUARD':
|
||||
this.onGuard(cmd.data)
|
||||
break
|
||||
case 'LIVE_OPEN_PLATFORM_SC':
|
||||
this.onSC(cmd.data)
|
||||
break
|
||||
case 'LIVE_OPEN_PLATFORM_LIVE_ROOM_ENTER':
|
||||
this.onEnter(cmd.data)
|
||||
break
|
||||
case 'LIVE_OPEN_PLATFORM_SUPER_CHAT_DEL':
|
||||
this.onScDel(cmd.data)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
chatClient.addEventListener('CONNECT_SUCCESS', () => {
|
||||
console.log(
|
||||
`[${this.type}] 已连接房间: ${auth.data?.anchor_info.room_id}`,
|
||||
)
|
||||
@@ -270,6 +288,10 @@ export default class OpenLiveClient extends BaseDanmakuClient {
|
||||
})
|
||||
}
|
||||
|
||||
public onLike(_command: any): void {
|
||||
// OpenLiveClient does not support like events
|
||||
}
|
||||
|
||||
public onScDel(command: any): void {
|
||||
const data = command.data as SCDelInfo
|
||||
this.eventsRaw.scDel?.forEach((d) => {
|
||||
|
||||
@@ -5,6 +5,8 @@ import App from './App.vue'
|
||||
import { InitVTsuru } from './data/Initializer'
|
||||
import emitter from './mitt'
|
||||
import router from './router'
|
||||
import { isTauri } from './data/constants'
|
||||
import { startHeartbeat } from './client/data/initialize'
|
||||
|
||||
loader.config({
|
||||
'paths': {
|
||||
@@ -24,5 +26,8 @@ const app = createApp(App)
|
||||
app.use(router).use(pinia).mount('#app')
|
||||
|
||||
InitVTsuru()
|
||||
if (isTauri()) {
|
||||
startHeartbeat();
|
||||
}
|
||||
|
||||
window.$mitt = emitter
|
||||
|
||||
Reference in New Issue
Block a user