add livelottery obs compoent

This commit is contained in:
2023-11-11 19:00:01 +08:00
parent 6d625b3ddc
commit 0b36224691
9 changed files with 350 additions and 32 deletions

View File

@@ -33,6 +33,7 @@
"vue-router": "4", "vue-router": "4",
"vue-turnstile": "^1.0.0", "vue-turnstile": "^1.0.0",
"vue3-aplayer": "^1.7.3", "vue3-aplayer": "^1.7.3",
"vue3-marquee": "^4.1.0",
"vuex": "^4.0.0" "vuex": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -2,18 +2,21 @@
<NMessageProvider> <NMessageProvider>
<NNotificationProvider> <NNotificationProvider>
<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">
<NElement style="height: 100vh"> <Suspense>
<Suspense> <div style="height: 100vh">
<ViewerLayout v-if="layout == 'viewer'" /> <NElement style="height: 100%;" v-if="layout != 'obs'">
<ManageLayout v-else-if="layout == 'manage'" /> <ViewerLayout v-if="layout == 'viewer'" />
<template v-else> <ManageLayout v-else-if="layout == 'manage'" />
<RouterView /> <template v-else-if="layout == ''">
</template> <RouterView />
<template #fallback> </template>
<NSpin size="large" show/> </NElement>
</template> <RouterView v-else/>
</Suspense> </div>
</NElement> <template #fallback>
<NSpin size="large" show />
</template>
</Suspense>
</NConfigProvider> </NConfigProvider>
</NNotificationProvider> </NNotificationProvider>
</NMessageProvider> </NMessageProvider>
@@ -38,6 +41,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('/obs')) {
document.title = route.meta.title + ' · OBS · VTsuru'
return 'obs'
} else { } else {
document.title = route.meta.title + ' · VTsuru' document.title = route.meta.title + ' · VTsuru'
return '' return ''

View File

@@ -36,6 +36,7 @@ export interface AccountInfo extends UserInfo {
settings: UserSetting settings: UserSetting
token: string token: string
biliAuthCode?: string
biliAuthCodeStatus: BiliAuthCodeStatusType biliAuthCodeStatus: BiliAuthCodeStatusType
eventFetcherOnline: boolean eventFetcherOnline: boolean
@@ -236,3 +237,22 @@ export interface OpenLiveInfo {
websocket_info: WebsocketInfo websocket_info: WebsocketInfo
anchor_info: AnchorInfo anchor_info: AnchorInfo
} }
export interface OpenLiveLotteryUserInfo {
name: string
uId: number
level?: number
avatar: string
fans_medal_level: number
fans_medal_name: string //粉丝勋章名
fans_medal_wearing_status: boolean //该房间粉丝勋章佩戴情况
guard_level: number
}
export enum OpenLiveLotteryType{
Waiting,
Result
}
export interface UpdateLiveLotteryUsersModel {
users: OpenLiveLotteryUserInfo[]
resultUsers: OpenLiveLotteryUserInfo[]
type: OpenLiveLotteryType
}

View File

@@ -205,6 +205,20 @@ const routes: Array<RouteRecordRaw> = [
}, },
], ],
}, },
{
path: '/obs',
name: 'obs',
children: [
{
path: 'live-lottery',
name: 'obs-live-lottery',
component: () => import('@/views/obs/LiveLotteryOBS.vue'),
meta: {
title: '直播抽奖',
},
},
],
},
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: 'notfound', name: 'notfound',

View File

@@ -0,0 +1,7 @@
<script setup lang="ts">
</script>
<template>
1
</template>

View File

