add web-fetcher

This commit is contained in:
2024-02-28 16:28:21 +08:00
parent 40fdc93fec
commit f1c06deffd
18 changed files with 527 additions and 84 deletions

View File

@@ -7,7 +7,7 @@ import { ref } from 'vue'
import { APIRoot, AccountInfo, FunctionTypes } from './api-models'
import { useRoute } from 'vue-router'
export const ACCOUNT = ref<AccountInfo>()
export const ACCOUNT = ref<AccountInfo>({} as AccountInfo)
export const isLoadingAccount = ref(true)
const route = useRoute()

View File

@@ -30,6 +30,20 @@ export interface UserInfo {
streamerInfo?: StreamerModel
}
}
export interface EventFetcherStateModel {
online: boolean
status: { [errorCode: string]: string }
version?: string
todayReceive: number
useCookie: boolean
type: EventFetcherType
}
export enum EventFetcherType {
Application,
OBS,
Server,
}
export interface AccountInfo extends UserInfo {
isEmailVerified: boolean
isBiliVerified: boolean
@@ -41,11 +55,7 @@ export interface AccountInfo extends UserInfo {
biliAuthCode?: string
biliAuthCodeStatus: BiliAuthCodeStatusType
eventFetcherOnline: boolean
eventFetcherStatus: string
eventFetcherStatusV3: { [errorCode: string]: string }
eventFetcherTodayReceive: number
eventFetcherVersion?: string
eventFetcherState: EventFetcherStateModel
nextSendEmailTime?: number
isServerFetcherOnline: boolean

View File

@@ -23,7 +23,7 @@ export async function QueryPostAPIWithParams<T>(
const url = new URL(urlString)
url.search = getParams(params)
headers ??= []
headers?.push(['Authorization', `Bearer ${cookie.value}`])
if (cookie.value) headers?.push(['Authorization', `Bearer ${cookie.value}`])
if (contentType) headers?.push(['Content-Type', contentType])
@@ -55,7 +55,7 @@ export async function QueryGetAPI<T>(
url.search = getParams(params)
if (cookie.value) {
headers ??= []
headers?.push(['Authorization', `Bearer ${cookie.value}`])
if (cookie.value) headers?.push(['Authorization', `Bearer ${cookie.value}`])
}
try {
const data = await fetch(url.toString(), {
@@ -81,6 +81,9 @@ function getParams(params?: [string, string][]) {
if (urlParams.has('as')) {
resultParams.set('as', urlParams.get('as') || '')
}
if (urlParams.has('token')) {
resultParams.set('token', urlParams.get('token') || '')
}
return resultParams.toString()
}
export async function QueryPostPaginationAPI<T>(url: string, body?: unknown): Promise<APIRoot<PaginationResponse<T>>> {

View File

@@ -1,17 +1,18 @@
<script setup lang="ts">
import { useAccount } from '@/api/account'
import { EventFetcherType } from '@/api/api-models'
import { FlashCheckmark16Filled, Info24Filled } from '@vicons/fluent'
import { NAlert, NButton, NDivider, NIcon, NTag, NText, NTooltip } from 'naive-ui'
import { computed } from 'vue'
const accountInfo = useAccount()
const state = accountInfo.value?.eventFetcherState
const status = computed(() => {
if (!accountInfo.value) return 'error'
if (accountInfo.value.eventFetcherOnline == true || accountInfo.value.isServerFetcherOnline == true) {
if (accountInfo.value.eventFetcherStatus) {
if (state.online == true) {
if (state.status == undefined || state.status == null) {
return 'warning'
} else if (Object.keys(accountInfo.value.eventFetcherStatusV3 ?? {}).length > 0) {
} else if (Object.keys(state.status ?? {}).length > 0) {
return 'warning'
} else {
return 'success'
@@ -23,7 +24,7 @@ const status = computed(() => {
</script>
<template>
<NAlert :type="status">
<NAlert :type="status" v-if="status">
<template #header>
EVENT-FETCHER 状态
<NTooltip>
@@ -50,7 +51,7 @@ const status = computed(() => {
<template #trigger>
<NTag size="small" :color="{ borderColor: 'white', textColor: 'white', color: '#4b6159' }">
<NIcon :component="FlashCheckmark16Filled" />
{{ accountInfo?.eventFetcherVersion ?? '未知' }}
{{ state.type == EventFetcherType.OBS ? 'OBS/网页端' : state.version ?? '未知' }}
</NTag>
</template>
你所使用的版本
@@ -58,7 +59,7 @@ const status = computed(() => {
<NDivider vertical />
</template>
<NTag :type="status">
<template v-if="accountInfo?.eventFetcherOnline == true && accountInfo?.eventFetcherStatus">
<template v-if="state?.online == true && (state?.status == null || state?.status == undefined)">
此版本已过期, 请更新
<NTooltip trigger="click">
<template #trigger>
@@ -75,19 +76,17 @@ const status = computed(() => {
</NText>
| 今日已接收
<NText color="white" strong>
{{ accountInfo?.eventFetcherTodayReceive }}
{{ state.todayReceive }}
</NText>
</template>
<template v-else-if="status == 'warning'">
<template v-if="accountInfo?.eventFetcherStatusV3">
异常: {{ Object.values(accountInfo.eventFetcherStatusV3).join('; ') }}
</template>
<template v-if="state.status"> 异常: {{ Object.values(state.status).join('; ') }} </template>
</template>
<template v-else-if="status == 'info'"> 未连接 </template>
</template>
</NTag>
<template v-if="accountInfo?.eventFetcherOnline != true">
<template v-if="!state.online">
<NDivider vertical />
<NButton
type="info"

View File

@@ -159,6 +159,7 @@ interface DanmakuEventsMap {
gift: (arg1: GiftInfo, arg2?: any) => void
sc: (arg1: SCInfo, arg2?: any) => void
guard: (arg1: GuardInfo, arg2?: any) => void
all: (arg1: any) => void
}
export default class DanmakuClient {
@@ -167,54 +168,83 @@ export default class DanmakuClient {
}
private client: any
private authInfo: AuthInfo | null
private timer: any | undefined
private isStarting = false
public authInfo: AuthInfo | null
public roomAuthInfo = ref<RoomAuthInfo>({} as RoomAuthInfo)
public authCode: string | undefined
public isRunning: boolean = false
private events: {
danmaku: ((arg1: DanmakuInfo, arg2?: any) => void)[]
gift: ((arg1: GiftInfo, arg2?: any) => void)[]
sc: ((arg1: SCInfo, arg2?: any) => void)[]
guard: ((arg1: GuardInfo, arg2?: any) => void)[]
all: ((arg1: any) => void)[]
} = {
danmaku: [],
gift: [],
sc: [],
guard: [],
all: [],
}
private eventsAsModel: {
danmaku: ((arg1: EventModel, arg2?: any) => void)[]
gift: ((arg1: EventModel, arg2?: any) => void)[]
sc: ((arg1: EventModel, arg2?: any) => void)[]
guard: ((arg1: EventModel, arg2?: any) => void)[]
all: ((arg1: any) => void)[]
} = {
danmaku: [],
gift: [],
sc: [],
guard: [],
all: [],
}
public async Start(): Promise<{ success: boolean; message: string }> {
if (!this.client) {
console.log('[OPEN-LIVE] 正在启动弹幕客户端')
const result = await this.initClient()
if (result.success) {
this.timer = setInterval(() => {
this.sendHeartbeat()
}, 20 * 1000)
}
return result
} else {
console.warn('[OPEN-LIVE] 弹幕客户端已被启动过')
if (this.isRunning) {
return {
success: false,
message: '弹幕客户端已启动',
message: '弹幕客户端已启动',
}
}
if (this.isStarting) {
return {
success: false,
message: '弹幕客户端正在启动',
}
}
this.isStarting = true
try {
if (!this.client) {
console.log('[OPEN-LIVE] 正在启动弹幕客户端')
const result = await this.initClient()
if (result.success) {
this.isRunning = true
this.timer ??= setInterval(() => {
this.sendHeartbeat()
}, 20 * 1000)
}
return result
} else {
console.warn('[OPEN-LIVE] 弹幕客户端已被启动过')
return {
success: false,
message: '弹幕客户端已被启动过',
}
}
} finally {
this.isStarting = false
}
}
public Stop() {
if (!this.isRunning) {
return
}
this.isRunning = false
if (this.client) {
console.log('[OPEN-LIVE] 正在停止弹幕客户端')
this.client.stop()
@@ -223,35 +253,51 @@ export default class DanmakuClient {
}
if (this.timer) {
clearInterval(this.timer)
this.timer = undefined
}
this.events = {
danmaku: [],
gift: [],
sc: [],
guard: [],
all: [],
}
this.eventsAsModel = {
danmaku: [],
gift: [],
sc: [],
guard: [],
all: [],
}
}
private sendHeartbeat() {
if (this.client) {
const query = this.authInfo
? QueryPostAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat', this.authInfo)
: QueryGetAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat-internal')
query.then((data) => {
if (data.code != 200) {
console.error('[OPEN-LIVE] 心跳失败')
this.client.stop()
this.client = null
this.initClient()
}
})
if (!this.isRunning) {
clearInterval(this.timer)
this.timer = undefined
return
}
const query = this.authInfo
? QueryPostAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat', this.authInfo)
: QueryGetAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat-internal')
query.then((data) => {
if (data.code != 200) {
console.error('[OPEN-LIVE] 心跳失败, 将重新连接')
this.client?.stop()
this.client = null
this.initClient()
}
})
}
private onRawMessage = (command: any) => {
this.eventsAsModel.all?.forEach((d) => {
d(command)
})
this.events.all?.forEach((d) => {
d(command)
})
}
private onDanmaku = (command: any) => {
const data = command.data as DanmakuInfo
@@ -378,7 +424,8 @@ export default class DanmakuClient {
public onEvent(eventName: 'gift', listener: (arg1: EventModel, arg2?: any) => void): this
public onEvent(eventName: 'sc', listener: (arg1: EventModel, arg2?: any) => void): this
public onEvent(eventName: 'guard', listener: (arg1: EventModel, arg2?: any) => void): this
public onEvent(eventName: 'danmaku' | 'gift' | 'sc' | 'guard', listener: (...args: any[]) => void): this {
public onEvent(eventName: 'all', listener: (arg1: any) => void): this
public onEvent(eventName: 'danmaku' | 'gift' | 'sc' | 'guard' | 'all', listener: (...args: any[]) => void): this {
if (!this.eventsAsModel[eventName]) {
this.eventsAsModel[eventName] = []
}
@@ -418,7 +465,7 @@ export default class DanmakuClient {
message: '',
}
} else {
console.log('[OPEN-LIVE] 无法开启场次')
console.log('[OPEN-LIVE] 无法开启场次: ' + auth.message)
return {
success: false,
message: auth.message,
@@ -456,5 +503,6 @@ export default class DanmakuClient {
LIVE_OPEN_PLATFORM_SEND_GIFT: this.onGift.bind(this),
LIVE_OPEN_PLATFORM_SUPER_CHAT: this.onSC.bind(this),
LIVE_OPEN_PLATFORM_GUARD: this.onGuard.bind(this),
RAW_MESSAGE: this.onRawMessage.bind(this),
}
}

View File

@@ -134,6 +134,13 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
}
this.onDelSuperChat({ ids })
}
rawMessageCallback(command) {
if (!this.onRawMessage) {
return
}
this.onRawMessage(command)
}
}
const CMD_CALLBACK_MAP = {
@@ -142,4 +149,5 @@ const CMD_CALLBACK_MAP = {
LIVE_OPEN_PLATFORM_GUARD: ChatClientDirectOpenLive.prototype.guardCallback,
LIVE_OPEN_PLATFORM_SUPER_CHAT: ChatClientDirectOpenLive.prototype.superChatCallback,
LIVE_OPEN_PLATFORM_SUPER_CHAT_DEL: ChatClientDirectOpenLive.prototype.superChatDelCallback,
RAW_MESSAGE: ChatClientDirectOpenLive.prototype.rawMessageCallback,
}

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// @ts-nocheck
import { BrotliDecode } from './brotli_decode'
import { setInterval, clearInterval, setTimeout, clearTimeout } from 'worker-timers'
@@ -145,7 +147,7 @@ export default class ChatClientOfficialBase {
this.sendAuth()
this.heartbeatTimerId = setInterval(this.sendHeartbeat.bind(this), HEARTBEAT_INTERVAL)
this.refreshReceiveTimeoutTimer()
console.log('ws 已连接')
//console.log('ws 已连接')
}
sendHeartbeat() {
@@ -270,7 +272,10 @@ export default class ChatClientOfficialBase {
// 没压缩过的直接反序列化
if (body.length !== 0) {
try {
body = JSON.parse(textDecoder.decode(body))
const text = textDecoder.decode(body)
this.onRawMessage(text)
this.CMD_CALLBACK_MAP['RAW_MESSAGE']?.call(this, text)
body = JSON.parse(text)
this.handlerCommand(body)
} catch (e) {
console.error('body=', body)
@@ -299,6 +304,7 @@ export default class ChatClientOfficialBase {
}
}
}
onRawMessage(command) {}
handlerCommand(command) {
let cmd = command.cmd || ''

View File

@@ -2,8 +2,8 @@ import DefaultIndexTemplateVue from '@/views/view/indexTemplate/DefaultIndexTemp
import { defineAsyncComponent, ref } from 'vue'
const debugAPI = import.meta.env.VITE_DEBUG_API
const releseAPI = `https://vtsuru.suki.club/api/`
const failoverAPI = `https://failover-api.vtsuru.suki.club/api/`
const releseAPI = `https://vtsuru.suki.club/`
const failoverAPI = `https://failover-api.vtsuru.suki.club/`
export const isBackendUsable = ref(true)
@@ -13,32 +13,38 @@ export const IMGUR_URL = FILE_BASE_URL + '/imgur/'
export const THINGS_URL = FILE_BASE_URL + '/things/'
export const apiFail = ref(false)
export const BASE_API = () =>
process.env.NODE_ENV === 'development' ? debugAPI : apiFail.value ? failoverAPI : releseAPI
export const BASE_API = {
toString: () =>
(process.env.NODE_ENV === 'development' ? debugAPI : apiFail.value ? failoverAPI : releseAPI) + 'api/',
}
export const FETCH_API = 'https://fetch.vtsuru.live/'
export const BASE_HUB_URL = {
toString: () =>
(process.env.NODE_ENV === 'development' ? debugAPI : apiFail.value ? failoverAPI : releseAPI) + 'hub/',
}
export const TURNSTILE_KEY = '0x4AAAAAAAETUSAKbds019h0'
export const USER_API_URL = { toString: () => `${BASE_API()}user/` }
export const ACCOUNT_API_URL = { toString: () => `${BASE_API()}account/` }
export const BILI_API_URL = { toString: () => `${BASE_API()}bili/` }
export const SONG_API_URL = { toString: () => `${BASE_API()}song-list/` }
export const NOTIFACTION_API_URL = { toString: () => `${BASE_API()}notifaction/` }
export const QUESTION_API_URL = { toString: () => `${BASE_API()}qa/` }
export const LOTTERY_API_URL = { toString: () => `${BASE_API()}lottery/` }
export const HISTORY_API_URL = { toString: () => `${BASE_API()}history/` }
export const SCHEDULE_API_URL = { toString: () => `${BASE_API()}schedule/` }
export const VIDEO_COLLECT_API_URL = { toString: () => `${BASE_API()}video-collect/` }
export const OPEN_LIVE_API_URL = { toString: () => `${BASE_API()}open-live/` }
export const SONG_REQUEST_API_URL = { toString: () => `${BASE_API()}song-request/` }
export const QUEUE_API_URL = { toString: () => `${BASE_API()}queue/` }
export const EVENT_API_URL = { toString: () => `${BASE_API()}event/` }
export const LIVE_API_URL = { toString: () => `${BASE_API()}live/` }
export const FEEDBACK_API_URL = { toString: () => `${BASE_API()}feedback/` }
export const MUSIC_REQUEST_API_URL = { toString: () => `${BASE_API()}music-request/` }
export const VTSURU_API_URL = { toString: () => `${BASE_API()}vtsuru/` }
export const POINT_API_URL = { toString: () => `${BASE_API()}point/` }
export const BILI_AUTH_API_URL = { toString: () => `${BASE_API()}bili-auth/` }
export const USER_API_URL = { toString: () => `${BASE_API}user/` }
export const ACCOUNT_API_URL = { toString: () => `${BASE_API}account/` }
export const BILI_API_URL = { toString: () => `${BASE_API}bili/` }
export const SONG_API_URL = { toString: () => `${BASE_API}song-list/` }
export const NOTIFACTION_API_URL = { toString: () => `${BASE_API}notifaction/` }
export const QUESTION_API_URL = { toString: () => `${BASE_API}qa/` }
export const LOTTERY_API_URL = { toString: () => `${BASE_API}lottery/` }
export const HISTORY_API_URL = { toString: () => `${BASE_API}history/` }
export const SCHEDULE_API_URL = { toString: () => `${BASE_API}schedule/` }
export const VIDEO_COLLECT_API_URL = { toString: () => `${BASE_API}video-collect/` }
export const OPEN_LIVE_API_URL = { toString: () => `${BASE_API}open-live/` }
export const SONG_REQUEST_API_URL = { toString: () => `${BASE_API}song-request/` }
export const QUEUE_API_URL = { toString: () => `${BASE_API}queue/` }
export const EVENT_API_URL = { toString: () => `${BASE_API}event/` }
export const LIVE_API_URL = { toString: () => `${BASE_API}live/` }
export const FEEDBACK_API_URL = { toString: () => `${BASE_API}feedback/` }
export const MUSIC_REQUEST_API_URL = { toString: () => `${BASE_API}music-request/` }
export const VTSURU_API_URL = { toString: () => `${BASE_API}vtsuru/` }
export const POINT_API_URL = { toString: () => `${BASE_API}point/` }
export const BILI_AUTH_API_URL = { toString: () => `${BASE_API}bili-auth/` }
export const ScheduleTemplateMap = {
'': {

View File

@@ -19,7 +19,7 @@ let currentVersion: string
let isHaveNewVersion = false
const { notification } = createDiscreteApi(['notification'])
QueryGetAPI<string>(BASE_API() + 'vtsuru/version')
QueryGetAPI<string>(BASE_API + 'vtsuru/version')
.then((version) => {
if (version.code == 200) {
currentVersion = version.data
@@ -42,7 +42,7 @@ QueryGetAPI<string>(BASE_API() + 'vtsuru/version')
if (isHaveNewVersion) {
return
}
QueryGetAPI<string>(BASE_API() + 'vtsuru/version').then((keepCheckData) => {
QueryGetAPI<string>(BASE_API + 'vtsuru/version').then((keepCheckData) => {
if (keepCheckData.code == 200 && keepCheckData.data != currentVersion) {
isHaveNewVersion = true
currentVersion = version.data

View File

@@ -42,5 +42,13 @@ export default {
title: '棉花糖展示',
},
},
{
path: 'web-fetcher',
name: 'obs-web-fetcher',
component: () => import('@/views/obs/WebFetcherOBS.vue'),
meta: {
title: '弹幕收集器 (OBS版',
},
},
],
}

150
src/store/useWebFetcher.ts Normal file
View File

@@ -0,0 +1,150 @@
import DanmakuClient from '@/data/DanmakuClient'
import { BASE_HUB_URL } from '@/data/constants'
import * as signalR from '@microsoft/signalr'
import * as msgpack from '@microsoft/signalr-protocol-msgpack'
import { useLocalStorage } from '@vueuse/core'
import { format } from 'date-fns'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
export const useWebFetcher = defineStore('WebFetcher', () => {
const cookie = useLocalStorage('JWT_Token', '')
const route = useRoute()
const client = new DanmakuClient(null)
const events: string[] = []
const isStarted = ref(false)
let timer: any
let signalRClient: signalR.HubConnection | null = null
let disconnectedByServer = false
async function Start() {
if (isStarted.value) {
return
}
while (!(await connectSignalR())) {
console.log('[WEB-FETCHER] 连接失败, 5秒后重试')
await new Promise((resolve) => setTimeout(resolve, 5000))
}
}
function Stop() {
if (!isStarted.value) {
return
}
isStarted.value = false
client.Stop()
if (timer) {
clearInterval(timer)
timer = undefined
}
signalRClient?.stop()
signalRClient = null
}
async function connectDanmakuClient() {
console.log('[WEB-FETCHER] 正在连接弹幕客户端...')
const result = await client.Start()
if (result.success) {
console.log('[WEB-FETCHER] 加载完成, 开始监听弹幕')
client.onEvent('all', onGetDanmakus)
timer ??= setInterval(() => {
sendEvents()
}, 1500)
} else {
console.log('[WEB-FETCHER] 弹幕客户端启动失败: ' + result.message)
}
return result
}
async function connectSignalR() {
console.log('[WEB-FETCHER] 正在连接到 vtsuru 服务器...')
const connection = new signalR.HubConnectionBuilder()
.withUrl(BASE_HUB_URL + 'web-fetcher?token=' + route.query.token, {
headers: {
Authorization: `Bearer ${cookie.value}`,
},
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
})
.withAutomaticReconnect([0, 2000, 10000, 30000])
.withHubProtocol(new msgpack.MessagePackHubProtocol())
.build()
connection.on('Disconnect', (reason: unknown) => {
console.log('[WEB-FETCHER] 被服务器断开连接: ' + reason)
disconnectedByServer = true
connection.stop()
signalRClient = null
})
connection.on('ConnectClient', async () => {
if (client.isRunning) {
return
}
let result = await connectDanmakuClient()
while (!result.success) {
console.log('[WEB-FETCHER] 弹幕客户端启动失败, 5秒后重试')
await new Promise((resolve) => setTimeout(resolve, 5000))
result = await connectDanmakuClient()
}
isStarted.value = true
})
connection.onclose(reconnect)
try {
await connection.start()
console.log('[WEB-FETCHER] 已连接到 vtsuru 服务器')
signalRClient = connection
return true
} catch (e) {
console.log('[WEB-FETCHER] 无法连接到 vtsuru 服务器: ' + e)
return false
}
}
async function reconnect() {
if (disconnectedByServer) {
return
}
try {
await signalRClient?.start()
console.log('[WEB-FETCHER] 已重新连接')
} catch (err) {
console.log(err)
setTimeout(reconnect, 5000) // 如果连接失败则每5秒尝试一次重新启动连接
}
}
function onGetDanmakus(command: any) {
events.push(command)
}
async function sendEvents() {
if (signalRClient?.state !== 'Connected') {
return
}
let tempEvents: string[] = []
let count = events.length
if (events.length > 20) {
tempEvents = events.slice(0, 20)
count = 20
} else {
tempEvents = events
count = events.length
}
if (tempEvents.length > 0) {
const result = await signalRClient?.invoke<{
Success: boolean
Message: string
}>('UploadEvents', tempEvents, false)
if (result?.Success) {
events.splice(0, count)
console.log(`[WEB-FETCHER] <${format(new Date(), 'HH:mm:ss')}> 上传了 ${count} 条弹幕`)
} else {
console.error('[WEB-FETCHER] 上传弹幕失败: ' + result?.Message)
}
}
}
return {
Start,
Stop,
client,
isStarted,
}
})

View File

@@ -454,7 +454,7 @@ onMounted(() => {
</script>
<template>
<NLayout v-if="accountInfo" style="height: 100vh">
<NLayout v-if="accountInfo.id" style="height: 100vh">
<NLayoutHeader bordered style="height: 50px; padding: 10px 15px 5px 15px">
<NPageHeader>
<template #title>

View File

@@ -11,7 +11,7 @@ const props = defineProps<{
const accountInfo = useAccount()
const message = useMessage()
let client = new DanmakuClient(null)
const client = new DanmakuClient(null)
const isClientLoading = ref(true)
onMounted(async () => {

View File

@@ -54,12 +54,15 @@ onMounted(() => {
}, 1000)
//@ts-expect-error 这里获取不了
window.obsstudio.onVisibilityChange = function (visibility: boolean) {
visiable.value = visibility
}
//@ts-expect-error 这里获取不了
window.obsstudio.onActiveChange = function (a: boolean) {
active.value = a
if (window.obsstudio) {
//@ts-expect-error 这里获取不了
window.obsstudio.onVisibilityChange = function (visibility: boolean) {
visiable.value = visibility
}
//@ts-expect-error 这里获取不了
window.obsstudio.onActiveChange = function (a: boolean) {
active.value = a
}
}
})
onUnmounted(() => {

View File

@@ -0,0 +1,41 @@
<script setup lang="ts">
import { useWebFetcher } from '@/store/useWebFetcher'
import { onMounted, onUnmounted, ref } from 'vue'
const webFetcher = useWebFetcher()
onMounted(() => {
webFetcher.Start()
})
onUnmounted(() => {})
</script>
<template>
<div
class="web-fetcher-status"
:style="{
backgroundColor: webFetcher.isStarted ? '#6dc56d' : '#e34a4a',
}"
></div>
</template>
<style scoped>
.web-fetcher-status {
width: 30px;
height: 30px;
border-radius: 50%;
box-shadow: 0 0 5px 1px rgba(0, 0, 0, 0.2);
border: 3px solid #00000033;
animation: animated-border 3s infinite;
transition: background-color 0.5s;
}
@keyframes animated-border {
0% {
box-shadow: 0 0 0px #589580;
}
100% {
box-shadow: 0 0 0 6px rgba(255, 255, 255, 0);
}
}
</style>

0
src/views/others Normal file
View File