refactor: 将 OBS 通知从 notification 改为 message 组件并优化点歌页面布局

- 将 OBS 通知组件从 n-notification 改为 n-message,简化显示逻辑
- 优化通知内容格式,将标题和元信息作为前缀显示
- 调整通知持续时间:成功 4 秒,错误 6 秒
- 重构点歌页面布局,将功能开关和 OBS 组件按钮移至顶部卡片
- 在 MinimalRequestOBS 组件中添加点歌要求信息显示(前缀、允许类型、SC、粉丝牌等)
- 优化点歌队
This commit is contained in:
2025-11-21 23:25:29 +08:00
parent 9691704da5
commit c79656bc77
10 changed files with 1016 additions and 612 deletions

View File

@@ -44,32 +44,30 @@ export const useOBSNotification = defineStore('obs-notification', () => {
} }
function showNotification(payload: ObsNotificationPayload) { function showNotification(payload: ObsNotificationPayload) {
const notification = window.$notification const message = window.$message
if (!notification) { if (!message) {
console.warn('[OBS] notification instance missing') console.warn('[OBS] message instance missing')
return return
} }
console.log('[OBS] 收到通知', payload) console.log('[OBS] 收到通知', payload)
const method = payload.Type === 'success' ? 'success' : 'error' const method = payload.Type === 'success' ? 'success' : 'error'
const title = resolveTitle(payload) const title = resolveTitle(payload)
const description = payload.Message || '未知通知'
const meta = resolveMeta(payload) const meta = resolveMeta(payload)
const prefix = [title, meta].filter(Boolean).join(' · ')
const description = payload.Message || '未知通知'
const finalContent = prefix ? `${prefix}\n${description}` : description
if (typeof notification[method] === 'function') { if (typeof message[method] === 'function') {
notification[method]({ message[method](finalContent, {
title: payload.Type === 'success' ? '成功' : `失败`, duration: method === 'error' ? 6000 : 4000,
description, closable: true,
duration: method === 'error' ? 8000 : 5000,
keepAliveOnHover: true,
}) })
} else { } else {
notification.create({ message.create(finalContent, {
title: payload.Type === 'success' ? '成功' : `失败`,
content: description,
duration: method === 'error' ? 8000 : 5000,
keepAliveOnHover: true,
type: method, type: method,
duration: method === 'error' ? 6000 : 4000,
closable: true,
}) })
} }
} }

View File

@@ -68,6 +68,7 @@ import { copyToClipboard } from '@/Utils'
import PointOrderManage from './PointOrderManage.vue' import PointOrderManage from './PointOrderManage.vue'
import PointSettings from './PointSettings.vue' import PointSettings from './PointSettings.vue'
import PointUserManage from './PointUserManage.vue' import PointUserManage from './PointUserManage.vue'
import PointTestPanel from './PointTestPanel.vue'
const message = useMessage() const message = useMessage()
const accountInfo = useAccount() const accountInfo = useAccount()
@@ -734,6 +735,15 @@ onMounted(() => { })
> >
<PointSettings /> <PointSettings />
</NTabPane> </NTabPane>
<!-- 测试标签页 -->
<NTabPane
name="test"
tab="测试"
display-directive="show:lazy"
>
<PointTestPanel />
</NTabPane>
</NTabs> </NTabs>
<!-- 添加/修改礼物模态框 --> <!-- 添加/修改礼物模态框 -->

View File

