mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
refactor: 将 OBS 通知从 notification 改为 message 组件并优化点歌页面布局
- 将 OBS 通知组件从 n-notification 改为 n-message,简化显示逻辑 - 优化通知内容格式,将标题和元信息作为前缀显示 - 调整通知持续时间:成功 4 秒,错误 6 秒 - 重构点歌页面布局,将功能开关和 OBS 组件按钮移至顶部卡片 - 在 MinimalRequestOBS 组件中添加点歌要求信息显示(前缀、允许类型、SC、粉丝牌等) - 优化点歌队
This commit is contained in:
@@ -44,32 +44,30 @@ export const useOBSNotification = defineStore('obs-notification', () => {
|
||||
}
|
||||
|
||||
function showNotification(payload: ObsNotificationPayload) {
|
||||
const notification = window.$notification
|
||||
if (!notification) {
|
||||
console.warn('[OBS] notification instance missing')
|
||||
const message = window.$message
|
||||
if (!message) {
|
||||
console.warn('[OBS] message instance missing')
|
||||
return
|
||||
}
|
||||
console.log('[OBS] 收到通知', payload)
|
||||
|
||||
const method = payload.Type === 'success' ? 'success' : 'error'
|
||||
const title = resolveTitle(payload)
|
||||
const description = payload.Message || '未知通知'
|
||||
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') {
|
||||
notification[method]({
|
||||
title: payload.Type === 'success' ? '成功' : `失败`,
|
||||
description,
|
||||
duration: method === 'error' ? 8000 : 5000,
|
||||
keepAliveOnHover: true,
|
||||
if (typeof message[method] === 'function') {
|
||||
message[method](finalContent, {
|
||||
duration: method === 'error' ? 6000 : 4000,
|
||||
closable: true,
|
||||
})
|
||||
} else {
|
||||
notification.create({
|
||||
title: payload.Type === 'success' ? '成功' : `失败`,
|
||||
content: description,
|
||||
duration: method === 'error' ? 8000 : 5000,
|
||||
keepAliveOnHover: true,
|
||||
message.create(finalContent, {
|
||||
type: method,
|
||||
duration: method === 'error' ? 6000 : 4000,
|
||||
closable: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ import { copyToClipboard } from '@/Utils'
|
||||
import PointOrderManage from './PointOrderManage.vue'
|
||||
import PointSettings from './PointSettings.vue'
|
||||
import PointUserManage from './PointUserManage.vue'
|
||||
import PointTestPanel from './PointTestPanel.vue'
|
||||
|
||||
const message = useMessage()
|
||||
const accountInfo = useAccount()
|
||||
@@ -734,6 +735,15 @@ onMounted(() => { })
|
||||
>
|
||||
<PointSettings />
|
||||
</NTabPane>
|
||||
|
||||
<!-- 测试标签页 -->
|
||||
<NTabPane
|
||||
name="test"
|
||||
tab="测试"
|
||||
display-directive="show:lazy"
|
||||
>
|
||||
<PointTestPanel />
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
|
||||
<!-- 添加/修改礼物模态框 -->
|
||||
|
||||
331
src/views/manage/point/PointTestPanel.vue
Normal file
331
src/views/manage/point/PointTestPanel.vue
Normal 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>
|
||||
@@ -172,6 +172,39 @@ onUnmounted(() => {
|
||||
description="暂无人点歌"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -370,4 +403,50 @@ onUnmounted(() => {
|
||||
.minimal-list-inner.animating:hover {
|
||||
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>
|
||||
|
||||
@@ -151,19 +151,43 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NAlert
|
||||
v-if="accountInfo.id"
|
||||
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.LiveRequest) ? 'success' : 'warning'"
|
||||
>
|
||||
启用弹幕点播功能
|
||||
<NSwitch
|
||||
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.LiveRequest)"
|
||||
@update:value="onUpdateFunctionEnable"
|
||||
/>
|
||||
|
||||
<br>
|
||||
<NText depth="3">
|
||||
如果没有部署
|
||||
<!-- 顶部功能开关与全局操作 -->
|
||||
<NCard v-if="accountInfo.id" size="small">
|
||||
<template #header>
|
||||
<NSpace align="center" justify="space-between">
|
||||
<NSpace align="center">
|
||||
<NText>启用弹幕点播功能</NText>
|
||||
<NSwitch
|
||||
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.LiveRequest)"
|
||||
@update:value="onUpdateFunctionEnable"
|
||||
/>
|
||||
</NSpace>
|
||||
|
||||
<!-- OBS 组件按钮 -->
|
||||
<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
|
||||
text
|
||||
type="primary"
|
||||
@@ -173,13 +197,14 @@ onUnmounted(() => {
|
||||
>
|
||||
VtsuruEventFetcher
|
||||
</NButton>
|
||||
则其需要保持此页面开启才能点播, 也不要同时开多个页面, 会导致点播重复 !(部署了则不影响)
|
||||
</NText>
|
||||
</NAlert>
|
||||
则其需要保持此页面开启才能点播, 也不要同时开多个页面, 会导致点播重复 (部署了则不影响)
|
||||
</NAlert>
|
||||
</NCard>
|
||||
|
||||
<NAlert
|
||||
v-else
|
||||
type="warning"
|
||||
title="你尚未注册并登录 VTsuru.live, 大部分规则设置将不可用 (因为我懒得在前段重写一遍逻辑"
|
||||
title="你尚未注册并登录 VTsuru.live, 大部分规则设置将不可用 (因为我懒得在前端重写一遍逻辑)"
|
||||
>
|
||||
<NButton
|
||||
tag="a"
|
||||
@@ -190,27 +215,12 @@ onUnmounted(() => {
|
||||
前往登录或注册
|
||||
</NButton>
|
||||
</NAlert>
|
||||
<br>
|
||||
<NCard size="small">
|
||||
<NSpace align="center">
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
type="primary"
|
||||
:disabled="!accountInfo"
|
||||
@click="showOBSModal = true"
|
||||
>
|
||||
OBS 组件
|
||||
</NButton>
|
||||
</template>
|
||||
{{ liveRequest.configCanEdit ? '' : '登陆后才可以使用此功能' }}
|
||||
</NTooltip>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
<br>
|
||||
<NCard>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<NCard style="margin-top: 12px">
|
||||
<NTabs
|
||||
v-if="!accountInfo || accountInfo.settings.enableFunctions.includes(FunctionTypes.LiveRequest)"
|
||||
type="line"
|
||||
animated
|
||||
display-directive="show:lazy"
|
||||
>
|
||||
@@ -223,12 +233,13 @@ onUnmounted(() => {
|
||||
<div
|
||||
v-if="liveRequest.selectedSong"
|
||||
class="song-list"
|
||||
style="margin-bottom: 15px"
|
||||
>
|
||||
<SongPlayer
|
||||
v-model:is-lrc-loading="liveRequest.isLrcLoading"
|
||||
:song="liveRequest.selectedSong"
|
||||
/>
|
||||
<NDivider style="margin: 15px 0 15px 0" />
|
||||
<NDivider style="margin: 15px 0" />
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
|
||||
@@ -385,288 +385,287 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace>
|
||||
<NAlert type="info">
|
||||
搜索时会优先选择非VIP歌曲, 所以点到付费曲目时可能会是猴版或者各种奇怪的歌
|
||||
</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>
|
||||
<NCard size="small">
|
||||
<template #header>
|
||||
<NSpace align="center" justify="space-between">
|
||||
<NSpace align="center">
|
||||
<NRadioGroup v-model:value="settings.platform">
|
||||
<NRadioButton value="netease">
|
||||
网易云
|
||||
</NRadioButton>
|
||||
<NRadioButton value="kugou">
|
||||
酷狗
|
||||
</NRadioButton>
|
||||
</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
|
||||
}
|
||||
"
|
||||
<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="small"
|
||||
@click="listening ? stopListen() : startListen()"
|
||||
>
|
||||
是否启用点歌冷却
|
||||
</NCheckbox>
|
||||
<NInputGroup
|
||||
v-if="settings.orderCooldown"
|
||||
style="width: 200px"
|
||||
{{ listening ? '停止监听' : '开始监听' }}
|
||||
</NButton>
|
||||
<NButton
|
||||
type="info"
|
||||
size="small"
|
||||
@click="showOBSModal = true"
|
||||
>
|
||||
<NInputGroupLabel> 冷却时间 (秒) </NInputGroupLabel>
|
||||
<NInputNumber
|
||||
v-model:value="settings.orderCooldown"
|
||||
@update:value="(value) => {
|
||||
if (!value || value <= 0) settings.orderCooldown = undefined
|
||||
}
|
||||
"
|
||||
/>
|
||||
</NInputGroup>
|
||||
OBS组件
|
||||
</NButton>
|
||||
</NSpace>
|
||||
<NSpace>
|
||||
<NCheckbox v-model:checked="settings.playMusicWhenFree">
|
||||
空闲时播放空闲歌单
|
||||
</NCheckbox>
|
||||
<NCheckbox v-model:checked="settings.orderMusicFirst">
|
||||
优先播放点歌
|
||||
</NCheckbox>
|
||||
</NSpace>
|
||||
<NSpace>
|
||||
<NTooltip>
|
||||
|
||||
<NSpace align="center">
|
||||
<NButton
|
||||
type="primary"
|
||||
secondary
|
||||
:disabled="!accountInfo"
|
||||
size="small"
|
||||
@click="uploadConfig"
|
||||
>
|
||||
保存配置到服务器
|
||||
</NButton>
|
||||
<NPopconfirm @positive-click="downloadConfig">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
type="info"
|
||||
@click="getOutputDevice"
|
||||
type="primary"
|
||||
secondary
|
||||
:disabled="!accountInfo"
|
||||
size="small"
|
||||
>
|
||||
获取输出设备
|
||||
从服务器获取配置
|
||||
</NButton>
|
||||
</template>
|
||||
获取和修改输出设备需要打开麦克风权限
|
||||
</NTooltip>
|
||||
<NSelect
|
||||
v-model:value="settings.deviceId"
|
||||
:options="deviceList"
|
||||
:fallback-option="() => ({ label: '未选择', value: '' })"
|
||||
style="min-width: 200px"
|
||||
@update:value="musicRquestStore.setSinkId"
|
||||
/>
|
||||
这将覆盖当前设置, 确定?
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</NTabPane>
|
||||
<NTabPane
|
||||
name="list"
|
||||
tab="闲置歌单"
|
||||
>
|
||||
<NSpace>
|
||||
<NPopconfirm @positive-click="clearMusic">
|
||||
<template #trigger>
|
||||
<NButton type="error">
|
||||
清空
|
||||
</NButton>
|
||||
</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>
|
||||
<NAlert type="info" closable style="margin-top: 10px">
|
||||
搜索时会优先选择非VIP歌曲, 所以点到付费曲目时可能会是猴版或者各种奇怪的歌
|
||||
</NAlert>
|
||||
</NCard>
|
||||
|
||||
<NCard style="margin-top: 12px">
|
||||
<NTabs type="line" animated>
|
||||
<NTabPane
|
||||
name="queue"
|
||||
tab="当前点歌"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<p :style="`min-height: ${30}px;width:97%;display:flex;align-items:center;`">
|
||||
<NEmpty v-if="musicRquestStore.waitingMusics.length == 0" description="暂无点歌">
|
||||
</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
|
||||
align="center"
|
||||
style="width: 100%"
|
||||
>
|
||||
<NPopconfirm @positive-click="delMusic(item)">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
type="error"
|
||||
secondary
|
||||
size="small"
|
||||
>
|
||||
删除
|
||||
</NButton>
|
||||
</template>
|
||||
确定删除?
|
||||
</NPopconfirm>
|
||||
|
||||
<NButton
|
||||
type="info"
|
||||
type="error"
|
||||
secondary
|
||||
size="small"
|
||||
@click="musicRquestStore.playMusic(item)"
|
||||
@click="settings.blacklist.splice(settings.blacklist.indexOf(item), 1)"
|
||||
>
|
||||
播放
|
||||
删除
|
||||
</NButton>
|
||||
<NText> {{ item.name }} - {{ item.author?.join('/') }} </NText>
|
||||
<NText> {{ item }} </NText>
|
||||
</NSpace>
|
||||
</p>
|
||||
</template>
|
||||
</NVirtualList>
|
||||
</NTabPane>
|
||||
<NTabPane
|
||||
name="blacklist"
|
||||
tab="黑名单"
|
||||
>
|
||||
<NList>
|
||||
<NListItem
|
||||
v-for="item in settings.blacklist"
|
||||
:key="item"
|
||||
>
|
||||
<NSpace
|
||||
align="center"
|
||||
style="width: 100%"
|
||||
>
|
||||
<NButton
|
||||
type="error"
|
||||
secondary
|
||||
size="small"
|
||||
@click="settings.blacklist.splice(settings.blacklist.indexOf(item), 1)"
|
||||
</NListItem>
|
||||
</NList>
|
||||
</NTabPane>
|
||||
|
||||
<NTabPane
|
||||
name="settings"
|
||||
tab="设置"
|
||||
>
|
||||
<NSpace vertical>
|
||||
<NSpace align="center">
|
||||
<NRadioGroup v-model:value="settings.platform">
|
||||
<NRadioButton value="netease">
|
||||
网易云
|
||||
</NRadioButton>
|
||||
<NRadioButton value="kugou">
|
||||
酷狗
|
||||
</NRadioButton>
|
||||
</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
|
||||
}
|
||||
"
|
||||
>
|
||||
删除
|
||||
</NButton>
|
||||
<NText> {{ item }} </NText>
|
||||
是否启用点歌冷却
|
||||
</NCheckbox>
|
||||
<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>
|
||||
</NListItem>
|
||||
</NList>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
<NDivider style="height: 100px" />
|
||||
<NSpace>
|
||||
<NCheckbox v-model:checked="settings.playMusicWhenFree">
|
||||
空闲时播放空闲歌单
|
||||
</NCheckbox>
|
||||
<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
|
||||
v-model:show="showNeteaseModal"
|
||||
preset="card"
|
||||
|
||||
@@ -1017,24 +1017,39 @@ function getIndexStyle(status: QueueStatus): CSSProperties {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 功能启用开关 -->
|
||||
<NAlert
|
||||
v-if="accountInfo?.id"
|
||||
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.Queue) ? 'success' : 'warning'"
|
||||
title="弹幕队列功能"
|
||||
closable
|
||||
>
|
||||
<!-- 顶部功能开关与全局操作 -->
|
||||
<NCard v-if="accountInfo?.id" size="small">
|
||||
<template #header>
|
||||
<NSpace align="center">
|
||||
<NText>启用弹幕队列功能</NText>
|
||||
<NSwitch
|
||||
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.Queue)"
|
||||
:loading="isLoading"
|
||||
@update:value="onUpdateFunctionEnable"
|
||||
/>
|
||||
<NSpace align="center" justify="space-between">
|
||||
<NSpace align="center">
|
||||
<NText>启用弹幕队列功能</NText>
|
||||
<NSwitch
|
||||
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.Queue)"
|
||||
: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>
|
||||
</template>
|
||||
<NText depth="3">
|
||||
<NAlert
|
||||
v-if="accountInfo.settings.enableFunctions.includes(FunctionTypes.Queue)"
|
||||
type="info"
|
||||
closable
|
||||
style="margin-top: 10px"
|
||||
>
|
||||
如果没有部署
|
||||
<NButton
|
||||
text
|
||||
@@ -1046,8 +1061,9 @@ function getIndexStyle(status: QueueStatus): CSSProperties {
|
||||
VtsuruEventFetcher
|
||||
</NButton>
|
||||
则其需要保持此页面开启才能点播, 也不要同时开多个页面, 会导致点播重复 (部署了则不影响)
|
||||
</NText>
|
||||
</NAlert>
|
||||
</NAlert>
|
||||
</NCard>
|
||||
|
||||
<!-- 未登录提示 -->
|
||||
<NAlert
|
||||
v-else
|
||||
@@ -1068,29 +1084,7 @@ function getIndexStyle(status: QueueStatus): CSSProperties {
|
||||
</NButton>
|
||||
</NAlert>
|
||||
|
||||
<NCard
|
||||
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;">
|
||||
<NCard style="margin-top: 12px">
|
||||
<!-- 主内容区域 -->
|
||||
<NTabs
|
||||
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">
|
||||
<NList
|
||||
<div
|
||||
v-if="queue.length > 0"
|
||||
hoverable
|
||||
clickable
|
||||
style="max-height: 60vh; overflow-y: auto;"
|
||||
class="queue-list-container"
|
||||
>
|
||||
<NListItem
|
||||
v-for="(queueData, index) in queue"
|
||||
:key="queueData.id"
|
||||
style="padding: 5px 0;"
|
||||
>
|
||||
<TransitionGroup name="list">
|
||||
<div
|
||||
v-for="(queueData, index) in queue"
|
||||
:key="queueData.id"
|
||||
class="queue-item-wrapper"
|
||||
>
|
||||
<NCard
|
||||
embedded
|
||||
size="small"
|
||||
@@ -1444,8 +1437,10 @@ function getIndexStyle(status: QueueStatus): CSSProperties {
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
</NListItem>
|
||||
</NList>
|
||||
<NDivider style="margin: 0" />
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
<NEmpty
|
||||
v-else
|
||||
description="当前队列为空"
|
||||
|
||||
@@ -206,13 +206,14 @@ const columns: DataTableColumns<SongRequestInfo> = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard size="small">
|
||||
<NSpace vertical :size="12">
|
||||
<NSpace>
|
||||
<NInputGroup style="width: 300px">
|
||||
<NInputGroup style="width: 250px">
|
||||
<NInputGroupLabel> 筛选曲名 </NInputGroupLabel>
|
||||
<NInput
|
||||
:value="songRequest.filterSongName"
|
||||
clearable
|
||||
placeholder="搜索歌曲..."
|
||||
@update:value="songRequest.filterSongName = $event"
|
||||
>
|
||||
<template #suffix>
|
||||
@@ -225,11 +226,12 @@ const columns: DataTableColumns<SongRequestInfo> = [
|
||||
</template>
|
||||
</NInput>
|
||||
</NInputGroup>
|
||||
<NInputGroup style="width: 300px">
|
||||
<NInputGroupLabel> 筛选用户名 </NInputGroupLabel>
|
||||
<NInputGroup style="width: 250px">
|
||||
<NInputGroupLabel> 筛选用户 </NInputGroupLabel>
|
||||
<NInput
|
||||
:value="songRequest.filterName"
|
||||
clearable
|
||||
placeholder="搜索用户..."
|
||||
@update:value="songRequest.filterName = $event"
|
||||
>
|
||||
<template #suffix>
|
||||
@@ -243,17 +245,17 @@ const columns: DataTableColumns<SongRequestInfo> = [
|
||||
</NInput>
|
||||
</NInputGroup>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
<br>
|
||||
<NDataTable
|
||||
ref="table"
|
||||
size="small"
|
||||
:columns="columns"
|
||||
:data="songRequest.songs"
|
||||
:bordered="false"
|
||||
:loading="songRequest.isLoading"
|
||||
:row-class-name="(row, index) => (row.status == SongRequestStatus.Singing || row.status == SongRequestStatus.Waiting ? 'song-active' : '')"
|
||||
/>
|
||||
<NDataTable
|
||||
ref="table"
|
||||
size="small"
|
||||
:columns="columns"
|
||||
:data="songRequest.songs"
|
||||
:bordered="false"
|
||||
:loading="songRequest.isLoading"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
:row-class-name="(row, index) => (row.status == SongRequestStatus.Singing || row.status == SongRequestStatus.Waiting ? 'song-active' : '')"
|
||||
/>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue'
|
||||
import type { SongRequestInfo } from '@/api/api-models'
|
||||
import {
|
||||
Checkmark12Regular,
|
||||
@@ -24,6 +25,7 @@ import { useLiveRequest } from '@/composables/useLiveRequest'
|
||||
|
||||
const props = defineProps<{
|
||||
song: SongRequestInfo
|
||||
index: number
|
||||
isLoading: boolean
|
||||
isLrcLoading: string
|
||||
updateKey: number
|
||||
@@ -55,27 +57,34 @@ function onBlockUser() {
|
||||
songRequest.blockUser(props.song)
|
||||
}
|
||||
|
||||
function getSCColor(price: number): string {
|
||||
if (price === 0) return `#2a60b2`
|
||||
if (price > 0 && price < 30) return `#2a60b2`
|
||||
if (price >= 30 && price < 50) return `#2a60b2`
|
||||
if (price >= 50 && price < 100) return `#427d9e`
|
||||
if (price >= 100 && price < 500) return `#c99801`
|
||||
if (price >= 500 && price < 1000) return `#e09443`
|
||||
if (price >= 1000 && price < 2000) return `#e54d4d`
|
||||
if (price >= 2000) return `#ab1a32`
|
||||
return ''
|
||||
}
|
||||
|
||||
function getGuardColor(level: number | null | undefined): string {
|
||||
if (level) {
|
||||
switch (level) {
|
||||
case 1: return 'rgb(122, 4, 35)'
|
||||
case 2: return 'rgb(157, 155, 255)'
|
||||
case 3: return 'rgb(104, 136, 241)'
|
||||
}
|
||||
function getIndexStyle(status: SongRequestStatus): CSSProperties {
|
||||
let backgroundColor
|
||||
switch (status) {
|
||||
case SongRequestStatus.Singing:
|
||||
backgroundColor = '#18a058'
|
||||
break
|
||||
case SongRequestStatus.Waiting:
|
||||
backgroundColor = '#2080f0'
|
||||
break
|
||||
default:
|
||||
backgroundColor = '#86909c'
|
||||
}
|
||||
|
||||
return {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
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
|
||||
embedded
|
||||
size="small"
|
||||
content-style="padding: 5px;"
|
||||
:style="`${isSingingStatus ? 'animation: animated-border 2.5s infinite;' : ''};height: 100%;`"
|
||||
content-style="padding: 8px 12px;"
|
||||
:bordered="isSingingStatus"
|
||||
:style="isSingingStatus ? 'border-left: 4px solid #18a058;' : 'border-left: 4px solid transparent;'"
|
||||
>
|
||||
<NSpace
|
||||
justify="space-between"
|
||||
align="center"
|
||||
style="height: 100%; margin: 0 5px 0 5px"
|
||||
>
|
||||
<NSpace align="center">
|
||||
<div
|
||||
:style="`border-radius: 4px; background-color: ${isSingingStatus ? '#75c37f' : '#577fb8'}; width: 10px; height: 20px`"
|
||||
/>
|
||||
<NText
|
||||
strong
|
||||
style="font-size: 18px"
|
||||
>
|
||||
<NSpace justify="space-between" align="center" :wrap="false">
|
||||
<!-- 左侧信息 -->
|
||||
<NSpace align="center" :size="8" :wrap="false">
|
||||
<!-- 序号 -->
|
||||
<span :style="getIndexStyle(song.status)">
|
||||
{{ index }}
|
||||
</span>
|
||||
|
||||
<!-- 歌曲名称 -->
|
||||
<NText strong style="font-size: 16px">
|
||||
{{ song.songName }}
|
||||
</NText>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<template v-if="song.from == SongRequestFrom.Manual">
|
||||
<!-- Manual -->
|
||||
<NTag
|
||||
size="small"
|
||||
:bordered="false"
|
||||
>
|
||||
<NTag size="tiny" :bordered="false">
|
||||
手动添加
|
||||
</NTag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NTag
|
||||
size="small"
|
||||
:bordered="false"
|
||||
type="info"
|
||||
>
|
||||
<NText
|
||||
italic
|
||||
depth="3"
|
||||
>
|
||||
{{ song.user?.name || '未知用户' }}
|
||||
</NText>
|
||||
<NTag size="tiny" :bordered="false" type="info" round>
|
||||
{{ song.user?.name || '未知用户' }}
|
||||
</NTag>
|
||||
</template>
|
||||
{{ song.user?.uid || '未知ID' }}
|
||||
UID: {{ song.user?.uid || '未知' }}
|
||||
</NTooltip>
|
||||
</template>
|
||||
<NSpace
|
||||
v-if="
|
||||
(song.from == SongRequestFrom.Danmaku || song.from == SongRequestFrom.SC)
|
||||
&& song.user?.fans_medal_wearing_status
|
||||
"
|
||||
|
||||
<!-- 粉丝牌 -->
|
||||
<NTag
|
||||
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
|
||||
size="tiny"
|
||||
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 size="tiny" round :bordered="false" type="info" style="margin-right: 4px;">
|
||||
{{ song.user?.fans_medal_level }}
|
||||
</NTag>
|
||||
</NSpace>
|
||||
<span style="color: #577fb8">{{ song.user?.fans_medal_name }}</span>
|
||||
</NTag>
|
||||
|
||||
<!-- 舰长 -->
|
||||
<NTag
|
||||
v-if="(song.user?.guard_level ?? 0) > 0"
|
||||
size="small"
|
||||
size="tiny"
|
||||
:bordered="false"
|
||||
:color="{ textColor: 'white', color: songRequest.getGuardColor(song.user?.guard_level) }"
|
||||
>
|
||||
{{ song.user?.guard_level == 1 ? '总督' : song.user?.guard_level == 2 ? '提督' : '舰长' }}
|
||||
</NTag>
|
||||
|
||||
<!-- SC/礼物 -->
|
||||
<NTag
|
||||
v-if="song.from == SongRequestFrom.SC"
|
||||
size="small"
|
||||
size="tiny"
|
||||
:color="{ textColor: 'white', color: songRequest.getSCColor(song.price ?? 0) }"
|
||||
>
|
||||
SC{{ song.price ? ` | ${song.price}` : '' }}
|
||||
</NTag>
|
||||
<NTag
|
||||
v-if="song.from == SongRequestFrom.Gift"
|
||||
size="small"
|
||||
size="tiny"
|
||||
:color="{ textColor: 'white', color: songRequest.getSCColor(song.price ?? 0) }"
|
||||
>
|
||||
礼物{{ song.price ? ` | ${song.price}` : '' }}
|
||||
</NTag>
|
||||
|
||||
<!-- 时间 -->
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NText style="font-size: small">
|
||||
<NTime
|
||||
:key="updateKey"
|
||||
:time="song.createAt"
|
||||
type="relative"
|
||||
/>
|
||||
<NText depth="3" style="font-size: 12px">
|
||||
<NTime :key="updateKey" :time="song.createAt" type="relative" />
|
||||
</NText>
|
||||
</template>
|
||||
<NTime :time="song.createAt" />
|
||||
</NTooltip>
|
||||
</NSpace>
|
||||
<NSpace
|
||||
justify="end"
|
||||
align="center"
|
||||
>
|
||||
|
||||
<!-- 右侧操作按钮 -->
|
||||
<NSpace justify="end" align="center" :size="6" :wrap="false">
|
||||
<NTooltip v-if="hasSong">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
circle
|
||||
size="small"
|
||||
type="success"
|
||||
style="height: 30px; width: 30px"
|
||||
ghost
|
||||
:loading="isLrcLoading == song?.song?.key"
|
||||
@click="onSelectSong"
|
||||
>
|
||||
@@ -216,43 +204,32 @@ const hasOtherSingSong = computed(() => {
|
||||
</template>
|
||||
试听
|
||||
</NTooltip>
|
||||
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
circle
|
||||
type="primary"
|
||||
style="height: 30px; width: 30px"
|
||||
size="small"
|
||||
:type="song.status == SongRequestStatus.Singing ? 'warning' : 'primary'"
|
||||
:ghost="song.status == SongRequestStatus.Singing"
|
||||
:disabled="hasOtherSingSong"
|
||||
:style="`animation: ${song.status == SongRequestStatus.Waiting ? '' : 'loading 5s linear infinite'}`"
|
||||
:secondary="song.status == SongRequestStatus.Singing"
|
||||
:loading="isLoading"
|
||||
@click="
|
||||
onUpdateStatus(
|
||||
song.status == SongRequestStatus.Singing
|
||||
? SongRequestStatus.Waiting
|
||||
: SongRequestStatus.Singing,
|
||||
)
|
||||
"
|
||||
@click="onUpdateStatus(song.status == SongRequestStatus.Singing ? SongRequestStatus.Waiting : SongRequestStatus.Singing)"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="Mic24Filled" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
{{
|
||||
hasOtherSingSong
|
||||
? '还有其他正在进行的点播'
|
||||
: song.status == SongRequestStatus.Waiting && song.id
|
||||
? '开始处理'
|
||||
: '停止处理'
|
||||
}}
|
||||
{{ hasOtherSingSong ? '还有其他正在演唱' : (song.status == SongRequestStatus.Waiting ? '开始演唱' : '暂停演唱') }}
|
||||
</NTooltip>
|
||||
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
circle
|
||||
type="primary"
|
||||
style="height: 30px; width: 30px"
|
||||
size="small"
|
||||
type="success"
|
||||
:loading="isLoading"
|
||||
@click="onUpdateStatus(SongRequestStatus.Finish)"
|
||||
>
|
||||
@@ -263,55 +240,37 @@ const hasOtherSingSong = computed(() => {
|
||||
</template>
|
||||
完成
|
||||
</NTooltip>
|
||||
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NPopconfirm
|
||||
@positive-click="onUpdateStatus(SongRequestStatus.Cancel)"
|
||||
>
|
||||
<NPopconfirm @positive-click="onUpdateStatus(SongRequestStatus.Cancel)">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
circle
|
||||
type="error"
|
||||
style="height: 30px; width: 30px"
|
||||
:loading="isLoading"
|
||||
>
|
||||
<NButton circle size="small" type="error" :loading="isLoading">
|
||||
<template #icon>
|
||||
<NIcon :component="Dismiss16Filled" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
是否取消处理?
|
||||
确定取消?
|
||||
</NPopconfirm>
|
||||
</template>
|
||||
取消
|
||||
</NTooltip>
|
||||
<NTooltip
|
||||
v-if="
|
||||
song.from == SongRequestFrom.Danmaku
|
||||
&& song.user?.uid
|
||||
&& song.status !== SongRequestStatus.Cancel
|
||||
"
|
||||
>
|
||||
|
||||
<NTooltip v-if="song.from == SongRequestFrom.Danmaku && song.user?.uid">
|
||||
<template #trigger>
|
||||
<NPopconfirm
|
||||
@positive-click="onBlockUser"
|
||||
>
|
||||
<NPopconfirm @positive-click="onBlockUser">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
circle
|
||||
type="error"
|
||||
style="height: 30px; width: 30px"
|
||||
:loading="isLoading"
|
||||
>
|
||||
<NButton circle size="small" type="error" ghost :loading="isLoading">
|
||||
<template #icon>
|
||||
<NIcon :component="PresenceBlocked16Regular" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
是否拉黑此用户?
|
||||
确定拉黑此用户?
|
||||
</NPopconfirm>
|
||||
</template>
|
||||
拉黑用户
|
||||
拉黑
|
||||
</NTooltip>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
|
||||
@@ -52,105 +52,125 @@ async function updateSettings() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard size="small">
|
||||
<NSpace align="center">
|
||||
<NTag
|
||||
type="success"
|
||||
:bordered="false"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="PeopleQueue24Filled" />
|
||||
</template>
|
||||
队列 | {{ waitingCount }}
|
||||
</NTag>
|
||||
<NTag
|
||||
type="success"
|
||||
:bordered="false"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="Checkmark12Regular" />
|
||||
</template>
|
||||
今日已处理 | {{ todayFinishedCount }} 个
|
||||
</NTag>
|
||||
<NInputGroup>
|
||||
<NInput
|
||||
:value="songRequest.newSongName"
|
||||
placeholder="手动添加"
|
||||
@update:value="songRequest.newSongName = $event"
|
||||
/>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="songRequest.addSongManual()"
|
||||
<NSpace vertical :size="12">
|
||||
<NCard size="small" :bordered="false" content-style="padding: 0;">
|
||||
<NSpace justify="space-between" align="center">
|
||||
<!-- 左侧统计 -->
|
||||
<NSpace align="center" :size="16">
|
||||
<NTag type="success" round :bordered="false">
|
||||
<template #icon>
|
||||
<NIcon :component="PeopleQueue24Filled" />
|
||||
</template>
|
||||
队列: {{ waitingCount }}
|
||||
</NTag>
|
||||
<NTag type="info" round :bordered="false">
|
||||
<template #icon>
|
||||
<NIcon :component="Checkmark12Regular" />
|
||||
</template>
|
||||
今日已点: {{ todayFinishedCount }}
|
||||
</NTag>
|
||||
<NText depth="3" style="font-size: 12px">
|
||||
共 {{ songRequest.activeSongs.length }} 首
|
||||
</NText>
|
||||
</NSpace>
|
||||
|
||||
<!-- 右侧操作 -->
|
||||
<NSpace align="center">
|
||||
<NInputGroup size="small">
|
||||
<NInput
|
||||
:value="songRequest.newSongName"
|
||||
placeholder="手动添加歌曲"
|
||||
@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"
|
||||
>
|
||||
添加
|
||||
</NButton>
|
||||
</NInputGroup>
|
||||
<NRadioGroup
|
||||
v-model:value="accountInfo.settings.songRequest.sortType"
|
||||
:disabled="!songRequest.configCanEdit"
|
||||
type="button"
|
||||
@update:value="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"
|
||||
@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="暂无曲目"
|
||||
/>
|
||||
<SongRequestItem
|
||||
:song="song"
|
||||
:index="index + 1"
|
||||
:is-loading="songRequest.isLoading"
|
||||
:is-lrc-loading="songRequest.isLrcLoading"
|
||||
:update-key="songRequest.updateKey"
|
||||
/>
|
||||
<NDivider style="margin: 0" />
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
<NEmpty
|
||||
v-else
|
||||
description="暂无点播内容"
|
||||
style="margin-top: 40px"
|
||||
/>
|
||||
</NSpace>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user