fix: no voice in speech page; custom personal page redirect notworking. feat: sync sroll bar between question display page an obs component

This commit is contained in:
2024-11-23 18:46:37 +08:00
parent 14267bab3a
commit 47ade4a965
33 changed files with 838 additions and 1119 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -34,6 +34,7 @@
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"linqts": "^2.0.0", "linqts": "^2.0.0",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"monaco-editor": "^0.52.0",
"music-metadata-browser": "^2.5.11", "music-metadata-browser": "^2.5.11",
"peerjs": "^1.5.4", "peerjs": "^1.5.4",
"pinia": "^2.2.6", "pinia": "^2.2.6",
@@ -43,6 +44,7 @@
"unplugin-vue-markdown": "^0.26.2", "unplugin-vue-markdown": "^0.26.2",
"uuid": "^11.0.2", "uuid": "^11.0.2",
"vite": "^5.4.10", "vite": "^5.4.10",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-svg-loader": "^5.1.0", "vite-svg-loader": "^5.1.0",
"vue": "3.5.12", "vue": "3.5.12",
"vue-echarts": "^7.0.3", "vue-echarts": "^7.0.3",

View File

@@ -229,6 +229,7 @@ export interface Setting_QuestionDisplay {
borderColor?: string borderColor?: string
borderWidth?: number borderWidth?: number
syncScroll: boolean
currentQuestion?: number currentQuestion?: number
} }

View File

