mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-07 02:46:55 +08:00
chore: format code style and update linting configuration
This commit is contained in:
@@ -1,53 +1,54 @@
|
||||
import type { KeepLiveWS } from 'bilibili-live-ws/browser' // 导入 bilibili-live-ws 库
|
||||
// BaseDanmakuClient.ts
|
||||
import { EventModel, EventDataTypes } from '@/api/api-models'; // 导入事件模型和类型枚举
|
||||
import { KeepLiveWS } from 'bilibili-live-ws/browser'; // 导入 bilibili-live-ws 库
|
||||
import type { EventModel } from '@/api/api-models'
|
||||
// 导入事件模型和类型枚举
|
||||
|
||||
// 定义基础弹幕客户端抽象类
|
||||
export default abstract class BaseDanmakuClient {
|
||||
constructor() {
|
||||
this.client = null; // 初始化客户端实例为 null
|
||||
this.client = null // 初始化客户端实例为 null
|
||||
// 初始化两套事件监听器存储
|
||||
this.eventsAsModel = this.createEmptyEventModelListeners();
|
||||
this.eventsRaw = this.createEmptyRawEventlisteners();
|
||||
this.eventsAsModel = this.createEmptyEventModelListeners()
|
||||
this.eventsRaw = this.createEmptyRawEventlisteners()
|
||||
}
|
||||
|
||||
// WebSocket 客户端实例
|
||||
public client: KeepLiveWS | null;
|
||||
public client: KeepLiveWS | null
|
||||
|
||||
// 客户端连接状态
|
||||
public state: 'padding' | 'connected' | 'connecting' | 'disconnected' =
|
||||
'padding';
|
||||
public state: 'padding' | 'connected' | 'connecting' | 'disconnected'
|
||||
= 'padding'
|
||||
|
||||
// 客户端类型 (由子类实现)
|
||||
public abstract type: 'openlive' | 'direct';
|
||||
public abstract type: 'openlive' | 'direct'
|
||||
// 目标服务器地址 (由子类实现)
|
||||
public abstract serverUrl: string;
|
||||
public abstract serverUrl: string
|
||||
|
||||
// --- 事件系统 1: 使用 EventModel ---
|
||||
// 事件监听器集合 (统一使用 EventModel)
|
||||
public eventsAsModel: {
|
||||
danmaku: ((arg1: EventModel, arg2?: any) => void)[];
|
||||
gift: ((arg1: EventModel, arg2?: any) => void)[];
|
||||
sc: ((arg1: EventModel, arg2?: any) => void)[];
|
||||
guard: ((arg1: EventModel, arg2?: any) => void)[];
|
||||
enter: ((arg1: EventModel, arg2?: any) => void)[]; // 新增: 用户进入事件
|
||||
scDel: ((arg1: EventModel, arg2?: any) => void)[]; // 新增: SC 删除事件
|
||||
all: ((arg1: any) => void)[]; // 'all' 事件监听器接收原始消息或特定事件包
|
||||
follow: ((arg1: EventModel, arg2?: any) => void)[]; // 新增: 关注事件
|
||||
};
|
||||
danmaku: ((arg1: EventModel, arg2?: any) => void)[]
|
||||
gift: ((arg1: EventModel, arg2?: any) => void)[]
|
||||
sc: ((arg1: EventModel, arg2?: any) => void)[]
|
||||
guard: ((arg1: EventModel, arg2?: any) => void)[]
|
||||
enter: ((arg1: EventModel, arg2?: any) => void)[] // 新增: 用户进入事件
|
||||
scDel: ((arg1: EventModel, arg2?: any) => void)[] // 新增: SC 删除事件
|
||||
all: ((arg1: any) => void)[] // 'all' 事件监听器接收原始消息或特定事件包
|
||||
follow: ((arg1: EventModel, arg2?: any) => void)[] // 新增: 关注事件
|
||||
}
|
||||
|
||||
// --- 事件系统 2: 使用原始数据类型 ---
|
||||
// 事件监听器集合 (使用原始数据结构, 类型设为 any, 由具体实现和调用者保证)
|
||||
public eventsRaw: {
|
||||
danmaku: ((arg1: any, arg2?: any) => void)[];
|
||||
gift: ((arg1: any, arg2?: any) => void)[];
|
||||
sc: ((arg1: any, arg2?: any) => void)[];
|
||||
guard: ((arg1: any, arg2?: any) => void)[];
|
||||
enter: ((arg1: any, arg2?: any) => void)[]; // 新增: 用户进入事件
|
||||
scDel: ((arg1: any, arg2?: any) => void)[]; // 新增: SC 删除事件
|
||||
all: ((arg1: any) => void)[]; // 'all' 事件监听器接收原始消息或特定事件包
|
||||
follow: ((arg1: any, arg2?: any) => void)[]; // 新增: 关注事件
|
||||
};
|
||||
danmaku: ((arg1: any, arg2?: any) => void)[]
|
||||
gift: ((arg1: any, arg2?: any) => void)[]
|
||||
sc: ((arg1: any, arg2?: any) => void)[]
|
||||
guard: ((arg1: any, arg2?: any) => void)[]
|
||||
enter: ((arg1: any, arg2?: any) => void)[] // 新增: 用户进入事件
|
||||
scDel: ((arg1: any, arg2?: any) => void)[] // 新增: SC 删除事件
|
||||
all: ((arg1: any) => void)[] // 'all' 事件监听器接收原始消息或特定事件包
|
||||
follow: ((arg1: any, arg2?: any) => void)[] // 新增: 关注事件
|
||||
}
|
||||
|
||||
// 创建空的 EventModel 监听器对象
|
||||
public createEmptyEventModelListeners() {
|
||||
@@ -60,7 +61,7 @@ export default abstract class BaseDanmakuClient {
|
||||
scDel: [],
|
||||
all: [],
|
||||
follow: [], // 初始化 follow 事件
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 创建空的 RawEvent 监听器对象
|
||||
@@ -74,63 +75,65 @@ export default abstract class BaseDanmakuClient {
|
||||
scDel: [],
|
||||
all: [],
|
||||
follow: [], // 初始化 follow 事件
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动弹幕客户端连接
|
||||
* @returns Promise<{ success: boolean; message: string }> 启动结果
|
||||
*/
|
||||
public async Start(): Promise<{ success: boolean; message: string; }> {
|
||||
public async Start(): Promise<{ success: boolean, message: string }> {
|
||||
// 如果已连接,直接返回成功
|
||||
if (this.state === 'connected') {
|
||||
return {
|
||||
success: true,
|
||||
message: '弹幕客户端已启动',
|
||||
};
|
||||
}
|
||||
}
|
||||
// 如果正在连接中,返回提示
|
||||
if (this.state === 'connecting') {
|
||||
return {
|
||||
success: false,
|
||||
message: '弹幕客户端正在启动',
|
||||
};
|
||||
}
|
||||
}
|
||||
// 设置状态为连接中
|
||||
this.state = 'connecting';
|
||||
this.state = 'connecting'
|
||||
try {
|
||||
// 确保 client 为 null 才初始化
|
||||
if (!this.client) {
|
||||
console.log(`[${this.type}] 正在启动弹幕客户端`);
|
||||
console.log(`[${this.type}] 正在启动弹幕客户端`)
|
||||
// 调用子类实现的初始化方法
|
||||
const result = await this.initClient();
|
||||
const result = await this.initClient()
|
||||
if (result.success) {
|
||||
this.state = 'connected';
|
||||
console.log(`[${this.type}] 弹幕客户端已完成启动`);
|
||||
this.state = 'connected'
|
||||
console.log(`[${this.type}] 弹幕客户端已完成启动`)
|
||||
} else {
|
||||
this.state = 'disconnected';
|
||||
console.error(`[${this.type}] 弹幕客户端启动失败: ${result.message}`);
|
||||
this.state = 'disconnected'
|
||||
console.error(`[${this.type}] 弹幕客户端启动失败: ${result.message}`)
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
} else {
|
||||
console.warn(`[${this.type}] 客户端实例已存在但状态异常,尝试重置状态`);
|
||||
this.state = 'disconnected';
|
||||
console.warn(`[${this.type}] 客户端实例已存在但状态异常,尝试重置状态`)
|
||||
this.state = 'disconnected'
|
||||
return {
|
||||
success: false,
|
||||
message: '客户端实例状态异常,请尝试重新启动',
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`[${this.type}] 启动过程中发生异常:`, err);
|
||||
this.state = 'disconnected';
|
||||
console.error(`[${this.type}] 启动过程中发生异常:`, err)
|
||||
this.state = 'disconnected'
|
||||
if (this.client) {
|
||||
try { this.client.close(); } catch { }
|
||||
this.client = null;
|
||||
try {
|
||||
this.client.close()
|
||||
} catch { }
|
||||
this.client = null
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: err?.message || err?.toString() || '未知错误',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,24 +143,24 @@ export default abstract class BaseDanmakuClient {
|
||||
public Stop() {
|
||||
// 如果已断开,则无需操作
|
||||
if (this.state === 'disconnected') {
|
||||
return;
|
||||
return
|
||||
}
|
||||
// 设置状态为已断开
|
||||
this.state = 'disconnected';
|
||||
this.state = 'disconnected'
|
||||
if (this.client) {
|
||||
console.log(`[${this.type}] 正在停止弹幕客户端`);
|
||||
console.log(`[${this.type}] 正在停止弹幕客户端`)
|
||||
try {
|
||||
this.client.close(); // 关闭 WebSocket 连接
|
||||
this.client.close() // 关闭 WebSocket 连接
|
||||
} catch (err) {
|
||||
console.error(`[${this.type}] 关闭客户端时发生错误:`, err);
|
||||
console.error(`[${this.type}] 关闭客户端时发生错误:`, err)
|
||||
}
|
||||
this.client = null; // 将客户端实例置为 null
|
||||
this.client = null // 将客户端实例置为 null
|
||||
} else {
|
||||
console.warn(`[${this.type}] 弹幕客户端未被启动, 忽略停止操作`);
|
||||
console.warn(`[${this.type}] 弹幕客户端未被启动, 忽略停止操作`)
|
||||
}
|
||||
// 注意: 清空所有事件监听器
|
||||
//this.eventsAsModel = this.createEmptyEventModelListeners();
|
||||
//this.eventsRaw = this.createEmptyRawEventlisteners();
|
||||
// this.eventsAsModel = this.createEmptyEventModelListeners();
|
||||
// this.eventsRaw = this.createEmptyRawEventlisteners();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,9 +168,9 @@ export default abstract class BaseDanmakuClient {
|
||||
* @returns Promise<{ success: boolean; message: string }> 初始化结果
|
||||
*/
|
||||
protected abstract initClient(): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
}>;
|
||||
success: boolean
|
||||
message: string
|
||||
}>
|
||||
|
||||
/**
|
||||
* 内部通用的客户端事件绑定和连接状态等待逻辑
|
||||
@@ -175,66 +178,70 @@ export default abstract class BaseDanmakuClient {
|
||||
* @returns Promise<{ success: boolean; message: string }> 连接结果
|
||||
*/
|
||||
protected async initClientInner(
|
||||
chatClient: KeepLiveWS
|
||||
): Promise<{ success: boolean; message: string; }> {
|
||||
let isConnected = false; // 标记是否连接成功
|
||||
let isError = false; // 标记是否发生错误
|
||||
let errorMsg = ''; // 存储错误信息
|
||||
chatClient: KeepLiveWS,
|
||||
): Promise<{ success: boolean, message: string }> {
|
||||
let isConnected = false // 标记是否连接成功
|
||||
let isError = false // 标记是否发生错误
|
||||
let errorMsg = '' // 存储错误信息
|
||||
|
||||
// 监听错误事件
|
||||
chatClient.on('error', (err: any) => {
|
||||
console.error(`[${this.type}] 客户端发生错误:`, err);
|
||||
isError = true;
|
||||
errorMsg = err?.message || err?.toString() || '未知错误';
|
||||
});
|
||||
console.error(`[${this.type}] 客户端发生错误:`, err)
|
||||
isError = true
|
||||
errorMsg = err?.message || err?.toString() || '未知错误'
|
||||
})
|
||||
|
||||
// 监听连接成功事件
|
||||
chatClient.on('live', () => {
|
||||
console.log(`[${this.type}] 弹幕客户端连接成功`);
|
||||
isConnected = true;
|
||||
});
|
||||
console.log(`[${this.type}] 弹幕客户端连接成功`)
|
||||
isConnected = true
|
||||
})
|
||||
|
||||
// 监听连接关闭事件
|
||||
chatClient.on('close', () => {
|
||||
console.log(`[${this.type}] 弹幕客户端连接已关闭`);
|
||||
console.log(`[${this.type}] 弹幕客户端连接已关闭`)
|
||||
if (this.state !== 'disconnected') {
|
||||
this.state = 'disconnected';
|
||||
this.client = null;
|
||||
this.state = 'disconnected'
|
||||
this.client = null
|
||||
}
|
||||
isConnected = false; // 标记为未连接
|
||||
});
|
||||
isConnected = false // 标记为未连接
|
||||
})
|
||||
|
||||
// 监听原始消息事件 (通用)
|
||||
// 注意: 子类可能也会监听特定事件名, 这里的 'msg' 是备用或处理未被特定监听器捕获的事件
|
||||
chatClient.on('msg', (command: any) => this.onRawMessage(command));
|
||||
chatClient.on('msg', (command: any) => this.onRawMessage(command))
|
||||
|
||||
this.client = chatClient; // 保存客户端实例
|
||||
this.client = chatClient // 保存客户端实例
|
||||
|
||||
// 等待连接成功或发生错误
|
||||
const timeout = 30000; // 30 秒超时
|
||||
const startTime = Date.now();
|
||||
const timeout = 30000 // 30 秒超时
|
||||
const startTime = Date.now()
|
||||
while (!isConnected && !isError) {
|
||||
if (Date.now() - startTime > timeout) {
|
||||
isError = true;
|
||||
errorMsg = '连接超时';
|
||||
console.error(`[${this.type}] ${errorMsg}`);
|
||||
break;
|
||||
isError = true
|
||||
errorMsg = '连接超时'
|
||||
console.error(`[${this.type}] ${errorMsg}`)
|
||||
break
|
||||
}
|
||||
await new Promise((resolve) => { setTimeout(resolve, 500); });
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500)
|
||||
})
|
||||
}
|
||||
|
||||
// 如果连接过程中发生错误,清理客户端实例
|
||||
if (isError && this.client) {
|
||||
try { this.client.close(); } catch { }
|
||||
this.client = null;
|
||||
this.state = 'disconnected';
|
||||
try {
|
||||
this.client.close()
|
||||
} catch { }
|
||||
this.client = null
|
||||
this.state = 'disconnected'
|
||||
}
|
||||
|
||||
// 返回连接结果
|
||||
return {
|
||||
success: isConnected && !isError,
|
||||
message: errorMsg,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,12 +251,16 @@ export default abstract class BaseDanmakuClient {
|
||||
public onRawMessage = (command: any) => {
|
||||
// 触发 'all' 事件监听器 (两套系统都触发)
|
||||
try {
|
||||
this.eventsAsModel.all?.forEach((listener) => { listener(command); });
|
||||
this.eventsRaw.all?.forEach((listener) => { listener(command); });
|
||||
this.eventsAsModel.all?.forEach((listener) => {
|
||||
listener(command)
|
||||
})
|
||||
this.eventsRaw.all?.forEach((listener) => {
|
||||
listener(command)
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(`[${this.type}] 处理 'all' 事件监听器时出错:`, err, command);
|
||||
console.error(`[${this.type}] 处理 'all' 事件监听器时出错:`, err, command)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// --- 抽象处理方法 (子类实现) ---
|
||||
// 这些方法负责接收原始数据, 触发 RawEvent, 转换数据, 触发 ModelEvent
|
||||
@@ -259,98 +270,96 @@ export default abstract class BaseDanmakuClient {
|
||||
* @param data - 原始消息数据部分 (any 类型)
|
||||
* @param rawCommand - 完整的原始消息对象 (可选, any 类型)
|
||||
*/
|
||||
public abstract onDanmaku(comand: any): void;
|
||||
public abstract onDanmaku(comand: any): void
|
||||
/**
|
||||
* 处理礼物消息 (子类实现)
|
||||
* @param data - 原始消息数据部分 (any 类型)
|
||||
* @param rawCommand - 完整的原始消息对象 (可选, any 类型)
|
||||
*/
|
||||
public abstract onGift(comand: any): void;
|
||||
public abstract onGift(comand: any): void
|
||||
/**
|
||||
* 处理 Super Chat 消息 (子类实现)
|
||||
* @param data - 原始消息数据部分 (any 类型)
|
||||
* @param rawCommand - 完整的原始消息对象 (可选, any 类型)
|
||||
*/
|
||||
public abstract onSC(comand: any): void;
|
||||
public abstract onSC(comand: any): void
|
||||
/**
|
||||
* 处理上舰/舰队消息 (子类实现)
|
||||
* @param data - 原始消息数据部分 (any 类型)
|
||||
* @param rawCommand - 完整的原始消息对象 (可选, any 类型)
|
||||
*/
|
||||
public abstract onGuard(comand: any): void;
|
||||
public abstract onGuard(comand: any): void
|
||||
/**
|
||||
* 处理用户进入消息 (子类实现)
|
||||
* @param data - 原始消息数据部分 (any 类型)
|
||||
* @param rawCommand - 完整的原始消息对象 (可选, any 类型)
|
||||
*/
|
||||
public abstract onEnter(comand: any): void;
|
||||
public abstract onEnter(comand: any): void
|
||||
/**
|
||||
* 处理 SC 删除消息 (子类实现)
|
||||
* @param data - 原始消息数据部分 (any 类型) - 通常可能只包含 message_id
|
||||
* @param rawCommand - 完整的原始消息对象 (可选, any 类型)
|
||||
*/
|
||||
public abstract onScDel(comand: any): void;
|
||||
|
||||
* 处理 SC 删除消息 (子类实现)
|
||||
* @param data - 原始消息数据部分 (any 类型) - 通常可能只包含 message_id
|
||||
* @param rawCommand - 完整的原始消息对象 (可选, any 类型)
|
||||
*/
|
||||
public abstract onScDel(comand: any): void
|
||||
|
||||
// --- 事件系统 1: on/off (使用 EventModel) ---
|
||||
public onEvent(eventName: 'danmaku', listener: (arg1: EventModel, arg2?: any) => void): this;
|
||||
public onEvent(eventName: 'gift', listener: (arg1: EventModel, arg2?: any) => void): this;
|
||||
public onEvent(eventName: 'sc', listener: (arg1: EventModel, arg2?: any) => void): this;
|
||||
public onEvent(eventName: 'guard', listener: (arg1: EventModel, arg2?: any) => void): this;
|
||||
public onEvent(eventName: 'enter', listener: (arg1: EventModel, arg2?: any) => void): this; // 新增
|
||||
public onEvent(eventName: 'scDel', listener: (arg1: EventModel, arg2?: any) => void): this; // 新增
|
||||
public onEvent(eventName: 'all', listener: (arg1: any) => void): this;
|
||||
public onEvent(eventName: 'follow', listener: (arg1: EventModel, arg2?: any) => void): this; // 新增
|
||||
public onEvent(eventName: 'danmaku', listener: (arg1: EventModel, arg2?: any) => void): this
|
||||
public onEvent(eventName: 'gift', listener: (arg1: EventModel, arg2?: any) => void): this
|
||||
public onEvent(eventName: 'sc', listener: (arg1: EventModel, arg2?: any) => void): this
|
||||
public onEvent(eventName: 'guard', listener: (arg1: EventModel, arg2?: any) => void): this
|
||||
public onEvent(eventName: 'enter', listener: (arg1: EventModel, arg2?: any) => void): this // 新增
|
||||
public onEvent(eventName: 'scDel', listener: (arg1: EventModel, arg2?: any) => void): this // 新增
|
||||
public onEvent(eventName: 'all', listener: (arg1: any) => void): this
|
||||
public onEvent(eventName: 'follow', listener: (arg1: EventModel, arg2?: any) => void): this // 新增
|
||||
public onEvent(eventName: keyof BaseDanmakuClient['eventsAsModel'], listener: (...args: any[]) => void): this {
|
||||
if (!this.eventsAsModel[eventName]) {
|
||||
// @ts-ignore
|
||||
this.eventsAsModel[eventName] = [];
|
||||
this.eventsAsModel[eventName] = []
|
||||
}
|
||||
// @ts-ignore
|
||||
this.eventsAsModel[eventName].push(listener);
|
||||
return this;
|
||||
this.eventsAsModel[eventName].push(listener)
|
||||
return this
|
||||
}
|
||||
|
||||
public offEvent(eventName: keyof BaseDanmakuClient['eventsAsModel'], listener: (...args: any[]) => void): this {
|
||||
if (this.eventsAsModel[eventName]?.length) {
|
||||
// @ts-ignore
|
||||
const index = this.eventsAsModel[eventName].indexOf(listener);
|
||||
const index = this.eventsAsModel[eventName].indexOf(listener)
|
||||
if (index > -1) {
|
||||
this.eventsAsModel[eventName].splice(index, 1);
|
||||
this.eventsAsModel[eventName].splice(index, 1)
|
||||
}
|
||||
}
|
||||
return this;
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
// --- 事件系统 2: on/off (使用原始数据) ---
|
||||
// 注意: listener 的 arg1 类型为 any, 需要调用者根据 eventName 自行转换或处理
|
||||
public on(eventName: 'danmaku', listener: (arg1: any, arg2?: any) => void): this;
|
||||
public on(eventName: 'gift', listener: (arg1: any, arg2?: any) => void): this;
|
||||
public on(eventName: 'sc', listener: (arg1: any, arg2?: any) => void): this;
|
||||
public on(eventName: 'guard', listener: (arg1: any, arg2?: any) => void): this;
|
||||
public on(eventName: 'enter', listener: (arg1: any, arg2?: any) => void): this; // 新增
|
||||
public on(eventName: 'scDel', listener: (arg1: any, arg2?: any) => void): this; // 新增
|
||||
public on(eventName: 'all', listener: (arg1: any) => void): this;
|
||||
public on(eventName: 'follow', listener: (arg1: any, arg2?: any) => void): this; // 新增
|
||||
public on(eventName: 'danmaku', listener: (arg1: any, arg2?: any) => void): this
|
||||
public on(eventName: 'gift', listener: (arg1: any, arg2?: any) => void): this
|
||||
public on(eventName: 'sc', listener: (arg1: any, arg2?: any) => void): this
|
||||
public on(eventName: 'guard', listener: (arg1: any, arg2?: any) => void): this
|
||||
public on(eventName: 'enter', listener: (arg1: any, arg2?: any) => void): this // 新增
|
||||
public on(eventName: 'scDel', listener: (arg1: any, arg2?: any) => void): this // 新增
|
||||
public on(eventName: 'all', listener: (arg1: any) => void): this
|
||||
public on(eventName: 'follow', listener: (arg1: any, arg2?: any) => void): this // 新增
|
||||
public on(eventName: keyof BaseDanmakuClient['eventsRaw'], listener: (...args: any[]) => void): this {
|
||||
if (!this.eventsRaw[eventName]) {
|
||||
// @ts-ignore
|
||||
this.eventsRaw[eventName] = [];
|
||||
this.eventsRaw[eventName] = []
|
||||
}
|
||||
// @ts-ignore
|
||||
this.eventsRaw[eventName].push(listener);
|
||||
return this;
|
||||
this.eventsRaw[eventName].push(listener)
|
||||
return this
|
||||
}
|
||||
|
||||
public off(eventName: keyof BaseDanmakuClient['eventsRaw'], listener: (...args: any[]) => void): this {
|
||||
if (this.eventsRaw[eventName]?.length) {
|
||||
// @ts-ignore
|
||||
const index = this.eventsRaw[eventName].indexOf(listener);
|
||||
const index = this.eventsRaw[eventName].indexOf(listener)
|
||||
if (index > -1) {
|
||||
this.eventsRaw[eventName].splice(index, 1);
|
||||
this.eventsRaw[eventName].splice(index, 1)
|
||||
}
|
||||
}
|
||||
return this;
|
||||
return this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +1,65 @@
|
||||
import { KeepLiveWS } from 'bilibili-live-ws/browser';
|
||||
import BaseDanmakuClient from './BaseDanmakuClient';
|
||||
import { EventDataTypes, GuardLevel } from '@/api/api-models';
|
||||
import { getUserAvatarUrl, GuidUtils } from '@/Utils';
|
||||
import { AVATAR_URL } from '../constants';
|
||||
export type DirectClientAuthInfo = {
|
||||
token: string;
|
||||
roomId: number;
|
||||
tokenUserId: number;
|
||||
buvid: string;
|
||||
};
|
||||
/** 直播间弹幕客户端, 只能在vtsuru.client环境使用
|
||||
import { KeepLiveWS } from 'bilibili-live-ws/browser'
|
||||
import { EventDataTypes, GuardLevel } from '@/api/api-models'
|
||||
import { GuidUtils } from '@/Utils'
|
||||
import { AVATAR_URL } from '../constants'
|
||||
import BaseDanmakuClient from './BaseDanmakuClient'
|
||||
|
||||
export interface DirectClientAuthInfo {
|
||||
token: string
|
||||
roomId: number
|
||||
tokenUserId: number
|
||||
buvid: string
|
||||
}
|
||||
/**
|
||||
* 直播间弹幕客户端, 只能在vtsuru.client环境使用
|
||||
*
|
||||
*/
|
||||
export default class DirectClient extends BaseDanmakuClient {
|
||||
public serverUrl: string = 'wss://broadcastlv.chat.bilibili.com/sub';
|
||||
public serverUrl: string = 'wss://broadcastlv.chat.bilibili.com/sub'
|
||||
|
||||
constructor(auth: DirectClientAuthInfo) {
|
||||
super();
|
||||
this.authInfo = auth;
|
||||
super()
|
||||
this.authInfo = auth
|
||||
}
|
||||
|
||||
public type = 'direct' as const;
|
||||
public type = 'direct' as const
|
||||
|
||||
public readonly authInfo: DirectClientAuthInfo;
|
||||
public readonly authInfo: DirectClientAuthInfo
|
||||
|
||||
protected async initClient(): Promise<{ success: boolean; message: string; }> {
|
||||
protected async initClient(): Promise<{ success: boolean, message: string }> {
|
||||
if (this.authInfo) {
|
||||
const chatClient = new KeepLiveWS(this.authInfo.roomId, {
|
||||
key: this.authInfo.token,
|
||||
buvid: this.authInfo.buvid,
|
||||
uid: this.authInfo.tokenUserId,
|
||||
protover: 3
|
||||
});
|
||||
protover: 3,
|
||||
})
|
||||
|
||||
chatClient.on('live', () => {
|
||||
console.log('[direct] 已连接房间: ' + this.authInfo.roomId);
|
||||
});
|
||||
chatClient.on('DANMU_MSG', (data) => this.onDanmaku(data));
|
||||
chatClient.on('SEND_GIFT', (data) => this.onGift(data));
|
||||
chatClient.on('GUARD_BUY', (data) => this.onGuard(data));
|
||||
chatClient.on('SUPER_CHAT_MESSAGE', (data) => this.onSC(data));
|
||||
chatClient.on('INTERACT_WORD', (data) => this.onEnter(data));
|
||||
chatClient.on('SUPER_CHAT_MESSAGE_DELETE', (data) => this.onScDel(data));
|
||||
console.log(`[direct] 已连接房间: ${this.authInfo.roomId}`)
|
||||
})
|
||||
chatClient.on('DANMU_MSG', data => this.onDanmaku(data))
|
||||
chatClient.on('SEND_GIFT', data => this.onGift(data))
|
||||
chatClient.on('GUARD_BUY', data => this.onGuard(data))
|
||||
chatClient.on('SUPER_CHAT_MESSAGE', data => this.onSC(data))
|
||||
chatClient.on('INTERACT_WORD', data => this.onEnter(data))
|
||||
chatClient.on('SUPER_CHAT_MESSAGE_DELETE', data => this.onScDel(data))
|
||||
|
||||
return await super.initClientInner(chatClient);
|
||||
return super.initClientInner(chatClient)
|
||||
} else {
|
||||
console.log('[direct] 无法开启场次, 未提供弹幕客户端认证信息');
|
||||
console.log('[direct] 无法开启场次, 未提供弹幕客户端认证信息')
|
||||
return {
|
||||
success: false,
|
||||
message: '未提供弹幕客户端认证信息'
|
||||
};
|
||||
message: '未提供弹幕客户端认证信息',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public onDanmaku(command: any): void {
|
||||
const info = command.info;
|
||||
this.eventsRaw?.danmaku?.forEach((d) => { d(info, command); });
|
||||
const info = command.info
|
||||
this.eventsRaw?.danmaku?.forEach((d) => {
|
||||
d(info, command)
|
||||
})
|
||||
this.eventsAsModel.danmaku?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
@@ -69,18 +74,21 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
fans_medal_level: info[0][15].user.medal?.level,
|
||||
fans_medal_name: info[0][15].user.medal?.name,
|
||||
fans_medal_wearing_status: info[0][15].user.medal?.is_light === 1,
|
||||
emoji: info[0]?.[13]?.url?.replace("http://", "https://") || '',
|
||||
uface: info[0][15].user.base.face.replace("http://", "https://"),
|
||||
emoji: info[0]?.[13]?.url?.replace('http://', 'https://') || '',
|
||||
uface: info[0][15].user.base.face.replace('http://', 'https://'),
|
||||
open_id: '',
|
||||
ouid: GuidUtils.numToGuid(info[2][0])
|
||||
ouid: GuidUtils.numToGuid(info[2][0]),
|
||||
},
|
||||
command
|
||||
);
|
||||
});
|
||||
command,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public onGift(command: any): void {
|
||||
const data = command.data;
|
||||
this.eventsRaw?.gift?.forEach((d) => { d(data, command); });
|
||||
const data = command.data
|
||||
this.eventsRaw?.gift?.forEach((d) => {
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.gift?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
@@ -95,17 +103,20 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
fans_medal_level: data.medal_info.medal_level,
|
||||
fans_medal_name: data.medal_info.medal_name,
|
||||
fans_medal_wearing_status: data.medal_info.is_lighted === 1,
|
||||
uface: data.face.replace("http://", "https://"),
|
||||
uface: data.face.replace('http://', 'https://'),
|
||||
open_id: '',
|
||||
ouid: GuidUtils.numToGuid(data.uid)
|
||||
ouid: GuidUtils.numToGuid(data.uid),
|
||||
},
|
||||
command
|
||||
);
|
||||
});
|
||||
command,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public onSC(command: any): void {
|
||||
const data = command.data;
|
||||
this.eventsRaw?.sc?.forEach((d) => { d(data, command); });
|
||||
const data = command.data
|
||||
this.eventsRaw?.sc?.forEach((d) => {
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.sc?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
@@ -120,17 +131,20 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
fans_medal_level: data.medal_info.medal_level,
|
||||
fans_medal_name: data.medal_info.medal_name,
|
||||
fans_medal_wearing_status: data.medal_info.is_lighted === 1,
|
||||
uface: data.user_info.face.replace("http://", "https://"),
|
||||
uface: data.user_info.face.replace('http://', 'https://'),
|
||||
open_id: '',
|
||||
ouid: GuidUtils.numToGuid(data.uid)
|
||||
ouid: GuidUtils.numToGuid(data.uid),
|
||||
},
|
||||
command
|
||||
);
|
||||
});
|
||||
command,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public onGuard(command: any): void {
|
||||
const data = command.data;
|
||||
this.eventsRaw?.guard?.forEach((d) => { d(data, command); });
|
||||
const data = command.data
|
||||
this.eventsRaw?.guard?.forEach((d) => {
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.guard?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
@@ -147,18 +161,21 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
fans_medal_wearing_status: false,
|
||||
uface: AVATAR_URL + data.uid,
|
||||
open_id: '',
|
||||
ouid: GuidUtils.numToGuid(data.uid)
|
||||
ouid: GuidUtils.numToGuid(data.uid),
|
||||
},
|
||||
command
|
||||
);
|
||||
});
|
||||
command,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public onEnter(command: any): void {
|
||||
const data = command.data;
|
||||
const msgType = data.msg_type;
|
||||
const data = command.data
|
||||
const msgType = data.msg_type
|
||||
|
||||
if (msgType === 1) {
|
||||
this.eventsRaw?.enter?.forEach((d) => { d(data, command); });
|
||||
this.eventsRaw?.enter?.forEach((d) => {
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.enter?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
@@ -173,16 +190,17 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
fans_medal_level: data.fans_medal?.medal_level || 0,
|
||||
fans_medal_name: data.fans_medal?.medal_name || '',
|
||||
fans_medal_wearing_status: data.fans_medal?.is_lighted === 1,
|
||||
uface: data.face?.replace("http://", "https://") || (AVATAR_URL + data.uid),
|
||||
uface: data.face?.replace('http://', 'https://') || (AVATAR_URL + data.uid),
|
||||
open_id: '',
|
||||
ouid: GuidUtils.numToGuid(data.uid)
|
||||
ouid: GuidUtils.numToGuid(data.uid),
|
||||
},
|
||||
command
|
||||
);
|
||||
});
|
||||
}
|
||||
else if (msgType === 2) {
|
||||
this.eventsRaw?.follow?.forEach((d) => { d(data, command); });
|
||||
command,
|
||||
)
|
||||
})
|
||||
} else if (msgType === 2) {
|
||||
this.eventsRaw?.follow?.forEach((d) => {
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.follow?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
@@ -197,18 +215,21 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
fans_medal_level: data.fans_medal?.medal_level || 0,
|
||||
fans_medal_name: data.fans_medal?.medal_name || '',
|
||||
fans_medal_wearing_status: data.fans_medal?.is_lighted === 1,
|
||||
uface: data.face?.replace("http://", "https://") || (AVATAR_URL + data.uid),
|
||||
uface: data.face?.replace('http://', 'https://') || (AVATAR_URL + data.uid),
|
||||
open_id: '',
|
||||
ouid: GuidUtils.numToGuid(data.uid)
|
||||
ouid: GuidUtils.numToGuid(data.uid),
|
||||
},
|
||||
command
|
||||
);
|
||||
});
|
||||
command,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public onScDel(command: any): void {
|
||||
const data = command.data;
|
||||
this.eventsRaw?.scDel?.forEach((d) => { d(data, command); });
|
||||
const data = command.data
|
||||
this.eventsRaw?.scDel?.forEach((d) => {
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.scDel?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
@@ -225,10 +246,10 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
fans_medal_wearing_status: false,
|
||||
uface: '',
|
||||
open_id: '',
|
||||
ouid: ''
|
||||
ouid: '',
|
||||
},
|
||||
command
|
||||
);
|
||||
});
|
||||
command,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +1,130 @@
|
||||
import { EventDataTypes, OpenLiveInfo } from '@/api/api-models';
|
||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query';
|
||||
import { GuidUtils } from '@/Utils';
|
||||
import { KeepLiveWS } from 'bilibili-live-ws/browser';
|
||||
import { clearInterval, setInterval } from 'worker-timers';
|
||||
import { OPEN_LIVE_API_URL } from '../constants';
|
||||
import BaseDanmakuClient from './BaseDanmakuClient';
|
||||
import type { OpenLiveInfo } from '@/api/api-models'
|
||||
import { KeepLiveWS } from 'bilibili-live-ws/browser'
|
||||
import { clearInterval, setInterval } from 'worker-timers'
|
||||
import { EventDataTypes } from '@/api/api-models'
|
||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||
import { GuidUtils } from '@/Utils'
|
||||
import { OPEN_LIVE_API_URL } from '../constants'
|
||||
import BaseDanmakuClient from './BaseDanmakuClient'
|
||||
|
||||
export default class OpenLiveClient extends BaseDanmakuClient {
|
||||
public serverUrl: string = '';
|
||||
public serverUrl: string = ''
|
||||
constructor(auth?: AuthInfo) {
|
||||
super();
|
||||
this.authInfo = auth;
|
||||
super()
|
||||
this.authInfo = auth
|
||||
}
|
||||
|
||||
public type = 'openlive' as const;
|
||||
public type = 'openlive' as const
|
||||
|
||||
private timer: any | undefined;
|
||||
private timer: any | undefined
|
||||
|
||||
public authInfo: AuthInfo | undefined;
|
||||
public roomAuthInfo: OpenLiveInfo | undefined;
|
||||
public authInfo: AuthInfo | undefined
|
||||
public roomAuthInfo: OpenLiveInfo | undefined
|
||||
|
||||
public async Start(): Promise<{ success: boolean; message: string; }> {
|
||||
const result = await super.Start();
|
||||
public async Start(): Promise<{ success: boolean, message: string }> {
|
||||
const result = await super.Start()
|
||||
if (result.success) {
|
||||
this.timer ??= setInterval(() => {
|
||||
this.sendHeartbeat();
|
||||
}, 20 * 1000);
|
||||
this.sendHeartbeat()
|
||||
}, 20 * 1000)
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public Stop() {
|
||||
super.Stop();
|
||||
clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
this.roomAuthInfo = undefined;
|
||||
return result
|
||||
}
|
||||
|
||||
protected async initClient(): Promise<{ success: boolean; message: string; }> {
|
||||
const auth = await this.getAuthInfo();
|
||||
public Stop() {
|
||||
super.Stop()
|
||||
clearInterval(this.timer)
|
||||
this.timer = undefined
|
||||
this.roomAuthInfo = undefined
|
||||
}
|
||||
|
||||
protected async initClient(): Promise<{ success: boolean, message: string }> {
|
||||
const auth = await this.getAuthInfo()
|
||||
if (auth.data) {
|
||||
const chatClient = new KeepLiveWS(auth.data.anchor_info.room_id, {
|
||||
authBody: JSON.parse(auth.data.websocket_info.auth_body),
|
||||
address: auth.data.websocket_info.wss_link[0]
|
||||
});
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_DM', (cmd) => this.onDanmaku(cmd));
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_GIFT', (cmd) => this.onGift(cmd));
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_GUARD', (cmd) => this.onGuard(cmd));
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_SC', (cmd) => this.onSC(cmd));
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_LIVE_ROOM_ENTER', (cmd) => this.onEnter(cmd));
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_SUPER_CHAT_DEL', (cmd) => this.onScDel(cmd));
|
||||
address: auth.data.websocket_info.wss_link[0],
|
||||
})
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_DM', cmd => this.onDanmaku(cmd))
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_GIFT', cmd => this.onGift(cmd))
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_GUARD', cmd => this.onGuard(cmd))
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_SC', cmd => this.onSC(cmd))
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_LIVE_ROOM_ENTER', cmd => this.onEnter(cmd))
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_SUPER_CHAT_DEL', cmd => this.onScDel(cmd))
|
||||
chatClient.on('live', () => {
|
||||
console.log(
|
||||
`[${this.type}] 已连接房间: ${auth.data?.anchor_info.room_id}`
|
||||
);
|
||||
});
|
||||
`[${this.type}] 已连接房间: ${auth.data?.anchor_info.room_id}`,
|
||||
)
|
||||
})
|
||||
|
||||
this.roomAuthInfo = auth.data;
|
||||
this.roomAuthInfo = auth.data
|
||||
|
||||
return await super.initClientInner(chatClient);
|
||||
return super.initClientInner(chatClient)
|
||||
} else {
|
||||
console.log(`[${this.type}] 无法开启场次: ` + auth.message);
|
||||
console.log(`[${this.type}] 无法开启场次: ${auth.message}`)
|
||||
return {
|
||||
success: false,
|
||||
message: auth.message
|
||||
};
|
||||
message: auth.message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getAuthInfo(): Promise<{
|
||||
data: OpenLiveInfo | null;
|
||||
message: string;
|
||||
data: OpenLiveInfo | null
|
||||
message: string
|
||||
}> {
|
||||
try {
|
||||
const data = await QueryPostAPI<OpenLiveInfo>(
|
||||
OPEN_LIVE_API_URL + 'start',
|
||||
this.authInfo?.Code ? this.authInfo : undefined
|
||||
);
|
||||
`${OPEN_LIVE_API_URL}start`,
|
||||
this.authInfo?.Code ? this.authInfo : undefined,
|
||||
)
|
||||
if (data.code == 200) {
|
||||
console.log(`[${this.type}] 已获取场次信息`);
|
||||
console.log(`[${this.type}] 已获取场次信息`)
|
||||
return {
|
||||
data: data.data,
|
||||
message: ''
|
||||
};
|
||||
message: '',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
data: null,
|
||||
message: data.message
|
||||
};
|
||||
message: data.message,
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
data: null,
|
||||
message: err?.toString() || '未知错误'
|
||||
};
|
||||
message: err?.toString() || '未知错误',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sendHeartbeat() {
|
||||
if (this.state !== 'connected') {
|
||||
clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
return;
|
||||
clearInterval(this.timer)
|
||||
this.timer = undefined
|
||||
return
|
||||
}
|
||||
const query = this.authInfo
|
||||
? QueryPostAPI<OpenLiveInfo>(
|
||||
OPEN_LIVE_API_URL + 'heartbeat',
|
||||
this.authInfo
|
||||
)
|
||||
: QueryGetAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat-internal');
|
||||
`${OPEN_LIVE_API_URL}heartbeat`,
|
||||
this.authInfo,
|
||||
)
|
||||
: QueryGetAPI<OpenLiveInfo>(`${OPEN_LIVE_API_URL}heartbeat-internal`)
|
||||
query.then((data) => {
|
||||
if (data.code != 200) {
|
||||
console.error(`[${this.type}] 心跳失败, 将重新连接`);
|
||||
this.client?.close();
|
||||
this.client = null;
|
||||
this.initClient();
|
||||
console.error(`[${this.type}] 心跳失败, 将重新连接`)
|
||||
this.client?.close()
|
||||
this.client = null
|
||||
this.initClient()
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
public onDanmaku(command: any) {
|
||||
const data = command.data as DanmakuInfo;
|
||||
const data = command.data as DanmakuInfo
|
||||
this.eventsRaw.danmaku?.forEach((d) => {
|
||||
d(data, command);
|
||||
});
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.danmaku?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
@@ -138,18 +142,19 @@ export default class OpenLiveClient extends BaseDanmakuClient {
|
||||
emoji: data.dm_type == 1 ? data.emoji_img_url : undefined,
|
||||
uface: data.uface,
|
||||
open_id: data.open_id,
|
||||
ouid: data.open_id ?? GuidUtils.numToGuid(data.uid)
|
||||
ouid: data.open_id ?? GuidUtils.numToGuid(data.uid),
|
||||
},
|
||||
command
|
||||
);
|
||||
});
|
||||
command,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public onGift(command: any) {
|
||||
const data = command.data as GiftInfo;
|
||||
const price = (data.price * data.gift_num) / 1000;
|
||||
const data = command.data as GiftInfo
|
||||
const price = (data.price * data.gift_num) / 1000
|
||||
this.eventsRaw.gift?.forEach((d) => {
|
||||
d(data, command);
|
||||
});
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.gift?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
@@ -166,17 +171,18 @@ export default class OpenLiveClient extends BaseDanmakuClient {
|
||||
fans_medal_wearing_status: data.fans_medal_wearing_status,
|
||||
uface: data.uface,
|
||||
open_id: data.open_id,
|
||||
ouid: data.open_id ?? GuidUtils.numToGuid(data.uid)
|
||||
ouid: data.open_id ?? GuidUtils.numToGuid(data.uid),
|
||||
},
|
||||
command
|
||||
);
|
||||
});
|
||||
command,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public onSC(command: any) {
|
||||
const data = command.data as SCInfo;
|
||||
const data = command.data as SCInfo
|
||||
this.eventsRaw.sc?.forEach((d) => {
|
||||
d(data, command);
|
||||
});
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.sc?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
@@ -193,17 +199,18 @@ export default class OpenLiveClient extends BaseDanmakuClient {
|
||||
fans_medal_wearing_status: data.fans_medal_wearing_status,
|
||||
uface: data.uface,
|
||||
open_id: data.open_id,
|
||||
ouid: data.open_id ?? GuidUtils.numToGuid(data.uid)
|
||||
ouid: data.open_id ?? GuidUtils.numToGuid(data.uid),
|
||||
},
|
||||
command
|
||||
);
|
||||
});
|
||||
command,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public onGuard(command: any) {
|
||||
const data = command.data as GuardInfo;
|
||||
const data = command.data as GuardInfo
|
||||
this.eventsRaw.guard?.forEach((d) => {
|
||||
d(data, command);
|
||||
});
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.guard?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
@@ -228,17 +235,18 @@ export default class OpenLiveClient extends BaseDanmakuClient {
|
||||
uface: data.user_info.uface,
|
||||
open_id: data.user_info.open_id,
|
||||
ouid:
|
||||
data.user_info.open_id ?? GuidUtils.numToGuid(data.user_info.uid)
|
||||
data.user_info.open_id ?? GuidUtils.numToGuid(data.user_info.uid),
|
||||
},
|
||||
command
|
||||
);
|
||||
});
|
||||
command,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public onEnter(command: any): void {
|
||||
const data = command.data as EnterInfo;
|
||||
const data = command.data as EnterInfo
|
||||
this.eventsRaw.enter?.forEach((d) => {
|
||||
d(data);
|
||||
});
|
||||
d(data)
|
||||
})
|
||||
this.eventsAsModel.enter?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
@@ -255,17 +263,18 @@ export default class OpenLiveClient extends BaseDanmakuClient {
|
||||
uface: data.uface,
|
||||
open_id: data.open_id,
|
||||
uid: 0,
|
||||
ouid: data.open_id
|
||||
ouid: data.open_id,
|
||||
},
|
||||
command
|
||||
);
|
||||
});
|
||||
command,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public onScDel(command: any): void {
|
||||
const data = command.data as SCDelInfo;
|
||||
const data = command.data as SCDelInfo
|
||||
this.eventsRaw.scDel?.forEach((d) => {
|
||||
d(data, command);
|
||||
});
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.scDel?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
@@ -282,114 +291,114 @@ export default class OpenLiveClient extends BaseDanmakuClient {
|
||||
uface: '',
|
||||
open_id: '',
|
||||
uid: 0,
|
||||
ouid: ''
|
||||
ouid: '',
|
||||
},
|
||||
command
|
||||
);
|
||||
});
|
||||
command,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export interface DanmakuInfo {
|
||||
room_id: number;
|
||||
uid: number;
|
||||
open_id: string;
|
||||
uname: string;
|
||||
msg: string;
|
||||
msg_id: string;
|
||||
fans_medal_level: number;
|
||||
fans_medal_name: string;
|
||||
fans_medal_wearing_status: boolean;
|
||||
guard_level: number;
|
||||
timestamp: number;
|
||||
uface: string;
|
||||
emoji_img_url: string;
|
||||
dm_type: number;
|
||||
room_id: number
|
||||
uid: number
|
||||
open_id: string
|
||||
uname: string
|
||||
msg: string
|
||||
msg_id: string
|
||||
fans_medal_level: number
|
||||
fans_medal_name: string
|
||||
fans_medal_wearing_status: boolean
|
||||
guard_level: number
|
||||
timestamp: number
|
||||
uface: string
|
||||
emoji_img_url: string
|
||||
dm_type: number
|
||||
}
|
||||
export interface GiftInfo {
|
||||
room_id: number;
|
||||
uid: number;
|
||||
open_id: string;
|
||||
uname: string;
|
||||
uface: string;
|
||||
gift_id: number;
|
||||
gift_name: string;
|
||||
gift_num: number;
|
||||
price: number;
|
||||
paid: boolean;
|
||||
fans_medal_level: number;
|
||||
fans_medal_name: string;
|
||||
fans_medal_wearing_status: boolean;
|
||||
guard_level: number;
|
||||
timestamp: number;
|
||||
msg_id: string;
|
||||
room_id: number
|
||||
uid: number
|
||||
open_id: string
|
||||
uname: string
|
||||
uface: string
|
||||
gift_id: number
|
||||
gift_name: string
|
||||
gift_num: number
|
||||
price: number
|
||||
paid: boolean
|
||||
fans_medal_level: number
|
||||
fans_medal_name: string
|
||||
fans_medal_wearing_status: boolean
|
||||
guard_level: number
|
||||
timestamp: number
|
||||
msg_id: string
|
||||
anchor_info: {
|
||||
uid: number;
|
||||
uname: string;
|
||||
uface: string;
|
||||
};
|
||||
gift_icon: string;
|
||||
combo_gift: boolean;
|
||||
uid: number
|
||||
uname: string
|
||||
uface: string
|
||||
}
|
||||
gift_icon: string
|
||||
combo_gift: boolean
|
||||
combo_info: {
|
||||
combo_base_num: number;
|
||||
combo_count: number;
|
||||
combo_id: string;
|
||||
combo_timeout: number;
|
||||
};
|
||||
combo_base_num: number
|
||||
combo_count: number
|
||||
combo_id: string
|
||||
combo_timeout: number
|
||||
}
|
||||
}
|
||||
export interface SCInfo {
|
||||
room_id: number; // 直播间id
|
||||
uid: number; // 购买用户UID
|
||||
open_id: string;
|
||||
uname: string; // 购买的用户昵称
|
||||
uface: string; // 购买用户头像
|
||||
message_id: number; // 留言id(风控场景下撤回留言需要)
|
||||
message: string; // 留言内容
|
||||
msg_id: string; // 消息唯一id
|
||||
rmb: number; // 支付金额(元)
|
||||
timestamp: number; // 赠送时间秒级
|
||||
start_time: number; // 生效开始时间
|
||||
end_time: number; // 生效结束时间
|
||||
guard_level: number; // 对应房间大航海登记 (新增)
|
||||
fans_medal_level: number; // 对应房间勋章信息 (新增)
|
||||
fans_medal_name: string; // 对应房间勋章名字 (新增)
|
||||
fans_medal_wearing_status: boolean; // 该房间粉丝勋章佩戴情况 (新增)
|
||||
room_id: number // 直播间id
|
||||
uid: number // 购买用户UID
|
||||
open_id: string
|
||||
uname: string // 购买的用户昵称
|
||||
uface: string // 购买用户头像
|
||||
message_id: number // 留言id(风控场景下撤回留言需要)
|
||||
message: string // 留言内容
|
||||
msg_id: string // 消息唯一id
|
||||
rmb: number // 支付金额(元)
|
||||
timestamp: number // 赠送时间秒级
|
||||
start_time: number // 生效开始时间
|
||||
end_time: number // 生效结束时间
|
||||
guard_level: number // 对应房间大航海登记 (新增)
|
||||
fans_medal_level: number // 对应房间勋章信息 (新增)
|
||||
fans_medal_name: string // 对应房间勋章名字 (新增)
|
||||
fans_medal_wearing_status: boolean // 该房间粉丝勋章佩戴情况 (新增)
|
||||
}
|
||||
export interface GuardInfo {
|
||||
user_info: {
|
||||
uid: number; // 用户uid
|
||||
open_id: string;
|
||||
uname: string; // 用户昵称
|
||||
uface: string; // 用户头像
|
||||
};
|
||||
guard_level: number; // 对应的大航海等级 1总督 2提督 3舰长
|
||||
guard_num: number;
|
||||
price: number; // 购买金额(1000=1元)
|
||||
guard_unit: string; // (个月)
|
||||
fans_medal_level: number; // 粉丝勋章等级
|
||||
fans_medal_name: string; // 粉丝勋章名
|
||||
fans_medal_wearing_status: boolean; // 该房间粉丝勋章佩戴情况
|
||||
timestamp: number;
|
||||
room_id: number;
|
||||
msg_id: string; // 消息唯一id
|
||||
uid: number // 用户uid
|
||||
open_id: string
|
||||
uname: string // 用户昵称
|
||||
uface: string // 用户头像
|
||||
}
|
||||
guard_level: number // 对应的大航海等级 1总督 2提督 3舰长
|
||||
guard_num: number
|
||||
price: number // 购买金额(1000=1元)
|
||||
guard_unit: string // (个月)
|
||||
fans_medal_level: number // 粉丝勋章等级
|
||||
fans_medal_name: string // 粉丝勋章名
|
||||
fans_medal_wearing_status: boolean // 该房间粉丝勋章佩戴情况
|
||||
timestamp: number
|
||||
room_id: number
|
||||
msg_id: string // 消息唯一id
|
||||
}
|
||||
export interface EnterInfo {
|
||||
open_id: string;
|
||||
uname: string;
|
||||
uface: string;
|
||||
timestamp: number;
|
||||
room_id: number;
|
||||
open_id: string
|
||||
uname: string
|
||||
uface: string
|
||||
timestamp: number
|
||||
room_id: number
|
||||
}
|
||||
// 假设的 SC 删除事件原始信息结构 (需要根据实际情况调整)
|
||||
export interface SCDelInfo {
|
||||
room_id: number;
|
||||
message_ids: number[]; // 被删除的 SC 的 message_id
|
||||
msg_id: string; // 删除操作的消息 ID
|
||||
room_id: number
|
||||
message_ids: number[] // 被删除的 SC 的 message_id
|
||||
msg_id: string // 删除操作的消息 ID
|
||||
}
|
||||
export interface AuthInfo {
|
||||
Timestamp: string;
|
||||
Code: string;
|
||||
Mid: string;
|
||||
Caller: string;
|
||||
CodeSign: string;
|
||||
}
|
||||
Timestamp: string
|
||||
Code: string
|
||||
Mid: string
|
||||
Caller: string
|
||||
CodeSign: string
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AccountInfo, DanmakuModel, EventDataTypes, ResponseLiveInfoModel } from '@/api/api-models'
|
||||
import type { AccountInfo, DanmakuModel, ResponseLiveInfoModel } from '@/api/api-models'
|
||||
import { XMLBuilder } from 'fast-xml-parser'
|
||||
import { List } from 'linqts'
|
||||
import { EventDataTypes } from '@/api/api-models'
|
||||
|
||||
const builder = new XMLBuilder({
|
||||
attributeNamePrefix: '@',
|
||||
@@ -28,7 +29,7 @@ export function GetString(
|
||||
})
|
||||
.ToArray()
|
||||
const obj = {
|
||||
live: live,
|
||||
live,
|
||||
danmakus: tempDanmakus,
|
||||
}
|
||||
switch (type) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { GetSelfAccount, UpdateAccountLoop, useAccount } from "@/api/account";
|
||||
import { QueryGetAPI } from "@/api/query";
|
||||
import { useBiliAuth } from "@/store/useBiliAuth";
|
||||
import { useNotificationStore } from "@/store/useNotificationStore";
|
||||
import { h } from "vue";
|
||||
import HyperDX from '@hyperdx/browser';
|
||||
import EasySpeech from "easy-speech";
|
||||
import { createDiscreteApi, NButton, NFlex, NText } from "naive-ui";
|
||||
import { apiFail, BASE_API_URL, isTauri } from "./constants";
|
||||
import { GetNotifactions } from "./notifactions";
|
||||
import HyperDX from '@hyperdx/browser'
|
||||
import EasySpeech from 'easy-speech'
|
||||
import { createDiscreteApi, NButton, NFlex, NText } from 'naive-ui'
|
||||
import { h } from 'vue'
|
||||
import { GetSelfAccount, UpdateAccountLoop, useAccount } from '@/api/account'
|
||||
import { QueryGetAPI } from '@/api/query'
|
||||
import { useBiliAuth } from '@/store/useBiliAuth'
|
||||
import { useNotificationStore } from '@/store/useNotificationStore'
|
||||
import { apiFail, BASE_API_URL, isTauri } from './constants'
|
||||
import { GetNotifactions } from './notifactions'
|
||||
|
||||
let currentVersion: string
|
||||
let isHaveNewVersion = false
|
||||
@@ -16,37 +16,36 @@ const { notification } = createDiscreteApi(['notification'])
|
||||
|
||||
export function InitVTsuru() {
|
||||
QueryGetAPI<string>(`${BASE_API_URL}vtsuru/version`)
|
||||
.then((version) => {
|
||||
if (version.code == 200) {
|
||||
currentVersion = version.data
|
||||
const savedVersion = localStorage.getItem('Version')
|
||||
localStorage.setItem('Version', currentVersion)
|
||||
.then((version) => {
|
||||
if (version.code == 200) {
|
||||
currentVersion = version.data
|
||||
const savedVersion = localStorage.getItem('Version')
|
||||
localStorage.setItem('Version', currentVersion)
|
||||
|
||||
if (currentVersion && savedVersion && savedVersion !== currentVersion) {
|
||||
setTimeout(() => {
|
||||
location.reload()
|
||||
}, 1000)
|
||||
// alert('发现新的版本更新, 请按 Ctrl+F5 强制刷新页面')
|
||||
notification.info({
|
||||
title: '发现新的版本更新',
|
||||
content: '将自动刷新页面',
|
||||
duration: 5000,
|
||||
meta: () => h(NText, { depth: 3 }, () => currentVersion),
|
||||
})
|
||||
if (currentVersion && savedVersion && savedVersion !== currentVersion) {
|
||||
setTimeout(() => {
|
||||
location.reload()
|
||||
}, 1000)
|
||||
// alert('发现新的版本更新, 请按 Ctrl+F5 强制刷新页面')
|
||||
notification.info({
|
||||
title: '发现新的版本更新',
|
||||
content: '将自动刷新页面',
|
||||
duration: 5000,
|
||||
meta: () => h(NText, { depth: 3 }, () => currentVersion),
|
||||
})
|
||||
} else {
|
||||
InitVersionCheck()
|
||||
}
|
||||
}
|
||||
else {
|
||||
InitVersionCheck();
|
||||
}
|
||||
}
|
||||
InitOther();
|
||||
})
|
||||
.catch(() => {
|
||||
apiFail.value = true
|
||||
console.log('默认API调用失败, 切换至故障转移节点')
|
||||
})
|
||||
.finally(async () => {
|
||||
InitOther()
|
||||
})
|
||||
.catch(() => {
|
||||
apiFail.value = true
|
||||
console.log('默认API调用失败, 切换至故障转移节点')
|
||||
})
|
||||
.finally(async () => {
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function InitOther() {
|
||||
@@ -55,7 +54,7 @@ async function InitOther() {
|
||||
apiKey: '7d1eb66c-24b8-445e-a406-dc2329fa9423',
|
||||
service: 'vtsuru.live',
|
||||
tracePropagationTargets: [/vtsuru.suki.club/i], // Set to link traces from frontend to backend requests
|
||||
//consoleCapture: true, // Capture console logs (default false)
|
||||
// consoleCapture: true, // Capture console logs (default false)
|
||||
advancedNetworkCapture: true, // Capture full HTTP request/response headers and bodies (default false)
|
||||
ignoreUrls: [/localhost/i],
|
||||
})
|
||||
@@ -98,8 +97,7 @@ function InitVersionCheck() {
|
||||
|
||||
if (window.$route.meta.forceReload || isTauri()) {
|
||||
location.reload()
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const n = notification.info({
|
||||
title: '发现新的版本更新',
|
||||
content: '是否现在刷新?',
|
||||
@@ -140,12 +138,10 @@ function InitTTS() {
|
||||
EasySpeech.init({ maxTimeout: 5000, interval: 250 })
|
||||
.then(() => console.log('[SpeechSynthesis] 已加载tts服务'))
|
||||
.catch(e => console.error(e))
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
console.log('[SpeechSynthesis] 当前浏览器不支持tts服务')
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.log('[SpeechSynthesis] 当前浏览器不支持tts服务')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useAccount } from '@/api/account'
|
||||
import type { DataConnection } from 'peerjs'
|
||||
import Peer from 'peerjs'
|
||||
import { useVTsuruHub } from '@/store/useVTsuruHub'
|
||||
import Peer, { DataConnection } from 'peerjs'
|
||||
|
||||
export interface ComponentsEventHubModel {
|
||||
IsMaster: boolean
|
||||
@@ -34,6 +34,9 @@ export abstract class BaseRTCClient {
|
||||
|
||||
abstract type: 'master' | 'slave'
|
||||
|
||||
protected abstract connectRTC(): void
|
||||
protected abstract processData(conn: DataConnection, data: RTCData): void
|
||||
|
||||
public on(eventName: string, listener: (args: any) => void) {
|
||||
eventName = eventName.toLowerCase()
|
||||
if (!this.events[eventName]) {
|
||||
@@ -53,89 +56,35 @@ export abstract class BaseRTCClient {
|
||||
|
||||
this.send('VTsuru.RTCEvent.Off', eventName)
|
||||
}
|
||||
|
||||
public send(eventName: string, data: any) {
|
||||
this.connections.forEach((item) =>
|
||||
item.send({
|
||||
Key: eventName,
|
||||
Data: data
|
||||
})
|
||||
)
|
||||
this.connections.forEach((item) => {
|
||||
if (item && item.open) {
|
||||
item.send({
|
||||
Key: eventName,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected connectRTC() {
|
||||
//console.log('[Components-Event] 正在连接到 PeerJS 服务器...')
|
||||
this.peer = new Peer({
|
||||
host: 'peer.suki.club',
|
||||
port: 443,
|
||||
key: 'vtsuru',
|
||||
secure: true,
|
||||
config: {
|
||||
iceServers: [
|
||||
{ url: 'stun:turn.suki.club' },
|
||||
{
|
||||
url: 'turn:turn.suki.club',
|
||||
username: this.user,
|
||||
credential: this.pass
|
||||
}
|
||||
]
|
||||
}
|
||||
//debug: 3
|
||||
})
|
||||
|
||||
this.peer?.on('open', async (id) => {
|
||||
console.log('[Components-Event] 已连接到 PeerJS 服务器: ' + id)
|
||||
this.vhub?.send('SetRTCToken', id, this.type == 'master')
|
||||
})
|
||||
this.peer?.on('error', (err) => {
|
||||
console.error(err)
|
||||
})
|
||||
this.peer?.on('close', () => {
|
||||
console.log('[Components-Event] PeerJS 连接已关闭')
|
||||
})
|
||||
this.peer?.on('disconnected', () => {
|
||||
console.log('[Components-Event] PeerJS 连接已断开')
|
||||
this.peer?.reconnect()
|
||||
})
|
||||
}
|
||||
public processData(conn: DataConnection, data: RTCData) {
|
||||
//console.log(data)
|
||||
if (data.Key == 'Heartbeat') {
|
||||
// 心跳
|
||||
return
|
||||
} else if (data.Key == 'VTsuru.RTCEvent.On') {
|
||||
// 添加事件
|
||||
this.handledEvents[conn.peer].push(data.Data)
|
||||
} else if (data.Key == 'VTsuru.RTCEvent.Off') {
|
||||
// 移除事件
|
||||
const i = this.handledEvents[conn.peer].indexOf(data.Data)
|
||||
if (i > -1) {
|
||||
this.handledEvents[conn.peer].splice(i, 1)
|
||||
}
|
||||
} else if (this.events[data.Key.toLowerCase()]) {
|
||||
this.events[data.Key].forEach((item) => item(data.Data))
|
||||
}
|
||||
}
|
||||
public async getAllRTC() {
|
||||
return (
|
||||
(await this.vhub.invoke<ComponentsEventHubModel[]>('GetOnlineRTC')) || []
|
||||
)
|
||||
}
|
||||
protected onConnectionClose(id: string) {
|
||||
this.connections = this.connections.filter((item) => item.peer != id)
|
||||
this.connections = this.connections.filter(item => item && item.peer != id)
|
||||
delete this.handledEvents[id]
|
||||
|
||||
console.log(
|
||||
`[Components-Event] <${this.connections.length}> ${this.type == 'master' ? 'Slave' : 'Master'} 下线: ` +
|
||||
id
|
||||
`[Components-Event] <${this.connections.length}> ${this.type == 'master' ? 'Slave' : 'Master'} 下线: ${
|
||||
id}`,
|
||||
)
|
||||
}
|
||||
|
||||
public async Init() {
|
||||
if (!this.isInited) {
|
||||
this.isInited = true
|
||||
await this.vhub.on('RTCOffline', (id: string) =>
|
||||
await this.vhub.on('RTCOffline', (...args: unknown[]) => {
|
||||
const id = args[0] as string
|
||||
this.onConnectionClose(id)
|
||||
)
|
||||
})
|
||||
this.connectRTC()
|
||||
}
|
||||
return this
|
||||
@@ -146,21 +95,28 @@ export class SlaveRTCClient extends BaseRTCClient {
|
||||
constructor(user: string, pass: string) {
|
||||
super(user, pass)
|
||||
}
|
||||
|
||||
type: 'slave' = 'slave' as const
|
||||
|
||||
protected async getAllRTC(): Promise<ComponentsEventHubModel[]> {
|
||||
return await this.vhub.invoke<ComponentsEventHubModel[]>('GetAllRTC') || []
|
||||
}
|
||||
|
||||
public async connectToAllMaster() {
|
||||
const masters = (await this.getAllRTC()).filter(
|
||||
(item) =>
|
||||
item.IsMaster &&
|
||||
item.Token != this.peer!.id &&
|
||||
!this.connections.some((conn) => conn.peer == item.Token)
|
||||
(item: ComponentsEventHubModel) =>
|
||||
item.IsMaster
|
||||
&& item.Token != this.peer!.id
|
||||
&& !this.connections.some(conn => conn.peer == item.Token),
|
||||
)
|
||||
masters.forEach((item) => {
|
||||
masters.forEach((item: ComponentsEventHubModel) => {
|
||||
this.connectToMaster(item.Token)
|
||||
//console.log('[Components-Event] 正在连接到现有 Master: ' + item.Token)
|
||||
// console.log('[Components-Event] 正在连接到现有 Master: ' + item.Token)
|
||||
})
|
||||
}
|
||||
|
||||
public connectToMaster(id: string) {
|
||||
if (this.connections.some((conn) => conn.peer == id)) return
|
||||
if (this.connections.some(conn => conn.peer == id)) return
|
||||
const c = this.peer?.connect(id)
|
||||
c?.on('open', () => {
|
||||
this.connections.push(c)
|
||||
@@ -168,17 +124,53 @@ export class SlaveRTCClient extends BaseRTCClient {
|
||||
this.handledEvents[id] = []
|
||||
|
||||
console.log(
|
||||
`[Components-Event] <${this.connections.length}> ==> Master 连接已建立: ` +
|
||||
id
|
||||
`[Components-Event] <${this.connections.length}> ==> Master 连接已建立: ${
|
||||
id}`,
|
||||
)
|
||||
})
|
||||
c?.on('error', (err) => console.error(err))
|
||||
c?.on('data', (data) => this.processData(c, data as RTCData))
|
||||
c?.on('error', err => console.error(err))
|
||||
c?.on('data', data => this.processData(c, data as RTCData))
|
||||
c?.on('close', () => this.onConnectionClose(c.peer))
|
||||
}
|
||||
|
||||
protected connectRTC(): void {
|
||||
this.peer = new Peer()
|
||||
this.peer.on('open', (id) => {
|
||||
console.log('[Components-Event] Slave Peer ID:', id)
|
||||
this.vhub.send('RegisterRTC', false, id)
|
||||
})
|
||||
this.peer.on('error', err => console.error('[Components-Event] Slave Peer Error:', err))
|
||||
}
|
||||
|
||||
protected processData(conn: DataConnection, data: RTCData): void {
|
||||
if (data.Key === 'VTsuru.RTCEvent.On') {
|
||||
if (!this.handledEvents[conn.peer]) {
|
||||
this.handledEvents[conn.peer] = []
|
||||
}
|
||||
if (!this.handledEvents[conn.peer].includes(data.Data)) {
|
||||
this.handledEvents[conn.peer].push(data.Data)
|
||||
}
|
||||
} else if (data.Key === 'VTsuru.RTCEvent.Off') {
|
||||
if (this.handledEvents[conn.peer]) {
|
||||
const index = this.handledEvents[conn.peer].indexOf(data.Data)
|
||||
if (index > -1) {
|
||||
this.handledEvents[conn.peer].splice(index, 1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const eventName = data.Key.toLowerCase()
|
||||
if (this.events[eventName]) {
|
||||
this.events[eventName].forEach(listener => listener(data.Data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Init() {
|
||||
await super.Init()
|
||||
this.vhub?.on('MasterOnline', (data: string) => this.connectToMaster(data))
|
||||
this.vhub?.on('MasterOnline', (...args: unknown[]) => {
|
||||
const data = args[0] as string
|
||||
this.connectToMaster(data)
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.connectToAllMaster()
|
||||
}, 500)
|
||||
@@ -193,26 +185,55 @@ export class MasterRTCClient extends BaseRTCClient {
|
||||
constructor(user: string, pass: string) {
|
||||
super(user, pass)
|
||||
}
|
||||
|
||||
type: 'master' = 'master' as const
|
||||
|
||||
public connectRTC() {
|
||||
super.connectRTC()
|
||||
protected connectRTC(): void {
|
||||
this.peer?.on('connection', (conn) => {
|
||||
conn.on('open', () => {
|
||||
this.connections.push(conn)
|
||||
this.handledEvents[conn.peer] = []
|
||||
console.log(
|
||||
`[Components-Event] <${this.connections.length}> Slave 上线: ` +
|
||||
conn.peer
|
||||
`[Components-Event] <${this.connections.length}> Slave 上线: ${
|
||||
conn.peer}`,
|
||||
)
|
||||
})
|
||||
conn.on('data', (d) => this.processData(conn, d as RTCData))
|
||||
conn.on('error', (err) => console.error(err))
|
||||
conn.on('data', d => this.processData(conn, d as RTCData))
|
||||
conn.on('error', err => console.error(err))
|
||||
conn.on('close', () => this.onConnectionClose(conn.peer))
|
||||
})
|
||||
|
||||
this.peer = new Peer()
|
||||
this.peer.on('open', (id) => {
|
||||
console.log('[Components-Event] Master Peer ID:', id)
|
||||
this.vhub.send('RegisterRTC', true, id)
|
||||
})
|
||||
this.peer.on('error', err => console.error('[Components-Event] Master Peer Error:', err))
|
||||
}
|
||||
|
||||
public Init() {
|
||||
protected processData(conn: DataConnection, data: RTCData): void {
|
||||
if (data.Key === 'VTsuru.RTCEvent.On') {
|
||||
if (!this.handledEvents[conn.peer]) {
|
||||
this.handledEvents[conn.peer] = []
|
||||
}
|
||||
if (!this.handledEvents[conn.peer].includes(data.Data)) {
|
||||
this.handledEvents[conn.peer].push(data.Data)
|
||||
}
|
||||
} else if (data.Key === 'VTsuru.RTCEvent.Off') {
|
||||
if (this.handledEvents[conn.peer]) {
|
||||
const index = this.handledEvents[conn.peer].indexOf(data.Data)
|
||||
if (index > -1) {
|
||||
this.handledEvents[conn.peer].splice(index, 1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.handledEvents[conn.peer]?.includes(data.Key.toLowerCase())) {
|
||||
conn.send(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Init() {
|
||||
return super.Init()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
import type {
|
||||
ScheduleWeekInfo,
|
||||
Setting_LiveRequest,
|
||||
SongRequestInfo,
|
||||
SongsInfo,
|
||||
UserInfo
|
||||
UserInfo,
|
||||
} from '@/api/api-models'
|
||||
|
||||
export interface SongListConfigType {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import UpdateNoteContainer from "@/components/UpdateNoteContainer.vue";
|
||||
import { NButton, NImage } from "naive-ui";
|
||||
import { VNode } from "vue";
|
||||
import type { VNode } from 'vue'
|
||||
import { NButton, NImage } from 'naive-ui'
|
||||
import UpdateNoteContainer from '@/components/UpdateNoteContainer.vue'
|
||||
|
||||
export const updateNotes: updateNoteType[] = [
|
||||
{
|
||||
@@ -18,9 +18,9 @@ export const updateNotes: updateNoteType[] = [
|
||||
'现在支持为礼物附加key, 可以在兑换礼物之后自动选择一个附加到礼物内容中',
|
||||
],
|
||||
[
|
||||
'礼物页面样式优化'
|
||||
]
|
||||
]
|
||||
'礼物页面样式优化',
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'new',
|
||||
@@ -33,10 +33,10 @@ export const updateNotes: updateNoteType[] = [
|
||||
[
|
||||
'签到功能增加仅签到功能, 可以只签到不给予积分, 修改设置项',
|
||||
() => h(NImage, { src: 'https://files.vtsuru.suki.club/updatelog/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE%202025-05-01%20080506.png', width: 300 }),
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ver: 6,
|
||||
@@ -53,9 +53,13 @@ export const updateNotes: updateNoteType[] = [
|
||||
[
|
||||
'客户端安装方式:',
|
||||
() => h(NButton, {
|
||||
text: true, tag: 'a', href: 'https://www.wolai.com/carN6qvUm3FErze9Xo53ii', target: '_blank', type: 'info'
|
||||
text: true,
|
||||
tag: 'a',
|
||||
href: 'https://www.wolai.com/carN6qvUm3FErze9Xo53ii',
|
||||
target: '_blank',
|
||||
type: 'info',
|
||||
}, () => '查看介绍'),
|
||||
]
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -64,10 +68,10 @@ export const updateNotes: updateNoteType[] = [
|
||||
content: [
|
||||
[
|
||||
'读弹幕现在支持进入直播间消息',
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ver: 5,
|
||||
@@ -83,11 +87,11 @@ export const updateNotes: updateNoteType[] = [
|
||||
],
|
||||
[
|
||||
'大部分功能都和 blivechat 一致, 不过目前还无法提供本地文件访问, 部分css中需要使用图片等本地资源样式的需要等 EventFetcher 更新相关功能后才能使用\r\n',
|
||||
'配置上传之后会自动同步到obs中'
|
||||
]
|
||||
'配置上传之后会自动同步到obs中',
|
||||
],
|
||||
],
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
ver: 4,
|
||||
@@ -106,12 +110,16 @@ export const updateNotes: updateNoteType[] = [
|
||||
'数据持久化存储,各类操作配置和运行状态不会丢失\r\n\r\n',
|
||||
'发送弹幕和私信需要客户端扫码登录, 客户端安装方式:',
|
||||
() => h(NButton, {
|
||||
text: true, tag: 'a', href: 'https://www.wolai.com/carN6qvUm3FErze9Xo53ii', target: '_blank', type: 'info'
|
||||
text: true,
|
||||
tag: 'a',
|
||||
href: 'https://www.wolai.com/carN6qvUm3FErze9Xo53ii',
|
||||
target: '_blank',
|
||||
type: 'info',
|
||||
}, () => '查看介绍'),
|
||||
]
|
||||
],
|
||||
],
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
ver: 3,
|
||||
@@ -125,10 +133,10 @@ export const updateNotes: updateNoteType[] = [
|
||||
'Tauri 客户端新增弹幕机功能, 可以在自己电脑上显示弹幕礼物等. ',
|
||||
'客户端需更新至0.1.2版本, 重启客户端后会自动更新',
|
||||
() => h(NImage, { src: 'https://pan.suki.club/d/vtsuru/imgur/81d76a89-96b8-44e9-be79-6caaa5741f64.png', width: 200 }),
|
||||
]
|
||||
],
|
||||
],
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
ver: 2,
|
||||
@@ -142,28 +150,40 @@ export const updateNotes: updateNoteType[] = [
|
||||
[
|
||||
'安装方式: ',
|
||||
() => h(NButton, {
|
||||
text: true, tag: 'a', href: 'https://www.wolai.com/carN6qvUm3FErze9Xo53ii', target: '_blank', type: 'info'
|
||||
text: true,
|
||||
tag: 'a',
|
||||
href: 'https://www.wolai.com/carN6qvUm3FErze9Xo53ii',
|
||||
target: '_blank',
|
||||
type: 'info',
|
||||
}, () => '查看介绍'),
|
||||
],
|
||||
[
|
||||
'当前可能存在一些问题, 可以加入秋秋群 873260337 进行反馈, 有功能需求也可以提出'
|
||||
'当前可能存在一些问题, 可以加入秋秋群 873260337 进行反馈, 有功能需求也可以提出',
|
||||
],
|
||||
[],
|
||||
[
|
||||
'源码: ',
|
||||
() => h(NButton, {
|
||||
text: true, tag: 'a', href: 'https://github.com/Megghy/vtsuru-fetvher-client', target: '_blank', type: 'info'
|
||||
text: true,
|
||||
tag: 'a',
|
||||
href: 'https://github.com/Megghy/vtsuru-fetvher-client',
|
||||
target: '_blank',
|
||||
type: 'info',
|
||||
}, () => ' 客户端 Repo '),
|
||||
' | ',
|
||||
() => h(NButton, {
|
||||
text: true, tag: 'a', href: 'https://github.com/Megghy/vtsuru.live/tree/master/src/client', target: '_blank', type: 'info'
|
||||
text: true,
|
||||
tag: 'a',
|
||||
href: 'https://github.com/Megghy/vtsuru.live/tree/master/src/client',
|
||||
target: '_blank',
|
||||
type: 'info',
|
||||
}, () => ' UI/逻辑 '),
|
||||
],
|
||||
[
|
||||
() => h(NImage, { src: 'https://pan.suki.club/d/vtsuru/imgur/01295402D7FBBF192FE5608179A4A7A6.png', width: 200 }),
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -177,16 +197,16 @@ export const updateNotes: updateNoteType[] = [
|
||||
[
|
||||
'新增一个歌单样式: 列表',
|
||||
() => h(NImage, { src: 'https://pan.suki.club/d/vtsuru/imgur/QQ20250408-134631.png', width: 300, height: 200 }),
|
||||
]
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
export const currentUpdateNoteVer = updateNotes.sort((a, b) => b.ver - a.ver)[0].ver;
|
||||
export const currentUpdateNote = updateNotes.sort((a, b) => b.ver - a.ver)[0].items;
|
||||
export const savedUpdateNoteVer = useStorage('UpdateNoteVer', 0);
|
||||
export const currentUpdateNoteVer = updateNotes.sort((a, b) => b.ver - a.ver)[0].ver
|
||||
export const currentUpdateNote = updateNotes.sort((a, b) => b.ver - a.ver)[0].items
|
||||
export const savedUpdateNoteVer = useStorage('UpdateNoteVer', 0)
|
||||
|
||||
export function checkUpdateNote() {
|
||||
if (savedUpdateNoteVer.value < currentUpdateNoteVer) {
|
||||
@@ -200,24 +220,24 @@ export function checkUpdateNote() {
|
||||
negativeText: '确定',
|
||||
positiveText: '下次更新前不再提示',
|
||||
onPositiveClick: () => {
|
||||
savedUpdateNoteVer.value = currentUpdateNoteVer;
|
||||
savedUpdateNoteVer.value = currentUpdateNoteVer
|
||||
},
|
||||
onClose: () => {
|
||||
savedUpdateNoteVer.value = currentUpdateNoteVer;
|
||||
}
|
||||
});
|
||||
savedUpdateNoteVer.value = currentUpdateNoteVer
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export type updateType = 'fix' | 'new' | 'optimize' | 'other';
|
||||
export type updateNoteType = {
|
||||
ver: number;
|
||||
date: string;
|
||||
items: updateNoteItemType[];
|
||||
};
|
||||
export type updateNoteItemType = {
|
||||
title?: string | (() => VNode);
|
||||
type: updateType;
|
||||
content: updateNoteItemContentType[];
|
||||
};
|
||||
export type updateNoteItemContentType = (() => VNode) | string | updateNoteItemContentType[];
|
||||
export type updateType = 'fix' | 'new' | 'optimize' | 'other'
|
||||
export interface updateNoteType {
|
||||
ver: number
|
||||
date: string
|
||||
items: updateNoteItemType[]
|
||||
}
|
||||
export interface updateNoteItemType {
|
||||
title?: string | (() => VNode)
|
||||
type: updateType
|
||||
content: updateNoteItemContentType[]
|
||||
}
|
||||
export type updateNoteItemContentType = (() => VNode) | string | updateNoteItemContentType[]
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import { UploadFileResponse } from '@/api/api-models';
|
||||
import { SelectOption } from 'naive-ui';
|
||||
import { VNode, h } from 'vue'; // 导入 Vue 的 VNode 类型和 h 函数(用于示例)
|
||||
import type { SelectOption } from 'naive-ui'
|
||||
import type { VNode } from 'vue'
|
||||
// 导入 Vue 的 VNode 类型和 h 函数(用于示例)
|
||||
import type { UploadFileResponse } from '@/api/api-models'
|
||||
|
||||
// --- 基础和通用类型 ---
|
||||
|
||||
interface TemplateConfigBase {
|
||||
name: string | VNode; // 名称,可以是字符串或 VNode
|
||||
key: string; // 唯一标识符,用于数据对象的键
|
||||
name: string | VNode // 名称,可以是字符串或 VNode
|
||||
key: string // 唯一标识符,用于数据对象的键
|
||||
/**
|
||||
* 可选的默认值。
|
||||
* 其具体类型在更具体的项类型中被细化。
|
||||
* TemplateConfigRenderItem 会使用其是否存在来进行类型推断。
|
||||
*/
|
||||
default?: any;
|
||||
default?: any
|
||||
/**
|
||||
* 可选的条件显示属性
|
||||
* 根据一个函数决定当前配置项是否可见
|
||||
* @param config 整个配置对象
|
||||
* @returns 是否显示此配置项
|
||||
*/
|
||||
visibleWhen?: (config: any) => boolean;
|
||||
visibleWhen?: (config: any) => boolean
|
||||
}
|
||||
|
||||
// 大多数项类型共享的通用属性 (暂时排除 RenderItem)
|
||||
@@ -32,43 +33,43 @@ type CommonProps<T = unknown> = TemplateConfigBase & {
|
||||
// 通常会是 'unknown' 类型。如果在实现中需要访问
|
||||
// 完整配置对象的特定属性,你可能需要进行类型断言
|
||||
// (例如:config as MyConfigType)。
|
||||
};
|
||||
}
|
||||
|
||||
// 数据访问器类型
|
||||
type DataAccessor<T, V> = {
|
||||
get: (config: T) => V;
|
||||
set: (config: T, value: V) => void;
|
||||
};
|
||||
interface DataAccessor<T, V> {
|
||||
get: (config: T) => V
|
||||
set: (config: T, value: V) => void
|
||||
}
|
||||
|
||||
// 添加辅助函数,用于从配置对象中安全获取数据
|
||||
export function getConfigValue<T, K extends keyof T>(config: T, key: K): T[K] {
|
||||
return config[key];
|
||||
return config[key]
|
||||
}
|
||||
|
||||
// 添加辅助函数,用于设置配置对象的数据
|
||||
export function setConfigValue<T, K extends keyof T>(config: T, key: K, value: T[K]): void {
|
||||
config[key] = value;
|
||||
config[key] = value
|
||||
}
|
||||
|
||||
// 创建一个默认的RGBA颜色对象
|
||||
export function createDefaultRGBA(r = 0, g = 0, b = 0, a = 1): RGBAColor {
|
||||
return { r, g, b, a };
|
||||
return { r, g, b, a }
|
||||
}
|
||||
|
||||
// 添加类型守卫函数,用于检查上传文件信息
|
||||
export function isUploadFileInfo(obj: any): obj is UploadFileResponse {
|
||||
return (
|
||||
obj &&
|
||||
typeof obj === 'object' &&
|
||||
'id' in obj &&
|
||||
typeof obj.id === 'number' &&
|
||||
'path' in obj &&
|
||||
typeof obj.path === 'string' &&
|
||||
'name' in obj &&
|
||||
typeof obj.name === 'string' &&
|
||||
'hash' in obj &&
|
||||
typeof obj.hash === 'string'
|
||||
);
|
||||
obj
|
||||
&& typeof obj === 'object'
|
||||
&& 'id' in obj
|
||||
&& typeof obj.id === 'number'
|
||||
&& 'path' in obj
|
||||
&& typeof obj.path === 'string'
|
||||
&& 'name' in obj
|
||||
&& typeof obj.name === 'string'
|
||||
&& 'hash' in obj
|
||||
&& typeof obj.hash === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,102 +78,101 @@ export function isUploadFileInfo(obj: any): obj is UploadFileResponse {
|
||||
* @template V - 此特定配置项的值的类型。
|
||||
*/
|
||||
export type TemplateConfigItemWithType<T = unknown, V = unknown> = CommonProps<T> & {
|
||||
type: string; // 类型判别属性
|
||||
data?: DataAccessor<T, V>; // 可选的数据访问器
|
||||
type: string // 类型判别属性
|
||||
data?: DataAccessor<T, V> // 可选的数据访问器
|
||||
/**
|
||||
* @description 可选的上传/更新回调函数。
|
||||
* @param data - 当前项更新的数据,类型为 V。
|
||||
* @param config - 整个配置数据对象,类型为 T (通常是 unknown)。
|
||||
*/
|
||||
onUploaded?: (data: V, config: T) => void;
|
||||
onUploaded?: (data: V, config: T) => void
|
||||
/**
|
||||
* 可选的默认值,约束为类型 V。
|
||||
* 覆盖了 TemplateConfigBase 中的 'any' 类型。
|
||||
*/
|
||||
default?: V;
|
||||
};
|
||||
default?: V
|
||||
}
|
||||
|
||||
// --- Widen 工具类型 (保持不变) ---
|
||||
// 递归地将类型拓宽为其基础类型。
|
||||
type Widen<T> =
|
||||
T extends string ? string :
|
||||
T extends number ? number :
|
||||
T extends boolean ? boolean :
|
||||
T extends bigint ? bigint :
|
||||
T extends symbol ? symbol :
|
||||
T extends undefined ? undefined :
|
||||
T extends null ? null :
|
||||
T extends Function ? T :
|
||||
T extends Date ? Date :
|
||||
T extends readonly (infer U)[] ? Widen<U>[] :
|
||||
T extends object ? { -readonly [K in keyof T]: Widen<T[K]> } :
|
||||
T;
|
||||
type Widen<T>
|
||||
= T extends string ? string
|
||||
: T extends number ? number
|
||||
: T extends boolean ? boolean
|
||||
: T extends bigint ? bigint
|
||||
: T extends symbol ? symbol
|
||||
: T extends undefined ? undefined
|
||||
: T extends null ? null
|
||||
: T extends Function ? T
|
||||
: T extends Date ? Date
|
||||
: T extends readonly (infer U)[] ? Widen<U>[]
|
||||
: T extends object ? { -readonly [K in keyof T]: Widen<T[K]> }
|
||||
: T
|
||||
|
||||
// --- 具体配置项类型定义 ---
|
||||
// T 在所有具体类型中默认为 unknown
|
||||
|
||||
|
||||
export type TemplateConfigSelectItem<T = unknown> = TemplateConfigItemWithType<T, string> & {
|
||||
type: 'select';
|
||||
options: SelectOption[] | ((config: T) => SelectOption[]); // 选项列表或者返回选项列表的函数
|
||||
placeholder?: string; // 可选的占位符
|
||||
clearable?: boolean; // 是否可清空
|
||||
};
|
||||
type: 'select'
|
||||
options: SelectOption[] | ((config: T) => SelectOption[]) // 选项列表或者返回选项列表的函数
|
||||
placeholder?: string // 可选的占位符
|
||||
clearable?: boolean // 是否可清空
|
||||
}
|
||||
|
||||
export type TemplateConfigStringItem<T = unknown> = TemplateConfigItemWithType<T, string> & {
|
||||
type: 'string';
|
||||
placeholder?: string; // 可选的占位符
|
||||
inputType?: 'text' | 'password' | 'textarea'; // 可选的输入类型
|
||||
};
|
||||
type: 'string'
|
||||
placeholder?: string // 可选的占位符
|
||||
inputType?: 'text' | 'password' | 'textarea' // 可选的输入类型
|
||||
}
|
||||
|
||||
export type TemplateConfigStringArrayItem<T = unknown> = TemplateConfigItemWithType<T, string[]> & {
|
||||
type: 'stringArray';
|
||||
};
|
||||
type: 'stringArray'
|
||||
}
|
||||
|
||||
export type TemplateConfigNumberItem<T = unknown> = TemplateConfigItemWithType<T, number> & {
|
||||
type: 'number';
|
||||
min?: number;
|
||||
max?: number;
|
||||
type: 'number'
|
||||
min?: number
|
||||
max?: number
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
// RGBA颜色对象接口
|
||||
export interface RGBAColor {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
r: number
|
||||
g: number
|
||||
b: number
|
||||
a: number
|
||||
}
|
||||
|
||||
// 修改 TemplateConfigColorItem 以使用 RGBAColor 接口
|
||||
export type TemplateConfigColorItem<T = unknown> = TemplateConfigItemWithType<T, RGBAColor> & {
|
||||
type: 'color';
|
||||
showAlpha?: boolean; // 控制是否显示透明度调整
|
||||
};
|
||||
type: 'color'
|
||||
showAlpha?: boolean // 控制是否显示透明度调整
|
||||
}
|
||||
|
||||
export type TemplateConfigSliderNumberItem<T = unknown> = TemplateConfigItemWithType<T, number> & {
|
||||
type: 'sliderNumber';
|
||||
step?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
};
|
||||
type: 'sliderNumber'
|
||||
step?: number
|
||||
min?: number
|
||||
max?: number
|
||||
}
|
||||
|
||||
export type TemplateConfigNumberArrayItem<T = unknown> = TemplateConfigItemWithType<T, number[]> & {
|
||||
type: 'numberArray';
|
||||
};
|
||||
type: 'numberArray'
|
||||
}
|
||||
export type TemplateConfigBooleanItem<T = unknown> = TemplateConfigItemWithType<T, boolean> & {
|
||||
type: 'boolean';
|
||||
description?: string; // 可选的描述
|
||||
};
|
||||
type: 'boolean'
|
||||
description?: string // 可选的描述
|
||||
}
|
||||
|
||||
// 将文件类型统一为数组,不再根据fileLimit区分
|
||||
export type TemplateConfigFileItem<T = unknown> =
|
||||
TemplateConfigItemWithType<T, UploadFileResponse[]> & {
|
||||
type: 'file';
|
||||
fileLimit?: number; // 变为可选参数,仅用于UI限制,不影响类型
|
||||
fileType?: string[];
|
||||
onUploaded?: (data: UploadFileResponse[], config: T) => void;
|
||||
};
|
||||
export type TemplateConfigFileItem<T = unknown>
|
||||
= TemplateConfigItemWithType<T, UploadFileResponse[]> & {
|
||||
type: 'file'
|
||||
fileLimit?: number // 变为可选参数,仅用于UI限制,不影响类型
|
||||
fileType?: string[]
|
||||
onUploaded?: (data: UploadFileResponse[], config: T) => void
|
||||
}
|
||||
|
||||
// --- 新增:装饰性图片配置 ---
|
||||
|
||||
@@ -180,13 +180,13 @@ export type TemplateConfigFileItem<T = unknown> =
|
||||
* @description 单个装饰图片的属性接口
|
||||
*/
|
||||
export interface DecorativeImageProperties extends UploadFileResponse {
|
||||
x: number; // X 坐标 (%)
|
||||
y: number; // Y 坐标 (%)
|
||||
width: number; // 宽度 (%)
|
||||
x: number // X 坐标 (%)
|
||||
y: number // Y 坐标 (%)
|
||||
width: number // 宽度 (%)
|
||||
// height: number; // 高度通常由宽度和图片比例决定,或设为 auto
|
||||
rotation: number; // 旋转角度 (deg)
|
||||
opacity: number; // 透明度 (0-1)
|
||||
zIndex: number; // 层叠顺序
|
||||
rotation: number // 旋转角度 (deg)
|
||||
opacity: number // 透明度 (0-1)
|
||||
zIndex: number // 层叠顺序
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,22 +195,22 @@ export interface DecorativeImageProperties extends UploadFileResponse {
|
||||
* @template T - 完整配置对象的类型 (默认为 unknown)。
|
||||
*/
|
||||
export interface TemplateConfigDecorativeImagesItem<T = unknown> extends TemplateConfigBase {
|
||||
type: 'decorativeImages'; // 新类型标识符
|
||||
default?: DecorativeImageProperties[]; // 默认值是图片属性数组
|
||||
type: 'decorativeImages' // 新类型标识符
|
||||
default?: DecorativeImageProperties[] // 默认值是图片属性数组
|
||||
|
||||
/**
|
||||
* @description 渲染此项的自定义 VNode (配置 UI)。
|
||||
* @param config 整个配置对象 (类型为 T, 默认为 unknown)。
|
||||
* @returns 表示配置 UI 的 VNode。
|
||||
*/
|
||||
render?(config: T): VNode;
|
||||
render?: (config: T) => VNode
|
||||
|
||||
/**
|
||||
* @description 当装饰图片数组更新时调用的回调。
|
||||
* @param data 更新后的 DecorativeImageProperties 数组。
|
||||
* @param config 整个配置对象。
|
||||
*/
|
||||
onUploaded?(data: DecorativeImageProperties[], config: T): void; // data 类型是数组
|
||||
onUploaded?: (data: DecorativeImageProperties[], config: T) => void // data 类型是数组
|
||||
|
||||
// 继承 TemplateConfigBase 的 default?: any
|
||||
}
|
||||
@@ -220,7 +220,7 @@ export interface TemplateConfigDecorativeImagesItem<T = unknown> extends Templat
|
||||
* @template T - 完整配置对象的类型 (默认为 unknown)。
|
||||
*/
|
||||
export interface TemplateConfigRenderItem<T = unknown> extends TemplateConfigBase { // 继承基础接口以获取 key, name, default 检查
|
||||
type: 'render';
|
||||
type: 'render'
|
||||
|
||||
/**
|
||||
* @description 渲染此项的自定义 VNode。
|
||||
@@ -236,7 +236,7 @@ export interface TemplateConfigRenderItem<T = unknown> extends TemplateConfigBas
|
||||
* 作为第二个参数传递。如果不存在 `default`,则传递 `undefined` 或 `null`。
|
||||
* 示例: `item.render(config, item.default)`
|
||||
*/
|
||||
render(this: this, config: T): VNode;
|
||||
render: (this: this, config: T) => VNode
|
||||
|
||||
/**
|
||||
* @description 可选的回调函数,当自定义渲染的组件发出更新信号时调用。
|
||||
@@ -245,7 +245,7 @@ export interface TemplateConfigRenderItem<T = unknown> extends TemplateConfigBas
|
||||
* @param config 整个配置对象 (类型为 T, 默认为 unknown)。
|
||||
* 在实现内部可能需要类型断言 (例如 `config as MyConfig`)。
|
||||
*/
|
||||
onUploaded?(this: this, data: this extends { default: infer D; } ? Widen<D> : unknown, config: T): void;
|
||||
onUploaded?: (this: this, data: this extends { default: infer D } ? Widen<D> : unknown, config: T) => void
|
||||
|
||||
// 继承自 TemplateConfigBase 的 'default?: any',这对于
|
||||
// 'this extends { default: infer D }' 类型检查能正确工作至关重要。
|
||||
@@ -257,51 +257,50 @@ export interface TemplateConfigRenderItem<T = unknown> extends TemplateConfigBas
|
||||
* @description 所有可能的配置项定义类型的联合类型。
|
||||
* 使用 `<any>` 作为完整配置类型 T 的占位符。
|
||||
*/
|
||||
export type ConfigItemDefinition =
|
||||
| TemplateConfigStringItem<any>
|
||||
| TemplateConfigNumberItem<any>
|
||||
| TemplateConfigStringArrayItem<any>
|
||||
| TemplateConfigNumberArrayItem<any>
|
||||
| TemplateConfigFileItem<any>
|
||||
| TemplateConfigRenderItem<any>
|
||||
| TemplateConfigDecorativeImagesItem<any>
|
||||
| TemplateConfigSliderNumberItem<any>
|
||||
| TemplateConfigBooleanItem<any>
|
||||
| TemplateConfigColorItem<any>
|
||||
| TemplateConfigSelectItem<any>;
|
||||
export type ConfigItemDefinition
|
||||
= | TemplateConfigStringItem<any>
|
||||
| TemplateConfigNumberItem<any>
|
||||
| TemplateConfigStringArrayItem<any>
|
||||
| TemplateConfigNumberArrayItem<any>
|
||||
| TemplateConfigFileItem<any>
|
||||
| TemplateConfigRenderItem<any>
|
||||
| TemplateConfigDecorativeImagesItem<any>
|
||||
| TemplateConfigSliderNumberItem<any>
|
||||
| TemplateConfigBooleanItem<any>
|
||||
| TemplateConfigColorItem<any>
|
||||
| TemplateConfigSelectItem<any>
|
||||
|
||||
/**
|
||||
* @description 从只读的配置项数组中提取最终的数据结构类型。
|
||||
* @template Items - 通过 `defineItems([...])` 推断出的只读元组类型。
|
||||
*/
|
||||
export type ExtractConfigData<
|
||||
Items extends readonly ConfigItemDefinition[]
|
||||
Items extends readonly ConfigItemDefinition[],
|
||||
> = {
|
||||
// 遍历联合类型 Items[number] 中所有项的 'key' 属性
|
||||
[K in Extract<Items[number], { key: string; }>['key']]:
|
||||
// 找到与当前键 K 匹配的具体项定义
|
||||
Extract<Items[number], { key: K; }> extends infer ItemWithKeyK
|
||||
// 遍历联合类型 Items[number] 中所有项的 'key' 属性
|
||||
[K in Extract<Items[number], { key: string }>['key']]:
|
||||
// 找到与当前键 K 匹配的具体项定义
|
||||
Extract<Items[number], { key: K }> extends infer ItemWithKeyK
|
||||
// 检查匹配到的项是否有 'default' 属性
|
||||
? ItemWithKeyK extends { default: infer DefaultType; }
|
||||
? ItemWithKeyK extends { default: infer DefaultType }
|
||||
// 如果有,使用 default 值的 Widen 处理后的类型
|
||||
? Widen<DefaultType>
|
||||
? Widen<DefaultType>
|
||||
// 如果没有 default,则根据 'type' 属性确定类型
|
||||
: ItemWithKeyK extends { type: 'string' | 'select'; } ? string
|
||||
: ItemWithKeyK extends { type: 'stringArray'; } ? string[]
|
||||
: ItemWithKeyK extends { type: 'number' | 'sliderNumber' ; } ? number
|
||||
: ItemWithKeyK extends { type: 'numberArray'; } ? number[]
|
||||
// 文件类型统一处理为数组
|
||||
: ItemWithKeyK extends { type: 'file'; } ? UploadFileResponse[]
|
||||
: ItemWithKeyK extends { type: 'boolean'; } ? boolean
|
||||
: ItemWithKeyK extends { type: 'color'; } ? RGBAColor
|
||||
: ItemWithKeyK extends { type: 'decorativeImages'; } ? DecorativeImageProperties[]
|
||||
// *** 优化应用:无 default 的 render 类型回退到 'unknown' ***
|
||||
: ItemWithKeyK extends { type: 'render'; } ? unknown
|
||||
// 其他意外情况的回退类型
|
||||
: unknown
|
||||
: ItemWithKeyK extends { type: 'string' | 'select' } ? string
|
||||
: ItemWithKeyK extends { type: 'stringArray' } ? string[]
|
||||
: ItemWithKeyK extends { type: 'number' | 'sliderNumber' } ? number
|
||||
: ItemWithKeyK extends { type: 'numberArray' } ? number[]
|
||||
// 文件类型统一处理为数组
|
||||
: ItemWithKeyK extends { type: 'file' } ? UploadFileResponse[]
|
||||
: ItemWithKeyK extends { type: 'boolean' } ? boolean
|
||||
: ItemWithKeyK extends { type: 'color' } ? RGBAColor
|
||||
: ItemWithKeyK extends { type: 'decorativeImages' } ? DecorativeImageProperties[]
|
||||
// *** 优化应用:无 default 的 render 类型回退到 'unknown' ***
|
||||
: ItemWithKeyK extends { type: 'render' } ? unknown
|
||||
// 其他意外情况的回退类型
|
||||
: unknown
|
||||
: never // 如果 K 正确派生,则不应发生
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// --- Key 约束辅助类型 ---
|
||||
/**
|
||||
@@ -310,20 +309,20 @@ export type ExtractConfigData<
|
||||
* - 其他type的key禁止以'File'结尾。
|
||||
* @template Item - 待检查的配置项定义类型。
|
||||
*/
|
||||
type ConstrainedKeyItem<Item extends ConfigItemDefinition> =
|
||||
// 所有包含UploadFileInfo的类型必须以'File'结尾
|
||||
Item extends { type: 'file' } | { type: 'decorativeImages' }
|
||||
// 强制key以'File'结尾
|
||||
? Omit<Item, 'key'> & { key: `${string}File` }
|
||||
: Item extends { key: infer K extends string }
|
||||
// 对于其它类型,检查key是否以'File'结尾
|
||||
? K extends `${string}File`
|
||||
// 如果以'File'结尾,则类型无效(never),导致TypeScript报错
|
||||
? never
|
||||
// 如果不以'File'结尾,则类型有效
|
||||
: Item
|
||||
// 如果Item没有key属性(理论上不应发生),保持原样
|
||||
: Item;
|
||||
type ConstrainedKeyItem<Item extends ConfigItemDefinition>
|
||||
// 所有包含UploadFileInfo的类型必须以'File'结尾
|
||||
= Item extends { type: 'file' } | { type: 'decorativeImages' }
|
||||
// 强制key以'File'结尾
|
||||
? Omit<Item, 'key'> & { key: `${string}File` }
|
||||
: Item extends { key: infer K extends string }
|
||||
// 对于其它类型,检查key是否以'File'结尾
|
||||
? K extends `${string}File`
|
||||
// 如果以'File'结尾,则类型无效(never),导致TypeScript报错
|
||||
? never
|
||||
// 如果不以'File'结尾,则类型有效
|
||||
: Item
|
||||
// 如果Item没有key属性(理论上不应发生),保持原样
|
||||
: Item
|
||||
|
||||
/**
|
||||
* @description 定义并验证配置项数组。
|
||||
@@ -334,28 +333,28 @@ type ConstrainedKeyItem<Item extends ConfigItemDefinition> =
|
||||
* @returns 类型被保留且经过验证的同一个只读数组。
|
||||
*/
|
||||
export function defineTemplateConfig<
|
||||
// 应用 ConstrainedKeyItem 约束到数组的每个元素
|
||||
const Items extends readonly ConstrainedKeyItem<ConfigItemDefinition>[]
|
||||
// 应用 ConstrainedKeyItem 约束到数组的每个元素
|
||||
const Items extends readonly ConstrainedKeyItem<ConfigItemDefinition>[],
|
||||
>(items: Items): Items {
|
||||
// 可选的运行时验证,用于在浏览器控制台提供更友好的错误提示
|
||||
items.forEach(item => {
|
||||
// 类型守卫确保 item 有 key 和 type 属性
|
||||
if ('key' in item && typeof item.key === 'string' && 'type' in item && typeof item.type === 'string') {
|
||||
// 检查是否是需要File后缀的类型
|
||||
const requiresFileSuffix = item.type === 'file' || item.type === 'decorativeImages';
|
||||
// 可选的运行时验证,用于在浏览器控制台提供更友好的错误提示
|
||||
items.forEach((item) => {
|
||||
// 类型守卫确保 item 有 key 和 type 属性
|
||||
if ('key' in item && typeof item.key === 'string' && 'type' in item && typeof item.type === 'string') {
|
||||
// 检查是否是需要File后缀的类型
|
||||
const requiresFileSuffix = item.type === 'file' || item.type === 'decorativeImages'
|
||||
|
||||
if (requiresFileSuffix) {
|
||||
if (!item.key.endsWith('File')) {
|
||||
console.error(`类型错误: 配置项 "${item.key}" 类型为 '${item.type}' 但 key 未以 'File' 结尾。`);
|
||||
}
|
||||
} else {
|
||||
if (item.key.endsWith('File')) {
|
||||
console.error(`类型错误: 配置项 "${item.key}" 类型为 '${item.type}' 但 key 以 'File' 结尾。`);
|
||||
}
|
||||
if (requiresFileSuffix) {
|
||||
if (!item.key.endsWith('File')) {
|
||||
console.error(`类型错误: 配置项 "${item.key}" 类型为 '${item.type}' 但 key 未以 'File' 结尾。`)
|
||||
}
|
||||
} else {
|
||||
if (item.key.endsWith('File')) {
|
||||
console.error(`类型错误: 配置项 "${item.key}" 类型为 '${item.type}' 但 key 以 'File' 结尾。`)
|
||||
}
|
||||
}
|
||||
});
|
||||
return items;
|
||||
}
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
// --- 增强型工具类型 ---
|
||||
@@ -363,27 +362,27 @@ export function defineTemplateConfig<
|
||||
/**
|
||||
* 确保数值在指定范围内的工具类型
|
||||
*/
|
||||
export type NumericRange<Min extends number, Max extends number> =
|
||||
number extends Min ? number :
|
||||
number extends Max ? number :
|
||||
Min | Max | Exclude<number, Min | Max>;
|
||||
export type NumericRange<Min extends number, Max extends number>
|
||||
= number extends Min ? number
|
||||
: number extends Max ? number
|
||||
: Min | Max | Exclude<number, Min | Max>
|
||||
|
||||
/**
|
||||
* 非空数组工具类型
|
||||
*/
|
||||
export type NonEmptyArray<T> = [T, ...T[]];
|
||||
export type NonEmptyArray<T> = [T, ...T[]]
|
||||
|
||||
// --- 改进 rgbaToString 函数,添加更严格的类型检查 ---
|
||||
export function rgbaToString(color: RGBAColor | undefined | null): string {
|
||||
if (!color) return 'rgba(0,0,0,0)';
|
||||
if (!color) return 'rgba(0,0,0,0)'
|
||||
|
||||
// 额外的类型安全检查
|
||||
const r = Math.min(255, Math.max(0, Math.round(color.r)));
|
||||
const g = Math.min(255, Math.max(0, Math.round(color.g)));
|
||||
const b = Math.min(255, Math.max(0, Math.round(color.b)));
|
||||
const a = Math.min(1, Math.max(0, color.a));
|
||||
const r = Math.min(255, Math.max(0, Math.round(color.r)))
|
||||
const g = Math.min(255, Math.max(0, Math.round(color.g)))
|
||||
const b = Math.min(255, Math.max(0, Math.round(color.b)))
|
||||
const a = Math.min(1, Math.max(0, color.a))
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -392,30 +391,30 @@ export function rgbaToString(color: RGBAColor | undefined | null): string {
|
||||
* @returns 创建配置对象的工厂函数
|
||||
*/
|
||||
export function createTemplateConfigFactory<const Items extends readonly ConstrainedKeyItem<ConfigItemDefinition>[]>(
|
||||
items: Items
|
||||
items: Items,
|
||||
) {
|
||||
// 返回一个工厂函数,用于创建初始化的配置对象
|
||||
return (): ExtractConfigData<Items> => {
|
||||
const config = {} as ExtractConfigData<Items>;
|
||||
const config = {} as ExtractConfigData<Items>
|
||||
|
||||
// 使用项定义中的默认值初始化配置对象
|
||||
for (const item of items) {
|
||||
if ('default' in item && item.default !== undefined) {
|
||||
if (typeof config === 'object' && item.key) {
|
||||
// @ts-ignore - 动态赋值
|
||||
config[item.key] = item.default;
|
||||
config[item.key] = item.default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模板配置校验函数类型
|
||||
*/
|
||||
export type TemplateConfigValidator<T> = (config: T) => { valid: boolean; message?: string };
|
||||
export type TemplateConfigValidator<T> = (config: T) => { valid: boolean, message?: string }
|
||||
|
||||
/**
|
||||
* 创建配置验证器
|
||||
@@ -423,7 +422,7 @@ export type TemplateConfigValidator<T> = (config: T) => { valid: boolean; messag
|
||||
* @returns 验证器函数
|
||||
*/
|
||||
export function createConfigValidator<T>(validator: TemplateConfigValidator<T>) {
|
||||
return validator;
|
||||
return validator
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -431,11 +430,11 @@ export function createConfigValidator<T>(validator: TemplateConfigValidator<T>)
|
||||
*/
|
||||
export function isValidRGBAColor(obj: any): obj is RGBAColor {
|
||||
return (
|
||||
obj &&
|
||||
typeof obj === 'object' &&
|
||||
'r' in obj && typeof obj.r === 'number' &&
|
||||
'g' in obj && typeof obj.g === 'number' &&
|
||||
'b' in obj && typeof obj.b === 'number' &&
|
||||
'a' in obj && typeof obj.a === 'number'
|
||||
);
|
||||
}
|
||||
obj
|
||||
&& typeof obj === 'object'
|
||||
&& 'r' in obj && typeof obj.r === 'number'
|
||||
&& 'g' in obj && typeof obj.g === 'number'
|
||||
&& 'b' in obj && typeof obj.b === 'number'
|
||||
&& 'a' in obj && typeof obj.a === 'number'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getUuid4Hex } from '../../views/obs/blivechat/utils'
|
||||
import * as constants from '../../views/obs/blivechat/constants'
|
||||
import { getUuid4Hex } from '../../views/obs/blivechat/utils'
|
||||
|
||||
export function getDefaultMsgHandler() {
|
||||
let dummyFunc = () => {}
|
||||
const dummyFunc = () => {}
|
||||
return {
|
||||
onAddText: dummyFunc,
|
||||
onAddGift: dummyFunc,
|
||||
@@ -12,12 +12,12 @@ export function getDefaultMsgHandler() {
|
||||
onUpdateTranslation: dummyFunc,
|
||||
|
||||
onFatalError: dummyFunc,
|
||||
onDebugMsg: dummyFunc
|
||||
onDebugMsg: dummyFunc,
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_AVATAR_URL =
|
||||
'https://i0.hdslb.com/bfs/face/member/noface.jpg@64w_64h'
|
||||
export const DEFAULT_AVATAR_URL
|
||||
= 'https://i0.hdslb.com/bfs/face/member/noface.jpg@64w_64h'
|
||||
|
||||
export class AddTextMsg {
|
||||
constructor({
|
||||
@@ -34,7 +34,7 @@ export class AddTextMsg {
|
||||
medalLevel = 0,
|
||||
id = getUuid4Hex(),
|
||||
translation = '',
|
||||
emoticon = null
|
||||
emoticon = null,
|
||||
} = {}) {
|
||||
this.avatarUrl = avatarUrl
|
||||
this.timestamp = timestamp
|
||||
@@ -62,7 +62,7 @@ export class AddGiftMsg {
|
||||
totalCoin = 0,
|
||||
totalFreeCoin = 0,
|
||||
giftName = '',
|
||||
num = 1
|
||||
num = 1,
|
||||
} = {}) {
|
||||
this.id = id
|
||||
this.avatarUrl = avatarUrl
|
||||
@@ -81,7 +81,7 @@ export class AddMemberMsg {
|
||||
avatarUrl = DEFAULT_AVATAR_URL,
|
||||
timestamp = new Date().getTime() / 1000,
|
||||
authorName = '',
|
||||
privilegeType = 1
|
||||
privilegeType = 1,
|
||||
} = {}) {
|
||||
this.id = id
|
||||
this.avatarUrl = avatarUrl
|
||||
@@ -99,7 +99,7 @@ export class AddSuperChatMsg {
|
||||
authorName = '',
|
||||
price = 0,
|
||||
content = '',
|
||||
translation = ''
|
||||
translation = '',
|
||||
} = {}) {
|
||||
this.id = id
|
||||
this.avatarUrl = avatarUrl
|
||||
@@ -142,7 +142,7 @@ export class DebugMsg {
|
||||
}
|
||||
export function processAvatarUrl(avatarUrl) {
|
||||
// 去掉协议,兼容HTTP、HTTPS
|
||||
let m = avatarUrl.match(/(?:https?:)?(.*)/)
|
||||
const m = avatarUrl.match(/(?:https?:)?(.*)/)
|
||||
if (m) {
|
||||
avatarUrl = m[1]
|
||||
}
|
||||
|
||||
@@ -1,134 +1,134 @@
|
||||
import DefaultIndexTemplateVue from '@/views/view/indexTemplate/DefaultIndexTemplate.vue';
|
||||
import { defineAsyncComponent, ref, markRaw } from 'vue';
|
||||
import { defineAsyncComponent, markRaw, ref } from 'vue'
|
||||
import DefaultIndexTemplateVue from '@/views/view/indexTemplate/DefaultIndexTemplate.vue'
|
||||
|
||||
const debugAPI =
|
||||
import.meta.env.VITE_API == 'dev'
|
||||
const debugAPI
|
||||
= import.meta.env.VITE_API == 'dev'
|
||||
? import.meta.env.VITE_DEBUG_DEV_API
|
||||
: import.meta.env.VITE_DEBUG_RELEASE_API;
|
||||
const releseAPI = `https://vtsuru.suki.club/`;
|
||||
const failoverAPI = `https://failover-api.vtsuru.suki.club/`;
|
||||
: import.meta.env.VITE_DEBUG_RELEASE_API
|
||||
const releseAPI = `https://vtsuru.suki.club/`
|
||||
const failoverAPI = `https://failover-api.vtsuru.suki.club/`
|
||||
|
||||
export const isBackendUsable = ref(true);
|
||||
export const isDev = import.meta.env.MODE === 'development';
|
||||
export const isBackendUsable = ref(true)
|
||||
export const isDev = import.meta.env.MODE === 'development'
|
||||
// @ts-ignore
|
||||
export const isTauri = () => window.__TAURI__ !== undefined || window.__TAURI_INTERNAL__ !== undefined || '__TAURI__' in window;
|
||||
export const isTauri = () => window.__TAURI__ !== undefined || window.__TAURI_INTERNAL__ !== undefined || '__TAURI__' in window
|
||||
|
||||
export const AVATAR_URL = 'https://workers.vrp.moe/api/bilibili/avatar/';
|
||||
export const FILE_BASE_URL = 'https://files.vtsuru.suki.club';
|
||||
export const IMGUR_URL = FILE_BASE_URL + '/imgur/';
|
||||
export const THINGS_URL = FILE_BASE_URL + '/things/';
|
||||
export const apiFail = ref(false);
|
||||
export const AVATAR_URL = 'https://workers.vrp.moe/api/bilibili/avatar/'
|
||||
export const FILE_BASE_URL = 'https://files.vtsuru.suki.club'
|
||||
export const IMGUR_URL = `${FILE_BASE_URL}/imgur/`
|
||||
export const THINGS_URL = `${FILE_BASE_URL}/things/`
|
||||
export const apiFail = ref(false)
|
||||
|
||||
export const BASE_URL =
|
||||
process.env.NODE_ENV === 'development'
|
||||
export const BASE_URL
|
||||
= process.env.NODE_ENV === 'development'
|
||||
? debugAPI
|
||||
: apiFail.value
|
||||
? failoverAPI
|
||||
: releseAPI;
|
||||
export const BASE_API_URL = BASE_URL + 'api/';
|
||||
export const FETCH_API = 'https://fetch.vtsuru.live/';
|
||||
export const BASE_HUB_URL =
|
||||
(process.env.NODE_ENV === 'development'
|
||||
: releseAPI
|
||||
export const BASE_API_URL = `${BASE_URL}api/`
|
||||
export const FETCH_API = 'https://fetch.vtsuru.live/'
|
||||
export const BASE_HUB_URL
|
||||
= `${process.env.NODE_ENV === 'development'
|
||||
? debugAPI
|
||||
: apiFail.value
|
||||
? failoverAPI
|
||||
: releseAPI) + 'hub/';
|
||||
: releseAPI}hub/`
|
||||
|
||||
export const TURNSTILE_KEY = '0x4AAAAAAAETUSAKbds019h0';
|
||||
export const TURNSTILE_KEY = '0x4AAAAAAAETUSAKbds019h0'
|
||||
|
||||
export const CURRENT_HOST = `${window.location.protocol}//${isDev ? window.location.host : 'vtsuru.live'}/`;
|
||||
export const CN_HOST = 'https://vtsuru.suki.club/';
|
||||
export const CURRENT_HOST = `${window.location.protocol}//${isDev ? window.location.host : 'vtsuru.live'}/`
|
||||
export const CN_HOST = 'https://vtsuru.suki.club/'
|
||||
|
||||
export const USER_API_URL = BASE_API_URL + 'user/';
|
||||
export const ACCOUNT_API_URL = BASE_API_URL + 'account/';
|
||||
export const BILI_API_URL = BASE_API_URL + 'bili/';
|
||||
export const SONG_API_URL = BASE_API_URL + 'song-list/';
|
||||
export const NOTIFACTION_API_URL = BASE_API_URL + 'notification/';
|
||||
export const QUESTION_API_URL = BASE_API_URL + 'qa/';
|
||||
export const LOTTERY_API_URL = BASE_API_URL + 'lottery/';
|
||||
export const HISTORY_API_URL = BASE_API_URL + 'history/';
|
||||
export const SCHEDULE_API_URL = BASE_API_URL + 'schedule/';
|
||||
export const VIDEO_COLLECT_API_URL = BASE_API_URL + 'video-collect/';
|
||||
export const OPEN_LIVE_API_URL = BASE_API_URL + 'open-live/';
|
||||
export const SONG_REQUEST_API_URL = BASE_API_URL + 'live-request/';
|
||||
export const QUEUE_API_URL = BASE_API_URL + 'queue/';
|
||||
export const EVENT_API_URL = BASE_API_URL + 'event/';
|
||||
export const LIVE_API_URL = BASE_API_URL + 'live/';
|
||||
export const FEEDBACK_API_URL = BASE_API_URL + 'feedback/';
|
||||
export const MUSIC_REQUEST_API_URL = BASE_API_URL + 'music-request/';
|
||||
export const VTSURU_API_URL = BASE_API_URL + 'vtsuru/';
|
||||
export const POINT_API_URL = BASE_API_URL + 'point/';
|
||||
export const BILI_AUTH_API_URL = BASE_API_URL + 'bili-auth/';
|
||||
export const FORUM_API_URL = BASE_API_URL + 'forum/';
|
||||
export const USER_INDEX_API_URL = BASE_API_URL + 'user-index/';
|
||||
export const ANALYZE_API_URL = BASE_API_URL + 'analyze/';
|
||||
export const CHECKIN_API_URL = BASE_API_URL + 'checkin/';
|
||||
export const USER_CONFIG_API_URL = BASE_API_URL + 'user-config/';
|
||||
export const FILE_API_URL = BASE_API_URL + 'files/';
|
||||
export const VOTE_API_URL = BASE_API_URL + 'vote/';
|
||||
export const USER_API_URL = `${BASE_API_URL}user/`
|
||||
export const ACCOUNT_API_URL = `${BASE_API_URL}account/`
|
||||
export const BILI_API_URL = `${BASE_API_URL}bili/`
|
||||
export const SONG_API_URL = `${BASE_API_URL}song-list/`
|
||||
export const NOTIFACTION_API_URL = `${BASE_API_URL}notification/`
|
||||
export const QUESTION_API_URL = `${BASE_API_URL}qa/`
|
||||
export const LOTTERY_API_URL = `${BASE_API_URL}lottery/`
|
||||
export const HISTORY_API_URL = `${BASE_API_URL}history/`
|
||||
export const SCHEDULE_API_URL = `${BASE_API_URL}schedule/`
|
||||
export const VIDEO_COLLECT_API_URL = `${BASE_API_URL}video-collect/`
|
||||
export const OPEN_LIVE_API_URL = `${BASE_API_URL}open-live/`
|
||||
export const SONG_REQUEST_API_URL = `${BASE_API_URL}live-request/`
|
||||
export const QUEUE_API_URL = `${BASE_API_URL}queue/`
|
||||
export const EVENT_API_URL = `${BASE_API_URL}event/`
|
||||
export const LIVE_API_URL = `${BASE_API_URL}live/`
|
||||
export const FEEDBACK_API_URL = `${BASE_API_URL}feedback/`
|
||||
export const MUSIC_REQUEST_API_URL = `${BASE_API_URL}music-request/`
|
||||
export const VTSURU_API_URL = `${BASE_API_URL}vtsuru/`
|
||||
export const POINT_API_URL = `${BASE_API_URL}point/`
|
||||
export const BILI_AUTH_API_URL = `${BASE_API_URL}bili-auth/`
|
||||
export const FORUM_API_URL = `${BASE_API_URL}forum/`
|
||||
export const USER_INDEX_API_URL = `${BASE_API_URL}user-index/`
|
||||
export const ANALYZE_API_URL = `${BASE_API_URL}analyze/`
|
||||
export const CHECKIN_API_URL = `${BASE_API_URL}checkin/`
|
||||
export const USER_CONFIG_API_URL = `${BASE_API_URL}user-config/`
|
||||
export const FILE_API_URL = `${BASE_API_URL}files/`
|
||||
export const VOTE_API_URL = `${BASE_API_URL}vote/`
|
||||
|
||||
export type TemplateMapType = {
|
||||
export interface TemplateMapType {
|
||||
[key: string]: {
|
||||
name: string;
|
||||
settingName?: string;
|
||||
component: any;
|
||||
};
|
||||
};
|
||||
name: string
|
||||
settingName?: string
|
||||
component: any
|
||||
}
|
||||
}
|
||||
export const ScheduleTemplateMap: TemplateMapType = {
|
||||
'': {
|
||||
name: '默认',
|
||||
//settingName: 'Template.Schedule.Default',
|
||||
// settingName: 'Template.Schedule.Default',
|
||||
component: markRaw(defineAsyncComponent(
|
||||
() => import('@/views/view/scheduleTemplate/DefaultScheduleTemplate.vue')
|
||||
))
|
||||
async () => import('@/views/view/scheduleTemplate/DefaultScheduleTemplate.vue'),
|
||||
)),
|
||||
},
|
||||
pinky: {
|
||||
'pinky': {
|
||||
name: '粉粉',
|
||||
//settingName: 'Template.Schedule.Pinky',
|
||||
// settingName: 'Template.Schedule.Pinky',
|
||||
component: markRaw(defineAsyncComponent(
|
||||
() => import('@/views/view/scheduleTemplate/PinkySchedule.vue')
|
||||
))
|
||||
async () => import('@/views/view/scheduleTemplate/PinkySchedule.vue'),
|
||||
)),
|
||||
},
|
||||
kawaii: {
|
||||
'kawaii': {
|
||||
name: '可爱手帐 (未完成',
|
||||
settingName: 'Template.Schedule.Kawaii',
|
||||
component: markRaw(defineAsyncComponent(
|
||||
() => import('@/views/view/scheduleTemplate/KawaiiSchedule.vue')
|
||||
))
|
||||
}
|
||||
};
|
||||
async () => import('@/views/view/scheduleTemplate/KawaiiSchedule.vue'),
|
||||
)),
|
||||
},
|
||||
}
|
||||
export const SongListTemplateMap: TemplateMapType = {
|
||||
'': {
|
||||
name: '默认',
|
||||
//settingName: 'Template.SongList.Default',
|
||||
// settingName: 'Template.SongList.Default',
|
||||
component: markRaw(defineAsyncComponent(
|
||||
() => import('@/views/view/songListTemplate/DefaultSongListTemplate.vue')
|
||||
))
|
||||
async () => import('@/views/view/songListTemplate/DefaultSongListTemplate.vue'),
|
||||
)),
|
||||
},
|
||||
traditional: {
|
||||
'traditional': {
|
||||
name: '列表 (较推荐',
|
||||
settingName: 'Template.SongList.Traditional',
|
||||
component: markRaw(defineAsyncComponent(
|
||||
() =>
|
||||
import('@/views/view/songListTemplate/TraditionalSongListTemplate.vue')
|
||||
))
|
||||
async () =>
|
||||
import('@/views/view/songListTemplate/TraditionalSongListTemplate.vue'),
|
||||
)),
|
||||
},
|
||||
simple: {
|
||||
'simple': {
|
||||
name: '简单',
|
||||
//settingName: 'Template.SongList.Simple',
|
||||
// settingName: 'Template.SongList.Simple',
|
||||
component: markRaw(defineAsyncComponent(
|
||||
() => import('@/views/view/songListTemplate/SimpleSongListTemplate.vue')
|
||||
))
|
||||
async () => import('@/views/view/songListTemplate/SimpleSongListTemplate.vue'),
|
||||
)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const IndexTemplateMap: TemplateMapType = {
|
||||
'': {
|
||||
name: '默认',
|
||||
//settingName: 'Template.Index.Default',
|
||||
component: markRaw(DefaultIndexTemplateVue)
|
||||
}
|
||||
};
|
||||
// settingName: 'Template.Index.Default',
|
||||
component: markRaw(DefaultIndexTemplateVue),
|
||||
},
|
||||
}
|
||||
|
||||
export const defaultDanmujiCss = `@import url("https://fonts.googleapis.com/css?family=Changa%20One");
|
||||
@import url("https://fonts.googleapis.com/css?family=Imprima");
|
||||
@@ -426,4 +426,4 @@ yt-live-chat-paid-message-renderer {
|
||||
animation: anim 0ms;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
`
|
||||
`
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { UploadFileResponse, UserFileLocation, UserFileTypes } from '@/api/api-models';
|
||||
import { QueryPostAPI } from '@/api/query';
|
||||
import { FILE_API_URL } from '@/data/constants';
|
||||
import type { UploadFileResponse, UserFileTypes } from '@/api/api-models'
|
||||
import { UserFileLocation } from '@/api/api-models'
|
||||
import { QueryPostAPI } from '@/api/query'
|
||||
import { FILE_API_URL } from '@/data/constants'
|
||||
|
||||
/**
|
||||
* 文件上传阶段
|
||||
*/
|
||||
export enum UploadStage {
|
||||
Preparing = "准备上传",
|
||||
Uploading = "上传中",
|
||||
Success = "上传成功",
|
||||
Failed = "上传失败"
|
||||
Preparing = '准备上传',
|
||||
Uploading = '上传中',
|
||||
Success = '上传成功',
|
||||
Failed = '上传失败',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,43 +25,43 @@ export async function uploadFiles(
|
||||
files: File | File[],
|
||||
type?: UserFileTypes,
|
||||
location: UserFileLocation = UserFileLocation.Local,
|
||||
onProgress?: (stage: string) => void
|
||||
onProgress?: (stage: string) => void,
|
||||
): Promise<UploadFileResponse[]> {
|
||||
try {
|
||||
onProgress?.(UploadStage.Preparing);
|
||||
onProgress?.(UploadStage.Preparing)
|
||||
|
||||
const formData = new FormData();
|
||||
const formData = new FormData()
|
||||
|
||||
// 支持单个文件或文件数组
|
||||
if (Array.isArray(files)) {
|
||||
files.forEach(file => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
files.forEach((file) => {
|
||||
formData.append('files', file)
|
||||
})
|
||||
} else {
|
||||
formData.append('files', files);
|
||||
formData.append('files', files)
|
||||
}
|
||||
|
||||
if (type !== undefined) {
|
||||
formData.append('type', type.toString());
|
||||
formData.append('type', type.toString())
|
||||
}
|
||||
|
||||
formData.append('location', location.toString());
|
||||
formData.append('location', location.toString())
|
||||
|
||||
onProgress?.(UploadStage.Uploading);
|
||||
onProgress?.(UploadStage.Uploading)
|
||||
|
||||
const result = await QueryPostAPI<UploadFileResponse[]>(FILE_API_URL + 'upload', formData);
|
||||
const result = await QueryPostAPI<UploadFileResponse[]>(`${FILE_API_URL}upload`, formData)
|
||||
|
||||
if (result.code === 200) {
|
||||
onProgress?.(UploadStage.Success);
|
||||
return result.data;
|
||||
onProgress?.(UploadStage.Success)
|
||||
return result.data
|
||||
} else {
|
||||
onProgress?.(UploadStage.Failed);
|
||||
throw new Error(result.message || '上传失败');
|
||||
onProgress?.(UploadStage.Failed)
|
||||
throw new Error(result.message || '上传失败')
|
||||
}
|
||||
} catch (error) {
|
||||
onProgress?.(UploadStage.Failed);
|
||||
console.error('文件上传错误:', error);
|
||||
throw error;
|
||||
onProgress?.(UploadStage.Failed)
|
||||
console.error('文件上传错误:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,10 +73,10 @@ export async function uploadFile(
|
||||
file: File,
|
||||
type?: UserFileTypes,
|
||||
location: UserFileLocation = UserFileLocation.Local,
|
||||
onProgress?: (stage: string) => void
|
||||
onProgress?: (stage: string) => void,
|
||||
): Promise<UploadFileResponse> {
|
||||
const results = await uploadFiles(file, type, location, onProgress);
|
||||
return results[0]; // 返回第一个结果
|
||||
const results = await uploadFiles(file, type, location, onProgress)
|
||||
return results[0] // 返回第一个结果
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,7 +87,7 @@ export async function uploadMultipleFiles(
|
||||
files: File[],
|
||||
type?: UserFileTypes,
|
||||
location: UserFileLocation = UserFileLocation.Local,
|
||||
onProgress?: (stage: string) => void
|
||||
onProgress?: (stage: string) => void,
|
||||
): Promise<UploadFileResponse[]> {
|
||||
return uploadFiles(files, type, location, onProgress);
|
||||
}
|
||||
return uploadFiles(files, type, location, onProgress)
|
||||
}
|
||||
|
||||
@@ -1,167 +1,102 @@
|
||||
import type StickSvg from '@/assets/controller/Shared/shared-Left Joystick.svg?component';
|
||||
import type RightStickSvg from '@/assets/controller/Shared/shared-Right Joystick.svg?component';
|
||||
import type StickClickSvg from '@/assets/controller/Shared/shared-Left Stick Click.svg?component';
|
||||
import type RightStickClickSvg from '@/assets/controller/Shared/shared-Right Stick Click.svg?component';
|
||||
import StickSvgComp from '@/assets/controller/Shared/shared-Left Joystick.svg?component'
|
||||
import RightStickSvgComp from '@/assets/controller/Shared/shared-Right Joystick.svg?component'
|
||||
import StickClickSvgComp from '@/assets/controller/Shared/shared-Left Stick Click.svg?component'
|
||||
import RightStickClickSvgComp from '@/assets/controller/Shared/shared-Right Stick Click.svg?component'
|
||||
|
||||
import type XboxBodyBlack from '@/assets/controller/Body/Xbox/XboxOneColor/Xbox One Controller VSCView Black.svg?component';
|
||||
import type XboxBodyWhite from '@/assets/controller/Body/Xbox/XboxOneColor/Xbox One Controller VSCView White.svg?component';
|
||||
import type XboxBodyBlue from '@/assets/controller/Body/Xbox/XboxOneColor/Xbox One S Controller VSCView Blue.svg?component';
|
||||
import type XboxBodyRed from '@/assets/controller/Body/Xbox/XboxOneColor/Xbox One S Controller VSCView Red.svg?component';
|
||||
import XboxBodyBlackComp from '@/assets/controller/Body/Xbox/XboxOneColor/Xbox One Controller VSCView Black.svg?component'
|
||||
import XboxBodyWhiteComp from '@/assets/controller/Body/Xbox/XboxOneColor/Xbox One Controller VSCView White.svg?component'
|
||||
import XboxBodyBlueComp from '@/assets/controller/Body/Xbox/XboxOneColor/Xbox One S Controller VSCView Blue.svg?component'
|
||||
import XboxBodyRedComp from '@/assets/controller/Body/Xbox/XboxOneColor/Xbox One S Controller VSCView Red.svg?component'
|
||||
|
||||
import type PS4BodyBlack from '@/assets/controller/Body/DS4/DS4 VSC SVG.svg?component';
|
||||
import type PS4BodyFront from '@/assets/controller/Body/DS4/DS4 VSC Front SVG.svg?component';
|
||||
import type PS4V2BodyBlack from '@/assets/controller/Body/DS4/DS4 V2 VSC SVG.svg?component';
|
||||
import type PS4V2BodyWhite from '@/assets/controller/Body/DS4/DS4 V2 VSC SVG - Glacier White.svg?component';
|
||||
import type PS4V2BodyRed from '@/assets/controller/Body/DS4/DS4 V2 VSC SVG - Magma Red.svg?component';
|
||||
import type PS4V2BodyBlue from '@/assets/controller/Body/DS4/DS4 V2 VSC SVG - Midnight Blue.svg?component';
|
||||
import type PS4V2BodyGold from '@/assets/controller/Body/DS4/DS4 V2 VSC SVG - Gold.svg?component';
|
||||
import type PS5BodyBlack from '@/assets/controller/Body/DS/DualSense VSC SVG.svg?component';
|
||||
import type PS5BodyWhite from '@/assets/controller/Body/DS/DualSense VSC SVG - White.svg?component';
|
||||
import PS4BodyBlackComp from '@/assets/controller/Body/DS4/DS4 VSC SVG.svg?component'
|
||||
import PS4BodyFrontComp from '@/assets/controller/Body/DS4/DS4 VSC Front SVG.svg?component'
|
||||
import PS4V2BodyBlackComp from '@/assets/controller/Body/DS4/DS4 V2 VSC SVG.svg?component'
|
||||
import PS4V2BodyWhiteComp from '@/assets/controller/Body/DS4/DS4 V2 VSC SVG - Glacier White.svg?component'
|
||||
import PS4V2BodyRedComp from '@/assets/controller/Body/DS4/DS4 V2 VSC SVG - Magma Red.svg?component'
|
||||
import PS4V2BodyBlueComp from '@/assets/controller/Body/DS4/DS4 V2 VSC SVG - Midnight Blue.svg?component'
|
||||
import PS4V2BodyGoldComp from '@/assets/controller/Body/DS4/DS4 V2 VSC SVG - Gold.svg?component'
|
||||
import PS5BodyBlackComp from '@/assets/controller/Body/DS/DualSense VSCView SVG.svg?component'
|
||||
import PS5BodyWhiteComp from '@/assets/controller/Body/DS/DualSense VSCView SVG Midnight Black.svg?component'
|
||||
|
||||
import type SwitchProBody from '@/assets/controller/Body/SwitchPro/Switch Pro Controller VSCView.svg?component';
|
||||
import SwitchProBodyComp from '@/assets/controller/Body/SwitchPro/Switch Pro Controller VSCView.svg?component'
|
||||
|
||||
import type XboxASvg from '@/assets/controller/Shared/shared-A.svg?component';
|
||||
import type XboxBSvg from '@/assets/controller/Shared/shared-B.svg?component';
|
||||
import type XboxXSvg from '@/assets/controller/Shared/shared-X.svg?component';
|
||||
import type XboxYSvg from '@/assets/controller/Shared/shared-Y.svg?component';
|
||||
import type XboxDpadUpSvg from '@/assets/controller/Shared/shared-D-PAD Up.svg?component';
|
||||
import type XboxDpadDownSvg from '@/assets/controller/Shared/shared-D-PA Down.svg?component';
|
||||
import type XboxDpadLeftSvg from '@/assets/controller/Shared/shared-D-PAD Left.svg?component';
|
||||
import type XboxDpadRightSvg from '@/assets/controller/Shared/shared-D-PAD Right.svg?component';
|
||||
import type XboxLBSvg from '@/assets/controller/Shared/shared-L1.svg?component';
|
||||
import type XboxRBSvg from '@/assets/controller/Shared/shared-R1.svg?component';
|
||||
import type XboxLTSvg from '@/assets/controller/Shared/shared-L2.svg?component';
|
||||
import type XboxRTSvg from '@/assets/controller/Shared/shared-R2.svg?component';
|
||||
import type XboxViewSvg from '@/assets/controller/Xbox/xbox-View.svg?component';
|
||||
import type XboxMenuSvg from '@/assets/controller/Xbox/xbox-Menu.svg?component';
|
||||
import type XboxGuideSvg from '@/assets/controller/Xbox/xbox-Guide.svg?component';
|
||||
import XboxASvgComp from '@/assets/controller/Shared/shared-A.svg?component'
|
||||
import XboxBSvgComp from '@/assets/controller/Shared/shared-B.svg?component'
|
||||
import XboxXSvgComp from '@/assets/controller/Shared/shared-X.svg?component'
|
||||
import XboxYSvgComp from '@/assets/controller/Shared/shared-Y.svg?component'
|
||||
import XboxDpadUpSvgComp from '@/assets/controller/Shared/shared-D-PAD Up.svg?component'
|
||||
import XboxDpadDownSvgComp from '@/assets/controller/Shared/shared-D-PA Down.svg?component'
|
||||
import XboxDpadLeftSvgComp from '@/assets/controller/Shared/shared-D-PAD Left.svg?component'
|
||||
import XboxDpadRightSvgComp from '@/assets/controller/Shared/shared-D-PAD Right.svg?component'
|
||||
import XboxLBSvgComp from '@/assets/controller/Shared/shared-L1.svg?component'
|
||||
import XboxRBSvgComp from '@/assets/controller/Shared/shared-R1.svg?component'
|
||||
import XboxLTSvgComp from '@/assets/controller/Shared/shared-L2.svg?component'
|
||||
import XboxRTSvgComp from '@/assets/controller/Shared/shared-R2.svg?component'
|
||||
import XboxViewSvgComp from '@/assets/controller/Xbox/xbox-View.svg?component'
|
||||
import XboxMenuSvgComp from '@/assets/controller/Xbox/xbox-Menu.svg?component'
|
||||
import XboxGuideSvgComp from '@/assets/controller/Xbox/xbox-Guide.svg?component'
|
||||
|
||||
import type PsCrossSvg from '@/assets/controller/PlayStation/ps-Cross.svg?component';
|
||||
import type PsCircleSvg from '@/assets/controller/PlayStation/ps-Circle.svg?component';
|
||||
import type PsSquareSvg from '@/assets/controller/PlayStation/ps-Square.svg?component';
|
||||
import type PsTriangleSvg from '@/assets/controller/PlayStation/ps-Triangle.svg?component';
|
||||
import type PsTouchpadSvg from '@/assets/controller/PlayStation/ps5-Touchpad.svg?component';
|
||||
import type PsL1Svg from '@/assets/controller/PlayStation/ps-D-PAD Left.svg?component';
|
||||
import type PsR1Svg from '@/assets/controller/PlayStation/ps-D-PAD Right.svg?component';
|
||||
import type PsL2Svg from '@/assets/controller/Shared/shared-L2.svg?component';
|
||||
import type PsR2Svg from '@/assets/controller/Shared/shared-R2.svg?component';
|
||||
import type PsCreateSvg from '@/assets/controller/PlayStation/ps5-Create.svg?component';
|
||||
import type PsOptionsSvg from '@/assets/controller/PlayStation/ps5-Option.svg?component';
|
||||
import type PsGuideSvg from '@/assets/controller/PlayStation/ps-Guide.svg?component';
|
||||
import PsCrossSvgComp from '@/assets/controller/PlayStation/ps-Cross.svg?component'
|
||||
import PsCircleSvgComp from '@/assets/controller/PlayStation/ps-Circle.svg?component'
|
||||
import PsSquareSvgComp from '@/assets/controller/PlayStation/ps-Square.svg?component'
|
||||
import PsTriangleSvgComp from '@/assets/controller/PlayStation/ps-Triangle.svg?component'
|
||||
import PsTouchpadSvgComp from '@/assets/controller/PlayStation/ps5-Touchpad.svg?component'
|
||||
import PsL1SvgComp from '@/assets/controller/PlayStation/ps-D-PAD Left.svg?component'
|
||||
import PsR1SvgComp from '@/assets/controller/PlayStation/ps-D-PAD Right.svg?component'
|
||||
import PsL2SvgComp from '@/assets/controller/Shared/shared-L2.svg?component'
|
||||
import PsR2SvgComp from '@/assets/controller/Shared/shared-R2.svg?component'
|
||||
import PsCreateSvgComp from '@/assets/controller/PlayStation/ps5-Create.svg?component'
|
||||
import PsOptionsSvgComp from '@/assets/controller/PlayStation/ps5-Option.svg?component'
|
||||
import PsGuideSvgComp from '@/assets/controller/PlayStation/ps-Guide.svg?component'
|
||||
|
||||
import type NintendoASvg from '@/assets/controller/Nintendo/nintendo-positional prompt A.svg?component';
|
||||
import type NintendoBSvg from '@/assets/controller/Nintendo/nintendo-positional prompt B.svg?component';
|
||||
import type NintendoXSvg from '@/assets/controller/Nintendo/nintendo-positional prompt X.svg?component';
|
||||
import type NintendoYSvg from '@/assets/controller/Nintendo/nintendo-positional prompt Y.svg?component';
|
||||
import type NintendoCaptureSvg from '@/assets/controller/Nintendo/nintendoswitch-Capture.svg?component';
|
||||
import type NintendoLSvg from '@/assets/controller/Nintendo/nintendo-L.svg?component';
|
||||
import type NintendoRSvg from '@/assets/controller/Nintendo/nintendo-R.svg?component';
|
||||
import type NintendoZLSvg from '@/assets/controller/Nintendo/nintendo-ZL.svg?component';
|
||||
import type NintendoZRSvg from '@/assets/controller/Nintendo/nintendo-ZR.svg?component';
|
||||
import type NintendoPlusSvg from '@/assets/controller/Nintendo/nintendo-Plus.svg?component';
|
||||
import type NintendoMinusSvg from '@/assets/controller/Nintendo/nintendo-Minus.svg?component';
|
||||
|
||||
import StickSvgComp from '@/assets/controller/Shared/shared-Left Joystick.svg?component';
|
||||
import RightStickSvgComp from '@/assets/controller/Shared/shared-Right Joystick.svg?component';
|
||||
import StickClickSvgComp from '@/assets/controller/Shared/shared-Left Stick Click.svg?component';
|
||||
import RightStickClickSvgComp from '@/assets/controller/Shared/shared-Right Stick Click.svg?component';
|
||||
|
||||
import XboxBodyBlackComp from '@/assets/controller/Body/Xbox/XboxOneColor/Xbox One Controller VSCView Black.svg?component';
|
||||
import XboxBodyWhiteComp from '@/assets/controller/Body/Xbox/XboxOneColor/Xbox One Controller VSCView White.svg?component';
|
||||
import XboxBodyBlueComp from '@/assets/controller/Body/Xbox/XboxOneColor/Xbox One S Controller VSCView Blue.svg?component';
|
||||
import XboxBodyRedComp from '@/assets/controller/Body/Xbox/XboxOneColor/Xbox One S Controller VSCView Red.svg?component';
|
||||
|
||||
import PS4BodyBlackComp from '@/assets/controller/Body/DS4/DS4 VSC SVG.svg?component';
|
||||
import PS4BodyFrontComp from '@/assets/controller/Body/DS4/DS4 VSC Front SVG.svg?component';
|
||||
import PS4V2BodyBlackComp from '@/assets/controller/Body/DS4/DS4 V2 VSC SVG.svg?component';
|
||||
import PS4V2BodyWhiteComp from '@/assets/controller/Body/DS4/DS4 V2 VSC SVG - Glacier White.svg?component';
|
||||
import PS4V2BodyRedComp from '@/assets/controller/Body/DS4/DS4 V2 VSC SVG - Magma Red.svg?component';
|
||||
import PS4V2BodyBlueComp from '@/assets/controller/Body/DS4/DS4 V2 VSC SVG - Midnight Blue.svg?component';
|
||||
import PS4V2BodyGoldComp from '@/assets/controller/Body/DS4/DS4 V2 VSC SVG - Gold.svg?component';
|
||||
import PS5BodyBlackComp from '@/assets/controller/Body/DS/DualSense VSCView SVG.svg?component';
|
||||
import PS5BodyWhiteComp from '@/assets/controller/Body/DS/DualSense VSCView SVG Midnight Black.svg?component';
|
||||
|
||||
import SwitchProBodyComp from '@/assets/controller/Body/SwitchPro/Switch Pro Controller VSCView.svg?component';
|
||||
|
||||
import XboxASvgComp from '@/assets/controller/Shared/shared-A.svg?component';
|
||||
import XboxBSvgComp from '@/assets/controller/Shared/shared-B.svg?component';
|
||||
import XboxXSvgComp from '@/assets/controller/Shared/shared-X.svg?component';
|
||||
import XboxYSvgComp from '@/assets/controller/Shared/shared-Y.svg?component';
|
||||
import XboxDpadUpSvgComp from '@/assets/controller/Shared/shared-D-PAD Up.svg?component';
|
||||
import XboxDpadDownSvgComp from '@/assets/controller/Shared/shared-D-PA Down.svg?component';
|
||||
import XboxDpadLeftSvgComp from '@/assets/controller/Shared/shared-D-PAD Left.svg?component';
|
||||
import XboxDpadRightSvgComp from '@/assets/controller/Shared/shared-D-PAD Right.svg?component';
|
||||
import XboxLBSvgComp from '@/assets/controller/Shared/shared-L1.svg?component';
|
||||
import XboxRBSvgComp from '@/assets/controller/Shared/shared-R1.svg?component';
|
||||
import XboxLTSvgComp from '@/assets/controller/Shared/shared-L2.svg?component';
|
||||
import XboxRTSvgComp from '@/assets/controller/Shared/shared-R2.svg?component';
|
||||
import XboxViewSvgComp from '@/assets/controller/Xbox/xbox-View.svg?component';
|
||||
import XboxMenuSvgComp from '@/assets/controller/Xbox/xbox-Menu.svg?component';
|
||||
import XboxGuideSvgComp from '@/assets/controller/Xbox/xbox-Guide.svg?component';
|
||||
|
||||
import PsCrossSvgComp from '@/assets/controller/PlayStation/ps-Cross.svg?component';
|
||||
import PsCircleSvgComp from '@/assets/controller/PlayStation/ps-Circle.svg?component';
|
||||
import PsSquareSvgComp from '@/assets/controller/PlayStation/ps-Square.svg?component';
|
||||
import PsTriangleSvgComp from '@/assets/controller/PlayStation/ps-Triangle.svg?component';
|
||||
import PsTouchpadSvgComp from '@/assets/controller/PlayStation/ps5-Touchpad.svg?component';
|
||||
import PsL1SvgComp from '@/assets/controller/PlayStation/ps-D-PAD Left.svg?component';
|
||||
import PsR1SvgComp from '@/assets/controller/PlayStation/ps-D-PAD Right.svg?component';
|
||||
import PsL2SvgComp from '@/assets/controller/Shared/shared-L2.svg?component';
|
||||
import PsR2SvgComp from '@/assets/controller/Shared/shared-R2.svg?component';
|
||||
import PsCreateSvgComp from '@/assets/controller/PlayStation/ps5-Create.svg?component';
|
||||
import PsOptionsSvgComp from '@/assets/controller/PlayStation/ps5-Option.svg?component';
|
||||
import PsGuideSvgComp from '@/assets/controller/PlayStation/ps-Guide.svg?component';
|
||||
|
||||
import NintendoASvgComp from '@/assets/controller/Nintendo/nintendo-positional prompt A.svg?component';
|
||||
import NintendoBSvgComp from '@/assets/controller/Nintendo/nintendo-positional prompt B.svg?component';
|
||||
import NintendoXSvgComp from '@/assets/controller/Nintendo/nintendo-positional prompt X.svg?component';
|
||||
import NintendoYSvgComp from '@/assets/controller/Nintendo/nintendo-positional prompt Y.svg?component';
|
||||
import NintendoCaptureSvgComp from '@/assets/controller/Nintendo/nintendoswitch-Capture.svg?component';
|
||||
import NintendoLSvgComp from '@/assets/controller/Nintendo/nintendo-L.svg?component';
|
||||
import NintendoRSvgComp from '@/assets/controller/Nintendo/nintendo-R.svg?component';
|
||||
import NintendoZLSvgComp from '@/assets/controller/Nintendo/nintendo-ZL.svg?component';
|
||||
import NintendoZRSvgComp from '@/assets/controller/Nintendo/nintendo-ZR.svg?component';
|
||||
import NintendoPlusSvgComp from '@/assets/controller/Nintendo/nintendo-Plus.svg?component';
|
||||
import NintendoMinusSvgComp from '@/assets/controller/Nintendo/nintendo-Minus.svg?component';
|
||||
|
||||
import { AllGamepadConfigs, LogicalButton, GamepadType } from '@/types/gamepad';
|
||||
import type { Component } from 'vue';
|
||||
import { h, markRaw } from 'vue';
|
||||
import NintendoASvgComp from '@/assets/controller/Nintendo/nintendo-positional prompt A.svg?component'
|
||||
import NintendoBSvgComp from '@/assets/controller/Nintendo/nintendo-positional prompt B.svg?component'
|
||||
import NintendoXSvgComp from '@/assets/controller/Nintendo/nintendo-positional prompt X.svg?component'
|
||||
import NintendoYSvgComp from '@/assets/controller/Nintendo/nintendo-positional prompt Y.svg?component'
|
||||
import NintendoCaptureSvgComp from '@/assets/controller/Nintendo/nintendoswitch-Capture.svg?component'
|
||||
import NintendoLSvgComp from '@/assets/controller/Nintendo/nintendo-L.svg?component'
|
||||
import NintendoRSvgComp from '@/assets/controller/Nintendo/nintendo-R.svg?component'
|
||||
import NintendoZLSvgComp from '@/assets/controller/Nintendo/nintendo-ZL.svg?component'
|
||||
import NintendoZRSvgComp from '@/assets/controller/Nintendo/nintendo-ZR.svg?component'
|
||||
import NintendoPlusSvgComp from '@/assets/controller/Nintendo/nintendo-Plus.svg?component'
|
||||
import NintendoMinusSvgComp from '@/assets/controller/Nintendo/nintendo-Minus.svg?component'
|
||||
|
||||
import type { AllGamepadConfigs, GamepadType, LogicalButton } from '@/types/gamepad'
|
||||
import type { Component } from 'vue'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
export interface BodyOptionConfig {
|
||||
name: string;
|
||||
body: Component;
|
||||
defaultViewBox?: string;
|
||||
name: string
|
||||
body: Component
|
||||
defaultViewBox?: string
|
||||
}
|
||||
|
||||
const L3 = 'LEFT_STICK_PRESS' as LogicalButton;
|
||||
const R3 = 'RIGHT_STICK_PRESS' as LogicalButton;
|
||||
const ACTIONDOWN = 'ACTION_DOWN' as LogicalButton;
|
||||
const ACTIONRIGHT = 'ACTION_RIGHT' as LogicalButton;
|
||||
const ACTIONLEFT = 'ACTION_LEFT' as LogicalButton;
|
||||
const ACTIONUP = 'ACTION_UP' as LogicalButton;
|
||||
const DPADUP = 'DPAD_UP' as LogicalButton;
|
||||
const DPADDOWN = 'DPAD_DOWN' as LogicalButton;
|
||||
const DPADLEFT = 'DPAD_LEFT' as LogicalButton;
|
||||
const DPADRIGHT = 'DPAD_RIGHT' as LogicalButton;
|
||||
const L1 = 'LEFT_SHOULDER_1' as LogicalButton;
|
||||
const R1 = 'RIGHT_SHOULDER_1' as LogicalButton;
|
||||
const L2 = 'LEFT_SHOULDER_2' as LogicalButton;
|
||||
const R2 = 'RIGHT_SHOULDER_2' as LogicalButton;
|
||||
const SELECT = 'SELECT' as LogicalButton;
|
||||
const START = 'START' as LogicalButton;
|
||||
const GUIDE = 'HOME' as LogicalButton;
|
||||
const PSTOUCHPAD = 'PS_TOUCHPAD' as LogicalButton;
|
||||
const NINTENDOCAPTURE = 'NINTENDO_CAPTURE' as LogicalButton;
|
||||
|
||||
const L3 = 'LEFT_STICK_PRESS' as LogicalButton
|
||||
const R3 = 'RIGHT_STICK_PRESS' as LogicalButton
|
||||
const ACTIONDOWN = 'ACTION_DOWN' as LogicalButton
|
||||
const ACTIONRIGHT = 'ACTION_RIGHT' as LogicalButton
|
||||
const ACTIONLEFT = 'ACTION_LEFT' as LogicalButton
|
||||
const ACTIONUP = 'ACTION_UP' as LogicalButton
|
||||
const DPADUP = 'DPAD_UP' as LogicalButton
|
||||
const DPADDOWN = 'DPAD_DOWN' as LogicalButton
|
||||
const DPADLEFT = 'DPAD_LEFT' as LogicalButton
|
||||
const DPADRIGHT = 'DPAD_RIGHT' as LogicalButton
|
||||
const L1 = 'LEFT_SHOULDER_1' as LogicalButton
|
||||
const R1 = 'RIGHT_SHOULDER_1' as LogicalButton
|
||||
const L2 = 'LEFT_SHOULDER_2' as LogicalButton
|
||||
const R2 = 'RIGHT_SHOULDER_2' as LogicalButton
|
||||
const SELECT = 'SELECT' as LogicalButton
|
||||
const START = 'START' as LogicalButton
|
||||
const GUIDE = 'HOME' as LogicalButton
|
||||
const PSTOUCHPAD = 'PS_TOUCHPAD' as LogicalButton
|
||||
const NINTENDOCAPTURE = 'NINTENDO_CAPTURE' as LogicalButton
|
||||
|
||||
export const controllerBodies: Record<GamepadType, BodyOptionConfig[]> = {
|
||||
xbox: [
|
||||
{ name: 'Xbox One 黑色', body: markRaw(XboxBodyBlackComp), defaultViewBox: '0 0 1543 956' },
|
||||
{ name: 'Xbox One 白色', body: markRaw(XboxBodyWhiteComp), defaultViewBox: '0 0 1543 956' },
|
||||
{ name: 'Xbox One S 蓝色', body: markRaw(XboxBodyBlueComp), defaultViewBox: '0 0 1543 956' },
|
||||
{ name: 'Xbox One S 红色', body: markRaw(XboxBodyRedComp), defaultViewBox: '0 0 1543 956' }
|
||||
{ name: 'Xbox One S 红色', body: markRaw(XboxBodyRedComp), defaultViewBox: '0 0 1543 956' },
|
||||
],
|
||||
ps: [
|
||||
{ name: 'PS5 DualSense 黑色', body: markRaw(PS5BodyBlackComp), defaultViewBox: '0 0 544.707 302.911' },
|
||||
@@ -172,31 +107,31 @@ export const controllerBodies: Record<GamepadType, BodyOptionConfig[]> = {
|
||||
{ name: 'PS4 V2 冰川白', body: markRaw(PS4V2BodyWhiteComp), defaultViewBox: '0 0 544.707 302.911' },
|
||||
{ name: 'PS4 V2 熔岩红', body: markRaw(PS4V2BodyRedComp), defaultViewBox: '0 0 544.707 302.911' },
|
||||
{ name: 'PS4 V2 午夜蓝', body: markRaw(PS4V2BodyBlueComp), defaultViewBox: '0 0 544.707 302.911' },
|
||||
{ name: 'PS4 V2 金色', body: markRaw(PS4V2BodyGoldComp), defaultViewBox: '0 0 544.707 302.911' }
|
||||
{ name: 'PS4 V2 金色', body: markRaw(PS4V2BodyGoldComp), defaultViewBox: '0 0 544.707 302.911' },
|
||||
],
|
||||
nintendo: [
|
||||
{ name: 'Switch Pro', body: markRaw(SwitchProBodyComp), defaultViewBox: '0 0 1200 780' }
|
||||
]
|
||||
};
|
||||
{ name: 'Switch Pro', body: markRaw(SwitchProBodyComp), defaultViewBox: '0 0 1200 780' },
|
||||
],
|
||||
}
|
||||
|
||||
function parseAspectRatioToViewBox(aspectRatio: string): string | undefined {
|
||||
const parts = aspectRatio.split('/');
|
||||
const parts = aspectRatio.split('/')
|
||||
if (parts.length === 2) {
|
||||
const width = parseFloat(parts[0]);
|
||||
const height = parseFloat(parts[1]);
|
||||
const width = Number.parseFloat(parts[0])
|
||||
const height = Number.parseFloat(parts[1])
|
||||
if (!isNaN(width) && !isNaN(height)) {
|
||||
return `0 0 ${width} ${height}`;
|
||||
return `0 0 ${width} ${height}`
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
export interface ControllerComponentStructure {
|
||||
name: string;
|
||||
path?: string;
|
||||
type: 'button' | 'stick' | 'dpad' | 'trigger' | 'group';
|
||||
logicalButton?: LogicalButton | string;
|
||||
childComponents?: ControllerComponentStructure[];
|
||||
name: string
|
||||
path?: string
|
||||
type: 'button' | 'stick' | 'dpad' | 'trigger' | 'group'
|
||||
logicalButton?: LogicalButton | string
|
||||
childComponents?: ControllerComponentStructure[]
|
||||
}
|
||||
|
||||
const xboxControllerStructure: ControllerComponentStructure[] = [
|
||||
@@ -216,8 +151,8 @@ const xboxControllerStructure: ControllerComponentStructure[] = [
|
||||
path: 'Left Stick',
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Left Stick Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Left Stick Color' }
|
||||
]
|
||||
{ name: 'Color', type: 'group', path: 'Left Stick Color' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '右摇杆',
|
||||
@@ -226,10 +161,10 @@ const xboxControllerStructure: ControllerComponentStructure[] = [
|
||||
path: 'Right Stick',
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Right Stick Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Right Stick Color' }
|
||||
]
|
||||
}
|
||||
]
|
||||
{ name: 'Color', type: 'group', path: 'Right Stick Color' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '中央按钮',
|
||||
@@ -243,8 +178,8 @@ const xboxControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Xbox Guide Button Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Xbox Guide Button Color' },
|
||||
{ name: 'Icon', type: 'group', path: 'Xbox Icon (OG)' }
|
||||
]
|
||||
{ name: 'Icon', type: 'group', path: 'Xbox Icon (OG)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'View按钮',
|
||||
@@ -254,8 +189,8 @@ const xboxControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'View Button Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'View Button Color' },
|
||||
{ name: 'Icon', type: 'group', path: 'View Button Icon' }
|
||||
]
|
||||
{ name: 'Icon', type: 'group', path: 'View Button Icon' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Menu按钮',
|
||||
@@ -265,10 +200,10 @@ const xboxControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Menu Button Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Menu Button Color' },
|
||||
{ name: 'Icon', type: 'group', path: 'Menu Button Icon' }
|
||||
]
|
||||
}
|
||||
]
|
||||
{ name: 'Icon', type: 'group', path: 'Menu Button Icon' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '面部按钮',
|
||||
@@ -278,7 +213,7 @@ const xboxControllerStructure: ControllerComponentStructure[] = [
|
||||
{
|
||||
name: '面部按钮点',
|
||||
type: 'group',
|
||||
path: 'Face Button Dot'
|
||||
path: 'Face Button Dot',
|
||||
},
|
||||
{
|
||||
name: 'A按钮',
|
||||
@@ -288,8 +223,8 @@ const xboxControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'A Button Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'A Button Color' },
|
||||
{ name: 'Text', type: 'group', path: 'A Button Text' }
|
||||
]
|
||||
{ name: 'Text', type: 'group', path: 'A Button Text' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'B按钮',
|
||||
@@ -299,8 +234,8 @@ const xboxControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Text', type: 'group', path: 'Text' },
|
||||
{ name: 'Color', type: 'group', path: 'Color' },
|
||||
{ name: 'Outline', type: 'group', path: 'B Button Outline' }
|
||||
]
|
||||
{ name: 'Outline', type: 'group', path: 'B Button Outline' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'X按钮',
|
||||
@@ -310,8 +245,8 @@ const xboxControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'X Button Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'X Button Color' },
|
||||
{ name: 'Text', type: 'group', path: 'X Button Text' }
|
||||
]
|
||||
{ name: 'Text', type: 'group', path: 'X Button Text' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Y按钮',
|
||||
@@ -321,10 +256,10 @@ const xboxControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Y Button Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Y Button Color' },
|
||||
{ name: 'Text', type: 'group', path: 'Y Button Text' }
|
||||
]
|
||||
}
|
||||
]
|
||||
{ name: 'Text', type: 'group', path: 'Y Button Text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '方向键组',
|
||||
@@ -339,34 +274,34 @@ const xboxControllerStructure: ControllerComponentStructure[] = [
|
||||
{ name: 'D-PAD正面轮廓', type: 'group', path: 'D-PAD Outline Front' },
|
||||
{ name: 'D-PAD底部轮廓', type: 'group', path: 'D-PAD Outline Bottom' },
|
||||
{ name: 'D-PAD相关主体轮廓', type: 'group', path: 'Body Outline' },
|
||||
{ name: 'D-PAD颜色', type: 'group', path: 'Color' }
|
||||
]
|
||||
{ name: 'D-PAD颜色', type: 'group', path: 'Color' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '上 (逻辑)',
|
||||
type: 'button',
|
||||
logicalButton: DPADUP,
|
||||
path: 'D-PAD Up'
|
||||
path: 'D-PAD Up',
|
||||
},
|
||||
{
|
||||
name: '下 (逻辑)',
|
||||
type: 'button',
|
||||
logicalButton: DPADDOWN,
|
||||
path: 'D-PAD Down'
|
||||
path: 'D-PAD Down',
|
||||
},
|
||||
{
|
||||
name: '左 (逻辑)',
|
||||
type: 'button',
|
||||
logicalButton: DPADLEFT,
|
||||
path: 'D-PAD Left'
|
||||
path: 'D-PAD Left',
|
||||
},
|
||||
{
|
||||
name: '右 (逻辑)',
|
||||
type: 'button',
|
||||
logicalButton: DPADRIGHT,
|
||||
path: 'D-PAD Right'
|
||||
}
|
||||
]
|
||||
path: 'D-PAD Right',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '肩部按钮',
|
||||
@@ -381,8 +316,8 @@ const xboxControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'LB Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Color' },
|
||||
{ name: 'Text', type: 'group', path: 'LB Text' }
|
||||
]
|
||||
{ name: 'Text', type: 'group', path: 'LB Text' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '右肩按钮',
|
||||
@@ -392,18 +327,18 @@ const xboxControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'RB Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Color' },
|
||||
{ name: 'Text', type: 'group', path: 'RB Text' }
|
||||
]
|
||||
{ name: 'Text', type: 'group', path: 'RB Text' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '配对按钮',
|
||||
type: 'button',
|
||||
path: 'Pair Button'
|
||||
path: 'Pair Button',
|
||||
},
|
||||
{ name: 'Xbox One 控制器轮廓 (肩部)', type: 'group', path: 'Xbox One Controller Outline'},
|
||||
{ name: 'Xbox One S 控制器轮廓 (肩部)', type: 'group', path: 'Xbox One S Controller Outline'},
|
||||
{ name: '肩部整体颜色', type: 'group', path: 'Color'}
|
||||
]
|
||||
{ name: 'Xbox One 控制器轮廓 (肩部)', type: 'group', path: 'Xbox One Controller Outline' },
|
||||
{ name: 'Xbox One S 控制器轮廓 (肩部)', type: 'group', path: 'Xbox One S Controller Outline' },
|
||||
{ name: '肩部整体颜色', type: 'group', path: 'Color' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '扳机键',
|
||||
@@ -418,8 +353,8 @@ const xboxControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'LT Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Color' },
|
||||
{ name: 'Text', type: 'group', path: 'LT Text' }
|
||||
]
|
||||
{ name: 'Text', type: 'group', path: 'LT Text' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '右扳机',
|
||||
@@ -429,12 +364,12 @@ const xboxControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'RT Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Color' },
|
||||
{ name: 'Text', type: 'group', path: 'RT Text' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
{ name: 'Text', type: 'group', path: 'RT Text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const psControllerStructure: ControllerComponentStructure[] = [
|
||||
{
|
||||
@@ -450,28 +385,28 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Symbol', type: 'group', path: 'Symbol' },
|
||||
{ name: 'Outline', type: 'group', path: 'Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Color' }
|
||||
]
|
||||
{ name: 'Color', type: 'group', path: 'Color' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'D-PAD Down',
|
||||
type: 'button',
|
||||
logicalButton: DPADDOWN,
|
||||
path: 'D-PAD Down'
|
||||
path: 'D-PAD Down',
|
||||
},
|
||||
{
|
||||
name: 'D-PAD Left',
|
||||
type: 'button',
|
||||
logicalButton: DPADLEFT,
|
||||
path: 'D-PAD Left'
|
||||
path: 'D-PAD Left',
|
||||
},
|
||||
{
|
||||
name: 'D-PAD Up',
|
||||
type: 'button',
|
||||
logicalButton: DPADUP,
|
||||
path: 'D-PAD Up'
|
||||
}
|
||||
]
|
||||
path: 'D-PAD Up',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '摇杆',
|
||||
@@ -484,8 +419,8 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
path: 'Left Stick',
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Left Stick Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Left Stick Color' }
|
||||
]
|
||||
{ name: 'Color', type: 'group', path: 'Left Stick Color' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '右摇杆',
|
||||
@@ -494,10 +429,10 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
path: 'Right Stick',
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Right Stick Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Right Stick Color' }
|
||||
]
|
||||
}
|
||||
]
|
||||
{ name: 'Color', type: 'group', path: 'Right Stick Color' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '面部按钮',
|
||||
@@ -506,13 +441,13 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
{
|
||||
name: 'Face Button',
|
||||
type: 'group',
|
||||
path: 'Face Button'
|
||||
path: 'Face Button',
|
||||
},
|
||||
{
|
||||
name: 'Option Button',
|
||||
type: 'button',
|
||||
logicalButton: START,
|
||||
path: 'Option Button'
|
||||
path: 'Option Button',
|
||||
},
|
||||
{
|
||||
name: 'Create Button',
|
||||
@@ -522,8 +457,8 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Color' },
|
||||
{ name: 'Symbol', type: 'group', path: 'Symbol' }
|
||||
]
|
||||
{ name: 'Symbol', type: 'group', path: 'Symbol' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '十字按钮',
|
||||
@@ -532,8 +467,8 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
logicalButton: ACTIONDOWN,
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Cross Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Cross Color' }
|
||||
]
|
||||
{ name: 'Color', type: 'group', path: 'Cross Color' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '圆形按钮',
|
||||
@@ -542,8 +477,8 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
logicalButton: ACTIONRIGHT,
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Circle Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Circle Color' }
|
||||
]
|
||||
{ name: 'Color', type: 'group', path: 'Circle Color' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '方形按钮',
|
||||
@@ -552,8 +487,8 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
logicalButton: ACTIONLEFT,
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Square Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Square Color' }
|
||||
]
|
||||
{ name: 'Color', type: 'group', path: 'Square Color' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '三角按钮',
|
||||
@@ -562,10 +497,10 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
logicalButton: ACTIONUP,
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Triangle Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Triangle Color' }
|
||||
]
|
||||
}
|
||||
]
|
||||
{ name: 'Color', type: 'group', path: 'Triangle Color' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '静音按钮',
|
||||
@@ -580,10 +515,10 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Mute With LED', type: 'group', path: 'Mute With LED' },
|
||||
{ name: 'Mute Without LED', type: 'group', path: 'Mute Without LED' },
|
||||
{ name: 'Mute Icon', type: 'group', path: 'Mute Icon' }
|
||||
]
|
||||
}
|
||||
]
|
||||
{ name: 'Mute Icon', type: 'group', path: 'Mute Icon' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Misc.',
|
||||
@@ -592,8 +527,8 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'PS Button', type: 'button', logicalButton: GUIDE, path: 'PS Button' },
|
||||
{ name: 'Speakers', type: 'group', path: 'Speakers' },
|
||||
{ name: 'USB-C Plug', type: 'group', path: 'USB-C Plug' }
|
||||
]
|
||||
{ name: 'USB-C Plug', type: 'group', path: 'USB-C Plug' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '触摸板',
|
||||
@@ -602,8 +537,8 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
path: 'Touchpad',
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Touchpad Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Touchpad Color' }
|
||||
]
|
||||
{ name: 'Color', type: 'group', path: 'Touchpad Color' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '分享按钮',
|
||||
@@ -612,8 +547,8 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
path: 'Create Button',
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Color' }
|
||||
]
|
||||
{ name: 'Color', type: 'group', path: 'Color' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '菜单按钮',
|
||||
@@ -622,10 +557,10 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
path: 'Option Button',
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Color' }
|
||||
]
|
||||
{ name: 'Color', type: 'group', path: 'Color' },
|
||||
],
|
||||
},
|
||||
{
|
||||
{
|
||||
name: '肩部按钮',
|
||||
type: 'group',
|
||||
childComponents: [
|
||||
@@ -637,8 +572,8 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Color' },
|
||||
{ name: 'Text', type: 'group', path: 'Text' }
|
||||
]
|
||||
{ name: 'Text', type: 'group', path: 'Text' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '右肩按钮',
|
||||
@@ -648,10 +583,10 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Color' },
|
||||
{ name: 'Text', type: 'group', path: 'Text' }
|
||||
]
|
||||
{ name: 'Text', type: 'group', path: 'Text' },
|
||||
],
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '扳机键',
|
||||
@@ -665,8 +600,8 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Color' },
|
||||
{ name: 'Text', type: 'group', path: 'Text' }
|
||||
]
|
||||
{ name: 'Text', type: 'group', path: 'Text' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '右扳机',
|
||||
@@ -676,12 +611,12 @@ const psControllerStructure: ControllerComponentStructure[] = [
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Color' },
|
||||
{ name: 'Text', type: 'group', path: 'Text' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
{ name: 'Text', type: 'group', path: 'Text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const nintendoControllerStructure: ControllerComponentStructure[] = [
|
||||
{
|
||||
@@ -694,8 +629,8 @@ const nintendoControllerStructure: ControllerComponentStructure[] = [
|
||||
logicalButton: 'LEFT_STICK',
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Left Stick Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Left Stick Color' }
|
||||
]
|
||||
{ name: 'Color', type: 'group', path: 'Left Stick Color' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '右摇杆',
|
||||
@@ -703,18 +638,18 @@ const nintendoControllerStructure: ControllerComponentStructure[] = [
|
||||
logicalButton: 'RIGHT_STICK',
|
||||
childComponents: [
|
||||
{ name: 'Outline', type: 'group', path: 'Right Stick Outline' },
|
||||
{ name: 'Color', type: 'group', path: 'Right Stick Color' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
{ name: 'Color', type: 'group', path: 'Right Stick Color' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const controllerStructures: Record<GamepadType, ControllerComponentStructure[]> = {
|
||||
xbox: xboxControllerStructure,
|
||||
ps: psControllerStructure,
|
||||
nintendo: nintendoControllerStructure
|
||||
};
|
||||
nintendo: nintendoControllerStructure,
|
||||
}
|
||||
|
||||
export const gamepadConfigs: AllGamepadConfigs = {
|
||||
xbox: {
|
||||
@@ -742,7 +677,7 @@ export const gamepadConfigs: AllGamepadConfigs = {
|
||||
{ type: 'button', logicalButton: SELECT, name: 'View', svg: markRaw(XboxViewSvgComp), position: { top: '35%', left: '52%', width: '6%' } },
|
||||
{ type: 'button', logicalButton: START, name: 'Menu', svg: markRaw(XboxMenuSvgComp), position: { top: '35%', left: '62%', width: '6%' } },
|
||||
{ type: 'button', logicalButton: GUIDE, name: 'Guide', svg: markRaw(XboxGuideSvgComp), position: { top: '20%', left: '50%', width: '8%' } },
|
||||
]
|
||||
],
|
||||
},
|
||||
ps: {
|
||||
name: 'PlayStation Controller',
|
||||
@@ -766,7 +701,7 @@ export const gamepadConfigs: AllGamepadConfigs = {
|
||||
{ type: 'button', logicalButton: SELECT, name: 'Create', svg: markRaw(PsCreateSvgComp), position: { top: '35%', left: '30%', width: '6%' } },
|
||||
{ type: 'button', logicalButton: START, name: 'Options', svg: markRaw(PsOptionsSvgComp), position: { top: '35%', left: '70%', width: '6%' } },
|
||||
{ type: 'button', logicalButton: GUIDE, name: 'PS Button', svg: markRaw(PsGuideSvgComp), position: { top: '40%', left: '50%', width: '8%' } },
|
||||
]
|
||||
],
|
||||
},
|
||||
nintendo: {
|
||||
name: 'Nintendo Switch Pro Controller',
|
||||
@@ -789,6 +724,6 @@ export const gamepadConfigs: AllGamepadConfigs = {
|
||||
{ type: 'button', logicalButton: R2, name: 'ZR', svg: markRaw(NintendoZRSvgComp), position: { top: '10%', left: '65%', width: '10%' } },
|
||||
{ type: 'button', logicalButton: SELECT, name: '-', svg: markRaw(NintendoMinusSvgComp), position: { top: '35%', left: '35%', width: '5%' } },
|
||||
{ type: 'button', logicalButton: START, name: '+', svg: markRaw(NintendoPlusSvgComp), position: { top: '35%', left: '65%', width: '5%' } },
|
||||
]
|
||||
}
|
||||
};
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { QueryGetAPI } from '@/api/query'
|
||||
import { useRequest } from 'vue-request'
|
||||
import { NOTIFACTION_API_URL, SONG_REQUEST_API_URL, isBackendUsable } from './constants'
|
||||
import { NotifactionInfo } from '@/api/api-models'
|
||||
import { useAccount } from '@/api/account'
|
||||
import type { NotifactionInfo } from '@/api/api-models'
|
||||
import { ref } from 'vue'
|
||||
import { useAccount } from '@/api/account'
|
||||
import { QueryGetAPI } from '@/api/query'
|
||||
import { isBackendUsable, SONG_REQUEST_API_URL } from './constants'
|
||||
|
||||
const account = useAccount()
|
||||
const n = ref<NotifactionInfo>()
|
||||
let isLoading = false
|
||||
function get() {
|
||||
if (isLoading) return
|
||||
QueryGetAPI<NotifactionInfo>(SONG_REQUEST_API_URL + 'get-active')
|
||||
QueryGetAPI<NotifactionInfo>(`${SONG_REQUEST_API_URL}get-active`)
|
||||
.then((data) => {
|
||||
if (data.code == 200) {
|
||||
n.value = data.data
|
||||
@@ -26,9 +25,9 @@ function get() {
|
||||
}
|
||||
|
||||
export const notifactions = () => n
|
||||
export const GetNotifactions = () => {
|
||||
export function GetNotifactions() {
|
||||
if (account) {
|
||||
//setInterval(get, 5000)
|
||||
//暂时不用
|
||||
// setInterval(get, 5000)
|
||||
// 暂时不用
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { ConfigItemDefinition } from './VTsuruConfigTypes';
|
||||
import type { Component, DefineComponent } from 'vue';
|
||||
import type { Component, DefineComponent } from 'vue'
|
||||
|
||||
/**
|
||||
* OBS 组件定义接口
|
||||
*/
|
||||
export type OBSComponentDefinition = {
|
||||
id: string; // 唯一标识符
|
||||
name: string; // 显示名称
|
||||
description: string; // 组件描述
|
||||
component: DefineComponent<{}, {}, any> | Component; // Vue 组件本身 (异步或同步)
|
||||
settingName?: string; // 用于在数据库中存储配置的名称,可选
|
||||
icon?: string; // 组件图标的路径 (可选)
|
||||
version?: string; // 组件版本 (可选)
|
||||
props?: Record<string, any>; // 传递给组件的额外props (可选)
|
||||
export interface OBSComponentDefinition {
|
||||
id: string // 唯一标识符
|
||||
name: string // 显示名称
|
||||
description: string // 组件描述
|
||||
component: DefineComponent<{}, {}, any> | Component // Vue 组件本身 (异步或同步)
|
||||
settingName?: string // 用于在数据库中存储配置的名称,可选
|
||||
icon?: string // 组件图标的路径 (可选)
|
||||
version?: string // 组件版本 (可选)
|
||||
props?: Record<string, any> // 传递给组件的额外props (可选)
|
||||
// 子组件需要暴露 Config 和 DefaultConfig 才能使用 DynamicForm
|
||||
// Config?: ConfigItemDefinition[];
|
||||
// DefaultConfig?: any;
|
||||
@@ -40,14 +39,14 @@ export const OBSComponentMap: Record<string, OBSComponentDefinition> = {
|
||||
id: 'Example',
|
||||
name: '示例组件',
|
||||
description: '一个基础的OBS组件,用于演示和测试功能。',
|
||||
component: defineAsyncComponent(() => import('@/views/obs_store/components/ExampleOBSComponent.vue')),
|
||||
component: defineAsyncComponent(async () => import('@/views/obs_store/components/ExampleOBSComponent.vue')),
|
||||
version: '1.0.0',
|
||||
},
|
||||
Controller: {
|
||||
id: 'Controller',
|
||||
name: '控制器',
|
||||
description: '将用户手柄操作映射到OBS的场景中',
|
||||
component: defineAsyncComponent(() => import('@/views/obs_store/components/gamepads/GamepadViewer.vue')),
|
||||
component: defineAsyncComponent(async () => import('@/views/obs_store/components/gamepads/GamepadViewer.vue')),
|
||||
version: '1.0.0',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user