update open live page

This commit is contained in:
2023-11-15 14:38:58 +08:00
parent 8e110956a4
commit f117f11407
11 changed files with 626 additions and 137 deletions

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { NCard, NDivider, NGradientText, NSpace, NText, NIcon, NGrid, NGridItem, NButton } from 'naive-ui'
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 { Lottery24Filled, MoneyOff24Filled, MoreHorizontal24Filled, VehicleShip24Filled, VideoAdd20Filled } from '@vicons/fluent'
@@ -15,12 +15,12 @@ const functions = [
},
{
name: '日程表',
desc: '提供多种样式的日程表 (还没做完',
desc: '提供多种样式的日程表 (样式还没做完',
icon: Calendar,
},
{
name: '歌单',
desc: '可以放自己的歌单或者能唱的歌, 支持多种样式 (也还没做完',
desc: '可以放自己的歌单或者能唱的歌, 支持多种样式 (样式也还没做完',
icon: MusicalNote,
},
{
@@ -38,6 +38,11 @@ const functions = [
desc: '从直播间弹幕或礼物抽取用户',
icon: Lottery24Filled,
},
{
name: '弹幕点歌',
desc: '可以让弹幕进行点歌!',
icon: ListCircle,
},
{
name: '视频征集',
desc: '创建用来收集视频链接的页面, 可以从动态爬取, 也可以提前对视频进行筛选',
@@ -85,6 +90,7 @@ const iconColor = 'white'
<NSpace justify="center">
<NButton type="primary" size="large" @click="$router.push({ name: 'manage-index' })"> 开始使用 </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>
</NSpace>
</NSpace>

View 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>

View File

@@ -34,7 +34,6 @@ async function getUsers() {
code: currentCode.value,
})
if (data.code == 200) {
console.log('[OPEN-LIVE] 已获历史抽奖用户')
return data.data
}
} catch (err) {

View File

@@ -1,7 +1,40 @@
<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>
<template>
1
</template>
<template>开发中...</template>

View 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>

View File

@@ -46,14 +46,8 @@ import { useLocalStorage, useStorage } from '@vueuse/core'
import { format } from 'date-fns'
import { Delete24Filled, Info24Filled } from '@vicons/fluent'
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 {
resultCount: number
lotteryType: 'single' | 'half'
@@ -93,46 +87,27 @@ const message = useMessage()
const accountInfo = useAccount()
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 currentUsers = ref<OpenLiveLotteryUserInfo[]>([])
const resultUsers = ref<OpenLiveLotteryUserInfo[]>([])
const isStartLottery = ref(false)
const isLottering = ref(false)
const isLotteried = ref(false)
const isConnected = ref(false)
const showModal = 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() {
try {
const data = await QueryGetAPI<UpdateLiveLotteryUsersModel>(LOTTERY_API_URL + 'live/get-users', {
code: code.value,
code: props.code,
})
if (data.code == 200) {
console.log('[OPEN-LIVE] 已获历史抽奖用户')
return data.data
}
} catch (err) {
@@ -142,49 +117,14 @@ async function getUsers() {
}
function updateUsers() {
QueryPostAPI(LOTTERY_API_URL + 'live/update-users', {
code: code.value,
code: props.code,
users: originUsers.value,
resultUsers: resultUsers.value,
type: isLotteried.value ? OpenLiveLotteryType.Result : OpenLiveLotteryType.Waiting,
}).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) {
if (originUsers.value.find((u) => u.uId == user.uId) || !isStartLottery.value) {
return
@@ -201,12 +141,6 @@ function addUser(user: OpenLiveLotteryUserInfo, danmu: any) {
function isUserValid(u: OpenLiveLotteryUserInfo, danmu: any) {
const cmd = danmu.cmd
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 (!u.fans_medal_wearing_status) return false
}
@@ -331,35 +265,37 @@ function removeUser(user: OpenLiveLotteryUserInfo) {
updateUsers()
}
function onDanmaku(command: any) {
const data = command.data
addUser(
{
uId: data.uid,
name: data.uname,
avatar: data.uface,
fans_medal_level: data.fans_medal_level,
fans_medal_name: data.fans_medal_name,
fans_medal_wearing_status: data.fans_medal_wearing_status,
guard_level: data.guard_level,
},
command
)
function onDanmaku(data: DanmakuInfo, command: any) {
if (lotteryOption.value.type == 'danmaku') {
addUser(
{
uId: data.uid,
name: data.uname,
avatar: data.uface,
fans_medal_level: data.fans_medal_level,
fans_medal_name: data.fans_medal_name,
fans_medal_wearing_status: data.fans_medal_wearing_status,
guard_level: data.guard_level,
},
command
)
}
}
function onGift(command: any) {
const data = command.data
addUser(
{
uId: data.uid,
name: data.uname,
avatar: data.uface,
fans_medal_level: data.fans_medal_level,
fans_medal_name: data.fans_medal_name,
fans_medal_wearing_status: data.fans_medal_wearing_status,
guard_level: data.guard_level,
},
command
)
function onGift(data: GiftInfo, command: any) {
if (lotteryOption.value.type == 'gift') {
addUser(
{
uId: data.uid,
name: data.uname,
avatar: data.uface,
fans_medal_level: data.fans_medal_level,
fans_medal_name: data.fans_medal_name,
fans_medal_wearing_status: data.fans_medal_wearing_status,
guard_level: data.guard_level,
},
command
)
}
}
function pause() {
isStartLottery.value = false
@@ -372,8 +308,7 @@ function continueLottery() {
let timer: any
onMounted(async () => {
authInfo.value = route.query as unknown as AuthInfo
if (authInfo.value?.Code) {
if (props.code) {
const users = (await getUsers())?.users ?? []
originUsers.value = users
currentUsers.value = JSON.parse(JSON.stringify(users))
@@ -382,18 +317,24 @@ onMounted(async () => {
message.info('从历史记录中加载 ' + users.length + ' 位用户')
}
}
if (props.client) {
props.client.on('danmaku', onDanmaku)
props.client.on('gift', onGift)
}
timer = setInterval(updateUsers, 1000 * 10)
})
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
props.client?.off('danmaku', onDanmaku)
props.client?.off('gift', onGift)
})
</script>
<template>
<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>
<NCard>
<template #header>
@@ -401,15 +342,10 @@ onUnmounted(() => {
<NDivider vertical />
<NButton text type="primary" tag="a" href="https://vtsuru.live" target="_blank"> 前往 VTsuru.live 主站 </NButton>
</template>
<NAlert v-if="!authInfo?.Code && accountInfo && !accountInfo.isBiliVerified" type="error"> 请先绑定B站账号 </NAlert>
<NAlert v-else-if="!authInfo?.Code && accountInfo && accountInfo.biliAuthCodeStatus != 1" type="error"> 身份码状态异常, 请重新绑定 </NAlert>
<NAlert v-if="!code && accountInfo && !accountInfo.isBiliVerified" type="error"> 请先绑定B站账号 </NAlert>
<NAlert v-else-if="!code && accountInfo && accountInfo.biliAuthCodeStatus != 1" type="error"> 身份码状态异常, 请重新绑定 </NAlert>
<NCard>
<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="success" @click="showOBSModal = true" size="small"> OBS组件</NButton>
</NSpace>
@@ -433,13 +369,13 @@ onUnmounted(() => {
</NInputGroup>
<NCheckbox :disabled="isStartLottery" v-model:checked="lotteryOption.needGuard"> 需要上舰 </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'">
<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>
<template #trigger>
<NInputGroup style="max-width: 250px">
@@ -493,9 +429,8 @@ onUnmounted(() => {
</NCard>
<NCard v-if="originUsers" size="small">
<NSpace justify="center" align="center">
<NTag :bordered="false" type="warning" v-if="!isConnected"> 开始前需要先连接直播间 </NTag>
<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="error" :disabled="isLottering || originUsers.length == 0" @click="clear"> 清空 </NButton>
</NSpace>
@@ -507,11 +442,11 @@ onUnmounted(() => {
<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">
<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>
<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)" />
<NSpace>
<NSpace v-if="item.fans_medal_wearing_status">
<NTag size="tiny" round>
<NTag size="tiny" round :bordered="false">
{{ item.fans_medal_level }}
@@ -521,6 +456,7 @@ onUnmounted(() => {
</span>
</NTag>
</NSpace>
<NTag v-else size="tiny" round :bordered="false"> 无粉丝牌 </NTag>
{{ item.name }}
</NSpace>
@@ -535,9 +471,6 @@ onUnmounted(() => {
</NGrid>
<NEmpty v-else description="暂无用户" />
</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>
</template>
<NModal v-model:show="showModal" preset="card" title="抽奖结果" style="max-width: 90%; width: 800px" closable>
@@ -590,3 +523,4 @@ onUnmounted(() => {
</NModal>
</NLayoutContent>
</template>
@/data/DanmakuClient