add rtc feature, set payment page to wip state

This commit is contained in:
2024-11-14 15:37:25 +08:00
parent 392a577a7e
commit 45bc8485b3
18 changed files with 544 additions and 52 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -15,6 +15,7 @@
<link rel="preconnect" href="https://rsms.me/" /> <link rel="preconnect" href="https://rsms.me/" />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" /> <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
<script src="https://unpkg.com/peerjs@latest/dist/peerjs.min.js"></script>
</head> </head>
<body> <body>

View File

@@ -11,21 +11,22 @@
"dependencies": { "dependencies": {
"@microsoft/signalr": "^8.0.7", "@microsoft/signalr": "^8.0.7",
"@microsoft/signalr-protocol-msgpack": "^8.0.7", "@microsoft/signalr-protocol-msgpack": "^8.0.7",
"@types/node": "^22.8.2", "@types/node": "^22.9.0",
"@typescript-eslint/eslint-plugin": "^8.12.1", "@typescript-eslint/eslint-plugin": "^8.13.0",
"@vicons/fluent": "^0.12.0", "@vicons/fluent": "^0.12.0",
"@vitejs/plugin-basic-ssl": "^1.1.0",
"@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue": "^5.1.4",
"@vue/cli": "^5.0.8", "@vue/cli": "^5.0.8",
"@vueuse/core": "^11.1.0", "@vueuse/core": "^11.2.0",
"@vueuse/router": "^11.1.0", "@vueuse/router": "^11.2.0",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12", "@wangeditor/editor-for-vue": "^5.1.12",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"easy-speech": "^2.4.0", "easy-speech": "^2.4.0",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"eslint": "^9.13.0", "eslint": "^9.14.0",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-oxlint": "^0.10.1", "eslint-plugin-oxlint": "^0.11.0",
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^5.2.1",
"fast-xml-parser": "^4.5.0", "fast-xml-parser": "^4.5.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
@@ -34,9 +35,10 @@
"linqts": "^2.0.0", "linqts": "^2.0.0",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"music-metadata-browser": "^2.5.11", "music-metadata-browser": "^2.5.11",
"pinia": "^2.2.4", "peerjs": "^1.5.4",
"pinia": "^2.2.6",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"qrcode.vue": "^3.5.1", "qrcode.vue": "^3.6.0",
"queue-typescript": "^1.0.1", "queue-typescript": "^1.0.1",
"unplugin-vue-markdown": "^0.26.2", "unplugin-vue-markdown": "^0.26.2",
"uuid": "^11.0.2", "uuid": "^11.0.2",
@@ -50,16 +52,16 @@
"vue3-aplayer": "^1.7.3", "vue3-aplayer": "^1.7.3",
"vue3-marquee": "^4.2.2", "vue3-marquee": "^4.2.2",
"vueuc": "^0.4.64", "vueuc": "^0.4.64",
"worker-timers": "^8.0.10", "worker-timers": "^8.0.11",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
"@types/bun": "^1.1.12", "@types/bun": "^1.1.13",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"@types/obs-studio": "^2.17.2", "@types/obs-studio": "^2.17.2",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@typescript-eslint/parser": "^8.12.1", "@typescript-eslint/parser": "^8.13.0",
"@vicons/ionicons5": "^0.12.0", "@vicons/ionicons5": "^0.12.0",
"@vitejs/plugin-vue-jsx": "^4.0.1", "@vitejs/plugin-vue-jsx": "^4.0.1",
"@vue/eslint-config-typescript": "^14.1.3", "@vue/eslint-config-typescript": "^14.1.3",

View File

@@ -0,0 +1,49 @@
// src/index.ts
import chalk from 'chalk'
import { spawn } from 'child_process'
// src/utils.ts
import { execSync } from 'child_process'
function validateCaddyIsInstalled() {
let caddyInstalled = false
try {
execSync('caddy version')
caddyInstalled = true
} catch {
caddyInstalled = false
console.error('caddy cli is not installed')
}
return caddyInstalled
}
// src/index.ts
function viteCaddyTlsPlugin(url?:string) {
return {
name: 'vite:caddy-tls',
async configResolved({ command }) {
if (command !== 'serve') return
console.log('starting caddy plugin...')
validateCaddyIsInstalled()
const handle = spawn(
`caddy reverse-proxy ${url ? `--from ${url}` : ''} --to http://localhost:5173`,
{
shell: true
}
)
handle.stdout.on('data', (data) => {
console.log(`stdout: ${data}`)
})
handle.stderr.on('data', () => {})
//const servers = parseNamesFromCaddyFile(`${cwd}/Caddyfile`);
console.log()
console.log(
chalk.green('\u{1F512} Caddy is running to proxy your traffic on https')
)
console.log()
console.log(`\u{1F517} Access your local server `)
console.log(chalk.blue(`\u{1F30D} https://${url ?? 'localhost'}`))
console.log()
}
}
}
export { viteCaddyTlsPlugin as default }

View File

@@ -9,7 +9,6 @@ import { useRoute } from 'vue-router'
export const ACCOUNT = ref<AccountInfo>({} as AccountInfo) export const ACCOUNT = ref<AccountInfo>({} as AccountInfo)
export const isLoadingAccount = ref(true) export const isLoadingAccount = ref(true)
const route = useRoute()
const { message } = createDiscreteApi(['message']) const { message } = createDiscreteApi(['message'])
const cookie = useLocalStorage('JWT_Token', '') const cookie = useLocalStorage('JWT_Token', '')
@@ -47,6 +46,7 @@ export async function GetSelfAccount() {
} }
export function UpdateAccountLoop() { export function UpdateAccountLoop() {
setInterval(() => { setInterval(() => {
const route = useRoute()
if (ACCOUNT.value && route?.name != 'question-display') { if (ACCOUNT.value && route?.name != 'question-display') {
// 防止在问题详情页刷新 // 防止在问题详情页刷新
GetSelfAccount() GetSelfAccount()
@@ -63,47 +63,68 @@ function refreshCookie() {
}) })
} }
export async function SaveAccountSettings() { export async function SaveAccountSettings() {
return await QueryPostAPI(ACCOUNT_API_URL + 'update-setting', ACCOUNT.value?.settings) return await QueryPostAPI(
ACCOUNT_API_URL + 'update-setting',
ACCOUNT.value?.settings
)
} }
export async function SaveEnableFunctions(functions: FunctionTypes[]) { export async function SaveEnableFunctions(functions: FunctionTypes[]) {
return await QueryPostAPI(ACCOUNT_API_URL + 'update-enable-functions', functions) return await QueryPostAPI(
ACCOUNT_API_URL + 'update-enable-functions',
functions
)
} }
export async function SaveSetting( export async function SaveSetting(
name: 'Queue' | 'Point' | 'Index' | 'General' | 'QuestionDisplay' | 'SongRequest' | 'QuestionBox' | 'SendEmail', name:
setting: unknown, | 'Queue'
| 'Point'
| 'Index'
| 'General'
| 'QuestionDisplay'
| 'SongRequest'
| 'QuestionBox'
| 'SendEmail',
setting: unknown
) { ) {
const result = await QueryPostAPIWithParams( const result = await QueryPostAPIWithParams(
ACCOUNT_API_URL + 'update-single-setting', ACCOUNT_API_URL + 'update-single-setting',
{ {
name, name
}, },
setting, setting
) )
return result.message return result.message
} }
export async function UpdateFunctionEnable(func: FunctionTypes) { export async function UpdateFunctionEnable(func: FunctionTypes) {
if (ACCOUNT.value) { if (ACCOUNT.value) {
const oldValue = JSON.parse(JSON.stringify(ACCOUNT.value.settings.enableFunctions)) const oldValue = JSON.parse(
JSON.stringify(ACCOUNT.value.settings.enableFunctions)
)
if (ACCOUNT.value?.settings.enableFunctions.includes(func)) { if (ACCOUNT.value?.settings.enableFunctions.includes(func)) {
ACCOUNT.value.settings.enableFunctions = ACCOUNT.value.settings.enableFunctions.filter((f) => f != func) ACCOUNT.value.settings.enableFunctions =
ACCOUNT.value.settings.enableFunctions.filter((f) => f != func)
} else { } else {
ACCOUNT.value.settings.enableFunctions.push(func) ACCOUNT.value.settings.enableFunctions.push(func)
} }
await SaveEnableFunctions(ACCOUNT.value?.settings.enableFunctions) await SaveEnableFunctions(ACCOUNT.value?.settings.enableFunctions)
.then((data) => { .then((data) => {
if (data.code == 200) { if (data.code == 200) {
message.success(`${ACCOUNT.value?.settings.enableFunctions.includes(func) ? '启用' : '禁用'}`) message.success(
`${ACCOUNT.value?.settings.enableFunctions.includes(func) ? '启用' : '禁用'}`
)
} else { } else {
if (ACCOUNT.value) { if (ACCOUNT.value) {
ACCOUNT.value.settings.enableFunctions = oldValue ACCOUNT.value.settings.enableFunctions = oldValue
} }
message.error( message.error(
`${ACCOUNT.value?.settings.enableFunctions.includes(func) ? '启用' : '禁用'}失败: ${data.message}`, `${ACCOUNT.value?.settings.enableFunctions.includes(func) ? '启用' : '禁用'}失败: ${data.message}`
) )
} }
}) })
.catch((err) => { .catch((err) => {
message.error(`${ACCOUNT.value?.settings.enableFunctions.includes(func) ? '启用' : '禁用'}失败: ${err}`) message.error(
`${ACCOUNT.value?.settings.enableFunctions.includes(func) ? '启用' : '禁用'}失败: ${err}`
)
}) })
} }
} }
@@ -111,74 +132,85 @@ export function useAccount() {
return ACCOUNT return ACCOUNT
} }
export async function Register(name: string, email: string, password: string, token: string): Promise<APIRoot<string>> { export async function Register(
name: string,
email: string,
password: string,
token: string
): Promise<APIRoot<string>> {
return QueryPostAPI<string>(`${ACCOUNT_API_URL}register`, { return QueryPostAPI<string>(`${ACCOUNT_API_URL}register`, {
name, name,
email, email,
password, password,
token, token
}) })
} }
export async function Login(nameOrEmail: string, password: string): Promise<APIRoot<string>> { export async function Login(
nameOrEmail: string,
password: string
): Promise<APIRoot<string>> {
return QueryPostAPI<string>(`${ACCOUNT_API_URL}login`, { return QueryPostAPI<string>(`${ACCOUNT_API_URL}login`, {
nameOrEmail, nameOrEmail,
password, password
}) })
} }
export async function Self(): Promise<APIRoot<AccountInfo>> { export async function Self(): Promise<APIRoot<AccountInfo>> {
return QueryPostAPI<AccountInfo>(`${ACCOUNT_API_URL}self`) return QueryPostAPI<AccountInfo>(`${ACCOUNT_API_URL}self`)
} }
export async function AddBiliBlackList(id: number, name: string): Promise<APIRoot<unknown>> { export async function AddBiliBlackList(
id: number,
name: string
): Promise<APIRoot<unknown>> {
return QueryGetAPI<AccountInfo>(`${ACCOUNT_API_URL}black-list/add-bili`, { return QueryGetAPI<AccountInfo>(`${ACCOUNT_API_URL}black-list/add-bili`, {
id: id, id: id,
name: name, name: name
}) })
} }
export async function DelBiliBlackList(id: number): Promise<APIRoot<unknown>> { export async function DelBiliBlackList(id: number): Promise<APIRoot<unknown>> {
return QueryGetAPI<AccountInfo>(`${ACCOUNT_API_URL}black-list/del-bili`, { return QueryGetAPI<AccountInfo>(`${ACCOUNT_API_URL}black-list/del-bili`, {
id: id, id: id
}) })
} }
export async function DelBlackList(id: number): Promise<APIRoot<unknown>> { export async function DelBlackList(id: number): Promise<APIRoot<unknown>> {
return QueryGetAPI<AccountInfo>(`${ACCOUNT_API_URL}black-list/del`, { return QueryGetAPI<AccountInfo>(`${ACCOUNT_API_URL}black-list/del`, {
id: id, id: id
}) })
} }
export function downloadConfigDirect(name: string) { export function downloadConfigDirect(name: string) {
return QueryGetAPI<string>(VTSURU_API_URL + 'get-config', { return QueryGetAPI<string>(VTSURU_API_URL + 'get-config', {
name: name, name: name
}) })
} }
export async function DownloadConfig<T>(name: string) { export async function DownloadConfig<T>(name: string) {
try { try {
const resp = await QueryGetAPI<string>(VTSURU_API_URL + 'get-config', { const resp = await QueryGetAPI<string>(VTSURU_API_URL + 'get-config', {
name: name, name: name
}) })
if (resp.code == 200) { if (resp.code == 200) {
console.log('已获取配置文件: ' + name) console.log('已获取配置文件: ' + name)
return { return {
msg: undefined, msg: undefined,
data: JSON.parse(resp.data) as T, data: JSON.parse(resp.data) as T
} }
} else if (resp.code == 404) { } else if (resp.code == 404) {
console.error(`未找到名为 ${name} 的配置文件`) console.error(`未找到名为 ${name} 的配置文件`)
return { return {
msg: `未找到名为 ${name} 的配置文件, 需要先上传`, msg: `未找到名为 ${name} 的配置文件, 需要先上传`,
data: undefined, data: undefined
} }
} else { } else {
console.error(`无法获取配置文件 [${name}]: ` + resp.message) console.error(`无法获取配置文件 [${name}]: ` + resp.message)
return { return {
msg: `无法获取配置文件 [${name}]: ` + resp.message, msg: `无法获取配置文件 [${name}]: ` + resp.message,
data: undefined, data: undefined
} }
} }
} catch (err) { } catch (err) {
console.error(`无法获取配置文件 [${name}]: ` + err) console.error(`无法获取配置文件 [${name}]: ` + err)
return { return {
msg: `无法获取配置文件 [${name}]: ` + err, msg: `无法获取配置文件 [${name}]: ` + err,
data: undefined, data: undefined
} }
} }
} }
@@ -186,7 +218,7 @@ export async function UploadConfig(name: string, data: unknown) {
try { try {
const resp = await QueryPostAPI(VTSURU_API_URL + 'set-config', { const resp = await QueryPostAPI(VTSURU_API_URL + 'set-config', {
name: name, name: name,
json: JSON.stringify(data), json: JSON.stringify(data)
}) })
if (resp.code == 200) { if (resp.code == 200) {
console.log('已保存配置文件至服务器:' + name) console.log('已保存配置文件至服务器:' + name)
@@ -208,7 +240,10 @@ export async function EnableFunction(func: FunctionTypes) {
if (await updateFunctionEnable()) { if (await updateFunctionEnable()) {
return true return true
} else { } else {
ACCOUNT.value.settings.enableFunctions.splice(ACCOUNT.value.settings.enableFunctions.indexOf(func), 1) ACCOUNT.value.settings.enableFunctions.splice(
ACCOUNT.value.settings.enableFunctions.indexOf(func),
1
)
return false return false
} }
} }
@@ -220,7 +255,10 @@ export async function DisableFunction(func: FunctionTypes) {
if (!ACCOUNT.value.settings.enableFunctions.includes(func)) { if (!ACCOUNT.value.settings.enableFunctions.includes(func)) {
return true return true
} else { } else {
ACCOUNT.value.settings.enableFunctions.splice(ACCOUNT.value.settings.enableFunctions.indexOf(func), 1) ACCOUNT.value.settings.enableFunctions.splice(
ACCOUNT.value.settings.enableFunctions.indexOf(func),
1
)
if (await updateFunctionEnable()) { if (await updateFunctionEnable()) {
return true return true
} else { } else {
@@ -234,7 +272,9 @@ export async function DisableFunction(func: FunctionTypes) {
async function updateFunctionEnable() { async function updateFunctionEnable() {
if (ACCOUNT.value) { if (ACCOUNT.value) {
try { try {
const data = await SaveEnableFunctions(ACCOUNT.value.settings.enableFunctions) const data = await SaveEnableFunctions(
ACCOUNT.value.settings.enableFunctions
)
if (data.code == 200) { if (data.code == 200) {
return true return true
} else { } else {

192
src/data/RTCClient.ts Normal file
View File

@@ -0,0 +1,192 @@
import { useAccount } from '@/api/account'
import { useVTsuruHub } from '@/store/useVTsuruHub'
import Peer, { DataConnection } from 'peerjs'
import { Ref, ref } from 'vue'
export interface ComponentsEventHubModel {
IsMaster: boolean
Token: string
}
export interface RTCData {
Key: string
Data: any
}
abstract class BaseRTCClient {
constructor(user: string, pass: string) {
this.user = user
this.pass = pass
}
protected user: string
protected pass: string
protected vhub = useVTsuruHub()
public isInited = false
public peer?: Peer
protected connections: DataConnection[] = []
protected events: {
[key: string]: ((args: unknown) => void)[]
} = {}
abstract type: 'master' | 'slave'
public on(eventName: string, listener: (args: unknown) => void) {
eventName = eventName.toLowerCase()
if (!this.events[eventName]) {
this.events[eventName] = []
}
this.events[eventName].push(listener)
}
public off(eventName: string, listener: (args: unknown) => void) {
if (this.events[eventName]) {
const index = this.events[eventName].indexOf(listener)
if (index > -1) {
this.events[eventName].splice(index, 1)
}
}
}
public send(eventName: string, data: unknown) {
this.connections.forEach((item) =>
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: [
{ urls: 'stun:turn.suki.club' },
{
urls: '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(data: RTCData) {
//console.log(data)
if (data.Key == 'Heartbeat') return
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)
console.log(
`[Components-Event] <${this.connections.length}> ${this.type == 'master' ? 'Slave' : 'Master'} 下线: ` +
id
)
}
public Init() {
if (!this.isInited) {
this.isInited = true
this.connectRTC()
}
this.vhub.on('RTCOffline', (id: string) => this.onConnectionClose(id))
return this
}
}
export class SlaveRTCClient extends BaseRTCClient {
constructor(user: string, pass: string) {
super(user, pass)
}
type: 'slave' = 'slave' as const
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)
)
masters.forEach((item) => {
this.connectToMaster(item.Token)
//console.log('[Components-Event] 正在连接到现有 Master: ' + item.Token)
})
}
public connectToMaster(token: string) {
if (this.connections.some((conn) => conn.peer == token)) return
const c = this.peer?.connect(token)
c?.on('open', () => {
this.connections.push(c)
console.log(
`[Components-Event] <${this.connections.length}> ==> Master 连接已建立: ` +
token
)
})
c?.on('data', (data) => this.processData(data as RTCData))
c?.on('close', () => this.onConnectionClose(c.peer))
}
public Init() {
super.Init()
this.vhub?.on('MasterOnline', (data: string) => this.connectToMaster(data))
setTimeout(() => {
this.connectToAllMaster()
}, 500)
setInterval(() => {
this.connectToAllMaster()
}, 30000)
return this
}
}
export class MasterRTCClient extends BaseRTCClient {
constructor(user: string, pass: string) {
super(user, pass)
}
type: 'master' = 'master' as const
public connectRTC() {
super.connectRTC()
this.peer?.on('connection', (conn) => {
conn.on('open', () => {
this.connections.push(conn)
console.log(
`[Components-Event] <${this.connections.length}> Slave 上线: ` +
conn.peer
)
})
conn.on('data', (data) => this.processData(data as RTCData))
conn.on('error', (err) => console.error(err))
conn.on('close', () => this.onConnectionClose(conn.peer))
})
}
public Init() {
return super.Init()
}
}

View File

@@ -97,7 +97,7 @@ const routes: Array<RouteRecordRaw> = [
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: 'notfound', name: 'notfound',
component: import('@/views/NotfoundView.vue'), component: () => import('@/views/NotfoundView.vue'),
meta: { meta: {
title: '页面不存在', title: '页面不存在',
}, },

View File

@@ -4,7 +4,15 @@ export default [
name: 'question-display', name: 'question-display',
component: () => import('@/views/single/QuestionDisplay.vue'), component: () => import('@/views/single/QuestionDisplay.vue'),
meta: { meta: {
title: '棉花糖展示页', title: '棉花糖展示页'
}, }
}, },
{
path: '/playground/test',
name: 'test',
component: () => import('@/views/TestView.vue'),
meta: {
title: '测试页'
}
}
] ]

View File

@@ -36,6 +36,7 @@ export const useAuthStore = defineStore('BiliAuth', () => {
async function getAuthInfo() { async function getAuthInfo() {
try { try {
isLoading.value = true isLoading.value = true
if(!currentToken.value) return
await QueryBiliAuthGetAPI<BiliAuthModel>(BILI_AUTH_API_URL + 'info').then((data) => { await QueryBiliAuthGetAPI<BiliAuthModel>(BILI_AUTH_API_URL + 'info').then((data) => {
if (data.code == 200) { if (data.code == 200) {
biliAuth.value = data.data biliAuth.value = data.data

44
src/store/useRTC.ts Normal file
View File

@@ -0,0 +1,44 @@
import { useAccount } from '@/api/account'
import { MasterRTCClient, SlaveRTCClient } from '@/data/RTCClient'
import { acceptHMRUpdate, defineStore } from 'pinia'
import { ref } from 'vue'
export const useWebRTC = defineStore('WebRTC', () => {
const masterClient = ref<MasterRTCClient>()
const slaveClient = ref<SlaveRTCClient>()
const accountInfo = useAccount()
function Init(type: 'master' | 'slave') {
if (type == 'master') {
if (masterClient.value) {
return masterClient
} else {
masterClient.value = new MasterRTCClient(
accountInfo.value.id.toString(),
accountInfo.value.token
)
masterClient.value.Init()
return masterClient
}
} else {
if (slaveClient.value) {
return slaveClient
} else {
slaveClient.value = new SlaveRTCClient(
accountInfo.value.id.toString(),
accountInfo.value.token
)
slaveClient.value.Init()
return slaveClient
}
}
}
return {
Init
}
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useWebRTC, import.meta.hot))
}

106
src/store/useVTsuruHub.ts Normal file
View File

@@ -0,0 +1,106 @@
import { useAccount } from '@/api/account'
import { BASE_HUB_URL } from '@/data/constants'
import {
HttpTransportType,
HubConnectionBuilder,
LogLevel
} from '@microsoft/signalr'
import { MessagePackHubProtocol } from '@microsoft/signalr-protocol-msgpack'
import { acceptHMRUpdate, defineStore } from 'pinia'
import { ref } from 'vue'
export const useVTsuruHub = defineStore('VTsuruHub', () => {
const accountInfo = useAccount()
const signalRClient = ref<signalR.HubConnection>()
const isInited = ref(false)
const isIniting = ref(false)
async function connectSignalR() {
if (isIniting.value) return
isIniting.value = true
//console.log('[Components-Event] 正在连接到 VTsuru 服务器...')
const connection = new HubConnectionBuilder()
.withUrl(BASE_HUB_URL + 'main?token=' + accountInfo.value.token, {
skipNegotiation: true,
transport: HttpTransportType.WebSockets,
logger: LogLevel.Error
})
.withAutomaticReconnect([0, 2000, 10000, 30000])
.withHubProtocol(new MessagePackHubProtocol())
.build()
connection.on('Finished', async () => {
connection.send('Finished')
})
connection.on('Disconnect', (reason: unknown) => {
console.log('[Hub] 被 VTsuru 服务器断开连接: ' + reason)
})
connection.onclose(reconnect)
try {
await connection.start()
console.log('[Hub] 已连接到 VTsuru 服务器')
signalRClient.value = connection
isInited.value = true
return true
} catch (e) {
console.log('[Hub] 无法连接到 VTsuru 服务器: ' + e)
return false
} finally {
isIniting.value = false
}
}
async function reconnect() {
try {
await signalRClient.value?.start()
signalRClient.value?.send('Reconnected')
console.log('[Hub] 已重新连接')
} catch (err) {
console.log(err)
setTimeout(reconnect, 5000) // 如果连接失败则每5秒尝试一次重新启动连接
}
}
async function send(methodName: string, ...args: any[]) {
if (!isInited.value) {
await connectSignalR()
}
signalRClient.value?.send(methodName, ...args)
}
async function invoke<T>(methodName: string, ...args: any[]) {
if (!isInited.value) {
await connectSignalR()
}
return signalRClient.value?.invoke<T>(methodName, ...args)
}
async function on(eventName: string, listener: (args: any) => any) {
if (!isInited.value) {
await connectSignalR()
}
signalRClient.value?.on(eventName, listener)
}
async function off(eventName: string, listener: (args: any) => any) {
if (!isInited.value) {
await connectSignalR()
}
signalRClient.value?.off(eventName, listener)
}
async function onreconnected(listener: (id: any) => any) {
if (!isInited.value) {
await connectSignalR()
}
signalRClient.value?.onreconnected(listener)
}
function Init() {
if (!isInited.value) {
connectSignalR()
}
return useVTsuruHub()
}
return { signalRClient, Init, send, invoke, on, off, onreconnected }
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useVTsuruHub, import.meta.hot))
}

36
src/views/TestView.vue Normal file
View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import { useAccount } from '@/api/account';
import { MasterRTCClient, SlaveRTCClient } from '@/data/RTCClient';
import { useWebRTC } from '@/store/useRTC';
import { NButton, NInput, NSpin } from 'naive-ui';
import { LogLevel, Peer } from 'peerjs';
import { computed, onMounted, Ref, ref } from 'vue';
import { useRoute } from 'vue-router';
const target = ref('');
const accountInfo = useAccount()
const route = useRoute()
const inputMsg = ref('')
const isMaster = computed(() => {
return route.query.slave == null || route.query.slave == undefined
})
let rtc: Ref<MasterRTCClient | undefined, MasterRTCClient | undefined> | Ref<SlaveRTCClient | undefined, SlaveRTCClient | undefined>
function mount() {
rtc = useWebRTC().Init(isMaster.value ? 'master' : 'slave')
}
</script>
<template>
<NSpin show v-if="!accountInfo.id" />
<div v-else @vue:mounted="mount">
master: {{ isMaster }}
{{ rtc?.peer?.id }}
<template v-if="isMaster">
<NInput v-model:value="inputMsg" />
<NButton @click="rtc.send('test', inputMsg)"> 发送 </NButton>
</template>
</div>
</template>

View File

@@ -486,6 +486,8 @@ onUnmounted(() => {
</NTooltip> </NTooltip>
</NInputGroup> </NInputGroup>
</NSpace> </NSpace>
<VueTurnstile ref="turnstile" :site-key="TURNSTILE_KEY" v-model="token" theme="auto" style="text-align: center" />
<template #footer> <template #footer>
<NButton @click="accountInfo?.isBiliVerified ? ChangeBili() : BindBili()" type="success" <NButton @click="accountInfo?.isBiliVerified ? ChangeBili() : BindBili()" type="success"
:loading="!token || isLoading"> :loading="!token || isLoading">
@@ -522,5 +524,4 @@ onUnmounted(() => {
<NButton @click="BindBiliAuth()" type="success" :loading="isLoading" :disabled="!biliAuthText"> 确定 </NButton> <NButton @click="BindBiliAuth()" type="success" :loading="isLoading" :disabled="!biliAuthText"> 确定 </NButton>
</template> </template>
</NModal> </NModal>
<VueTurnstile ref="turnstile" :site-key="TURNSTILE_KEY" v-model="token" theme="auto" style="text-align: center" />
</template> </template>

View File

@@ -36,7 +36,10 @@ onMounted(() => {
</script> </script>
<template> <template>
<NTabs animated type="line"> <div v-if="true">
WIP...
</div>
<NTabs v-else animated type="line">
<NTabPane name="弹幕储存" tab="弹幕储存"> <NTabPane name="弹幕储存" tab="弹幕储存">
<template #tab> <template #tab>
<component :is="tabDisplay(ConsumptionTypes.DanmakuStorage)" /> <component :is="tabDisplay(ConsumptionTypes.DanmakuStorage)" />

View File

@@ -2,6 +2,7 @@
import { DanmakuUserInfo, SongsInfo } from '@/api/api-models' import { DanmakuUserInfo, SongsInfo } from '@/api/api-models'
import { QueryGetAPI } from '@/api/query' import { QueryGetAPI } from '@/api/query'
import { AVATAR_URL, MUSIC_REQUEST_API_URL } from '@/data/constants' import { AVATAR_URL, MUSIC_REQUEST_API_URL } from '@/data/constants'
import { useWebRTC } from '@/store/useRTC'
import { useElementSize } from '@vueuse/core' import { useElementSize } from '@vueuse/core'
import { NDivider, NEmpty, useMessage } from 'naive-ui' import { NDivider, NEmpty, useMessage } from 'naive-ui'
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
@@ -22,6 +23,7 @@ const route = useRoute()
const currentId = computed(() => { const currentId = computed(() => {
return props.id ?? route.query.id return props.id ?? route.query.id
}) })
const rtc = useWebRTC().Init('slave')
const listContainerRef = ref() const listContainerRef = ref()
const footerRef = ref() const footerRef = ref()

View File

@@ -5,9 +5,11 @@ import { QUESTION_API_URL } from '@/data/constants'
import { useRouteQuery } from '@vueuse/router' import { useRouteQuery } from '@vueuse/router'
import { onMounted, onUnmounted, ref } from 'vue' import { onMounted, onUnmounted, ref } from 'vue'
import QuestionDisplayCard from '../manage/QuestionDisplayCard.vue' import QuestionDisplayCard from '../manage/QuestionDisplayCard.vue'
import { useWebRTC } from '@/store/useRTC'
const hash = ref('') const hash = ref('')
const token = useRouteQuery('token') const token = useRouteQuery('token')
const rtc = useWebRTC().Init('slave')
const question = ref<QAInfo>() const question = ref<QAInfo>()
const setting = ref<Setting_QuestionDisplay>({} as Setting_QuestionDisplay) const setting = ref<Setting_QuestionDisplay>({} as Setting_QuestionDisplay)

View File

@@ -18,6 +18,7 @@ import { Vue3Marquee } from 'vue3-marquee'
import { NCard, NDivider, NEmpty, NSpace, NText, useMessage } from 'naive-ui' import { NCard, NDivider, NEmpty, NSpace, NText, useMessage } from 'naive-ui'
import { List } from 'linqts' import { List } from 'linqts'
import { isSameDay } from 'date-fns' import { isSameDay } from 'date-fns'
import { useWebRTC } from '@/store/useRTC'
const props = defineProps<{ const props = defineProps<{
id?: number id?: number
@@ -28,6 +29,7 @@ const route = useRoute()
const currentId = computed(() => { const currentId = computed(() => {
return props.id ?? route.query.id return props.id ?? route.query.id
}) })
const rtc = useWebRTC().Init('slave')
const listContainerRef = ref() const listContainerRef = ref()
const footerRef = ref() const footerRef = ref()

View File

@@ -5,31 +5,34 @@ import path from 'path'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import svgLoader from 'vite-svg-loader' import svgLoader from 'vite-svg-loader'
import Markdown from 'unplugin-vue-markdown/vite' import Markdown from 'unplugin-vue-markdown/vite'
import caddyTls from './plugins/vite-plugin-caddy'
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue({ vue({
script: { script: {
propsDestructure: true, propsDestructure: true,
defineModel: true, defineModel: true
}, },
include: [/\.vue$/, /\.md$/], include: [/\.vue$/, /\.md$/]
}), }),
svgLoader(), svgLoader(),
vueJsx(), vueJsx(),
Markdown({ Markdown({
/* options */ /* options */
}), }),
caddyTls()
], ],
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, 'src'), '@': path.resolve(__dirname, 'src')
}, }
}, },
define: { define: {
'process.env': {}, 'process.env': {},
global: 'window'
}, },
optimizeDeps: { optimizeDeps: {
include: ['@vicons/fluent', '@vicons/ionicons5', 'vue', 'vue-router'], include: ['@vicons/fluent', '@vicons/ionicons5', 'vue', 'vue-router']
}, }
}) })