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

View File

@@ -229,6 +229,7 @@ export interface Setting_QuestionDisplay {
borderColor?: string
borderWidth?: number
syncScroll: boolean
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">
import { VideoCollectTable } from '@/api/api-models'
import { CURRENT_HOST } from '@/data/constants';
import router from '@/router'
import { Clock24Regular, NumberRow24Regular } from '@vicons/fluent'
import {
@@ -28,7 +29,7 @@ const renderCountdown: CountdownProps['render'] = (info: { hours: number; minute
function onClick() {
if (props.canClick == true) {
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 {
router.push({ name: 'manage-videoCollect-Detail', params: { id: props.item.id } })
}

View File

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

View File

@@ -28,6 +28,8 @@ export const BASE_HUB_URL = {
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 ACCOUNT_API_URL = { toString: () => `${BASE_API_URL}account/` }
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 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')
.then((version) => {
if (version.code == 200) {
@@ -99,6 +92,7 @@ QueryGetAPI<string>(BASE_API_URL + 'vtsuru/version')
})
.finally(async () => {
//加载其他数据
InitTTS()
await GetSelfAccount()
const account = useAccount()
const useAuth = useAuthStore()
@@ -110,8 +104,16 @@ QueryGetAPI<string>(BASE_API_URL + 'vtsuru/version')
useAuth.getAuthInfo()
GetNotifactions()
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() {
try {
const result = EasySpeech.detect()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import {
} from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
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 { Clock24Filled, Person24Filled } from '@vicons/fluent'
import { useWindowSize } from '@vueuse/core'
@@ -395,10 +395,8 @@ onActivated(async () => {
<NButton type="warning" size="small" @click="closeTable">
{{ videoDetail.table.isFinish ? '开启表' : '关闭表' }}
</NButton>
<NButton
size="small"
@click="$router.push({ name: 'video-collect-list', params: { id: videoDetail.table.id } })"
>
<NButton size="small"
@click="$router.push({ name: 'video-collect-list', params: { id: videoDetail.table.id } })">
结果表
</NButton>
<NPopconfirm :on-positive-click="deleteTable">
@@ -447,14 +445,9 @@ onActivated(async () => {
</NTabs>
</template>
<NModal v-model:show="shareModalVisiable" title="分享" preset="card" style="width: 600px; max-width: 90vw">
<Qrcode
:value="'https://vtsuru.live/video-collect/' + videoDetail.table.shortId"
level="Q"
:size="100"
background="#fff"
:margin="1"
/>
<NInput :value="'https://vtsuru.live/video-collect/' + videoDetail.table.shortId" />
<Qrcode :value="`${CURRENT_HOST}video-collect/` + videoDetail.table.shortId" level="Q" :size="100" background="#fff"
:margin="1" />
<NInput :value="`${CURRENT_HOST}video-collect/` + videoDetail.table.shortId" />
<NDivider />
<NSpace justify="center">
<NButton type="primary" @click="saveQRCode"> 保存二维码 </NButton>
@@ -469,20 +462,12 @@ onActivated(async () => {
<NInput v-model:value="updateModel.description" placeholder="可以是备注之类的" maxlength="300" show-count />
</NFormItem>
<NFormItem label="视频数量" path="maxVideoCount">
<NInputNumber
v-model:value="updateModel.maxVideoCount"
placeholder="最大数量"
type="number"
style="max-width: 150px"
/>
<NInputNumber v-model:value="updateModel.maxVideoCount" placeholder="最大数量" type="number"
style="max-width: 150px" />
</NFormItem>
<NFormItem label="结束时间" path="endAt">
<NDatePicker
v-model:value="updateModel.endAt"
type="datetime"
placeholder="结束征集的时间"
:isDateDisabled="dateDisabled"
/>
<NDatePicker v-model:value="updateModel.endAt" type="datetime" placeholder="结束征集的时间"
:isDateDisabled="dateDisabled" />
<NDivider vertical />
<NText depth="3"> 最低为一小时 </NText>
</NFormItem>

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ import {
import { QueryGetAPI, QueryPostAPI, QueryPostAPIWithParams } from '@/api/query'
import SongPlayer from '@/components/SongPlayer.vue'
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 {
Checkmark12Regular,
@@ -793,15 +793,11 @@ onUnmounted(() => {
</script>
<template>
<NAlert
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? 'success' : 'warning'"
v-if="accountInfo.id"
>
<NAlert :type="accountInfo.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? 'success' : 'warning'"
v-if="accountInfo.id">
启用弹幕点播功能
<NSwitch
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.SongRequest)"
@update:value="onUpdateFunctionEnable"
/>
<NSwitch :value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.SongRequest)"
@update:value="onUpdateFunctionEnable" />
<br />
<NText depth="3">
@@ -812,11 +808,7 @@ onUnmounted(() => {
则其需要保持此页面开启才能点播, 也不要同时开多个页面, 会导致点播重复 !(部署了则不影响)
</NText>
</NAlert>
<NAlert
type="warning"
v-else
title="你尚未注册并登录 VTsuru.live, 大部分规则设置将不可用 (因为我懒得在前段重写一遍逻辑"
>
<NAlert type="warning" v-else title="你尚未注册并登录 VTsuru.live, 大部分规则设置将不可用 (因为我懒得在前段重写一遍逻辑">
<NButton tag="a" href="/manage" target="_blank" type="primary"> 前往登录或注册 </NButton>
</NAlert>
<br />
@@ -832,11 +824,8 @@ onUnmounted(() => {
</NCard>
<br />
<NCard>
<NTabs
v-if="!accountInfo || accountInfo.settings.enableFunctions.includes(FunctionTypes.SongRequest)"
animated
display-directive="show:lazy"
>
<NTabs v-if="!accountInfo || accountInfo.settings.enableFunctions.includes(FunctionTypes.SongRequest)" animated
display-directive="show:lazy">
<NTabPane name="list" tab="列表">
<NCard size="small">
<NSpace align="center">
@@ -861,12 +850,8 @@ onUnmounted(() => {
<NInput placeholder="手动添加" v-model:value="newSongName" />
<NButton type="primary" @click="addSongManual"> 添加 </NButton>
</NInputGroup>
<NRadioGroup
v-model:value="settings.sortType"
:disabled="!configCanEdit"
@update:value="updateSettings"
type="button"
>
<NRadioGroup v-model:value="settings.sortType" :disabled="!configCanEdit" @update:value="updateSettings"
type="button">
<NRadioButton :value="QueueSortType.TimeFirst"> 加入时间优先 </NRadioButton>
<NRadioButton :value="QueueSortType.PaymentFist"> 付费价格优先 </NRadioButton>
<NRadioButton :value="QueueSortType.GuardFirst"> 舰长优先 (按等级) </NRadioButton>
@@ -893,17 +878,13 @@ onUnmounted(() => {
</Transition>
<NList v-if="activeSongs.length > 0" :show-divider="false" hoverable>
<NListItem v-for="song in activeSongs" :key="song.id" style="padding: 5px">
<NCard
embedded
size="small"
content-style="padding: 5px;"
:style="`${song.status == SongRequestStatus.Singing ? 'animation: animated-border 2.5s infinite;' : ''};height: 100%;`"
>
<NCard embedded 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 align="center">
<div
:style="`border-radius: 4px; background-color: ${song.status == SongRequestStatus.Singing ? '#75c37f' : '#577fb8'}; width: 10px; height: 20px`"
></div>
:style="`border-radius: 4px; background-color: ${song.status == SongRequestStatus.Singing ? '#75c37f' : '#577fb8'}; width: 10px; height: 20px`">
</div>
<NText strong style="font-size: 18px">
{{ song.songName }}
</NText>
@@ -922,12 +903,10 @@ onUnmounted(() => {
{{ song.user?.uid }}
</NTooltip>
</template>
<NSpace
v-if="
<NSpace v-if="
(song.from == SongRequestFrom.Danmaku || song.from == SongRequestFrom.SC) &&
song.user?.fans_medal_wearing_status
"
>
">
<NTag size="tiny" round>
<NTag size="tiny" round :bordered="false">
<NText depth="3">
@@ -939,26 +918,16 @@ onUnmounted(() => {
</span>
</NTag>
</NSpace>
<NTag
v-if="(song.user?.guard_level ?? 0) > 0"
size="small"
:bordered="false"
:color="{ textColor: 'white', color: GetGuardColor(song.user?.guard_level) }"
>
<NTag v-if="(song.user?.guard_level ?? 0) > 0" size="small" :bordered="false"
:color="{ textColor: 'white', color: GetGuardColor(song.user?.guard_level) }">
{{ song.user?.guard_level == 1 ? '总督' : song.user?.guard_level == 2 ? '提督' : '舰长' }}
</NTag>
<NTag
v-if="song.from == SongRequestFrom.SC"
size="small"
:color="{ textColor: 'white', color: GetSCColor(song.price ?? 0) }"
>
<NTag v-if="song.from == SongRequestFrom.SC" size="small"
:color="{ textColor: 'white', color: GetSCColor(song.price ?? 0) }">
SC | {{ song.price }}
</NTag>
<NTag
v-if="song.from == SongRequestFrom.Gift"
size="small"
:color="{ textColor: 'white', color: GetSCColor(song.price ?? 0) }"
>
<NTag v-if="song.from == SongRequestFrom.Gift" size="small"
:color="{ textColor: 'white', color: GetSCColor(song.price ?? 0) }">
Gift | {{ song.price }}
</NTag>
<NTooltip>
@@ -973,13 +942,8 @@ onUnmounted(() => {
<NSpace justify="end" align="center">
<NTooltip v-if="song.song">
<template #trigger>
<NButton
circle
type="success"
style="height: 30px; width: 30px"
:loading="isLrcLoading == song?.song?.key"
@click="selectedSong = song.song"
>
<NButton circle type="success" style="height: 30px; width: 30px"
:loading="isLrcLoading == song?.song?.key" @click="selectedSong = song.song">
<template #icon>
<NIcon :component="Play24Filled" />
</template>
@@ -989,14 +953,8 @@ onUnmounted(() => {
</NTooltip>
<NTooltip>
<template #trigger>
<NButton
circle
type="primary"
style="height: 30px; width: 30px"
:disabled="
songs.findIndex((s) => s.id != song.id && s.status == SongRequestStatus.Singing) > -1
"
@click="
<NButton circle type="primary" style="height: 30px; width: 30px" :disabled="songs.findIndex((s) => s.id != song.id && s.status == SongRequestStatus.Singing) > -1
" @click="
updateSongStatus(
song,
song.status == SongRequestStatus.Singing
@@ -1005,9 +963,7 @@ onUnmounted(() => {
)
"
:style="`animation: ${song.status == SongRequestStatus.Waiting ? '' : 'loading 5s linear infinite'}`"
:secondary="song.status == SongRequestStatus.Singing"
:loading="isLoading"
>
:secondary="song.status == SongRequestStatus.Singing" :loading="isLoading">
<template #icon>
<NIcon :component="Mic24Filled" />
</template>
@@ -1023,13 +979,8 @@ onUnmounted(() => {
</NTooltip>
<NTooltip>
<template #trigger>
<NButton
circle
type="success"
style="height: 30px; width: 30px"
:loading="isLoading"
@click="updateSongStatus(song, SongRequestStatus.Finish)"
>
<NButton circle type="success" style="height: 30px; width: 30px" :loading="isLoading"
@click="updateSongStatus(song, SongRequestStatus.Finish)">
<template #icon>
<NIcon :component="Checkmark12Regular" />
</template>
@@ -1054,13 +1005,8 @@ onUnmounted(() => {
</NTooltip>
<NTooltip>
<template #trigger>
<NButton
circle
type="error"
style="height: 30px; width: 30px"
:loading="isLoading"
@click="updateSongStatus(song, SongRequestStatus.Cancel)"
>
<NButton circle type="error" style="height: 30px; width: 30px" :loading="isLoading"
@click="updateSongStatus(song, SongRequestStatus.Cancel)">
<template #icon>
<NIcon :component="Dismiss16Filled" />
</template>
@@ -1096,20 +1042,14 @@ onUnmounted(() => {
</NInputGroup>
</NSpace>
</NCard>
<NDataTable
size="small"
ref="table"
:columns="columns"
:data="songs"
:pagination="{
<NDataTable size="small" ref="table" :columns="columns" :data="songs" :pagination="{
itemCount: songs.length,
pageSizes: [20, 50, 100],
showSizePicker: true,
prefix({ itemCount }) {
return `共 ${itemCount} 条记录`
},
}"
/>
}" />
</NTabPane>
<NTabPane name="setting" tab="设置">
<NSpin :show="isLoading">
@@ -1132,26 +1072,17 @@ onUnmounted(() => {
<NButton @click="updateSettings" type="info" :disabled="!configCanEdit">确定</NButton>
</NInputGroup>
<NSpace align="center">
<NCheckbox
v-model:checked="settings.enableOnStreaming"
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
<NCheckbox v-model:checked="settings.enableOnStreaming" @update:checked="updateSettings"
:disabled="!configCanEdit">
仅在直播时才允许加入
</NCheckbox>
<NCheckbox
v-model:checked="settings.allowAllDanmaku"
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
<NCheckbox v-model:checked="settings.allowAllDanmaku" @update:checked="updateSettings"
:disabled="!configCanEdit">
允许所有弹幕点播
</NCheckbox>
<template v-if="!settings.allowAllDanmaku">
<NCheckbox
v-model:checked="settings.needWearFanMedal"
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
<NCheckbox v-model:checked="settings.needWearFanMedal" @update:checked="updateSettings"
:disabled="!configCanEdit">
需要拥有粉丝牌
</NCheckbox>
<NInputGroup v-if="settings.needWearFanMedal" style="width: 250px">
@@ -1159,28 +1090,16 @@ onUnmounted(() => {
<NInputNumber v-model:value="settings.fanMedalMinLevel" :disabled="!configCanEdit" />
<NButton @click="updateSettings" type="info" :disabled="!configCanEdit">确定</NButton>
</NInputGroup>
<NCheckbox
v-if="!settings.allowAllDanmaku"
v-model:checked="settings.needJianzhang"
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
<NCheckbox v-if="!settings.allowAllDanmaku" v-model:checked="settings.needJianzhang"
@update:checked="updateSettings" :disabled="!configCanEdit">
只允许舰长
</NCheckbox>
<NCheckbox
v-if="!settings.allowAllDanmaku"
v-model:checked="settings.needTidu"
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
<NCheckbox v-if="!settings.allowAllDanmaku" v-model:checked="settings.needTidu"
@update:checked="updateSettings" :disabled="!configCanEdit">
只允许提督
</NCheckbox>
<NCheckbox
v-if="!settings.allowAllDanmaku"
v-model:checked="settings.needZongdu"
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
<NCheckbox v-if="!settings.allowAllDanmaku" v-model:checked="settings.needZongdu"
@update:checked="updateSettings" :disabled="!configCanEdit">
只允许总督
</NCheckbox>
</template>
@@ -1190,11 +1109,8 @@ onUnmounted(() => {
允许通过 SuperChat 点播
</NCheckbox>
<span v-if="settings.allowSC">
<NCheckbox
v-model:checked="settings.allowSC"
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
<NCheckbox v-model:checked="settings.allowSC" @update:checked="updateSettings"
:disabled="!configCanEdit">
SC 点播无视限制
</NCheckbox>
<NTooltip>
@@ -1273,29 +1189,20 @@ onUnmounted(() => {
</NSpace> -->
<NDivider> 点歌 </NDivider>
<NSpace>
<NCheckbox
v-model:checked="settings.onlyAllowSongList"
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
<NCheckbox v-model:checked="settings.onlyAllowSongList" @update:checked="updateSettings"
:disabled="!configCanEdit">
仅允许点
<NButton text tag="a" href="/manage/song-list" target="_blank" type="info"> 歌单 </NButton>
内的歌曲
</NCheckbox>
<NCheckbox
v-model:checked="settings.allowFromWeb"
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
<NCheckbox v-model:checked="settings.allowFromWeb" @update:checked="updateSettings"
:disabled="!configCanEdit">
允许通过网页点歌
</NCheckbox>
</NSpace>
<NDivider> 冷却 (单位: 秒) </NDivider>
<NCheckbox
v-model:checked="settings.enableCooldown"
@update:checked="updateSettings"
:disabled="!configCanEdit"
>
<NCheckbox v-model:checked="settings.enableCooldown" @update:checked="updateSettings"
:disabled="!configCanEdit">
启用点播冷却
</NCheckbox>
<NSpace v-if="settings.enableCooldown">
@@ -1329,25 +1236,16 @@ onUnmounted(() => {
<NButton @click="updateSettings" type="primary">确定</NButton>
</template>
</NInputGroup>
<NCheckbox
v-model:checked="settings.showRequireInfo"
:disabled="!configCanEdit"
@update:checked="updateSettings"
>
<NCheckbox v-model:checked="settings.showRequireInfo" :disabled="!configCanEdit"
@update:checked="updateSettings">
显示底部的需求信息
</NCheckbox>
<NCheckbox
v-model:checked="settings.showUserName"
:disabled="!configCanEdit"
@update:checked="updateSettings"
>
<NCheckbox v-model:checked="settings.showUserName" :disabled="!configCanEdit"
@update:checked="updateSettings">
显示点播用户名
</NCheckbox>
<NCheckbox
v-model:checked="settings.showFanMadelInfo"
:disabled="!configCanEdit"
@update:checked="updateSettings"
>
<NCheckbox v-model:checked="settings.showFanMadelInfo" :disabled="!configCanEdit"
@update:checked="updateSettings">
显示点播用户粉丝牌
</NCheckbox>
</NSpace>
@@ -1368,7 +1266,7 @@ onUnmounted(() => {
<LiveRequestOBS :id="accountInfo?.id" />
</div>
<br />
<NInput :value="'https://vtsuru.live/obs/live-request?id=' + accountInfo?.id" />
<NInput :value="`${CURRENT_HOST}obs/live-request?id=` + accountInfo?.id" />
<NDivider />
<NCollapse>
<NCollapseItem title="使用说明">
@@ -1385,15 +1283,18 @@ onUnmounted(() => {
<style>
@keyframes loading {
/*以百分比来规定改变发生的时间 也可以通过"from"和"to",等价于0% 和 100%*/
0% {
/*rotate(2D旋转) scale(放大或者缩小) translate(移动) skew(翻转)*/
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes animated-border {
0% {
box-shadow: 0 0 0px #589580;
@@ -1403,6 +1304,7 @@ onUnmounted(() => {
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0);
}
}
@keyframes animated-border-round {
0% {
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 { QueryGetAPI, QueryPostAPI } from '@/api/query'
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 { MusicRequestSettings, useMusicRequestProvider } from '@/store/useMusicRequest'
import { useStorage } from '@vueuse/core'
@@ -387,14 +387,9 @@ onUnmounted(() => {
</NSpace>
<NDivider />
<NSpace align="center">
<NButton
@click="listening ? stopListen() : startListen()"
:type="listening ? 'error' : 'primary'"
:style="{ animation: listening ? 'animated-border 2.5s infinite' : '' }"
data-umami-event="Use Music Request"
:data-umami-event-uid="accountInfo?.biliId"
size="large"
>
<NButton @click="listening ? stopListen() : startListen()" :type="listening ? 'error' : 'primary'"
:style="{ animation: listening ? 'animated-border 2.5s infinite' : '' }" data-umami-event="Use Music Request"
:data-umami-event-uid="accountInfo?.biliId" size="large">
{{ listening ? '停止监听' : '开始监听' }}
</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>
<NButton
@click="musicRquestStore.waitingMusics.splice(musicRquestStore.waitingMusics.indexOf(item), 1)"
type="error"
secondary
size="small"
>
<NButton @click="musicRquestStore.waitingMusics.splice(musicRquestStore.waitingMusics.indexOf(item), 1)"
type="error" secondary size="small">
取消
</NButton>
<NButton @click="blockMusic(item.music)" type="warning" secondary size="small"> 拉黑 </NButton>
@@ -455,26 +446,18 @@ onUnmounted(() => {
<NInputGroupLabel> 点歌弹幕前缀 </NInputGroupLabel>
<NInput v-model:value="settings.orderPrefix" />
</NInputGroup>
<NCheckbox
:checked="settings.orderCooldown != undefined"
@update:checked="
(checked: boolean) => {
<NCheckbox :checked="settings.orderCooldown != undefined" @update:checked="(checked: boolean) => {
settings.orderCooldown = checked ? 300 : undefined
}
"
>
">
是否启用点歌冷却
</NCheckbox>
<NInputGroup v-if="settings.orderCooldown" style="width: 200px">
<NInputGroupLabel> 冷却时间 () </NInputGroupLabel>
<NInputNumber
v-model:value="settings.orderCooldown"
@update:value="
(value) => {
<NInputNumber v-model:value="settings.orderCooldown" @update:value="(value) => {
if (!value || value <= 0) settings.orderCooldown = undefined
}
"
/>
" />
</NInputGroup>
</NSpace>
<NSpace>
@@ -488,13 +471,9 @@ onUnmounted(() => {
</template>
获取和修改输出设备需要打开麦克风权限
</NTooltip>
<NSelect
v-model:value="settings.deviceId"
:options="deviceList"
:fallback-option="() => ({ label: '未选择', value: '' })"
style="min-width: 200px"
@update:value="musicRquestStore.setSinkId"
/>
<NSelect v-model:value="settings.deviceId" :options="deviceList"
:fallback-option="() => ({ label: '未选择', value: '' })" style="min-width: 200px"
@update:value="musicRquestStore.setSinkId" />
</NSpace>
</NSpace>
</NTabPane>
@@ -532,12 +511,8 @@ onUnmounted(() => {
<NList>
<NListItem v-for="item in settings.blacklist" :key="item">
<NSpace align="center" style="width: 100%">
<NButton
@click="settings.blacklist.splice(settings.blacklist.indexOf(item), 1)"
type="error"
secondary
size="small"
>
<NButton @click="settings.blacklist.splice(settings.blacklist.indexOf(item), 1)" type="error" secondary
size="small">
删除
</NButton>
<NText> {{ item }} </NText>
@@ -548,14 +523,8 @@ onUnmounted(() => {
</NTabs>
<NDivider style="height: 100px" />
<NModal v-model:show="showNeteaseModal" preset="card" :title="`获取歌单`" style="max-width: 600px">
<NInput
clearable
style="width: 100%"
autosize
:status="neteaseSongListId ? 'success' : 'error'"
v-model:value="neteaseIdInput"
placeholder="直接输入歌单Id或者网页链接"
>
<NInput clearable style="width: 100%" autosize :status="neteaseSongListId ? 'success' : 'error'"
v-model:value="neteaseIdInput" placeholder="直接输入歌单Id或者网页链接">
<template #suffix>
<NTag v-if="neteaseSongListId" type="success" size="small"> 歌单Id: {{ neteaseSongListId }} </NTag>
</template>
@@ -566,13 +535,8 @@ onUnmounted(() => {
</NButton>
<template v-if="neteaseSongsOptions.length > 0">
<NDivider style="margin: 10px" />
<NTransfer
style="height: 500px"
ref="transfer"
v-model:value="selectedNeteaseSongs"
:options="neteaseSongsOptions"
source-filterable
/>
<NTransfer style="height: 500px" ref="transfer" v-model:value="selectedNeteaseSongs"
:options="neteaseSongsOptions" source-filterable />
<NDivider style="margin: 10px" />
<NButton type="primary" @click="addNeteaseSongs" :loading="isLoading">
添加到歌单 | {{ selectedNeteaseSongs.length }} 首
@@ -586,7 +550,7 @@ onUnmounted(() => {
<MusicRequestOBS :id="accountInfo?.id" />
</div>
<br />
<NInput :value="'https://vtsuru.live/obs/music-request?id=' + accountInfo?.id" />
<NInput :value="`${CURRENT_HOST}obs/music-request?id=` + accountInfo?.id" />
<NDivider />
<NCollapse>
<NCollapseItem title="使用说明">
@@ -606,6 +570,7 @@ onUnmounted(() => {
max-height: 300px;
overflow-y: auto;
}
@keyframes animated-border {
0% {
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 { QueryGetAPI, QueryPostAPI } from '@/api/query'
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 { Delete24Filled, Info24Filled } from '@vicons/fluent'
import { useLocalStorage, useStorage } from '@vueuse/core'
@@ -342,12 +342,7 @@ onUnmounted(() => {
</script>
<template>
<NResult
v-if="!code && !accountInfo"
status="403"
title="403"
description="该页面只能从幻星平台访问或者注册用户使用"
/>
<NResult v-if="!code && !accountInfo" status="403" title="403" description="该页面只能从幻星平台访问或者注册用户使用" />
<template v-else>
<NCard>
<template #header>
@@ -391,13 +386,8 @@ onUnmounted(() => {
<NCollapseTransition>
<NInputGroup v-if="lotteryOption.needFanMedal" style="max-width: 200px">
<NInputGroupLabel> 最低粉丝牌等级 </NInputGroupLabel>
<NInputNumber
v-model:value="lotteryOption.fanCardLevel"
min="1"
max="50"
:default-value="1"
:disabled="isLottering || isStartLottery"
/>
<NInputNumber v-model:value="lotteryOption.fanCardLevel" min="1" max="50" :default-value="1"
:disabled="isLottering || isStartLottery" />
</NInputGroup>
</NCollapseTransition>
<template v-if="lotteryOption.type == 'danmaku'">
@@ -405,21 +395,14 @@ onUnmounted(() => {
<template #trigger>
<NInputGroup style="max-width: 250px">
<NInputGroupLabel> 弹幕内容 </NInputGroupLabel>
<NInput
:disabled="isStartLottery"
v-model:value="lotteryOption.danmakuKeyword"
placeholder="留空则任何弹幕都可以"
/>
<NInput :disabled="isStartLottery" v-model:value="lotteryOption.danmakuKeyword"
placeholder="留空则任何弹幕都可以" />
</NInputGroup>
</template>
符合规则的弹幕才会被添加到抽奖队列中
</NTooltip>
<NRadioGroup
v-model:value="lotteryOption.danmakuFilterType"
name="判定类型"
:disabled="isLottering"
size="small"
>
<NRadioGroup v-model:value="lotteryOption.danmakuFilterType" name="判定类型" :disabled="isLottering"
size="small">
<NRadioButton :disabled="isStartLottery" value="all"> 完全一致 </NRadioButton>
<NRadioButton :disabled="isStartLottery" value="contains"> 包含 </NRadioButton>
<NRadioButton :disabled="isStartLottery" value="regex"> 正则 </NRadioButton>
@@ -428,11 +411,8 @@ onUnmounted(() => {
<template v-else-if="lotteryOption.type == 'gift'">
<NInputGroup style="max-width: 250px">
<NInputGroupLabel> 最低价格 </NInputGroupLabel>
<NInputNumber
:disabled="isStartLottery"
v-model:value="lotteryOption.giftMinPrice"
placeholder="留空则不限制"
/>
<NInputNumber :disabled="isStartLottery" v-model:value="lotteryOption.giftMinPrice"
placeholder="留空则不限制" />
</NInputGroup>
<NInputGroup style="max-width: 200px">
<NInputGroupLabel> 礼物名称 </NInputGroupLabel>
@@ -467,12 +447,8 @@ onUnmounted(() => {
</NCard>
<NCard v-if="originUsers" size="small">
<NSpace justify="center" align="center">
<NButton
type="primary"
@click="continueLottery"
:loading="isStartLottery"
:disabled="isStartLottery || isLotteried || !client"
>
<NButton type="primary" @click="continueLottery" :loading="isStartLottery"
:disabled="isStartLottery || isLotteried || !client">
开始
</NButton>
<NButton type="warning" :disabled="!isStartLottery" @click="pause"> 停止 </NButton>
@@ -482,15 +458,9 @@ onUnmounted(() => {
<template v-if="isStartLottery"> 进行抽取前需要先停止 </template>
</NDivider>
<NSpace justify="center">
<NButton
type="primary"
secondary
@click="startLottery"
:loading="isLottering"
:disabled="isStartLottery || isLotteried"
data-umami-event="Open-Live Use Lottery"
:data-umami-event-uid="client?.authInfo?.anchor_info?.uid"
>
<NButton type="primary" secondary @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 type="info" secondary :disabled="isStartLottery || isLottering || !isLotteried" @click="reset">
@@ -503,15 +473,8 @@ onUnmounted(() => {
<NCard size="small" :title="item.name" style="height: 155px" embedded>
<template #header>
<NSpace align="center" vertical :size="5">
<NAvatar
round
lazy
borderd
:size="64"
:src="item.avatar + '@64w_64h'"
:img-props="{ referrerpolicy: 'no-referrer' }"
style="box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2)"
/>
<NAvatar round lazy borderd :size="64" :src="item.avatar + '@64w_64h'"
:img-props="{ referrerpolicy: 'no-referrer' }" style="box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2)" />
<NSpace v-if="item.fans_medal_wearing_status">
<NTag size="tiny" round>
<NTag size="tiny" round :bordered="false">
@@ -526,12 +489,8 @@ onUnmounted(() => {
{{ item.name }}
</NSpace>
<NButton
style="position: absolute; right: 5px; top: 5px; color: #753e3e"
@click="removeUser(item)"
size="small"
circle
>
<NButton style="position: absolute; right: 5px; top: 5px; color: #753e3e" @click="removeUser(item)"
size="small" circle>
<template #icon>
<NIcon :component="Delete24Filled" />
</template>
@@ -572,21 +531,15 @@ onUnmounted(() => {
</NScrollbar>
<NEmpty v-else description="暂无记录" />
</NModal>
<NModal
v-model:show="showOBSModal"
preset="card"
title="OBS 组件"
style="max-width: 90%; width: 800px; max-height: 90vh"
closable
content-style="overflow: auto"
>
<NModal v-model:show="showOBSModal" preset="card" title="OBS 组件"
style="max-width: 90%; width: 800px; max-height: 90vh" closable content-style="overflow: auto">
<NAlert title="这是什么? " type="info"> 将等待队列以及结果显示在OBS中 </NAlert>
<NDivider> 浏览 </NDivider>
<div style="height: 400px; width: 250px; position: relative; margin: 0 auto">
<LiveLotteryOBS :code="code" />
</div>
<br />
<NInput :value="'https://vtsuru.live/obs/live-lottery?code=' + code" />
<NInput :value="`${CURRENT_HOST}obs/live-lottery?code=` + code" />
<NDivider />
<NCollapse>
<NCollapseItem title="使用说明">

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { AddressInfo } from '@/api/api-models'
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 { useStorage } from '@vueuse/core'
import {
@@ -242,16 +242,11 @@ function logout() {
<NListItem v-for="address in biliAuth.address" :key="address.id">
<AddressDisplay :address="address">
<template #actions>
<NButton
size="small"
@click="
() => {
<NButton size="small" @click="() => {
currentAddress = address
showAddressModal = true
}
"
type="info"
>
" type="info">
修改
</NButton>
<NPopconfirm @positive-click="() => deleteAddress(address?.id ?? '')">
@@ -267,7 +262,7 @@ function logout() {
</NFlex>
</NCollapseItem>
<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>
</NCollapse>
</NCard>
@@ -285,72 +280,40 @@ function logout() {
<NListItem v-for="item in useAuth.biliTokens" :key="item.token" @click="switchAuth(item.token)">
<NFlex align="center">
<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>
</NListItem>
</NList>
</NCard>
</NFlex>
</NSpin>
<NModal
v-model:show="showAddressModal"
preset="card"
style="width: 800px; max-width: 90vw; height: auto"
title="添加/更新地址"
>
<NModal v-model:show="showAddressModal" preset="card" style="width: 800px; max-width: 90vw; height: auto"
title="添加/更新地址">
<NSpin v-if="currentAddress" :show="isLoading">
<NForm ref="formRef" :model="currentAddress" :rules="rules">
<NFormItem label="地址" path="area" required>
<NFlex style="width: 100%">
<NSelect
v-model:value="currentAddress.province"
:options="provinceOptions"
@update:value="onAreaSelectChange(0)"
placeholder="请选择省"
style="width: 100px"
filterable
/>
<NSelect
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"
<NSelect v-model:value="currentAddress.province" :options="provinceOptions"
@update:value="onAreaSelectChange(0)" placeholder="请选择省" style="width: 100px" filterable />
<NSelect 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)"
:disabled="!currentAddress?.district"
placeholder="请选择街道"
style="width: 150px"
filterable
/>
:disabled="!currentAddress?.district" placeholder="请选择街道" style="width: 150px" filterable />
</NFlex>
</NFormItem>
<NFormItem label="详细地址" path="address" required>
<NInput v-model:value="currentAddress.address" placeholder="详细地址" type="textarea" />
</NFormItem>
<NFormItem label="联系电话" path="phone" required>
<NInputNumber
v-model:value="currentAddress.phone"
placeholder="联系电话"
:show-button="false"
style="width: 200px"
/>
<NInputNumber v-model:value="currentAddress.phone" placeholder="联系电话" :show-button="false"
style="width: 200px" />
</NFormItem>
<NFormItem label="联系人" path="name" required>
<NInput v-model:value="currentAddress.name" placeholder="联系人" style="max-width: 150px" />
@@ -365,12 +328,10 @@ function logout() {
</NForm>
</NSpin>
</NModal>
<NModal
v-model:show="showAgreementModal"
title="用户协议"
preset="card"
style="width: 800px; max-width: 90vw; height: 90vh"
>
<NScrollbar style="height: 80vh"> <UserAgreement /></NScrollbar>
<NModal v-model:show="showAgreementModal" title="用户协议" preset="card"
style="width: 800px; max-width: 90vw; height: 90vh">
<NScrollbar style="height: 80vh">
<UserAgreement />
</NScrollbar>
</NModal>
</template>

View File

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

View File

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

View File

@@ -72,7 +72,8 @@ export const Config: TemplateConfig<ConfigType> = {
<NDivider />
<template v-if="userInfo?.biliId">
<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>
<NDivider />
@@ -85,17 +86,10 @@ export const Config: TemplateConfig<ConfigType> = {
</template>
<NSpace justify="center" align="center" vertical>
<NAvatar
v-if="biliInfo"
:src="biliInfo?.face"
:size="width > 750 ? 175 : 100"
round
bordered
:img-props="{
<NAvatar v-if="biliInfo" :src="biliInfo?.face" :size="width > 750 ? 175 : 100" 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)' }"
/>
: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">
<NText strong style="font-size: 32px"> {{ biliInfo?.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">
<NFlex align="center">
<NDivider vertical />
<NButton
type="info"
secondary
tag="a"
:href="link[1]"
target="_blank"
v-for="link in Object.entries(indexInfo.links || {})"
:key="link[0] + link[1]"
>
<NButton type="info" secondary tag="a" :href="link[1]" target="_blank"
v-for="link in Object.entries(indexInfo.links || {})" :key="link[0] + link[1]">
{{ link[0] }}
</NButton>
</NFlex>

View File

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

View File

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