@@ -0,0 +1,32 @@
<template>
<div ref="editorContainer" :style="`height: ${height}px;`"></div>
</template>
<script setup lang="ts">
import { editor } from 'monaco-editor'; // 全部导入
import { ref, onMounted } from 'vue';
const value = defineModel<string>('value')
const { language, height = 400 } = defineProps<{
language: string
height?: number
}>()
const editorContainer = ref()
onMounted(() => {
const e = editor.create(editorContainer.value, {
value: value.value,
language: language,
minimap: {
enabled: true
},
colorDecorators: true,
automaticLayout: true
})
e.onDidChangeModelContent(() => {
value.value = e.getValue()
})
})
</script>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { VideoCollectTable } from '@/api/api-models' import { VideoCollectTable } from '@/api/api-models'
import { CURRENT_HOST } from '@/data/constants';
import router from '@/router' import router from '@/router'
import { Clock24Regular, NumberRow24Regular } from '@vicons/fluent' import { Clock24Regular, NumberRow24Regular } from '@vicons/fluent'
import { import {
@@ -28,7 +29,7 @@ const renderCountdown: CountdownProps['render'] = (info: { hours: number; minute
function onClick() { function onClick() {
if (props.canClick == true) { if (props.canClick == true) {
if (props.from == 'user') { if (props.from == 'user') {
window.open('https://vtsuru.live/video-collect/' + props.item.shortId, '_blank') window.open(`${CURRENT_HOST}video-collect/` + props.item.shortId, '_blank')
} else { } else {
router.push({ name: 'manage-videoCollect-Detail', params: { id: props.item.id } }) router.push({ name: 'manage-videoCollect-Detail', params: { id: props.item.id } })
} }

View File

@@ -11,7 +11,7 @@ export interface RTCData {
Data: any Data: any
} }
abstract class BaseRTCClient { export abstract class BaseRTCClient {
constructor(user: string, pass: string) { constructor(user: string, pass: string) {
this.user = user this.user = user
this.pass = pass this.pass = pass

View File

@@ -28,6 +28,8 @@ export const BASE_HUB_URL = {
export const TURNSTILE_KEY = '0x4AAAAAAAETUSAKbds019h0' export const TURNSTILE_KEY = '0x4AAAAAAAETUSAKbds019h0'
export const CURRENT_HOST = `${window.location.protocol}//${window.location.host}/`
export const USER_API_URL = { toString: () => `${BASE_API_URL}user/` } export const USER_API_URL = { toString: () => `${BASE_API_URL}user/` }
export const ACCOUNT_API_URL = { toString: () => `${BASE_API_URL}account/` } export const ACCOUNT_API_URL = { toString: () => `${BASE_API_URL}account/` }
export const BILI_API_URL = { toString: () => `${BASE_API_URL}bili/` } export const BILI_API_URL = { toString: () => `${BASE_API_URL}bili/` }

View File

@@ -13,13 +13,6 @@ import { useVTsuruHub } from './store/useVTsuruHub'
const pinia = createPinia() const pinia = createPinia()
const app = createApp(App)
app.use(router).use(pinia).mount('#app')
let currentVersion: string
let isHaveNewVersion = false
const { notification } = createDiscreteApi(['notification'])
QueryGetAPI<string>(BASE_API_URL + 'vtsuru/version') QueryGetAPI<string>(BASE_API_URL + 'vtsuru/version')
.then((version) => { .then((version) => {
if (version.code == 200) { if (version.code == 200) {
@@ -99,6 +92,7 @@ QueryGetAPI<string>(BASE_API_URL + 'vtsuru/version')
}) })
.finally(async () => { .finally(async () => {
//加载其他数据 //加载其他数据
InitTTS()
await GetSelfAccount() await GetSelfAccount()
const account = useAccount() const account = useAccount()
const useAuth = useAuthStore() const useAuth = useAuthStore()
@@ -110,8 +104,16 @@ QueryGetAPI<string>(BASE_API_URL + 'vtsuru/version')
useAuth.getAuthInfo() useAuth.getAuthInfo()
GetNotifactions() GetNotifactions()
UpdateAccountLoop() UpdateAccountLoop()
InitTTS()
}) })
const app = createApp(App)
app.use(router).use(pinia).mount('#app')
let currentVersion: string
let isHaveNewVersion = false
const { notification } = createDiscreteApi(['notification'])
function InitTTS() { function InitTTS() {
try { try {
const result = EasySpeech.detect() const result = EasySpeech.detect()

View File

@@ -8,8 +8,8 @@ export default //管理页面
name: 'manage-index', name: 'manage-index',
component: () => import('@/views/manage/DashboardView.vue'), component: () => import('@/views/manage/DashboardView.vue'),
meta: { meta: {
title: '面板', title: '面板'
}, }
}, },
{ {
path: 'song-list', path: 'song-list',
@@ -17,8 +17,8 @@ export default //管理页面
component: () => import('@/views/manage/SongListManageView.vue'), component: () => import('@/views/manage/SongListManageView.vue'),
meta: { meta: {
title: '歌单', title: '歌单',
keepAlive: true, keepAlive: true
}, }
}, },
{ {
path: 'question-box', path: 'question-box',
@@ -26,8 +26,8 @@ export default //管理页面
component: () => import('@/views/manage/QuestionBoxManageView.vue'), component: () => import('@/views/manage/QuestionBoxManageView.vue'),
meta: { meta: {
title: '提问箱', title: '提问箱',
keepAlive: true, keepAlive: true
}, }
}, },
{ {
path: 'lottery', path: 'lottery',
@@ -35,8 +35,8 @@ export default //管理页面
component: () => import('@/views/manage/LotteryView.vue'), component: () => import('@/views/manage/LotteryView.vue'),
meta: { meta: {
title: '动态抽奖', title: '动态抽奖',
keepAlive: true, keepAlive: true
}, }
}, },
{ {
path: 'history', path: 'history',
@@ -44,8 +44,8 @@ export default //管理页面
component: () => import('@/views/manage/HistoryView.vue'), component: () => import('@/views/manage/HistoryView.vue'),
meta: { meta: {
title: '数据跟踪', title: '数据跟踪',
keepAlive: true, keepAlive: true
}, }
}, },
{ {
path: 'schedule', path: 'schedule',
@@ -53,8 +53,8 @@ export default //管理页面
component: () => import('@/views/manage/ScheduleManageView.vue'), component: () => import('@/views/manage/ScheduleManageView.vue'),
meta: { meta: {
title: '日程', title: '日程',
keepAlive: true, keepAlive: true
}, }
}, },
{ {
path: 'event', path: 'event',
@@ -62,8 +62,8 @@ export default //管理页面
component: () => import('@/views/manage/EventView.vue'), component: () => import('@/views/manage/EventView.vue'),
meta: { meta: {
title: '事件记录', title: '事件记录',
keepAlive: true, keepAlive: true
}, }
}, },
{ {
path: 'video-collect', path: 'video-collect',
@@ -71,8 +71,8 @@ export default //管理页面
component: () => import('@/views/manage/VideoCollectManageView.vue'), component: () => import('@/views/manage/VideoCollectManageView.vue'),
meta: { meta: {
title: '视频征集', title: '视频征集',
keepAlive: true, keepAlive: true
}, }
}, },
{ {
path: 'video-collect/:id', path: 'video-collect/:id',
@@ -80,8 +80,8 @@ export default //管理页面
component: () => import('@/views/manage/VideoCollectDetailView.vue'), component: () => import('@/views/manage/VideoCollectDetailView.vue'),
meta: { meta: {
title: '详情 · 视频征集', title: '详情 · 视频征集',
parent: 'manage-videoCollect', parent: 'manage-videoCollect'
}, }
}, },
{ {
path: 'live-lottery', path: 'live-lottery',
@@ -90,8 +90,8 @@ export default //管理页面
meta: { meta: {
title: '直播抽奖', title: '直播抽奖',
keepAlive: true, keepAlive: true,
danmaku: true, danmaku: true
}, }
}, },
{ {
path: 'queue', path: 'queue',
@@ -100,8 +100,8 @@ export default //管理页面
meta: { meta: {
title: '排队', title: '排队',
keepAlive: true, keepAlive: true,
danmaku: true, danmaku: true
}, }
}, },
{ {
path: 'speech', path: 'speech',
@@ -110,8 +110,8 @@ export default //管理页面
meta: { meta: {
title: '读弹幕', title: '读弹幕',
keepAlive: true, keepAlive: true,
danmaku: true, danmaku: true
}, }
}, },
{ {
path: 'live-request', path: 'live-request',
@@ -120,18 +120,29 @@ export default //管理页面
meta: { meta: {
title: '点播', title: '点播',
keepAlive: true, keepAlive: true,
danmaku: true, danmaku: true
}, }
}, },
{ {
path: 'music-request', path: 'music-request',
name: 'manage-musicRequest', name: 'manage-musicRequest',
component: () => import('@/views/open_live/MusicRequest.vue'), component: () => import('@/views/open_live/MusicRequest.vue'),
meta: {
title: '点歌',
keepAlive: true,
danmaku: true
}
},
{
path: 'danmuji',
name: 'manage-danmuji',
component: () => import('@/views/manage/DanmujiManageView.vue'),
meta: { meta: {
title: '点歌', title: '点歌',
keepAlive: true, keepAlive: true,
danmaku: true, danmaku: true,
}, isNew: true
}
}, },
{ {
path: 'live', path: 'live',
@@ -139,40 +150,40 @@ export default //管理页面
component: () => import('@/views/manage/LiveManager.vue'), component: () => import('@/views/manage/LiveManager.vue'),
meta: { meta: {
title: '直播记录', title: '直播记录',
keepAlive: true, keepAlive: true
}, }
}, },
{ {
path: 'live/:id', path: 'live/:id',
name: 'manage-liveDetail', name: 'manage-liveDetail',
component: () => import('@/views/manage/LiveDetailManage.vue'), component: () => import('@/views/manage/LiveDetailManage.vue'),
meta: { meta: {
title: '直播详情', title: '直播详情'
}, }
}, },
{ {
path: 'feedback', path: 'feedback',
name: 'manage-feedback', name: 'manage-feedback',
component: () => import('@/views/FeedbackManage.vue'), component: () => import('@/views/FeedbackManage.vue'),
meta: { meta: {
title: '反馈', title: '反馈'
}, }
}, },
{ {
path: 'point', path: 'point',
name: 'manage-point', name: 'manage-point',
component: () => import('@/views/manage/point/PointManage.vue'), component: () => import('@/views/manage/point/PointManage.vue'),
meta: { meta: {
title: '积分', title: '积分'
}, }
}, },
{ {
path: 'forum', path: 'forum',
name: 'manage-forum', name: 'manage-forum',
component: () => import('@/views/manage/ForumManage.vue'), component: () => import('@/views/manage/ForumManage.vue'),
meta: { meta: {
title: '粉丝讨论区', title: '粉丝讨论区'
}, }
}, }
], ]
} }

View File

@@ -1,55 +1,68 @@
import { useAccount } from '@/api/account' import { useAccount } from '@/api/account'
import { MasterRTCClient, SlaveRTCClient } from '@/data/RTCClient' import {
BaseRTCClient,
MasterRTCClient,
SlaveRTCClient
} from '@/data/RTCClient'
import { nonFunctionArgSeparator } from 'html2canvas/dist/types/css/syntax/parser'
import { acceptHMRUpdate, defineStore } from 'pinia' import { acceptHMRUpdate, defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
export const useWebRTC = defineStore('WebRTC', () => { export const useWebRTC = defineStore('WebRTC', () => {
const masterClient = ref<MasterRTCClient>() const client = ref<BaseRTCClient>()
const slaveClient = ref<SlaveRTCClient>()
const accountInfo = useAccount() const accountInfo = useAccount()
let isInitializing = false let isInitializing = false
function Init( function on(event: string, callback: (...args: any[]) => void) {
type: 'master' | 'slave' client.value?.on(event, callback)
): MasterRTCClient | SlaveRTCClient | undefined { }
function off(event: string, callback: (...args: any[]) => void) {
client.value?.off(event, callback)
}
function send(event: string, data: any) {
client.value?.send(event, data)
}
async function Init(type: 'master' | 'slave') {
if (isInitializing) { if (isInitializing) {
return return useWebRTC()
} }
try { try {
isInitializing = true isInitializing = true
navigator.locks.request( await navigator.locks.request(
'rtcClientInit', 'rtcClientInit',
{ {
ifAvailable: true ifAvailable: true
}, },
async (lock) => { async (lock) => {
if (lock) { if (lock) {
if (type == 'master') { while (!accountInfo.value.id) {
if (masterClient.value) { await new Promise((resolve) => setTimeout(resolve, 500))
return masterClient
} else {
masterClient.value = new MasterRTCClient(
accountInfo.value.id.toString(),
accountInfo.value.token
)
await masterClient.value.Init()
return masterClient
}
} else {
if (slaveClient.value) {
return slaveClient
} else {
slaveClient.value = new SlaveRTCClient(
accountInfo.value.id?.toString(),
accountInfo.value.token
)
await slaveClient.value.Init()
return slaveClient
}
} }
if (client.value) {
return client.value
}
if (type == 'master') {
client.value = new MasterRTCClient(
accountInfo.value.id.toString(),
accountInfo.value.token
)
} else {
client.value = new SlaveRTCClient(
accountInfo.value.id?.toString(),
accountInfo.value.token
)
}
await client.value.Init()
return useWebRTC()
} else {
return useWebRTC()
} }
} }
) )
return useWebRTC()
} catch (e) { } catch (e) {
console.error(e) console.error(e)
throw e throw e
@@ -59,7 +72,10 @@ export const useWebRTC = defineStore('WebRTC', () => {
} }
return { return {
Init Init,
send,
on,
off
} }
}) })

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { GetSelfAccount, useAccount } from '@/api/account' import { GetSelfAccount, useAccount } from '@/api/account'
import { QueryGetAPI } from '@/api/query' import { QueryGetAPI } from '@/api/query'
import { BILI_API_URL, BILI_AUTH_API_URL } from '@/data/constants' import { BILI_API_URL, BILI_AUTH_API_URL, CURRENT_HOST } from '@/data/constants'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { randomUUID } from 'crypto' import { randomUUID } from 'crypto'
import { import {
@@ -187,7 +187,7 @@ onMounted(async () => {
<NText> 你的登陆链接为: </NText> <NText> 你的登陆链接为: </NText>
<NInputGroup> <NInputGroup>
<NInput <NInput
:value="`https://vtsuru.live/bili-user?auth=${currentToken}`" :value="`${CURRENT_HOST}bili-user?auth=${currentToken}`"
type="textarea" type="textarea"
:allow-input="() => false" :allow-input="() => false"
/> />

View File

@@ -130,7 +130,7 @@ const iconColor = 'white'
</NButton> </NButton>
</NSpace> </NSpace>
</NSpace> </NSpace>
<NButton size="large" @click="$router.push('/user/Megghy')"> 展示 </NButton> <NButton size="large" @click="$router.push('/@Megghy')"> 展示 </NButton>
<NButton <NButton
size="large" size="large"
tag="a" tag="a"

View File

@@ -26,9 +26,11 @@ import { useElementSize, useStorage } from '@vueuse/core'
import { import {
NAlert, NAlert,
NBackTop, NBackTop,
NBadge,
NButton, NButton,
NCountdown, NCountdown,
NDivider, NDivider,
NFlex,
NIcon, NIcon,
NLayout, NLayout,
NLayoutContent, NLayoutContent,
@@ -318,6 +320,23 @@ const menuOptions = [
icon: renderIcon(Chat24Filled), icon: renderIcon(Chat24Filled),
disabled: accountInfo.value?.isEmailVerified == false, disabled: accountInfo.value?.isEmailVerified == false,
children: [ children: [
{
label: () =>
h(NBadge, { value: '新', offset: [15, 12], type: 'info' }, () => h(NTooltip, {}, {
trigger: () => h(
RouterLink,
{
to: {
name: 'manage-danmuji',
},
},
{ default: () => '弹幕机' },
),
default: () => '兼容 blivechat 样式 (其实就是直接用的 blivechat 组件',
})),
key: 'manage-danmuji',
icon: renderIcon(Lottery24Filled),
},
{ {
label: () => label: () =>
h( h(
@@ -464,12 +483,8 @@ onMounted(() => {
</template> </template>
<template #extra> <template #extra>
<NSpace align="center" justify="center"> <NSpace align="center" justify="center">
<NSwitch <NSwitch :default-value="!isDarkMode" @update:value="(value: string & number & boolean) => (themeType = value ? ThemeType.Light : ThemeType.Dark)
: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" />
</template> </template>
@@ -477,12 +492,8 @@ onMounted(() => {
<NIcon :component="Moon" /> <NIcon :component="Moon" />
</template> </template>
</NSwitch> </NSwitch>
<NButton <NButton size="small" style="right: 0px; position: relative" type="primary"
size="small" @click="$router.push({ name: 'user-index', params: { id: accountInfo?.name } })">
style="right: 0px; position: relative"
type="primary"
@click="$router.push({ name: 'user-index', params: { id: accountInfo?.name } })"
>
回到展示页 回到展示页
</NButton> </NButton>
</NSpace> </NSpace>
@@ -490,17 +501,8 @@ onMounted(() => {
</NPageHeader> </NPageHeader>
</NLayoutHeader> </NLayoutHeader>
<NLayout has-sider style="height: calc(100vh - 50px)"> <NLayout has-sider style="height: calc(100vh - 50px)">
<NLayoutSider <NLayoutSider ref="sider" bordered show-trigger collapse-mode="width" :default-collapsed="windowWidth < 750"
ref="sider" :collapsed-width="64" :width="180" :native-scrollbar="false" :scrollbar-props="{ trigger: 'none', style: {} }">
bordered
show-trigger
collapse-mode="width"
:default-collapsed="windowWidth < 750"
:collapsed-width="64"
:width="180"
:native-scrollbar="false"
:scrollbar-props="{ trigger: 'none', style: {} }"
>
<NSpace vertical style="margin-top: 16px" align="center"> <NSpace vertical style="margin-top: 16px" align="center">
<NSpace justify="center"> <NSpace justify="center">
<NButton @click="$router.push({ name: 'manage-index' })" type="info" style="width: 100%"> <NButton @click="$router.push({ name: 'manage-index' })" type="info" style="width: 100%">
@@ -527,14 +529,9 @@ onMounted(() => {
<template v-if="width >= 180"> 认证用户主页 </template> <template v-if="width >= 180"> 认证用户主页 </template>
</NButton> </NButton>
</NSpace> </NSpace>
<NMenu <NMenu style="margin-top: 12px" :disabled="accountInfo?.isEmailVerified != true"
style="margin-top: 12px" :default-value="($route.meta.parent as string) ?? $route.name?.toString()" :collapsed-width="64"
:disabled="accountInfo?.isEmailVerified != true" :collapsed-icon-size="22" :options="menuOptions" />
:default-value="($route.meta.parent as string) ?? $route.name?.toString()"
:collapsed-width="64"
:collapsed-icon-size="22"
:options="menuOptions"
/>
<NSpace v-if="width > 150" justify="center" align="center" vertical> <NSpace v-if="width > 150" justify="center" align="center" vertical>
<NText depth="3"> <NText depth="3">
有更多功能建议请 有更多功能建议请
@@ -544,10 +541,18 @@ onMounted(() => {
<NButton text type="info" @click="$router.push({ name: 'about' })"> 关于本站 </NButton> <NButton text type="info" @click="$router.push({ name: 'about' })"> 关于本站 </NButton>
</NText> </NText>
</NSpace> </NSpace>
<NDivider style="margin-bottom: 8px;" />
<NFlex justify="center" align="center">
<NText
:style="`font-size: 12px; text-align: center;color: ${isDarkMode ? '#555' : '#c0c0c0'};visibility: ${width < 180 ? 'hidden' : 'visible'}`">
By Megghy
</NText>
</NFlex>
</NLayoutSider> </NLayoutSider>
<NLayout> <NLayout>
<NScrollbar :style="`height: calc(100vh - 50px - ${aplayerHeight}px)`"> <NScrollbar :style="`height: calc(100vh - 50px - ${aplayerHeight}px)`">
<NLayoutContent style="box-sizing: border-box; padding: 20px; min-width: 300px; height: 100%"> <NLayoutContent
:style="`box-sizing: border-box; padding: 20px; min-width: 300px; height: calc(100vh - 50px - ${aplayerHeight}px);`">
<RouterView v-if="accountInfo?.isEmailVerified" v-slot="{ Component, route }"> <RouterView v-if="accountInfo?.isEmailVerified" v-slot="{ Component, route }">
<KeepAlive> <KeepAlive>
<Suspense> <Suspense>
@@ -566,11 +571,8 @@ onMounted(() => {
<NButton size="small" type="info" :disabled="!canResendEmail" @click="resendEmail"> <NButton size="small" type="info" :disabled="!canResendEmail" @click="resendEmail">
重新发送验证邮件 重新发送验证邮件
</NButton> </NButton>
<NCountdown <NCountdown v-if="!canResendEmail" :duration="(accountInfo?.nextSendEmailTime ?? 0) - Date.now()"
v-if="!canResendEmail" @finish="canResendEmail = true" />
:duration="(accountInfo?.nextSendEmailTime ?? 0) - Date.now()"
@finish="canResendEmail = true"
/>
<NPopconfirm @positive-click="logout" size="small"> <NPopconfirm @positive-click="logout" size="small">
<template #trigger> <template #trigger>
@@ -586,21 +588,12 @@ onMounted(() => {
</NScrollbar> </NScrollbar>
<NLayoutFooter :style="`height: ${aplayerHeight}px;overflow: auto`"> <NLayoutFooter :style="`height: ${aplayerHeight}px;overflow: auto`">
<div style="display: flex; align-items: center; margin: 0 10px 0 10px"> <div style="display: flex; align-items: center; margin: 0 10px 0 10px">
<APlayer <APlayer v-if="musicRquestStore.aplayerMusics.length > 0" ref="aplayer"
v-if="musicRquestStore.aplayerMusics.length > 0" :list="musicRquestStore.aplayerMusics" v-model:music="musicRquestStore.currentMusic"
ref="aplayer" v-model:volume="musicRquestStore.settings.volume" v-model:shuffle="musicRquestStore.settings.shuffle"
:list="musicRquestStore.aplayerMusics" v-model:repeat="musicRquestStore.settings.repeat" :listMaxHeight="'200'" mutex listFolded
v-model:music="musicRquestStore.currentMusic" @ended="musicRquestStore.onMusicEnd" @play="musicRquestStore.onMusicPlay"
v-model:volume="musicRquestStore.settings.volume" style="flex: 1; min-width: 400px" />
v-model:shuffle="musicRquestStore.settings.shuffle"
v-model:repeat="musicRquestStore.settings.repeat"
:listMaxHeight="'200'"
mutex
listFolded
@ended="musicRquestStore.onMusicEnd"
@play="musicRquestStore.onMusicPlay"
style="flex: 1; min-width: 400px"
/>
<NSpace vertical> <NSpace vertical>
<NTag :bordered="false" type="info" size="small"> <NTag :bordered="false" type="info" size="small">
队列: {{ musicRquestStore.waitingMusics.length }} 队列: {{ musicRquestStore.waitingMusics.length }}
@@ -613,8 +606,7 @@ onMounted(() => {
</NLayout> </NLayout>
</NLayout> </NLayout>
<template v-else> <template v-else>
<NLayoutContent <NLayoutContent style="
style="
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -622,8 +614,7 @@ onMounted(() => {
padding: 50px; padding: 50px;
height: 100%; height: 100%;
box-sizing: border-box; box-sizing: border-box;
" ">
>
<template v-if="!isLoadingAccount"> <template v-if="!isLoadingAccount">
<NSpace vertical justify="center" align="center"> <NSpace vertical justify="center" align="center">
<NText> 请登录或注册后使用 </NText> <NText> 请登录或注册后使用 </NText>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAccount } from '@/api/account'; import { useAccount } from '@/api/account';
import { MasterRTCClient, SlaveRTCClient } from '@/data/RTCClient'; import { BaseRTCClient, MasterRTCClient, SlaveRTCClient } from '@/data/RTCClient';
import { useDanmakuClient } from '@/store/useDanmakuClient'; import { useDanmakuClient } from '@/store/useDanmakuClient';
import { useWebRTC } from '@/store/useRTC'; import { useWebRTC } from '@/store/useRTC';
import { NButton, NInput, NSpin } from 'naive-ui'; import { NButton, NInput, NSpin } from 'naive-ui';
@@ -19,11 +19,11 @@ const isMaster = computed(() => {
const dc = useDanmakuClient() const dc = useDanmakuClient()
const customCss = ref('') const customCss = ref('')
let rtc: Ref<MasterRTCClient | SlaveRTCClient | undefined> = ref() let rtc= useWebRTC()
const danmujiRef = ref() const danmujiRef = ref()
function mount() { async function mount() {
rtc.value = useWebRTC().Init(isMaster.value ? 'master' : 'slave') rtc.Init(isMaster.value ? 'master' : 'slave')
dc.initClient() dc.initClient()
} }
</script> </script>

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
import { NFlex, NInput } from 'naive-ui';
import DanmujiOBS from '../obs/DanmujiOBS.vue';
import { useAccount } from '@/api/account';
import MonacoEditorComponent from '@/components/MonacoEditorComponent.vue';
import { ref } from 'vue';
import { CURRENT_HOST } from '@/data/constants';
const accountInfo = useAccount()
const css = ref('')
</script>
<template>
<NFlex wrap style="height: 100%">
<NFlex style="flex: 1;" vertical>
<NInput :allowInput="() => false" :value="`${CURRENT_HOST}obs/danmuji?token=${accountInfo.token}`" />
<MonacoEditorComponent language="css" :height="500" v-model:value="css" />
</NFlex>
<div class="danmuji-obs" style="width: 300px; height: calc(100% - 2px); min-height: 500px;border: 1px solid #adadad;border-radius: 8px;
overflow: hidden;">
<DanmujiOBS :isOBS="false" style="height: 100%; width: 100%;" :customCss="css" />
</div>
</NFlex>
</template>
<style scoped>
.danmuji-obs {
--danmuji-bg: #333;
background-color: #222;
background-image: linear-gradient(45deg, var(--danmuji-bg) 25%, transparent 25%),
linear-gradient(-45deg, var(--danmuji-bg) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--danmuji-bg) 75%),
linear-gradient(-45deg, transparent 75%, var(--danmuji-bg) 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0;
}
</style>

View File

@@ -1,11 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { copyToClipboard, downloadImage } from '@/Utils' import { copyToClipboard, downloadImage } from '@/Utils'
import { DisableFunction, EnableFunction, SaveAccountSettings, SaveSetting, useAccount } from '@/api/account' import { DisableFunction, EnableFunction, SaveAccountSettings, SaveSetting, useAccount } from '@/api/account'
import { FunctionTypes, QAInfo } from '@/api/api-models' import { FunctionTypes, QAInfo, Setting_QuestionDisplay } from '@/api/api-models'
import { QueryGetAPI } from '@/api/query' import { QueryGetAPI } from '@/api/query'
import { QUESTION_API_URL } from '@/data/constants' import { CURRENT_HOST, QUESTION_API_URL } from '@/data/constants'
import router from '@/router' import router from '@/router'
import { Heart, HeartOutline, SwapHorizontal } from '@vicons/ionicons5' import { Heart, HeartOutline, SwapHorizontal } from '@vicons/ionicons5'
// @ts-ignore
import { saveAs } from 'file-saver' import { saveAs } from 'file-saver'
import html2canvas from 'html2canvas' import html2canvas from 'html2canvas'
import { import {
@@ -47,6 +48,8 @@ import { useRoute } from 'vue-router'
import QuestionItem from '@/components/QuestionItems.vue' import QuestionItem from '@/components/QuestionItems.vue'
import { Delete24Filled, Delete24Regular, Eye24Filled, EyeOff24Filled, Info24Filled } from '@vicons/fluent' import { Delete24Filled, Delete24Regular, Eye24Filled, EyeOff24Filled, Info24Filled } from '@vicons/fluent'
import { useQuestionBox } from '@/store/useQuestionBox' import { useQuestionBox } from '@/store/useQuestionBox'
import { useStorage } from '@vueuse/core'
import QuestionDisplayCard from './QuestionDisplayCard.vue'
const accountInfo = useAccount() const accountInfo = useAccount()
const route = useRoute() const route = useRoute()
@@ -62,15 +65,34 @@ const replyMessage = ref()
const addTagName = ref('') const addTagName = ref('')
const showSettingCard = ref(true) const showSettingCard = ref(true)
const showOBSModal = ref(false)
const defaultSettings = {} as Setting_QuestionDisplay
const setting = computed({
get: () => {
if (accountInfo.value && accountInfo.value.settings) {
return accountInfo.value.settings.questionDisplay
}
return defaultSettings
},
set: (value) => {
if (accountInfo.value) {
accountInfo.value.settings.questionDisplay = value
}
},
})
const shareCardRef = ref() const shareCardRef = ref()
const shareUrl = computed(() => 'https://vtsuru.live/@' + accountInfo.value?.name + '/question-box') const shareUrl = computed(() => `${CURRENT_HOST}@` + accountInfo.value?.name + '/question-box')
const ps = ref(20) const ps = ref(20)
const pn = ref(1) const pn = ref(1)
const pagedQuestions = computed(() => const pagedQuestions = computed(() =>
useQB.recieveQuestionsFiltered.slice((pn.value - 1) * ps.value, pn.value * ps.value), useQB.recieveQuestionsFiltered.slice((pn.value - 1) * ps.value, pn.value * ps.value),
) )
const savedCardSize = useStorage<{ width: number; height: number }>('Settings.QuestionDisplay.CardSize', {
width: 400,
height: 400,
})
let isRevieveGetted = false let isRevieveGetted = false
let isSendGetted = false let isSendGetted = false
@@ -168,26 +190,20 @@ onMounted(() => {
<template> <template>
<NSpace align="center"> <NSpace align="center">
<NAlert <NAlert :type="accountInfo.settings.enableFunctions.includes(FunctionTypes.QuestionBox) ? 'success' : 'warning'"
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.QuestionBox) ? 'success' : 'warning'" style="max-width: 200px">
style="max-width: 200px"
>
启用提问箱 启用提问箱
<NDivider vertical /> <NDivider vertical />
<NSwitch <NSwitch :value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.QuestionBox)"
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.QuestionBox)" @update:value="setFunctionEnable" />
@update:value="setFunctionEnable"
/>
</NAlert> </NAlert>
<NButton type="primary" @click="refresh"> 刷新 </NButton> <NButton type="primary" @click="refresh"> 刷新 </NButton>
<NButton type="primary" @click="shareModalVisiable = true" secondary> 分享 </NButton> <NButton type="primary" @click="shareModalVisiable = true" secondary> 分享 </NButton>
<NButton <NButton type="primary" @click="$router.push({ name: 'user-questionBox', params: { id: accountInfo.name } })"
type="primary" secondary>
@click="$router.push({ name: 'user-questionBox', params: { id: accountInfo.name } })"
secondary
>
前往提问页 前往提问页
</NButton> </NButton>
<NButton @click="showOBSModal = true" type="primary" secondary> 预览OBS组件 </NButton>
</NSpace> </NSpace>
<NDivider style="margin: 10px 0 10px 0" /> <NDivider style="margin: 10px 0 10px 0" />
<NSpin v-if="useQB.isLoading" show /> <NSpin v-if="useQB.isLoading" show />
@@ -195,15 +211,11 @@ onMounted(() => {
<NTabPane tab="我收到的" name="0" display-directive="show:lazy"> <NTabPane tab="我收到的" name="0" display-directive="show:lazy">
<NFlex align="center"> <NFlex align="center">
<NButton @click="$router.push({ name: 'question-display' })" type="primary"> 打开展示页 </NButton> <NButton @click="$router.push({ name: 'question-display' })" type="primary"> 打开展示页 </NButton>
<NSelect <NSelect v-model:value="useQB.displayTag" placeholder="选择当前话题" filterable clearable
v-model:value="useQB.displayTag" :options="useQB.tags.map((s) => ({ label: s.name, value: s.name }))" style="width: 200px">
placeholder="选择当前话题" <template #header>
filterable <NText strong depth="3"> 在设置选项卡中添加或删除话题 </NText>
clearable </template>
:options="useQB.tags.map((s) => ({ label: s.name, value: s.name }))"
style="width: 200px"
>
<template #header> <NText strong depth="3"> 在设置选项卡中添加或删除话题 </NText> </template>
</NSelect> </NSelect>
<NCheckbox v-model:checked="useQB.onlyFavorite"> 只显示收藏 </NCheckbox> <NCheckbox v-model:checked="useQB.onlyFavorite"> 只显示收藏 </NCheckbox>
<NCheckbox v-model:checked="useQB.onlyPublic"> 只显示公开 </NCheckbox> <NCheckbox v-model:checked="useQB.onlyPublic"> 只显示公开 </NCheckbox>
@@ -212,14 +224,8 @@ onMounted(() => {
<NDivider style="margin: 10px 0 10px 0" /> <NDivider style="margin: 10px 0 10px 0" />
<NEmpty v-if="useQB.recieveQuestionsFiltered.length == 0" description="暂无收到的提问" /> <NEmpty v-if="useQB.recieveQuestionsFiltered.length == 0" description="暂无收到的提问" />
<div v-else> <div v-else>
<NPagination <NPagination v-model:page="pn" v-model:page-size="ps" :item-count="useQB.recieveQuestionsFiltered.length"
v-model:page="pn" show-quick-jumper show-size-picker :page-sizes="[20, 50, 100]" />
v-model:page-size="ps"
:item-count="useQB.recieveQuestionsFiltered.length"
show-quick-jumper
show-size-picker
:page-sizes="[20, 50, 100]"
/>
<NDivider style="margin: 10px 0 10px 0" /> <NDivider style="margin: 10px 0 10px 0" />
<QuestionItem :questions="pagedQuestions"> <QuestionItem :questions="pagedQuestions">
<template #footer="{ item }"> <template #footer="{ item }">
@@ -230,10 +236,8 @@ onMounted(() => {
<NButton v-else size="small" @click="useQB.read(item, false)" type="warning">重设为未读</NButton> <NButton v-else size="small" @click="useQB.read(item, false)" type="warning">重设为未读</NButton>
<NButton size="small" @click="useQB.favorite(item, !item.isFavorite)"> <NButton size="small" @click="useQB.favorite(item, !item.isFavorite)">
<template #icon> <template #icon>
<NIcon <NIcon :component="item.isFavorite ? Heart : HeartOutline"
:component="item.isFavorite ? Heart : HeartOutline" :color="item.isFavorite ? '#dd484f' : ''" />
:color="item.isFavorite ? '#dd484f' : ''"
/>
</template> </template>
收藏 收藏
</NButton> </NButton>
@@ -264,14 +268,8 @@ onMounted(() => {
</template> </template>
</QuestionItem> </QuestionItem>
<NDivider style="margin: 10px 0 10px 0" /> <NDivider style="margin: 10px 0 10px 0" />
<NPagination <NPagination v-model:page="pn" v-model:page-size="ps" :item-count="useQB.recieveQuestionsFiltered.length"
v-model:page="pn" show-quick-jumper show-size-picker :page-sizes="[20, 50, 100]" />
v-model:page-size="ps"
:item-count="useQB.recieveQuestionsFiltered.length"
show-quick-jumper
show-size-picker
:page-sizes="[20, 50, 100]"
/>
</div> </div>
</NTabPane> </NTabPane>
<NTabPane ref="parentRef" tab="我发送的" name="1" display-directive="show:lazy"> <NTabPane ref="parentRef" tab="我发送的" name="1" display-directive="show:lazy">
@@ -320,10 +318,8 @@ onMounted(() => {
<NTabPane tab="设置" name="2" display-directive="show:lazy"> <NTabPane tab="设置" name="2" display-directive="show:lazy">
<NDivider> 设定 </NDivider> <NDivider> 设定 </NDivider>
<NSpin :show="useQB.isLoading"> <NSpin :show="useQB.isLoading">
<NCheckbox <NCheckbox v-model:checked="accountInfo.settings.questionBox.allowUnregistedUser"
v-model:checked="accountInfo.settings.questionBox.allowUnregistedUser" @update:checked="saveSettings">
@update:checked="saveSettings"
>
允许未注册用户进行提问 允许未注册用户进行提问
</NCheckbox> </NCheckbox>
<NDivider> <NDivider>
@@ -395,14 +391,7 @@ onMounted(() => {
<NModal preset="card" v-model:show="replyModalVisiable" style="max-width: 90vw; width: 500px"> <NModal preset="card" v-model:show="replyModalVisiable" style="max-width: 90vw; width: 500px">
<template #header> 回复 </template> <template #header> 回复 </template>
<NSpace vertical> <NSpace vertical>
<NInput <NInput placeholder="请输入回复" type="textarea" v-model:value="replyMessage" maxlength="1000" show-count clearable />
placeholder="请输入回复"
type="textarea"
v-model:value="replyMessage"
maxlength="1000"
show-count
clearable
/>
<NSpin :show="useQB.isChangingPublic"> <NSpin :show="useQB.isChangingPublic">
<NCheckbox @update:checked="(v) => useQB.setPublic(v)" :default-checked="useQB.currentQuestion?.isPublic"> <NCheckbox @update:checked="(v) => useQB.setPublic(v)" :default-checked="useQB.currentQuestion?.isPublic">
公开可见 公开可见
@@ -410,12 +399,8 @@ onMounted(() => {
</NSpin> </NSpin>
</NSpace> </NSpace>
<NDivider style="margin: 10px 0 10px 0" /> <NDivider style="margin: 10px 0 10px 0" />
<NButton <NButton :loading="useQB.isRepling" @click="useQB.reply(useQB.currentQuestion?.id ?? -1, replyMessage)"
:loading="useQB.isRepling" type="primary" :secondary="useQB.currentQuestion?.answer ? true : false">
@click="useQB.reply(useQB.currentQuestion?.id ?? -1, replyMessage)"
type="primary"
:secondary="useQB.currentQuestion?.answer ? true : false"
>
{{ useQB.currentQuestion?.answer ? '修改' : '发送' }} {{ useQB.currentQuestion?.answer ? '修改' : '发送' }}
</NButton> </NButton>
</NModal> </NModal>
@@ -428,15 +413,8 @@ onMounted(() => {
</NText> </NText>
<NDivider class="share-card divider-1" /> <NDivider class="share-card divider-1" />
<NText class="share-card site"> VTSURU.LIVE </NText> <NText class="share-card site"> VTSURU.LIVE </NText>
<QrcodeVue <QrcodeVue class="share-card qrcode" :value="shareUrl" level="Q" :size="100" background="#00000000"
class="share-card qrcode" foreground="#ffffff" :margin="1" />
:value="shareUrl"
level="Q"
:size="100"
background="#00000000"
foreground="#ffffff"
:margin="1"
/>
</div> </div>
<NDivider style="margin: 10px" /> <NDivider style="margin: 10px" />
<NInputGroup> <NInputGroup>
@@ -449,6 +427,25 @@ onMounted(() => {
<NButton type="primary" @click="saveQRCode"> 保存二维码 </NButton> <NButton type="primary" @click="saveQRCode"> 保存二维码 </NButton>
</NSpace> </NSpace>
</NModal> </NModal>
<NModal preset="card" v-model:show="showOBSModal" closable style="max-width: 90vw; width: auto" title="OBS组件"
content-style="display: flex; align-items: center; justify-content: center; flex-direction: column">
<NAlert type="info">
操作显示的内容请前往
<NButton text @click="$router.push({ name: 'question-display' })"> 展示管理页 </NButton>
</NAlert>
<br />
<div :style="{
width: savedCardSize.width + 'px',
height: savedCardSize.height + 'px',
}">
<QuestionDisplayCard :question="useQB.displayQuestion" :setting="setting" />
</div>
<NDivider />
<NInput readonly :value="CURRENT_HOST + 'obs/question-display?token=' + accountInfo?.token" />
<NDivider />
<NButton type="primary" @click="$router.push({ name: 'question-display' })"> 前往展示管理页 </NButton>
</NModal>
</template> </template>
<style> <style>
@@ -463,6 +460,7 @@ onMounted(() => {
border-radius: 10px; border-radius: 10px;
background: linear-gradient(to right, #66bea3, #9179be); background: linear-gradient(to right, #66bea3, #9179be);
} }
.share-card.qrcode { .share-card.qrcode {
position: absolute; position: absolute;
right: 10px; right: 10px;
@@ -470,6 +468,7 @@ onMounted(() => {
border-radius: 4px; border-radius: 4px;
background: linear-gradient(to right, #3d554e, #503e74); background: linear-gradient(to right, #3d554e, #503e74);
} }
.share-card.title { .share-card.title {
position: absolute; position: absolute;
font-size: 80px; font-size: 80px;
@@ -478,6 +477,7 @@ onMounted(() => {
color: #e6e6e662; color: #e6e6e662;
font-weight: 550; font-weight: 550;
} }
/* .share-card.type { /* .share-card.type {
position: absolute; position: absolute;
font-size: 20px; font-size: 20px;
@@ -494,6 +494,7 @@ onMounted(() => {
font-weight: 550; font-weight: 550;
color: #e6e6e662; color: #e6e6e662;
} }
.share-card.name { .share-card.name {
position: absolute; position: absolute;
font-size: 30px; font-size: 30px;
@@ -505,6 +506,7 @@ onMounted(() => {
color: #e6e6e6; color: #e6e6e6;
font-weight: 550; font-weight: 550;
} }
.share-card.site { .share-card.site {
position: absolute; position: absolute;
font-size: 12px; font-size: 12px;
@@ -513,6 +515,7 @@ onMounted(() => {
color: #e6e6e6a4; color: #e6e6e6a4;
font-weight: 550; font-weight: 550;
} }
.share-card.divider-1 { .share-card.divider-1 {
position: absolute; position: absolute;
width: 400px; width: 400px;

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, onUnmounted, watch } from 'vue' import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { QAInfo, QuestionDisplayAlign, Setting_QuestionDisplay } from '@/api/api-models' import { QAInfo, QuestionDisplayAlign, Setting_QuestionDisplay } from '@/api/api-models'
import { useDebounceFn, useStorage } from '@vueuse/core' import { useDebounceFn, useScroll, useStorage } from '@vueuse/core'
const props = defineProps<{ const props = defineProps<{
question: QAInfo | undefined question: QAInfo | undefined
@@ -10,6 +10,9 @@ const props = defineProps<{
showGreenBorder?: boolean showGreenBorder?: boolean
css?: string css?: string
}>() }>()
defineExpose({ setScroll, setScrollTop })
const emit = defineEmits<{ scroll: [value: { clientHeight: number, scrollHeight: number, scrollTop: number }] }>()
let styleElement: HTMLStyleElement let styleElement: HTMLStyleElement
const cssDebounce = useDebounceFn(() => { const cssDebounce = useDebounceFn(() => {
if (styleElement) { if (styleElement) {
@@ -19,6 +22,10 @@ const cssDebounce = useDebounceFn(() => {
}, 1000) }, 1000)
watch(() => props.css, cssDebounce) watch(() => props.css, cssDebounce)
const contentRef = ref()
const { x, y, isScrolling, arrivedState, directions } = useScroll(contentRef)
const align = computed(() => { const align = computed(() => {
switch (props.setting.align) { switch (props.setting.align) {
case QuestionDisplayAlign.Left: case QuestionDisplayAlign.Left:
@@ -30,6 +37,28 @@ const align = computed(() => {
} }
return 'left' return 'left'
}) })
function setScrollTop(top: number) {
contentRef.value?.scrollTo({
top: top,
behavior: 'smooth',
})
}
function setScroll(value: { clientHeight: number, scrollHeight: number, scrollTop: number }) {
if (contentRef.value.clientHeight == contentRef.value.scrollHeight) {
setScrollTop(value.scrollTop)
} else {
const scrollRatio1 = value.scrollTop / (value.scrollHeight - value.clientHeight);
const scrollTop = scrollRatio1 * (contentRef.value.scrollHeight - contentRef.value.clientHeight);
setScrollTop(scrollTop)
}
}
function scrollCallback(e: Event) {
emit('scroll', {
clientHeight: contentRef.value?.clientHeight ?? 0,
scrollHeight: contentRef.value?.scrollHeight ?? 0,
scrollTop: contentRef.value?.scrollTop ?? 0
})
}
onMounted(() => { onMounted(() => {
// 创建<style>元素并添加到<head>中 // 创建<style>元素并添加到<head>中
@@ -37,62 +66,50 @@ onMounted(() => {
// 可能需要对 userStyleString 做安全处理以避免XSS攻击 // 可能需要对 userStyleString 做安全处理以避免XSS攻击
styleElement.textContent = props.css ?? '' styleElement.textContent = props.css ?? ''
document.head.appendChild(styleElement) document.head.appendChild(styleElement)
contentRef.value?.addEventListener('scroll', scrollCallback)
}) })
onUnmounted(() => { onUnmounted(() => {
if (styleElement && styleElement.parentNode) { if (styleElement && styleElement.parentNode) {
styleElement.parentNode.removeChild(styleElement) styleElement.parentNode.removeChild(styleElement)
} }
contentRef.value?.removeEventListener('scroll', scrollCallback)
}) })
</script> </script>
<template> <template>
<div <div class="question-display-root" :style="{
class="question-display-root" backgroundColor: '#' + setting.borderColor,
:style="{ borderColor: setting.borderColor ? '#' + setting.borderColor : undefined,
backgroundColor: '#' + setting.borderColor, borderWidth: setting.borderWidth ? setting.borderWidth + 'px' : undefined,
borderColor: setting.borderColor ? '#' + setting.borderColor : undefined, borderTopWidth: setting.showUserName && question ? 0 : setting.borderWidth,
borderWidth: setting.borderWidth ? setting.borderWidth + 'px' : undefined, }" :display="question ? 1 : 0">
borderTopWidth: setting.showUserName && question ? 0 : setting.borderWidth,
}"
:display="question ? 1 : 0"
>
<Transition name="scale" mode="out-in"> <Transition name="scale" mode="out-in">
<div <div v-if="setting.showUserName && question" class="question-display-user-name" :style="{
v-if="setting.showUserName && question" color: '#' + setting.nameFontColor,
class="question-display-user-name" fontSize: setting.nameFontSize + 'px',
:style="{ fontWeight: setting.nameFontWeight ? setting.nameFontWeight : undefined,
color: '#' + setting.nameFontColor, fontFamily: setting.nameFont,
fontSize: setting.nameFontSize + 'px', }">
fontWeight: setting.nameFontWeight ? setting.nameFontWeight : undefined,
fontFamily: setting.nameFont,
}"
>
{{ question?.sender?.name ?? '匿名用户' }} {{ question?.sender?.name ?? '匿名用户' }}
</div> </div>
</Transition> </Transition>
<div <div class="question-display-content" @scroll="scrollCallback" ref="contentRef" :style="{
class="question-display-content" color: '#' + setting.fontColor,
:style="{ backgroundColor: '#' + setting.backgroundColor,
color: '#' + setting.fontColor, fontSize: setting.fontSize + 'px',
backgroundColor: '#' + setting.backgroundColor, fontWeight: setting.fontWeight ? setting.fontWeight : undefined,
fontSize: setting.fontSize + 'px', textAlign: align,
fontWeight: setting.fontWeight ? setting.fontWeight : undefined, fontFamily: setting.font,
textAlign: align, }" :is-empty="question ? 0 : 1">
fontFamily: setting.font,
}"
:is-empty="question ? 0 : 1"
>
<Transition name="fade" mode="out-in"> <Transition name="fade" mode="out-in">
<template v-if="question"> <template v-if="question">
<div> <div>
<div class="question-display-text"> <div class="question-display-text">
{{ question?.question.message }} {{ question?.question.message }}
</div> </div>
<img <img class="question-display-image" v-if="setting.showImage && question?.question.image"
class="question-display-image" :src="question?.question.image" />
v-if="setting.showImage && question?.question.image"
:src="question?.question.image"
/>
</div> </div>
</template> </template>
<div v-else class="question-display-loading loading" :style="{ color: '#' + setting.fontColor }"> <div v-else class="question-display-loading loading" :style="{ color: '#' + setting.fontColor }">
@@ -118,6 +135,7 @@ onUnmounted(() => {
box-sizing: border-box; box-sizing: border-box;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.question-display-content { .question-display-content {
border-radius: 10px; border-radius: 10px;
display: flex; display: flex;
@@ -127,6 +145,7 @@ onUnmounted(() => {
padding: 24px; padding: 24px;
overflow: auto; overflow: auto;
} }
.question-display-user-name { .question-display-user-name {
text-align: center; text-align: center;
margin: 5px; margin: 5px;
@@ -134,9 +153,11 @@ onUnmounted(() => {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.question-display-text { .question-display-text {
min-height: 50px; min-height: 50px;
} }
.question-display-image { .question-display-image {
max-width: 40%; max-width: 40%;
max-height: 40%; max-height: 40%;
@@ -172,7 +193,7 @@ onUnmounted(() => {
<style scoped> <style scoped>
.loading, .loading,
.loading > div { .loading>div {
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -187,7 +208,7 @@ onUnmounted(() => {
color: #333; color: #333;
} }
.loading > div { .loading>div {
display: inline-block; display: inline-block;
float: none; float: none;
background-color: currentColor; background-color: currentColor;
@@ -199,18 +220,18 @@ onUnmounted(() => {
height: 10px; height: 10px;
} }
.loading > div { .loading>div {
width: 10px; width: 10px;
height: 10px; height: 10px;
border-radius: 100%; border-radius: 100%;
} }
.loading > div:first-child { .loading>div:first-child {
transform: translateX(0%); transform: translateX(0%);
animation: ball-newton-cradle-left 1.5s 0s ease-out infinite; animation: ball-newton-cradle-left 1.5s 0s ease-out infinite;
} }
.loading > div:last-child { .loading>div:last-child {
transform: translateX(0%); transform: translateX(0%);
animation: ball-newton-cradle-right 1.5s 0s ease-out infinite; animation: ball-newton-cradle-right 1.5s 0s ease-out infinite;
} }
@@ -220,7 +241,7 @@ onUnmounted(() => {
height: 4px; height: 4px;
} }
.loading.la-sm > div { .loading.la-sm>div {
width: 4px; width: 4px;
height: 4px; height: 4px;
} }
@@ -230,7 +251,7 @@ onUnmounted(() => {
height: 20px; height: 20px;
} }
.loading.la-2x > div { .loading.la-2x>div {
width: 20px; width: 20px;
height: 20px; height: 20px;
} }
@@ -240,7 +261,7 @@ onUnmounted(() => {
height: 30px; height: 30px;
} }
.loading.la-3x > div { .loading.la-3x>div {
width: 30px; width: 30px;
height: 30px; height: 30px;
} }
@@ -270,11 +291,12 @@ onUnmounted(() => {
transform: translateX(0%); transform: translateX(0%);
} }
} }
.loading > div:first-child {
.loading>div:first-child {
animation: ball-newton-cradle-left 1.5s 0s ease-in-out infinite; animation: ball-newton-cradle-left 1.5s 0s ease-in-out infinite;
} }
.loading > div:last-child { .loading>div:last-child {
animation: ball-newton-cradle-right 1.5s 0s ease-in-out infinite; animation: ball-newton-cradle-right 1.5s 0s ease-in-out infinite;
} }
</style> </style>

View File

@@ -10,7 +10,7 @@ import {
} from '@/api/api-models' } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query' import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import VideoCollectInfoCard from '@/components/VideoCollectInfoCard.vue' import VideoCollectInfoCard from '@/components/VideoCollectInfoCard.vue'
import { VIDEO_COLLECT_API_URL } from '@/data/constants' import { CURRENT_HOST, VIDEO_COLLECT_API_URL } from '@/data/constants'
import router from '@/router' import router from '@/router'
import { Clock24Filled, Person24Filled } from '@vicons/fluent' import { Clock24Filled, Person24Filled } from '@vicons/fluent'
import { useWindowSize } from '@vueuse/core' import { useWindowSize } from '@vueuse/core'
@@ -148,74 +148,74 @@ const gridRender = (type: 'padding' | 'reject' | 'accept') => {
return videos.length == 0 return videos.length == 0
? h(NEmpty) ? h(NEmpty)
: h(NGrid, { cols: '1 500:2 700:3 900:4 1200:5 ', xGap: '12', yGap: '12', responsive: 'self' }, () => : h(NGrid, { cols: '1 500:2 700:3 900:4 1200:5 ', xGap: '12', yGap: '12', responsive: 'self' }, () =>
videos?.map((v) => videos?.map((v) =>
h(NGridItem, () => h(NGridItem, () =>
h( h(
NCard, NCard,
{ style: 'height: 330px;', embedded: true, size: 'small' }, { style: 'height: 330px;', embedded: true, size: 'small' },
{ {
cover: () => cover: () =>
h('div', { style: 'position: relative;height: 150px;' }, [ h('div', { style: 'position: relative;height: 150px;' }, [
h('img', { h('img', {
src: v.video.cover.replace('http://', 'https://'), src: v.video.cover.replace('http://', 'https://'),
referrerpolicy: 'no-referrer', referrerpolicy: 'no-referrer',
style: 'max-height: 100%; object-fit: contain;cursor: pointer', style: 'max-height: 100%; object-fit: contain;cursor: pointer',
onClick: () => window.open('https://www.bilibili.com/video/' + v.info.bvid, '_blank'), onClick: () => window.open('https://www.bilibili.com/video/' + v.info.bvid, '_blank'),
}), }),
h(
NSpace,
{
style: { position: 'relative', bottom: '20px', background: '#00000073' },
justify: 'space-around',
},
() => [
h('span', [
h(NIcon, { component: Clock24Filled, color: 'lightgrey' }),
h(NText, { style: 'color: lightgrey;size:small;' }, () => formatSeconds(v.video.length)),
]),
h('span', [
h(NIcon, { component: Person24Filled, color: 'lightgrey' }),
h(NText, { style: 'color: lightgrey;size:small;' }, () => v.video.ownerName),
]),
],
),
]),
header: () =>
h( h(
NButton, NSpace,
{ {
style: 'width: 100%;', style: { position: 'relative', bottom: '20px', background: '#00000073' },
text: true, justify: 'space-around',
onClick: () => window.open('https://www.bilibili.com/video/' + v.info.bvid, '_blank'),
}, },
() => () => [
h( h('span', [
NEllipsis, h(NIcon, { component: Clock24Filled, color: 'lightgrey' }),
{ style: 'max-width: 100%;' }, h(NText, { style: 'color: lightgrey;size:small;' }, () => formatSeconds(v.video.length)),
{
default: () => v.video.title,
tooltip: () => h('div', { style: 'max-width: 300px' }, v.video.title),
},
),
),
default: () =>
h(NScrollbar, { style: 'height: 65px;' }, () =>
h(NCard, { contentStyle: 'padding: 5px;' }, () =>
v.info.senders.map((s) => [
h('div', { style: 'font-size: 12px;' }, [
h('div', `推荐人: ${s.sender ?? '未填写'} [${s.senderId ?? '未填写'}]`),
h('div', `推荐理由: ${s.description ?? '未填写'}`),
]),
h(NSpace, { style: 'margin: 0;' }),
]), ]),
), h('span', [
h(NIcon, { component: Person24Filled, color: 'lightgrey' }),
h(NText, { style: 'color: lightgrey;size:small;' }, () => v.video.ownerName),
]),
],
), ),
footer: () => footer(v.info), ]),
}, header: () =>
), h(
NButton,
{
style: 'width: 100%;',
text: true,
onClick: () => window.open('https://www.bilibili.com/video/' + v.info.bvid, '_blank'),
},
() =>
h(
NEllipsis,
{ style: 'max-width: 100%;' },
{
default: () => v.video.title,
tooltip: () => h('div', { style: 'max-width: 300px' }, v.video.title),
},
),
),
default: () =>
h(NScrollbar, { style: 'height: 65px;' }, () =>
h(NCard, { contentStyle: 'padding: 5px;' }, () =>
v.info.senders.map((s) => [
h('div', { style: 'font-size: 12px;' }, [
h('div', `推荐人: ${s.sender ?? '未填写'} [${s.senderId ?? '未填写'}]`),
h('div', `推荐理由: ${s.description ?? '未填写'}`),
]),
h(NSpace, { style: 'margin: 0;' }),
]),
),
),
footer: () => footer(v.info),
},
), ),
), ),
) ),
)
} }
const paddingButtonGroup = (v: VideoInfo) => const paddingButtonGroup = (v: VideoInfo) =>
h(NSpace, { size: 'small', justify: 'space-around' }, () => [ h(NSpace, { size: 'small', justify: 'space-around' }, () => [
@@ -395,10 +395,8 @@ onActivated(async () => {
<NButton type="warning" size="small" @click="closeTable"> <NButton type="warning" size="small" @click="closeTable">
{{ videoDetail.table.isFinish ? '开启表' : '关闭表' }} {{ videoDetail.table.isFinish ? '开启表' : '关闭表' }}
</NButton> </NButton>
<NButton <NButton size="small"
size="small" @click="$router.push({ name: 'video-collect-list', params: { id: videoDetail.table.id } })">
@click="$router.push({ name: 'video-collect-list', params: { id: videoDetail.table.id } })"
>
结果表 结果表
</NButton> </NButton>
<NPopconfirm :on-positive-click="deleteTable"> <NPopconfirm :on-positive-click="deleteTable">
@@ -447,14 +445,9 @@ onActivated(async () => {
</NTabs> </NTabs>
</template> </template>
<NModal v-model:show="shareModalVisiable" title="分享" preset="card" style="width: 600px; max-width: 90vw"> <NModal v-model:show="shareModalVisiable" title="分享" preset="card" style="width: 600px; max-width: 90vw">
<Qrcode <Qrcode :value="`${CURRENT_HOST}video-collect/` + videoDetail.table.shortId" level="Q" :size="100" background="#fff"
:value="'https://vtsuru.live/video-collect/' + videoDetail.table.shortId" :margin="1" />
level="Q" <NInput :value="`${CURRENT_HOST}video-collect/` + videoDetail.table.shortId" />
:size="100"
background="#fff"
:margin="1"
/>
<NInput :value="'https://vtsuru.live/video-collect/' + videoDetail.table.shortId" />
<NDivider /> <NDivider />
<NSpace justify="center"> <NSpace justify="center">
<NButton type="primary" @click="saveQRCode"> 保存二维码 </NButton> <NButton type="primary" @click="saveQRCode"> 保存二维码 </NButton>
@@ -469,20 +462,12 @@ onActivated(async () => {
<NInput v-model:value="updateModel.description" placeholder="可以是备注之类的" maxlength="300" show-count /> <NInput v-model:value="updateModel.description" placeholder="可以是备注之类的" maxlength="300" show-count />
</NFormItem> </NFormItem>
<NFormItem label="视频数量" path="maxVideoCount"> <NFormItem label="视频数量" path="maxVideoCount">
<NInputNumber <NInputNumber v-model:value="updateModel.maxVideoCount" placeholder="最大数量" type="number"
v-model:value="updateModel.maxVideoCount" style="max-width: 150px" />
placeholder="最大数量"
type="number"
style="max-width: 150px"
/>
</NFormItem> </NFormItem>
<NFormItem label="结束时间" path="endAt"> <NFormItem label="结束时间" path="endAt">
<NDatePicker <NDatePicker v-model:value="updateModel.endAt" type="datetime" placeholder="结束征集的时间"
v-model:value="updateModel.endAt" :isDateDisabled="dateDisabled" />
type="datetime"
placeholder="结束征集的时间"
:isDateDisabled="dateDisabled"
/>
<NDivider vertical /> <NDivider vertical />
<NText depth="3"> 最低为一小时 </NText> <NText depth="3"> 最低为一小时 </NText>
</NFormItem> </NFormItem>

View File

@@ -76,7 +76,7 @@ const defaultConfig: DanmujiConfig = {
} as DanmujiConfig } as DanmujiConfig
let textEmoticons: { keyword: string, url: string }[] = [] let textEmoticons: { keyword: string, url: string }[] = []
const config = ref<DanmujiConfig>(JSON.parse(JSON.stringify(defaultConfig))) const config = ref<DanmujiConfig>(JSON.parse(JSON.stringify(defaultConfig)))
const rtc = useWebRTC().Init('slave') const rtc = await useWebRTC().Init('slave')
const emoticonsTrie = computed(() => { const emoticonsTrie = computed(() => {
let res = new trie.Trie() let res = new trie.Trie()

View File

@@ -23,7 +23,7 @@ const route = useRoute()
const currentId = computed(() => { const currentId = computed(() => {
return props.id ?? route.query.id return props.id ?? route.query.id
}) })
const rtc = useWebRTC().Init('slave') const rtc = await useWebRTC().Init('slave')
const listContainerRef = ref() const listContainerRef = ref()
const footerRef = ref() const footerRef = ref()

View File

@@ -9,11 +9,13 @@ import { useWebRTC } from '@/store/useRTC'
const hash = ref('') const hash = ref('')
const token = useRouteQuery('token') const token = useRouteQuery('token')
const rtc = useWebRTC().Init('slave') const rtc = await useWebRTC().Init('slave')
const question = ref<QAInfo>() const question = ref<QAInfo>()
const setting = ref<Setting_QuestionDisplay>({} as Setting_QuestionDisplay) const setting = ref<Setting_QuestionDisplay>({} as Setting_QuestionDisplay)
const cardRef = ref()
async function checkIfChanged() { async function checkIfChanged() {
try { try {
const data = await QueryGetAPI<string>(QUESTION_API_URL + 'get-hash', { const data = await QueryGetAPI<string>(QUESTION_API_URL + 'get-hash', {
@@ -45,6 +47,9 @@ async function getQuestionAndSetting() {
console.log(err) console.log(err)
} }
} }
function handleScroll(value: { clientHeight: number, scrollHeight: number, scrollTop: number }) {
cardRef.value?.setScroll(value)
}
const visiable = ref(true) const visiable = ref(true)
const active = ref(true) const active = ref(true)
@@ -66,12 +71,16 @@ onMounted(() => {
active.value = a active.value = a
} }
} }
rtc?.on('function.question.sync-scroll', handleScroll)
}) })
onUnmounted(() => { onUnmounted(() => {
clearInterval(timer) clearInterval(timer)
rtc?.off('function.question.sync-scroll', handleScroll)
}) })
</script> </script>
<template> <template>
<QuestionDisplayCard :question="question" :setting="setting" /> <QuestionDisplayCard ref="cardRef" :question="question" :setting="setting" />
</template> </template>

View File

@@ -29,7 +29,7 @@ const route = useRoute()
const currentId = computed(() => { const currentId = computed(() => {
return props.id ?? route.query.id return props.id ?? route.query.id
}) })
const rtc = useWebRTC().Init('slave') const rtc = await useWebRTC().Init('slave')
const listContainerRef = ref() const listContainerRef = ref()
const footerRef = ref() const footerRef = ref()

View File

@@ -15,7 +15,7 @@ import {
import { QueryGetAPI, QueryPostAPI, QueryPostAPIWithParams } from '@/api/query' import { QueryGetAPI, QueryPostAPI, QueryPostAPIWithParams } from '@/api/query'
import SongPlayer from '@/components/SongPlayer.vue' import SongPlayer from '@/components/SongPlayer.vue'
import { RoomAuthInfo } from '@/data/DanmakuClient' import { RoomAuthInfo } from '@/data/DanmakuClient'
import { SONG_REQUEST_API_URL } from '@/data/constants' import { CURRENT_HOST, SONG_REQUEST_API_URL } from '@/data/constants'
import { useDanmakuClient } from '@/store/useDanmakuClient' import { useDanmakuClient } from '@/store/useDanmakuClient'
import { import {
Checkmark12Regular, Checkmark12Regular,
@@ -633,24 +633,24 @@ const columns = [
() => [ () => [
data.status == SongRequestStatus.Finish || data.status == SongRequestStatus.Cancel data.status == SongRequestStatus.Finish || data.status == SongRequestStatus.Cancel
? h(NTooltip, null, { ? h(NTooltip, null, {
trigger: () => trigger: () =>
h( h(
NButton, NButton,
{ {
size: 'small', size: 'small',
type: 'info', type: 'info',
circle: true, circle: true,
loading: isLoading.value, loading: isLoading.value,
onClick: () => { onClick: () => {
updateSongStatus(data, SongRequestStatus.Waiting) updateSongStatus(data, SongRequestStatus.Waiting)
},
}, },
{ },
icon: () => h(NIcon, { component: ReloadCircleSharp }), {
}, icon: () => h(NIcon, { component: ReloadCircleSharp }),
), },
default: () => '重新放回等待列表', ),
}) default: () => '重新放回等待列表',
})
: undefined, : undefined,
h( h(
NPopconfirm, NPopconfirm,
@@ -731,7 +731,7 @@ async function updateActive() {
message.error('无法获取点播队列: ' + data.message) message.error('无法获取点播队列: ' + data.message)
return [] return []
} }
} catch (err) {} } catch (err) { }
} }
function blockUser(item: SongRequestInfo) { function blockUser(item: SongRequestInfo) {
if (item.from != SongRequestFrom.Danmaku) { if (item.from != SongRequestFrom.Danmaku) {
@@ -793,15 +793,11 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<NAlert <NAlert :type="accountInfo.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? 'success' : 'warning'"
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? 'success' : 'warning'" v-if="accountInfo.id">
v-if="accountInfo.id"
>
启用弹幕点播功能 启用弹幕点播功能
<NSwitch <NSwitch :value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.SongRequest)"
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.SongRequest)" @update:value="onUpdateFunctionEnable" />
@update:value="onUpdateFunctionEnable"
/>
<br /> <br />
<NText depth="3"> <NText depth="3">
@@ -812,11 +808,7 @@ onUnmounted(() => {
则其需要保持此页面开启才能点播, 也不要同时开多个页面, 会导致点播重复 !(部署了则不影响) 则其需要保持此页面开启才能点播, 也不要同时开多个页面, 会导致点播重复 !(部署了则不影响)
</NText> </NText>
</NAlert> </NAlert>
<NAlert <NAlert type="warning" v-else title="你尚未注册并登录 VTsuru.live, 大部分规则设置将不可用 (因为我懒得在前段重写一遍逻辑">
type="warning"
v-else
title="你尚未注册并登录 VTsuru.live, 大部分规则设置将不可用 (因为我懒得在前段重写一遍逻辑"
>
<NButton tag="a" href="/manage" target="_blank" type="primary"> 前往登录或注册 </NButton> <NButton tag="a" href="/manage" target="_blank" type="primary"> 前往登录或注册 </NButton>
</NAlert> </NAlert>
<br /> <br />
@@ -832,11 +824,8 @@ onUnmounted(() => {
</NCard> </NCard>
<br /> <br />
<NCard> <NCard>
<NTabs <NTabs v-if="!accountInfo || accountInfo.settings.enableFunctions.includes(FunctionTypes.SongRequest)" animated
v-if="!accountInfo || accountInfo.settings.enableFunctions.includes(FunctionTypes.SongRequest)" display-directive="show:lazy">
animated
display-directive="show:lazy"
>
<NTabPane name="list" tab="列表"> <NTabPane name="list" tab="列表">
<NCard size="small"> <NCard size="small">
<NSpace align="center"> <NSpace align="center">
@@ -861,12 +850,8 @@ onUnmounted(() => {
<NInput placeholder="手动添加" v-model:value="newSongName" /> <NInput placeholder="手动添加" v-model:value="newSongName" />
<NButton type="primary" @click="addSongManual"> 添加 </NButton> <NButton type="primary" @click="addSongManual"> 添加 </NButton>
</NInputGroup> </NInputGroup>
<NRadioGroup <NRadioGroup v-model:value="settings.sortType" :disabled="!configCanEdit" @update:value="updateSettings"
v-model:value="settings.sortType" type="button">
:disabled="!configCanEdit"
@update:value="updateSettings"
type="button"
>
<NRadioButton :value="QueueSortType.TimeFirst"> 加入时间优先 </NRadioButton> <NRadioButton :value="QueueSortType.TimeFirst"> 加入时间优先 </NRadioButton>
<NRadioButton :value="QueueSortType.PaymentFist"> 付费价格优先 </NRadioButton> <NRadioButton :value="QueueSortType.PaymentFist"> 付费价格优先 </NRadioButton>
<NRadioButton :value="QueueSortType.GuardFirst"> 舰长优先 (按等级) </NRadioButton> <NRadioButton :value="QueueSortType.GuardFirst"> 舰长优先 (按等级) </NRadioButton>
@@ -893,17 +878,13 @@ onUnmounted(() => {
</Transition> </Transition>
<NList v-if="activeSongs.length > 0" :show-divider="false" hoverable> <NList v-if="activeSongs.length > 0" :show-divider="false" hoverable>
<NListItem v-for="song in activeSongs" :key="song.id" style="padding: 5px"> <NListItem v-for="song in activeSongs" :key="song.id" style="padding: 5px">
<NCard <NCard embedded size="small" content-style="padding: 5px;"
embedded :style="`${song.status == SongRequestStatus.Singing ? 'animation: animated-border 2.5s infinite;' : ''};height: 100%;`">
size="small"
content-style="padding: 5px;"
:style="`${song.status == SongRequestStatus.Singing ? 'animation: animated-border 2.5s infinite;' : ''};height: 100%;`"
>
<NSpace justify="space-between" align="center" style="height: 100%; margin: 0 5px 0 5px"> <NSpace justify="space-between" align="center" style="height: 100%; margin: 0 5px 0 5px">
<NSpace align="center"> <NSpace align="center">
<div <div
:style="`border-radius: 4px; background-color: ${song.status == SongRequestStatus.Singing ? '#75c37f' : '#577fb8'}; width: 10px; height: 20px`" :style="`border-radius: 4px; background-color: ${song.status == SongRequestStatus.Singing ? '#75c37f' : '#577fb8'}; width: 10px; height: 20px`">
></div> </div>
<NText strong style="font-size: 18px"> <NText strong style="font-size: 18px">
{{ song.songName }} {{ song.songName }}
</NText> </NText>
@@ -922,12 +903,10 @@ onUnmounted(() => {
{{ song.user?.uid }} {{ song.user?.uid }}
</NTooltip> </NTooltip>
</template> </template>
<NSpace <NSpace v-if="
v-if=" (song.from == SongRequestFrom.Danmaku || song.from == SongRequestFrom.SC) &&
(song.from == SongRequestFrom.Danmaku || song.from == SongRequestFrom.SC) && song.user?.fans_medal_wearing_status
song.user?.fans_medal_wearing_status ">
"
>
<NTag size="tiny" round> <NTag size="tiny" round>
<NTag size="tiny" round :bordered="false"> <NTag size="tiny" round :bordered="false">
<NText depth="3"> <NText depth="3">
@@ -939,26 +918,16 @@ onUnmounted(() => {
</span> </span>
</NTag> </NTag>
</NSpace> </NSpace>
<NTag <NTag v-if="(song.user?.guard_level ?? 0) > 0" size="small" :bordered="false"
v-if="(song.user?.guard_level ?? 0) > 0" :color="{ textColor: 'white', color: GetGuardColor(song.user?.guard_level) }">
size="small"
:bordered="false"
:color="{ textColor: 'white', color: GetGuardColor(song.user?.guard_level) }"
>
{{ song.user?.guard_level == 1 ? '总督' : song.user?.guard_level == 2 ? '提督' : '舰长' }} {{ song.user?.guard_level == 1 ? '总督' : song.user?.guard_level == 2 ? '提督' : '舰长' }}
</NTag> </NTag>
<NTag <NTag v-if="song.from == SongRequestFrom.SC" size="small"
v-if="song.from == SongRequestFrom.SC" :color="{ textColor: 'white', color: GetSCColor(song.price ?? 0) }">
size="small"
:color="{ textColor: 'white', color: GetSCColor(song.price ?? 0) }"
>
SC | {{ song.price }} SC | {{ song.price }}
</NTag> </NTag>
<NTag <NTag v-if="song.from == SongRequestFrom.Gift" size="small"
v-if="song.from == SongRequestFrom.Gift" :color="{ textColor: 'white', color: GetSCColor(song.price ?? 0) }">
size="small"
:color="{ textColor: 'white', color: GetSCColor(song.price ?? 0) }"
>
Gift | {{ song.price }} Gift | {{ song.price }}
</NTag> </NTag>
<NTooltip> <NTooltip>
@@ -973,13 +942,8 @@ onUnmounted(() => {
<NSpace justify="end" align="center"> <NSpace justify="end" align="center">
<NTooltip v-if="song.song"> <NTooltip v-if="song.song">
<template #trigger> <template #trigger>
<NButton <NButton circle type="success" style="height: 30px; width: 30px"
circle :loading="isLrcLoading == song?.song?.key" @click="selectedSong = song.song">
type="success"
style="height: 30px; width: 30px"
:loading="isLrcLoading == song?.song?.key"
@click="selectedSong = song.song"
>
<template #icon> <template #icon>
<NIcon :component="Play24Filled" /> <NIcon :component="Play24Filled" />
</template> </template>
@@ -989,25 +953,17 @@ onUnmounted(() => {
</NTooltip> </NTooltip>
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NButton <NButton circle type="primary" style="height: 30px; width: 30px" :disabled="songs.findIndex((s) => s.id != song.id && s.status == SongRequestStatus.Singing) > -1
circle " @click="
type="primary"
style="height: 30px; width: 30px"
:disabled="
songs.findIndex((s) => s.id != song.id && s.status == SongRequestStatus.Singing) > -1
"
@click="
updateSongStatus( updateSongStatus(
song, song,
song.status == SongRequestStatus.Singing song.status == SongRequestStatus.Singing
? SongRequestStatus.Waiting ? SongRequestStatus.Waiting
: SongRequestStatus.Singing, : SongRequestStatus.Singing,
) )
" "
:style="`animation: ${song.status == SongRequestStatus.Waiting ? '' : 'loading 5s linear infinite'}`" :style="`animation: ${song.status == SongRequestStatus.Waiting ? '' : 'loading 5s linear infinite'}`"
:secondary="song.status == SongRequestStatus.Singing" :secondary="song.status == SongRequestStatus.Singing" :loading="isLoading">
:loading="isLoading"
>
<template #icon> <template #icon>
<NIcon :component="Mic24Filled" /> <NIcon :component="Mic24Filled" />
</template> </template>
@@ -1023,13 +979,8 @@ onUnmounted(() => {
</NTooltip> </NTooltip>
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NButton <NButton circle type="success" style="height: 30px; width: 30px" :loading="isLoading"
circle @click="updateSongStatus(song, SongRequestStatus.Finish)">
type="success"
style="height: 30px; width: 30px"
:loading="isLoading"
@click="updateSongStatus(song, SongRequestStatus.Finish)"
>
<template #icon> <template #icon>
<NIcon :component="Checkmark12Regular" /> <NIcon :component="Checkmark12Regular" />
</template> </template>
@@ -1054,13 +1005,8 @@ onUnmounted(() => {
</NTooltip> </NTooltip>
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NButton <NButton circle type="error" style="height: 30px; width: 30px" :loading="isLoading"
circle @click="updateSongStatus(song, SongRequestStatus.Cancel)">
type="error"
style="height: 30px; width: 30px"
:loading="isLoading"
@click="updateSongStatus(song, SongRequestStatus.Cancel)"
>
<template #icon> <template #icon>
<NIcon :component="Dismiss16Filled" /> <NIcon :component="Dismiss16Filled" />
</template> </template>
@@ -1096,20 +1042,14 @@ onUnmounted(() => {
</NInputGroup> </NInputGroup>
</NSpace> </NSpace>
</NCard> </NCard>
<NDataTable <NDataTable size="small" ref="table" :columns="columns" :data="songs" :pagination="{
size="small" itemCount: songs.length,
ref="table" pageSizes: [20, 50, 100],
:columns="columns" showSizePicker: true,
:data="songs" prefix({ itemCount }) {
:pagination="{ return `共 ${itemCount} 条记录`
itemCount: songs.length, },
pageSizes: [20, 50, 100], }" />
showSizePicker: true,
prefix({ itemCount }) {
return `共 ${itemCount} 条记录`
},
}"
/>
</NTabPane> </NTabPane>
<NTabPane name="setting" tab="设置"> <NTabPane name="setting" tab="设置">
<NSpin :show="isLoading"> <NSpin :show="isLoading">
@@ -1132,26 +1072,17 @@ onUnmounted(() => {
<NButton @click="updateSettings" type="info" :disabled="!configCanEdit">确定</NButton> <NButton @click="updateSettings" type="info" :disabled="!configCanEdit">确定</NButton>
</NInputGroup> </NInputGroup>
<NSpace align="center"> <NSpace align="center">
<NCheckbox <NCheckbox v-model:checked="settings.enableOnStreaming" @update:checked="updateSettings"
v-model:checked="settings.enableOnStreaming" :disabled="!configCanEdit">
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
仅在直播时才允许加入 仅在直播时才允许加入
</NCheckbox> </NCheckbox>
<NCheckbox <NCheckbox v-model:checked="settings.allowAllDanmaku" @update:checked="updateSettings"
v-model:checked="settings.allowAllDanmaku" :disabled="!configCanEdit">
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
允许所有弹幕点播 允许所有弹幕点播
</NCheckbox> </NCheckbox>
<template v-if="!settings.allowAllDanmaku"> <template v-if="!settings.allowAllDanmaku">
<NCheckbox <NCheckbox v-model:checked="settings.needWearFanMedal" @update:checked="updateSettings"
v-model:checked="settings.needWearFanMedal" :disabled="!configCanEdit">
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
需要拥有粉丝牌 需要拥有粉丝牌
</NCheckbox> </NCheckbox>
<NInputGroup v-if="settings.needWearFanMedal" style="width: 250px"> <NInputGroup v-if="settings.needWearFanMedal" style="width: 250px">
@@ -1159,28 +1090,16 @@ onUnmounted(() => {
<NInputNumber v-model:value="settings.fanMedalMinLevel" :disabled="!configCanEdit" /> <NInputNumber v-model:value="settings.fanMedalMinLevel" :disabled="!configCanEdit" />
<NButton @click="updateSettings" type="info" :disabled="!configCanEdit">确定</NButton> <NButton @click="updateSettings" type="info" :disabled="!configCanEdit">确定</NButton>
</NInputGroup> </NInputGroup>
<NCheckbox <NCheckbox v-if="!settings.allowAllDanmaku" v-model:checked="settings.needJianzhang"
v-if="!settings.allowAllDanmaku" @update:checked="updateSettings" :disabled="!configCanEdit">
v-model:checked="settings.needJianzhang"
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
只允许舰长 只允许舰长
</NCheckbox> </NCheckbox>
<NCheckbox <NCheckbox v-if="!settings.allowAllDanmaku" v-model:checked="settings.needTidu"
v-if="!settings.allowAllDanmaku" @update:checked="updateSettings" :disabled="!configCanEdit">
v-model:checked="settings.needTidu"
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
只允许提督 只允许提督
</NCheckbox> </NCheckbox>
<NCheckbox <NCheckbox v-if="!settings.allowAllDanmaku" v-model:checked="settings.needZongdu"
v-if="!settings.allowAllDanmaku" @update:checked="updateSettings" :disabled="!configCanEdit">
v-model:checked="settings.needZongdu"
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
只允许总督 只允许总督
</NCheckbox> </NCheckbox>
</template> </template>
@@ -1190,11 +1109,8 @@ onUnmounted(() => {
允许通过 SuperChat 点播 允许通过 SuperChat 点播
</NCheckbox> </NCheckbox>
<span v-if="settings.allowSC"> <span v-if="settings.allowSC">
<NCheckbox <NCheckbox v-model:checked="settings.allowSC" @update:checked="updateSettings"
v-model:checked="settings.allowSC" :disabled="!configCanEdit">
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
SC 点播无视限制 SC 点播无视限制
</NCheckbox> </NCheckbox>
<NTooltip> <NTooltip>
@@ -1273,29 +1189,20 @@ onUnmounted(() => {
</NSpace> --> </NSpace> -->
<NDivider> 点歌 </NDivider> <NDivider> 点歌 </NDivider>
<NSpace> <NSpace>
<NCheckbox <NCheckbox v-model:checked="settings.onlyAllowSongList" @update:checked="updateSettings"
v-model:checked="settings.onlyAllowSongList" :disabled="!configCanEdit">
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
仅允许点 仅允许点
<NButton text tag="a" href="/manage/song-list" target="_blank" type="info"> 歌单 </NButton> <NButton text tag="a" href="/manage/song-list" target="_blank" type="info"> 歌单 </NButton>
内的歌曲 内的歌曲
</NCheckbox> </NCheckbox>
<NCheckbox <NCheckbox v-model:checked="settings.allowFromWeb" @update:checked="updateSettings"
v-model:checked="settings.allowFromWeb" :disabled="!configCanEdit">
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
允许通过网页点歌 允许通过网页点歌
</NCheckbox> </NCheckbox>
</NSpace> </NSpace>
<NDivider> 冷却 (单位: 秒) </NDivider> <NDivider> 冷却 (单位: 秒) </NDivider>
<NCheckbox <NCheckbox v-model:checked="settings.enableCooldown" @update:checked="updateSettings"
v-model:checked="settings.enableCooldown" :disabled="!configCanEdit">
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
启用点播冷却 启用点播冷却
</NCheckbox> </NCheckbox>
<NSpace v-if="settings.enableCooldown"> <NSpace v-if="settings.enableCooldown">
@@ -1329,25 +1236,16 @@ onUnmounted(() => {
<NButton @click="updateSettings" type="primary">确定</NButton> <NButton @click="updateSettings" type="primary">确定</NButton>
</template> </template>
</NInputGroup> </NInputGroup>
<NCheckbox <NCheckbox v-model:checked="settings.showRequireInfo" :disabled="!configCanEdit"
v-model:checked="settings.showRequireInfo" @update:checked="updateSettings">
:disabled="!configCanEdit"
@update:checked="updateSettings"
>
显示底部的需求信息 显示底部的需求信息
</NCheckbox> </NCheckbox>
<NCheckbox <NCheckbox v-model:checked="settings.showUserName" :disabled="!configCanEdit"
v-model:checked="settings.showUserName" @update:checked="updateSettings">
:disabled="!configCanEdit"
@update:checked="updateSettings"
>
显示点播用户名 显示点播用户名
</NCheckbox> </NCheckbox>
<NCheckbox <NCheckbox v-model:checked="settings.showFanMadelInfo" :disabled="!configCanEdit"
v-model:checked="settings.showFanMadelInfo" @update:checked="updateSettings">
:disabled="!configCanEdit"
@update:checked="updateSettings"
>
显示点播用户粉丝牌 显示点播用户粉丝牌
</NCheckbox> </NCheckbox>
</NSpace> </NSpace>
@@ -1368,7 +1266,7 @@ onUnmounted(() => {
<LiveRequestOBS :id="accountInfo?.id" /> <LiveRequestOBS :id="accountInfo?.id" />
</div> </div>
<br /> <br />
<NInput :value="'https://vtsuru.live/obs/live-request?id=' + accountInfo?.id" /> <NInput :value="`${CURRENT_HOST}obs/live-request?id=` + accountInfo?.id" />
<NDivider /> <NDivider />
<NCollapse> <NCollapse>
<NCollapseItem title="使用说明"> <NCollapseItem title="使用说明">
@@ -1385,15 +1283,18 @@ onUnmounted(() => {
<style> <style>
@keyframes loading { @keyframes loading {
/*以百分比来规定改变发生的时间 也可以通过"from"和"to",等价于0% 和 100%*/ /*以百分比来规定改变发生的时间 也可以通过"from"和"to",等价于0% 和 100%*/
0% { 0% {
/*rotate(2D旋转) scale(放大或者缩小) translate(移动) skew(翻转)*/ /*rotate(2D旋转) scale(放大或者缩小) translate(移动) skew(翻转)*/
transform: rotate(0deg); transform: rotate(0deg);
} }
100% { 100% {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
@keyframes animated-border { @keyframes animated-border {
0% { 0% {
box-shadow: 0 0 0px #589580; box-shadow: 0 0 0px #589580;
@@ -1403,6 +1304,7 @@ onUnmounted(() => {
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0); box-shadow: 0 0 0 4px rgba(255, 255, 255, 0);
} }
} }
@keyframes animated-border-round { @keyframes animated-border-round {
0% { 0% {
box-shadow: 0 0 0px #589580; box-shadow: 0 0 0px #589580;

View File

@@ -3,7 +3,7 @@ import { DownloadConfig, UploadConfig, useAccount } from '@/api/account'
import { DanmakuUserInfo, EventModel, SongFrom, SongsInfo } from '@/api/api-models' import { DanmakuUserInfo, EventModel, SongFrom, SongsInfo } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query' import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import { RoomAuthInfo } from '@/data/DanmakuClient' import { RoomAuthInfo } from '@/data/DanmakuClient'
import { MUSIC_REQUEST_API_URL, SONG_API_URL } from '@/data/constants' import { CURRENT_HOST, MUSIC_REQUEST_API_URL, SONG_API_URL } from '@/data/constants'
import { useDanmakuClient } from '@/store/useDanmakuClient' import { useDanmakuClient } from '@/store/useDanmakuClient'
import { MusicRequestSettings, useMusicRequestProvider } from '@/store/useMusicRequest' import { MusicRequestSettings, useMusicRequestProvider } from '@/store/useMusicRequest'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
@@ -103,10 +103,10 @@ const neteaseSongListId = computed(() => {
return Number(match[1]) return Number(match[1])
} }
} }
} catch (err) {} } catch (err) { }
try { try {
return Number(neteaseIdInput.value) return Number(neteaseIdInput.value)
} catch {} } catch { }
return null return null
}) })
const neteaseSongs = ref<SongsInfo[]>([]) const neteaseSongs = ref<SongsInfo[]>([])
@@ -149,7 +149,7 @@ async function searchMusic(keyword: string) {
} }
return undefined return undefined
} }
function switchTo() {} function switchTo() { }
async function getNeteaseSongList() { async function getNeteaseSongList() {
isLoading.value = true isLoading.value = true
await QueryGetAPI<SongsInfo[]>(SONG_API_URL + 'get-netease-list', { await QueryGetAPI<SongsInfo[]>(SONG_API_URL + 'get-netease-list', {
@@ -387,14 +387,9 @@ onUnmounted(() => {
</NSpace> </NSpace>
<NDivider /> <NDivider />
<NSpace align="center"> <NSpace align="center">
<NButton <NButton @click="listening ? stopListen() : startListen()" :type="listening ? 'error' : 'primary'"
@click="listening ? stopListen() : startListen()" :style="{ animation: listening ? 'animated-border 2.5s infinite' : '' }" data-umami-event="Use Music Request"
:type="listening ? 'error' : 'primary'" :data-umami-event-uid="accountInfo?.biliId" size="large">
:style="{ animation: listening ? 'animated-border 2.5s infinite' : '' }"
data-umami-event="Use Music Request"
:data-umami-event-uid="accountInfo?.biliId"
size="large"
>
{{ listening ? '停止监听' : '开始监听' }} {{ listening ? '停止监听' : '开始监听' }}
</NButton> </NButton>
<NButton @click="showOBSModal = true" type="info" size="small"> OBS组件 </NButton> <NButton @click="showOBSModal = true" type="info" size="small"> OBS组件 </NButton>
@@ -420,12 +415,8 @@ onUnmounted(() => {
<NButton @click="musicRquestStore.playMusic(item.music)" type="primary" secondary size="small"> <NButton @click="musicRquestStore.playMusic(item.music)" type="primary" secondary size="small">
播放 播放
</NButton> </NButton>
<NButton <NButton @click="musicRquestStore.waitingMusics.splice(musicRquestStore.waitingMusics.indexOf(item), 1)"
@click="musicRquestStore.waitingMusics.splice(musicRquestStore.waitingMusics.indexOf(item), 1)" type="error" secondary size="small">
type="error"
secondary
size="small"
>
取消 取消
</NButton> </NButton>
<NButton @click="blockMusic(item.music)" type="warning" secondary size="small"> 拉黑 </NButton> <NButton @click="blockMusic(item.music)" type="warning" secondary size="small"> 拉黑 </NButton>
@@ -455,26 +446,18 @@ onUnmounted(() => {
<NInputGroupLabel> 点歌弹幕前缀 </NInputGroupLabel> <NInputGroupLabel> 点歌弹幕前缀 </NInputGroupLabel>
<NInput v-model:value="settings.orderPrefix" /> <NInput v-model:value="settings.orderPrefix" />
</NInputGroup> </NInputGroup>
<NCheckbox <NCheckbox :checked="settings.orderCooldown != undefined" @update:checked="(checked: boolean) => {
:checked="settings.orderCooldown != undefined" settings.orderCooldown = checked ? 300 : undefined
@update:checked=" }
(checked: boolean) => { ">
settings.orderCooldown = checked ? 300 : undefined
}
"
>
是否启用点歌冷却 是否启用点歌冷却
</NCheckbox> </NCheckbox>
<NInputGroup v-if="settings.orderCooldown" style="width: 200px"> <NInputGroup v-if="settings.orderCooldown" style="width: 200px">
<NInputGroupLabel> 冷却时间 () </NInputGroupLabel> <NInputGroupLabel> 冷却时间 () </NInputGroupLabel>
<NInputNumber <NInputNumber v-model:value="settings.orderCooldown" @update:value="(value) => {
v-model:value="settings.orderCooldown" if (!value || value <= 0) settings.orderCooldown = undefined
@update:value=" }
(value) => { " />
if (!value || value <= 0) settings.orderCooldown = undefined
}
"
/>
</NInputGroup> </NInputGroup>
</NSpace> </NSpace>
<NSpace> <NSpace>
@@ -488,13 +471,9 @@ onUnmounted(() => {
</template> </template>
获取和修改输出设备需要打开麦克风权限 获取和修改输出设备需要打开麦克风权限
</NTooltip> </NTooltip>
<NSelect <NSelect v-model:value="settings.deviceId" :options="deviceList"
v-model:value="settings.deviceId" :fallback-option="() => ({ label: '未选择', value: '' })" style="min-width: 200px"
:options="deviceList" @update:value="musicRquestStore.setSinkId" />
:fallback-option="() => ({ label: '未选择', value: '' })"
style="min-width: 200px"
@update:value="musicRquestStore.setSinkId"
/>
</NSpace> </NSpace>
</NSpace> </NSpace>
</NTabPane> </NTabPane>
@@ -532,12 +511,8 @@ onUnmounted(() => {
<NList> <NList>
<NListItem v-for="item in settings.blacklist" :key="item"> <NListItem v-for="item in settings.blacklist" :key="item">
<NSpace align="center" style="width: 100%"> <NSpace align="center" style="width: 100%">
<NButton <NButton @click="settings.blacklist.splice(settings.blacklist.indexOf(item), 1)" type="error" secondary
@click="settings.blacklist.splice(settings.blacklist.indexOf(item), 1)" size="small">
type="error"
secondary
size="small"
>
删除 删除
</NButton> </NButton>
<NText> {{ item }} </NText> <NText> {{ item }} </NText>
@@ -548,14 +523,8 @@ onUnmounted(() => {
</NTabs> </NTabs>
<NDivider style="height: 100px" /> <NDivider style="height: 100px" />
<NModal v-model:show="showNeteaseModal" preset="card" :title="`获取歌单`" style="max-width: 600px"> <NModal v-model:show="showNeteaseModal" preset="card" :title="`获取歌单`" style="max-width: 600px">
<NInput <NInput clearable style="width: 100%" autosize :status="neteaseSongListId ? 'success' : 'error'"
clearable v-model:value="neteaseIdInput" placeholder="直接输入歌单Id或者网页链接">
style="width: 100%"
autosize
:status="neteaseSongListId ? 'success' : 'error'"
v-model:value="neteaseIdInput"
placeholder="直接输入歌单Id或者网页链接"
>
<template #suffix> <template #suffix>
<NTag v-if="neteaseSongListId" type="success" size="small"> 歌单Id: {{ neteaseSongListId }} </NTag> <NTag v-if="neteaseSongListId" type="success" size="small"> 歌单Id: {{ neteaseSongListId }} </NTag>
</template> </template>
@@ -566,13 +535,8 @@ onUnmounted(() => {
</NButton> </NButton>
<template v-if="neteaseSongsOptions.length > 0"> <template v-if="neteaseSongsOptions.length > 0">
<NDivider style="margin: 10px" /> <NDivider style="margin: 10px" />
<NTransfer <NTransfer style="height: 500px" ref="transfer" v-model:value="selectedNeteaseSongs"
style="height: 500px" :options="neteaseSongsOptions" source-filterable />
ref="transfer"
v-model:value="selectedNeteaseSongs"
:options="neteaseSongsOptions"
source-filterable
/>
<NDivider style="margin: 10px" /> <NDivider style="margin: 10px" />
<NButton type="primary" @click="addNeteaseSongs" :loading="isLoading"> <NButton type="primary" @click="addNeteaseSongs" :loading="isLoading">
添加到歌单 | {{ selectedNeteaseSongs.length }} 首 添加到歌单 | {{ selectedNeteaseSongs.length }} 首
@@ -586,7 +550,7 @@ onUnmounted(() => {
<MusicRequestOBS :id="accountInfo?.id" /> <MusicRequestOBS :id="accountInfo?.id" />
</div> </div>
<br /> <br />
<NInput :value="'https://vtsuru.live/obs/music-request?id=' + accountInfo?.id" /> <NInput :value="`${CURRENT_HOST}obs/music-request?id=` + accountInfo?.id" />
<NDivider /> <NDivider />
<NCollapse> <NCollapse>
<NCollapseItem title="使用说明"> <NCollapseItem title="使用说明">
@@ -606,6 +570,7 @@ onUnmounted(() => {
max-height: 300px; max-height: 300px;
overflow-y: auto; overflow-y: auto;
} }
@keyframes animated-border { @keyframes animated-border {
0% { 0% {
box-shadow: 0 0 0px #589580; box-shadow: 0 0 0px #589580;

View File

@@ -3,7 +3,7 @@ import { useAccount } from '@/api/account'
import { OpenLiveLotteryType, OpenLiveLotteryUserInfo, UpdateLiveLotteryUsersModel } from '@/api/api-models' import { OpenLiveLotteryType, OpenLiveLotteryUserInfo, UpdateLiveLotteryUsersModel } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query' import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import { DanmakuInfo, GiftInfo, RoomAuthInfo } from '@/data/DanmakuClient' import { DanmakuInfo, GiftInfo, RoomAuthInfo } from '@/data/DanmakuClient'
import { LOTTERY_API_URL } from '@/data/constants' import { CURRENT_HOST, LOTTERY_API_URL } from '@/data/constants'
import { useDanmakuClient } from '@/store/useDanmakuClient' import { useDanmakuClient } from '@/store/useDanmakuClient'
import { Delete24Filled, Info24Filled } from '@vicons/fluent' import { Delete24Filled, Info24Filled } from '@vicons/fluent'
import { useLocalStorage, useStorage } from '@vueuse/core' import { useLocalStorage, useStorage } from '@vueuse/core'
@@ -108,7 +108,7 @@ async function getUsers() {
if (data.code == 200) { if (data.code == 200) {
return data.data return data.data
} }
} catch (err) {} } catch (err) { }
return null return null
} }
function updateUsers() { function updateUsers() {
@@ -181,7 +181,7 @@ function startLottery() {
if (currentUsers.value.length > lotteryOption.value.resultCount) { if (currentUsers.value.length > lotteryOption.value.resultCount) {
console.log( console.log(
`[${currentUsers.value.length}] 移除` + `[${currentUsers.value.length}] 移除` +
currentUsers.value.splice(getRandomInt(currentUsers.value.length), 1)[0].name, currentUsers.value.splice(getRandomInt(currentUsers.value.length), 1)[0].name,
) )
setTimeout(() => { setTimeout(() => {
removeSingleUser() removeSingleUser()
@@ -199,7 +199,7 @@ function startLottery() {
while (currentUsers.value.length > lotteryOption.value.resultCount) { while (currentUsers.value.length > lotteryOption.value.resultCount) {
console.log( console.log(
`[${currentUsers.value.length}] 移除` + `[${currentUsers.value.length}] 移除` +
currentUsers.value.splice(getRandomInt(currentUsers.value.length), 1)[0].name, currentUsers.value.splice(getRandomInt(currentUsers.value.length), 1)[0].name,
) )
} }
onFinishLottery() onFinishLottery()
@@ -210,7 +210,7 @@ function startLottery() {
while (currentUsers.value.length > half) { while (currentUsers.value.length > half) {
console.log( console.log(
`[${currentUsers.value.length}] 移除` + `[${currentUsers.value.length}] 移除` +
currentUsers.value.splice(getRandomInt(currentUsers.value.length), 1)[0].name, currentUsers.value.splice(getRandomInt(currentUsers.value.length), 1)[0].name,
) )
} }
} }
@@ -342,12 +342,7 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<NResult <NResult v-if="!code && !accountInfo" status="403" title="403" description="该页面只能从幻星平台访问或者注册用户使用" />
v-if="!code && !accountInfo"
status="403"
title="403"
description="该页面只能从幻星平台访问或者注册用户使用"
/>
<template v-else> <template v-else>
<NCard> <NCard>
<template #header> <template #header>
@@ -391,13 +386,8 @@ onUnmounted(() => {
<NCollapseTransition> <NCollapseTransition>
<NInputGroup v-if="lotteryOption.needFanMedal" style="max-width: 200px"> <NInputGroup v-if="lotteryOption.needFanMedal" style="max-width: 200px">
<NInputGroupLabel> 最低粉丝牌等级 </NInputGroupLabel> <NInputGroupLabel> 最低粉丝牌等级 </NInputGroupLabel>
<NInputNumber <NInputNumber v-model:value="lotteryOption.fanCardLevel" min="1" max="50" :default-value="1"
v-model:value="lotteryOption.fanCardLevel" :disabled="isLottering || isStartLottery" />
min="1"
max="50"
:default-value="1"
:disabled="isLottering || isStartLottery"
/>
</NInputGroup> </NInputGroup>
</NCollapseTransition> </NCollapseTransition>
<template v-if="lotteryOption.type == 'danmaku'"> <template v-if="lotteryOption.type == 'danmaku'">
@@ -405,21 +395,14 @@ onUnmounted(() => {
<template #trigger> <template #trigger>
<NInputGroup style="max-width: 250px"> <NInputGroup style="max-width: 250px">
<NInputGroupLabel> 弹幕内容 </NInputGroupLabel> <NInputGroupLabel> 弹幕内容 </NInputGroupLabel>
<NInput <NInput :disabled="isStartLottery" v-model:value="lotteryOption.danmakuKeyword"
:disabled="isStartLottery" placeholder="留空则任何弹幕都可以" />
v-model:value="lotteryOption.danmakuKeyword"
placeholder="留空则任何弹幕都可以"
/>
</NInputGroup> </NInputGroup>
</template> </template>
符合规则的弹幕才会被添加到抽奖队列中 符合规则的弹幕才会被添加到抽奖队列中
</NTooltip> </NTooltip>
<NRadioGroup <NRadioGroup v-model:value="lotteryOption.danmakuFilterType" name="判定类型" :disabled="isLottering"
v-model:value="lotteryOption.danmakuFilterType" size="small">
name="判定类型"
:disabled="isLottering"
size="small"
>
<NRadioButton :disabled="isStartLottery" value="all"> 完全一致 </NRadioButton> <NRadioButton :disabled="isStartLottery" value="all"> 完全一致 </NRadioButton>
<NRadioButton :disabled="isStartLottery" value="contains"> 包含 </NRadioButton> <NRadioButton :disabled="isStartLottery" value="contains"> 包含 </NRadioButton>
<NRadioButton :disabled="isStartLottery" value="regex"> 正则 </NRadioButton> <NRadioButton :disabled="isStartLottery" value="regex"> 正则 </NRadioButton>
@@ -428,11 +411,8 @@ onUnmounted(() => {
<template v-else-if="lotteryOption.type == 'gift'"> <template v-else-if="lotteryOption.type == 'gift'">
<NInputGroup style="max-width: 250px"> <NInputGroup style="max-width: 250px">
<NInputGroupLabel> 最低价格 </NInputGroupLabel> <NInputGroupLabel> 最低价格 </NInputGroupLabel>
<NInputNumber <NInputNumber :disabled="isStartLottery" v-model:value="lotteryOption.giftMinPrice"
:disabled="isStartLottery" placeholder="留空则不限制" />
v-model:value="lotteryOption.giftMinPrice"
placeholder="留空则不限制"
/>
</NInputGroup> </NInputGroup>
<NInputGroup style="max-width: 200px"> <NInputGroup style="max-width: 200px">
<NInputGroupLabel> 礼物名称 </NInputGroupLabel> <NInputGroupLabel> 礼物名称 </NInputGroupLabel>
@@ -467,12 +447,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">
<NButton <NButton type="primary" @click="continueLottery" :loading="isStartLottery"
type="primary" :disabled="isStartLottery || isLotteried || !client">
@click="continueLottery"
:loading="isStartLottery"
:disabled="isStartLottery || isLotteried || !client"
>
开始 开始
</NButton> </NButton>
<NButton type="warning" :disabled="!isStartLottery" @click="pause"> 停止 </NButton> <NButton type="warning" :disabled="!isStartLottery" @click="pause"> 停止 </NButton>
@@ -482,15 +458,9 @@ onUnmounted(() => {
<template v-if="isStartLottery"> 进行抽取前需要先停止 </template> <template v-if="isStartLottery"> 进行抽取前需要先停止 </template>
</NDivider> </NDivider>
<NSpace justify="center"> <NSpace justify="center">
<NButton <NButton type="primary" secondary @click="startLottery" :loading="isLottering"
type="primary" :disabled="isStartLottery || isLotteried" data-umami-event="Open-Live Use Lottery"
secondary :data-umami-event-uid="client?.authInfo?.anchor_info?.uid">
@click="startLottery"
:loading="isLottering"
:disabled="isStartLottery || isLotteried"
data-umami-event="Open-Live Use Lottery"
:data-umami-event-uid="client?.authInfo?.anchor_info?.uid"
>
进行抽取 进行抽取
</NButton> </NButton>
<NButton type="info" secondary :disabled="isStartLottery || isLottering || !isLotteried" @click="reset"> <NButton type="info" secondary :disabled="isStartLottery || isLottering || !isLotteried" @click="reset">
@@ -503,15 +473,8 @@ onUnmounted(() => {
<NCard size="small" :title="item.name" style="height: 155px" embedded> <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 <NAvatar round lazy borderd :size="64" :src="item.avatar + '@64w_64h'"
round :img-props="{ referrerpolicy: 'no-referrer' }" style="box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2)" />
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 v-if="item.fans_medal_wearing_status"> <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">
@@ -526,12 +489,8 @@ onUnmounted(() => {
{{ item.name }} {{ item.name }}
</NSpace> </NSpace>
<NButton <NButton style="position: absolute; right: 5px; top: 5px; color: #753e3e" @click="removeUser(item)"
style="position: absolute; right: 5px; top: 5px; color: #753e3e" size="small" circle>
@click="removeUser(item)"
size="small"
circle
>
<template #icon> <template #icon>
<NIcon :component="Delete24Filled" /> <NIcon :component="Delete24Filled" />
</template> </template>
@@ -572,21 +531,15 @@ onUnmounted(() => {
</NScrollbar> </NScrollbar>
<NEmpty v-else description="暂无记录" /> <NEmpty v-else description="暂无记录" />
</NModal> </NModal>
<NModal <NModal v-model:show="showOBSModal" preset="card" title="OBS 组件"
v-model:show="showOBSModal" style="max-width: 90%; width: 800px; max-height: 90vh" closable content-style="overflow: auto">
preset="card"
title="OBS 组件"
style="max-width: 90%; width: 800px; max-height: 90vh"
closable
content-style="overflow: auto"
>
<NAlert title="这是什么? " type="info"> 将等待队列以及结果显示在OBS中 </NAlert> <NAlert title="这是什么? " type="info"> 将等待队列以及结果显示在OBS中 </NAlert>
<NDivider> 浏览 </NDivider> <NDivider> 浏览 </NDivider>
<div style="height: 400px; width: 250px; position: relative; margin: 0 auto"> <div style="height: 400px; width: 250px; position: relative; margin: 0 auto">
<LiveLotteryOBS :code="code" /> <LiveLotteryOBS :code="code" />
</div> </div>
<br /> <br />
<NInput :value="'https://vtsuru.live/obs/live-lottery?code=' + code" /> <NInput :value="`${CURRENT_HOST}obs/live-lottery?code=` + code" />
<NDivider /> <NDivider />
<NCollapse> <NCollapse>
<NCollapseItem title="使用说明"> <NCollapseItem title="使用说明">

View File

@@ -15,7 +15,7 @@ import {
} from '@/api/api-models' } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI, QueryPostAPIWithParams } from '@/api/query' import { QueryGetAPI, QueryPostAPI, QueryPostAPIWithParams } from '@/api/query'
import { RoomAuthInfo } from '@/data/DanmakuClient' import { RoomAuthInfo } from '@/data/DanmakuClient'
import { QUEUE_API_URL } from '@/data/constants' import { CURRENT_HOST, QUEUE_API_URL } from '@/data/constants'
import { useDanmakuClient } from '@/store/useDanmakuClient' import { useDanmakuClient } from '@/store/useDanmakuClient'
import { import {
Checkmark12Regular, Checkmark12Regular,
@@ -610,24 +610,24 @@ const columns = [
() => [ () => [
data.status == QueueStatus.Finish || data.status == QueueStatus.Cancel data.status == QueueStatus.Finish || data.status == QueueStatus.Cancel
? h(NTooltip, null, { ? h(NTooltip, null, {
trigger: () => trigger: () =>
h( h(
NButton, NButton,
{ {
size: 'small', size: 'small',
type: 'info', type: 'info',
circle: true, circle: true,
loading: isLoading.value, loading: isLoading.value,
onClick: () => { onClick: () => {
updateStatus(data, QueueStatus.Waiting) updateStatus(data, QueueStatus.Waiting)
},
}, },
{ },
icon: () => h(NIcon, { component: ReloadCircleSharp }), {
}, icon: () => h(NIcon, { component: ReloadCircleSharp }),
), },
default: () => '重新放回等待列表', ),
}) default: () => '重新放回等待列表',
})
: undefined, : undefined,
h( h(
NPopconfirm, NPopconfirm,
@@ -702,7 +702,7 @@ async function updateActive() {
message.error('无法获取队列: ' + data.message) message.error('无法获取队列: ' + data.message)
return [] return []
} }
} catch (err) {} } catch (err) { }
} }
function blockUser(item: ResponseQueueModel) { function blockUser(item: ResponseQueueModel) {
if (item.from != QueueFrom.Danmaku) { if (item.from != QueueFrom.Danmaku) {
@@ -762,15 +762,11 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<NAlert <NAlert :type="accountInfo.settings.enableFunctions.includes(FunctionTypes.Queue) ? 'success' : 'warning'"
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.Queue) ? 'success' : 'warning'" v-if="accountInfo.id">
v-if="accountInfo.id"
>
启用弹幕队列功能 启用弹幕队列功能
<NSwitch <NSwitch :value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.Queue)"
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.Queue)" @update:value="onUpdateFunctionEnable" />
@update:value="onUpdateFunctionEnable"
/>
<br /> <br />
<NText depth="3"> <NText depth="3">
@@ -781,11 +777,7 @@ onUnmounted(() => {
则其需要保持此页面开启才能使用, 也不要同时开多个页面, 会导致重复 !(部署了则不影响) 则其需要保持此页面开启才能使用, 也不要同时开多个页面, 会导致重复 !(部署了则不影响)
</NText> </NText>
</NAlert> </NAlert>
<NAlert <NAlert type="warning" v-else title="你尚未注册并登录 VTsuru.live, 大部分规则设置将不可用 (因为我懒得在前段重写一遍逻辑">
type="warning"
v-else
title="你尚未注册并登录 VTsuru.live, 大部分规则设置将不可用 (因为我懒得在前段重写一遍逻辑"
>
<NButton tag="a" href="/manage" target="_blank" type="primary"> 前往登录或注册 </NButton> <NButton tag="a" href="/manage" target="_blank" type="primary"> 前往登录或注册 </NButton>
</NAlert> </NAlert>
<br /> <br />
@@ -801,11 +793,8 @@ onUnmounted(() => {
</NCard> </NCard>
<br /> <br />
<NCard> <NCard>
<NTabs <NTabs v-if="!accountInfo || accountInfo.settings.enableFunctions.includes(FunctionTypes.Queue)" animated
v-if="!accountInfo || accountInfo.settings.enableFunctions.includes(FunctionTypes.Queue)" display-directive="show:lazy">
animated
display-directive="show:lazy"
>
<NTabPane name="list" tab="列表"> <NTabPane name="list" tab="列表">
<NCard size="small"> <NCard size="small">
<NSpace align="center"> <NSpace align="center">
@@ -833,12 +822,8 @@ onUnmounted(() => {
</template> </template>
确定全部取消吗? 确定全部取消吗?
</NPopconfirm> </NPopconfirm>
<NRadioGroup <NRadioGroup v-model:value="settings.sortType" :disabled="!configCanEdit" @update:value="updateSettings"
v-model:value="settings.sortType" type="button">
:disabled="!configCanEdit"
@update:value="updateSettings"
type="button"
>
<NRadioButton :value="QueueSortType.TimeFirst"> 加入时间优先 </NRadioButton> <NRadioButton :value="QueueSortType.TimeFirst"> 加入时间优先 </NRadioButton>
<NRadioButton :value="QueueSortType.PaymentFist"> 付费价格优先 </NRadioButton> <NRadioButton :value="QueueSortType.PaymentFist"> 付费价格优先 </NRadioButton>
<NRadioButton :value="QueueSortType.GuardFirst"> 舰长优先 (按等级) </NRadioButton> <NRadioButton :value="QueueSortType.GuardFirst"> 舰长优先 (按等级) </NRadioButton>
@@ -853,17 +838,12 @@ onUnmounted(() => {
<NDivider> {{ queue.length }} </NDivider> <NDivider> {{ queue.length }} </NDivider>
<NList v-if="queue.length > 0" :show-divider="false" hoverable> <NList v-if="queue.length > 0" :show-divider="false" hoverable>
<NListItem v-for="(queueData, index) in queue" :key="queueData.id" style="padding: 5px"> <NListItem v-for="(queueData, index) in queue" :key="queueData.id" style="padding: 5px">
<NCard <NCard embedded size="small" content-style="padding: 5px;"
embedded :style="`${queueData.status == QueueStatus.Progressing ? 'animation: animated-border 2.5s infinite;' : ''};height: 100%;`">
size="small"
content-style="padding: 5px;"
:style="`${queueData.status == QueueStatus.Progressing ? 'animation: animated-border 2.5s infinite;' : ''};height: 100%;`"
>
<NSpace justify="space-between" align="center" style="height: 100%; margin: 0 5px 0 5px"> <NSpace justify="space-between" align="center" style="height: 100%; margin: 0 5px 0 5px">
<NSpace align="center"> <NSpace align="center">
<div <div
:style="`border-radius: 4px; background-color: ${queueData.status == QueueStatus.Progressing ? '#75c37f' : '#577fb8'}; width: 20px; height: 20px;text-align: center;color: white;`" :style="`border-radius: 4px; background-color: ${queueData.status == QueueStatus.Progressing ? '#75c37f' : '#577fb8'}; width: 20px; height: 20px;text-align: center;color: white;`">
>
{{ index + 1 }} {{ index + 1 }}
</div> </div>
<NText strong style="font-size: 18px"> <NText strong style="font-size: 18px">
@@ -877,12 +857,10 @@ onUnmounted(() => {
<template v-if="queueData.from == QueueFrom.Manual"> <template v-if="queueData.from == QueueFrom.Manual">
<NTag size="small" :bordered="false"> 手动添加 </NTag> <NTag size="small" :bordered="false"> 手动添加 </NTag>
</template> </template>
<NSpace <NSpace v-if="
v-if=" (queueData.from == QueueFrom.Danmaku || queueData.from == QueueFrom.Gift) &&
(queueData.from == QueueFrom.Danmaku || queueData.from == QueueFrom.Gift) && queueData.user?.fans_medal_wearing_status
queueData.user?.fans_medal_wearing_status ">
"
>
<NTag size="tiny" round> <NTag size="tiny" round>
<NTag size="tiny" round :bordered="false"> <NTag size="tiny" round :bordered="false">
<NText depth="3"> <NText depth="3">
@@ -894,12 +872,8 @@ onUnmounted(() => {
</span> </span>
</NTag> </NTag>
</NSpace> </NSpace>
<NTag <NTag v-if="(queueData.user?.guard_level ?? 0) > 0" size="small" :bordered="false"
v-if="(queueData.user?.guard_level ?? 0) > 0" :color="{ textColor: 'white', color: GetGuardColor(queueData.user?.guard_level) }">
size="small"
:bordered="false"
:color="{ textColor: 'white', color: GetGuardColor(queueData.user?.guard_level) }"
>
{{ queueData.user?.guard_level == 1 ? '总督' : queueData.user?.guard_level == 2 ? '提督' : '舰长' }} {{ queueData.user?.guard_level == 1 ? '总督' : queueData.user?.guard_level == 2 ? '提督' : '舰长' }}
</NTag> </NTag>
<NTag v-if="(queueData.giftPrice ?? 0) > 0" size="small" :bordered="false" type="error"> <NTag v-if="(queueData.giftPrice ?? 0) > 0" size="small" :bordered="false" type="error">
@@ -933,23 +907,15 @@ onUnmounted(() => {
<NSpace justify="end" align="center"> <NSpace justify="end" align="center">
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NButton <NButton circle type="primary" style="height: 30px; width: 30px" :disabled="queue.findIndex((s) => s.id != queueData.id && s.status == QueueStatus.Progressing) > -1
circle " @click="
type="primary"
style="height: 30px; width: 30px"
:disabled="
queue.findIndex((s) => s.id != queueData.id && s.status == QueueStatus.Progressing) > -1
"
@click="
updateStatus( updateStatus(
queueData, queueData,
queueData.status == QueueStatus.Progressing ? QueueStatus.Waiting : QueueStatus.Progressing, queueData.status == QueueStatus.Progressing ? QueueStatus.Waiting : QueueStatus.Progressing,
) )
" "
:style="`animation: ${queueData.status == QueueStatus.Waiting ? '' : 'loading 5s linear infinite'}`" :style="`animation: ${queueData.status == QueueStatus.Waiting ? '' : 'loading 5s linear infinite'}`"
:secondary="queueData.status == QueueStatus.Progressing" :secondary="queueData.status == QueueStatus.Progressing" :loading="isLoading">
:loading="isLoading"
>
<template #icon> <template #icon>
<NIcon :component="ClipboardTextLtr24Filled" /> <NIcon :component="ClipboardTextLtr24Filled" />
</template> </template>
@@ -965,13 +931,8 @@ onUnmounted(() => {
</NTooltip> </NTooltip>
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NButton <NButton circle type="success" style="height: 30px; width: 30px" :loading="isLoading"
circle @click="updateStatus(queueData, QueueStatus.Finish)">
type="success"
style="height: 30px; width: 30px"
:loading="isLoading"
@click="updateStatus(queueData, QueueStatus.Finish)"
>
<template #icon> <template #icon>
<NIcon :component="Checkmark12Regular" /> <NIcon :component="Checkmark12Regular" />
</template> </template>
@@ -996,13 +957,8 @@ onUnmounted(() => {
</NTooltip> </NTooltip>
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NButton <NButton circle type="error" style="height: 30px; width: 30px" :loading="isLoading"
circle @click="updateStatus(queueData, QueueStatus.Cancel)">
type="error"
style="height: 30px; width: 30px"
:loading="isLoading"
@click="updateStatus(queueData, QueueStatus.Cancel)"
>
<template #icon> <template #icon>
<NIcon :component="Dismiss16Filled" /> <NIcon :component="Dismiss16Filled" />
</template> </template>
@@ -1030,20 +986,14 @@ onUnmounted(() => {
</NInputGroup> </NInputGroup>
</NSpace> </NSpace>
</NCard> </NCard>
<NDataTable <NDataTable size="small" ref="table" :columns="columns" :data="originQueue" :pagination="{
size="small" itemCount: originQueue.length,
ref="table" pageSizes: [20, 50, 100],
:columns="columns" showSizePicker: true,
:data="originQueue" prefix({ itemCount }) {
:pagination="{ return `共 ${itemCount} 条记录`
itemCount: originQueue.length, },
pageSizes: [20, 50, 100], }" />
showSizePicker: true,
prefix({ itemCount }) {
return `共 ${itemCount} 条记录`
},
}"
/>
</NTabPane> </NTabPane>
<NTabPane name="setting" tab="设置"> <NTabPane name="setting" tab="设置">
<NSpin :show="isLoading"> <NSpin :show="isLoading">
@@ -1058,12 +1008,8 @@ onUnmounted(() => {
</template> </template>
<NInput v-else v-model:value="defaultKeyword" /> <NInput v-else v-model:value="defaultKeyword" />
</NInputGroup> </NInputGroup>
<NRadioGroup <NRadioGroup v-model:value="settings.matchType" :disabled="!configCanEdit" @update:value="updateSettings"
v-model:value="settings.matchType" type="button">
:disabled="!configCanEdit"
@update:value="updateSettings"
type="button"
>
<NRadioButton :value="KeywordMatchType.Full"> 完全一致 </NRadioButton> <NRadioButton :value="KeywordMatchType.Full"> 完全一致 </NRadioButton>
<NRadioButton :value="KeywordMatchType.Contains"> 包含 </NRadioButton> <NRadioButton :value="KeywordMatchType.Contains"> 包含 </NRadioButton>
<NRadioButton :value="KeywordMatchType.Regex"> 正则 </NRadioButton> <NRadioButton :value="KeywordMatchType.Regex"> 正则 </NRadioButton>
@@ -1075,18 +1021,12 @@ onUnmounted(() => {
<NButton @click="updateSettings" type="info" :disabled="!configCanEdit">确定</NButton> <NButton @click="updateSettings" type="info" :disabled="!configCanEdit">确定</NButton>
</NInputGroup> </NInputGroup>
<NSpace align="center"> <NSpace align="center">
<NCheckbox <NCheckbox v-model:checked="settings.enableOnStreaming" @update:checked="updateSettings"
v-model:checked="settings.enableOnStreaming" :disabled="!configCanEdit">
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
仅在直播时才允许加入 仅在直播时才允许加入
</NCheckbox> </NCheckbox>
<NCheckbox <NCheckbox v-model:checked="settings.allowAllDanmaku" @update:checked="updateSettings"
v-model:checked="settings.allowAllDanmaku" :disabled="!configCanEdit">
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
允许所有用户加入 允许所有用户加入
</NCheckbox> </NCheckbox>
<template v-if="!settings.allowAllDanmaku"> <template v-if="!settings.allowAllDanmaku">
@@ -1095,38 +1035,23 @@ onUnmounted(() => {
<NInputNumber v-model:value="settings.fanMedalMinLevel" :disabled="!configCanEdit" min="0" /> <NInputNumber v-model:value="settings.fanMedalMinLevel" :disabled="!configCanEdit" min="0" />
<NButton @click="updateSettings" type="info" :disabled="!configCanEdit">确定</NButton> <NButton @click="updateSettings" type="info" :disabled="!configCanEdit">确定</NButton>
</NInputGroup> </NInputGroup>
<NCheckbox <NCheckbox v-if="!settings.allowAllDanmaku" v-model:checked="settings.needJianzhang"
v-if="!settings.allowAllDanmaku" @update:checked="updateSettings" :disabled="!configCanEdit">
v-model:checked="settings.needJianzhang"
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
允许舰长 允许舰长
</NCheckbox> </NCheckbox>
<NCheckbox <NCheckbox v-if="!settings.allowAllDanmaku" v-model:checked="settings.needTidu"
v-if="!settings.allowAllDanmaku" @update:checked="updateSettings" :disabled="!configCanEdit">
v-model:checked="settings.needTidu"
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
允许提督 允许提督
</NCheckbox> </NCheckbox>
<NCheckbox <NCheckbox v-if="!settings.allowAllDanmaku" v-model:checked="settings.needZongdu"
v-if="!settings.allowAllDanmaku" @update:checked="updateSettings" :disabled="!configCanEdit">
v-model:checked="settings.needZongdu"
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
允许总督 允许总督
</NCheckbox> </NCheckbox>
</template> </template>
</NSpace> </NSpace>
<NSpace align="center"> <NSpace align="center">
<NCheckbox <NCheckbox v-model:checked="settings.allowGift" @update:checked="updateSettings"
v-model:checked="settings.allowGift" :disabled="!configCanEdit">
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
允许通过发送礼物加入队列 允许通过发送礼物加入队列
</NCheckbox> </NCheckbox>
<template v-if="settings.allowGift"> <template v-if="settings.allowGift">
@@ -1137,34 +1062,19 @@ onUnmounted(() => {
</NInputGroup> </NInputGroup>
<NSpace align="center"> <NSpace align="center">
礼物名 礼物名
<NSelect <NSelect style="width: 250px" v-model:value="settings.giftNames" :disabled="!configCanEdit" filterable
style="width: 250px" multiple tag placeholder="礼物名称,按回车确认" :show-arrow="false" :show="false"
v-model:value="settings.giftNames" @update:value="updateSettings" />
:disabled="!configCanEdit"
filterable
multiple
tag
placeholder="礼物名称,按回车确认"
:show-arrow="false"
:show="false"
@update:value="updateSettings"
/>
</NSpace> </NSpace>
<span> <span>
<NRadioGroup <NRadioGroup v-model:value="settings.giftFilterType" :disabled="!configCanEdit"
v-model:value="settings.giftFilterType" @update:value="updateSettings">
:disabled="!configCanEdit"
@update:value="updateSettings"
>
<NRadioButton :value="QueueGiftFilterType.And"> 需同时满足礼物名和价格 </NRadioButton> <NRadioButton :value="QueueGiftFilterType.And"> 需同时满足礼物名和价格 </NRadioButton>
<NRadioButton :value="QueueGiftFilterType.Or"> 礼物名/价格 二选一 </NRadioButton> <NRadioButton :value="QueueGiftFilterType.Or"> 礼物名/价格 二选一 </NRadioButton>
</NRadioGroup> </NRadioGroup>
</span> </span>
<NCheckbox <NCheckbox v-model:checked="settings.sendGiftDirectJoin" @update:checked="updateSettings"
v-model:checked="settings.sendGiftDirectJoin" :disabled="!configCanEdit">
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
赠送礼物后自动加入队列 赠送礼物后自动加入队列
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
@@ -1174,36 +1084,24 @@ onUnmounted(() => {
</NTooltip> </NTooltip>
</NCheckbox> </NCheckbox>
<NCheckbox <NCheckbox v-model:checked="settings.sendGiftIgnoreLimit" @update:checked="updateSettings"
v-model:checked="settings.sendGiftIgnoreLimit" :disabled="!configCanEdit">
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
赠送礼物后无视用户等级限制 赠送礼物后无视用户等级限制
</NCheckbox> </NCheckbox>
</template> </template>
<NCheckbox <NCheckbox v-model:checked="settings.allowIncreasePaymentBySendGift" @update:checked="updateSettings"
v-model:checked="settings.allowIncreasePaymentBySendGift" :disabled="!configCanEdit">
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
在队列中时允许继续发送礼物累计付费量 (仅限上方设定的礼物) 在队列中时允许继续发送礼物累计付费量 (仅限上方设定的礼物)
</NCheckbox> </NCheckbox>
<NCheckbox <NCheckbox v-if="settings.allowIncreasePaymentBySendGift"
v-if="settings.allowIncreasePaymentBySendGift" v-model:checked="settings.allowIncreaseByAnyPayment" @update:checked="updateSettings"
v-model:checked="settings.allowIncreaseByAnyPayment" :disabled="!configCanEdit">
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
允许发送任意礼物来叠加付费量 允许发送任意礼物来叠加付费量
</NCheckbox> </NCheckbox>
</NSpace> </NSpace>
<NDivider> 冷却 (单位: 秒) </NDivider> <NDivider> 冷却 (单位: 秒) </NDivider>
<NCheckbox <NCheckbox v-model:checked="settings.enableCooldown" @update:checked="updateSettings"
v-model:checked="settings.enableCooldown" :disabled="!configCanEdit">
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
启用排队冷却 启用排队冷却
</NCheckbox> </NCheckbox>
<NSpace v-if="settings.enableCooldown"> <NSpace v-if="settings.enableCooldown">
@@ -1229,25 +1127,16 @@ onUnmounted(() => {
</NInputGroup> </NInputGroup>
</NSpace> </NSpace>
<NDivider> OBS </NDivider> <NDivider> OBS </NDivider>
<NCheckbox <NCheckbox v-model:checked="settings.showRequireInfo" :disabled="!configCanEdit"
v-model:checked="settings.showRequireInfo" @update:checked="updateSettings">
:disabled="!configCanEdit"
@update:checked="updateSettings"
>
显示底部的需求信息 显示底部的需求信息
</NCheckbox> </NCheckbox>
<NCheckbox <NCheckbox v-model:checked="settings.showPayment" :disabled="!configCanEdit"
v-model:checked="settings.showPayment" @update:checked="updateSettings">
:disabled="!configCanEdit"
@update:checked="updateSettings"
>
显示付费信息 显示付费信息
</NCheckbox> </NCheckbox>
<NCheckbox <NCheckbox v-model:checked="settings.showFanMadelInfo" :disabled="!configCanEdit"
v-model:checked="settings.showFanMadelInfo" @update:checked="updateSettings">
:disabled="!configCanEdit"
@update:checked="updateSettings"
>
显示用户粉丝牌 显示用户粉丝牌
</NCheckbox> </NCheckbox>
<NDivider> 其他 </NDivider> <NDivider> 其他 </NDivider>
@@ -1267,7 +1156,7 @@ onUnmounted(() => {
<QueueOBS :id="accountInfo?.id" /> <QueueOBS :id="accountInfo?.id" />
</div> </div>
<br /> <br />
<NInput :value="'https://vtsuru.live/obs/queue?id=' + accountInfo?.id" /> <NInput :value="`${CURRENT_HOST}obs/queue?id=` + accountInfo?.id" />
<NDivider /> <NDivider />
<NCollapse> <NCollapse>
<NCollapseItem title="使用说明"> <NCollapseItem title="使用说明">
@@ -1284,15 +1173,18 @@ onUnmounted(() => {
<style> <style>
@keyframes loading { @keyframes loading {
/*以百分比来规定改变发生的时间 也可以通过"from"和"to",等价于0% 和 100%*/ /*以百分比来规定改变发生的时间 也可以通过"from"和"to",等价于0% 和 100%*/
0% { 0% {
/*rotate(2D旋转) scale(放大或者缩小) translate(移动) skew(翻转)*/ /*rotate(2D旋转) scale(放大或者缩小) translate(移动) skew(翻转)*/
transform: rotate(0deg); transform: rotate(0deg);
} }
100% { 100% {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
@keyframes animated-border { @keyframes animated-border {
0% { 0% {
box-shadow: 0 0 0px #589580; box-shadow: 0 0 0px #589580;
@@ -1302,6 +1194,7 @@ onUnmounted(() => {
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0); box-shadow: 0 0 0 4px rgba(255, 255, 255, 0);
} }
} }
@keyframes animated-border-round { @keyframes animated-border-round {
0% { 0% {
box-shadow: 0 0 0px #589580; box-shadow: 0 0 0px #589580;

View File

@@ -111,8 +111,6 @@ const speechSynthesisInfo = ref<{
}>() }>()
const languageDisplayName = new Intl.DisplayNames(['zh'], { type: 'language' }) const languageDisplayName = new Intl.DisplayNames(['zh'], { type: 'language' })
const voiceOptions = computed(() => { const voiceOptions = computed(() => {
const status = EasySpeech.status()
if (status.status != 'init: complete') return []
return new List(EasySpeech.voices()) return new List(EasySpeech.voices())
.Select((v) => { .Select((v) => {
return { return {

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { AddressInfo } from '@/api/api-models' import { AddressInfo } from '@/api/api-models'
import AddressDisplay from '@/components/manage/AddressDisplay.vue' import AddressDisplay from '@/components/manage/AddressDisplay.vue'
import { POINT_API_URL, THINGS_URL } from '@/data/constants' import { CURRENT_HOST, POINT_API_URL, THINGS_URL } from '@/data/constants'
import { useAuthStore } from '@/store/useAuthStore' import { useAuthStore } from '@/store/useAuthStore'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { import {
@@ -242,16 +242,11 @@ function logout() {
<NListItem v-for="address in biliAuth.address" :key="address.id"> <NListItem v-for="address in biliAuth.address" :key="address.id">
<AddressDisplay :address="address"> <AddressDisplay :address="address">
<template #actions> <template #actions>
<NButton <NButton size="small" @click="() => {
size="small" currentAddress = address
@click=" showAddressModal = true
() => { }
currentAddress = address " type="info">
showAddressModal = true
}
"
type="info"
>
修改 修改
</NButton> </NButton>
<NPopconfirm @positive-click="() => deleteAddress(address?.id ?? '')"> <NPopconfirm @positive-click="() => deleteAddress(address?.id ?? '')">
@@ -267,7 +262,7 @@ function logout() {
</NFlex> </NFlex>
</NCollapseItem> </NCollapseItem>
<NCollapseItem title="登录链接" name="2"> <NCollapseItem title="登录链接" name="2">
<NInput type="textarea" :value="'https://vtsuru.live/bili-user?auth=' + useAuth.biliToken" readonly /> <NInput type="textarea" :value="`${CURRENT_HOST}bili-user?auth=` + useAuth.biliToken" readonly />
</NCollapseItem> </NCollapseItem>
</NCollapse> </NCollapse>
</NCard> </NCard>
@@ -285,72 +280,40 @@ function logout() {
<NListItem v-for="item in useAuth.biliTokens" :key="item.token" @click="switchAuth(item.token)"> <NListItem v-for="item in useAuth.biliTokens" :key="item.token" @click="switchAuth(item.token)">
<NFlex align="center"> <NFlex align="center">
<NTag v-if="useAuth.biliToken == item.token" type="info"> 当前账号 </NTag> <NTag v-if="useAuth.biliToken == item.token" type="info"> 当前账号 </NTag>
{{ item.name }} <NDivider vertical style="margin: 0" /><NText depth="3"> {{ item.uId }} </NText> {{ item.name }}
<NDivider vertical style="margin: 0" />
<NText depth="3"> {{ item.uId }} </NText>
</NFlex> </NFlex>
</NListItem> </NListItem>
</NList> </NList>
</NCard> </NCard>
</NFlex> </NFlex>
</NSpin> </NSpin>
<NModal <NModal v-model:show="showAddressModal" preset="card" style="width: 800px; max-width: 90vw; height: auto"
v-model:show="showAddressModal" title="添加/更新地址">
preset="card"
style="width: 800px; max-width: 90vw; height: auto"
title="添加/更新地址"
>
<NSpin v-if="currentAddress" :show="isLoading"> <NSpin v-if="currentAddress" :show="isLoading">
<NForm ref="formRef" :model="currentAddress" :rules="rules"> <NForm ref="formRef" :model="currentAddress" :rules="rules">
<NFormItem label="地址" path="area" required> <NFormItem label="地址" path="area" required>
<NFlex style="width: 100%"> <NFlex style="width: 100%">
<NSelect <NSelect v-model:value="currentAddress.province" :options="provinceOptions"
v-model:value="currentAddress.province" @update:value="onAreaSelectChange(0)" placeholder="请选择省" style="width: 100px" filterable />
:options="provinceOptions" <NSelect v-model:value="currentAddress.city" :key="currentAddress.province"
@update:value="onAreaSelectChange(0)" :options="cityOptions(currentAddress.province)" :disabled="!currentAddress?.province"
placeholder="请选择省" @update:value="onAreaSelectChange(1)" placeholder="请选择市" style="width: 100px" filterable />
style="width: 100px" <NSelect v-model:value="currentAddress.district" :key="currentAddress.city"
filterable :options="districtOptions(currentAddress.province, currentAddress.city)" :disabled="!currentAddress?.city"
/> @update:value="onAreaSelectChange(2)" placeholder="请选择区" style="width: 100px" filterable />
<NSelect <NSelect v-model:value="currentAddress.street" :key="currentAddress.district"
v-model:value="currentAddress.city"
:key="currentAddress.province"
:options="cityOptions(currentAddress.province)"
:disabled="!currentAddress?.province"
@update:value="onAreaSelectChange(1)"
placeholder="请选择市"
style="width: 100px"
filterable
/>
<NSelect
v-model:value="currentAddress.district"
:key="currentAddress.city"
:options="districtOptions(currentAddress.province, currentAddress.city)"
:disabled="!currentAddress?.city"
@update:value="onAreaSelectChange(2)"
placeholder="请选择区"
style="width: 100px"
filterable
/>
<NSelect
v-model:value="currentAddress.street"
:key="currentAddress.district"
:options="streetOptions(currentAddress.province, currentAddress.city, currentAddress.district)" :options="streetOptions(currentAddress.province, currentAddress.city, currentAddress.district)"
:disabled="!currentAddress?.district" :disabled="!currentAddress?.district" placeholder="请选择街道" style="width: 150px" filterable />
placeholder="请选择街道"
style="width: 150px"
filterable
/>
</NFlex> </NFlex>
</NFormItem> </NFormItem>
<NFormItem label="详细地址" path="address" required> <NFormItem label="详细地址" path="address" required>
<NInput v-model:value="currentAddress.address" placeholder="详细地址" type="textarea" /> <NInput v-model:value="currentAddress.address" placeholder="详细地址" type="textarea" />
</NFormItem> </NFormItem>
<NFormItem label="联系电话" path="phone" required> <NFormItem label="联系电话" path="phone" required>
<NInputNumber <NInputNumber v-model:value="currentAddress.phone" placeholder="联系电话" :show-button="false"
v-model:value="currentAddress.phone" style="width: 200px" />
placeholder="联系电话"
:show-button="false"
style="width: 200px"
/>
</NFormItem> </NFormItem>
<NFormItem label="联系人" path="name" required> <NFormItem label="联系人" path="name" required>
<NInput v-model:value="currentAddress.name" placeholder="联系人" style="max-width: 150px" /> <NInput v-model:value="currentAddress.name" placeholder="联系人" style="max-width: 150px" />
@@ -365,12 +328,10 @@ function logout() {
</NForm> </NForm>
</NSpin> </NSpin>
</NModal> </NModal>
<NModal <NModal v-model:show="showAgreementModal" title="用户协议" preset="card"
v-model:show="showAgreementModal" style="width: 800px; max-width: 90vw; height: 90vh">
title="用户协议" <NScrollbar style="height: 80vh">
preset="card" <UserAgreement />
style="width: 800px; max-width: 90vw; height: 90vh" </NScrollbar>
>
<NScrollbar style="height: 80vh"> <UserAgreement /></NScrollbar>
</NModal> </NModal>
</template> </template>

View File

@@ -2,11 +2,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { SaveSetting, useAccount } from '@/api/account' import { SaveSetting, useAccount } from '@/api/account'
import { QuestionDisplayAlign, Setting_QuestionDisplay } from '@/api/api-models' import { QuestionDisplayAlign, Setting_QuestionDisplay } from '@/api/api-models'
import { QueryPostAPI } from '@/api/query'
import QuestionItem from '@/components/QuestionItem.vue' import QuestionItem from '@/components/QuestionItem.vue'
import QuestionItems from '@/components/QuestionItems.vue' import QuestionItems from '@/components/QuestionItems.vue'
import { QUESTION_API_URL } from '@/data/constants' import { CURRENT_HOST } from '@/data/constants'
import { useQuestionBox } from '@/store/useQuestionBox' import { useQuestionBox } from '@/store/useQuestionBox'
import { useWebRTC } from '@/store/useRTC'
import QuestionDisplayCard from '@/views/manage/QuestionDisplayCard.vue' import QuestionDisplayCard from '@/views/manage/QuestionDisplayCard.vue'
import { import {
ArrowCircleLeft12Filled, ArrowCircleLeft12Filled,
@@ -16,8 +16,8 @@ import {
TextAlignLeft16Filled, TextAlignLeft16Filled,
TextAlignRight16Filled, TextAlignRight16Filled,
} from '@vicons/fluent' } from '@vicons/fluent'
import { Heart, HeartOutline, Delete24Filled } from '@vicons/ionicons5' import { Heart, HeartOutline } from '@vicons/ionicons5'
import { useDebounceFn, useElementSize, useStorage } from '@vueuse/core' import { useDebounceFn, useElementSize, useStorage, useThrottleFn } from '@vueuse/core'
import { import {
NButton, NButton,
NCard, NCard,
@@ -33,13 +33,12 @@ import {
NInputGroupLabel, NInputGroupLabel,
NInputNumber, NInputNumber,
NModal, NModal,
NPopconfirm,
NRadioButton, NRadioButton,
NRadioGroup, NRadioGroup,
NScrollbar, NScrollbar,
NSelect, NSelect,
NTooltip, NTooltip,
useMessage, useMessage
} from 'naive-ui' } from 'naive-ui'
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
@@ -47,6 +46,7 @@ const message = useMessage()
const accountInfo = useAccount() const accountInfo = useAccount()
const defaultSettings = {} as Setting_QuestionDisplay const defaultSettings = {} as Setting_QuestionDisplay
const useQB = useQuestionBox() const useQB = useQuestionBox()
const rtc = await useWebRTC().Init('master')
const showSettingDrawer = ref(false) const showSettingDrawer = ref(false)
const showGreenBorder = ref(false) const showGreenBorder = ref(false)
@@ -70,6 +70,11 @@ watch([cardSize.width, cardSize.height], () => {
debouncedSize() debouncedSize()
} }
}) })
const scrollInfo = ref<{ clientHeight: number; scrollHeight: number; scrollTop: number }>()
const debouncedScroll = useDebounceFn(() => {
rtc?.send('function.question.sync-scroll', scrollInfo.value)
}, 200)
const setting = computed({ const setting = computed({
get: () => { get: () => {
@@ -126,6 +131,13 @@ async function loadFonts() {
message.error('你的浏览器不支持获取字体列表') message.error('你的浏览器不支持获取字体列表')
} }
} }
function syncScroll(value: { clientHeight: number; scrollHeight: number; scrollTop: number }) {
if (!setting.value.syncScroll) {
return
}
scrollInfo.value = value
debouncedScroll()
}
onMounted(() => { onMounted(() => {
useQB.GetRecieveQAInfo() useQB.GetRecieveQAInfo()
@@ -144,14 +156,8 @@ onMounted(() => {
<NButton @click="$router.push({ name: 'manage-questionBox' })" size="tiny" secondary> 回到控制台 </NButton> <NButton @click="$router.push({ name: 'manage-questionBox' })" size="tiny" secondary> 回到控制台 </NButton>
</template> </template>
<NFlex align="center"> <NFlex align="center">
<NSelect <NSelect v-model:value="useQB.displayTag" placeholder="选择当前话题" filterable clearable
v-model:value="useQB.displayTag" :options="useQB.tags.map((s) => ({ label: s.name, value: s.name }))" style="width: 200px" />
placeholder="选择当前话题"
filterable
clearable
:options="useQB.tags.map((s) => ({ label: s.name, value: s.name }))"
style="width: 200px"
/>
<NButton @click="useQB.GetRecieveQAInfo" type="primary"> 刷新 </NButton> <NButton @click="useQB.GetRecieveQAInfo" type="primary"> 刷新 </NButton>
<NCheckbox v-model:checked="useQB.onlyFavorite"> 只显示收藏 </NCheckbox> <NCheckbox v-model:checked="useQB.onlyFavorite"> 只显示收藏 </NCheckbox>
<NCheckbox v-model:checked="useQB.onlyUnread"> 只显示未读 </NCheckbox> <NCheckbox v-model:checked="useQB.onlyUnread"> 只显示未读 </NCheckbox>
@@ -170,7 +176,7 @@ onMounted(() => {
</NButton> </NButton>
</NFlex> </NFlex>
</template> </template>
<QuestionItem :item="useQB.displayQuestion" /> <QuestionItem :item="useQB.displayQuestion" style="max-height: 200px;overflow-y: auto" />
</NCard> </NCard>
<NDivider style="margin: 10px 0 10px 0" /> <NDivider style="margin: 10px 0 10px 0" />
</template> </template>
@@ -181,18 +187,12 @@ onMounted(() => {
<NFlex> <NFlex>
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NButton <NButton @click="useQB.setCurrentQuestion(item)" size="small"
@click="useQB.setCurrentQuestion(item)"
size="small"
:type="item.id != useQB.displayQuestion?.id ? 'default' : 'primary'" :type="item.id != useQB.displayQuestion?.id ? 'default' : 'primary'"
:secondary="item.id != useQB.displayQuestion?.id" :secondary="item.id != useQB.displayQuestion?.id">
>
<template #icon> <template #icon>
<NIcon <NIcon :component="item.id != useQB.displayQuestion?.id ? ArrowCircleRight12Filled : ArrowCircleLeft12Filled
:component=" " />
item.id != useQB.displayQuestion?.id ? ArrowCircleRight12Filled : ArrowCircleLeft12Filled
"
/>
</template> </template>
</NButton> </NButton>
</template> </template>
@@ -201,15 +201,11 @@ onMounted(() => {
<NButton v-if="!item.isReaded" size="small" @click="useQB.read(item, true)" type="success" secondary> <NButton v-if="!item.isReaded" size="small" @click="useQB.read(item, true)" type="success" secondary>
设为已读 设为已读
</NButton> </NButton>
<NButton v-else size="small" @click="useQB.read(item, false)" type="warning" secondary <NButton v-else size="small" @click="useQB.read(item, false)" type="warning" secondary>重设为未读</NButton>
>重设为未读</NButton
>
<NButton size="small" @click="useQB.favorite(item, !item.isFavorite)"> <NButton size="small" @click="useQB.favorite(item, !item.isFavorite)">
<template #icon> <template #icon>
<NIcon <NIcon :component="item.isFavorite ? Heart : HeartOutline"
:component="item.isFavorite ? Heart : HeartOutline" :color="item.isFavorite ? '#dd484f' : ''" />
:color="item.isFavorite ? '#dd484f' : ''"
/>
</template> </template>
收藏 收藏
</NButton> </NButton>
@@ -218,7 +214,7 @@ onMounted(() => {
</QuestionItems> </QuestionItems>
</NScrollbar> </NScrollbar>
</NFlex> </NFlex>
<NCard style="min-height: 600px"> <NCard style="min-height: 600px; min-width: 50vw;">
<NFlex vertical :size="0" style="height: 100%"> <NFlex vertical :size="0" style="height: 100%">
<NFlex align="center"> <NFlex align="center">
<NButton @click="showSettingDrawer = true" type="primary"> 打开设置 </NButton> <NButton @click="showSettingDrawer = true" type="primary"> 打开设置 </NButton>
@@ -232,6 +228,16 @@ onMounted(() => {
用于使用 OBS 直接捕获浏览器窗口时消除背景 用于使用 OBS 直接捕获浏览器窗口时消除背景
</NTooltip> </NTooltip>
</NCheckbox> </NCheckbox>
<NCheckbox v-model:checked="setting.syncScroll" @update:checked="updateSettings">
同步滚动
<NTooltip>
<template #trigger>
<NIcon :component="Info24Filled" />
</template>
实验性功能, 当前页面组件内容滚动时也会同步到OBS组件, 当组件大小不同时可能会发生无法预料的问题
</NTooltip>
</NCheckbox>
<template v-if="useQB.displayQuestion"> <template v-if="useQB.displayQuestion">
<NDivider vertical /> <NDivider vertical />
<NButton @click="useQB.read(useQB.displayQuestion, true)" type="success"> 将当前问题设为已读 </NButton> <NButton @click="useQB.read(useQB.displayQuestion, true)" type="success"> 将当前问题设为已读 </NButton>
@@ -247,18 +253,15 @@ onMounted(() => {
</NTooltip> </NTooltip>
</NDivider> </NDivider>
<NFlex justify="center" align="center" style="height: 100%"> <NFlex justify="center" align="center" style="height: 100%">
<div <div ref="cardRef" class="resize-box" :style="{
ref="cardRef" border: showGreenBorder ? '24px solid green' : '',
class="resize-box" background: showGreenBorder ? 'green' : '',
:style="{ padding: '10px',
border: showGreenBorder ? '24px solid green' : '', width: savedCardSize.width + 'px',
background: showGreenBorder ? 'green' : '', height: savedCardSize.height + 'px',
padding: '10px', }">
width: savedCardSize.width + 'px', <QuestionDisplayCard :question="useQB.displayQuestion" :setting="setting" :css="customCss"
height: savedCardSize.height + 'px', @scroll="syncScroll" />
}"
>
<QuestionDisplayCard :question="useQB.displayQuestion" :setting="setting" :css="customCss" />
</div> </div>
</NFlex> </NFlex>
</NFlex> </NFlex>
@@ -303,23 +306,13 @@ onMounted(() => {
</NInputGroup> </NInputGroup>
<NInputGroup style="max-width: 300px"> <NInputGroup style="max-width: 300px">
<NInputGroupLabel>字重</NInputGroupLabel> <NInputGroupLabel>字重</NInputGroupLabel>
<NInputNumber <NInputNumber v-model:value="setting.fontWeight" :min="1" :max="10000" step="100"
v-model:value="setting.fontWeight" placeholder="只有部分字体支持" />
:min="1"
:max="10000"
step="100"
placeholder="只有部分字体支持"
/>
<NButton @click="updateSettings" type="info">保存</NButton> <NButton @click="updateSettings" type="info">保存</NButton>
</NInputGroup> </NInputGroup>
<NFlex> <NFlex>
<NSelect <NSelect v-model:value="setting.font" :options="fontsOptions" filterable @update:value="updateSettings"
v-model:value="setting.font" placeholder="选择内容字体" />
:options="fontsOptions"
filterable
@update:value="updateSettings"
placeholder="选择内容字体"
/>
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NButton @click="loadFonts" type="info" secondary> 获取字体列表 </NButton> <NButton @click="loadFonts" type="info" secondary> 获取字体列表 </NButton>
@@ -330,51 +323,27 @@ onMounted(() => {
<NFlex justify="space-around" style="width: 100%"> <NFlex justify="space-around" style="width: 100%">
<NFlex style="min-width: 80px"> <NFlex style="min-width: 80px">
字体颜色 字体颜色
<NColorPicker <NColorPicker :value="setting.fontColor ? '#' + setting.fontColor : undefined" show-preview
:value="setting.fontColor ? '#' + setting.fontColor : undefined" :modes="['hex']" :actions="['clear', 'confirm']" :show-alpha="false" @update:value="(c: string | null | undefined) => {
show-preview setting.fontColor = c?.replace('#', '')
:modes="['hex']" }
:actions="['clear', 'confirm']" " @confirm="updateSettings" />
:show-alpha="false"
@update:value="
(c: string | null | undefined) => {
setting.fontColor = c?.replace('#', '')
}
"
@confirm="updateSettings"
/>
</NFlex> </NFlex>
<NFlex style="min-width: 80px"> <NFlex style="min-width: 80px">
背景颜色 背景颜色
<NColorPicker <NColorPicker :value="setting.backgroundColor ? '#' + setting.backgroundColor : undefined" show-preview
:value="setting.backgroundColor ? '#' + setting.backgroundColor : undefined" :modes="['hex']" :actions="['clear', 'confirm']" :show-alpha="false" @update:value="(c: string | null | undefined) => {
show-preview setting.backgroundColor = c?.replace('#', '')
:modes="['hex']" }
:actions="['clear', 'confirm']" " @confirm="updateSettings" />
:show-alpha="false"
@update:value="
(c: string | null | undefined) => {
setting.backgroundColor = c?.replace('#', '')
}
"
@confirm="updateSettings"
/>
</NFlex> </NFlex>
<NFlex style="min-width: 80px"> <NFlex style="min-width: 80px">
边框颜色 边框颜色
<NColorPicker <NColorPicker :value="setting.borderColor ? '#' + setting.borderColor : undefined" show-preview
:value="setting.borderColor ? '#' + setting.borderColor : undefined" :modes="['hex']" :actions="['clear', 'confirm']" :show-alpha="false" @update:value="(c: string | null | undefined) => {
show-preview setting.borderColor = c?.replace('#', '')
:modes="['hex']" }
:actions="['clear', 'confirm']" " @confirm="updateSettings" />
:show-alpha="false"
@update:value="
(c: string | null | undefined) => {
setting.borderColor = c?.replace('#', '')
}
"
@confirm="updateSettings"
/>
</NFlex> </NFlex>
</NFlex> </NFlex>
</NFlex> </NFlex>
@@ -388,23 +357,13 @@ onMounted(() => {
</NInputGroup> </NInputGroup>
<NInputGroup style="max-width: 300px"> <NInputGroup style="max-width: 300px">
<NInputGroupLabel>字重</NInputGroupLabel> <NInputGroupLabel>字重</NInputGroupLabel>
<NInputNumber <NInputNumber v-model:value="setting.nameFontWeight" :min="1" :max="10000" step="100"
v-model:value="setting.nameFontWeight" placeholder="只有部分字体支持" />
:min="1"
:max="10000"
step="100"
placeholder="只有部分字体支持"
/>
<NButton @click="updateSettings" type="info">保存</NButton> <NButton @click="updateSettings" type="info">保存</NButton>
</NInputGroup> </NInputGroup>
<NFlex> <NFlex>
<NSelect <NSelect v-model:value="setting.nameFont" :options="fontsOptions" filterable
v-model:value="setting.nameFont" @update:value="updateSettings" placeholder="选择用户名字体" />
:options="fontsOptions"
filterable
@update:value="updateSettings"
placeholder="选择用户名字体"
/>
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NButton @click="loadFonts" type="info" secondary> 获取字体列表 </NButton> <NButton @click="loadFonts" type="info" secondary> 获取字体列表 </NButton>
@@ -414,19 +373,11 @@ onMounted(() => {
</NFlex> </NFlex>
<NFlex style="min-width: 80px"> <NFlex style="min-width: 80px">
字体颜色 字体颜色
<NColorPicker <NColorPicker :value="setting.nameFontColor ? '#' + setting.nameFontColor : undefined" show-preview
:value="setting.nameFontColor ? '#' + setting.nameFontColor : undefined" :modes="['hex']" :actions="['clear', 'confirm']" :show-alpha="false" @update:value="(c: string | null | undefined) => {
show-preview setting.nameFontColor = c?.replace('#', '')
:modes="['hex']" }
:actions="['clear', 'confirm']" " @confirm="updateSettings" />
:show-alpha="false"
@update:value="
(c: string | null | undefined) => {
setting.nameFontColor = c?.replace('#', '')
}
"
@confirm="updateSettings"
/>
</NFlex> </NFlex>
</NFlex> </NFlex>
</NCard> </NCard>
@@ -443,24 +394,16 @@ onMounted(() => {
</NFlex> </NFlex>
</NDrawerContent> </NDrawerContent>
</NDrawer> </NDrawer>
<NModal <NModal preset="card" v-model:show="showOBSModal" closable style="max-width: 90vw; width: auto" title="OBS组件"
preset="card" content-style="display: flex; align-items: center; justify-content: center; flex-direction: column">
v-model:show="showOBSModal" <div :style="{
closable width: savedCardSize.width + 'px',
style="max-width: 90vw; width: auto" height: savedCardSize.height + 'px',
title="OBS组件" }">
content-style="display: flex; align-items: center; justify-content: center; flex-direction: column"
>
<div
:style="{
width: savedCardSize.width + 'px',
height: savedCardSize.height + 'px',
}"
>
<QuestionDisplayCard :question="useQB.displayQuestion" :setting="setting" /> <QuestionDisplayCard :question="useQB.displayQuestion" :setting="setting" />
</div> </div>
<NDivider /> <NDivider />
<NInput readonly :value="'https://vtsuru.live/obs/question-display?token=' + accountInfo?.token" /> <NInput readonly :value="`${CURRENT_HOST}obs/question-display?token=` + accountInfo?.token" />
</NModal> </NModal>
</template> </template>
@@ -476,6 +419,7 @@ onMounted(() => {
overflow-y: hidden; overflow-y: hidden;
padding: 10px; padding: 10px;
} }
.n-drawer-mask { .n-drawer-mask {
background-color: rgba(0, 0, 0, 0); background-color: rgba(0, 0, 0, 0);
} }

View File

@@ -193,7 +193,7 @@ onUnmounted(() => {
<NInput <NInput
:disabled="isSelf" :disabled="isSelf"
show-count show-count
maxlength="1000" maxlength="5000"
type="textarea" type="textarea"
:count-graphemes="countGraphemes" :count-graphemes="countGraphemes"
v-model:value="questionMessage" v-model:value="questionMessage"

View File

@@ -72,7 +72,8 @@ export const Config: TemplateConfig<ConfigType> = {
<NDivider /> <NDivider />
<template v-if="userInfo?.biliId"> <template v-if="userInfo?.biliId">
<template v-if="userInfo?.id == accountInfo?.id"> <template v-if="userInfo?.id == accountInfo?.id">
<NButton type="primary" @click="$router.push({ name: 'manage-index', query: { tab: 'index' } })"> <NButton type="primary"
@click="$router.push({ name: 'manage-index', query: { tab: 'setting', setting: 'index' } })">
自定义个人主页 自定义个人主页
</NButton> </NButton>
<NDivider /> <NDivider />
@@ -85,17 +86,10 @@ export const Config: TemplateConfig<ConfigType> = {
</template> </template>
<NSpace justify="center" align="center" vertical> <NSpace justify="center" align="center" vertical>
<NAvatar <NAvatar v-if="biliInfo" :src="biliInfo?.face" :size="width > 750 ? 175 : 100" round bordered :img-props="{
v-if="biliInfo" referrerpolicy: 'no-referrer',
:src="biliInfo?.face" }"
:size="width > 750 ? 175 : 100" :style="{ boxShadow: isDarkMode ? 'rgb(195 192 192 / 35%) 0px 5px 20px' : '0 5px 15px rgba(0, 0, 0, 0.2)' }" />
round
bordered
:img-props="{
referrerpolicy: 'no-referrer',
}"
:style="{ boxShadow: isDarkMode ? 'rgb(195 192 192 / 35%) 0px 5px 20px' : '0 5px 15px rgba(0, 0, 0, 0.2)' }"
/>
<NSpace align="baseline" justify="center"> <NSpace align="baseline" justify="center">
<NText strong style="font-size: 32px"> {{ biliInfo?.name }} </NText> <NText strong style="font-size: 32px"> {{ biliInfo?.name }} </NText>
<NText strong style="font-size: 20px" depth="3"> ({{ userInfo?.name }}) </NText> <NText strong style="font-size: 20px" depth="3"> ({{ userInfo?.name }}) </NText>
@@ -116,15 +110,8 @@ export const Config: TemplateConfig<ConfigType> = {
<temlate v-if="Object.keys(indexInfo.links || {}).length > 0"> <temlate v-if="Object.keys(indexInfo.links || {}).length > 0">
<NFlex align="center"> <NFlex align="center">
<NDivider vertical /> <NDivider vertical />
<NButton <NButton type="info" secondary tag="a" :href="link[1]" target="_blank"
type="info" v-for="link in Object.entries(indexInfo.links || {})" :key="link[0] + link[1]">
secondary
tag="a"
:href="link[1]"
target="_blank"
v-for="link in Object.entries(indexInfo.links || {})"
:key="link[0] + link[1]"
>
{{ link[0] }} {{ link[0] }}
</NButton> </NButton>
</NFlex> </NFlex>

View File

@@ -4,17 +4,17 @@ import { SongListConfigType, SongListConfigTypeWithConfig } from '@/data/Templat
import { TemplateConfig } from '@/data/VTsuruTypes'; import { TemplateConfig } from '@/data/VTsuruTypes';
import { FILE_BASE_URL } from '@/data/constants'; import { FILE_BASE_URL } from '@/data/constants';
const props = defineProps<SongListConfigTypeWithConfig<ConfigType>>() const props = defineProps<SongListConfigTypeWithConfig<TraditionalConfigType>>()
defineExpose({ Config, DefaultConfig }) defineExpose({ Config, DefaultConfig })
</script> </script>
<script lang="ts"> <script lang="ts">
export type ConfigType = { export type TraditionalConfigType = {
background: string[], background: string[],
notice: string, notice: string,
} }
export const DefaultConfig = {} as ConfigType export const DefaultConfig = {} as TraditionalConfigType
export const Config: TemplateConfig<ConfigType> = { export const Config: TemplateConfig<TraditionalConfigType> = {
name: 'Template.SongList.Traditional', name: 'Template.SongList.Traditional',
items: [ items: [
{ {

View File

@@ -6,6 +6,7 @@ import { defineConfig } from 'vite'
import svgLoader from 'vite-svg-loader' import svgLoader from 'vite-svg-loader'
import Markdown from 'unplugin-vue-markdown/vite' import Markdown from 'unplugin-vue-markdown/vite'
import caddyTls from './plugins/vite-plugin-caddy' import caddyTls from './plugins/vite-plugin-caddy'
import ViteMonacoPlugin from 'vite-plugin-monaco-editor'
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
@@ -26,7 +27,8 @@ export default defineConfig({
Markdown({ Markdown({
/* options */ /* options */
}), }),
caddyTls() caddyTls(),
ViteMonacoPlugin({ languageWorkers: ['css'] })
], ],
resolve: { resolve: {
alias: { alias: {
@@ -39,5 +41,5 @@ export default defineConfig({
}, },
optimizeDeps: { optimizeDeps: {
include: ['@vicons/fluent', '@vicons/ionicons5', 'vue', 'vue-router'] include: ['@vicons/fluent', '@vicons/ionicons5', 'vue', 'vue-router']
}, }
}) })