chore: format code style and update linting configuration

This commit is contained in:
Megghy
2025-10-02 10:38:23 +08:00
parent 6fd046adcd
commit 758549d29d
253 changed files with 16258 additions and 15833 deletions

View File

@@ -1,10 +1,10 @@
import { BiliAuthModel, ResponsePointGoodModel } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import { BILI_AUTH_API_URL, POINT_API_URL } from '@/data/constants'
import type { MessageApiInjection } from 'naive-ui/es/message/src/MessageProvider'
import type { BiliAuthModel, ResponsePointGoodModel } from '@/api/api-models'
import { useStorage } from '@vueuse/core'
import { MessageApiInjection } from 'naive-ui/es/message/src/MessageProvider'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import { BILI_AUTH_API_URL, POINT_API_URL } from '@/data/constants'
export const useBiliAuth = defineStore('BiliAuth', () => {
const biliAuth = ref<BiliAuthModel>({} as BiliAuthModel)
@@ -36,13 +36,13 @@ export const useBiliAuth = defineStore('BiliAuth', () => {
async function getAuthInfo() {
try {
isLoading.value = true
if(!currentToken.value) return
await QueryBiliAuthGetAPI<BiliAuthModel>(BILI_AUTH_API_URL + 'info').then((data) => {
if (!currentToken.value) return
await QueryBiliAuthGetAPI<BiliAuthModel>(`${BILI_AUTH_API_URL}info`).then((data) => {
if (data.code == 200) {
biliAuth.value = data.data
console.log('[bili-auth] 已获取 Bilibili 认证信息')
// 将token加入到biliTokens
const index = biliTokens.value.findIndex((t) => t.id == biliAuth.value.id)
const index = biliTokens.value.findIndex(t => t.id == biliAuth.value.id)
if (index >= 0) {
biliTokens.value[index] = {
id: biliAuth.value.id,
@@ -50,7 +50,7 @@ export const useBiliAuth = defineStore('BiliAuth', () => {
name: biliAuth.value.name,
uId: biliAuth.value.userId,
}
//console.log('更新已存在的认证账户: ' + biliAuth.value.userId)
// console.log('更新已存在的认证账户: ' + biliAuth.value.userId)
} else {
biliTokens.value.push({
id: biliAuth.value.id,
@@ -58,35 +58,35 @@ export const useBiliAuth = defineStore('BiliAuth', () => {
name: biliAuth.value.name,
uId: biliAuth.value.userId,
})
console.log('添加新的认证账户: ' + biliAuth.value.userId)
console.log(`添加新的认证账户: ${biliAuth.value.userId}`)
}
isInvalid.value = false
return true
} else {
console.error('[bili-auth] 无法获取 Bilibili 认证信息: ' + data.message)
console.error(`[bili-auth] 无法获取 Bilibili 认证信息: ${data.message}`)
isInvalid.value = true
logout()
//message.error('无法获取 Bilibili 认证信息: ' + data.message)
// message.error('无法获取 Bilibili 认证信息: ' + data.message)
}
})
} catch (err) {
console.error('[bili-auth] 无法获取 Bilibili 认证信息: ' + err)
//message.error('无法获取 Bilibili 认证信息: ' + err)
console.error(`[bili-auth] 无法获取 Bilibili 认证信息: ${err}`)
// message.error('无法获取 Bilibili 认证信息: ' + err)
} finally {
isLoading.value = false
}
return false
}
function QueryBiliAuthGetAPI<T>(url: string, params?: any, headers?: [string, string][]) {
async function QueryBiliAuthGetAPI<T>(url: string, params?: any, headers?: [string, string][]) {
headers ??= []
if (headers.find((h) => h[0] == 'Bili-Auth') == null) {
if (headers.find(h => h[0] == 'Bili-Auth') == null) {
headers.push(['Bili-Auth', currentToken.value ?? ''])
}
return QueryGetAPI<T>(url, params, headers)
}
function QueryBiliAuthPostAPI<T>(url: string, body?: unknown, headers?: [string, string][]) {
async function QueryBiliAuthPostAPI<T>(url: string, body?: unknown, headers?: [string, string][]) {
headers ??= []
if (headers.find((h) => h[0] == 'Bili-Auth') == null) {
if (headers.find(h => h[0] == 'Bili-Auth') == null) {
headers.push(['Bili-Auth', currentToken.value ?? ''])
}
return QueryPostAPI<T>(url, body, headers)
@@ -94,14 +94,14 @@ export const useBiliAuth = defineStore('BiliAuth', () => {
async function GetSpecificPoint(id: number) {
try {
const data = await QueryBiliAuthGetAPI<number>(POINT_API_URL + 'user/get-point', { id: id })
const data = await QueryBiliAuthGetAPI<number>(`${POINT_API_URL}user/get-point`, { id })
if (data.code == 200) {
return data.data
} else {
console.error('[point] 无法获取在指定直播间拥有的积分: ' + data.message)
console.error(`[point] 无法获取在指定直播间拥有的积分: ${data.message}`)
}
} catch (err) {
console.error('[point] 无法获取在指定直播间拥有的积分: ' + err)
console.error(`[point] 无法获取在指定直播间拥有的积分: ${err}`)
}
return null
}
@@ -110,24 +110,24 @@ export const useBiliAuth = defineStore('BiliAuth', () => {
return []
}
try {
const resp = await QueryGetAPI<ResponsePointGoodModel[]>(POINT_API_URL + 'get-goods', {
id: id,
const resp = await QueryGetAPI<ResponsePointGoodModel[]>(`${POINT_API_URL}get-goods`, {
id,
})
if (resp.code == 200) {
return resp.data
} else {
message?.error('无法获取数据: ' + resp.message)
console.error('无法获取数据: ' + resp.message)
message?.error(`无法获取数据: ${resp.message}`)
console.error(`无法获取数据: ${resp.message}`)
}
} catch (err) {
message?.error('无法获取数据: ' + err)
console.error('无法获取数据: ' + err)
message?.error(`无法获取数据: ${err}`)
console.error(`无法获取数据: ${err}`)
}
return []
}
function logout() {
biliAuth.value = {} as BiliAuthModel
biliTokens.value = biliTokens.value.filter((t) => t.token != currentToken.value)
biliTokens.value = biliTokens.value.filter(t => t.token != currentToken.value)
currentToken.value = ''
console.log('[bili-auth] 已登出 Bilibili 认证')
}

View File

@@ -1,36 +1,38 @@
import { EventModel, OpenLiveInfo } from '@/api/api-models';
import BaseDanmakuClient from '@/data/DanmakuClients/BaseDanmakuClient';
import DirectClient, { DirectClientAuthInfo } from '@/data/DanmakuClients/DirectClient';
import OpenLiveClient, { AuthInfo } from '@/data/DanmakuClients/OpenLiveClient';
import { defineStore } from 'pinia';
import { computed, ref, shallowRef } from 'vue'; // 引入 shallowRef
import type { EventModel, OpenLiveInfo } from '@/api/api-models'
import type BaseDanmakuClient from '@/data/DanmakuClients/BaseDanmakuClient'
import type { DirectClientAuthInfo } from '@/data/DanmakuClients/DirectClient'
import type { AuthInfo } from '@/data/DanmakuClients/OpenLiveClient'
import { defineStore } from 'pinia'
import { computed, ref, shallowRef } from 'vue' // 引入 shallowRef
import DirectClient from '@/data/DanmakuClients/DirectClient'
import OpenLiveClient from '@/data/DanmakuClients/OpenLiveClient'
// 定义支持的事件名称类型
type EventName = 'danmaku' | 'gift' | 'sc' | 'guard' | 'enter' | 'scDel' | 'follow';
type EventNameWithAll = EventName | 'all';
type EventName = 'danmaku' | 'gift' | 'sc' | 'guard' | 'enter' | 'scDel' | 'follow'
type EventNameWithAll = EventName | 'all'
// 定义监听器函数类型
type Listener = (arg1: any, arg2: any) => void;
type EventListener = (arg1: EventModel, arg2: any) => void;
type Listener = (arg1: any, arg2: any) => void
type EventListener = (arg1: EventModel, arg2: any) => void
// --- 修正点: 确保 AllEventListener 定义符合要求 ---
// AllEventListener 明确只接受一个参数
type AllEventListener = (arg1: any) => void;
type AllEventListener = (arg1: any) => void
// --- 修正点: 定义一个统一的监听器类型,用于内部实现签名 ---
type GenericListener = Listener | AllEventListener;
type GenericListener = Listener | AllEventListener
export const useDanmakuClient = defineStore('DanmakuClient', () => {
// 使用 shallowRef 存储 danmakuClient 实例, 性能稍好
const danmakuClient = shallowRef<BaseDanmakuClient | undefined>(new OpenLiveClient()); // 默认实例化一个 OpenLiveClient
const danmakuClient = shallowRef<BaseDanmakuClient | undefined>(new OpenLiveClient()) // 默认实例化一个 OpenLiveClient
// 连接状态: 'waiting'-等待初始化, 'connecting'-连接中, 'connected'-已连接
const state = ref<'waiting' | 'connecting' | 'connected'>('waiting');
const state = ref<'waiting' | 'connecting' | 'connected'>('waiting')
// 计算属性, 判断是否已连接
const connected = computed(() => state.value === 'connected');
const connected = computed(() => state.value === 'connected')
// 存储开放平台认证信息 (如果使用 OpenLiveClient)
const authInfo = ref<OpenLiveInfo>();
const authInfo = ref<OpenLiveInfo>()
// 初始化锁, 防止并发初始化
let isInitializing = false;
let isInitializing = false
/**
* @description 注册事件监听器 (特定于 OpenLiveClient 的原始事件 或 调用 onEvent)
@@ -39,23 +41,23 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => {
* @remarks 对于 OpenLiveClient, 直接操作其内部 events; 对于其他客户端, 调用 onEvent.
*/
// --- 修正点: 保持重载签名不变 ---
function onEvent(eventName: 'all', listener: AllEventListener): void;
function onEvent(eventName: EventName, listener: Listener): void;
function onEvent(eventName: 'all', listener: AllEventListener): void
function onEvent(eventName: EventName, listener: Listener): void
// --- 修正点: 实现签名使用联合类型,并在内部进行类型断言 ---
function onEvent(eventName: keyof BaseDanmakuClient['eventsAsModel'], listener: GenericListener): void {
if (!danmakuClient.value) {
console.warn("[DanmakuClient] 尝试在客户端初始化之前调用 'onEvent'。");
return;
console.warn('[DanmakuClient] 尝试在客户端初始化之前调用 \'onEvent\'。')
return
}
try {
if (eventName === 'all') {
// 对于 'all' 事件, 直接使用 AllEventListener 类型
danmakuClient.value.eventsAsModel[eventName].push(listener as AllEventListener);
danmakuClient.value.eventsAsModel[eventName].push(listener as AllEventListener)
} else {
danmakuClient.value.eventsAsModel[eventName].push(listener);
danmakuClient.value.eventsAsModel[eventName].push(listener)
}
} catch (error) {
console.error(`[DanmakuClient] 注册事件监听器: ${eventName} 失败: ${error}`);
console.error(`[DanmakuClient] 注册事件监听器: ${eventName} 失败: ${error}`)
}
}
@@ -66,45 +68,44 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => {
* @remarks 监听器存储在 Store 中, 会在客户端重连后自动重新附加.
*/
// --- 修正点: 保持重载签名不变 ---
function on(eventName: 'all', listener: AllEventListener): void;
function on(eventName: EventName, listener: Listener): void;
function on(eventName: 'all', listener: AllEventListener): void
function on(eventName: EventName, listener: Listener): void
// --- 修正点: 实现签名使用联合类型,并在内部进行类型断言 ---
function on(eventName: EventNameWithAll, listener: GenericListener): void {
if (!danmakuClient.value) {
console.warn("[DanmakuClient] 尝试在客户端初始化之前调用 'on'。");
return;
console.warn('[DanmakuClient] 尝试在客户端初始化之前调用 \'on\'。')
return
}
danmakuClient.value.eventsRaw[eventName].push(listener);
danmakuClient.value.eventsRaw[eventName].push(listener)
}
/**
* @description 移除事件监听器 (模型化数据, 从 Store 中移除)
* @param eventName 事件名称 ('danmaku', 'gift', 'sc', 'guard', 'all')
* @param listener 要移除的回调函数
*/
// --- 修正点: 保持重载签名不变 ---
function offEvent(eventName: 'all', listener: AllEventListener): void;
function offEvent(eventName: EventName, listener: Listener): void;
function offEvent(eventName: 'all', listener: AllEventListener): void
function offEvent(eventName: EventName, listener: Listener): void
// --- 修正点: 实现签名使用联合类型,并在内部进行类型断言 ---
function offEvent(eventName: keyof BaseDanmakuClient['eventsRaw'], listener: GenericListener): void {
if (!danmakuClient.value) {
console.warn("[DanmakuClient] 尝试在客户端初始化之前调用 'offEvent'。");
return;
console.warn('[DanmakuClient] 尝试在客户端初始化之前调用 \'offEvent\'。')
return
}
if (eventName === 'all') {
// 对于 'all' 事件, 直接使用 AllEventListener 类型
const modelListeners = danmakuClient.value.eventsAsModel[eventName] as AllEventListener[];
const index = modelListeners.indexOf(listener as AllEventListener);
const modelListeners = danmakuClient.value.eventsAsModel[eventName] as AllEventListener[]
const index = modelListeners.indexOf(listener as AllEventListener)
if (index > -1) {
modelListeners.splice(index, 1);
modelListeners.splice(index, 1)
}
} else {
const index = danmakuClient.value.eventsAsModel[eventName].indexOf(listener);
const index = danmakuClient.value.eventsAsModel[eventName].indexOf(listener)
if (index > -1) {
danmakuClient.value.eventsAsModel[eventName].splice(index, 1);
danmakuClient.value.eventsAsModel[eventName].splice(index, 1)
} else {
console.warn(`[DanmakuClient] 试图移除未注册的监听器: ${listener}`);
console.warn(`[DanmakuClient] 试图移除未注册的监听器: ${listener}`)
}
}
}
@@ -115,17 +116,17 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => {
* @param listener 要移除的回调函数
*/
// --- 修正点: 保持重载签名不变 ---
function off(eventName: 'all', listener: AllEventListener): void;
function off(eventName: EventName, listener: Listener): void;
function off(eventName: 'all', listener: AllEventListener): void
function off(eventName: EventName, listener: Listener): void
// --- 修正点: 实现签名使用联合类型,并在内部进行类型断言 ---
function off(eventName: EventNameWithAll, listener: GenericListener): void {
if (!danmakuClient.value) {
console.warn("[DanmakuClient] 尝试在客户端初始化之前调用 'off'。");
return;
console.warn('[DanmakuClient] 尝试在客户端初始化之前调用 \'off\'。')
return
}
const index = danmakuClient.value.eventsRaw[eventName].indexOf(listener);
const index = danmakuClient.value.eventsRaw[eventName].indexOf(listener)
if (index > -1) {
danmakuClient.value.eventsRaw[eventName].splice(index, 1);
danmakuClient.value.eventsRaw[eventName].splice(index, 1)
}
// 直接从 eventsRaw 中移除监听器
}
@@ -136,7 +137,7 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => {
* @returns
*/
async function initOpenlive(auth?: AuthInfo) {
return initClient(new OpenLiveClient(auth));
return initClient(new OpenLiveClient(auth))
}
/**
@@ -145,31 +146,29 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => {
* @returns
*/
async function initDirect(auth: DirectClientAuthInfo) {
return initClient(new DirectClient(auth));
return initClient(new DirectClient(auth))
}
// 辅助函数: 从客户端的 eventsAsModel 移除单个监听器
// --- 修正点: 修正 detachListenerFromClient 的签名和实现以处理联合类型 ---
function detachListenerFromClient(client: BaseDanmakuClient, eventName: EventNameWithAll, listener: GenericListener): void {
if (client.eventsAsModel[eventName]) {
if (eventName === 'all') {
const modelListeners = client.eventsAsModel[eventName] as AllEventListener[];
const index = modelListeners.indexOf(listener as AllEventListener);
const modelListeners = client.eventsAsModel[eventName] as AllEventListener[]
const index = modelListeners.indexOf(listener as AllEventListener)
if (index > -1) {
modelListeners.splice(index, 1);
modelListeners.splice(index, 1)
}
} else {
const modelListeners = client.eventsAsModel[eventName] as Listener[];
const index = modelListeners.indexOf(listener as Listener);
const modelListeners = client.eventsAsModel[eventName] as Listener[]
const index = modelListeners.indexOf(listener as Listener)
if (index > -1) {
modelListeners.splice(index, 1);
modelListeners.splice(index, 1)
}
}
}
}
/**
* @description 通用客户端初始化逻辑
* @param client 要初始化的客户端实例
@@ -179,122 +178,120 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => {
// 防止重复初始化或在非等待状态下初始化
if (isInitializing) {
while (isInitializing) {
await new Promise((resolve) => setTimeout(resolve, 100)); // 等待初始化完成
await new Promise(resolve => setTimeout(resolve, 100)) // 等待初始化完成
}
return useDanmakuClient(); // 如果已连接,则视为“成功”
return useDanmakuClient() // 如果已连接,则视为“成功”
}
if (state.value === 'connected') {
return useDanmakuClient(); // 如果已连接,则视为“成功”
return useDanmakuClient() // 如果已连接,则视为“成功”
}
isInitializing = true;
state.value = 'connecting';
console.log('[DanmakuClient] 开始初始化...');
isInitializing = true
state.value = 'connecting'
console.log('[DanmakuClient] 开始初始化...')
let oldEventsAsModel = danmakuClient.value?.eventsAsModel;
let oldEventsRaw = danmakuClient.value?.eventsRaw;
let oldEventsAsModel = danmakuClient.value?.eventsAsModel
let oldEventsRaw = danmakuClient.value?.eventsRaw
if (!oldEventsAsModel || Object.keys(oldEventsAsModel).length === 0) {
oldEventsAsModel = client.createEmptyEventModelListeners();
oldEventsAsModel = client.createEmptyEventModelListeners()
}
if (!oldEventsRaw || Object.keys(oldEventsRaw).length === 0) {
oldEventsRaw = client.createEmptyRawEventlisteners();
oldEventsRaw = client.createEmptyRawEventlisteners()
}
// 先停止并清理旧客户端 (如果存在)
if (danmakuClient.value) {
console.log('[DanmakuClient] 正在处理旧的客户端实例...');
console.log('[DanmakuClient] 正在处理旧的客户端实例...')
if (danmakuClient.value.state === 'connected') {
await disposeClientInstance(danmakuClient.value);
await disposeClientInstance(danmakuClient.value)
}
}
// 设置新的客户端实例
danmakuClient.value = client;
danmakuClient.value = client
// 确保新客户端有空的监听器容器 (BaseDanmakuClient 应负责初始化)
danmakuClient.value.eventsAsModel = oldEventsAsModel;
danmakuClient.value.eventsRaw = oldEventsRaw;
danmakuClient.value.eventsAsModel = oldEventsAsModel
danmakuClient.value.eventsRaw = oldEventsRaw
// 通常在 client 实例化或 Start 时处理,或者在 attachListenersToClient 中确保存在
let connectSuccess = false;
const maxRetries = 5; // Example: Limit retries
let retryCount = 0;
let connectSuccess = false
const maxRetries = 5 // Example: Limit retries
let retryCount = 0
const attemptConnect = async () => {
if (!danmakuClient.value) return false; // Guard against client being disposed during wait
if (!danmakuClient.value) return false // Guard against client being disposed during wait
try {
console.log(`[DanmakuClient] 尝试连接 (第 ${retryCount + 1} 次)...`);
const result = await danmakuClient.value.Start(); // 启动连接
console.log(`[DanmakuClient] 尝试连接 (第 ${retryCount + 1} 次)...`)
const result = await danmakuClient.value.Start() // 启动连接
if (result.success) {
// 连接成功
authInfo.value = danmakuClient.value instanceof OpenLiveClient ? danmakuClient.value.roomAuthInfo : undefined;
state.value = 'connected';
authInfo.value = danmakuClient.value instanceof OpenLiveClient ? danmakuClient.value.roomAuthInfo : undefined
state.value = 'connected'
// 将 Store 中存储的监听器 (来自 onEvent) 附加到新连接的客户端的 eventsAsModel
console.log('[DanmakuClient] 初始化成功');
connectSuccess = true;
return true; // 连接成功, 退出重试循环
console.log('[DanmakuClient] 初始化成功')
connectSuccess = true
return true // 连接成功, 退出重试循环
} else {
// 连接失败
console.error(`[DanmakuClient] 连接尝试失败: ${result.message}`);
return false; // 继续重试
console.error(`[DanmakuClient] 连接尝试失败: ${result.message}`)
return false // 继续重试
}
} catch (error) {
// 捕获 Start() 可能抛出的异常
console.error(`[DanmakuClient] 连接尝试期间发生异常:`, error);
return false; // 继续重试
console.error(`[DanmakuClient] 连接尝试期间发生异常:`, error)
return false // 继续重试
}
};
}
// 循环尝试连接, 直到成功或达到重试次数
while (!connectSuccess && retryCount < maxRetries) {
if (state.value !== 'connecting') { // 检查状态是否在循环开始时改变
console.log('[DanmakuClient] 初始化被外部中止。');
isInitializing = false;
//return false; // 初始化被中止
break;
console.log('[DanmakuClient] 初始化被外部中止。')
isInitializing = false
// return false; // 初始化被中止
break
}
if (!(await attemptConnect())) {
retryCount++;
retryCount++
if (retryCount < maxRetries && state.value === 'connecting') {
console.log(`[DanmakuClient] 5 秒后重试连接... (${retryCount}/${maxRetries})`);
await new Promise((resolve) => setTimeout(resolve, 5000));
console.log(`[DanmakuClient] 5 秒后重试连接... (${retryCount}/${maxRetries})`)
await new Promise(resolve => setTimeout(resolve, 5000))
// 再次检查在等待期间状态是否改变
if (state.value !== 'connecting') {
console.log('[DanmakuClient] 在重试等待期间初始化被中止。');
isInitializing = false;
//return false; // 初始化被中止
break;
console.log('[DanmakuClient] 在重试等待期间初始化被中止。')
isInitializing = false
// return false; // 初始化被中止
break
}
} else if (state.value === 'connecting') {
console.error(`[DanmakuClient] 已达到最大连接重试次数 (${maxRetries})。初始化失败。`);
console.error(`[DanmakuClient] 已达到最大连接重试次数 (${maxRetries})。初始化失败。`)
// 连接失败,重置状态
await dispose(); // 清理资源
await dispose() // 清理资源
// state.value = 'waiting'; // dispose 会设置状态
// isInitializing = false; // dispose 会设置
// return false; // 返回失败状态
break;
break
}
}
}
isInitializing = false; // 无论成功失败,初始化过程结束
isInitializing = false // 无论成功失败,初始化过程结束
// 返回最终的连接状态
return useDanmakuClient();
return useDanmakuClient()
}
// 封装停止和清理客户端实例的逻辑
async function disposeClientInstance(client: BaseDanmakuClient) {
try {
console.log('[DanmakuClient] 正在停止客户端实例...');
client.Stop(); // 停止客户端连接和内部处理
console.log('[DanmakuClient] 正在停止客户端实例...')
client.Stop() // 停止客户端连接和内部处理
// 可能需要添加额外的清理逻辑,例如移除所有监听器
// client.eventsAsModel = client.createEmptyEventModelListeners(); // 清空监听器
// client.eventsRaw = client.createEmptyRawEventlisteners(); // 清空监听器
console.log('[DanmakuClient] 客户端实例已停止。');
console.log('[DanmakuClient] 客户端实例已停止。')
} catch (error) {
console.error('[DanmakuClient] 停止客户端时出错:', error);
console.error('[DanmakuClient] 停止客户端时出错:', error)
}
}
@@ -302,31 +299,31 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => {
* @description 停止并清理当前客户端连接和资源
*/
async function dispose() {
console.log('[DanmakuClient] 正在停止并清理客户端...');
isInitializing = false; // 允许在 dispose 后重新初始化
console.log('[DanmakuClient] 正在停止并清理客户端...')
isInitializing = false // 允许在 dispose 后重新初始化
if (danmakuClient.value) {
await disposeClientInstance(danmakuClient.value);
//danmakuClient.value = undefined; // 保留, 用户再次获取event
await disposeClientInstance(danmakuClient.value)
// danmakuClient.value = undefined; // 保留, 用户再次获取event
}
state.value = 'waiting'; // 重置状态为等待
authInfo.value = undefined; // 清理认证信息
state.value = 'waiting' // 重置状态为等待
authInfo.value = undefined // 清理认证信息
// isInitializing = false; // 在函数开始处已设置
console.log('[DanmakuClient] 已处理。');
console.log('[DanmakuClient] 已处理。')
// 注意: Store 中 listeners.value (来自 onEvent) 默认不清空, 以便重连后恢复
}
return {
danmakuClient, // 当前弹幕客户端实例 (shallowRef)
state, // 连接状态 ('waiting', 'connecting', 'connected')
authInfo, // OpenLive 认证信息 (ref)
connected, // 是否已连接 (computed)
onEvent, // 注册事件监听器 (模型化数据, 存储于 Store)
offEvent, // 移除事件监听器 (模型化数据, 从 Store 移除)
on, // 注册事件监听器 (直接操作 client.eventsRaw)
off, // 移除事件监听器 (直接操作 client.eventsRaw 或调用 offEvent)
initOpenlive, // 初始化 OpenLive 客户端
initDirect, // 初始化 Direct 客户端
dispose, // 停止并清理客户端
};
});
state, // 连接状态 ('waiting', 'connecting', 'connected')
authInfo, // OpenLive 认证信息 (ref)
connected, // 是否已连接 (computed)
onEvent, // 注册事件监听器 (模型化数据, 存储于 Store)
offEvent, // 移除事件监听器 (模型化数据, 从 Store 移除)
on, // 注册事件监听器 (直接操作 client.eventsRaw)
off, // 移除事件监听器 (直接操作 client.eventsRaw 或调用 offEvent)
initOpenlive, // 初始化 OpenLive 客户端
initDirect, // 初始化 Direct 客户端
dispose, // 停止并清理客户端
}
})

View File

@@ -1,4 +1,5 @@
import {
import type { MessageApiInjection } from 'naive-ui/es/message/src/MessageProvider'
import type {
ForumCommentModel,
ForumCommentSortTypes,
ForumModel,
@@ -8,12 +9,11 @@ import {
ForumTopicModel,
ForumTopicSortTypes,
} from '@/api/models/forum'
import { QueryGetAPI, QueryGetPaginationAPI, QueryPostAPI } from '@/api/query'
import { FORUM_API_URL } from '@/data/constants'
import { createDiscreteApi } from 'naive-ui'
import { MessageApiInjection } from 'naive-ui/es/message/src/MessageProvider'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { QueryGetAPI, QueryGetPaginationAPI, QueryPostAPI } from '@/api/query'
import { FORUM_API_URL } from '@/data/constants'
export const useForumStore = defineStore('forum', () => {
const { message } = createDiscreteApi(['message'])
@@ -28,15 +28,15 @@ export const useForumStore = defineStore('forum', () => {
async function GetForumInfo(owner: number) {
try {
isLoading.value = true
const data = await QueryGetAPI<ForumModel>(FORUM_API_URL + 'get-forum', { owner: owner })
const data = await QueryGetAPI<ForumModel>(`${FORUM_API_URL}get-forum`, { owner })
if (data.code == 200) {
return data.data
} else if (data.code != 404) {
message?.error('无法获取数据: ' + data.message)
message?.error(`无法获取数据: ${data.message}`)
return undefined
}
} catch (err) {
message?.error('无法获取数据: ' + err)
message?.error(`无法获取数据: ${err}`)
return undefined
} finally {
isLoading.value = false
@@ -45,15 +45,15 @@ export const useForumStore = defineStore('forum', () => {
async function GetManagedForums() {
try {
isLoading.value = true
const data = await QueryGetAPI<ForumModel[]>(FORUM_API_URL + 'get-managed-forums')
const data = await QueryGetAPI<ForumModel[]>(`${FORUM_API_URL}get-managed-forums`)
if (data.code == 200) {
return data.data
} else {
message?.error('无法获取数据: ' + data.message)
message?.error(`无法获取数据: ${data.message}`)
return undefined
}
} catch (err) {
message?.error('无法获取数据: ' + err)
message?.error(`无法获取数据: ${err}`)
return undefined
} finally {
isLoading.value = false
@@ -69,12 +69,12 @@ export const useForumStore = defineStore('forum', () => {
) {
try {
isLoading.value = true
const data = await QueryGetPaginationAPI<ForumTopicBaseModel[]>(FORUM_API_URL + 'get-topics', {
const data = await QueryGetPaginationAPI<ForumTopicBaseModel[]>(`${FORUM_API_URL}get-topics`, {
owner,
pageSize: ps,
page: pn,
sort,
section: section,
section,
})
if (data.code == 200) {
return {
@@ -83,13 +83,13 @@ export const useForumStore = defineStore('forum', () => {
more: data.more,
}
} else {
message?.error('无法获取数据: ' + data.message)
console.error('无法获取数据: ' + data.message)
message?.error(`无法获取数据: ${data.message}`)
console.error(`无法获取数据: ${data.message}`)
return undefined
}
} catch (err) {
message?.error('无法获取数据: ' + err)
console.error('无法获取数据: ' + err)
message?.error(`无法获取数据: ${err}`)
console.error(`无法获取数据: ${err}`)
return undefined
} finally {
isLoading.value = false
@@ -98,17 +98,17 @@ export const useForumStore = defineStore('forum', () => {
async function GetTopicDetail(topic: number) {
try {
isLoading.value = true
const data = await QueryGetAPI<ForumTopicModel>(FORUM_API_URL + 'get-topic', { topic: topic })
const data = await QueryGetAPI<ForumTopicModel>(`${FORUM_API_URL}get-topic`, { topic })
if (data.code == 200) {
return data.data
} else {
message?.error('无法获取数据: ' + data.message)
console.error('无法获取数据: ' + data.message)
message?.error(`无法获取数据: ${data.message}`)
console.error(`无法获取数据: ${data.message}`)
return undefined
}
} catch (err) {
message?.error('无法获取数据: ' + err)
console.error('无法获取数据: ' + err)
message?.error(`无法获取数据: ${err}`)
console.error(`无法获取数据: ${err}`)
return undefined
} finally {
isLoading.value = false
@@ -117,7 +117,7 @@ export const useForumStore = defineStore('forum', () => {
async function GetComments(topic: number, pn: number, ps: number, sort: ForumCommentSortTypes) {
try {
isLoading.value = true
const data = await QueryGetPaginationAPI<ForumCommentModel[]>(FORUM_API_URL + 'get-comments', {
const data = await QueryGetPaginationAPI<ForumCommentModel[]>(`${FORUM_API_URL}get-comments`, {
topic,
pageSize: ps,
page: pn,
@@ -126,13 +126,13 @@ export const useForumStore = defineStore('forum', () => {
if (data.code == 200) {
return data
} else {
console.error('无法获取数据: ' + data.message)
message?.error('无法获取数据: ' + data.message)
console.error(`无法获取数据: ${data.message}`)
message?.error(`无法获取数据: ${data.message}`)
return undefined
}
} catch (err) {
console.error('无法获取数据: ' + err)
message?.error('无法获取数据: ' + err)
console.error(`无法获取数据: ${err}`)
message?.error(`无法获取数据: ${err}`)
return undefined
} finally {
isLoading.value = false
@@ -141,18 +141,18 @@ export const useForumStore = defineStore('forum', () => {
async function ApplyToForum(owner: number) {
try {
isLoading.value = true
const data = await QueryGetAPI<ForumModel>(FORUM_API_URL + 'apply', { owner: owner })
const data = await QueryGetAPI<ForumModel>(`${FORUM_API_URL}apply`, { owner })
if (data.code == 200) {
message?.success('已提交申请, 等待管理员审核')
return true
} else {
message?.error('无法获取数据: ' + data.message)
console.error('无法获取数据: ' + data.message)
message?.error(`无法获取数据: ${data.message}`)
console.error(`无法获取数据: ${data.message}`)
return false
}
} catch (err) {
message?.error('无法获取数据: ' + err)
console.error('无法获取数据: ' + err)
message?.error(`无法获取数据: ${err}`)
console.error(`无法获取数据: ${err}`)
return false
} finally {
isLoading.value = false
@@ -162,58 +162,58 @@ export const useForumStore = defineStore('forum', () => {
async function PostTopic(topic: ForumPostTopicModel, token: string) {
try {
isLoading.value = true
const data = await QueryPostAPI<ForumTopicModel>(FORUM_API_URL + 'post-topic', topic, [['Turnstile', token]])
const data = await QueryPostAPI<ForumTopicModel>(`${FORUM_API_URL}post-topic`, topic, [['Turnstile', token]])
if (data.code == 200) {
message?.success('发布成功')
return data.data
} else {
message?.error('发布失败: ' + data.message)
console.error('发布失败: ' + data.message)
message?.error(`发布失败: ${data.message}`)
console.error(`发布失败: ${data.message}`)
return undefined
}
} catch (err) {
message?.error('发布失败: ' + err)
console.error('发布失败: ' + err)
message?.error(`发布失败: ${err}`)
console.error(`发布失败: ${err}`)
return undefined
} finally {
isLoading.value = false
}
}
async function PostComment(model: { topic: number; content: string }, token: string) {
async function PostComment(model: { topic: number, content: string }, token: string) {
try {
isLoading.value = true
const data = await QueryPostAPI<ForumCommentModel>(FORUM_API_URL + 'post-comment', model, [['Turnstile', token]])
const data = await QueryPostAPI<ForumCommentModel>(`${FORUM_API_URL}post-comment`, model, [['Turnstile', token]])
if (data.code == 200) {
message?.success('评论成功')
return data.data
} else {
message?.error('评论失败: ' + data.message)
console.error('评论失败: ' + data.message)
message?.error(`评论失败: ${data.message}`)
console.error(`评论失败: ${data.message}`)
return undefined
}
} catch (err) {
message?.error('评论失败: ' + err)
console.error('评论失败: ' + err)
message?.error(`评论失败: ${err}`)
console.error(`评论失败: ${err}`)
return undefined
} finally {
isLoading.value = false
}
}
async function PostReply(model: { comment: number; content: string; replyTo?: number }, token: string) {
async function PostReply(model: { comment: number, content: string, replyTo?: number }, token: string) {
try {
isLoading.value = true
const data = await QueryPostAPI<ForumCommentModel>(FORUM_API_URL + 'post-reply', model, [['Turnstile', token]])
const data = await QueryPostAPI<ForumCommentModel>(`${FORUM_API_URL}post-reply`, model, [['Turnstile', token]])
if (data.code == 200) {
message?.success('评论成功')
return data.data
} else {
message?.error('评论失败: ' + data.message)
console.error('评论失败: ' + data.message)
message?.error(`评论失败: ${data.message}`)
console.error(`评论失败: ${data.message}`)
return undefined
}
} catch (err) {
message?.error('评论失败: ' + err)
console.error('评论失败: ' + err)
message?.error(`评论失败: ${err}`)
console.error(`评论失败: ${err}`)
return undefined
} finally {
isLoading.value = false
@@ -222,18 +222,18 @@ export const useForumStore = defineStore('forum', () => {
async function LikeTopic(topic: number, like: boolean) {
try {
isLikeLoading.value = true
const data = await QueryGetAPI(FORUM_API_URL + 'like-topic', { topic: topic, like: like })
const data = await QueryGetAPI(`${FORUM_API_URL}like-topic`, { topic, like })
if (data.code == 200) {
//message?.success('已点赞')
// message?.success('已点赞')
return true
} else {
message?.error('点赞失败: ' + data.message)
console.error('点赞失败: ' + data.message)
message?.error(`点赞失败: ${data.message}`)
console.error(`点赞失败: ${data.message}`)
return false
}
} catch (err) {
message?.error('点赞失败: ' + err)
console.error('点赞失败: ' + err)
message?.error(`点赞失败: ${err}`)
console.error(`点赞失败: ${err}`)
return false
} finally {
isLikeLoading.value = false
@@ -242,18 +242,18 @@ export const useForumStore = defineStore('forum', () => {
async function LikeComment(comment: number, like: boolean) {
try {
isLikeLoading.value = true
const data = await QueryGetAPI(FORUM_API_URL + 'like-comment', { comment: comment, like: like })
const data = await QueryGetAPI(`${FORUM_API_URL}like-comment`, { comment, like })
if (data.code == 200) {
//message?.success('已点赞')
// message?.success('已点赞')
return true
} else {
message?.error('点赞失败: ' + data.message)
console.error('点赞失败: ' + data.message)
message?.error(`点赞失败: ${data.message}`)
console.error(`点赞失败: ${data.message}`)
return false
}
} catch (err) {
message?.error('点赞失败: ' + err)
console.error('点赞失败: ' + err)
message?.error(`点赞失败: ${err}`)
console.error(`点赞失败: ${err}`)
return false
} finally {
isLikeLoading.value = false
@@ -277,18 +277,18 @@ export const useForumStore = defineStore('forum', () => {
async function SetTopicTop(topic: number, top: boolean) {
try {
isLoading.value = true
const data = await QueryGetAPI(FORUM_API_URL + 'manage/set-topic-top', { topic: topic, top: top })
const data = await QueryGetAPI(`${FORUM_API_URL}manage/set-topic-top`, { topic, top })
if (data.code == 200) {
message?.success('完成')
return true
} else {
message?.error('操作失败: ' + data.message)
console.error('操作失败: ' + data.message)
message?.error(`操作失败: ${data.message}`)
console.error(`操作失败: ${data.message}`)
return false
}
} catch (err) {
message?.error('操作失败: ' + err)
console.error('操作失败: ' + err)
message?.error(`操作失败: ${err}`)
console.error(`操作失败: ${err}`)
return false
} finally {
isLoading.value = false
@@ -297,18 +297,18 @@ export const useForumStore = defineStore('forum', () => {
async function DelTopic(topic: number) {
try {
isLoading.value = true
const data = await QueryGetAPI(FORUM_API_URL + 'manage/delete-topic', { topic: topic })
const data = await QueryGetAPI(`${FORUM_API_URL}manage/delete-topic`, { topic })
if (data.code == 200) {
message?.success('删除成功')
return true
} else {
message?.error('删除失败: ' + data.message)
console.error('删除失败: ' + data.message)
message?.error(`删除失败: ${data.message}`)
console.error(`删除失败: ${data.message}`)
return false
}
} catch (err) {
message?.error('删除失败: ' + err)
console.error('删除失败: ' + err)
message?.error(`删除失败: ${err}`)
console.error(`删除失败: ${err}`)
return false
} finally {
isLoading.value = false
@@ -317,18 +317,18 @@ export const useForumStore = defineStore('forum', () => {
async function DelComment(comment: number) {
try {
isLoading.value = true
const data = await QueryGetAPI(FORUM_API_URL + 'delete-comment', { comment: comment })
const data = await QueryGetAPI(`${FORUM_API_URL}delete-comment`, { comment })
if (data.code == 200) {
message?.success('删除成功')
return true
} else {
message?.error('删除失败: ' + data.message)
console.error('删除失败: ' + data.message)
message?.error(`删除失败: ${data.message}`)
console.error(`删除失败: ${data.message}`)
return false
}
} catch (err) {
message?.error('删除失败: ' + err)
console.error('删除失败: ' + err)
message?.error(`删除失败: ${err}`)
console.error(`删除失败: ${err}`)
return false
} finally {
isLoading.value = false
@@ -337,18 +337,18 @@ export const useForumStore = defineStore('forum', () => {
async function DelReply(reply: number) {
try {
isLoading.value = true
const data = await QueryGetAPI(FORUM_API_URL + 'manage/delete-reply', { reply: reply })
const data = await QueryGetAPI(`${FORUM_API_URL}manage/delete-reply`, { reply })
if (data.code == 200) {
message?.success('删除成功')
return true
} else {
message?.error('删除失败: ' + data.message)
console.error('删除失败: ' + data.message)
message?.error(`删除失败: ${data.message}`)
console.error(`删除失败: ${data.message}`)
return false
}
} catch (err) {
message?.error('删除失败: ' + err)
console.error('删除失败: ' + err)
message?.error(`删除失败: ${err}`)
console.error(`删除失败: ${err}`)
return false
} finally {
isLoading.value = false
@@ -357,18 +357,18 @@ export const useForumStore = defineStore('forum', () => {
async function RestoreComment(comment: number) {
try {
isLoading.value = true
const data = await QueryGetAPI(FORUM_API_URL + 'manage/restore-comment', { comment: comment })
const data = await QueryGetAPI(`${FORUM_API_URL}manage/restore-comment`, { comment })
if (data.code == 200) {
message?.success('恢复成功')
return true
} else {
message?.error('恢复失败: ' + data.message)
console.error('恢复失败: ' + data.message)
message?.error(`恢复失败: ${data.message}`)
console.error(`恢复失败: ${data.message}`)
return false
}
} catch (err) {
message?.error('恢复失败: ' + err)
console.error('恢复失败: ' + err)
message?.error(`恢复失败: ${err}`)
console.error(`恢复失败: ${err}`)
return false
} finally {
isLoading.value = false
@@ -377,18 +377,18 @@ export const useForumStore = defineStore('forum', () => {
async function RestoreTopic(topic: number) {
try {
isLoading.value = true
const data = await QueryGetAPI(FORUM_API_URL + 'manage/restore-topic', { topic })
const data = await QueryGetAPI(`${FORUM_API_URL}manage/restore-topic`, { topic })
if (data.code == 200) {
message?.success('恢复成功')
return true
} else {
message?.error('恢复失败: ' + data.message)
console.error('恢复失败: ' + data.message)
message?.error(`恢复失败: ${data.message}`)
console.error(`恢复失败: ${data.message}`)
return false
}
} catch (err) {
message?.error('恢复失败: ' + err)
console.error('恢复失败: ' + err)
message?.error(`恢复失败: ${err}`)
console.error(`恢复失败: ${err}`)
return false
} finally {
isLoading.value = false
@@ -397,18 +397,18 @@ export const useForumStore = defineStore('forum', () => {
async function ConfirmApply(owner: number, id: number) {
try {
isLoading.value = true
const data = await QueryGetAPI(FORUM_API_URL + 'manage/confirm-apply', { forum: owner, id: id })
const data = await QueryGetAPI(`${FORUM_API_URL}manage/confirm-apply`, { forum: owner, id })
if (data.code == 200) {
message?.success('已通过申请')
return true
} else {
message?.error('确认失败: ' + data.message)
console.error('确认失败: ' + data.message)
message?.error(`确认失败: ${data.message}`)
console.error(`确认失败: ${data.message}`)
return false
}
} catch (err) {
message?.error('确认失败: ' + err)
console.error('确认失败: ' + err)
message?.error(`确认失败: ${err}`)
console.error(`确认失败: ${err}`)
return false
} finally {
isLoading.value = false

View File

@@ -1,59 +1,57 @@
// src/composables/useGamepad.ts
import { ref, onMounted, onUnmounted, reactive, readonly, Ref, DeepReadonly, computed, watch } from 'vue';
import { defineStore } from 'pinia';
import { useGamepad } from '@vueuse/core';
import type {
GamepadConnectionInfo,
RawGamepadState,
NormalizedGamepadState,
LogicalButton,
ButtonInputState,
StickInputState
} from '@/types/gamepad'; // 使用 @ 指向 src 目录
import { LogicalButtonsList } from '@/types/gamepad';
GamepadConnectionInfo,
LogicalButton,
NormalizedGamepadState,
} from '@/types/gamepad' // 使用 @ 指向 src 目录
import { useGamepad } from '@vueuse/core'
import { defineStore } from 'pinia'
// src/composables/useGamepad.ts
import { computed, reactive, readonly, ref, watch } from 'vue'
import { LogicalButtonsList } from '@/types/gamepad'
// 定义事件处理函数类型
type GamepadEventHandler = (gamepadInfo: GamepadConnectionInfo, index: number) => void;
type GamepadEventHandler = (gamepadInfo: GamepadConnectionInfo, index: number) => void
// 标准按钮映射根据标准布局将gamepad API的按钮索引映射到逻辑按钮
const standardButtonMap: Partial<Record<number, LogicalButton>> = {
0: 'ACTION_DOWN', // Xbox A / PS Cross / Nintendo B
1: 'ACTION_RIGHT', // Xbox B / PS Circle / Nintendo A
2: 'ACTION_LEFT', // Xbox X / PS Square / Nintendo Y
3: 'ACTION_UP', // Xbox Y / PS Triangle / Nintendo X
0: 'ACTION_DOWN', // Xbox A / PS Cross / Nintendo B
1: 'ACTION_RIGHT', // Xbox B / PS Circle / Nintendo A
2: 'ACTION_LEFT', // Xbox X / PS Square / Nintendo Y
3: 'ACTION_UP', // Xbox Y / PS Triangle / Nintendo X
4: 'LEFT_SHOULDER_1', // LB / L1
5: 'RIGHT_SHOULDER_1', // RB / R1
6: 'LEFT_SHOULDER_2', // LT / L2 (触发器)
6: 'LEFT_SHOULDER_2', // LT / L2 (触发器)
7: 'RIGHT_SHOULDER_2', // RT / R2 (触发器)
8: 'SELECT', // Xbox View / PS Select / Nintendo -
9: 'START', // Xbox Menu / PS Start / Nintendo +
8: 'SELECT', // Xbox View / PS Select / Nintendo -
9: 'START', // Xbox Menu / PS Start / Nintendo +
10: 'LEFT_STICK_PRESS', // 左摇杆按下
11: 'RIGHT_STICK_PRESS', // 右摇杆按下
12: 'DPAD_UP', // 方向键上
13: 'DPAD_DOWN', // 方向键下
14: 'DPAD_LEFT', // 方向键左
15: 'DPAD_RIGHT', // 方向键右
16: 'HOME', // Xbox Home / PS Home / Nintendo Home
17: 'PS_TOUCHPAD', // PS触摸板按下
12: 'DPAD_UP', // 方向键上
13: 'DPAD_DOWN', // 方向键下
14: 'DPAD_LEFT', // 方向键左
15: 'DPAD_RIGHT', // 方向键右
16: 'HOME', // Xbox Home / PS Home / Nintendo Home
17: 'PS_TOUCHPAD', // PS触摸板按下
// 18可能对应任天堂的截图按钮
18: 'NINTENDO_CAPTURE'
};
18: 'NINTENDO_CAPTURE',
}
export const useGamepadStore = defineStore('gamepad', () => {
const { gamepads, onConnected, onDisconnected } = useGamepad();
const { gamepads, onConnected, onDisconnected } = useGamepad()
const connectedGamepadInfo = ref<GamepadConnectionInfo | null>(null);
const activeGamepadIndex = ref<number | null>(null);
const connectedGamepadInfo = ref<GamepadConnectionInfo | null>(null)
const activeGamepadIndex = ref<number | null>(null)
// 存储自定义事件处理器
const connectedHandlers: Set<GamepadEventHandler> = new Set();
const disconnectedHandlers: Set<GamepadEventHandler> = new Set();
const connectedHandlers: Set<GamepadEventHandler> = new Set()
const disconnectedHandlers: Set<GamepadEventHandler> = new Set()
// 初始化所有按钮状态
const initialButtonStates = LogicalButtonsList.reduce((acc, key) => {
acc[key] = { pressed: false, value: 0 };
return acc;
}, {} as Record<LogicalButton, ButtonInputState>);
acc[key] = { pressed: false, value: 0 }
return acc
}, {} as Record<LogicalButton, ButtonInputState>)
// 手柄状态,包含按钮和摇杆
const normalizedGamepadState = reactive<NormalizedGamepadState>({
@@ -62,123 +60,123 @@ export const useGamepadStore = defineStore('gamepad', () => {
LEFT_STICK: { x: 0, y: 0 },
RIGHT_STICK: { x: 0, y: 0 },
},
});
})
// 计算属性:手柄是否已连接
const isGamepadConnected = computed(() => activeGamepadIndex.value !== null && !!gamepads.value[activeGamepadIndex.value]?.connected);
const isGamepadConnected = computed(() => activeGamepadIndex.value !== null && !!gamepads.value[activeGamepadIndex.value]?.connected)
// 重置手柄状态
const resetNormalizedState = () => {
Object.keys(normalizedGamepadState.buttons).forEach(key => {
const buttonKey = key as LogicalButton;
if (normalizedGamepadState.buttons[buttonKey]) {
normalizedGamepadState.buttons[buttonKey]!.pressed = false;
normalizedGamepadState.buttons[buttonKey]!.value = 0;
}
});
normalizedGamepadState.sticks.LEFT_STICK = { x: 0, y: 0 };
normalizedGamepadState.sticks.RIGHT_STICK = { x: 0, y: 0 };
};
Object.keys(normalizedGamepadState.buttons).forEach((key) => {
const buttonKey = key as LogicalButton
if (normalizedGamepadState.buttons[buttonKey]) {
normalizedGamepadState.buttons[buttonKey].pressed = false
normalizedGamepadState.buttons[buttonKey].value = 0
}
})
normalizedGamepadState.sticks.LEFT_STICK = { x: 0, y: 0 }
normalizedGamepadState.sticks.RIGHT_STICK = { x: 0, y: 0 }
}
// 更新手柄状态
const updateNormalizedState = (gamepad: Gamepad | undefined) => {
if (!gamepad || !gamepad.connected) {
resetNormalizedState();
return;
resetNormalizedState()
return
}
// 更新按钮状态
gamepad.buttons.forEach((button, index) => {
const logicalKey = standardButtonMap[index];
const logicalKey = standardButtonMap[index]
if (logicalKey && normalizedGamepadState.buttons[logicalKey]) {
normalizedGamepadState.buttons[logicalKey]!.pressed = button.pressed;
normalizedGamepadState.buttons[logicalKey]!.value = button.value;
normalizedGamepadState.buttons[logicalKey].pressed = button.pressed
normalizedGamepadState.buttons[logicalKey].value = button.value
}
});
})
// 更新摇杆状态
normalizedGamepadState.sticks.LEFT_STICK.x = gamepad.axes[0] ?? 0;
normalizedGamepadState.sticks.LEFT_STICK.y = gamepad.axes[1] ?? 0;
normalizedGamepadState.sticks.RIGHT_STICK.x = gamepad.axes[2] ?? 0;
normalizedGamepadState.sticks.RIGHT_STICK.y = gamepad.axes[3] ?? 0;
};
normalizedGamepadState.sticks.LEFT_STICK.x = gamepad.axes[0] ?? 0
normalizedGamepadState.sticks.LEFT_STICK.y = gamepad.axes[1] ?? 0
normalizedGamepadState.sticks.RIGHT_STICK.x = gamepad.axes[2] ?? 0
normalizedGamepadState.sticks.RIGHT_STICK.y = gamepad.axes[3] ?? 0
}
// 手柄连接事件处理
onConnected((index: number) => {
const gamepad = navigator.getGamepads()[index];
if (!gamepad) return;
const gamepad = navigator.getGamepads()[index]
if (!gamepad) return
console.log('手柄已连接:', gamepad.id, '索引:', index);
console.log('手柄已连接:', gamepad.id, '索引:', index)
// 如果当前没有活动的,或者连接的是同一个,则激活
if (activeGamepadIndex.value === null || activeGamepadIndex.value === index) {
activeGamepadIndex.value = index;
activeGamepadIndex.value = index
connectedGamepadInfo.value = {
id: gamepad.id,
mapping: gamepad.mapping
};
mapping: gamepad.mapping,
}
// 触发外部注册的连接事件处理器
if (connectedGamepadInfo.value) {
connectedHandlers.forEach(handler => {
connectedHandlers.forEach((handler) => {
try {
handler(connectedGamepadInfo.value!, index);
handler(connectedGamepadInfo.value!, index)
} catch (err) {
console.error('手柄连接事件处理器执行错误:', err);
console.error('手柄连接事件处理器执行错误:', err)
}
});
})
}
} else {
// 如果已有活动手柄,而新连接的手柄不是当前活动的,则忽略,或按需处理(例如允许切换)
console.log(`另一个手柄 (索引: ${activeGamepadIndex.value}) 已经处于活动状态`);
console.log(`另一个手柄 (索引: ${activeGamepadIndex.value}) 已经处于活动状态`)
}
});
})
// 手柄断开连接事件处理
onDisconnected((index: number) => {
const gamepadCache = gamepads.value[index];
if (!gamepadCache) return;
const gamepadCache = gamepads.value[index]
if (!gamepadCache) return
console.log('手柄已断开连接:', gamepadCache.id);
console.log('手柄已断开连接:', gamepadCache.id)
// 保存断开连接前的信息,用于触发事件
const disconnectedInfo = connectedGamepadInfo.value ? { ...connectedGamepadInfo.value } : null;
const disconnectedInfo = connectedGamepadInfo.value ? { ...connectedGamepadInfo.value } : null
if (activeGamepadIndex.value === index) {
activeGamepadIndex.value = null;
connectedGamepadInfo.value = null;
resetNormalizedState();
activeGamepadIndex.value = null
connectedGamepadInfo.value = null
resetNormalizedState()
// 触发外部注册的断开连接事件处理器
if (disconnectedInfo) {
disconnectedHandlers.forEach(handler => {
disconnectedHandlers.forEach((handler) => {
try {
handler(disconnectedInfo, index);
handler(disconnectedInfo, index)
} catch (err) {
console.error('手柄断开连接事件处理器执行错误:', err);
console.error('手柄断开连接事件处理器执行错误:', err)
}
});
})
}
// 尝试连接其他已连接的手柄 (VueUse 的 gamepads 数组会自动更新)
const nextGamepad = gamepads.value.find(gp => gp && gp.connected);
const nextGamepad = gamepads.value.find(gp => gp && gp.connected)
if (nextGamepad) {
activeGamepadIndex.value = nextGamepad.index;
activeGamepadIndex.value = nextGamepad.index
connectedGamepadInfo.value = {
id: nextGamepad.id,
mapping: nextGamepad.mapping
};
mapping: nextGamepad.mapping,
}
// 如果自动切换到其他手柄,也触发连接事件
connectedHandlers.forEach(handler => {
connectedHandlers.forEach((handler) => {
try {
handler(connectedGamepadInfo.value!, nextGamepad.index);
handler(connectedGamepadInfo.value!, nextGamepad.index)
} catch (err) {
console.error('手柄连接事件处理器执行错误:', err);
console.error('手柄连接事件处理器执行错误:', err)
}
});
})
}
}
});
})
// 监视 VueUse 的 gamepads 数组中的活动手柄
// VueUse 内部使用 rAF 来更新 gamepads 数组中的状态
@@ -187,38 +185,38 @@ export const useGamepadStore = defineStore('gamepad', () => {
// 确保 activeGamepadIndex.value 不为 null并且对应的 gamepad 存在
return activeGamepadIndex.value !== null && gamepads.value[activeGamepadIndex.value]
? gamepads.value[activeGamepadIndex.value]
: undefined;
: undefined
},
(activePad) => {
updateNormalizedState(activePad);
updateNormalizedState(activePad)
},
{ deep: true, immediate: true } // immediate: true 保证初始状态也被处理
);
{ deep: true, immediate: true }, // immediate: true 保证初始状态也被处理
)
// 对外提供的连接事件注册方法
const onGamepadConnected = (handler: GamepadEventHandler) => {
connectedHandlers.add(handler);
connectedHandlers.add(handler)
// 如果当前已有连接的手柄,立即触发一次事件
if (isGamepadConnected.value && connectedGamepadInfo.value && activeGamepadIndex.value !== null) {
handler(connectedGamepadInfo.value, activeGamepadIndex.value);
handler(connectedGamepadInfo.value, activeGamepadIndex.value)
}
// 返回取消注册的函数
return () => {
connectedHandlers.delete(handler);
};
};
connectedHandlers.delete(handler)
}
}
// 对外提供的断开连接事件注册方法
const onGamepadDisconnected = (handler: GamepadEventHandler) => {
disconnectedHandlers.add(handler);
disconnectedHandlers.add(handler)
// 返回取消注册的函数
return () => {
disconnectedHandlers.delete(handler);
};
};
disconnectedHandlers.delete(handler)
}
}
return {
connectedGamepadInfo: readonly(connectedGamepadInfo),
@@ -226,5 +224,5 @@ export const useGamepadStore = defineStore('gamepad', () => {
isGamepadConnected: readonly(isGamepadConnected),
onConnected: onGamepadConnected,
onDisconnected: onGamepadDisconnected,
};
});
}
})

View File

@@ -1,5 +1,5 @@
import type { LoadingBarApi } from 'naive-ui'
import { defineStore } from 'pinia'
import { LoadingBarApi } from 'naive-ui'
import { ref } from 'vue'
export const useLoadingBarStore = defineStore('provider', () => {

View File

@@ -1,15 +1,14 @@
import { DanmakuUserInfo, SongFrom, SongsInfo } from '@/api/api-models'
import { createNaiveUIApi, theme } from '@/Utils'
import type { DanmakuUserInfo, SongsInfo } from '@/api/api-models'
import { useStorage } from '@vueuse/core'
import { ConfigProviderProps, createDiscreteApi, zhCN } from 'naive-ui'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { SongFrom } from '@/api/api-models'
export type WaitMusicInfo = {
export interface WaitMusicInfo {
from: DanmakuUserInfo
music: SongsInfo
}
export type Music = {
export interface Music {
id: number
title: string
artist: string
@@ -17,7 +16,7 @@ export type Music = {
pic: string
lrc: string
}
export type MusicRequestSettings = {
export interface MusicRequestSettings {
playMusicWhenFree: boolean
repeat: 'repeat-one' | 'repeat-all' | 'no-repeat'
@@ -37,11 +36,11 @@ export type MusicRequestSettings = {
export const useMusicRequestProvider = defineStore('MusicRequest', () => {
const waitingMusics = useStorage<WaitMusicInfo[]>(
'Setting.MusicRequest.Waiting',
[]
[],
)
const originMusics = ref<SongsInfo[]>([])
const aplayerMusics = computed(() =>
originMusics.value.map((m) => songToMusic(m))
originMusics.value.map(m => songToMusic(m)),
)
const currentMusic = ref<Music>({
id: -1,
@@ -49,7 +48,7 @@ export const useMusicRequestProvider = defineStore('MusicRequest', () => {
artist: '',
src: '',
pic: '',
lrc: ''
lrc: '',
} as Music)
const currentOriginMusic = ref<WaitMusicInfo>()
const isPlayingOrderMusic = ref(false)
@@ -66,28 +65,28 @@ export const useMusicRequestProvider = defineStore('MusicRequest', () => {
orderMusicFirst: true,
platform: 'netease',
blacklist: []
blacklist: [],
})
const message = window.$message
const message = window.$message
function addWaitingMusic(info: WaitMusicInfo) {
if (
(settings.value.orderMusicFirst && !isPlayingOrderMusic.value) ||
aplayerRef.value?.audio.paused == true
(settings.value.orderMusicFirst && !isPlayingOrderMusic.value)
|| aplayerRef.value?.audio.paused == true
) {
playMusic(info.music)
isPlayingOrderMusic.value = true
console.log(
`正在播放 [${info.from.name}] 点的 ${info.music.name} - ${info.music.author?.join('/')}`
`正在播放 [${info.from.name}] 点的 ${info.music.name} - ${info.music.author?.join('/')}`,
)
message.success(
`正在播放 [${info.from.name}] 点的 ${info.music.name} - ${info.music.author?.join('/')}`
`正在播放 [${info.from.name}] 点的 ${info.music.name} - ${info.music.author?.join('/')}`,
)
} else {
waitingMusics.value.push(info)
message.success(
`[${info.from.name}] 点了一首 ${info.music.name} - ${info.music.author?.join('/')}`
`[${info.from.name}] 点了一首 ${info.music.name} - ${info.music.author?.join('/')}`,
)
}
}
@@ -128,7 +127,7 @@ const message = window.$message
}
}
function playMusic(music: SongsInfo) {
//pauseMusic()
// pauseMusic()
currentMusic.value = songToMusic(music)
aplayerRef.value?.thenPlay()
}
@@ -147,16 +146,16 @@ const message = window.$message
? `https://music.163.com/song/media/outer/url?id=${s.id}.mp3`
: s.url,
pic: s.cover ?? '',
lrc: ''
lrc: '',
} as Music
}
function setSinkId() {
try {
aplayerRef.value?.audio.setSinkId(settings.value.deviceId ?? 'default')
console.log('设置音频输出设备为 ' + (settings.value.deviceId ?? '默认'))
console.log(`设置音频输出设备为 ${settings.value.deviceId ?? '默认'}`)
} catch (err) {
console.error(err)
message.error('设置音频输出设备失败: ' + err)
message.error(`设置音频输出设备失败: ${err}`)
}
}
function nextMusic() {
@@ -184,6 +183,6 @@ const message = window.$message
onMusicPlay,
pauseMusic,
nextMusic,
aplayerRef
aplayerRef,
}
})

View File

@@ -1,10 +1,10 @@
import { QueryGetAPI } from '@/api/query'
import { NOTIFACTION_API_URL } from '@/data/constants'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { QueryGetAPI } from '@/api/query'
import { NOTIFACTION_API_URL } from '@/data/constants'
export type NotificationData = {
export interface NotificationData {
title: string
}
@@ -19,7 +19,7 @@ export const useNotificationStore = defineStore('notification', () => {
return // 暂时没写这部分相关逻辑
try {
const result = await QueryGetAPI<NotificationData[]>(
NOTIFACTION_API_URL + 'get-unread'
`${NOTIFACTION_API_URL}get-unread`,
)
if (result.code == 200) {
unread.value = result.data
@@ -40,6 +40,6 @@ export const useNotificationStore = defineStore('notification', () => {
}
return {
init,
unread
unread,
}
})

View File

@@ -1,9 +1,10 @@
import { useAccount } from '@/api/account'
import { ConsumptionTypes, IDeductionSetting, UserConsumptionSetting } from '@/api/models/consumption'
import { QueryPostAPIWithParams } from '@/api/query'
import { ACCOUNT_API_URL } from '@/data/constants'
import type { IDeductionSetting, UserConsumptionSetting } from '@/api/models/consumption'
import { defineStore } from 'pinia'
import { computed } from 'vue'
import { useAccount } from '@/api/account'
import { ConsumptionTypes } from '@/api/models/consumption'
import { QueryPostAPIWithParams } from '@/api/query'
import { ACCOUNT_API_URL } from '@/data/constants'
export const useConsumptionSettingStore = defineStore(
'consumptionSetting',
@@ -15,20 +16,20 @@ export const useConsumptionSettingStore = defineStore(
const consumptionTypeMap = {
[ConsumptionTypes.DanmakuStorage]: {
name: '弹幕存储',
key: 'danmakuStorage'
}
key: 'danmakuStorage',
},
}
async function UpdateConsumptionSetting(
type: ConsumptionTypes,
value: unknown
value: unknown,
) {
return await QueryPostAPIWithParams(
ACCOUNT_API_URL + 'update-consumption-setting',
return QueryPostAPIWithParams(
`${ACCOUNT_API_URL}update-consumption-setting`,
{
type: type
type,
},
value
value,
)
}
function GetSetting(type: ConsumptionTypes) {
@@ -37,5 +38,5 @@ export const useConsumptionSettingStore = defineStore(
}
return { consumptionSetting, consumptionTypeMap, UpdateConsumptionSetting, GetSetting }
}
},
)

View File

@@ -1,24 +1,24 @@
import { ResponsePointGoodModel } from "@/api/api-models";
import { QueryGetAPI } from "@/api/query";
import { POINT_API_URL } from "@/data/constants";
import { MessageApiInjection } from "naive-ui/es/message/src/MessageProvider";
import { defineStore } from "pinia";
import { useBiliAuth } from "./useBiliAuth";
import { GuidUtils } from "@/Utils";
import type { MessageApiInjection } from 'naive-ui/es/message/src/MessageProvider'
import type { ResponsePointGoodModel } from '@/api/api-models'
import { defineStore } from 'pinia'
import { QueryGetAPI } from '@/api/query'
import { POINT_API_URL } from '@/data/constants'
import { GuidUtils } from '@/Utils'
import { useBiliAuth } from './useBiliAuth'
export const usePointStore = defineStore('point', () => {
const useAuth = useBiliAuth()
async function GetSpecificPoint(id: number) {
try {
const data = await useAuth.QueryBiliAuthGetAPI<number>(POINT_API_URL + 'user/get-point', { id: id })
const data = await useAuth.QueryBiliAuthGetAPI<number>(`${POINT_API_URL}user/get-point`, { id })
if (data.code == 200) {
return data.data
} else {
console.error('[point] 无法获取在指定直播间拥有的积分: ' + data.message)
console.error(`[point] 无法获取在指定直播间拥有的积分: ${data.message}`)
}
} catch (err) {
console.error('[point] 无法获取在指定直播间拥有的积分: ' + err)
console.error(`[point] 无法获取在指定直播间拥有的积分: ${err}`)
}
return null
}
@@ -27,18 +27,18 @@ export const usePointStore = defineStore('point', () => {
return []
}
try {
const resp = await QueryGetAPI<ResponsePointGoodModel[]>(POINT_API_URL + 'get-goods', {
id: id,
const resp = await QueryGetAPI<ResponsePointGoodModel[]>(`${POINT_API_URL}get-goods`, {
id,
})
if (resp.code == 200) {
return resp.data
} else {
message?.error('无法获取数据: ' + resp.message)
console.error('无法获取数据: ' + resp.message)
message?.error(`无法获取数据: ${resp.message}`)
console.error(`无法获取数据: ${resp.message}`)
}
} catch (err) {
message?.error('无法获取数据: ' + err)
console.error('无法获取数据: ' + err)
message?.error(`无法获取数据: ${err}`)
console.error(`无法获取数据: ${err}`)
}
return []
}
@@ -52,44 +52,46 @@ export const usePointStore = defineStore('point', () => {
*/
async function addPoints(userId: string, count: number, reason: string, remark?: string) {
if (count === 0) {
console.warn('[point] 积分变动数量不能为0');
return null;
console.warn('[point] 积分变动数量不能为0')
return null
}
try {
// 根据用户ID构建参数
const params: Record<string, any> = GuidUtils.isGuidFromUserId(userId) ? {
uId: GuidUtils.guidToLong(userId),
count: count,
reason: reason || '',
} : {
oid: userId,
count: count,
reason: reason || '',
};
const params: Record<string, any> = GuidUtils.isGuidFromUserId(userId)
? {
uId: GuidUtils.guidToLong(userId),
count,
reason: reason || '',
}
: {
oid: userId,
count,
reason: reason || '',
}
if (remark) {
params.remark = remark;
params.remark = remark
}
const data = await QueryGetAPI<number>(POINT_API_URL + 'give-point', params);
const data = await QueryGetAPI<number>(`${POINT_API_URL}give-point`, params)
if (data.code === 200) {
console.log(`[point] 用户 ${userId} 积分${count > 0 ? '增加' : '减少'} ${Math.abs(count)} 成功,当前积分:${data.data}`);
return data.data; // 返回修改后的积分值
console.log(`[point] 用户 ${userId} 积分${count > 0 ? '增加' : '减少'} ${Math.abs(count)} 成功,当前积分:${data.data}`)
return data.data // 返回修改后的积分值
} else {
console.error('[point] 积分操作失败:', data.message);
return null;
console.error('[point] 积分操作失败:', data.message)
return null
}
} catch (err) {
console.error('[point] 积分操作出错:', err);
return null;
console.error('[point] 积分操作出错:', err)
return null
}
}
return {
GetSpecificPoint,
GetGoods,
addPoints
addPoints,
}
})
})

View File

@@ -1,18 +1,18 @@
import { useAccount } from '@/api/account'
import { QAInfo, ViolationTypes } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import { ACCOUNT_API_URL, QUESTION_API_URL } from '@/data/constants'
import type { QAInfo } from '@/api/api-models'
import { List } from 'linqts'
import { useMessage } from 'naive-ui'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { useAccount } from '@/api/account'
import { ViolationTypes } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import { ACCOUNT_API_URL, QUESTION_API_URL } from '@/data/constants'
export type QATagInfo = {
export interface QATagInfo {
name: string
createAt: number
visiable: boolean
}
//SENSITIVE_TERM, HATE, VIOLENCE, PORNOGRAPHY, POLITICS, ADVERTISING, AGGRESSION, EMOTIONAL
// SENSITIVE_TERM, HATE, VIOLENCE, PORNOGRAPHY, POLITICS, ADVERTISING, AGGRESSION, EMOTIONAL
export const useQuestionBox = defineStore('QuestionBox', () => {
const isLoading = ref(false)
@@ -25,7 +25,7 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
const sendQuestions = ref<QAInfo[]>([])
const trashQuestions = computed(() => {
return recieveQuestions.value.filter(
(q) => q.reviewResult && q.reviewResult.isApproved == false
q => q.reviewResult && q.reviewResult.isApproved == false,
)
})
const tags = ref<QATagInfo[]>([])
@@ -37,60 +37,60 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
const recieveQuestionsFiltered = computed(() => {
const result = recieveQuestions.value.filter((q) => {
/*if (q.id == displayQuestion.value?.id) {
/* if (q.id == displayQuestion.value?.id) {
return false
}*/
} */
return (
(!q.reviewResult || q.reviewResult.isApproved == true) &&
(q.isFavorite || !onlyFavorite.value) &&
(q.isPublic || !onlyPublic.value) &&
(!q.isReaded || !onlyUnread.value) &&
(!displayTag.value || q.tag == displayTag.value)
(!q.reviewResult || q.reviewResult.isApproved == true)
&& (q.isFavorite || !onlyFavorite.value)
&& (q.isPublic || !onlyPublic.value)
&& (!q.isReaded || !onlyUnread.value)
&& (!displayTag.value || q.tag == displayTag.value)
)
})
return result
//displayQuestion排在最前面
//return displayQuestion.value ? [displayQuestion.value, ...result] : result
// displayQuestion排在最前面
// return displayQuestion.value ? [displayQuestion.value, ...result] : result
})
const currentQuestion = ref<QAInfo>()
const displayQuestion = ref<QAInfo>()
const displayTag = ref<string>()
let isRevieveGetted = false
//const isSendGetted = false
// const isSendGetted = false
const message = window.$message
async function GetRecieveQAInfo() {
isLoading.value = true
await QueryGetAPI<{ questions: QAInfo[]; reviewCount: number }>(
QUESTION_API_URL + 'get-recieve'
await QueryGetAPI<{ questions: QAInfo[], reviewCount: number }>(
`${QUESTION_API_URL}get-recieve`,
)
.then((data) => {
if (data.code == 200) {
if (data.data.questions.length > 0) {
recieveQuestions.value = new List(data.data.questions)
.OrderBy((d) => d.isReaded)
.ThenByDescending((d) => d.sendAt)
.OrderBy(d => d.isReaded)
.ThenByDescending(d => d.sendAt)
.ToArray()
reviewing.value = data.data.reviewCount
const displayId =
accountInfo.value?.settings.questionDisplay.currentQuestion
const displayId
= accountInfo.value?.settings.questionDisplay.currentQuestion
if (displayId && displayQuestion.value?.id != displayId) {
displayQuestion.value = recieveQuestions.value.find(
(q) => q.id == displayId
q => q.id == displayId,
)
}
}
//message.success('共收取 ' + data.data.length + ' 条提问')
// message.success('共收取 ' + data.data.length + ' 条提问')
isRevieveGetted = true
} else {
message.error(data.message)
}
})
.catch((err) => {
message.error('发生错误: ' + err)
message.error(`发生错误: ${err}`)
})
.finally(() => {
isLoading.value = false
@@ -98,44 +98,44 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
}
async function GetSendQAInfo() {
isLoading.value = true
await QueryGetAPI<QAInfo[]>(QUESTION_API_URL + 'get-send')
await QueryGetAPI<QAInfo[]>(`${QUESTION_API_URL}get-send`)
.then((data) => {
if (data.code == 200) {
sendQuestions.value = data.data
//message.success('共发送 ' + data.data.length + ' 条提问')
// message.success('共发送 ' + data.data.length + ' 条提问')
} else {
message.error(data.message)
}
})
.catch((err) => {
message.error('发生错误: ' + err)
message.error(`发生错误: ${err}`)
})
.finally(() => {
isLoading.value = false
})
}
async function DelQA(id: number) {
await QueryGetAPI(QUESTION_API_URL + 'del', {
id: id
await QueryGetAPI(`${QUESTION_API_URL}del`, {
id,
})
.then((data) => {
if (data.code == 200) {
message.success('删除成功')
recieveQuestions.value = recieveQuestions.value.filter(
(q) => q.id != id
q => q.id != id,
)
} else {
message.error(data.message)
}
})
.catch((err) => {
message.error('发生错误: ' + err)
message.error(`发生错误: ${err}`)
})
}
async function GetTags() {
isLoading.value = true
await QueryGetAPI<QATagInfo[]>(QUESTION_API_URL + 'get-tags', {
id: accountInfo.value?.id
await QueryGetAPI<QATagInfo[]>(`${QUESTION_API_URL}get-tags`, {
id: accountInfo.value?.id,
})
.then((data) => {
if (data.code == 200) {
@@ -145,14 +145,14 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
}
})
.catch((err) => {
message.error('发生错误: ' + err)
message.error(`发生错误: ${err}`)
})
.finally(() => {
isLoading.value = false
})
}
function getViolationString(violation: ViolationTypes) {
//SENSITIVE_TERM, HATE, VIOLENCE, PORNOGRAPHY, POLITICS, ADVERTISING, AGGRESSION
// SENSITIVE_TERM, HATE, VIOLENCE, PORNOGRAPHY, POLITICS, ADVERTISING, AGGRESSION
switch (violation) {
case ViolationTypes.SENSITIVE_TERM:
return '敏感词'
@@ -175,23 +175,23 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
message.warning('请输入标签')
return
}
if (tags.value.find((t) => t.name == tag)) {
if (tags.value.find(t => t.name == tag)) {
message.warning('标签已存在')
return
}
await QueryGetAPI(QUESTION_API_URL + 'add-tag', {
tag: tag
await QueryGetAPI(`${QUESTION_API_URL}add-tag`, {
tag,
})
.then((data) => {
if (data.code == 200) {
message.success('添加成功')
GetTags()
} else {
message.error('添加失败: ' + data.message)
message.error(`添加失败: ${data.message}`)
}
})
.catch((err) => {
message.error('添加失败: ' + err)
message.error(`添加失败: ${err}`)
})
}
async function delTag(tag: string) {
@@ -199,23 +199,23 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
message.warning('请输入标签')
return
}
if (!tags.value.find((t) => t.name == tag)) {
if (!tags.value.find(t => t.name == tag)) {
message.warning('标签不存在')
return
}
await QueryGetAPI(QUESTION_API_URL + 'del-tag', {
tag: tag
await QueryGetAPI(`${QUESTION_API_URL}del-tag`, {
tag,
})
.then((data) => {
if (data.code == 200) {
message.success('删除成功')
GetTags()
} else {
message.error('删除失败: ' + data.message)
message.error(`删除失败: ${data.message}`)
}
})
.catch((err) => {
message.error('删除失败: ' + err)
message.error(`删除失败: ${err}`)
})
}
async function updateTagVisiable(tag: string, visiable: boolean) {
@@ -223,85 +223,85 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
message.warning('请输入标签')
return
}
if (!tags.value.find((t) => t.name == tag)) {
if (!tags.value.find(t => t.name == tag)) {
message.warning('标签不存在')
return
}
await QueryGetAPI(QUESTION_API_URL + 'update-tag-visiable', {
tag: tag,
visiable: visiable
await QueryGetAPI(`${QUESTION_API_URL}update-tag-visiable`, {
tag,
visiable,
})
.then((data) => {
if (data.code == 200) {
message.success('修改成功')
GetTags()
} else {
message.error('修改失败: ' + data.message)
message.error(`修改失败: ${data.message}`)
}
})
.catch((err) => {
message.error('修改失败: ' + err)
message.error(`修改失败: ${err}`)
})
}
async function reply(id: number, msg: string) {
isRepling.value = true
await QueryPostAPI<QAInfo>(QUESTION_API_URL + 'reply', {
await QueryPostAPI<QAInfo>(`${QUESTION_API_URL}reply`, {
Id: id,
Message: msg
Message: msg,
})
.then((data) => {
if (data.code == 200) {
const index = recieveQuestions.value.findIndex((q) => q.id == id)
const index = recieveQuestions.value.findIndex(q => q.id == id)
if (index > -1) {
recieveQuestions.value[index] = data.data
}
message.success('回复成功')
currentQuestion.value = undefined
//replyModalVisiable.value = false
// replyModalVisiable.value = false
} else {
message.error('发送失败: ' + data.message)
message.error(`发送失败: ${data.message}`)
}
})
.catch((err) => {
message.error('发送失败: ' + err)
message.error(`发送失败: ${err}`)
})
.finally(() => {
isRepling.value = false
})
}
async function read(question: QAInfo, read: boolean) {
await QueryGetAPI(QUESTION_API_URL + 'read', {
await QueryGetAPI(`${QUESTION_API_URL}read`, {
id: question.id,
read: read ? 'true' : 'false'
read: read ? 'true' : 'false',
})
.then((data) => {
if (data.code == 200) {
question.isReaded = read
if (read && displayQuestion.value?.id == question.id) {
setCurrentQuestion(question) //取消设为当前展示的问题
setCurrentQuestion(question) // 取消设为当前展示的问题
}
} else {
message.error('修改失败: ' + data.message)
message.error(`修改失败: ${data.message}`)
}
})
.catch((err) => {
message.error('修改失败: ' + err)
message.error(`修改失败: ${err}`)
})
}
async function favorite(question: QAInfo, fav: boolean) {
await QueryGetAPI(QUESTION_API_URL + 'favorite', {
await QueryGetAPI(`${QUESTION_API_URL}favorite`, {
id: question.id,
favorite: fav
favorite: fav,
})
.then((data) => {
if (data.code == 200) {
question.isFavorite = fav
} else {
message.error('修改失败: ' + data.message)
message.error(`修改失败: ${data.message}`)
}
})
.catch((err) => {
message.error('修改失败: ' + err)
message.error(`修改失败: ${err}`)
})
}
async function approve(question: QAInfo, approve: boolean) {
@@ -309,15 +309,15 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
message.error('暂时不支持取消审核')
return
}
await QueryGetAPI(QUESTION_API_URL + 'approve', {
await QueryGetAPI(`${QUESTION_API_URL}approve`, {
id: question.id,
approve: approve ? 'true' : 'false'
approve: approve ? 'true' : 'false',
})
.then((data) => {
if (data.code == 200) {
question.reviewResult = undefined
const trashIndex = trashQuestions.value.findIndex(
(q) => q.id == question.id
q => q.id == question.id,
)
if (trashIndex > -1) {
trashQuestions.value.splice(trashIndex, 1)
@@ -325,60 +325,60 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
recieveQuestions.value.unshift(question)
message.success('已标记为审核通过')
} else {
message.error('修改失败: ' + data.message)
message.error(`修改失败: ${data.message}`)
}
})
.catch((err) => {
message.error('修改失败: ' + err)
message.error(`修改失败: ${err}`)
})
}
async function setPublic(pub: boolean) {
isChangingPublic.value = true
await QueryGetAPI(QUESTION_API_URL + 'public', {
await QueryGetAPI(`${QUESTION_API_URL}public`, {
id: currentQuestion.value?.id,
public: pub
public: pub,
})
.then((data) => {
if (data.code == 200) {
if (currentQuestion.value) currentQuestion.value.isPublic = pub
message.success('已修改公开状态')
} else {
message.error('修改失败: ' + data.message)
message.error(`修改失败: ${data.message}`)
}
})
.catch((err) => {
message.error('修改失败: ' + err)
message.error(`修改失败: ${err}`)
})
.finally(() => {
isChangingPublic.value = false
})
}
async function blacklist(question: QAInfo) {
await QueryGetAPI(ACCOUNT_API_URL + 'black-list/add', {
id: question.sender.id
await QueryGetAPI(`${ACCOUNT_API_URL}black-list/add`, {
id: question.sender.id,
})
.then(async (data) => {
if (data.code == 200) {
await QueryGetAPI(QUESTION_API_URL + 'del', {
id: question.id
await QueryGetAPI(`${QUESTION_API_URL}del`, {
id: question.id,
}).then((data) => {
if (data.code == 200) {
message.success('已拉黑 ' + question.sender.name)
message.success(`已拉黑 ${question.sender.name}`)
} else {
message.error('修改失败: ' + data.message)
message.error(`修改失败: ${data.message}`)
}
})
} else {
message.error('拉黑失败: ' + data.message)
message.error(`拉黑失败: ${data.message}`)
}
})
.catch((err) => {
message.error('拉黑失败: ' + err)
message.error(`拉黑失败: ${err}`)
})
}
async function markAsNormal(question: QAInfo) {
await QueryGetAPI(QUESTION_API_URL + 'mark-as-normal', {
id: question.id
await QueryGetAPI(`${QUESTION_API_URL}mark-as-normal`, {
id: question.id,
})
.then((data) => {
if (data.code == 200) {
@@ -387,7 +387,7 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
}
})
.catch((err) => {
message.error('标记失败: ' + err)
message.error(`标记失败: ${err}`)
})
}
async function setCurrentQuestion(item: QAInfo | undefined) {
@@ -399,20 +399,20 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
}
try {
const data = await QueryGetAPI(
QUESTION_API_URL + 'set-current',
`${QUESTION_API_URL}set-current`,
isCurrent || !item
? null
: {
id: item.id
}
id: item.id,
},
)
if (data.code == 200) {
//message.success('设置成功')
// message.success('设置成功')
} else {
message.error('设置失败: ' + data.message)
message.error(`设置失败: ${data.message}`)
}
} catch (err) {
message.error('设置失败:' + err)
message.error(`设置失败:${err}`)
}
}
@@ -447,6 +447,6 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
blacklist,
markAsNormal,
setCurrentQuestion,
getViolationString
getViolationString,
}
})

View File

@@ -1,13 +1,14 @@
import { cookie, useAccount } from '@/api/account'
import {
import type {
BaseRTCClient,
MasterRTCClient,
SlaveRTCClient
} from '@/data/RTCClient'
import { useStorage } from '@vueuse/core'
import { acceptHMRUpdate, defineStore } from 'pinia'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { cookie, useAccount } from '@/api/account'
import {
MasterRTCClient,
SlaveRTCClient,
} from '@/data/RTCClient'
export const useWebRTC = defineStore('WebRTC', () => {
const client = ref<BaseRTCClient>()
@@ -42,7 +43,7 @@ export const useWebRTC = defineStore('WebRTC', () => {
return
}
while (!accountInfo.value.id) {
await new Promise((resolve) => setTimeout(resolve, 500))
await new Promise(resolve => setTimeout(resolve, 500))
}
if (client.value) {
return client.value
@@ -50,12 +51,12 @@ export const useWebRTC = defineStore('WebRTC', () => {
if (type == 'master') {
client.value = new MasterRTCClient(
accountInfo.value.id.toString(),
accountInfo.value.token
accountInfo.value.token,
)
} else {
client.value = new SlaveRTCClient(
accountInfo.value.id?.toString(),
accountInfo.value.token
accountInfo.value.token,
)
}
await client.value.Init()
@@ -63,7 +64,7 @@ export const useWebRTC = defineStore('WebRTC', () => {
} else {
return useWebRTC()
}
}
},
)
return useWebRTC()
} catch (e) {

View File

@@ -1,16 +1,17 @@
import { useAccount } from '@/api/account'
import { BASE_HUB_URL } from '@/data/constants'
import {
HttpTransportType,
HubConnectionBuilder,
LogLevel
LogLevel,
} from '@microsoft/signalr'
import type { HubConnection } from '@microsoft/signalr'
import { MessagePackHubProtocol } from '@microsoft/signalr-protocol-msgpack'
import { acceptHMRUpdate, defineStore } from 'pinia'
import { ref } from 'vue'
import { useAccount } from '@/api/account'
import { BASE_HUB_URL } from '@/data/constants'
export const useVTsuruHub = defineStore('VTsuruHub', () => {
const signalRClient = ref<signalR.HubConnection>()
const signalRClient = ref<HubConnection>()
const isInited = ref(false)
const isIniting = ref(false)
const accountInfo = useAccount()
@@ -18,14 +19,18 @@ export const useVTsuruHub = defineStore('VTsuruHub', () => {
async function connectSignalR() {
if (isIniting.value) return
isIniting.value = true
while (!accountInfo.value.id || accountInfo.value.id < 1)
await new Promise((resolve) => setTimeout(resolve, 1000))
//console.log('[Components-Event] 正在连接到 VTsuru 服务器...')
let currentAccount = accountInfo.value
while (!currentAccount || !currentAccount.id || currentAccount.id < 1) {
await new Promise(resolve => setTimeout(resolve, 1000))
currentAccount = accountInfo.value
}
// console.log('[Components-Event] 正在连接到 VTsuru 服务器...')
const connection = new HubConnectionBuilder()
.withUrl(BASE_HUB_URL + 'main?token=' + accountInfo.value.token, {
.withUrl(`${BASE_HUB_URL}main?token=${currentAccount.token}`, {
skipNegotiation: true,
transport: HttpTransportType.WebSockets,
logger: LogLevel.Error
logger: LogLevel.Error,
})
.withAutomaticReconnect([0, 2000, 10000, 30000])
.withHubProtocol(new MessagePackHubProtocol())
@@ -34,7 +39,7 @@ export const useVTsuruHub = defineStore('VTsuruHub', () => {
connection.send('Finished')
})
connection.on('Disconnect', (reason: unknown) => {
console.log('[Hub] 被 VTsuru 服务器断开连接: ' + reason)
console.log(`[Hub] 被 VTsuru 服务器断开连接: ${reason}`)
})
connection.onclose(reconnect)
@@ -45,7 +50,7 @@ export const useVTsuruHub = defineStore('VTsuruHub', () => {
isInited.value = true
return true
} catch (e) {
console.log('[Hub] 无法连接到 VTsuru 服务器: ' + e)
console.log(`[Hub] 无法连接到 VTsuru 服务器: ${e}`)
return false
} finally {
isIniting.value = false
@@ -62,31 +67,31 @@ export const useVTsuruHub = defineStore('VTsuruHub', () => {
}
}
async function send(methodName: string, ...args: any[]) {
async function send(methodName: string, ...args: unknown[]) {
if (!isInited.value) {
await connectSignalR()
}
signalRClient.value?.send(methodName, ...args)
}
async function invoke<T>(methodName: string, ...args: any[]) {
async function invoke<T>(methodName: string, ...args: unknown[]) {
if (!isInited.value) {
await connectSignalR()
}
return signalRClient.value?.invoke<T>(methodName, ...args)
}
async function on(eventName: string, listener: (args: any) => any) {
async function on(eventName: string, listener: (...args: unknown[]) => void) {
if (!isInited.value) {
await connectSignalR()
}
signalRClient.value?.on(eventName, listener)
}
async function off(eventName: string, listener: (args: any) => any) {
async function off(eventName: string, listener: (...args: unknown[]) => void) {
if (!isInited.value) {
await connectSignalR()
}
signalRClient.value?.off(eventName, listener)
}
async function onreconnected(listener: (id: any) => any) {
async function onreconnected(listener: (connectionId?: string) => void) {
if (!isInited.value) {
await connectSignalR()
}

View File

@@ -1,65 +1,64 @@
import { cookie, useAccount } from '@/api/account';
import { getEventType, recordEvent, streamingInfo } from '@/client/data/info';
import { QueryBiliAPI } from '@/client/data/utils';
import { BASE_HUB_URL, isDev, isTauri } from '@/data/constants';
import { DirectClientAuthInfo } from '@/data/DanmakuClients/DirectClient';
import * as signalR from '@microsoft/signalr';
import * as msgpack from '@microsoft/signalr-protocol-msgpack';
import { ZstdCodec, ZstdInit } from '@oneidentity/zstd-js/wasm';
import { platform, version } from '@tauri-apps/plugin-os';
import { defineStore } from 'pinia';
import { computed, ref, shallowRef } from 'vue'; // shallowRef 用于非深度响应对象
import { useRoute } from 'vue-router';
import { useWebRTC } from './useRTC';
import type { ZstdCodec } from '@oneidentity/zstd-js/wasm'
import type { DirectClientAuthInfo } from '@/data/DanmakuClients/DirectClient'
import * as signalR from '@microsoft/signalr'
import * as msgpack from '@microsoft/signalr-protocol-msgpack'
import { encode } from '@msgpack/msgpack'
import { ZstdInit } from '@oneidentity/zstd-js/wasm'
import { getVersion } from '@tauri-apps/api/app'
import { platform, version } from '@tauri-apps/plugin-os'
import { defineStore } from 'pinia'
import { computed, ref, shallowRef } from 'vue' // shallowRef 用于非深度响应对象
import { useRoute } from 'vue-router'
import { cookie, useAccount } from '@/api/account'
import { getEventType, recordEvent, streamingInfo } from '@/client/data/info'
import { onReceivedNotification } from '@/client/data/notification'
import { onReceivedNotification } from '@/client/data/notification';
import { encode } from "@msgpack/msgpack";
import { getVersion } from '@tauri-apps/api/app';
import { useDanmakuClient } from './useDanmakuClient';
import { QueryBiliAPI } from '@/client/data/utils'
import { BASE_HUB_URL, isDev, isTauri } from '@/data/constants'
import { useDanmakuClient } from './useDanmakuClient'
import { useWebRTC } from './useRTC'
export const useWebFetcher = defineStore('WebFetcher', () => {
const route = useRoute();
const account = useAccount();
const rtc = useWebRTC();
const webfetcherType = ref<'openlive' | 'direct'>('openlive'); // 弹幕客户端类型
const route = useRoute()
const account = useAccount()
const rtc = useWebRTC()
const webfetcherType = ref<'openlive' | 'direct'>('openlive') // 弹幕客户端类型
// --- 连接与状态 ---
const state = ref<'disconnected' | 'connecting' | 'connected'>('disconnected'); // SignalR 连接状态
const startedAt = ref<Date>(); // 本次启动时间
const signalRClient = shallowRef<signalR.HubConnection>(); // SignalR 客户端实例 (浅响应)
const signalRId = ref<string>(); // SignalR 连接 ID
const client = useDanmakuClient();
let timer: any; // 事件发送定时器
let disconnectedByServer = false;
let isFromClient = false; // 是否由Tauri客户端启动
const state = ref<'disconnected' | 'connecting' | 'connected'>('disconnected') // SignalR 连接状态
const startedAt = ref<Date>() // 本次启动时间
const signalRClient = shallowRef<signalR.HubConnection>() // SignalR 客户端实例 (浅响应)
const signalRId = ref<string>() // SignalR 连接 ID
const client = useDanmakuClient()
let timer: any // 事件发送定时器
let disconnectedByServer = false
let isFromClient = false // 是否由Tauri客户端启动
// --- 新增: 详细状态与信息 ---
/** 弹幕客户端内部状态 */
const danmakuClientState = ref<'stopped' | 'connecting' | 'connected'>('stopped'); // 更详细的弹幕客户端状态
const danmakuClientState = ref<'stopped' | 'connecting' | 'connected'>('stopped') // 更详细的弹幕客户端状态
/** 弹幕服务器连接地址 */
const danmakuServerUrl = ref<string>();
const danmakuServerUrl = ref<string>()
/** SignalR 连接 ID */
const signalRConnectionId = ref<string>();
const signalRConnectionId = ref<string>()
// const heartbeatLatency = ref<number>(null); // 心跳延迟暂不实现,复杂度较高
// --- 事件处理 ---
const events: string[] = []; // 待发送事件队列
const events: string[] = [] // 待发送事件队列
// --- 新增: 会话统计 (在 Start 时重置) ---
/** 本次会话处理的总事件数 */
const sessionEventCount = ref(0);
const sessionEventCount = ref(0)
/** 本次会话各类型事件计数 */
const sessionEventTypeCounts = ref<{ [key: string]: number; }>({});
const sessionEventTypeCounts = ref<{ [key: string]: number }>({})
/** 本次会话成功上传次数 */
const successfulUploads = ref(0);
const successfulUploads = ref(0)
/** 本次会话失败上传次数 */
const failedUploads = ref(0);
const failedUploads = ref(0)
/** 本次会话发送的总字节数 (压缩后) */
const bytesSentSession = ref(0);
let zstd: ZstdCodec | undefined = undefined; // Zstd 编码器实例 (如果需要压缩)
const bytesSentSession = ref(0)
let zstd: ZstdCodec | undefined // Zstd 编码器实例 (如果需要压缩)
const prefix = computed(() => isFromClient ? '[web-fetcher-iframe] ' : '[web-fetcher] ');
const prefix = computed(() => isFromClient ? '[web-fetcher-iframe] ' : '[web-fetcher] ')
/**
* 启动 WebFetcher 服务
@@ -67,97 +66,98 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
async function Start(
type: 'openlive' | 'direct' = 'openlive',
directAuthInfo?: DirectClientAuthInfo,
_isFromClient: boolean = false
): Promise<{ success: boolean; message: string; }> {
_isFromClient: boolean = false,
): Promise<{ success: boolean, message: string }> {
if (state.value === 'connected' || state.value === 'connecting') {
console.log(prefix.value + '已经启动,无需重复启动');
return { success: true, message: '已启动' };
console.log(`${prefix.value}已经启动,无需重复启动`)
return { success: true, message: '已启动' }
}
try {
zstd ??= await ZstdInit();
zstd ??= await ZstdInit()
} catch (error) {
console.error(prefix.value + '当前浏览器不支持zstd压缩, 回退到原始数据传输');
console.error(`${prefix.value}当前浏览器不支持zstd压缩, 回退到原始数据传输`)
}
webfetcherType.value = type; // 设置弹幕客户端类型
webfetcherType.value = type // 设置弹幕客户端类型
// 重置会话统计数据
resetSessionStats();
startedAt.value = new Date();
isFromClient = _isFromClient;
state.value = 'connecting'; // 设置为连接中状态
resetSessionStats()
startedAt.value = new Date()
isFromClient = _isFromClient
state.value = 'connecting' // 设置为连接中状态
// 使用 navigator.locks 确保同一时间只有一个 Start 操作执行
const result = await navigator.locks.request('webFetcherStartLock', async () => {
console.log(prefix.value + '开始启动...');
console.log(`${prefix.value}开始启动...`)
while (!(await connectSignalR())) {
console.log(prefix.value + '连接 SignalR 失败, 5秒后重试');
await new Promise((resolve) => setTimeout(resolve, 5000));
console.log(`${prefix.value}连接 SignalR 失败, 5秒后重试`)
await new Promise(resolve => setTimeout(resolve, 5000))
// 如果用户手动停止,则退出重试循环
if (state.value === 'disconnected') return { success: false, message: '用户手动停止' };
if (state.value === 'disconnected') return { success: false, message: '用户手动停止' }
}
let danmakuResult = await connectDanmakuClient(type, directAuthInfo);
let danmakuResult = await connectDanmakuClient(type, directAuthInfo)
while (!danmakuResult?.success) {
console.log(prefix.value + '弹幕客户端启动失败, 5秒后重试');
await new Promise((resolve) => setTimeout(resolve, 5000));
console.log(`${prefix.value}弹幕客户端启动失败, 5秒后重试`)
await new Promise(resolve => setTimeout(resolve, 5000))
// 如果用户手动停止,则退出重试循环
if (state.value === 'disconnected') return { success: false, message: '用户手动停止' };
danmakuResult = await connectDanmakuClient(type, directAuthInfo);
if (state.value === 'disconnected') return { success: false, message: '用户手动停止' }
danmakuResult = await connectDanmakuClient(type, directAuthInfo)
}
// 只有在两个连接都成功后才设置为 connected
state.value = 'connected';
disconnectedByServer = false;
console.log(prefix.value + '启动成功');
return { success: true, message: '启动成功' };
});
state.value = 'connected'
disconnectedByServer = false
console.log(`${prefix.value}启动成功`)
return { success: true, message: '启动成功' }
})
// 如果启动过程中因为手动停止而失败,需要确保状态是 disconnected
if (!result.success) {
Stop(); // 确保清理资源
return { success: false, message: result.message || '启动失败' };
Stop() // 确保清理资源
return { success: false, message: result.message || '启动失败' }
}
return result;
return result
}
/**
* 停止 WebFetcher 服务
*/
function Stop() {
if (state.value === 'disconnected') return;
if (state.value === 'disconnected') return
console.log(prefix.value + '正在停止...');
state.value = 'disconnected'; // 立即设置状态,防止重连逻辑触发
console.log(`${prefix.value}正在停止...`)
state.value = 'disconnected' // 立即设置状态,防止重连逻辑触发
// 清理定时器
if (timer) { clearInterval(timer); timer = undefined; }
if (timer) {
clearInterval(timer); timer = undefined
}
// 停止弹幕客户端
client.dispose();
danmakuClientState.value = 'stopped';
danmakuServerUrl.value = undefined;
client.dispose()
danmakuClientState.value = 'stopped'
danmakuServerUrl.value = undefined
// 停止 SignalR 连接
signalRClient.value?.stop();
signalRClient.value = undefined;
signalRConnectionId.value = undefined;
signalRClient.value?.stop()
signalRClient.value = undefined
signalRConnectionId.value = undefined
// 清理状态
startedAt.value = undefined;
events.length = 0; // 清空事件队列
startedAt.value = undefined
events.length = 0 // 清空事件队列
// resetSessionStats(); // 会话统计在下次 Start 时重置
console.log(prefix.value + '已停止');
console.log(`${prefix.value}已停止`)
}
/** 重置会话统计数据 */
function resetSessionStats() {
sessionEventCount.value = 0;
sessionEventTypeCounts.value = {};
successfulUploads.value = 0;
failedUploads.value = 0;
bytesSentSession.value = 0;
sessionEventCount.value = 0
sessionEventTypeCounts.value = {}
successfulUploads.value = 0
failedUploads.value = 0
bytesSentSession.value = 0
}
/**
@@ -165,180 +165,177 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
*/
async function connectDanmakuClient(
type: 'openlive' | 'direct',
directConnectInfo?: DirectClientAuthInfo
directConnectInfo?: DirectClientAuthInfo,
) {
if (client.state !== 'waiting') {
console.log(prefix.value + '弹幕客户端已连接或正在连接');
return { success: true, message: '弹幕客户端已启动' };
console.log(`${prefix.value}弹幕客户端已连接或正在连接`)
return { success: true, message: '弹幕客户端已启动' }
}
console.log(prefix.value + '正在连接弹幕客户端...');
danmakuClientState.value = 'connecting';
console.log(`${prefix.value}正在连接弹幕客户端...`)
danmakuClientState.value = 'connecting'
if (type === 'openlive') {
await client.initOpenlive();
await client.initOpenlive()
} else {
if (!directConnectInfo) {
danmakuClientState.value = 'stopped';
console.error(prefix.value + '未提供直连弹幕客户端认证信息');
return { success: false, message: '未提供弹幕客户端认证信息' };
danmakuClientState.value = 'stopped'
console.error(`${prefix.value}未提供直连弹幕客户端认证信息`)
return { success: false, message: '未提供弹幕客户端认证信息' }
}
await client.initDirect(directConnectInfo);
await client.initDirect(directConnectInfo)
}
// 监听所有事件,用于处理和转发
client?.on('all', onGetDanmakus);
client?.on('all', onGetDanmakus)
if (client.connected) {
console.log(prefix.value + '弹幕客户端连接成功, 开始监听弹幕');
danmakuClientState.value = 'connected'; // 明确设置状态
danmakuServerUrl.value = client.danmakuClient!.serverUrl; // 获取服务器地址
console.log(`${prefix.value}弹幕客户端连接成功, 开始监听弹幕`)
danmakuClientState.value = 'connected' // 明确设置状态
danmakuServerUrl.value = client.danmakuClient!.serverUrl // 获取服务器地址
// 启动事件发送定时器 (如果之前没有启动)
timer ??= setInterval(sendEvents, 2000); // 每 2 秒尝试发送一次事件
return { success: true, message: '弹幕客户端已启动' };
timer ??= setInterval(sendEvents, 2000) // 每 2 秒尝试发送一次事件
return { success: true, message: '弹幕客户端已启动' }
} else {
console.error(prefix.value + '弹幕客户端启动失败');
danmakuClientState.value = 'stopped';
danmakuServerUrl.value = undefined;
client.dispose(); // 启动失败,清理实例,下次会重建
return { success: false, message: '弹幕客户端启动失败' };
console.error(`${prefix.value}弹幕客户端启动失败`)
danmakuClientState.value = 'stopped'
danmakuServerUrl.value = undefined
client.dispose() // 启动失败,清理实例,下次会重建
return { success: false, message: '弹幕客户端启动失败' }
}
}
let isConnectingSignalR = false;
let isConnectingSignalR = false
/**
* 连接 SignalR 服务器
*/
async function connectSignalR(): Promise<boolean> {
if (isConnectingSignalR) {
return false;
return false
}
isConnectingSignalR = true;
isConnectingSignalR = true
if (signalRClient.value && signalRClient.value.state !== signalR.HubConnectionState.Disconnected) {
console.log(prefix.value + "SignalR 已连接或正在连接");
return true;
console.log(`${prefix.value}SignalR 已连接或正在连接`)
return true
}
signalRClient.value?.stop();
signalRClient.value = undefined;
signalRConnectionId.value = undefined;
console.log(prefix.value + '正在连接到 vtsuru 服务器...');
signalRClient.value?.stop()
signalRClient.value = undefined
signalRConnectionId.value = undefined
console.log(`${prefix.value}正在连接到 vtsuru 服务器...`)
const connection = new signalR.HubConnectionBuilder()
.withUrl(BASE_HUB_URL + 'web-fetcher?token=' + (route.query.token ?? account.value.token), { // 使用 account.token
.withUrl(`${BASE_HUB_URL}web-fetcher?token=${route.query.token ?? account.value.token}`, { // 使用 account.token
headers: { Authorization: `Bearer ${cookie.value?.cookie}` },
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets
transport: signalR.HttpTransportType.WebSockets,
})
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: retryContext => {
return retryContext.elapsedMilliseconds < 60 * 1000 ? 10 * 1000 : 30 * 1000;
}
nextRetryDelayInMilliseconds: (retryContext) => {
return retryContext.elapsedMilliseconds < 60 * 1000 ? 10 * 1000 : 30 * 1000
},
}) // 自动重连策略
.withHubProtocol(new msgpack.MessagePackHubProtocol()) // 使用 MessagePack 协议
.build();
.build()
connection.on('Disconnect', (reason: unknown) => {
console.log(prefix.value + '被服务器断开连接: ' + reason);
disconnectedByServer = true; // 标记是服务器主动断开
window.$message.error(`被服务器要求断开连接: ${reason}`);
});
console.log(`${prefix.value}被服务器断开连接: ${reason}`)
disconnectedByServer = true // 标记是服务器主动断开
window.$message.error(`被服务器要求断开连接: ${reason}`)
})
// --- 尝试启动连接 ---
try {
await connection.start();
signalRConnectionId.value = connection.connectionId ?? undefined; // 保存连接ID
signalRId.value = await sendSelfInfo(connection); // 发送客户端信息
await connection.send('Finished'); // 通知服务器已准备好
signalRClient.value = connection; // 保存实例
await connection.start()
signalRConnectionId.value = connection.connectionId ?? undefined // 保存连接ID
signalRId.value = await sendSelfInfo(connection) // 发送客户端信息
await connection.send('Finished') // 通知服务器已准备好
signalRClient.value = connection // 保存实例
// --- SignalR 事件监听 ---
connection.onreconnecting(error => {
console.log(prefix.value + `与服务器断开,正在尝试重连... ${error?.message || ''}`);
state.value = 'connecting'; // 更新状态为连接中
signalRConnectionId.value = undefined; // 连接断开ID失效
});
connection.onreconnecting((error) => {
console.log(`${prefix.value}与服务器断开,正在尝试重连... ${error?.message || ''}`)
state.value = 'connecting' // 更新状态为连接中
signalRConnectionId.value = undefined // 连接断开ID失效
})
connection.onreconnected(async connectionId => {
console.log(prefix.value + `与服务器重新连接成功! ConnectionId: ${connectionId}`);
signalRConnectionId.value = connectionId ?? undefined;
state.value = 'connected'; // 更新状态为已连接
signalRId.value = connectionId ?? await sendSelfInfo(connection); // 更新连接ID
connection.send('Reconnected').catch(err => console.error(prefix.value + "Send Reconnected failed: " + err));
});
connection.onreconnected(async (connectionId) => {
console.log(`${prefix.value}与服务器重新连接成功! ConnectionId: ${connectionId}`)
signalRConnectionId.value = connectionId ?? undefined
state.value = 'connected' // 更新状态为已连接
signalRId.value = connectionId ?? await sendSelfInfo(connection) // 更新连接ID
connection.send('Reconnected').catch(err => console.error(`${prefix.value}Send Reconnected failed: ${err}`))
})
connection.onclose(async (error) => {
// 只有在不是由 Stop() 或服务器明确要求断开时才记录错误并尝试独立重连(虽然 withAutomaticReconnect 应该处理)
if (state.value !== 'disconnected' && !disconnectedByServer) {
console.error(prefix.value + `与服务器连接关闭: ${error?.message || '未知原因'}. 30秒后将自动重启`);
//state.value = 'connecting'; // 标记为连接中,等待自动重连
//signalRConnectionId.value = undefined;
//await connection.start();
console.error(`${prefix.value}与服务器连接关闭: ${error?.message || '未知原因'}. 30秒后将自动重启`)
// state.value = 'connecting'; // 标记为连接中,等待自动重连
// signalRConnectionId.value = undefined;
// await connection.start();
// 停止 SignalR 连接
signalRClient.value?.stop();
signalRClient.value = undefined;
signalRConnectionId.value = undefined;
signalRClient.value?.stop()
signalRClient.value = undefined
signalRConnectionId.value = undefined
setTimeout(() => {
console.log(prefix.value + '尝试重启...');
connectSignalR(); // 30秒后尝试重启
}, 30 * 1000); // 30秒后自动重启
console.log(`${prefix.value}尝试重启...`)
connectSignalR() // 30秒后尝试重启
}, 30 * 1000) // 30秒后自动重启
} else if (disconnectedByServer) {
console.log(prefix.value + `连接已被服务器关闭.`);
//Stop(); // 服务器要求断开,则彻底停止
console.log(`${prefix.value}连接已被服务器关闭.`)
// Stop(); // 服务器要求断开,则彻底停止
} else {
console.log(prefix.value + `连接已手动关闭.`);
console.log(`${prefix.value}连接已手动关闭.`)
}
});
connection.on('Request', async (url: string, method: string, body: string, useCookie: boolean) => onRequest(url, method, body, useCookie));
connection.on('Notification', (type: string, data: any) => { onReceivedNotification(type, data); });
})
connection.on('Request', async (url: string, method: string, body: string, useCookie: boolean) => onRequest(url, method, body, useCookie))
connection.on('Notification', (type: string, data: any) => {
onReceivedNotification(type, data)
})
console.log(prefix.value + '已连接到 vtsuru 服务器, ConnectionId: ' + signalRId.value); // 调试输出连接状态
console.log(`${prefix.value}已连接到 vtsuru 服务器, ConnectionId: ${signalRId.value}`) // 调试输出连接状态
// state.value = 'connected'; // 状态将在 Start 函数末尾统一设置
return true;
return true
} catch (e) {
console.error(prefix.value + '无法连接到 vtsuru 服务器: ' + e);
signalRConnectionId.value = undefined;
signalRClient.value = undefined;
console.error(`${prefix.value}无法连接到 vtsuru 服务器: ${e}`)
signalRConnectionId.value = undefined
signalRClient.value = undefined
// state.value = 'disconnected'; // 保持 connecting 或由 Start 控制
return false;
return false
} finally {
isConnectingSignalR = false;
isConnectingSignalR = false
}
}
async function sendSelfInfo(client: signalR.HubConnection) {
return client.invoke('SetSelfInfo',
isFromClient ? `tauri ${platform()} ${version()}` : navigator.userAgent,
isFromClient ? 'tauri' : 'web',
isFromClient ? await getVersion() : '1.0.0',
webfetcherType.value === 'direct');
return client.invoke('SetSelfInfo', isFromClient ? `tauri ${platform()} ${version()}` : navigator.userAgent, isFromClient ? 'tauri' : 'web', isFromClient ? await getVersion() : '1.0.0', webfetcherType.value === 'direct')
}
interface ResponseFetchRequestData {
Message: string
Success: boolean
Data: string
}
type ResponseFetchRequestData = {
Message: string;
Success: boolean;
Data: string;
};
async function onRequest(url: string, method: string, body: string, useCookie: boolean) {
if (!isTauri()) {
console.error(prefix.value + '非Tauri环境下无法处理请求: ' + url);
console.error(`${prefix.value}非Tauri环境下无法处理请求: ${url}`)
return {
Message: '非Tauri环境',
Success: false,
Data: ''
};
Data: '',
}
}
const result = await QueryBiliAPI(url, method, body, useCookie);
console.log(`${prefix.value}请求 (${method})${url}: `, result.statusText);
const result = await QueryBiliAPI(url, method, body, useCookie)
console.log(`${prefix.value}请求 (${method})${url}: `, result.statusText)
if (result.ok) {
const data = await result.text();
const data = await result.text()
return {
Message: '请求成功',
Success: true,
Data: data
} as ResponseFetchRequestData;
Data: data,
} as ResponseFetchRequestData
} else {
return {
Message: '请求失败: ' + result.statusText,
Message: `请求失败: ${result.statusText}`,
Success: false,
Data: ''
} as ResponseFetchRequestData;
Data: '',
} as ResponseFetchRequestData
}
}
@@ -366,80 +363,81 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
function onGetDanmakus(command: any) {
if (isFromClient) {
// 1. 解析事件类型
const eventType = getEventType(command);
const eventType = getEventType(command)
// 2. 记录到每日统计 (调用 statistics 模块)
recordEvent(eventType);
recordEvent(eventType)
// 3. 更新会话统计
sessionEventCount.value++;
sessionEventTypeCounts.value[eventType] = (sessionEventTypeCounts.value[eventType] || 0) + 1;
sessionEventCount.value++
sessionEventTypeCounts.value[eventType] = (sessionEventTypeCounts.value[eventType] || 0) + 1
}
// 4. 加入待发送队列 (确保是字符串)
const eventString = typeof command === 'string' ? command : JSON.stringify(command);
const eventString = typeof command === 'string' ? command : JSON.stringify(command)
if (isDev) {
//console.log(prefix.value + '收到弹幕事件: ' + eventString); // 开发模式下打印所有事件 (可选)
// console.log(prefix.value + '收到弹幕事件: ' + eventString); // 开发模式下打印所有事件 (可选)
}
if (events.length >= 10000) {
events.shift(); // 如果队列过长,移除最旧的事件
events.shift() // 如果队列过长,移除最旧的事件
}
events.push(eventString);
events.push(eventString)
}
let updateCount = 0;
let updateCount = 0
/**
* 定期将队列中的事件发送到服务器
*/
async function sendEvents() {
updateCount++;
updateCount++
// 确保 SignalR 已连接
if (!signalRClient.value || signalRClient.value.state !== signalR.HubConnectionState.Connected) {
return;
return
}
// 如果没有事件,则不发送
if (events.length === 0) {
return;
return
}
if (updateCount % 60 == 0) {
// 每60秒更新一次连接信息
if (signalRClient.value) {
await sendSelfInfo(signalRClient.value);
await sendSelfInfo(signalRClient.value)
}
}
// 批量处理事件每次最多发送20条
const batchSize = 30;
const batch = events.slice(0, batchSize);
const batchSize = 30
const batch = events.slice(0, batchSize)
try {
let result: { Success: boolean; Message: string; } = { Success: false, Message: '' };
let length = 0;
let eventCharLength = batch.map(event => event.length).reduce((a, b) => a + b, 0); // 计算字符长度
let result: { Success: boolean, Message: string } = { Success: false, Message: '' }
let length = 0
const eventCharLength = batch.map(event => event.length).reduce((a, b) => a + b, 0) // 计算字符长度
if (zstd && eventCharLength > 100) {
const data = zstd.ZstdSimple.compress(encode(batch), 11);
length = data.length;
result = await signalRClient.value.invoke<{ Success: boolean; Message: string; }>(
'UploadEventsCompressedV2', data
);
}
else {
length = new TextEncoder().encode(batch.join()).length;
result = await signalRClient.value.invoke<{ Success: boolean; Message: string; }>(
'UploadEvents', batch, webfetcherType.value === 'direct' ? true : false
);
const data = zstd.ZstdSimple.compress(encode(batch), 11)
length = data.length
result = await signalRClient.value.invoke<{ Success: boolean, Message: string }>(
'UploadEventsCompressedV2',
data,
)
} else {
length = new TextEncoder().encode(batch.join()).length
result = await signalRClient.value.invoke<{ Success: boolean, Message: string }>(
'UploadEvents',
batch,
webfetcherType.value === 'direct',
)
}
if (result?.Success) {
events.splice(0, batch.length); // 从队列中移除已成功发送的事件
successfulUploads.value++;
bytesSentSession.value += length;
events.splice(0, batch.length) // 从队列中移除已成功发送的事件
successfulUploads.value++
bytesSentSession.value += length
} else {
failedUploads.value++;
console.error(prefix.value + '上传弹幕失败: ' + result?.Message);
failedUploads.value++
console.error(`${prefix.value}上传弹幕失败: ${result?.Message}`)
}
} catch (err) {
failedUploads.value++;
console.error(prefix.value + '发送事件时出错: ' + err);
failedUploads.value++
console.error(`${prefix.value}发送事件时出错: ${err}`)
}
}
@@ -459,7 +457,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
// 连接详情
danmakuClientState,
danmakuServerUrl,
//signalRConnectionId,
// signalRConnectionId,
// heartbeatLatency, // 暂不暴露
// 会话统计
@@ -471,6 +469,6 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
// 实例 (谨慎暴露,主要用于调试或特定场景)
signalRClient: computed(() => signalRClient.value), // 返回计算属性以防直接修改
client
};
});
client,
}
})