mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-07 02:46:55 +08:00
update open live page
This commit is contained in:
@@ -4,14 +4,15 @@
|
|||||||
<NConfigProvider :theme-overrides="themeOverrides" :theme="theme" style="height: 100vh" :locale="zhCN" :date-locale="dateZhCN">
|
<NConfigProvider :theme-overrides="themeOverrides" :theme="theme" style="height: 100vh" :locale="zhCN" :date-locale="dateZhCN">
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<div style="height: 100vh">
|
<div style="height: 100vh">
|
||||||
<NElement style="height: 100%;" v-if="layout != 'obs'">
|
<NElement style="height: 100%" v-if="layout != 'obs'">
|
||||||
<ViewerLayout v-if="layout == 'viewer'" />
|
<ViewerLayout v-if="layout == 'viewer'" />
|
||||||
<ManageLayout v-else-if="layout == 'manage'" />
|
<ManageLayout v-else-if="layout == 'manage'" />
|
||||||
|
<OpenLiveLayout v-else-if="layout == 'open-live'" />
|
||||||
<template v-else-if="layout == ''">
|
<template v-else-if="layout == ''">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</template>
|
</template>
|
||||||
</NElement>
|
</NElement>
|
||||||
<RouterView v-else/>
|
<RouterView v-else />
|
||||||
</div>
|
</div>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<NSpin size="large" show />
|
<NSpin size="large" show />
|
||||||
@@ -31,6 +32,7 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { ThemeType, UserInfo } from './api/api-models'
|
import { ThemeType, UserInfo } from './api/api-models'
|
||||||
import { useUser } from './api/user'
|
import { useUser } from './api/user'
|
||||||
|
import OpenLiveLayout from './views/OpenLiveLayout.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
@@ -41,6 +43,9 @@ const layout = computed(() => {
|
|||||||
} else if (route.path.startsWith('/manage')) {
|
} else if (route.path.startsWith('/manage')) {
|
||||||
document.title = route.meta.title + ' · 管理 · VTsuru'
|
document.title = route.meta.title + ' · 管理 · VTsuru'
|
||||||
return 'manage'
|
return 'manage'
|
||||||
|
} else if (route.path.startsWith('/open-live')) {
|
||||||
|
document.title = route.meta.title + ' · 开放平台 · VTsuru'
|
||||||
|
return 'open-live'
|
||||||
} else if (route.path.startsWith('/obs')) {
|
} else if (route.path.startsWith('/obs')) {
|
||||||
document.title = route.meta.title + ' · OBS · VTsuru'
|
document.title = route.meta.title + ' · OBS · VTsuru'
|
||||||
return 'obs'
|
return 'obs'
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import { QueryPostAPI } from '@/api/query'
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useLocalStorage } from '@vueuse/core'
|
import { useLocalStorage } from '@vueuse/core'
|
||||||
import { createDiscreteApi } from 'naive-ui'
|
import { createDiscreteApi } from 'naive-ui'
|
||||||
|
import { isSameDay } from 'date-fns'
|
||||||
|
|
||||||
export const ACCOUNT = ref<AccountInfo>()
|
export const ACCOUNT = ref<AccountInfo>()
|
||||||
export const isLoadingAccount = ref(true)
|
export const isLoadingAccount = ref(true)
|
||||||
|
|
||||||
const { message } = createDiscreteApi(['message'])
|
const { message } = createDiscreteApi(['message'])
|
||||||
const cookie = useLocalStorage('JWT_Token', '')
|
const cookie = useLocalStorage('JWT_Token', '')
|
||||||
|
const cookieRefreshDate = useLocalStorage('JWT_Token_Last_Refresh', Date.now())
|
||||||
|
|
||||||
export async function GetSelfAccount() {
|
export async function GetSelfAccount() {
|
||||||
if (cookie.value) {
|
if (cookie.value) {
|
||||||
@@ -18,6 +20,9 @@ export async function GetSelfAccount() {
|
|||||||
ACCOUNT.value = result.data
|
ACCOUNT.value = result.data
|
||||||
isLoadingAccount.value = false
|
isLoadingAccount.value = false
|
||||||
console.log('[vtsuru] 已获取账户信息')
|
console.log('[vtsuru] 已获取账户信息')
|
||||||
|
if (!isSameDay(new Date(), cookieRefreshDate.value)) {
|
||||||
|
refreshCookie()
|
||||||
|
}
|
||||||
return result.data
|
return result.data
|
||||||
} else if (result.code == 401) {
|
} else if (result.code == 401) {
|
||||||
localStorage.removeItem('JWT_Token')
|
localStorage.removeItem('JWT_Token')
|
||||||
@@ -25,7 +30,7 @@ export async function GetSelfAccount() {
|
|||||||
message.error('Cookie 已失效, 需要重新登陆')
|
message.error('Cookie 已失效, 需要重新登陆')
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
location.reload()
|
location.reload()
|
||||||
}, 1500);
|
}, 1500)
|
||||||
} else {
|
} else {
|
||||||
console.warn('[vtsuru] ' + result.message)
|
console.warn('[vtsuru] ' + result.message)
|
||||||
message.error(result.message)
|
message.error(result.message)
|
||||||
@@ -33,6 +38,15 @@ export async function GetSelfAccount() {
|
|||||||
}
|
}
|
||||||
isLoadingAccount.value = false
|
isLoadingAccount.value = false
|
||||||
}
|
}
|
||||||
|
function refreshCookie() {
|
||||||
|
QueryPostAPI<string>(`${ACCOUNT_API_URL}refresh-token`).then((data) => {
|
||||||
|
if (data.code == 200) {
|
||||||
|
cookie.value = data.data
|
||||||
|
cookieRefreshDate.value = Date.now()
|
||||||
|
console.log('[vtsuru] 已刷新Cookie')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
export function useAccount() {
|
export function useAccount() {
|
||||||
return ACCOUNT
|
return ACCOUNT
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,7 +179,6 @@ function onLoginButtonClick() {
|
|||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
turnstile.value?.reset()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
274
src/data/DanmakuClient.ts
Normal file
274
src/data/DanmakuClient.ts
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import { OpenLiveInfo } from '@/api/api-models'
|
||||||
|
import { QueryPostAPI } from '@/api/query'
|
||||||
|
import ChatClientDirectOpenLive from '@/data/chat/ChatClientDirectOpenLive.js'
|
||||||
|
import { OPEN_LIVE_API_URL } from './constants'
|
||||||
|
import { ref, toRef } from 'vue'
|
||||||
|
|
||||||
|
export interface DanmakuInfo {
|
||||||
|
room_id: number
|
||||||
|
uid: number
|
||||||
|
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
|
||||||
|
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
|
||||||
|
combo_info: {
|
||||||
|
combo_base_num: number
|
||||||
|
combo_count: number
|
||||||
|
combo_id: string
|
||||||
|
combo_timeout: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface SCInfo {
|
||||||
|
room_id: number // 直播间id
|
||||||
|
uid: number // 购买用户UID
|
||||||
|
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 AuthInfo {
|
||||||
|
Timestamp: string
|
||||||
|
Code: string
|
||||||
|
Mid: string
|
||||||
|
Caller: string
|
||||||
|
CodeSign: string
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 场次信息
|
||||||
|
*/
|
||||||
|
interface GameInfo {
|
||||||
|
/**
|
||||||
|
* 场次id,心跳key(心跳保持20s-60s)调用一次,超过60s无心跳自动关闭,长连停止推送消息
|
||||||
|
*/
|
||||||
|
game_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 长连信息
|
||||||
|
*/
|
||||||
|
interface WebsocketInfo {
|
||||||
|
/**
|
||||||
|
* 长连使用的请求json体 第三方无需关注内容,建立长连时使用即可
|
||||||
|
*/
|
||||||
|
auth_body: string
|
||||||
|
/**
|
||||||
|
* wss 长连地址
|
||||||
|
*/
|
||||||
|
wss_link: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主播信息
|
||||||
|
*/
|
||||||
|
interface AnchorInfo {
|
||||||
|
/**
|
||||||
|
* 主播房间号
|
||||||
|
*/
|
||||||
|
room_id: number
|
||||||
|
/**
|
||||||
|
* 主播昵称
|
||||||
|
*/
|
||||||
|
uname: string
|
||||||
|
/**
|
||||||
|
* 主播头像
|
||||||
|
*/
|
||||||
|
uface: string
|
||||||
|
/**
|
||||||
|
* 主播uid
|
||||||
|
*/
|
||||||
|
uid: number
|
||||||
|
}
|
||||||
|
export interface RoomAuthInfo {
|
||||||
|
/**
|
||||||
|
* 场次信息
|
||||||
|
*/
|
||||||
|
game_info: GameInfo
|
||||||
|
/**
|
||||||
|
* 长连信息
|
||||||
|
*/
|
||||||
|
websocket_info: WebsocketInfo
|
||||||
|
/**
|
||||||
|
* 主播信息
|
||||||
|
*/
|
||||||
|
anchor_info: AnchorInfo
|
||||||
|
}
|
||||||
|
interface DanmakuEventsMap {
|
||||||
|
danmaku: (arg1: DanmakuInfo, arg2?: any) => void
|
||||||
|
gift: (arg1: GiftInfo, arg2?: any) => void
|
||||||
|
sc: (arg1: SCInfo, arg2?: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class DanmakuClient {
|
||||||
|
constructor(auth: AuthInfo | null) {
|
||||||
|
this.authInfo = auth
|
||||||
|
}
|
||||||
|
|
||||||
|
private client: any
|
||||||
|
private authInfo: AuthInfo | null
|
||||||
|
private timer: any | undefined
|
||||||
|
|
||||||
|
public roomAuthInfo = ref<RoomAuthInfo>({} as RoomAuthInfo)
|
||||||
|
public authCode: string | undefined
|
||||||
|
|
||||||
|
private events: {
|
||||||
|
danmaku: ((arg1: DanmakuInfo, arg2?: any) => void)[]
|
||||||
|
gift: ((arg1: GiftInfo, arg2?: any) => void)[]
|
||||||
|
sc: ((arg1: SCInfo, arg2?: any) => void)[]
|
||||||
|
} = {
|
||||||
|
danmaku: [],
|
||||||
|
gift: [],
|
||||||
|
sc: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Start(): Promise<boolean> {
|
||||||
|
if (!this.client) {
|
||||||
|
console.log('[OPEN-LIVE] 正在启动弹幕客户端')
|
||||||
|
const result = await this.initClient()
|
||||||
|
if (result) {
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
this.sendHeartbeat()
|
||||||
|
}, 20 * 1000)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
console.warn('[OPEN-LIVE] 弹幕客户端已被启动过')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public Stop() {
|
||||||
|
if (this.client) {
|
||||||
|
console.log('[OPEN-LIVE] 正在停止弹幕客户端')
|
||||||
|
this.client.stop()
|
||||||
|
} else {
|
||||||
|
console.warn('[OPEN-LIVE] 弹幕客户端未被启动')
|
||||||
|
}
|
||||||
|
clearInterval(this.timer)
|
||||||
|
this.events = {
|
||||||
|
danmaku: [],
|
||||||
|
gift: [],
|
||||||
|
sc: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private sendHeartbeat() {
|
||||||
|
if (this.client) {
|
||||||
|
QueryPostAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat', this.authInfo?.Code ? this.authInfo : undefined).then((data) => {
|
||||||
|
if (data.code != 200) {
|
||||||
|
console.error('[OPEN-LIVE] 心跳失败: ' + data.message)
|
||||||
|
this.client.stop()
|
||||||
|
this.client = null
|
||||||
|
this.initClient()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private onDanmaku = (command: any) => {
|
||||||
|
const data = command.data as DanmakuInfo
|
||||||
|
if (this.events.danmaku) {
|
||||||
|
this.events.danmaku.forEach((d) => {
|
||||||
|
d(data, command)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private onGift = (command: any) => {
|
||||||
|
const data = command.data as GiftInfo
|
||||||
|
if (this.events.gift) {
|
||||||
|
this.events.gift.forEach((d) => {
|
||||||
|
d(data, command)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public on(eventName: 'danmaku', listener: DanmakuEventsMap['danmaku']): this
|
||||||
|
public on(eventName: 'gift', listener: DanmakuEventsMap['gift']): this
|
||||||
|
public on(eventName: 'sc', listener: DanmakuEventsMap['sc']): this
|
||||||
|
public on(eventName: 'danmaku' | 'gift' | 'sc', listener: (...args: any[]) => void): this {
|
||||||
|
if (!this.events[eventName]) {
|
||||||
|
this.events[eventName] = []
|
||||||
|
}
|
||||||
|
this.events[eventName].push(listener)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
public off(eventName: 'danmaku' | 'gift' | 'sc', listener: (...args: any[]) => void): this {
|
||||||
|
if (this.events[eventName]) {
|
||||||
|
const index = this.events[eventName].indexOf(listener)
|
||||||
|
if (index > -1) {
|
||||||
|
this.events[eventName].splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
private async initClient() {
|
||||||
|
const auth = await this.getAuthInfo()
|
||||||
|
if (auth) {
|
||||||
|
const chatClient = new ChatClientDirectOpenLive(auth)
|
||||||
|
//chatClient.msgHandler = this;
|
||||||
|
chatClient.CMD_CALLBACK_MAP = this.CMD_CALLBACK_MAP
|
||||||
|
chatClient.start()
|
||||||
|
console.log('[OPEN-LIVE] 已连接房间: ' + auth.anchor_info.room_id)
|
||||||
|
this.roomAuthInfo.value = auth
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
console.log('[OPEN-LIVE] 无法开启场次')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private async getAuthInfo() {
|
||||||
|
try {
|
||||||
|
const data = await QueryPostAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'start', this.authInfo?.Code ? this.authInfo : undefined)
|
||||||
|
if (data.code == 200) {
|
||||||
|
console.log('[OPEN-LIVE] 已获取场次信息')
|
||||||
|
return data.data
|
||||||
|
} else {
|
||||||
|
console.error('无法获取场次数据: ' + data.message)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private CMD_CALLBACK_MAP = {
|
||||||
|
LIVE_OPEN_PLATFORM_DM: this.onDanmaku,
|
||||||
|
LIVE_OPEN_PLATFORM_SEND_GIFT: this.onGift,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -195,6 +195,14 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
path: '/open-live',
|
path: '/open-live',
|
||||||
name: 'open-live',
|
name: 'open-live',
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'open-live-index',
|
||||||
|
component: () => import('@/views/open_live/OpenLiveIndex.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '开放平台',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'lottery',
|
path: 'lottery',
|
||||||
name: 'open-live-lottery',
|
name: 'open-live-lottery',
|
||||||
@@ -203,6 +211,14 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
title: '直播抽奖',
|
title: '直播抽奖',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'song-request',
|
||||||
|
name: 'open-live-song-request',
|
||||||
|
component: () => import('@/views/open_live/MusicRequest.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '点歌',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NCard, NDivider, NGradientText, NSpace, NText, NIcon, NGrid, NGridItem, NButton } from 'naive-ui'
|
import { NCard, NDivider, NGradientText, NSpace, NText, NIcon, NGrid, NGridItem, NButton } from 'naive-ui'
|
||||||
import vtb from '@/svgs/ic_vtuber.svg'
|
import vtb from '@/svgs/ic_vtuber.svg'
|
||||||
import { AnalyticsSharp, Calendar, Chatbox, MusicalNote } from '@vicons/ionicons5'
|
import { AnalyticsSharp, Calendar, Chatbox, ListCircle, MusicalNote } from '@vicons/ionicons5'
|
||||||
import { useWindowSize } from '@vueuse/core'
|
import { useWindowSize } from '@vueuse/core'
|
||||||
import { Lottery24Filled, MoneyOff24Filled, MoreHorizontal24Filled, VehicleShip24Filled, VideoAdd20Filled } from '@vicons/fluent'
|
import { Lottery24Filled, MoneyOff24Filled, MoreHorizontal24Filled, VehicleShip24Filled, VideoAdd20Filled } from '@vicons/fluent'
|
||||||
|
|
||||||
@@ -15,12 +15,12 @@ const functions = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '日程表',
|
name: '日程表',
|
||||||
desc: '提供多种样式的日程表 (还没做完',
|
desc: '提供多种样式的日程表 (样式还没做完',
|
||||||
icon: Calendar,
|
icon: Calendar,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '歌单',
|
name: '歌单',
|
||||||
desc: '可以放自己的歌单或者能唱的歌, 支持多种样式 (也还没做完',
|
desc: '可以放自己的歌单或者能唱的歌, 支持多种样式 (样式也还没做完',
|
||||||
icon: MusicalNote,
|
icon: MusicalNote,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -38,6 +38,11 @@ const functions = [
|
|||||||
desc: '从直播间弹幕或礼物抽取用户',
|
desc: '从直播间弹幕或礼物抽取用户',
|
||||||
icon: Lottery24Filled,
|
icon: Lottery24Filled,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '弹幕点歌',
|
||||||
|
desc: '可以让弹幕进行点歌!',
|
||||||
|
icon: ListCircle,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '视频征集',
|
name: '视频征集',
|
||||||
desc: '创建用来收集视频链接的页面, 可以从动态爬取, 也可以提前对视频进行筛选',
|
desc: '创建用来收集视频链接的页面, 可以从动态爬取, 也可以提前对视频进行筛选',
|
||||||
@@ -85,6 +90,7 @@ const iconColor = 'white'
|
|||||||
<NSpace justify="center">
|
<NSpace justify="center">
|
||||||
<NButton type="primary" size="large" @click="$router.push({ name: 'manage-index' })"> 开始使用 </NButton>
|
<NButton type="primary" size="large" @click="$router.push({ name: 'manage-index' })"> 开始使用 </NButton>
|
||||||
<NButton size="large" @click="$router.push('/user/Megghy')"> 展示 </NButton>
|
<NButton size="large" @click="$router.push('/user/Megghy')"> 展示 </NButton>
|
||||||
|
<NButton size="large" tag="a" href="https://play-live.bilibili.com/details/1698742711771" target="_blank" color="#ff778f"> 幻星平台 </NButton>
|
||||||
<NButton type="info" size="large" @click="$router.push({ name: 'about' })"> 关于 </NButton>
|
<NButton type="info" size="large" @click="$router.push({ name: 'about' })"> 关于 </NButton>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
|
|||||||
173
src/views/OpenLiveLayout.vue
Normal file
173
src/views/OpenLiveLayout.vue
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { isDarkMode } from '@/Utils'
|
||||||
|
import { OpenLiveInfo, ThemeType } from '@/api/api-models'
|
||||||
|
import DanmakuClient, { AuthInfo } from '@/data/DanmakuClient'
|
||||||
|
import { Lottery24Filled } from '@vicons/fluent'
|
||||||
|
import { Moon, MusicalNote, Sunny } from '@vicons/ionicons5'
|
||||||
|
import { useElementSize, useStorage } from '@vueuse/core'
|
||||||
|
import {
|
||||||
|
NAvatar,
|
||||||
|
NIcon,
|
||||||
|
NLayout,
|
||||||
|
NLayoutHeader,
|
||||||
|
NLayoutSider,
|
||||||
|
NMenu,
|
||||||
|
NSpace,
|
||||||
|
NText,
|
||||||
|
NButton,
|
||||||
|
NResult,
|
||||||
|
NPageHeader,
|
||||||
|
NSwitch,
|
||||||
|
NModal,
|
||||||
|
NEllipsis,
|
||||||
|
MenuOption,
|
||||||
|
NSpin,
|
||||||
|
NLayoutContent,
|
||||||
|
NLayoutFooter,
|
||||||
|
NBackTop,
|
||||||
|
NScrollbar,
|
||||||
|
useMessage,
|
||||||
|
NDivider,
|
||||||
|
NTag,
|
||||||
|
} from 'naive-ui'
|
||||||
|
import { ref, onMounted, h } from 'vue'
|
||||||
|
import { RouterLink, useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const message = useMessage()
|
||||||
|
const themeType = useStorage('Settings.Theme', ThemeType.Auto)
|
||||||
|
|
||||||
|
const sider = ref()
|
||||||
|
const { width } = useElementSize(sider)
|
||||||
|
|
||||||
|
const authInfo = ref<AuthInfo>()
|
||||||
|
const client = ref<DanmakuClient>()
|
||||||
|
|
||||||
|
const menuOptions = [
|
||||||
|
{
|
||||||
|
label: () =>
|
||||||
|
h(
|
||||||
|
RouterLink,
|
||||||
|
{
|
||||||
|
to: {
|
||||||
|
name: 'open-live-lottery',
|
||||||
|
query: route.query,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ default: () => '抽奖' }
|
||||||
|
),
|
||||||
|
key: 'open-live-lottery',
|
||||||
|
icon: renderIcon(Lottery24Filled),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () =>
|
||||||
|
h(
|
||||||
|
RouterLink,
|
||||||
|
{
|
||||||
|
to: {
|
||||||
|
name: 'open-live-song-request',
|
||||||
|
query: route.query,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ default: () => '点歌' }
|
||||||
|
),
|
||||||
|
key: 'open-live-song-request',
|
||||||
|
icon: renderIcon(MusicalNote),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function renderIcon(icon: unknown) {
|
||||||
|
return () => h(NIcon, null, { default: () => h(icon as any) })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
authInfo.value = route.query as unknown as AuthInfo
|
||||||
|
if (authInfo.value?.Code) {
|
||||||
|
client.value = new DanmakuClient(authInfo.value)
|
||||||
|
await client.value.Start()
|
||||||
|
} else {
|
||||||
|
message.error('你不是从幻星平台访问此页面, 或未提供对应参数, 无法使用此功能')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NLayoutContent v-if="!authInfo?.Code" style="height: 100vh">
|
||||||
|
<NResult status="error" title="无效访问">
|
||||||
|
<template #footer>
|
||||||
|
请前往
|
||||||
|
<NButton text type="primary" tag="a" href="https://play-live.bilibili.com/details/1698742711771" target="_blank"> 幻星平台 | VTsuru </NButton>
|
||||||
|
并点击 获取 , 再点击 获取 H5 插件链接来获取可用链接
|
||||||
|
<br />
|
||||||
|
或者直接在那个页面用也可以, 虽然并不推荐
|
||||||
|
</template>
|
||||||
|
</NResult>
|
||||||
|
</NLayoutContent>
|
||||||
|
<NLayout v-else style="height: 100vh">
|
||||||
|
<NLayoutHeader style="height: 45px; padding: 5px 15px 5px 15px" bordered>
|
||||||
|
<NPageHeader :subtitle="($route.meta.title as string) ?? ''">
|
||||||
|
<template #extra>
|
||||||
|
<NSpace align="center">
|
||||||
|
<NTag :type="client ? 'success' : 'warning'"> {{ client ? `已连接 | ${client.roomAuthInfo.value?.anchor_info.uname}` : '未连接' }} </NTag>
|
||||||
|
<NSwitch :default-value="!isDarkMode()" @update:value="(value: string & number & boolean) => themeType = value ? ThemeType.Light : ThemeType.Dark">
|
||||||
|
<template #checked>
|
||||||
|
<NIcon :component="Sunny" />
|
||||||
|
</template>
|
||||||
|
<template #unchecked>
|
||||||
|
<NIcon :component="Moon" />
|
||||||
|
</template>
|
||||||
|
</NSwitch>
|
||||||
|
</NSpace>
|
||||||
|
</template>
|
||||||
|
<template #title>
|
||||||
|
<NText strong style="font-size: 1.4rem; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); text-justify: auto"> VTSURU | 开放平台 </NText>
|
||||||
|
</template>
|
||||||
|
</NPageHeader>
|
||||||
|
</NLayoutHeader>
|
||||||
|
<NLayout has-sider style="height: calc(100vh - 45px - 30px)">
|
||||||
|
<NLayoutSider bordered ref="sider" show-trigger default-collapsed collapse-mode="width" :collapsed-width="64" :width="180" :native-scrollbar="false" style="height: 100%">
|
||||||
|
<Transition>
|
||||||
|
<div v-if="client?.roomAuthInfo" style="margin-top: 8px">
|
||||||
|
<NSpace vertical justify="center" align="center">
|
||||||
|
<NAvatar
|
||||||
|
:src="client?.roomAuthInfo.value?.anchor_info.uface"
|
||||||
|
:img-props="{ referrerpolicy: 'no-referrer' }"
|
||||||
|
round
|
||||||
|
bordered
|
||||||
|
:style="{ boxShadow: isDarkMode() ? 'rgb(195 192 192 / 35%) 0px 0px 8px' : '0 2px 3px rgba(0, 0, 0, 0.1)' }"
|
||||||
|
/>
|
||||||
|
<NEllipsis v-if="width > 100" style="max-width: 100%">
|
||||||
|
<NText strong>
|
||||||
|
{{ client?.roomAuthInfo.value?.anchor_info.uname }}
|
||||||
|
</NText>
|
||||||
|
</NEllipsis>
|
||||||
|
</NSpace>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
<NMenu :default-value="$route.name?.toString()" :collapsed-width="64" :collapsed-icon-size="22" :options="menuOptions" />
|
||||||
|
<NSpace justify="center">
|
||||||
|
<NText depth="3" v-if="width > 150">
|
||||||
|
有更多功能建议请
|
||||||
|
<NButton text type="info" @click="$router.push({ name: 'about' })"> 反馈 </NButton>
|
||||||
|
</NText>
|
||||||
|
</NSpace>
|
||||||
|
</NLayoutSider>
|
||||||
|
<NLayoutContent yle="height: 100%" :native-scrollbar="false">
|
||||||
|
<RouterView v-if="client?.roomAuthInfo" v-slot="{ Component }">
|
||||||
|
<KeepAlive>
|
||||||
|
<component :is="Component" :room-info="client?.roomAuthInfo" :client="client" :code="authInfo.Code" />
|
||||||
|
</KeepAlive>
|
||||||
|
</RouterView>
|
||||||
|
<template v-else>
|
||||||
|
<NSpin show />
|
||||||
|
</template>
|
||||||
|
<NBackTop />
|
||||||
|
</NLayoutContent>
|
||||||
|
</NLayout>
|
||||||
|
<NLayoutFooter style="height: 30px" bordered>
|
||||||
|
<NSpace justify="center" align="center" style="height: 100%">
|
||||||
|
<NButton text tag="a" href="/" target="_blank" type="info" secondary> vtsuru.live </NButton>
|
||||||
|
</NSpace>
|
||||||
|
</NLayoutFooter>
|
||||||
|
</NLayout>
|
||||||
|
</template>
|
||||||
@@ -34,7 +34,6 @@ async function getUsers() {
|
|||||||
code: currentCode.value,
|
code: currentCode.value,
|
||||||
})
|
})
|
||||||
if (data.code == 200) {
|
if (data.code == 200) {
|
||||||
console.log('[OPEN-LIVE] 已获历史抽奖用户')
|
|
||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,7 +1,40 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useAccount } from '@/api/account'
|
||||||
|
import DanmakuClient, { AuthInfo, DanmakuInfo, RoomAuthInfo, SCInfo } from '@/data/DanmakuClient'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const accountInfo = useAccount()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
client: DanmakuClient
|
||||||
|
roomInfo: RoomAuthInfo
|
||||||
|
code: string | undefined
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function onGetDanmaku(danmaku: DanmakuInfo) {
|
||||||
|
|
||||||
|
}
|
||||||
|
function onGetSC(danmaku: SCInfo) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const authInfo = route.query as unknown as AuthInfo
|
||||||
|
if (!authInfo?.Code && !accountInfo.value?.isBiliVerified) {
|
||||||
|
message.warning('你并不是从幻星平台进入此页面, 且本站账号也未进行 Bilibili 账号认证, 此功能将不可用')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
props.client.on('danmaku', onGetDanmaku)
|
||||||
|
props.client.on('sc', onGetSC)
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
props.client.off('danmaku', onGetDanmaku)
|
||||||
|
props.client.off('sc', onGetSC)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>开发中...</template>
|
||||||
1
|
|
||||||
</template>
|
|
||||||
|
|||||||
36
src/views/open_live/OpenLiveIndex.vue
Normal file
36
src/views/open_live/OpenLiveIndex.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import DanmakuClient, { RoomAuthInfo } from '@/data/DanmakuClient';
|
||||||
|
import { NButton, NCard, NDivider, NSpace } from 'naive-ui'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
client: DanmakuClient
|
||||||
|
roomInfo: RoomAuthInfo
|
||||||
|
code: string | undefined
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NDivider> 功能 </NDivider>
|
||||||
|
<NSpace justify="center">
|
||||||
|
<NCard hoverable embedded size="small" title="弹幕抽奖" style="width: 300px">
|
||||||
|
通过弹幕或者礼物收集用户, 并进行抽取, 允许设置多种条件
|
||||||
|
<template #footer>
|
||||||
|
<NButton @click="$router.push({ name: 'open-live-lottery', query: $route.query })" type="primary"> 前往使用 </NButton>
|
||||||
|
</template>
|
||||||
|
</NCard>
|
||||||
|
<NCard hoverable embedded size="small" title="弹幕点歌" style="width: 300px">
|
||||||
|
通过弹幕或者SC进行点歌, 注册后可以保存和导出 (开发中
|
||||||
|
<template #footer>
|
||||||
|
<NButton @click="$router.push({ name: 'open-live-song-request', query: $route.query })" type="primary"> 前往使用 </NButton>
|
||||||
|
</template>
|
||||||
|
</NCard>
|
||||||
|
</NSpace>
|
||||||
|
<NDivider> 还有更多 </NDivider>
|
||||||
|
<NSpace justify="center" align="center" vertical>
|
||||||
|
动态抽奖、视频征集、歌单、棉花糖、日程表...
|
||||||
|
<p>
|
||||||
|
详见
|
||||||
|
<NButton text tag="a" href="/" target="_blank" type="primary"> VTsuru.live </NButton>
|
||||||
|
</p>
|
||||||
|
</NSpace>
|
||||||
|
</template>
|
||||||
@@ -46,14 +46,8 @@ import { useLocalStorage, useStorage } from '@vueuse/core'
|
|||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import { Delete24Filled, Info24Filled } from '@vicons/fluent'
|
import { Delete24Filled, Info24Filled } from '@vicons/fluent'
|
||||||
import LiveLotteryOBS from '../obs/LiveLotteryOBS.vue'
|
import LiveLotteryOBS from '../obs/LiveLotteryOBS.vue'
|
||||||
|
import DanmakuClient, { AuthInfo, DanmakuInfo, GiftInfo, RoomAuthInfo } from '@/data/DanmakuClient'
|
||||||
|
|
||||||
interface AuthInfo {
|
|
||||||
Timestamp: string
|
|
||||||
Code: string
|
|
||||||
Mid: string
|
|
||||||
Caller: string
|
|
||||||
CodeSign: string
|
|
||||||
}
|
|
||||||
interface LotteryOption {
|
interface LotteryOption {
|
||||||
resultCount: number
|
resultCount: number
|
||||||
lotteryType: 'single' | 'half'
|
lotteryType: 'single' | 'half'
|
||||||
@@ -93,46 +87,27 @@ const message = useMessage()
|
|||||||
const accountInfo = useAccount()
|
const accountInfo = useAccount()
|
||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
|
|
||||||
const authInfo = ref<AuthInfo>()
|
|
||||||
const authResult = ref<OpenLiveInfo | null>(null)
|
|
||||||
const code = computed(() => {
|
|
||||||
return authInfo.value?.Code ?? accountInfo.value?.biliAuthCode
|
|
||||||
})
|
|
||||||
|
|
||||||
const originUsers = ref<OpenLiveLotteryUserInfo[]>([])
|
const originUsers = ref<OpenLiveLotteryUserInfo[]>([])
|
||||||
const currentUsers = ref<OpenLiveLotteryUserInfo[]>([])
|
const currentUsers = ref<OpenLiveLotteryUserInfo[]>([])
|
||||||
const resultUsers = ref<OpenLiveLotteryUserInfo[]>([])
|
const resultUsers = ref<OpenLiveLotteryUserInfo[]>([])
|
||||||
const isStartLottery = ref(false)
|
const isStartLottery = ref(false)
|
||||||
const isLottering = ref(false)
|
const isLottering = ref(false)
|
||||||
const isLotteried = ref(false)
|
const isLotteried = ref(false)
|
||||||
const isConnected = ref(false)
|
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const showOBSModal = ref(false)
|
const showOBSModal = ref(false)
|
||||||
|
|
||||||
let chatClient: any
|
const props = defineProps<{
|
||||||
|
client: DanmakuClient
|
||||||
|
roomInfo: RoomAuthInfo
|
||||||
|
code: string | undefined
|
||||||
|
}>()
|
||||||
|
|
||||||
async function get() {
|
|
||||||
try {
|
|
||||||
const data = await QueryPostAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'start', authInfo.value?.Code ? authInfo.value : undefined)
|
|
||||||
if (data.code == 200) {
|
|
||||||
console.log('[OPEN-LIVE] 已获取场次信息')
|
|
||||||
return data.data
|
|
||||||
} else {
|
|
||||||
message.error('无法获取场次数据: ' + data.message)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
async function getUsers() {
|
async function getUsers() {
|
||||||
try {
|
try {
|
||||||
const data = await QueryGetAPI<UpdateLiveLotteryUsersModel>(LOTTERY_API_URL + 'live/get-users', {
|
const data = await QueryGetAPI<UpdateLiveLotteryUsersModel>(LOTTERY_API_URL + 'live/get-users', {
|
||||||
code: code.value,
|
code: props.code,
|
||||||
})
|
})
|
||||||
if (data.code == 200) {
|
if (data.code == 200) {
|
||||||
console.log('[OPEN-LIVE] 已获历史抽奖用户')
|
|
||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -142,49 +117,14 @@ async function getUsers() {
|
|||||||
}
|
}
|
||||||
function updateUsers() {
|
function updateUsers() {
|
||||||
QueryPostAPI(LOTTERY_API_URL + 'live/update-users', {
|
QueryPostAPI(LOTTERY_API_URL + 'live/update-users', {
|
||||||
code: code.value,
|
code: props.code,
|
||||||
users: originUsers.value,
|
users: originUsers.value,
|
||||||
resultUsers: resultUsers.value,
|
resultUsers: resultUsers.value,
|
||||||
type: isLotteried.value ? OpenLiveLotteryType.Result : OpenLiveLotteryType.Waiting,
|
type: isLotteried.value ? OpenLiveLotteryType.Result : OpenLiveLotteryType.Waiting,
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error('[OPEN-LIVE] 更新历史抽奖用户失败: ' + err)
|
console.error('[OPEN-LIVE-Lottery] 更新历史抽奖用户失败: ' + err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
async function start() {
|
|
||||||
if (!chatClient) {
|
|
||||||
await connectRoom()
|
|
||||||
isConnected.value = true
|
|
||||||
setInterval(() => {
|
|
||||||
if (chatClient) {
|
|
||||||
QueryPostAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat', authInfo.value).then((data) => {
|
|
||||||
if (data.code != 200) {
|
|
||||||
console.error('[OPEN-LIVE] 心跳失败: ' + data.message)
|
|
||||||
chatClient.stop()
|
|
||||||
chatClient = null
|
|
||||||
connectRoom()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, 20 * 1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function connectRoom() {
|
|
||||||
const auth = await get()
|
|
||||||
if (auth) {
|
|
||||||
authResult.value = auth
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
initChatClient()
|
|
||||||
}
|
|
||||||
async function initChatClient() {
|
|
||||||
chatClient = new ChatClientDirectOpenLive(authResult.value)
|
|
||||||
|
|
||||||
//chatClient.msgHandler = this;
|
|
||||||
chatClient.CMD_CALLBACK_MAP = CMD_CALLBACK_MAP
|
|
||||||
chatClient.start()
|
|
||||||
console.log('[OPEN-LIVE] 已连接房间: ' + authResult.value?.anchor_info.room_id)
|
|
||||||
}
|
|
||||||
function addUser(user: OpenLiveLotteryUserInfo, danmu: any) {
|
function addUser(user: OpenLiveLotteryUserInfo, danmu: any) {
|
||||||
if (originUsers.value.find((u) => u.uId == user.uId) || !isStartLottery.value) {
|
if (originUsers.value.find((u) => u.uId == user.uId) || !isStartLottery.value) {
|
||||||
return
|
return
|
||||||
@@ -201,12 +141,6 @@ function addUser(user: OpenLiveLotteryUserInfo, danmu: any) {
|
|||||||
function isUserValid(u: OpenLiveLotteryUserInfo, danmu: any) {
|
function isUserValid(u: OpenLiveLotteryUserInfo, danmu: any) {
|
||||||
const cmd = danmu.cmd
|
const cmd = danmu.cmd
|
||||||
const data = danmu.data
|
const data = danmu.data
|
||||||
if (cmd === 'LIVE_OPEN_PLATFORM_DM' && lotteryOption.value.type != 'danmaku') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (cmd === 'LIVE_OPEN_PLATFORM_SEND_GIFT' && lotteryOption.value.type != 'gift') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (lotteryOption.value.needWearFanMedal) {
|
if (lotteryOption.value.needWearFanMedal) {
|
||||||
if (!u.fans_medal_wearing_status) return false
|
if (!u.fans_medal_wearing_status) return false
|
||||||
}
|
}
|
||||||
@@ -331,35 +265,37 @@ function removeUser(user: OpenLiveLotteryUserInfo) {
|
|||||||
updateUsers()
|
updateUsers()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDanmaku(command: any) {
|
function onDanmaku(data: DanmakuInfo, command: any) {
|
||||||
const data = command.data
|
if (lotteryOption.value.type == 'danmaku') {
|
||||||
addUser(
|
addUser(
|
||||||
{
|
{
|
||||||
uId: data.uid,
|
uId: data.uid,
|
||||||
name: data.uname,
|
name: data.uname,
|
||||||
avatar: data.uface,
|
avatar: data.uface,
|
||||||
fans_medal_level: data.fans_medal_level,
|
fans_medal_level: data.fans_medal_level,
|
||||||
fans_medal_name: data.fans_medal_name,
|
fans_medal_name: data.fans_medal_name,
|
||||||
fans_medal_wearing_status: data.fans_medal_wearing_status,
|
fans_medal_wearing_status: data.fans_medal_wearing_status,
|
||||||
guard_level: data.guard_level,
|
guard_level: data.guard_level,
|
||||||
},
|
},
|
||||||
command
|
command
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function onGift(command: any) {
|
function onGift(data: GiftInfo, command: any) {
|
||||||
const data = command.data
|
if (lotteryOption.value.type == 'gift') {
|
||||||
addUser(
|
addUser(
|
||||||
{
|
{
|
||||||
uId: data.uid,
|
uId: data.uid,
|
||||||
name: data.uname,
|
name: data.uname,
|
||||||
avatar: data.uface,
|
avatar: data.uface,
|
||||||
fans_medal_level: data.fans_medal_level,
|
fans_medal_level: data.fans_medal_level,
|
||||||
fans_medal_name: data.fans_medal_name,
|
fans_medal_name: data.fans_medal_name,
|
||||||
fans_medal_wearing_status: data.fans_medal_wearing_status,
|
fans_medal_wearing_status: data.fans_medal_wearing_status,
|
||||||
guard_level: data.guard_level,
|
guard_level: data.guard_level,
|
||||||
},
|
},
|
||||||
command
|
command
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function pause() {
|
function pause() {
|
||||||
isStartLottery.value = false
|
isStartLottery.value = false
|
||||||
@@ -372,8 +308,7 @@ function continueLottery() {
|
|||||||
|
|
||||||
let timer: any
|
let timer: any
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
authInfo.value = route.query as unknown as AuthInfo
|
if (props.code) {
|
||||||
if (authInfo.value?.Code) {
|
|
||||||
const users = (await getUsers())?.users ?? []
|
const users = (await getUsers())?.users ?? []
|
||||||
originUsers.value = users
|
originUsers.value = users
|
||||||
currentUsers.value = JSON.parse(JSON.stringify(users))
|
currentUsers.value = JSON.parse(JSON.stringify(users))
|
||||||
@@ -382,18 +317,24 @@ onMounted(async () => {
|
|||||||
message.info('从历史记录中加载 ' + users.length + ' 位用户')
|
message.info('从历史记录中加载 ' + users.length + ' 位用户')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (props.client) {
|
||||||
|
props.client.on('danmaku', onDanmaku)
|
||||||
|
props.client.on('gift', onGift)
|
||||||
|
}
|
||||||
timer = setInterval(updateUsers, 1000 * 10)
|
timer = setInterval(updateUsers, 1000 * 10)
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (timer) {
|
if (timer) {
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
}
|
}
|
||||||
|
props.client?.off('danmaku', onDanmaku)
|
||||||
|
props.client?.off('gift', onGift)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NLayoutContent style="height: 100vh; padding: 20px">
|
<NLayoutContent style="height: 100vh; padding: 20px">
|
||||||
<NResult v-if="!authInfo?.Code && !accountInfo" status="403" title="403" description="该页面只能从饭贩访问或者注册用户使用" />
|
<NResult v-if="code && !accountInfo" status="403" title="403" description="该页面只能从饭贩访问或者注册用户使用" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<NCard>
|
<NCard>
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -401,15 +342,10 @@ onUnmounted(() => {
|
|||||||
<NDivider vertical />
|
<NDivider vertical />
|
||||||
<NButton text type="primary" tag="a" href="https://vtsuru.live" target="_blank"> 前往 VTsuru.live 主站 </NButton>
|
<NButton text type="primary" tag="a" href="https://vtsuru.live" target="_blank"> 前往 VTsuru.live 主站 </NButton>
|
||||||
</template>
|
</template>
|
||||||
<NAlert v-if="!authInfo?.Code && accountInfo && !accountInfo.isBiliVerified" type="error"> 请先绑定B站账号 </NAlert>
|
<NAlert v-if="!code && accountInfo && !accountInfo.isBiliVerified" type="error"> 请先绑定B站账号 </NAlert>
|
||||||
<NAlert v-else-if="!authInfo?.Code && accountInfo && accountInfo.biliAuthCodeStatus != 1" type="error"> 身份码状态异常, 请重新绑定 </NAlert>
|
<NAlert v-else-if="!code && accountInfo && accountInfo.biliAuthCodeStatus != 1" type="error"> 身份码状态异常, 请重新绑定 </NAlert>
|
||||||
<NCard>
|
<NCard>
|
||||||
<NSpace align="center">
|
<NSpace align="center">
|
||||||
连接状态:
|
|
||||||
<NTag :type="isConnected ? 'success' : 'warning'"> {{ isConnected ? `已连接 | ${authResult?.anchor_info.uname}` : '未连接' }} </NTag>
|
|
||||||
<NButton v-if="!isConnected" type="primary" @click="start" size="small" :disabled="!authInfo?.Code && (!accountInfo?.isBiliVerified || accountInfo.biliAuthCodeStatus != 1)">
|
|
||||||
连接直播间
|
|
||||||
</NButton>
|
|
||||||
<NButton type="info" @click="showModal = true" size="small"> 抽奖历史</NButton>
|
<NButton type="info" @click="showModal = true" size="small"> 抽奖历史</NButton>
|
||||||
<NButton type="success" @click="showOBSModal = true" size="small"> OBS组件</NButton>
|
<NButton type="success" @click="showOBSModal = true" size="small"> OBS组件</NButton>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
@@ -433,13 +369,13 @@ onUnmounted(() => {
|
|||||||
</NInputGroup>
|
</NInputGroup>
|
||||||
<NCheckbox :disabled="isStartLottery" v-model:checked="lotteryOption.needGuard"> 需要上舰 </NCheckbox>
|
<NCheckbox :disabled="isStartLottery" v-model:checked="lotteryOption.needGuard"> 需要上舰 </NCheckbox>
|
||||||
<NCheckbox :disabled="isStartLottery" v-model:checked="lotteryOption.needFanMedal"> 需要粉丝牌 </NCheckbox>
|
<NCheckbox :disabled="isStartLottery" v-model:checked="lotteryOption.needFanMedal"> 需要粉丝牌 </NCheckbox>
|
||||||
|
<NCollapseTransition>
|
||||||
|
<NInputGroup v-if="lotteryOption.needFanMedal" style="max-width: 200px">
|
||||||
|
<NInputGroupLabel> 最低粉丝牌等级 </NInputGroupLabel>
|
||||||
|
<NInputNumber v-model:value="lotteryOption.fanCardLevel" min="1" max="50" :default-value="1" :disabled="isLottering || isStartLottery" />
|
||||||
|
</NInputGroup>
|
||||||
|
</NCollapseTransition>
|
||||||
<template v-if="lotteryOption.type == 'danmaku'">
|
<template v-if="lotteryOption.type == 'danmaku'">
|
||||||
<NCollapseTransition>
|
|
||||||
<NInputGroup v-if="lotteryOption.needFanMedal" style="max-width: 200px">
|
|
||||||
<NInputGroupLabel> 最低粉丝牌等级 </NInputGroupLabel>
|
|
||||||
<NInputNumber v-model:value="lotteryOption.fanCardLevel" min="1" max="50" :default-value="1" :disabled="isLottering || isStartLottery" />
|
|
||||||
</NInputGroup>
|
|
||||||
</NCollapseTransition>
|
|
||||||
<NTooltip>
|
<NTooltip>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NInputGroup style="max-width: 250px">
|
<NInputGroup style="max-width: 250px">
|
||||||
@@ -493,9 +429,8 @@ onUnmounted(() => {
|
|||||||
</NCard>
|
</NCard>
|
||||||
<NCard v-if="originUsers" size="small">
|
<NCard v-if="originUsers" size="small">
|
||||||
<NSpace justify="center" align="center">
|
<NSpace justify="center" align="center">
|
||||||
<NTag :bordered="false" type="warning" v-if="!isConnected"> 开始前需要先连接直播间 </NTag>
|
|
||||||
<NSpin v-if="isStartLottery" size="small" />
|
<NSpin v-if="isStartLottery" size="small" />
|
||||||
<NButton type="primary" @click="continueLottery" :loading="isLottering" :disabled="isStartLottery || isLotteried || !isConnected"> 开始 </NButton>
|
<NButton type="primary" @click="continueLottery" :loading="isLottering" :disabled="isStartLottery || isLotteried || !client"> 开始 </NButton>
|
||||||
<NButton type="warning" :disabled="!isStartLottery" @click="pause"> 停止 </NButton>
|
<NButton type="warning" :disabled="!isStartLottery" @click="pause"> 停止 </NButton>
|
||||||
<NButton type="error" :disabled="isLottering || originUsers.length == 0" @click="clear"> 清空 </NButton>
|
<NButton type="error" :disabled="isLottering || originUsers.length == 0" @click="clear"> 清空 </NButton>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
@@ -507,11 +442,11 @@ onUnmounted(() => {
|
|||||||
<NDivider style="margin: 10px 0 10px 0"> 共 {{ currentUsers?.length }} 人</NDivider>
|
<NDivider style="margin: 10px 0 10px 0"> 共 {{ currentUsers?.length }} 人</NDivider>
|
||||||
<NGrid v-if="currentUsers.length > 0" cols="1 500:2 800:3 1000:4" :x-gap="12" :y-gap="8">
|
<NGrid v-if="currentUsers.length > 0" cols="1 500:2 800:3 1000:4" :x-gap="12" :y-gap="8">
|
||||||
<NGridItem v-for="item in currentUsers" v-bind:key="item.uId">
|
<NGridItem v-for="item in currentUsers" v-bind:key="item.uId">
|
||||||
<NCard size="small" :title="item.name" style="height: 155px">
|
<NCard size="small" :title="item.name" style="height: 155px" embedded>
|
||||||
<template #header>
|
<template #header>
|
||||||
<NSpace align="center" vertical :size="5">
|
<NSpace align="center" vertical :size="5">
|
||||||
<NAvatar round lazy borderd :size="64" :src="item.avatar + '@64w_64h'" :img-props="{ referrerpolicy: 'no-referrer' }" style="box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2)" />
|
<NAvatar round lazy borderd :size="64" :src="item.avatar + '@64w_64h'" :img-props="{ referrerpolicy: 'no-referrer' }" style="box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2)" />
|
||||||
<NSpace>
|
<NSpace v-if="item.fans_medal_wearing_status">
|
||||||
<NTag size="tiny" round>
|
<NTag size="tiny" round>
|
||||||
<NTag size="tiny" round :bordered="false">
|
<NTag size="tiny" round :bordered="false">
|
||||||
{{ item.fans_medal_level }}
|
{{ item.fans_medal_level }}
|
||||||
@@ -521,6 +456,7 @@ onUnmounted(() => {
|
|||||||
</span>
|
</span>
|
||||||
</NTag>
|
</NTag>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
|
<NTag v-else size="tiny" round :bordered="false"> 无粉丝牌 </NTag>
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</NSpace>
|
</NSpace>
|
||||||
|
|
||||||
@@ -535,9 +471,6 @@ onUnmounted(() => {
|
|||||||
</NGrid>
|
</NGrid>
|
||||||
<NEmpty v-else description="暂无用户" />
|
<NEmpty v-else description="暂无用户" />
|
||||||
</NCard>
|
</NCard>
|
||||||
<NSpace justify="center" style="margin-top: 20px">
|
|
||||||
<NButton type="info" text tag="a" href="https://vtsuru.live" target="_blank"> vtsuru.live </NButton>
|
|
||||||
</NSpace>
|
|
||||||
</NCard>
|
</NCard>
|
||||||
</template>
|
</template>
|
||||||
<NModal v-model:show="showModal" preset="card" title="抽奖结果" style="max-width: 90%; width: 800px" closable>
|
<NModal v-model:show="showModal" preset="card" title="抽奖结果" style="max-width: 90%; width: 800px" closable>
|
||||||
@@ -590,3 +523,4 @@ onUnmounted(() => {
|
|||||||
</NModal>
|
</NModal>
|
||||||
</NLayoutContent>
|
</NLayoutContent>
|
||||||
</template>
|
</template>
|
||||||
|
@/data/DanmakuClient
|
||||||
|
|||||||
Reference in New Issue
Block a user