mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
add rtc feature, set payment page to wip state
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
|
||||
<link rel="preconnect" href="https://rsms.me/" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
<script src="https://unpkg.com/peerjs@latest/dist/peerjs.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
24
package.json
24
package.json
@@ -11,21 +11,22 @@
|
||||
"dependencies": {
|
||||
"@microsoft/signalr": "^8.0.7",
|
||||
"@microsoft/signalr-protocol-msgpack": "^8.0.7",
|
||||
"@types/node": "^22.8.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.12.1",
|
||||
"@types/node": "^22.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.13.0",
|
||||
"@vicons/fluent": "^0.12.0",
|
||||
"@vitejs/plugin-basic-ssl": "^1.1.0",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vue/cli": "^5.0.8",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"@vueuse/router": "^11.1.0",
|
||||
"@vueuse/core": "^11.2.0",
|
||||
"@vueuse/router": "^11.2.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"date-fns": "^4.1.0",
|
||||
"easy-speech": "^2.4.0",
|
||||
"echarts": "^5.5.1",
|
||||
"eslint": "^9.13.0",
|
||||
"eslint": "^9.14.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",
|
||||
"fast-xml-parser": "^4.5.0",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -34,9 +35,10 @@
|
||||
"linqts": "^2.0.0",
|
||||
"mitt": "^3.0.1",
|
||||
"music-metadata-browser": "^2.5.11",
|
||||
"pinia": "^2.2.4",
|
||||
"peerjs": "^1.5.4",
|
||||
"pinia": "^2.2.6",
|
||||
"prettier": "^3.3.3",
|
||||
"qrcode.vue": "^3.5.1",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"queue-typescript": "^1.0.1",
|
||||
"unplugin-vue-markdown": "^0.26.2",
|
||||
"uuid": "^11.0.2",
|
||||
@@ -50,16 +52,16 @@
|
||||
"vue3-aplayer": "^1.7.3",
|
||||
"vue3-marquee": "^4.2.2",
|
||||
"vueuc": "^0.4.64",
|
||||
"worker-timers": "^8.0.10",
|
||||
"worker-timers": "^8.0.11",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@types/bun": "^1.1.12",
|
||||
"@types/bun": "^1.1.13",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/obs-studio": "^2.17.2",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/parser": "^8.12.1",
|
||||
"@typescript-eslint/parser": "^8.13.0",
|
||||
"@vicons/ionicons5": "^0.12.0",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||
"@vue/eslint-config-typescript": "^14.1.3",
|
||||
|
||||
49
plugins/vite-plugin-caddy.ts
Normal file
49
plugins/vite-plugin-caddy.ts
Normal 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 }
|
||||
@@ -9,7 +9,6 @@ import { useRoute } from 'vue-router'
|
||||
|
||||
export const ACCOUNT = ref<AccountInfo>({} as AccountInfo)
|
||||
export const isLoadingAccount = ref(true)
|
||||
const route = useRoute()
|
||||
|
||||
const { message } = createDiscreteApi(['message'])
|
||||
const cookie = useLocalStorage('JWT_Token', '')
|
||||
@@ -47,6 +46,7 @@ export async function GetSelfAccount() {
|
||||
}
|
||||
export function UpdateAccountLoop() {
|
||||
setInterval(() => {
|
||||
const route = useRoute()
|
||||
if (ACCOUNT.value && route?.name != 'question-display') {
|
||||
// 防止在问题详情页刷新
|
||||
GetSelfAccount()
|
||||
@@ -63,47 +63,68 @@ function refreshCookie() {
|
||||
})
|
||||
}
|
||||
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[]) {
|
||||
return await QueryPostAPI(ACCOUNT_API_URL + 'update-enable-functions', functions)
|
||||
return await QueryPostAPI(
|
||||
ACCOUNT_API_URL + 'update-enable-functions',
|
||||
functions
|
||||
)
|
||||
}
|
||||
export async function SaveSetting(
|
||||
name: 'Queue' | 'Point' | 'Index' | 'General' | 'QuestionDisplay' | 'SongRequest' | 'QuestionBox' | 'SendEmail',
|
||||
setting: unknown,
|
||||
name:
|
||||
| 'Queue'
|
||||
| 'Point'
|
||||
| 'Index'
|
||||
| 'General'
|
||||
| 'QuestionDisplay'
|
||||
| 'SongRequest'
|
||||
| 'QuestionBox'
|
||||
| 'SendEmail',
|
||||
setting: unknown
|
||||
) {
|
||||
const result = await QueryPostAPIWithParams(
|
||||
ACCOUNT_API_URL + 'update-single-setting',
|
||||
{
|
||||
name,
|
||||
name
|
||||
},
|
||||
setting,
|
||||
setting
|
||||
)
|
||||
return result.message
|
||||
}
|
||||
export async function UpdateFunctionEnable(func: FunctionTypes) {
|
||||
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)) {
|
||||
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 {
|
||||
ACCOUNT.value.settings.enableFunctions.push(func)
|
||||
}
|
||||
await SaveEnableFunctions(ACCOUNT.value?.settings.enableFunctions)
|
||||
.then((data) => {
|
||||
if (data.code == 200) {
|
||||
message.success(`已${ACCOUNT.value?.settings.enableFunctions.includes(func) ? '启用' : '禁用'}`)
|
||||
message.success(
|
||||
`已${ACCOUNT.value?.settings.enableFunctions.includes(func) ? '启用' : '禁用'}`
|
||||
)
|
||||
} else {
|
||||
if (ACCOUNT.value) {
|
||||
ACCOUNT.value.settings.enableFunctions = oldValue
|
||||
}
|
||||
message.error(
|
||||
`${ACCOUNT.value?.settings.enableFunctions.includes(func) ? '启用' : '禁用'}失败: ${data.message}`,
|
||||
`${ACCOUNT.value?.settings.enableFunctions.includes(func) ? '启用' : '禁用'}失败: ${data.message}`
|
||||
)
|
||||
}
|
||||
})
|
||||
.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
|
||||
}
|
||||
|
||||
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`, {
|
||||
name,
|
||||
email,
|
||||
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`, {
|
||||
nameOrEmail,
|
||||
password,
|
||||
password
|
||||
})
|
||||
}
|
||||
export async function Self(): Promise<APIRoot<AccountInfo>> {
|
||||
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`, {
|
||||
id: id,
|
||||
name: name,
|
||||
name: name
|
||||
})
|
||||
}
|
||||
export async function DelBiliBlackList(id: number): Promise<APIRoot<unknown>> {
|
||||
return QueryGetAPI<AccountInfo>(`${ACCOUNT_API_URL}black-list/del-bili`, {
|
||||
id: id,
|
||||
id: id
|
||||
})
|
||||
}
|
||||
export async function DelBlackList(id: number): Promise<APIRoot<unknown>> {
|
||||
return QueryGetAPI<AccountInfo>(`${ACCOUNT_API_URL}black-list/del`, {
|
||||
id: id,
|
||||
id: id
|
||||
})
|
||||
}
|
||||
export function downloadConfigDirect(name: string) {
|
||||
return QueryGetAPI<string>(VTSURU_API_URL + 'get-config', {
|
||||
name: name,
|
||||
name: name
|
||||
})
|
||||
}
|
||||
export async function DownloadConfig<T>(name: string) {
|
||||
try {
|
||||
const resp = await QueryGetAPI<string>(VTSURU_API_URL + 'get-config', {
|
||||
name: name,
|
||||
name: name
|
||||
})
|
||||
if (resp.code == 200) {
|
||||
console.log('已获取配置文件: ' + name)
|
||||
return {
|
||||
msg: undefined,
|
||||
data: JSON.parse(resp.data) as T,
|
||||
data: JSON.parse(resp.data) as T
|
||||
}
|
||||
} else if (resp.code == 404) {
|
||||
console.error(`未找到名为 ${name} 的配置文件`)
|
||||
return {
|
||||
msg: `未找到名为 ${name} 的配置文件, 需要先上传`,
|
||||
data: undefined,
|
||||
data: undefined
|
||||
}
|
||||
} else {
|
||||
console.error(`无法获取配置文件 [${name}]: ` + resp.message)
|
||||
return {
|
||||
msg: `无法获取配置文件 [${name}]: ` + resp.message,
|
||||
data: undefined,
|
||||
data: undefined
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`无法获取配置文件 [${name}]: ` + err)
|
||||
return {
|
||||
msg: `无法获取配置文件 [${name}]: ` + err,
|
||||
data: undefined,
|
||||
data: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,7 +218,7 @@ export async function UploadConfig(name: string, data: unknown) {
|
||||
try {
|
||||
const resp = await QueryPostAPI(VTSURU_API_URL + 'set-config', {
|
||||
name: name,
|
||||
json: JSON.stringify(data),
|
||||
json: JSON.stringify(data)
|
||||
})
|
||||
if (resp.code == 200) {
|
||||
console.log('已保存配置文件至服务器:' + name)
|
||||
@@ -208,7 +240,10 @@ export async function EnableFunction(func: FunctionTypes) {
|
||||
if (await updateFunctionEnable()) {
|
||||
return true
|
||||
} 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
|
||||
}
|
||||
}
|
||||
@@ -220,7 +255,10 @@ export async function DisableFunction(func: FunctionTypes) {
|
||||
if (!ACCOUNT.value.settings.enableFunctions.includes(func)) {
|
||||
return true
|
||||
} 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()) {
|
||||
return true
|
||||
} else {
|
||||
@@ -234,7 +272,9 @@ export async function DisableFunction(func: FunctionTypes) {
|
||||
async function updateFunctionEnable() {
|
||||
if (ACCOUNT.value) {
|
||||
try {
|
||||
const data = await SaveEnableFunctions(ACCOUNT.value.settings.enableFunctions)
|
||||
const data = await SaveEnableFunctions(
|
||||
ACCOUNT.value.settings.enableFunctions
|
||||
)
|
||||
if (data.code == 200) {
|
||||
return true
|
||||
} else {
|
||||
|
||||
192
src/data/RTCClient.ts
Normal file
192
src/data/RTCClient.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'notfound',
|
||||
component: import('@/views/NotfoundView.vue'),
|
||||
component: () => import('@/views/NotfoundView.vue'),
|
||||
meta: {
|
||||
title: '页面不存在',
|
||||
},
|
||||
|
||||
@@ -4,7 +4,15 @@ export default [
|
||||
name: 'question-display',
|
||||
component: () => import('@/views/single/QuestionDisplay.vue'),
|
||||
meta: {
|
||||
title: '棉花糖展示页',
|
||||
},
|
||||
title: '棉花糖展示页'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/playground/test',
|
||||
name: 'test',
|
||||
component: () => import('@/views/TestView.vue'),
|
||||
meta: {
|
||||
title: '测试页'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -36,6 +36,7 @@ export const useAuthStore = defineStore('BiliAuth', () => {
|
||||
async function getAuthInfo() {
|
||||
try {
|
||||
isLoading.value = true
|
||||
if(!currentToken.value) return
|
||||
await QueryBiliAuthGetAPI<BiliAuthModel>(BILI_AUTH_API_URL + 'info').then((data) => {
|
||||
if (data.code == 200) {
|
||||
biliAuth.value = data.data
|
||||
|
||||
44
src/store/useRTC.ts
Normal file
44
src/store/useRTC.ts
Normal 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
106
src/store/useVTsuruHub.ts
Normal 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
36
src/views/TestView.vue
Normal 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>
|
||||
@@ -486,6 +486,8 @@ onUnmounted(() => {
|
||||
</NTooltip>
|
||||
</NInputGroup>
|
||||
</NSpace>
|
||||
|
||||
<VueTurnstile ref="turnstile" :site-key="TURNSTILE_KEY" v-model="token" theme="auto" style="text-align: center" />
|
||||
<template #footer>
|
||||
<NButton @click="accountInfo?.isBiliVerified ? ChangeBili() : BindBili()" type="success"
|
||||
:loading="!token || isLoading">
|
||||
@@ -522,5 +524,4 @@ onUnmounted(() => {
|
||||
<NButton @click="BindBiliAuth()" type="success" :loading="isLoading" :disabled="!biliAuthText"> 确定 </NButton>
|
||||
</template>
|
||||
</NModal>
|
||||
<VueTurnstile ref="turnstile" :site-key="TURNSTILE_KEY" v-model="token" theme="auto" style="text-align: center" />
|
||||
</template>
|
||||
|
||||
@@ -36,7 +36,10 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NTabs animated type="line">
|
||||
<div v-if="true">
|
||||
WIP...
|
||||
</div>
|
||||
<NTabs v-else animated type="line">
|
||||
<NTabPane name="弹幕储存" tab="弹幕储存">
|
||||
<template #tab>
|
||||
<component :is="tabDisplay(ConsumptionTypes.DanmakuStorage)" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { DanmakuUserInfo, SongsInfo } from '@/api/api-models'
|
||||
import { QueryGetAPI } from '@/api/query'
|
||||
import { AVATAR_URL, MUSIC_REQUEST_API_URL } from '@/data/constants'
|
||||
import { useWebRTC } from '@/store/useRTC'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { NDivider, NEmpty, useMessage } from 'naive-ui'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
@@ -22,6 +23,7 @@ const route = useRoute()
|
||||
const currentId = computed(() => {
|
||||
return props.id ?? route.query.id
|
||||
})
|
||||
const rtc = useWebRTC().Init('slave')
|
||||
|
||||
const listContainerRef = ref()
|
||||
const footerRef = ref()
|
||||
|
||||
@@ -5,9 +5,11 @@ import { QUESTION_API_URL } from '@/data/constants'
|
||||
import { useRouteQuery } from '@vueuse/router'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import QuestionDisplayCard from '../manage/QuestionDisplayCard.vue'
|
||||
import { useWebRTC } from '@/store/useRTC'
|
||||
|
||||
const hash = ref('')
|
||||
const token = useRouteQuery('token')
|
||||
const rtc = useWebRTC().Init('slave')
|
||||
|
||||
const question = ref<QAInfo>()
|
||||
const setting = ref<Setting_QuestionDisplay>({} as Setting_QuestionDisplay)
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Vue3Marquee } from 'vue3-marquee'
|
||||
import { NCard, NDivider, NEmpty, NSpace, NText, useMessage } from 'naive-ui'
|
||||
import { List } from 'linqts'
|
||||
import { isSameDay } from 'date-fns'
|
||||
import { useWebRTC } from '@/store/useRTC'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number
|
||||
@@ -28,6 +29,7 @@ const route = useRoute()
|
||||
const currentId = computed(() => {
|
||||
return props.id ?? route.query.id
|
||||
})
|
||||
const rtc = useWebRTC().Init('slave')
|
||||
|
||||
const listContainerRef = ref()
|
||||
const footerRef = ref()
|
||||
|
||||
@@ -5,31 +5,34 @@ import path from 'path'
|
||||
import { defineConfig } from 'vite'
|
||||
import svgLoader from 'vite-svg-loader'
|
||||
import Markdown from 'unplugin-vue-markdown/vite'
|
||||
import caddyTls from './plugins/vite-plugin-caddy'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue({
|
||||
script: {
|
||||
propsDestructure: true,
|
||||
defineModel: true,
|
||||
defineModel: true
|
||||
},
|
||||
include: [/\.vue$/, /\.md$/],
|
||||
include: [/\.vue$/, /\.md$/]
|
||||
}),
|
||||
svgLoader(),
|
||||
vueJsx(),
|
||||
Markdown({
|
||||
/* options */
|
||||
}),
|
||||
caddyTls()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
},
|
||||
'@': path.resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
define: {
|
||||
'process.env': {},
|
||||
global: 'window'
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['@vicons/fluent', '@vicons/ionicons5', 'vue', 'vue-router'],
|
||||
},
|
||||
include: ['@vicons/fluent', '@vicons/ionicons5', 'vue', 'vue-router']
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user