chore: format code style and update linting configuration

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

View File

@@ -1,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
}
}
}

View File

@@ -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,
)
})
}
}

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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服务')
}
}
}

View File

@@ -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()
}
}

View File

@@ -1,9 +1,9 @@
import {
import type {
ScheduleWeekInfo,
Setting_LiveRequest,
SongRequestInfo,
SongsInfo,
UserInfo
UserInfo,
} from '@/api/api-models'
export interface SongListConfigType {

View File

@@ -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[]

View File

@@ -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'
)
}

View File

@@ -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]
}

View File

@@ -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;
}
`
`

View File

@@ -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)
}

View File

@@ -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%' } },
]
}
};
],
},
}

View File

@@ -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)
// 暂时不用
}
}

View File

@@ -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',
},
};
}