@@ -0,0 +1,331 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import {
NCard,
NButton,
NForm,
NFormItem,
NInputNumber,
NInput,
NSelect,
NFlex,
NAlert,
NStatistic,
NPopconfirm,
useMessage,
NSpin,
NTag,
NDivider,
} from 'naive-ui'
import { EventDataTypes } from '@/api/api-models'
import { useAccount } from '@/api/account'
import { QueryPostAPI as Post, QueryGetAPI as Get } from '@/api/query'
import { POINT_API_URL } from '@/data/constants'
const message = useMessage()
const accountInfo = useAccount()
// 测试表单数据
const testForm = ref({
type: EventDataTypes.Message,
giftName: '',
giftPrice: 0,
giftCount: 1,
guardLevel: '舰长',
})
// 测试账户积分
const testAccountPoint = ref<number>(0)
const isLoading = ref(false)
const isTesting = ref(false)
// 事件类型选项
const eventTypeOptions = [
{ label: '弹幕', value: EventDataTypes.Message },
{ label: '礼物', value: EventDataTypes.Gift },
{ label: '上舰', value: EventDataTypes.Guard },
{ label: 'SC', value: EventDataTypes.SC },
]
// 舰长等级选项
const guardLevelOptions = [
{ label: '舰长', value: '舰长' },
{ label: '提督', value: '提督' },
{ label: '总督', value: '总督' },
]
// 是否显示礼物相关字段
const showGiftFields = computed(() => testForm.value.type === EventDataTypes.Gift)
const showGuardFields = computed(() => testForm.value.type === EventDataTypes.Guard)
const showPriceField = computed(
() => testForm.value.type === EventDataTypes.SC || testForm.value.type === EventDataTypes.Gift
)
// 获取测试账户积分
async function fetchTestAccountPoint() {
isLoading.value = true
try {
const res = await Get<number>(POINT_API_URL + 'get-test-account-point')
if (res.code === 200) {
testAccountPoint.value = res.data ?? 0
}
} catch (err) {
console.error('获取测试账户积分失败:', err)
} finally {
isLoading.value = false
}
}
// 执行测试
async function runTest() {
isTesting.value = true
try {
const payload: any = {
type: testForm.value.type,
}
// 根据类型添加参数
if (testForm.value.type === EventDataTypes.Gift) {
if (!testForm.value.giftName.trim()) {
message.error('请输入礼物名称')
return
}
if (testForm.value.giftPrice < 0) {
message.error('礼物价格不能为负数')
return
}
payload.giftName = testForm.value.giftName
payload.giftPrice = testForm.value.giftPrice
payload.giftCount = testForm.value.giftCount
} else if (testForm.value.type === EventDataTypes.Guard) {
payload.guardLevel = testForm.value.guardLevel
} else if (testForm.value.type === EventDataTypes.SC) {
if (testForm.value.giftPrice <= 0) {
message.error('SC价格必须大于0')
return
}
payload.giftPrice = testForm.value.giftPrice
}
const res = await Post<{ success: boolean; message: string; pointsAwarded?: number }>(POINT_API_URL + 'test-point',
payload
)
if (res.code === 200 && res.data) {
if (res.data.success) {
message.success(res.data.message)
// 刷新测试账户积分
await fetchTestAccountPoint()
} else {
message.warning(res.data.message)
}
} else {
message.error(res.message || '测试失败')
}
} catch (err: any) {
message.error(`测试失败: ${err.message || err}`)
console.error('测试失败:', err)
} finally {
isTesting.value = false
}
}
// 重置测试账户
async function resetTestAccount() {
isLoading.value = true
try {
const res = await Post(POINT_API_URL + 'reset-test-account', {})
if (res.code === 200) {
message.success('测试账户已重置')
testAccountPoint.value = 0
} else {
message.error(res.message || '重置失败')
}
} catch (err: any) {
message.error(`重置失败: ${err.message || err}`)
console.error('重置失败:', err)
} finally {
isLoading.value = false
}
}
// 初始化时获取积分
fetchTestAccountPoint()
</script>
<template>
<NCard title="积分测试系统">
<template #header-extra>
<NTag type="info">
测试账户 OUId: 00000000-0000-0000-0000-000000000000
</NTag>
</template>
<NSpin :show="isLoading">
<NFlex
vertical
:gap="16"
>
<NAlert
type="info"
closable
>
此功能用于测试积分系统的配置所有测试事件将记录到一个专用的 mock 账户OUId=0你可以在这里测试不同类型事件的积分获取情况
</NAlert>
<!-- 测试账户积分显示 -->
<NCard
size="small"
:bordered="false"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
>
<NFlex
justify="space-between"
align="center"
>
<div>
<NStatistic
label="测试账户当前积分"
:value="testAccountPoint"
style="color: white"
>
<template #suffix>
<span style="color: white; font-size: 16px"></span>
</template>
</NStatistic>
</div>
<NPopconfirm
@positive-click="resetTestAccount"
>
<template #trigger>
<NButton
type="error"
:loading="isLoading"
secondary
>
重置积分
</NButton>
</template>
确定要重置测试账户的积分吗
</NPopconfirm>
</NFlex>
</NCard>
<NDivider>测试表单</NDivider>
<!-- 测试表单 -->
<NForm
label-placement="left"
label-width="120"
>
<NFormItem
label="事件类型"
required
>
<NSelect
v-model:value="testForm.type"
:options="eventTypeOptions"
placeholder="选择事件类型"
/>
</NFormItem>
<!-- 礼物相关字段 -->
<template v-if="showGiftFields">
<NFormItem
label="礼物名称"
required
>
<NInput
v-model:value="testForm.giftName"
placeholder="例如: 小心心、辣条"
clearable
/>
</NFormItem>
<NFormItem
label="礼物价格"
required
>
<NInputNumber
v-model:value="testForm.giftPrice"
placeholder="礼物价格(元)"
:min="0"
:precision="2"
style="width: 100%"
/>
</NFormItem>
<NFormItem label="礼物数量">
<NInputNumber
v-model:value="testForm.giftCount"
placeholder="礼物数量"
:min="1"
style="width: 100%"
/>
</NFormItem>
</template>
<!-- 上舰相关字段 -->
<NFormItem
v-if="showGuardFields"
label="舰长等级"
required
>
<NSelect
v-model:value="testForm.guardLevel"
:options="guardLevelOptions"
placeholder="选择舰长等级"
/>
</NFormItem>
<!-- SC 价格字段 -->
<NFormItem
v-if="testForm.type === EventDataTypes.SC"
label="SC价格"
required
>
<NInputNumber
v-model:value="testForm.giftPrice"
placeholder="SC价格"
:min="0.01"
:precision="2"
style="width: 100%"
/>
</NFormItem>
<NFormItem>
<NFlex
justify="end"
style="width: 100%"
>
<NButton
type="primary"
:loading="isTesting"
@click="runTest"
>
执行测试
</NButton>
</NFlex>
</NFormItem>
</NForm>
<NAlert
v-if="!accountInfo?.settings?.point"
type="warning"
>
请先配置积分设置
</NAlert>
</NFlex>
</NSpin>
</NCard>
</template>
<style scoped>
:deep(.n-statistic .n-statistic-value__content) {
color: white !important;
}
:deep(.n-statistic .n-statistic__label) {
color: rgba(255, 255, 255, 0.9) !important;
}
</style>

View File

