add not connect alert

This commit is contained in:
2023-11-17 22:18:27 +08:00
parent 2d9325821f
commit 2f9d9366c8
9 changed files with 468 additions and 206 deletions

View File

@@ -6,7 +6,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Permissions-Policy" content="interest-cohort=()"> <meta http-equiv="Permissions-Policy" content="interest-cohort=()">
<title>vtsuru.live</title> <title>vtsuru.live</title>
<meta name="description" content="主包工具站" /> <meta name="description" content="为主播提供便利功能的网站" />
<script async src="https://umami.vtsuru.live/script.js" data-website-id="05567214-d234-4076-9228-e4d69e3d202f"></script>
</head> </head>
<body> <body>

View File

@@ -47,6 +47,9 @@ function refreshCookie() {
} }
}) })
} }
export async function SaveAccountSettings() {
return await QueryPostAPI(ACCOUNT_API_URL + 'update-setting', ACCOUNT.value?.settings)
}
export function useAccount() { export function useAccount() {
return ACCOUNT return ACCOUNT
} }

View File

@@ -60,16 +60,38 @@ export interface Setting_QuestionBox {
export interface UserSetting { export interface UserSetting {
sendEmail: Setting_SendEmail sendEmail: Setting_SendEmail
questionBox: Setting_QuestionBox questionBox: Setting_QuestionBox
songRequest: Setting_SongRequest
enableFunctions: FunctionTypes[] enableFunctions: FunctionTypes[]
indexTemplate: string | null, indexTemplate: string | null,
songListTemplate: string | null songListTemplate: string | null
scheduleTemplate: string | null scheduleTemplate: string | null
} }
export interface Setting_SongRequest {
orderPrefix: string
onlyAllowSongList: boolean
queueMaxSize: number
allowAllDanmaku: boolean
allowFromWeb: boolean
needWearFanMedal: boolean
needJianzhang: boolean
needTidu: boolean
needZongdu: boolean
allowSC: boolean
sCIgnoreLimit: boolean
sCMinPrice: number
fanMedalMinLevel: number
allowReorderSong: boolean
cooldownSecond: number
zongduCooldownSecond: number
tiduCooldownSecond: number
jianzhangCooldownSecond: number
}
export enum FunctionTypes { export enum FunctionTypes {
SongList, SongList,
QuestionBox, QuestionBox,
Schedule, Schedule,
SongRequest,
} }
export interface SongAuthorInfo { export interface SongAuthorInfo {
name: string name: string
@@ -257,6 +279,7 @@ export interface UpdateLiveLotteryUsersModel {
type: OpenLiveLotteryType type: OpenLiveLotteryType
} }
export interface SongRequestInfo { export interface SongRequestInfo {
id: number
songName: string songName: string
song?: SongsInfo song?: SongsInfo
status: SongRequestStatus status: SongRequestStatus
@@ -267,11 +290,11 @@ export interface SongRequestInfo {
} }
export interface SongRequestUserInfo { export interface SongRequestUserInfo {
name: string name: string
uId: number uid: number
guardLevel: number guard_level: number
fansMedalLevel: number fans_medal_level: number
fansMedalName: string fans_medal_name: string
fansMedalWearingStatus: boolean fans_medal_wearing_status: boolean
} }
export enum SongRequestFrom { export enum SongRequestFrom {
@@ -287,3 +310,23 @@ export enum SongRequestStatus {
Finish, Finish,
Cancel, Cancel,
} }
export interface EventModel {
type: EventDataTypes
name: string
avatar: string
uid: number
msg: string
time: number
num: number
price: number
guard_level: number
fans_medal_level: number
fans_medal_name: string
fans_medal_wearing_status: boolean
}
export enum EventDataTypes {
Guard,
SC,
Gift,
Message,
}

View File

@@ -23,6 +23,8 @@ export const HISTORY_API_URL = `${BASE_API}history/`
export const SCHEDULE_API_URL = `${BASE_API}schedule/` export const SCHEDULE_API_URL = `${BASE_API}schedule/`
export const VIDEO_COLLECT_API_URL = `${BASE_API}video-collect/` export const VIDEO_COLLECT_API_URL = `${BASE_API}video-collect/`
export const OPEN_LIVE_API_URL = `${BASE_API}open-live/` export const OPEN_LIVE_API_URL = `${BASE_API}open-live/`
export const SONG_REQUEST_API_URL = `${BASE_API}song-request/`
export const EVENT_API_URL = `${BASE_API}event/`
export const ScheduleTemplateMap = { export const ScheduleTemplateMap = {
'': { name: '默认', compoent: defineAsyncComponent(() => import('@/views/view/scheduleTemplate/DefaultScheduleTemplate.vue')) }, '': { name: '默认', compoent: defineAsyncComponent(() => import('@/views/view/scheduleTemplate/DefaultScheduleTemplate.vue')) },

View File

@@ -1,6 +1,6 @@
import { QueryGetAPI } from '@/api/query' import { QueryGetAPI } from '@/api/query'
import { useRequest } from 'vue-request' import { useRequest } from 'vue-request'
import { NOTIFACTION_API_URL, isBackendUsable } from './constants' import { NOTIFACTION_API_URL, SONG_REQUEST_API_URL, isBackendUsable } from './constants'
import { NotifactionInfo } from '@/api/api-models' import { NotifactionInfo } from '@/api/api-models'
import { useAccount } from '@/api/account' import { useAccount } from '@/api/account'
import { ref } from 'vue' import { ref } from 'vue'
@@ -10,7 +10,7 @@ const n = ref<NotifactionInfo>()
let isLoading = false let isLoading = false
function get() { function get() {
if (isLoading) return if (isLoading) return
QueryGetAPI<NotifactionInfo>(NOTIFACTION_API_URL + 'get') QueryGetAPI<NotifactionInfo>(SONG_REQUEST_API_URL + 'get-active')
.then((data) => { .then((data) => {
if (data.code == 200) { if (data.code == 200) {
n.value = data.data n.value = data.data

View File

@@ -29,8 +29,9 @@ import {
useMessage, useMessage,
NDivider, NDivider,
NTag, NTag,
NAlert,
} from 'naive-ui' } from 'naive-ui'
import { ref, onMounted, h } from 'vue' import { ref, onMounted, h, onUnmounted } from 'vue'
import { RouterLink, useRoute } from 'vue-router' import { RouterLink, useRoute } from 'vue-router'
const route = useRoute() const route = useRoute()
@@ -89,6 +90,9 @@ onMounted(async () => {
message.error('你不是从幻星平台访问此页面, 或未提供对应参数, 无法使用此功能') message.error('你不是从幻星平台访问此页面, 或未提供对应参数, 无法使用此功能')
} }
}) })
onUnmounted(() => {
client.value?.Stop()
})
</script> </script>
<template> <template>
@@ -108,7 +112,7 @@ onMounted(async () => {
<NPageHeader :subtitle="($route.meta.title as string) ?? ''"> <NPageHeader :subtitle="($route.meta.title as string) ?? ''">
<template #extra> <template #extra>
<NSpace align="center"> <NSpace align="center">
<NTag :type="client ? 'success' : 'warning'"> {{ client ? `已连接 | ${client.roomAuthInfo.value?.anchor_info.uname}` : '未连接' }} </NTag> <NTag :type="client?.roomAuthInfo.value ? 'success' : 'warning'"> {{ client?.roomAuthInfo.value ? `已连接 | ${client.roomAuthInfo.value?.anchor_info.uname}` : '未连接' }} </NTag>
<NSwitch :default-value="!isDarkMode()" @update:value="(value: string & number & boolean) => themeType = value ? ThemeType.Light : ThemeType.Dark"> <NSwitch :default-value="!isDarkMode()" @update:value="(value: string & number & boolean) => themeType = value ? ThemeType.Light : ThemeType.Dark">
<template #checked> <template #checked>
<NIcon :component="Sunny" /> <NIcon :component="Sunny" />
@@ -152,14 +156,16 @@ onMounted(async () => {
</NText> </NText>
</NSpace> </NSpace>
</NLayoutSider> </NLayoutSider>
<NLayoutContent yle="height: 100%" :native-scrollbar="false"> <NLayoutContent style="height: 100%; padding: 10px;" :native-scrollbar="false">
<RouterView v-if="client?.roomAuthInfo" v-slot="{ Component }"> <RouterView v-if="client?.roomAuthInfo.value" v-slot="{ Component }">
<KeepAlive> <KeepAlive>
<component :is="Component" :room-info="client?.roomAuthInfo" :client="client" :code="authInfo.Code" /> <component :is="Component" :room-info="client?.roomAuthInfo" :client="client" :code="authInfo.Code" />
</KeepAlive> </KeepAlive>
</RouterView> </RouterView>
<template v-else> <template v-else>
<NAlert type="info" title="正在请求弹幕客户端认证信息...">
<NSpin show /> <NSpin show />
</NAlert>
</template> </template>
<NBackTop /> <NBackTop />
</NLayoutContent> </NLayoutContent>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAccount } from '@/api/account' import { SaveAccountSettings, useAccount } from '@/api/account'
import { NButton, NCard, NCheckbox, NCheckboxGroup, NDivider, NForm, NModal, NSelect, NSpace, NSpin, NSwitch, NTabPane, NTabs, SelectOption, useMessage } from 'naive-ui' import { NButton, NCard, NCheckbox, NCheckboxGroup, NDivider, NForm, NModal, NSelect, NSpace, NSpin, NSwitch, NTabPane, NTabs, SelectOption, useMessage } from 'naive-ui'
import { Ref, computed, h, onMounted, ref, defineAsyncComponent, onActivated } from 'vue' import { Ref, computed, h, onMounted, ref, defineAsyncComponent, onActivated } from 'vue'
import { FunctionTypes, ScheduleWeekInfo, SongFrom, SongLanguage, SongsInfo } from '@/api/api-models' import { FunctionTypes, ScheduleWeekInfo, SongFrom, SongLanguage, SongsInfo } from '@/api/api-models'
@@ -216,10 +216,10 @@ async function SaveComboGroupSetting(value: (string | number)[], meta: { actionT
} }
} }
async function SaveComboSetting() { async function SaveComboSetting() {
if (accountInfo.value) {
isSaving.value = true isSaving.value = true
if (accountInfo.value) {
//UpdateEnableFunction(meta.value as FunctionTypes, meta.actionType == 'check') //UpdateEnableFunction(meta.value as FunctionTypes, meta.actionType == 'check')
await QueryPostAPI(ACCOUNT_API_URL + 'update-setting', accountInfo.value?.settings) await SaveAccountSettings()
.then((data) => { .then((data) => {
if (data.code == 200) { if (data.code == 200) {
message.success('已保存') message.success('已保存')

View File

@@ -1,46 +1,245 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAccount } from '@/api/account' import { SaveAccountSettings, useAccount } from '@/api/account'
import { SongRequestInfo } from '@/api/api-models' import { EventDataTypes, EventModel, FunctionTypes, OpenLiveInfo, Setting_SongRequest, SongRequestFrom, SongRequestInfo, SongRequestStatus, SongRequestUserInfo } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI, QueryPostAPIWithParams } from '@/api/query'
import DanmakuClient, { AuthInfo, DanmakuInfo, RoomAuthInfo, SCInfo } from '@/data/DanmakuClient' import DanmakuClient, { AuthInfo, DanmakuInfo, RoomAuthInfo, SCInfo } from '@/data/DanmakuClient'
import { NList, NTabPane, NTabs, useMessage } from 'naive-ui' import { OPEN_LIVE_API_URL, SONG_REQUEST_API_URL } from '@/data/constants'
import { onMounted, onUnmounted, ref } from 'vue' import { Mic24Filled, PeopleQueue24Filled } from '@vicons/fluent'
import { useStorage } from '@vueuse/core'
import { number } from 'echarts'
import { NAlert, NButton, NCard, NDivider, NIcon, NInput, NInputGroup, NList, NListItem, NSpace, NSwitch, NTabPane, NTabs, NTag, NText, NTooltip, useMessage } from 'naive-ui'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
const defaultSettings = {
orderPrefix: '点歌',
onlyAllowSongList: false,
queueMaxSize: 10,
allowAllDanmaku: false,
allowFromWeb: true,
needWearFanMedal: false,
needJianzhang: false,
needTidu: false,
needZongdu: false,
allowSC: true,
sCIgnoreLimit: true,
sCMinPrice: 30,
fanMedalMinLevel: 0,
allowReorderSong: false,
cooldownSecond: 1200,
zongduCooldownSecond: 300,
tiduCooldownSecond: 600,
jianzhangCooldownSecond: 900,
} as Setting_SongRequest
const route = useRoute() const route = useRoute()
const accountInfo = useAccount() const accountInfo = useAccount()
const message = useMessage() const message = useMessage()
const settings = computed({
get: () => {
if (accountInfo.value) {
return accountInfo.value.settings.songRequest
}
return defaultSettings
},
set: (value) => {
if (accountInfo.value) {
accountInfo.value.settings.songRequest = value
}
},
})
const props = defineProps<{ const props = defineProps<{
client: DanmakuClient client: DanmakuClient
roomInfo: RoomAuthInfo roomInfo: RoomAuthInfo
code: string | undefined code: string | undefined
isOpenLive?: boolean
}>() }>()
const activeSongs = ref<SongRequestInfo[]>([]) const localActiveSongs = useStorage('SongRequest.ActiveSongs', [] as SongRequestInfo[])
const activeSongs = ref<SongRequestInfo[]>(await getActiveSong())
const newSongName = ref('')
const defaultPrefix = useStorage('Settings.SongRequest.DefaultPrefix', '点歌')
async function getActiveSong() {
if (accountInfo.value) {
try {
const data = await QueryGetAPI<SongRequestInfo[]>(SONG_REQUEST_API_URL + 'get-active', {
id: accountInfo.value.id,
})
if (data.code == 200) {
console.log('[OPEN-LIVE-Song-Request] 已获取点歌队列')
return data.data
} else {
message.error('无法获取点歌队列: ' + data.message)
return []
}
} catch (err) {
console.error(err)
}
return []
} else {
return localActiveSongs.value
}
}
async function addSong(danmaku: EventModel) {
console.log(`[OPEN-LIVE-Song-Request] 收到 [${danmaku.name}] 的点歌${danmaku.type == EventDataTypes.SC ? 'SC' : '弹幕'}: ${danmaku.msg}`)
if (accountInfo.value) {
await QueryPostAPI<SongRequestInfo>(SONG_REQUEST_API_URL + 'try-add', danmaku)
.then((data) => {
if (data.code == 200) {
message.success(`[${danmaku.name}] 添加曲目: ${data.data.songName}`)
} else {
message.error(`[${danmaku.name}] 添加曲目失败: ${data.message}`)
console.log(`[OPEN-LIVE-Song-Request] [${danmaku.name}] 添加曲目失败: ${data.message}`)
}
})
.catch((err) => {
console.error(err)
})
} else {
const songData = {
songName: danmaku.msg.trim().substring(settings.value.orderPrefix.length),
song: undefined,
status: SongRequestStatus.Waiting,
from: danmaku.type == EventDataTypes.Message ? SongRequestFrom.Danmaku : SongRequestFrom.SC,
scPrice: danmaku.type == EventDataTypes.SC ? danmaku.price : 0,
user: {
name: danmaku.name,
uid: danmaku.uid,
fans_medal_level: danmaku.fans_medal_level,
fans_medal_name: danmaku.fans_medal_name,
fans_medal_wearing_status: danmaku.fans_medal_wearing_status,
guard_level: danmaku.guard_level,
} as SongRequestUserInfo,
createAt: Date.now(),
} as SongRequestInfo
localActiveSongs.value.push(songData)
message.success(`[${danmaku.name}] 添加曲目: ${songData.songName}`)
}
}
async function addSongManual() {
if (!newSongName.value) {
message.error('请输入曲目名')
return
}
if (accountInfo.value) {
await QueryPostAPIWithParams<SongRequestInfo>(SONG_REQUEST_API_URL + 'add', {
name: newSongName.value,
})
.then((data) => {
if (data.code == 200) {
message.success(`已手动添加曲目: ${data.data.songName}`)
} else {
message.error(`手动添加曲目失败: ${data.message}`)
}
})
.catch((err) => {
console.error(err)
})
} else {
const songData = {
songName: newSongName.value,
song: undefined,
status: SongRequestStatus.Waiting,
from: SongRequestFrom.Manual,
scPrice: undefined,
user: undefined,
createAt: Date.now(),
} as SongRequestInfo
localActiveSongs.value.push(songData)
message.success(`已手动添加曲目: ${songData.songName}`)
}
}
function onGetDanmaku(danmaku: DanmakuInfo) { function onGetDanmaku(danmaku: DanmakuInfo) {
console.log(danmaku) if (checkMessage(danmaku.msg)) {
addSong({
msg: danmaku.msg,
type: EventDataTypes.Message,
time: danmaku.timestamp,
uid: danmaku.uid,
name: danmaku.uname,
avatar: danmaku.uface,
fans_medal_level: danmaku.fans_medal_level,
fans_medal_name: danmaku.fans_medal_name,
fans_medal_wearing_status: danmaku.fans_medal_wearing_status,
guard_level: danmaku.guard_level,
num: 1,
price: 0,
} as EventModel)
}
} }
function onGetSC(danmaku: SCInfo) { function onGetSC(danmaku: SCInfo) {
console.log(danmaku) if (settings.value.allowSC && checkMessage(danmaku.message)) {
addSong({
msg: danmaku.message,
type: EventDataTypes.SC,
time: danmaku.timestamp,
uid: danmaku.uid,
name: danmaku.uname,
fans_medal_level: danmaku.fans_medal_level,
fans_medal_name: danmaku.fans_medal_name,
fans_medal_wearing_status: danmaku.fans_medal_wearing_status,
guard_level: danmaku.guard_level,
avatar: danmaku.uface,
num: 1,
price: danmaku.rmb,
} as EventModel)
}
}
function checkMessage(msg: string) {
return msg
.trim()
.toLowerCase()
.startsWith(accountInfo.value ? settings.value.orderPrefix : defaultPrefix.value)
}
async function onUpdateFunctionEnable() {
if (accountInfo.value) {
const oldValue = JSON.parse(JSON.stringify(accountInfo.value.settings.enableFunctions))
if (accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest)) {
accountInfo.value.settings.enableFunctions = accountInfo.value.settings.enableFunctions.filter((f) => f != FunctionTypes.SongRequest)
} else {
accountInfo.value.settings.enableFunctions.push(FunctionTypes.SongRequest)
}
accountInfo.value.settings.enableFunctions.push
await SaveAccountSettings()
.then((data) => {
if (data.code == 200) {
message.success(`${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? '启用' : '禁用'}点歌功能`)
} else {
if (accountInfo.value) {
accountInfo.value.settings.enableFunctions = oldValue
}
message.error(`点歌功能${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? '启用' : '禁用'}失败: ${data.message}`)
}
})
.catch((err) => {
console.error(err)
message.error(`点歌功能${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? '启用' : '禁用'}失败: ${err}`)
})
}
} }
let timer: any
onMounted(() => { onMounted(() => {
if (accountInfo.value) {
settings.value = accountInfo.value.settings.songRequest
}
props.client.on('danmaku', onGetDanmaku) props.client.on('danmaku', onGetDanmaku)
props.client.on('sc', onGetSC) props.client.on('sc', onGetSC)
timer = setInterval(() => {})
}) })
onUnmounted(() => { onUnmounted(() => {
props.client.off('danmaku', onGetDanmaku) props.client.off('danmaku', onGetDanmaku)
props.client.off('sc', onGetSC) props.client.off('sc', onGetSC)
clearInterval(timer)
}) })
</script> </script>
<template> <template>
开发中... 开发中...
<NTabs animated>
<NTabPane name="list" tab="列表">
<NList> </NList>
</NTabPane>
<NTabPane name="history" tab="历史"> </NTabPane>
</NTabs>
</template> </template>

View File

@@ -333,7 +333,6 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<NLayoutContent style="height: 100vh; padding: 20px">
<NResult v-if="!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>
@@ -435,7 +434,17 @@ onUnmounted(() => {
</NSpace> </NSpace>
<NDivider style="margin: 20px 0 20px 0"> <template v-if="isStartLottery"> 进行抽取前需要先停止 </template> </NDivider> <NDivider style="margin: 20px 0 20px 0"> <template v-if="isStartLottery"> 进行抽取前需要先停止 </template> </NDivider>
<NSpace justify="center"> <NSpace justify="center">
<NButton type="primary" secondary @click="startLottery" :loading="isLottering" :disabled="isStartLottery || isLotteried"> 进行抽取 </NButton> <NButton
type="primary"
secondary
@click="startLottery"
:loading="isLottering"
:disabled="isStartLottery || isLotteried"
data-umami-event="Open-Live Use Lottery"
:data-umami-event-uid="client.roomAuthInfo.value?.anchor_info.uid"
>
进行抽取
</NButton>
<NButton type="info" secondary :disabled="isStartLottery || isLottering || !isLotteried" @click="reset"> 重置 </NButton> <NButton type="info" secondary :disabled="isStartLottery || isLottering || !isLotteried" @click="reset"> 重置 </NButton>
</NSpace> </NSpace>
<NDivider style="margin: 10px 0 10px 0"> {{ currentUsers?.length }} </NDivider> <NDivider style="margin: 10px 0 10px 0"> {{ currentUsers?.length }} </NDivider>
@@ -520,6 +529,4 @@ onUnmounted(() => {
<NDivider /> <NDivider />
</NModal> </NModal>
</NLayoutContent>
</template> </template>
@/data/DanmakuClient