@@ -0,0 +1,168 @@
<script setup lang="ts">
import { OpenLiveLotteryType, OpenLiveLotteryUserInfo, UpdateLiveLotteryUsersModel } from '@/api/api-models'
import { QueryGetAPI } from '@/api/query'
import { LOTTERY_API_URL } from '@/data/constants'
import { useElementSize } from '@vueuse/core'
import { NCard, NDivider, NEmpty, NSpace, NText, useMessage } from 'naive-ui'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { Vue3Marquee } from 'vue3-marquee'
const props = defineProps<{
code?: string
}>()
const message = useMessage()
const route = useRoute()
const currentCode = computed(() => {
return props.code ?? route.query.code
})
const listContainerRef = ref()
const { height, width } = useElementSize(listContainerRef)
const result = ref(await getUsers())
const users = computed(() => {
return result.value?.users
})
const isMoreThanContainer = computed(() => {
return (users.value?.length ?? 0) * 50 > height.value
})
async function getUsers() {
try {
const data = await QueryGetAPI<UpdateLiveLotteryUsersModel>(LOTTERY_API_URL + 'live/get-users', {
code: currentCode.value,
})
if (data.code == 200) {
console.log('[OPEN-LIVE] 已获历史抽奖用户')
return data.data
}
} catch (err) {
console.error(err)
}
return {
users: [],
resultUsers: [],
type: OpenLiveLotteryType.Waiting,
} as UpdateLiveLotteryUsersModel
}
let timer: any
onMounted(() => {
timer = setInterval(async () => {
const r = await getUsers()
if (r) {
result.value = r
}
}, 2000)
})
onUnmounted(() => {
clearInterval(timer)
})
</script>
<template>
<div class="lottery-background" v-bind="$attrs">
<p class="lottery-header">抽奖</p>
<NDivider v-if="result.type == OpenLiveLotteryType.Waiting" class="lottery-divider">
<p class="lottery-header-count">已有 {{ users?.length ?? 0 }} </p>
</NDivider>
<div class="lottery-content" ref="listContainerRef">
<template v-if="users.length > 0">
<Vue3Marquee v-if="result.type == OpenLiveLotteryType.Waiting" vertical :pause="!isMoreThanContainer" :duration="20" :style="`height: ${height}px;`">
<span class="lottery-list-item" :id="index.toString()" v-for="(user, index) in users" :key="user.uId" style="height: 50px">
<img class="lottery-avatar" :src="user.avatar + '@30h'" referrerpolicy="no-referrer" />
<div>
<p class="lottery-name">{{ user.name }}</p>
</div>
</span>
</Vue3Marquee>
</template>
<div v-else style="position: relative; top: 20%">
<NEmpty description="暂无人参与" />
</div>
<template v-if="result.type == OpenLiveLotteryType.Result">
<p style="text-align: center; font-size: 20px; margin: 0; font-weight: bold; color: #eeabab">结果</p>
<Vue3Marquee v-if="100 * result.resultUsers.length > width" justify="center" style="height: 100px">
<div
v-for="user in result.resultUsers"
:key="user.uId"
title="抽奖结果"
style="height: 100px; width: 100px; display: flex; flex-direction: column; align-items: center; border-radius: 5px; border: #fff 1px solid; padding: 10px; margin: 10px"
>
<NSpace vertical>
<img height="50" width="50" style="border-radius: 50%" :src="user.avatar + '@50h_50w'" referrerpolicy="no-referrer" />
<NText style="font-size: large">
{{ user.name }}
</NText>
</NSpace>
</div>
</Vue3Marquee>
<NSpace justify="center">
<div
v-for="user in result.resultUsers"
:key="user.uId"
title="抽奖结果"
style="height: 100px; width: 100px; display: flex; flex-direction: column; align-items: center; border-radius: 5px; border: #fff 1px solid; padding: 10px; margin: 10px"
>
<img height="50" width="50" style="border-radius: 50%" :src="user.avatar + '@50h_50w'" referrerpolicy="no-referrer" />
<NText style="font-size: large; margin-top: 10px">
{{ user.name }}
</NText>
</div>
</NSpace>
</template>
</div>
</div>
</template>
<style scoped>
.lottery-background {
display: flex !important;
flex-direction: column !important;
height: 100% !important;
width: 100% !important;
min-height: 100px !important;
min-width: 100px !important;
background-color: #0f0f0f48 !important;
border-radius: 10px !important;
color: white !important;
}
.lottery-header {
margin: 0 !important;
color: #fff !important;
text-align: center !important;
font-size: 24px !important;
font-weight: bold !important;
text-shadow: 0 0 10px #ca7b7b6e, 0 0 20px #ffffff8e, 0 0 30px #61606086, 0 0 40px rgba(64, 156, 179, 0.555) !important;
}
.lottery-header-count {
color: #ffffffbd !important;
text-align: center !important;
font-size: 14px !important;
}
.lottery-divider {
margin: -10px 10px -10px 10px !important;
width: 90% !important;
}
.n-divider__line {
background-color: #ffffffd5 !important;
}
.lottery-content {
background-color: #0f0f0f4f !important;
margin: 10px !important;
padding: 10px !important;
height: 100% !important;
border-radius: 10px !important;
}
.lottery-list-item {
display: flex !important;
align-items: center !important;
gap: 10px !important;
transition: color 0.3s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
.lottery-avatar {
height: 30px !important;
border-radius: 50% !important;
}
</style>

View File

@@ -1,15 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, h, onMounted, ref } from 'vue' import { computed, h, onMounted, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { QueryPostAPI } from '@/api/query' import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import { OPEN_LIVE_API_URL } from '@/data/constants' import { LOTTERY_API_URL, OPEN_LIVE_API_URL } from '@/data/constants'
import { LotteryUserInfo, OpenLiveInfo } from '@/api/api-models' import { LotteryUserInfo, OpenLiveInfo, OpenLiveLotteryType, OpenLiveLotteryUserInfo } from '@/api/api-models'
import { import {
NAlert, NAlert,
NAvatar, NAvatar,
NButton, NButton,
NCard, NCard,
NCheckbox, NCheckbox,
NCollapse,
NCollapseItem,
NCollapseTransition, NCollapseTransition,
NDivider, NDivider,
NEmpty, NEmpty,
@@ -21,6 +23,7 @@ import {
NInputGroupLabel, NInputGroupLabel,
NInputNumber, NInputNumber,
NLayoutContent, NLayoutContent,
NLi,
NList, NList,
NListItem, NListItem,
NModal, NModal,
@@ -33,6 +36,7 @@ import {
NTag, NTag,
NTime, NTime,
NTooltip, NTooltip,
NUl,
useMessage, useMessage,
useNotification, useNotification,
} from 'naive-ui' } from 'naive-ui'
@@ -41,6 +45,7 @@ import ChatClientDirectOpenLive from '@/data/chat/ChatClientDirectOpenLive.js'
import { useLocalStorage, useStorage } from '@vueuse/core' 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'
interface AuthInfo { interface AuthInfo {
Timestamp: string Timestamp: string
@@ -49,18 +54,6 @@ interface AuthInfo {
Caller: string Caller: string
CodeSign: string CodeSign: string
} }
interface OpenLiveLotteryBaseUserInfo {
name: string
uId: number
level?: number
avatar: string
}
interface OpenLiveLotteryUserInfo extends OpenLiveLotteryBaseUserInfo {
fans_medal_level: number
fans_medal_name: string //粉丝勋章名
fans_medal_wearing_status: boolean //该房间粉丝勋章佩戴情况
guard_level: number
}
interface LotteryOption { interface LotteryOption {
resultCount: number resultCount: number
lotteryType: 'single' | 'half' lotteryType: 'single' | 'half'
@@ -75,7 +68,7 @@ interface LotteryOption {
giftName?: string giftName?: string
} }
interface LotteryHistory { interface LotteryHistory {
users: OpenLiveLotteryBaseUserInfo[] users: OpenLiveLotteryUserInfo[]
time: number time: number
} }
const CMD_CALLBACK_MAP = { const CMD_CALLBACK_MAP = {
@@ -102,6 +95,9 @@ const notification = useNotification()
const authInfo = ref<AuthInfo>() const authInfo = ref<AuthInfo>()
const authResult = ref<OpenLiveInfo | null>(null) 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[]>([])
@@ -111,6 +107,7 @@ const isLottering = ref(false)
const isLotteried = ref(false) const isLotteried = ref(false)
const isConnected = ref(false) const isConnected = ref(false)
const showModal = ref(false) const showModal = ref(false)
const showOBSModal = ref(false)
let chatClient: any let chatClient: any
@@ -129,6 +126,30 @@ async function get() {
} }
return null return null
} }
async function getUsers() {
try {
const data = await QueryGetAPI<OpenLiveLotteryUserInfo[]>(LOTTERY_API_URL + 'live/get-users', {
code: code.value,
})
if (data.code == 200) {
console.log('[OPEN-LIVE] 已获历史抽奖用户')
return data.data
}
} catch (err) {
console.error(err)
}
return null
}
function updateUsers() {
QueryPostAPI(LOTTERY_API_URL + 'live/update-users', {
code: code.value,
users: originUsers.value,
resultUsers: resultUsers.value,
type: isLotteried.value ? OpenLiveLotteryType.Result : OpenLiveLotteryType.Waiting,
}).catch((err) => {
console.error('[OPEN-LIVE] 更新历史抽奖用户失败: ' + err)
})
}
async function start() { async function start() {
if (!chatClient) { if (!chatClient) {
const auth = await get() const auth = await get()
@@ -160,6 +181,7 @@ function addUser(user: OpenLiveLotteryUserInfo, danmu: any) {
originUsers.value.push(user) originUsers.value.push(user)
currentUsers.value.push(user) currentUsers.value.push(user)
console.log(`[OPEN-LIVE-Lottery] ${user.name} 添加到队列中`) console.log(`[OPEN-LIVE-Lottery] ${user.name} 添加到队列中`)
updateUsers()
} else { } else {
console.log(`[OPEN-LIVE-Lottery] ${user.name} 因不符合条件而被忽略`) console.log(`[OPEN-LIVE-Lottery] ${user.name} 因不符合条件而被忽略`)
} }
@@ -264,6 +286,7 @@ function onFinishLottery() {
message.success('已保存至历史') message.success('已保存至历史')
}, },
}) })
updateUsers()
lotteryHistory.value.push({ lotteryHistory.value.push({
users: currentUsers.value ?? [], users: currentUsers.value ?? [],
time: Date.now(), time: Date.now(),
@@ -272,6 +295,7 @@ function onFinishLottery() {
function reset() { function reset() {
currentUsers.value = JSON.parse(JSON.stringify(originUsers.value)) currentUsers.value = JSON.parse(JSON.stringify(originUsers.value))
isLotteried.value = false isLotteried.value = false
updateUsers()
} }
function clear() { function clear() {
originUsers.value = [] originUsers.value = []
@@ -279,10 +303,14 @@ function clear() {
resultUsers.value = [] resultUsers.value = []
currentUsers.value = [] currentUsers.value = []
message.success('已清空队列') message.success('已清空队列')
updateUsers()
} }
function removeUser(user: OpenLiveLotteryUserInfo) { function removeUser(user: OpenLiveLotteryUserInfo) {
currentUsers.value = currentUsers.value.filter((u) => u.uId != user.uId) currentUsers.value = currentUsers.value.filter((u) => u.uId != user.uId)
originUsers.value = originUsers.value.filter((u) => u.uId != user.uId) originUsers.value = originUsers.value.filter((u) => u.uId != user.uId)
updateUsers()
} }
function onDanmaku(command: any) { function onDanmaku(command: any) {
@@ -324,16 +352,51 @@ function continueLottery() {
message.info('开始监听') message.info('开始监听')
} }
onMounted(() => { let timer: any
onMounted(async () => {
authInfo.value = route.query as unknown as AuthInfo authInfo.value = route.query as unknown as AuthInfo
if (authInfo.value?.Code) {
const users = (await getUsers()) ?? []
originUsers.value = users
currentUsers.value = JSON.parse(JSON.stringify(users))
console.log('[OPEN-LIVE-Lottery] 从历史记录中加载 ' + users.length + ' 位用户')
if (users.length > 0) {
message.info('从历史记录中加载 ' + users.length + ' 位用户')
}
}
timer = setInterval(updateUsers, 1000 * 10)
const data: OpenLiveLotteryUserInfo[] = []
for (let i = 0; i < 13; i++) {
const userInfo: OpenLiveLotteryUserInfo = {
name: `User ${i + 1}`,
uId: i + 1,
level: i + 10,
avatar: `http://i0.hdslb.com/bfs/face/284f87fba8ff1b9c9564925747c7dc456df65cca.jpg`,
fans_medal_level: i + 1,
fans_medal_name: `Fans Medal ${i + 1}`,
fans_medal_wearing_status: true,
guard_level: i + 5,
}
data.push(userInfo)
}
originUsers.value = data
currentUsers.value = JSON.parse(JSON.stringify(data))
})
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
}) })
</script> </script>
<template> <template>
<NLayoutContent style="height: 100vh"> <NLayoutContent style="height: 100vh; padding: 20px">
<NResult v-if="!authInfo?.Code && !accountInfo" status="403" title="403" description="该页面只能从饭贩访问或者注册用户使用" /> <NResult v-if="!authInfo?.Code && !accountInfo" status="403" title="403" description="该页面只能从饭贩访问或者注册用户使用" />
<template v-else> <template v-else>
<NCard style="margin: 20px"> <NCard>
<template #header> <template #header>
直播抽奖 直播抽奖
<NDivider vertical /> <NDivider vertical />
@@ -349,6 +412,7 @@ onMounted(() => {
连接直播间 连接直播间
</NButton> </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>
</NSpace> </NSpace>
</NCard> </NCard>
<NCard size="small" embedded title="抽奖选项"> <NCard size="small" embedded title="抽奖选项">
@@ -503,5 +567,27 @@ onMounted(() => {
</NScrollbar> </NScrollbar>
<NEmpty v-else description="暂无记录" /> <NEmpty v-else description="暂无记录" />
</NModal> </NModal>
<NModal v-model:show="showOBSModal" preset="card" title="OBS 组件" style="max-width: 90%; width: 800px; max-height: 90vh" closable content-style="overflow: auto">
<NAlert title="这是什么? " type="info"> 将等待队列以及结果显示在OBS中 </NAlert>
<NDivider> 浏览 </NDivider>
<div style="height: 400px; width: 250px; position: relative; margin: 0 auto">
<LiveLotteryOBS :code="code" />
</div>
<br />
<NInput :value="'https://localhost:5173/obs/live-lottery?code=' + code" />
<NDivider />
<NCollapse>
<NCollapseItem title="使用说明">
<NUl>
<NLi> OBS 来源中添加源, 选择 浏览器</NLi>
<NLi> URL 栏填入上方链接</NLi>
<NLi>根据自己的需要调整宽度和高度</NLi>
<NLi>完事</NLi>
</NUl>
</NCollapseItem>
</NCollapse>
<NDivider />
</NModal>
</NLayoutContent> </NLayoutContent>
</template> </template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { NGridItem,NGrid } from 'naive-ui';
</script>
<template>
<NGrid>
<NGridItem>
</NGridItem>
</NGrid>
</template>

View File

@@ -3362,6 +3362,11 @@ vue3-aplayer@^1.7.3:
dependencies: dependencies:
vue-loader "^16.1.2" vue-loader "^16.1.2"
vue3-marquee@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/vue3-marquee/-/vue3-marquee-4.1.0.tgz#145baa65dd40059358e4079d51ab596c2ccf1299"
integrity sha512-AkvpNC6+7CwvIBgiAr8qMs1XvhGhfSS2ahlMEp80YXAmDOP8nDdn/smQ6eWtusf+hLX21yTaSOoKGcill4bCRg==
vue@^3.2.13, vue@^3.2.45: vue@^3.2.13, vue@^3.2.45:
version "3.3.7" version "3.3.7"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.3.7.tgz#972a218682443a3819d121261b2bff914417f4f0" resolved "https://registry.yarnpkg.com/vue/-/vue-3.3.7.tgz#972a218682443a3819d121261b2bff914417f4f0"