@@ -172,6 +172,39 @@ onUnmounted(() => {
description="暂无人点歌" description="暂无人点歌"
/> />
</div> </div>
<div
v-if="settings.showRequireInfo"
class="minimal-requirements"
>
<div class="minimal-requirements-row">
<div class="minimal-requirements-tag">
<span class="tag-label">前缀</span>
<span class="tag-value">{{ settings.orderPrefix || '-' }}</span>
</div>
<div class="minimal-requirements-tag">
<span class="tag-label">允许</span>
<span class="tag-value">{{ settings.allowAllDanmaku ? '所有弹幕' : allowGuardTypes.length > 0 ? allowGuardTypes.join('/') : '无' }}</span>
</div>
</div>
<div class="minimal-requirements-row">
<div class="minimal-requirements-tag">
<span class="tag-label">SC</span>
<span class="tag-value">{{ settings.allowSC ? `≥ ¥${settings.scMinPrice}` : '不允许' }}</span>
</div>
<div class="minimal-requirements-tag">
<span class="tag-label">粉丝牌</span>
<span class="tag-value">
{{
settings.needWearFanMedal
? settings.fanMedalMinLevel > 0
? `${settings.fanMedalMinLevel}`
: '需佩戴'
: '无需'
}}
</span>
</div>
</div>
</div>
</div> </div>
</template> </template>
@@ -370,4 +403,50 @@ onUnmounted(() => {
.minimal-list-inner.animating:hover { .minimal-list-inner.animating:hover {
animation-play-state: paused; animation-play-state: paused;
} }
.minimal-requirements {
margin-top: 6px;
padding: 4px 4px 0;
display: flex;
flex-direction: column;
gap: 4px;
font-size: 11px;
color: #e2e8f0;
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.85);
}
.minimal-requirements-row {
display: flex;
gap: 8px;
}
.minimal-requirements-tag {
flex: 1;
min-width: 0;
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(2px);
}
.tag-label {
font-size: 10px;
letter-spacing: 0.08em;
color: rgba(226, 232, 240, 0.75);
text-transform: uppercase;
opacity: 0.8;
}
.tag-value {
margin-top: 0;
font-size: 12px;
font-weight: 600;
color: #e2e8f0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style> </style>

View File

@@ -151,18 +151,42 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<NAlert <!-- 顶部功能开关与全局操作 -->
v-if="accountInfo.id" <NCard v-if="accountInfo.id" size="small">
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.LiveRequest) ? 'success' : 'warning'" <template #header>
> <NSpace align="center" justify="space-between">
启用弹幕点播功能 <NSpace align="center">
<NSwitch <NText>启用弹幕点播功能</NText>
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.LiveRequest)" <NSwitch
@update:value="onUpdateFunctionEnable" :value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.LiveRequest)"
/> @update:value="onUpdateFunctionEnable"
/>
</NSpace>
<br> <!-- OBS 组件按钮 -->
<NText depth="3"> <NTooltip>
<template #trigger>
<NButton
type="primary"
size="small"
:disabled="!accountInfo"
@click="showOBSModal = true"
>
OBS 组件
</NButton>
</template>
{{ liveRequest.configCanEdit ? '配置 OBS 样式与参数' : '登陆后才可以使用此功能' }}
</NTooltip>
</NSpace>
</template>
<!-- 提示信息 -->
<NAlert
v-if="accountInfo.settings.enableFunctions.includes(FunctionTypes.LiveRequest)"
type="info"
closable
style="margin-top: 10px"
>
如果没有部署 如果没有部署
<NButton <NButton
text text
@@ -173,13 +197,14 @@ onUnmounted(() => {
> >
VtsuruEventFetcher VtsuruEventFetcher
</NButton> </NButton>
则其需要保持此页面开启才能点播, 也不要同时开多个页面, 会导致点播重复 !(部署了则不影响) 则其需要保持此页面开启才能点播, 也不要同时开多个页面, 会导致点播重复 (部署了则不影响)
</NText> </NAlert>
</NAlert> </NCard>
<NAlert <NAlert
v-else v-else
type="warning" type="warning"
title="你尚未注册并登录 VTsuru.live, 大部分规则设置将不可用 (因为我懒得在前重写一遍逻辑" title="你尚未注册并登录 VTsuru.live, 大部分规则设置将不可用 (因为我懒得在前重写一遍逻辑)"
> >
<NButton <NButton
tag="a" tag="a"
@@ -190,27 +215,12 @@ onUnmounted(() => {
前往登录或注册 前往登录或注册
</NButton> </NButton>
</NAlert> </NAlert>
<br>
<NCard size="small"> <!-- 主体内容 -->
<NSpace align="center"> <NCard style="margin-top: 12px">
<NTooltip>
<template #trigger>
<NButton
type="primary"
:disabled="!accountInfo"
@click="showOBSModal = true"
>
OBS 组件
</NButton>
</template>
{{ liveRequest.configCanEdit ? '' : '登陆后才可以使用此功能' }}
</NTooltip>
</NSpace>
</NCard>
<br>
<NCard>
<NTabs <NTabs
v-if="!accountInfo || accountInfo.settings.enableFunctions.includes(FunctionTypes.LiveRequest)" v-if="!accountInfo || accountInfo.settings.enableFunctions.includes(FunctionTypes.LiveRequest)"
type="line"
animated animated
display-directive="show:lazy" display-directive="show:lazy"
> >
@@ -223,12 +233,13 @@ onUnmounted(() => {
<div <div
v-if="liveRequest.selectedSong" v-if="liveRequest.selectedSong"
class="song-list" class="song-list"
style="margin-bottom: 15px"
> >
<SongPlayer <SongPlayer
v-model:is-lrc-loading="liveRequest.isLrcLoading" v-model:is-lrc-loading="liveRequest.isLrcLoading"
:song="liveRequest.selectedSong" :song="liveRequest.selectedSong"
/> />
<NDivider style="margin: 15px 0 15px 0" /> <NDivider style="margin: 15px 0" />
</div> </div>
</Transition> </Transition>

View File

@@ -385,288 +385,287 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<NSpace> <NCard size="small">
<NAlert type="info"> <template #header>
搜索时会优先选择非VIP歌曲, 所以点到付费曲目时可能会是猴版或者各种奇怪的歌 <NSpace align="center" justify="space-between">
</NAlert>
</NSpace>
<NDivider />
<NSpace align="center">
<NButton
: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"
@click="listening ? stopListen() : startListen()"
>
{{ listening ? '停止监听' : '开始监听' }}
</NButton>
<NButton
type="info"
size="small"
@click="showOBSModal = true"
>
OBS组件
</NButton>
<NButton
size="small"
@click="showNeteaseModal = true"
>
从网易云歌单导入空闲歌单
</NButton>
<NButton
type="primary"
secondary
:disabled="!accountInfo"
size="small"
@click="uploadConfig"
>
保存配置到服务器
</NButton>
<NPopconfirm @positive-click="downloadConfig">
<template #trigger>
<NButton
type="primary"
secondary
:disabled="!accountInfo"
size="small"
>
从服务器获取配置
</NButton>
</template>
这将覆盖当前设置, 确定?
</NPopconfirm>
</NSpace>
<NDivider />
<NCollapse :default-expanded-names="['1']">
<NCollapseItem
title="队列"
name="1"
>
<NEmpty v-if="musicRquestStore.waitingMusics.length == 0">
暂无
</NEmpty>
<NList
v-else
size="small"
bordered
>
<NListItem
v-for="item in musicRquestStore.waitingMusics"
:key="item.music.name"
>
<NSpace align="center">
<NButton
type="primary"
secondary
size="small"
@click="musicRquestStore.playMusic(item.music)"
>
播放
</NButton>
<NButton
type="error"
secondary
size="small"
@click="musicRquestStore.waitingMusics.splice(musicRquestStore.waitingMusics.indexOf(item), 1)"
>
取消
</NButton>
<NButton
type="warning"
secondary
size="small"
@click="blockMusic(item.music)"
>
拉黑
</NButton>
<span>
<NTag
v-if="item.music.from == SongFrom.Netease"
type="success"
size="small"
> 网易</NTag>
<NTag
v-else-if="item.music.from == SongFrom.Kugou"
type="success"
size="small"
> 酷狗</NTag>
</span>
<NText>
{{ item.from.name }}
</NText>
<NText depth="3">
{{ item.music.name }} - {{ item.music.author?.join('/') }}
</NText>
</NSpace>
</NListItem>
</NList>
</NCollapseItem>
</NCollapse>
<NDivider />
<NTabs>
<NTabPane
name="settings"
tab="设置"
>
<NSpace vertical>
<NSpace align="center"> <NSpace align="center">
<NRadioGroup v-model:value="settings.platform"> <NButton
<NRadioButton value="netease"> :type="listening ? 'error' : 'primary'"
网易云 :style="{ animation: listening ? 'animated-border 2.5s infinite' : '' }"
</NRadioButton> data-umami-event="Use Music Request"
<NRadioButton value="kugou"> :data-umami-event-uid="accountInfo?.biliId"
酷狗 size="small"
</NRadioButton> @click="listening ? stopListen() : startListen()"
</NRadioGroup>
<NInputGroup style="width: 250px">
<NInputGroupLabel> 点歌弹幕前缀 </NInputGroupLabel>
<NInput v-model:value="settings.orderPrefix" />
</NInputGroup>
<NCheckbox
:checked="settings.orderCooldown != undefined"
@update:checked="(checked: boolean) => {
settings.orderCooldown = checked ? 300 : undefined
}
"
> >
是否启用点歌冷却 {{ listening ? '停止监听' : '开始监听' }}
</NCheckbox> </NButton>
<NInputGroup <NButton
v-if="settings.orderCooldown" type="info"
style="width: 200px" size="small"
@click="showOBSModal = true"
> >
<NInputGroupLabel> 冷却时间 () </NInputGroupLabel> OBS组件
<NInputNumber </NButton>
v-model:value="settings.orderCooldown"
@update:value="(value) => {
if (!value || value <= 0) settings.orderCooldown = undefined
}
"
/>
</NInputGroup>
</NSpace> </NSpace>
<NSpace>
<NCheckbox v-model:checked="settings.playMusicWhenFree"> <NSpace align="center">
空闲时播放空闲歌单 <NButton
</NCheckbox> type="primary"
<NCheckbox v-model:checked="settings.orderMusicFirst"> secondary
优先播放点歌 :disabled="!accountInfo"
</NCheckbox> size="small"
</NSpace> @click="uploadConfig"
<NSpace> >
<NTooltip> 保存配置到服务器
</NButton>
<NPopconfirm @positive-click="downloadConfig">
<template #trigger> <template #trigger>
<NButton <NButton
type="info" type="primary"
@click="getOutputDevice" secondary
:disabled="!accountInfo"
size="small"
> >
获取输出设备 从服务器获取配置
</NButton> </NButton>
</template> </template>
获取和修改输出设备需要打开麦克风权限 这将覆盖当前设置, 确定?
</NTooltip> </NPopconfirm>
<NSelect
v-model:value="settings.deviceId"
:options="deviceList"
:fallback-option="() => ({ label: '未选择', value: '' })"
style="min-width: 200px"
@update:value="musicRquestStore.setSinkId"
/>
</NSpace> </NSpace>
</NSpace> </NSpace>
</NTabPane> </template>
<NTabPane <NAlert type="info" closable style="margin-top: 10px">
name="list" 搜索时会优先选择非VIP歌曲, 所以点到付费曲目时可能会是猴版或者各种奇怪的歌
tab="闲置歌单" </NAlert>
> </NCard>
<NSpace>
<NPopconfirm @positive-click="clearMusic"> <NCard style="margin-top: 12px">
<template #trigger> <NTabs type="line" animated>
<NButton type="error"> <NTabPane
清空 name="queue"
</NButton> tab="当前点歌"
</template>
确定清空吗?
</NPopconfirm>
<NButton @click="showNeteaseModal = true">
从网易云歌单导入
</NButton>
</NSpace>
<NDivider style="margin: 15px 0 10px 0" />
<NEmpty v-if="musicRquestStore.originMusics.length == 0">
暂无
</NEmpty>
<NVirtualList
v-else
style="max-height: 1000px"
:item-size="30"
:items="originMusics"
item-resizable
> >
<template #default="{ item }"> <NEmpty v-if="musicRquestStore.waitingMusics.length == 0" description="暂无点歌">
<p :style="`min-height: ${30}px;width:97%;display:flex;align-items:center;`"> </NEmpty>
<NList
v-else
size="small"
bordered
>
<NListItem
v-for="item in musicRquestStore.waitingMusics"
:key="item.music.name"
>
<NSpace align="center">
<NButton
type="primary"
secondary
size="small"
@click="musicRquestStore.playMusic(item.music)"
>
播放
</NButton>
<NButton
type="error"
secondary
size="small"
@click="musicRquestStore.waitingMusics.splice(musicRquestStore.waitingMusics.indexOf(item), 1)"
>
取消
</NButton>
<NButton
type="warning"
secondary
size="small"
@click="blockMusic(item.music)"
>
拉黑
</NButton>
<span>
<NTag
v-if="item.music.from == SongFrom.Netease"
type="success"
size="small"
> 网易</NTag>
<NTag
v-else-if="item.music.from == SongFrom.Kugou"
type="success"
size="small"
> 酷狗</NTag>
</span>
<NText>
{{ item.from.name }}
</NText>
<NText depth="3">
{{ item.music.name }} - {{ item.music.author?.join('/') }}
</NText>
</NSpace>
</NListItem>
</NList>
</NTabPane>
<NTabPane
name="list"
tab="闲置歌单"
>
<NSpace style="margin-bottom: 10px">
<NPopconfirm @positive-click="clearMusic">
<template #trigger>
<NButton type="error" size="small">
清空
</NButton>
</template>
确定清空吗?
</NPopconfirm>
<NButton size="small" @click="showNeteaseModal = true">
从网易云歌单导入
</NButton>
</NSpace>
<NEmpty v-if="musicRquestStore.originMusics.length == 0">
暂无
</NEmpty>
<NVirtualList
v-else
style="max-height: 600px"
:item-size="36"
:items="originMusics"
item-resizable
>
<template #default="{ item }">
<div :style="`height: ${36}px; display:flex; align-items:center; padding: 0 5px;`">
<NSpace
align="center"
style="width: 100%"
>
<NPopconfirm @positive-click="delMusic(item)">
<template #trigger>
<NButton
type="error"
secondary
size="tiny"
>
删除
</NButton>
</template>
确定删除?
</NPopconfirm>
<NButton
type="info"
secondary
size="tiny"
@click="musicRquestStore.playMusic(item)"
>
播放
</NButton>
<NText> {{ item.name }} - {{ item.author?.join('/') }} </NText>
</NSpace>
</div>
</template>
</NVirtualList>
</NTabPane>
<NTabPane
name="blacklist"
tab="黑名单"
>
<NList bordered>
<NListItem
v-for="item in settings.blacklist"
:key="item"
>
<NSpace <NSpace
align="center" align="center"
style="width: 100%" style="width: 100%"
> >
<NPopconfirm @positive-click="delMusic(item)">
<template #trigger>
<NButton
type="error"
secondary
size="small"
>
删除
</NButton>
</template>
确定删除?
</NPopconfirm>
<NButton <NButton
type="info" type="error"
secondary secondary
size="small" size="small"
@click="musicRquestStore.playMusic(item)" @click="settings.blacklist.splice(settings.blacklist.indexOf(item), 1)"
> >
播放 删除
</NButton> </NButton>
<NText> {{ item.name }} - {{ item.author?.join('/') }} </NText> <NText> {{ item }} </NText>
</NSpace> </NSpace>
</p> </NListItem>
</template> </NList>
</NVirtualList> </NTabPane>
</NTabPane>
<NTabPane <NTabPane
name="blacklist" name="settings"
tab="黑名单" tab="设置"
> >
<NList> <NSpace vertical>
<NListItem <NSpace align="center">
v-for="item in settings.blacklist" <NRadioGroup v-model:value="settings.platform">
:key="item" <NRadioButton value="netease">
> 网易云
<NSpace </NRadioButton>
align="center" <NRadioButton value="kugou">
style="width: 100%" 酷狗
> </NRadioButton>
<NButton </NRadioGroup>
type="error" <NInputGroup style="width: 250px">
secondary <NInputGroupLabel> 点歌弹幕前缀 </NInputGroupLabel>
size="small" <NInput v-model:value="settings.orderPrefix" />
@click="settings.blacklist.splice(settings.blacklist.indexOf(item), 1)" </NInputGroup>
<NCheckbox
:checked="settings.orderCooldown != undefined"
@update:checked="(checked: boolean) => {
settings.orderCooldown = checked ? 300 : undefined
}
"
> >
删除 是否启用点歌冷却
</NButton> </NCheckbox>
<NText> {{ item }} </NText> <NInputGroup
v-if="settings.orderCooldown"
style="width: 200px"
>
<NInputGroupLabel> 冷却时间 () </NInputGroupLabel>
<NInputNumber
v-model:value="settings.orderCooldown"
@update:value="(value) => {
if (!value || value <= 0) settings.orderCooldown = undefined
}
"
/>
</NInputGroup>
</NSpace> </NSpace>
</NListItem> <NSpace>
</NList> <NCheckbox v-model:checked="settings.playMusicWhenFree">
</NTabPane> 空闲时播放空闲歌单
</NTabs> </NCheckbox>
<NDivider style="height: 100px" /> <NCheckbox v-model:checked="settings.orderMusicFirst">
优先播放点歌
</NCheckbox>
</NSpace>
<NSpace>
<NTooltip>
<template #trigger>
<NButton
type="info"
@click="getOutputDevice"
>
获取输出设备
</NButton>
</template>
获取和修改输出设备需要打开麦克风权限
</NTooltip>
<NSelect
v-model:value="settings.deviceId"
:options="deviceList"
:fallback-option="() => ({ label: '未选择', value: '' })"
style="min-width: 200px"
@update:value="musicRquestStore.setSinkId"
/>
</NSpace>
</NSpace>
</NTabPane>
</NTabs>
</NCard>
<NModal <NModal
v-model:show="showNeteaseModal" v-model:show="showNeteaseModal"
preset="card" preset="card"

View File

@@ -1017,24 +1017,39 @@ function getIndexStyle(status: QueueStatus): CSSProperties {
</script> </script>
<template> <template>
<!-- 功能启用开关 --> <!-- 顶部功能开关与全局操作 -->
<NAlert <NCard v-if="accountInfo?.id" size="small">
v-if="accountInfo?.id"
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.Queue) ? 'success' : 'warning'"
title="弹幕队列功能"
closable
>
<template #header> <template #header>
<NSpace align="center"> <NSpace align="center" justify="space-between">
<NText>启用弹幕队列功能</NText> <NSpace align="center">
<NSwitch <NText>启用弹幕队列功能</NText>
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.Queue)" <NSwitch
:loading="isLoading" :value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.Queue)"
@update:value="onUpdateFunctionEnable" :loading="isLoading"
/> @update:value="onUpdateFunctionEnable"
/>
</NSpace>
<NTooltip :disabled="configCanEdit">
<template #trigger>
<NButton
type="primary"
size="small"
:disabled="!configCanEdit"
@click="showOBSModal = true"
>
OBS 组件
</NButton>
</template>
登录后可使用 OBS 组件功能
</NTooltip>
</NSpace> </NSpace>
</template> </template>
<NText depth="3"> <NAlert
v-if="accountInfo.settings.enableFunctions.includes(FunctionTypes.Queue)"
type="info"
closable
style="margin-top: 10px"
>
如果没有部署 如果没有部署
<NButton <NButton
text text
@@ -1046,8 +1061,9 @@ function getIndexStyle(status: QueueStatus): CSSProperties {
VtsuruEventFetcher VtsuruEventFetcher
</NButton> </NButton>
则其需要保持此页面开启才能点播, 也不要同时开多个页面, 会导致点播重复 (部署了则不影响) 则其需要保持此页面开启才能点播, 也不要同时开多个页面, 会导致点播重复 (部署了则不影响)
</NText> </NAlert>
</NAlert> </NCard>
<!-- 未登录提示 --> <!-- 未登录提示 -->
<NAlert <NAlert
v-else v-else
@@ -1068,29 +1084,7 @@ function getIndexStyle(status: QueueStatus): CSSProperties {
</NButton> </NButton>
</NAlert> </NAlert>
<NCard <NCard style="margin-top: 12px">
size="small"
style="margin-top: 10px;"
>
<NSpace align="center">
<!-- OBS 组件按钮 -->
<NTooltip :disabled="configCanEdit">
<template #trigger>
<NButton
type="primary"
:disabled="!configCanEdit"
@click="showOBSModal = true"
>
OBS 组件
</NButton>
</template>
登录后可使用 OBS 组件功能
</NTooltip>
<!-- 其他全局操作按钮可以在这里添加 -->
</NSpace>
</NCard>
<NCard style="margin-top: 10px;">
<!-- 主内容区域 --> <!-- 主内容区域 -->
<NTabs <NTabs
v-if="!accountInfo.id || accountInfo.settings.enableFunctions.includes(FunctionTypes.Queue)" v-if="!accountInfo.id || accountInfo.settings.enableFunctions.includes(FunctionTypes.Queue)"
@@ -1214,17 +1208,16 @@ function getIndexStyle(status: QueueStatus): CSSProperties {
<!-- 队列列表 --> <!-- 队列列表 -->
<NSpin :show="isLoading && originQueue.length === 0"> <NSpin :show="isLoading && originQueue.length === 0">
<NList <div
v-if="queue.length > 0" v-if="queue.length > 0"
hoverable class="queue-list-container"
clickable
style="max-height: 60vh; overflow-y: auto;"
> >
<NListItem <TransitionGroup name="list">
v-for="(queueData, index) in queue" <div
:key="queueData.id" v-for="(queueData, index) in queue"
style="padding: 5px 0;" :key="queueData.id"
> class="queue-item-wrapper"
>
<NCard <NCard
embedded embedded
size="small" size="small"
@@ -1444,8 +1437,10 @@ function getIndexStyle(status: QueueStatus): CSSProperties {
</NSpace> </NSpace>
</NSpace> </NSpace>
</NCard> </NCard>
</NListItem> <NDivider style="margin: 0" />
</NList> </div>
</TransitionGroup>
</div>
<NEmpty <NEmpty
v-else v-else
description="当前队列为空" description="当前队列为空"

View File

@@ -206,13 +206,14 @@ const columns: DataTableColumns<SongRequestInfo> = [
</script> </script>
<template> <template>
<NCard size="small"> <NSpace vertical :size="12">
<NSpace> <NSpace>
<NInputGroup style="width: 300px"> <NInputGroup style="width: 250px">
<NInputGroupLabel> 筛选曲名 </NInputGroupLabel> <NInputGroupLabel> 筛选曲名 </NInputGroupLabel>
<NInput <NInput
:value="songRequest.filterSongName" :value="songRequest.filterSongName"
clearable clearable
placeholder="搜索歌曲..."
@update:value="songRequest.filterSongName = $event" @update:value="songRequest.filterSongName = $event"
> >
<template #suffix> <template #suffix>
@@ -225,11 +226,12 @@ const columns: DataTableColumns<SongRequestInfo> = [
</template> </template>
</NInput> </NInput>
</NInputGroup> </NInputGroup>
<NInputGroup style="width: 300px"> <NInputGroup style="width: 250px">
<NInputGroupLabel> 筛选用户 </NInputGroupLabel> <NInputGroupLabel> 筛选用户 </NInputGroupLabel>
<NInput <NInput
:value="songRequest.filterName" :value="songRequest.filterName"
clearable clearable
placeholder="搜索用户..."
@update:value="songRequest.filterName = $event" @update:value="songRequest.filterName = $event"
> >
<template #suffix> <template #suffix>
@@ -243,17 +245,17 @@ const columns: DataTableColumns<SongRequestInfo> = [
</NInput> </NInput>
</NInputGroup> </NInputGroup>
</NSpace> </NSpace>
</NCard> <NDataTable
<br> ref="table"
<NDataTable size="small"
ref="table" :columns="columns"
size="small" :data="songRequest.songs"
:columns="columns" :bordered="false"
:data="songRequest.songs" :loading="songRequest.isLoading"
:bordered="false" :pagination="{ pageSize: 10 }"
:loading="songRequest.isLoading" :row-class-name="(row, index) => (row.status == SongRequestStatus.Singing || row.status == SongRequestStatus.Waiting ? 'song-active' : '')"
:row-class-name="(row, index) => (row.status == SongRequestStatus.Singing || row.status == SongRequestStatus.Waiting ? 'song-active' : '')" />
/> </NSpace>
</template> </template>
<style> <style>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { CSSProperties } from 'vue'
import type { SongRequestInfo } from '@/api/api-models' import type { SongRequestInfo } from '@/api/api-models'
import { import {
Checkmark12Regular, Checkmark12Regular,
@@ -24,6 +25,7 @@ import { useLiveRequest } from '@/composables/useLiveRequest'
const props = defineProps<{ const props = defineProps<{
song: SongRequestInfo song: SongRequestInfo
index: number
isLoading: boolean isLoading: boolean
isLrcLoading: string isLrcLoading: string
updateKey: number updateKey: number
@@ -55,27 +57,34 @@ function onBlockUser() {
songRequest.blockUser(props.song) songRequest.blockUser(props.song)
} }
function getSCColor(price: number): string { function getIndexStyle(status: SongRequestStatus): CSSProperties {
if (price === 0) return `#2a60b2` let backgroundColor
if (price > 0 && price < 30) return `#2a60b2` switch (status) {
if (price >= 30 && price < 50) return `#2a60b2` case SongRequestStatus.Singing:
if (price >= 50 && price < 100) return `#427d9e` backgroundColor = '#18a058'
if (price >= 100 && price < 500) return `#c99801` break
if (price >= 500 && price < 1000) return `#e09443` case SongRequestStatus.Waiting:
if (price >= 1000 && price < 2000) return `#e54d4d` backgroundColor = '#2080f0'
if (price >= 2000) return `#ab1a32` break
return '' default:
} backgroundColor = '#86909c'
}
function getGuardColor(level: number | null | undefined): string {
if (level) { return {
switch (level) { display: 'inline-flex',
case 1: return 'rgb(122, 4, 35)' alignItems: 'center',
case 2: return 'rgb(157, 155, 255)' justifyContent: 'center',
case 3: return 'rgb(104, 136, 241)' fontWeight: 'bold',
} width: '24px',
minWidth: '24px', // 防止压缩
height: '24px',
borderRadius: '50%',
color: 'white',
fontSize: '13px',
backgroundColor,
marginRight: '8px',
flexShrink: 0, // 防止压缩
} }
return ''
} }
// 获取父组件中的活跃歌曲 // 获取父组件中的活跃歌曲
@@ -91,121 +100,100 @@ const hasOtherSingSong = computed(() => {
<NCard <NCard
embedded embedded
size="small" size="small"
content-style="padding: 5px;" content-style="padding: 8px 12px;"
:style="`${isSingingStatus ? 'animation: animated-border 2.5s infinite;' : ''};height: 100%;`" :bordered="isSingingStatus"
:style="isSingingStatus ? 'border-left: 4px solid #18a058;' : 'border-left: 4px solid transparent;'"
> >
<NSpace <NSpace justify="space-between" align="center" :wrap="false">
justify="space-between" <!-- 左侧信息 -->
align="center" <NSpace align="center" :size="8" :wrap="false">
style="height: 100%; margin: 0 5px 0 5px" <!-- 序号 -->
> <span :style="getIndexStyle(song.status)">
<NSpace align="center"> {{ index }}
<div </span>
:style="`border-radius: 4px; background-color: ${isSingingStatus ? '#75c37f' : '#577fb8'}; width: 10px; height: 20px`"
/> <!-- 歌曲名称 -->
<NText <NText strong style="font-size: 16px">
strong
style="font-size: 18px"
>
{{ song.songName }} {{ song.songName }}
</NText> </NText>
<!-- 用户信息 -->
<template v-if="song.from == SongRequestFrom.Manual"> <template v-if="song.from == SongRequestFrom.Manual">
<!-- Manual --> <NTag size="tiny" :bordered="false">
<NTag
size="small"
:bordered="false"
>
手动添加 手动添加
</NTag> </NTag>
</template> </template>
<template v-else> <template v-else>
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NTag <NTag size="tiny" :bordered="false" type="info" round>
size="small" {{ song.user?.name || '未知用户' }}
:bordered="false"
type="info"
>
<NText
italic
depth="3"
>
{{ song.user?.name || '未知用户' }}
</NText>
</NTag> </NTag>
</template> </template>
{{ song.user?.uid || '未知ID' }} UID: {{ song.user?.uid || '未知' }}
</NTooltip> </NTooltip>
</template> </template>
<NSpace
v-if=" <!-- 粉丝牌 -->
(song.from == SongRequestFrom.Danmaku || song.from == SongRequestFrom.SC) <NTag
&& song.user?.fans_medal_wearing_status v-if="(song.from == SongRequestFrom.Danmaku || song.from == SongRequestFrom.SC) && song.user?.fans_medal_wearing_status"
" size="tiny"
round
:bordered="false"
style="padding: 0 6px 0 0;"
> >
<NTag <NTag size="tiny" round :bordered="false" type="info" style="margin-right: 4px;">
size="tiny" {{ song.user?.fans_medal_level }}
round
>
<NTag
size="tiny"
round
:bordered="false"
>
<NText depth="3">
{{ song.user?.fans_medal_level }}
</NText>
</NTag>
<span style="color: #577fb8">
{{ song.user?.fans_medal_name }}
</span>
</NTag> </NTag>
</NSpace> <span style="color: #577fb8">{{ song.user?.fans_medal_name }}</span>
</NTag>
<!-- 舰长 -->
<NTag <NTag
v-if="(song.user?.guard_level ?? 0) > 0" v-if="(song.user?.guard_level ?? 0) > 0"
size="small" size="tiny"
:bordered="false" :bordered="false"
:color="{ textColor: 'white', color: songRequest.getGuardColor(song.user?.guard_level) }" :color="{ textColor: 'white', color: songRequest.getGuardColor(song.user?.guard_level) }"
> >
{{ song.user?.guard_level == 1 ? '总督' : song.user?.guard_level == 2 ? '提督' : '舰长' }} {{ song.user?.guard_level == 1 ? '总督' : song.user?.guard_level == 2 ? '提督' : '舰长' }}
</NTag> </NTag>
<!-- SC/礼物 -->
<NTag <NTag
v-if="song.from == SongRequestFrom.SC" v-if="song.from == SongRequestFrom.SC"
size="small" size="tiny"
:color="{ textColor: 'white', color: songRequest.getSCColor(song.price ?? 0) }" :color="{ textColor: 'white', color: songRequest.getSCColor(song.price ?? 0) }"
> >
SC{{ song.price ? ` | ${song.price}` : '' }} SC{{ song.price ? ` | ${song.price}` : '' }}
</NTag> </NTag>
<NTag <NTag
v-if="song.from == SongRequestFrom.Gift" v-if="song.from == SongRequestFrom.Gift"
size="small" size="tiny"
:color="{ textColor: 'white', color: songRequest.getSCColor(song.price ?? 0) }" :color="{ textColor: 'white', color: songRequest.getSCColor(song.price ?? 0) }"
> >
礼物{{ song.price ? ` | ${song.price}` : '' }} 礼物{{ song.price ? ` | ${song.price}` : '' }}
</NTag> </NTag>
<!-- 时间 -->
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NText style="font-size: small"> <NText depth="3" style="font-size: 12px">
<NTime <NTime :key="updateKey" :time="song.createAt" type="relative" />
:key="updateKey"
:time="song.createAt"
type="relative"
/>
</NText> </NText>
</template> </template>
<NTime :time="song.createAt" /> <NTime :time="song.createAt" />
</NTooltip> </NTooltip>
</NSpace> </NSpace>
<NSpace
justify="end" <!-- 右侧操作按钮 -->
align="center" <NSpace justify="end" align="center" :size="6" :wrap="false">
>
<NTooltip v-if="hasSong"> <NTooltip v-if="hasSong">
<template #trigger> <template #trigger>
<NButton <NButton
circle circle
size="small"
type="success" type="success"
style="height: 30px; width: 30px" ghost
:loading="isLrcLoading == song?.song?.key" :loading="isLrcLoading == song?.song?.key"
@click="onSelectSong" @click="onSelectSong"
> >
@@ -216,43 +204,32 @@ const hasOtherSingSong = computed(() => {
</template> </template>
试听 试听
</NTooltip> </NTooltip>
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NButton <NButton
circle circle
type="primary" size="small"
style="height: 30px; width: 30px" :type="song.status == SongRequestStatus.Singing ? 'warning' : 'primary'"
:ghost="song.status == SongRequestStatus.Singing"
:disabled="hasOtherSingSong" :disabled="hasOtherSingSong"
:style="`animation: ${song.status == SongRequestStatus.Waiting ? '' : 'loading 5s linear infinite'}`"
:secondary="song.status == SongRequestStatus.Singing"
:loading="isLoading" :loading="isLoading"
@click=" @click="onUpdateStatus(song.status == SongRequestStatus.Singing ? SongRequestStatus.Waiting : SongRequestStatus.Singing)"
onUpdateStatus(
song.status == SongRequestStatus.Singing
? SongRequestStatus.Waiting
: SongRequestStatus.Singing,
)
"
> >
<template #icon> <template #icon>
<NIcon :component="Mic24Filled" /> <NIcon :component="Mic24Filled" />
</template> </template>
</NButton> </NButton>
</template> </template>
{{ {{ hasOtherSingSong ? '还有其他正在演唱' : (song.status == SongRequestStatus.Waiting ? '开始演唱' : '暂停演唱') }}
hasOtherSingSong
? '还有其他正在进行的点播'
: song.status == SongRequestStatus.Waiting && song.id
? '开始处理'
: '停止处理'
}}
</NTooltip> </NTooltip>
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NButton <NButton
circle circle
type="primary" size="small"
style="height: 30px; width: 30px" type="success"
:loading="isLoading" :loading="isLoading"
@click="onUpdateStatus(SongRequestStatus.Finish)" @click="onUpdateStatus(SongRequestStatus.Finish)"
> >
@@ -263,55 +240,37 @@ const hasOtherSingSong = computed(() => {
</template> </template>
完成 完成
</NTooltip> </NTooltip>
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NPopconfirm <NPopconfirm @positive-click="onUpdateStatus(SongRequestStatus.Cancel)">
@positive-click="onUpdateStatus(SongRequestStatus.Cancel)"
>
<template #trigger> <template #trigger>
<NButton <NButton circle size="small" type="error" :loading="isLoading">
circle
type="error"
style="height: 30px; width: 30px"
:loading="isLoading"
>
<template #icon> <template #icon>
<NIcon :component="Dismiss16Filled" /> <NIcon :component="Dismiss16Filled" />
</template> </template>
</NButton> </NButton>
</template> </template>
是否取消处理? 确定取消?
</NPopconfirm> </NPopconfirm>
</template> </template>
取消 取消
</NTooltip> </NTooltip>
<NTooltip
v-if=" <NTooltip v-if="song.from == SongRequestFrom.Danmaku && song.user?.uid">
song.from == SongRequestFrom.Danmaku
&& song.user?.uid
&& song.status !== SongRequestStatus.Cancel
"
>
<template #trigger> <template #trigger>
<NPopconfirm <NPopconfirm @positive-click="onBlockUser">
@positive-click="onBlockUser"
>
<template #trigger> <template #trigger>
<NButton <NButton circle size="small" type="error" ghost :loading="isLoading">
circle
type="error"
style="height: 30px; width: 30px"
:loading="isLoading"
>
<template #icon> <template #icon>
<NIcon :component="PresenceBlocked16Regular" /> <NIcon :component="PresenceBlocked16Regular" />
</template> </template>
</NButton> </NButton>
</template> </template>
是否拉黑此用户? 确定拉黑此用户?
</NPopconfirm> </NPopconfirm>
</template> </template>
拉黑用户 拉黑
</NTooltip> </NTooltip>
</NSpace> </NSpace>
</NSpace> </NSpace>

View File

@@ -52,105 +52,125 @@ async function updateSettings() {
</script> </script>
<template> <template>
<NCard size="small"> <NSpace vertical :size="12">
<NSpace align="center"> <NCard size="small" :bordered="false" content-style="padding: 0;">
<NTag <NSpace justify="space-between" align="center">
type="success" <!-- 左侧统计 -->
:bordered="false" <NSpace align="center" :size="16">
> <NTag type="success" round :bordered="false">
<template #icon> <template #icon>
<NIcon :component="PeopleQueue24Filled" /> <NIcon :component="PeopleQueue24Filled" />
</template> </template>
队列 | {{ waitingCount }} 队列: {{ waitingCount }}
</NTag> </NTag>
<NTag <NTag type="info" round :bordered="false">
type="success" <template #icon>
:bordered="false" <NIcon :component="Checkmark12Regular" />
> </template>
<template #icon> 今日已点: {{ todayFinishedCount }}
<NIcon :component="Checkmark12Regular" /> </NTag>
</template> <NText depth="3" style="font-size: 12px">
今日已处理 | {{ todayFinishedCount }} {{ songRequest.activeSongs.length }}
</NTag> </NText>
<NInputGroup> </NSpace>
<NInput
:value="songRequest.newSongName" <!-- 右侧操作 -->
placeholder="手动添加" <NSpace align="center">
@update:value="songRequest.newSongName = $event" <NInputGroup size="small">
/> <NInput
<NButton :value="songRequest.newSongName"
type="primary" placeholder="手动添加歌曲"
@click="songRequest.addSongManual()" @update:value="songRequest.newSongName = $event"
style="width: 150px"
/>
<NButton type="primary" ghost @click="songRequest.addSongManual()">
添加
</NButton>
</NInputGroup>
<NRadioGroup
v-model:value="accountInfo.settings.songRequest.sortType"
:disabled="!songRequest.configCanEdit"
size="small"
@update:value="updateSettings"
>
<NRadioButton :value="QueueSortType.TimeFirst">时间</NRadioButton>
<NRadioButton :value="QueueSortType.PaymentFist">付费</NRadioButton>
<NRadioButton :value="QueueSortType.GuardFirst">舰长</NRadioButton>
<NRadioButton :value="QueueSortType.FansMedalFirst">粉丝牌</NRadioButton>
</NRadioGroup>
<NCheckbox
:checked="currentIsReverse"
size="small"
@update:checked="value => {
if (songRequest.configCanEdit) {
accountInfo.settings.songRequest.isReverse = value
updateSettings()
} else {
songRequest.isReverse = value
}
}"
>
倒序
</NCheckbox>
<NPopconfirm @positive-click="songRequest.deactiveAllSongs()">
<template #trigger>
<NButton type="error" size="small" ghost>
全部取消
</NButton>
</template>
确定全部取消吗?
</NPopconfirm>
</NSpace>
</NSpace>
</NCard>
<div v-if="songRequest.activeSongs.length > 0" class="song-list-container">
<TransitionGroup name="list">
<div
v-for="(song, index) in songRequest.activeSongs"
:key="song.id"
class="song-item-wrapper"
> >
添加 <SongRequestItem
</NButton> :song="song"
</NInputGroup> :index="index + 1"
<NRadioGroup :is-loading="songRequest.isLoading"
v-model:value="accountInfo.settings.songRequest.sortType" :is-lrc-loading="songRequest.isLrcLoading"
:disabled="!songRequest.configCanEdit" :update-key="songRequest.updateKey"
type="button" />
@update:value="value => { <NDivider style="margin: 0" />
updateSettings() </div>
}" </TransitionGroup>
> </div>
<NRadioButton :value="QueueSortType.TimeFirst"> <NEmpty
加入时间优先 v-else
</NRadioButton> description="暂无点播内容"
<NRadioButton :value="QueueSortType.PaymentFist"> style="margin-top: 40px"
付费价格优先 />
</NRadioButton> </NSpace>
<NRadioButton :value="QueueSortType.GuardFirst">
舰长优先 (按等级)
</NRadioButton>
<NRadioButton :value="QueueSortType.FansMedalFirst">
粉丝牌等级优先
</NRadioButton>
</NRadioGroup>
<NCheckbox
:checked="currentIsReverse"
@update:checked="value => {
if (songRequest.configCanEdit) {
accountInfo.settings.songRequest.isReverse = value
updateSettings()
}
else {
songRequest.isReverse = value
}
}"
>
倒序
</NCheckbox>
<NPopconfirm @positive-click="songRequest.deactiveAllSongs()">
<template #trigger>
<NButton type="error">
全部取消
</NButton>
</template>
确定全部取消吗?
</NPopconfirm>
</NSpace>
</NCard>
<NDivider> {{ songRequest.activeSongs.length }} </NDivider>
<NList
v-if="songRequest.activeSongs.length > 0"
:show-divider="false"
hoverable
>
<NListItem
v-for="song in songRequest.activeSongs"
:key="song.id"
style="padding: 5px"
>
<SongRequestItem
:song="song"
:is-loading="songRequest.isLoading"
:is-lrc-loading="songRequest.isLrcLoading"
:update-key="songRequest.updateKey"
/>
</NListItem>
</NList>
<NEmpty
v-else
description="暂无曲目"
/>
</template> </template>
<style scoped>
.song-list-container {
margin-top: 10px;
position: relative;
}
.song-item-wrapper {
transition: all 0.3s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(-20px);
}
.list-leave-active {
position: absolute;
width: 100%;
}
</style>