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

View File

@@ -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
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(.*)*',
name: 'notfound',
component: import('@/views/NotfoundView.vue'),
component: () => import('@/views/NotfoundView.vue'),
meta: {
title: '页面不存在',
},

View File

@@ -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: '测试页'
}
}
]

View File

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

View File

@@ -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)" />

View File

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

View File

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

View File

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