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-turnstile": "^1.0.0",
"vue3-aplayer": "^1.7.3",
"vue3-marquee": "^4.1.0",
"vuex": "^4.0.0"
},
"devDependencies": {

View File

@@ -2,18 +2,21 @@
<NMessageProvider>
<NNotificationProvider>
<NConfigProvider :theme-overrides="themeOverrides" :theme="theme" style="height: 100vh" :locale="zhCN" :date-locale="dateZhCN">
<NElement style="height: 100vh">
<Suspense>
<ViewerLayout v-if="layout == 'viewer'" />
<ManageLayout v-else-if="layout == 'manage'" />
<template v-else>
<RouterView />
</template>
<template #fallback>
<NSpin size="large" show/>
</template>
</Suspense>
</NElement>
<Suspense>
<div style="height: 100vh">
<NElement style="height: 100%;" v-if="layout != 'obs'">
<ViewerLayout v-if="layout == 'viewer'" />
<ManageLayout v-else-if="layout == 'manage'" />
<template v-else-if="layout == ''">
<RouterView />
</template>
</NElement>
<RouterView v-else/>
</div>
<template #fallback>
<NSpin size="large" show />
</template>
</Suspense>
</NConfigProvider>
</NNotificationProvider>
</NMessageProvider>
@@ -38,6 +41,9 @@ const layout = computed(() => {
} else if (route.path.startsWith('/manage')) {
document.title = route.meta.title + ' · 管理 · VTsuru'
return 'manage'
} else if (route.path.startsWith('/obs')) {
document.title = route.meta.title + ' · OBS · VTsuru'
return 'obs'
} else {
document.title = route.meta.title + ' · VTsuru'
return ''

View File

@@ -36,6 +36,7 @@ export interface AccountInfo extends UserInfo {
settings: UserSetting
token: string
biliAuthCode?: string
biliAuthCodeStatus: BiliAuthCodeStatusType
eventFetcherOnline: boolean
@@ -236,3 +237,22 @@ export interface OpenLiveInfo {
websocket_info: WebsocketInfo
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(.*)*',
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">
import { computed, h, onMounted, ref } from 'vue'
import { computed, h, onMounted, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { QueryPostAPI } from '@/api/query'
import { OPEN_LIVE_API_URL } from '@/data/constants'
import { LotteryUserInfo, OpenLiveInfo } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import { LOTTERY_API_URL, OPEN_LIVE_API_URL } from '@/data/constants'
import { LotteryUserInfo, OpenLiveInfo, OpenLiveLotteryType, OpenLiveLotteryUserInfo } from '@/api/api-models'
import {
NAlert,
NAvatar,
NButton,
NCard,
NCheckbox,
NCollapse,
NCollapseItem,
NCollapseTransition,
NDivider,
NEmpty,
@@ -21,6 +23,7 @@ import {
NInputGroupLabel,
NInputNumber,
NLayoutContent,
NLi,
NList,
NListItem,
NModal,
@@ -33,6 +36,7 @@ import {
NTag,
NTime,
NTooltip,
NUl,
useMessage,
useNotification,
} from 'naive-ui'
@@ -41,6 +45,7 @@ import ChatClientDirectOpenLive from '@/data/chat/ChatClientDirectOpenLive.js'
import { useLocalStorage, useStorage } from '@vueuse/core'
import { format } from 'date-fns'
import { Delete24Filled, Info24Filled } from '@vicons/fluent'
import LiveLotteryOBS from '../obs/LiveLotteryOBS.vue'
interface AuthInfo {
Timestamp: string
@@ -49,18 +54,6 @@ interface AuthInfo {
Caller: 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 {
resultCount: number
lotteryType: 'single' | 'half'
@@ -75,7 +68,7 @@ interface LotteryOption {
giftName?: string
}
interface LotteryHistory {
users: OpenLiveLotteryBaseUserInfo[]
users: OpenLiveLotteryUserInfo[]
time: number
}
const CMD_CALLBACK_MAP = {
@@ -102,6 +95,9 @@ 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[]>([])
@@ -111,6 +107,7 @@ const isLottering = ref(false)
const isLotteried = ref(false)
const isConnected = ref(false)
const showModal = ref(false)
const showOBSModal = ref(false)
let chatClient: any
@@ -129,6 +126,30 @@ async function get() {
}
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() {
if (!chatClient) {
const auth = await get()
@@ -160,6 +181,7 @@ function addUser(user: OpenLiveLotteryUserInfo, danmu: any) {
originUsers.value.push(user)
currentUsers.value.push(user)
console.log(`[OPEN-LIVE-Lottery] ${user.name} 添加到队列中`)
updateUsers()
} else {
console.log(`[OPEN-LIVE-Lottery] ${user.name} 因不符合条件而被忽略`)
}
@@ -264,6 +286,7 @@ function onFinishLottery() {
message.success('已保存至历史')
},
})
updateUsers()
lotteryHistory.value.push({
users: currentUsers.value ?? [],
time: Date.now(),
@@ -272,6 +295,7 @@ function onFinishLottery() {
function reset() {
currentUsers.value = JSON.parse(JSON.stringify(originUsers.value))
isLotteried.value = false
updateUsers()
}
function clear() {
originUsers.value = []
@@ -279,10 +303,14 @@ function clear() {
resultUsers.value = []
currentUsers.value = []
message.success('已清空队列')
updateUsers()
}
function removeUser(user: OpenLiveLotteryUserInfo) {
currentUsers.value = currentUsers.value.filter((u) => u.uId != user.uId)
originUsers.value = originUsers.value.filter((u) => u.uId != user.uId)
updateUsers()
}
function onDanmaku(command: any) {
@@ -324,16 +352,51 @@ function continueLottery() {
message.info('开始监听')
}
onMounted(() => {
let timer: any
onMounted(async () => {
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>
<template>
<NLayoutContent style="height: 100vh">
<NLayoutContent style="height: 100vh; padding: 20px">
<NResult v-if="!authInfo?.Code && !accountInfo" status="403" title="403" description="该页面只能从饭贩访问或者注册用户使用" />
<template v-else>
<NCard style="margin: 20px">
<NCard>
<template #header>
直播抽奖
<NDivider vertical />
@@ -349,6 +412,7 @@ onMounted(() => {
连接直播间
</NButton>
<NButton type="info" @click="showModal = true" size="small"> 抽奖历史</NButton>
<NButton type="success" @click="showOBSModal = true" size="small"> OBS组件</NButton>
</NSpace>
</NCard>
<NCard size="small" embedded title="抽奖选项">
@@ -503,5 +567,27 @@ onMounted(() => {
</NScrollbar>
<NEmpty v-else description="暂无记录" />
</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>
</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:
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:
version "3.3.7"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.3.7.tgz#972a218682443a3819d121261b2bff914417f4f0"