feat: 更新 LiveRequest 组件,重构代码并优化歌曲请求逻辑

- 添加时间戳以解决缓存问题
- 重构组件结构,简化逻辑,增强可读性
- 更新歌曲请求设置和管理功能
This commit is contained in:
2025-04-27 03:33:48 +08:00
parent 4d997b6615
commit 00ce0fc7e1
8 changed files with 1669 additions and 1399 deletions

View File

@@ -141,6 +141,10 @@ function getParams(params: any) {
if (urlParams.has('token')) {
resultParams.set('token', urlParams.get('token') || '')
}
// 添加时间戳用于解决意外添加的缓存
resultParams.set('timestamp', Date.now().toString())
return resultParams.toString()
}
export async function QueryPostPaginationAPI<T>(

View File

@@ -0,0 +1,522 @@
import { ref, computed, Ref } from 'vue'
import { useStorage } from '@vueuse/core'
import { List } from 'linqts'
import { NTime, useMessage, useNotification } from 'naive-ui'
import { h } from 'vue'
import {
DanmakuUserInfo,
EventDataTypes,
EventModel,
QueueSortType,
SongRequestFrom,
SongRequestInfo,
SongRequestStatus,
SongsInfo
} from '@/api/api-models'
import {
QueryGetAPI,
QueryPostAPI,
QueryPostAPIWithParams
} from '@/api/query'
import { SONG_REQUEST_API_URL } from '@/data/constants'
import { AddBiliBlackList, useAccount } from '@/api/account'
export const useLiveRequest = defineStore('songRequest', () => {
const accountInfo = useAccount()
const localActiveSongs = useStorage('SongRequest.ActiveSongs', [] as SongRequestInfo[])
const isWarnMessageAutoClose = useStorage('SongRequest.Settings.WarnMessageAutoClose', false)
const isReverse = useStorage('SongRequest.Settings.Reverse', false)
const defaultPrefix = useStorage('Settings.SongRequest.DefaultPrefix', '点播')
const isLoading = ref(false)
const originSongs = ref<SongRequestInfo[]>([])
const updateKey = ref(0)
const filterSongName = ref('')
const filterSongNameContains = ref(false)
const filterName = ref('')
const filterNameContains = ref(false)
const newSongName = ref('')
const selectedSong = ref<SongsInfo>()
const isLrcLoading = ref('')
const configCanEdit = computed(() => {
return accountInfo.value != null && accountInfo.value?.id !== undefined
})
const songs = computed(() => {
let result = new List(originSongs.value).Where((s) => {
if (filterName.value) {
if (filterNameContains.value) {
if (!s?.user?.name.toLowerCase().includes(filterName.value.toLowerCase())) {
return false
}
} else if (s?.user?.name.toLowerCase() !== filterName.value.toLowerCase()) {
return false
}
} else if (filterSongName.value) {
if (filterSongNameContains.value) {
if (!s?.songName.toLowerCase().includes(filterSongName.value.toLowerCase())) {
return false
}
} else if (s?.songName.toLowerCase() !== filterSongName.value.toLowerCase()) {
return false
}
}
return true
})
const settings = accountInfo.value?.settings?.songRequest || {}
switch (settings.sortType) {
case QueueSortType.TimeFirst: {
result = result.ThenBy((q) => q.createAt)
break
}
case QueueSortType.GuardFirst: {
result = result
.OrderBy((q) => (q.user?.guard_level == 0 || q.user?.guard_level == null ? 4 : q.user.guard_level))
.ThenBy((q) => q.createAt)
break
}
case QueueSortType.PaymentFist: {
result = result.OrderByDescending((q) => q.price ?? 0).ThenBy((q) => q.createAt)
break
}
case QueueSortType.FansMedalFirst: {
result = result.OrderByDescending((q) => q.user?.fans_medal_level ?? 0).ThenBy((q) => q.createAt)
break
}
}
if ((configCanEdit.value && settings.isReverse) || (!configCanEdit.value && isReverse.value)) {
return result.Reverse().ToArray()
} else {
return result.ToArray()
}
})
const activeSongs = computed(() => {
return (accountInfo.value?.id ? songs.value : localActiveSongs.value)
.sort((a, b) => b.status - a.status)
.filter((song) => {
return song.status == SongRequestStatus.Waiting || song.status == SongRequestStatus.Singing
})
})
const historySongs = computed(() => {
return (accountInfo.value?.id ? songs.value : localActiveSongs.value)
.sort((a, b) => a.status - b.status)
.filter((song) => {
return song.status == SongRequestStatus.Finish || song.status == SongRequestStatus.Cancel
})
})
const STATUS_MAP = {
[SongRequestStatus.Waiting]: '等待中',
[SongRequestStatus.Singing]: '处理中',
[SongRequestStatus.Finish]: '已处理',
[SongRequestStatus.Cancel]: '已取消',
}
async function getAllSong() {
if (accountInfo.value?.id) {
try {
const data = await QueryGetAPI<SongRequestInfo[]>(SONG_REQUEST_API_URL + 'get-all', {
id: accountInfo.value.id,
})
if (data.code == 200) {
console.log('[SONG-REQUEST] 已获取所有数据')
return new List(data.data).OrderByDescending((s) => s.createAt).ToArray()
} else {
window.$message.error('无法获取数据: ' + data.message)
return []
}
} catch (err) {
window.$message.error('无法获取数据')
}
return []
} else {
return localActiveSongs.value
}
}
async function initData() {
originSongs.value = await getAllSong()
}
async function addSong(danmaku: EventModel) {
console.log(
`[SONG-REQUEST] 收到 [${danmaku.uname}] 的点播${danmaku.type == EventDataTypes.SC ? 'SC' : '弹幕'}: ${danmaku.msg}`,
)
const settings = accountInfo.value?.settings?.songRequest || {}
if (settings.enableOnStreaming && accountInfo.value?.streamerInfo?.isStreaming != true) {
window.$notification.info({
title: `${danmaku.uname} 点播失败`,
description: '当前未在直播中, 无法添加点播请求. 或者关闭设置中的仅允许直播时加入',
meta: () => h(NTime, { type: 'relative', time: Date.now(), key: updateKey.value }),
})
return
}
if (accountInfo.value?.id) {
await QueryPostAPI<SongRequestInfo>(SONG_REQUEST_API_URL + 'try-add', danmaku).then((data) => {
if (data.code == 200) {
window.$message.success(`[${danmaku.uname}] 添加曲目: ${data.data.songName}`)
if (data.message != 'EventFetcher') originSongs.value.unshift(data.data)
} else {
const time = Date.now()
window.$notification.warning({
title: danmaku.uname + ' 点播失败',
description: data.message,
duration: isWarnMessageAutoClose.value ? 3000 : 0,
meta: () => h(NTime, { type: 'relative', time: time, key: updateKey.value }),
})
console.log(`[SONG-REQUEST] [${danmaku.uname}] 添加曲目失败: ${data.message}`)
}
})
} else {
const songData = {
songName: danmaku.msg.trim().substring(settings.orderPrefix?.length || defaultPrefix.value.length),
song: undefined,
status: SongRequestStatus.Waiting,
from: danmaku.type == EventDataTypes.Message ? SongRequestFrom.Danmaku : SongRequestFrom.SC,
scPrice: danmaku.type == EventDataTypes.SC ? danmaku.price : 0,
user: {
name: danmaku.uname,
uid: danmaku.uid,
oid: danmaku.open_id,
face: danmaku.uface,
fans_medal_level: danmaku.fans_medal_level,
fans_medal_name: danmaku.fans_medal_name,
fans_medal_wearing_status: danmaku.fans_medal_wearing_status,
guard_level: danmaku.guard_level,
} as DanmakuUserInfo,
createAt: Date.now(),
isInLocal: true,
id: songs.value.length == 0 ? 1 : new List(songs.value).Max((s) => s.id) + 1,
} as SongRequestInfo
localActiveSongs.value.unshift(songData)
window.$message.success(`[${danmaku.uname}] 添加: ${songData.songName}`)
}
}
async function addSongManual() {
if (!newSongName.value) {
window.$message.error('请输入名称')
return
}
if (accountInfo.value?.id) {
await QueryPostAPIWithParams<SongRequestInfo>(SONG_REQUEST_API_URL + 'add', {
name: newSongName.value,
}).then((data) => {
if (data.code == 200) {
window.$message.success(`已手动添加: ${data.data.songName}`)
originSongs.value.unshift(data.data)
newSongName.value = ''
console.log(`[SONG-REQUEST] 已手动添加: ${data.data.songName}`)
} else {
window.$message.error(`手动添加失败: ${data.message}`)
}
})
} else {
const songData = {
songName: newSongName.value,
song: undefined,
status: SongRequestStatus.Waiting,
from: SongRequestFrom.Manual,
scPrice: undefined,
user: undefined,
createAt: Date.now(),
isInLocal: true,
id: songs.value.length == 0 ? 1 : new List(songs.value).Max((s) => s.id) + 1,
} as SongRequestInfo
localActiveSongs.value.unshift(songData)
window.$message.success(`已手动添加: ${songData.songName}`)
newSongName.value = ''
}
}
async function updateSongStatus(song: SongRequestInfo, status: SongRequestStatus) {
if (!configCanEdit.value) {
song.status = status
return
}
isLoading.value = true
let statusString = ''
let statusString2 = ''
switch (status) {
case SongRequestStatus.Waiting:
statusString = 'active'
statusString2 = '等待中'
break
case SongRequestStatus.Cancel:
statusString = 'cancel'
statusString2 = '已取消'
break
case SongRequestStatus.Finish:
statusString = 'finish'
statusString2 = '已完成'
break
case SongRequestStatus.Singing:
statusString = 'singing'
statusString2 = '处理中'
break
}
await QueryGetAPI(SONG_REQUEST_API_URL + statusString, {
id: song.id,
})
.then((data) => {
if (data.code == 200) {
console.log(`[SONG-REQUEST] 更新状态: ${song.songName} -> ${statusString}`)
song.status = status
if (status > SongRequestStatus.Singing) {
song.finishAt = Date.now()
}
window.$message.success(`已更新状态为: ${statusString2}`)
} else {
console.log(`[SONG-REQUEST] 更新状态失败: ${data.message}`)
window.$message.error(`更新状态失败: ${data.message}`)
}
})
.catch((err) => {
window.$message.error(`更新状态失败`)
})
.finally(() => {
isLoading.value = false
})
}
async function deleteSongs(values: SongRequestInfo[]) {
isLoading.value = true
try {
const data = await QueryPostAPI(
SONG_REQUEST_API_URL + 'del',
values.map((s) => s.id),
)
if (data.code == 200) {
window.$message.success('删除成功')
originSongs.value = originSongs.value.filter((s) => !values.includes(s))
} else {
window.$message.error('删除失败: ' + data.message)
}
} catch (err) {
window.$message.error('删除失败')
} finally {
isLoading.value = false
}
}
async function deactiveAllSongs() {
isLoading.value = true
try {
const data = await QueryGetAPI(SONG_REQUEST_API_URL + 'deactive')
if (data.code == 200) {
window.$message.success('已全部取消')
songs.value.forEach((s) => {
if (s.status <= SongRequestStatus.Singing) {
s.status = SongRequestStatus.Cancel
}
})
} else {
window.$message.error('取消失败: ' + data.message)
}
} catch (err) {
window.$message.error('取消失败')
} finally {
isLoading.value = false
}
}
async function updateActive() {
if (!accountInfo.value?.id) return
try {
const data = await QueryGetAPI<SongRequestInfo[]>(SONG_REQUEST_API_URL + 'get-active', {
id: accountInfo.value?.id,
})
if (data.code == 200) {
data.data.forEach((item) => {
const song = originSongs.value.find((s) => s.id == item.id)
if (song) {
if (song.status != item.status) song.status = item.status
} else {
originSongs.value.unshift(item)
if (item.from == SongRequestFrom.Web) {
window.$message.success(`[${item.user?.name}] 直接从网页歌单点播: ${item.songName}`)
}
}
})
} else {
window.$message.error('无法获取点播队列: ' + data.message)
}
} catch (err) {
console.error('[SONG-REQUEST] 更新活跃歌曲失败', err)
}
}
async function blockUser(item: SongRequestInfo) {
if (item.from != SongRequestFrom.Danmaku) {
window.$message.error(`[${item.user?.name}] 不是来自弹幕的用户`)
return
}
if (item.user) {
try {
const data = await AddBiliBlackList(item.user.uid, item.user.name)
if (data.code == 200) {
window.$message.success(`[${item.user?.name}] 已添加到黑名单`)
updateSongStatus(item, SongRequestStatus.Cancel)
} else {
window.$message.error(data.message)
}
} catch (err) {
window.$message.error('添加黑名单失败')
}
}
}
function checkMessage(msg: string) {
if (accountInfo.value?.settings?.enableFunctions?.includes(6) != true) {
return false
}
const prefix = accountInfo.value?.settings?.songRequest?.orderPrefix || defaultPrefix.value
return msg.trim().toLowerCase().startsWith(prefix.toLowerCase())
}
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)'
}
}
return ''
}
function onGetDanmaku(danmaku: EventModel) {
if (checkMessage(danmaku.msg)) {
addSong(danmaku)
}
}
function onGetSC(danmaku: EventModel) {
const settings = accountInfo.value?.settings?.songRequest || {}
if (settings.allowSC && checkMessage(danmaku.msg)) {
addSong(danmaku)
}
}
let updateActiveTimer: any = null
function startUpdateTimer() {
stopUpdateTimer()
updateActiveTimer = setInterval(() => {
updateActive()
}, 2000)
}
function stopUpdateTimer() {
if (updateActiveTimer) {
clearInterval(updateActiveTimer)
updateActiveTimer = null
}
}
let updateKeyTimer: any = null
function startUpdateKeyTimer() {
stopUpdateKeyTimer()
updateKeyTimer = setInterval(() => {
updateKey.value++
}, 1000)
}
function stopUpdateKeyTimer() {
if (updateKeyTimer) {
clearInterval(updateKeyTimer)
updateKeyTimer = null
}
}
async function init() {
await initData()
startUpdateKeyTimer()
startUpdateTimer()
}
function dispose() {
stopUpdateKeyTimer()
stopUpdateTimer()
}
return {
// 状态
isLoading,
originSongs,
songs,
activeSongs,
historySongs,
newSongName,
selectedSong,
isLrcLoading,
filterSongName,
filterSongNameContains,
filterName,
filterNameContains,
updateKey,
STATUS_MAP,
isWarnMessageAutoClose,
isReverse,
defaultPrefix,
// 计算
configCanEdit,
// 方法
init,
dispose,
addSong,
addSongManual,
updateSongStatus,
deleteSongs,
deactiveAllSongs,
updateActive,
blockUser,
checkMessage,
onGetDanmaku,
onGetSC,
getSCColor,
getGuardColor
}
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,267 @@
<script setup lang="ts">
import { h, ref, computed } from 'vue'
import { NCard, NSpace, NInputGroup, NInputGroupLabel, NInput, NCheckbox, NDataTable, NTime, NTag, NText, NIcon, NButton, NPopconfirm, NTooltip } from 'naive-ui'
import { Delete24Filled, ArrowCounterclockwise24Regular } from '@vicons/fluent'
import { SongRequestFrom, SongRequestInfo, SongRequestStatus } from '@/api/api-models'
import { useLiveRequest } from '@/composables/useLiveRequest'
import type { DataTableColumns } from 'naive-ui'
// 使用useLiveRequest
const songRequest = useLiveRequest()
const table = ref()
const statusFilterOptions = computed(() => {
return Object.values(SongRequestStatus)
.filter((t) => /^\d+$/.test(t.toString()))
.map((t) => {
return {
label: songRequest.STATUS_MAP[t as SongRequestStatus],
value: t,
}
})
})
const columns: DataTableColumns<SongRequestInfo> = [
{
title: '曲名',
key: 'songName',
},
{
title: '用户名',
key: 'user.name',
render: (row: SongRequestInfo) => {
return h(
NTooltip,
{ trigger: 'hover' },
{
trigger: () =>
h(
NTag,
{ bordered: false, size: 'small' },
row.from == 3 // Manual
? () => h(NText, { italic: true }, () => '手动添加')
: () => row.user?.name || '未知用户',
),
default: () => (row.from == 3 ? '就是主播自己' : row.user?.uid || '未知ID'),
},
)
},
},
{
title: '来自',
key: 'from',
render(row: SongRequestInfo) {
let fromType: 'info' | 'success' | 'default' | 'error' = 'info'
switch (row.from) {
case SongRequestFrom.Danmaku: { // Danmaku
fromType = 'info'
break
}
case SongRequestFrom.SC: { // SC
fromType = 'error'
break
}
case SongRequestFrom.Web: { // Web
fromType = 'success'
break
}
case SongRequestFrom.Manual: { // Manual
fromType = 'default'
break
}
}
return h(NTag, { size: 'small', type: fromType }, () => {
switch (row.from) {
case SongRequestFrom.Danmaku: {
return '弹幕'
}
case SongRequestFrom.SC: {
return 'SuperChat' + (row.price ? ` | ${row.price}` : '')
}
case SongRequestFrom.Gift: {
return '礼物' + (row.price ? ` | ${row.price}` : '')
}
case SongRequestFrom.Manual: {
return '手动添加'
}
case SongRequestFrom.Web: {
return '网页添加'
}
default:
return '未知'
}
})
},
},
{
title: '状态',
key: 'status',
filter(value, row: SongRequestInfo) {
return ~row.status == Number(value)
},
filterOptions: statusFilterOptions.value,
render(row: SongRequestInfo) {
let statusType: 'info' | 'success' | 'warning' | 'error' = 'info'
switch (row.status) {
case SongRequestStatus.Singing: {
statusType = 'success'
break
}
case SongRequestStatus.Waiting: {
statusType = 'warning'
break
}
case SongRequestStatus.Finish: {
statusType = 'info'
break
}
case SongRequestStatus.Cancel: {
statusType = 'error'
break
}
}
return h(
NTag,
{
type: statusType,
size: 'small',
style: row.status == SongRequestStatus.Singing ? 'animation: animated-border 2.5s infinite;' : '',
},
() => songRequest.STATUS_MAP[row.status],
)
},
},
{
title: '时间',
key: 'time',
sorter: (a: SongRequestInfo, b: SongRequestInfo) => a.createAt - b.createAt,
render: (row: SongRequestInfo) => {
return h(NTime, { time: row.createAt })
},
},
{
title: '操作',
key: 'manage',
width: 100,
render(row: SongRequestInfo) {
return h(
NSpace,
{
justify: 'center',
size: 10,
},
() => [
row.status == SongRequestStatus.Finish || row.status == SongRequestStatus.Cancel
? h(NTooltip, null, {
trigger: () =>
h(
NButton,
{
size: 'small',
type: 'info',
circle: true,
loading: songRequest.isLoading,
onClick: () => {
songRequest.updateSongStatus(row, SongRequestStatus.Waiting)
},
},
{
icon: () => h(NIcon, { component: ArrowCounterclockwise24Regular }),
},
),
default: () => '重新放回等待列表',
})
: undefined,
h(
NPopconfirm,
{ onPositiveClick: () => songRequest.deleteSongs([row]) },
{
trigger: () =>
h(NTooltip, null, {
trigger: () =>
h(
NButton,
{
size: 'small',
type: 'error',
circle: true,
loading: songRequest.isLoading,
},
{
icon: () => h(NIcon, { component: Delete24Filled }),
},
),
default: () => '删除记录',
}),
default: () => '确定删除?',
},
),
],
)
},
},
]
</script>
<template>
<NCard size="small">
<NSpace>
<NInputGroup style="width: 300px">
<NInputGroupLabel> 筛选曲名 </NInputGroupLabel>
<NInput
:value="songRequest.filterSongName"
clearable
@update:value="songRequest.filterSongName = $event"
>
<template #suffix>
<NCheckbox
:checked="songRequest.filterSongNameContains"
@update:checked="songRequest.filterSongNameContains = $event"
>
包含
</NCheckbox>
</template>
</NInput>
</NInputGroup>
<NInputGroup style="width: 300px">
<NInputGroupLabel> 筛选用户名 </NInputGroupLabel>
<NInput
:value="songRequest.filterName"
clearable
@update:value="songRequest.filterName = $event"
>
<template #suffix>
<NCheckbox
:checked="songRequest.filterNameContains"
@update:checked="songRequest.filterNameContains = $event"
>
包含
</NCheckbox>
</template>
</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' : '')"
/>
</template>
<style>
.song-active {
color: white;
background-color: #24292e;
}
.song-active:hover {
background-color: #586069 !important;
}
</style>

View File

@@ -0,0 +1,307 @@
<script setup lang="ts">
import { computed, inject } from 'vue'
import {
Checkmark12Regular,
Dismiss16Filled,
Mic24Filled,
Play24Filled,
PresenceBlocked16Regular,
} from '@vicons/fluent'
import {
NSpace,
NText,
NTag,
NTime,
NTooltip,
NButton,
NIcon,
NCard,
NPopconfirm
} from 'naive-ui'
import { SongRequestInfo, SongRequestStatus, SongsInfo, SongRequestFrom } from '@/api/api-models'
import { useLiveRequest } from '@/composables/useLiveRequest'
const props = defineProps<{
song: SongRequestInfo
isLoading: boolean
isLrcLoading: string
updateKey: number
hasOtherSingSong?: boolean
}>()
// 使用useLiveRequest
const songRequest = useLiveRequest()
const isActiveSong = computed(() => props.song.status <= SongRequestStatus.Singing)
const isSingingStatus = computed(() => props.song.status === SongRequestStatus.Singing)
const hasSong = computed(() => !!props.song.song?.url)
const canStartSinging = computed(() => {
return props.song.status === SongRequestStatus.Waiting
})
function onSelectSong() {
if (hasSong.value) {
songRequest.selectedSong = props.song.song!
}
}
function onUpdateStatus(status: SongRequestStatus) {
songRequest.updateSongStatus(props.song, status)
}
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)'
}
}
return ''
}
// 获取父组件中的活跃歌曲
const activeSongs = inject<SongRequestInfo[]>('activeSongs', [])
// 判断是否有其他正在演唱的歌曲
const hasOtherSingSong = computed(() => {
return activeSongs.findIndex((s: SongRequestInfo) => s.id != props.song.id && s.status == SongRequestStatus.Singing) > -1
})
</script>
<template>
<NCard
embedded
size="small"
content-style="padding: 5px;"
:style="`${isSingingStatus ? 'animation: animated-border 2.5s infinite;' : ''};height: 100%;`"
>
<NSpace
justify="space-between"
align="center"
style="height: 100%; margin: 0 5px 0 5px"
>
<NSpace align="center">
<div
:style="`border-radius: 4px; background-color: ${isSingingStatus ? '#75c37f' : '#577fb8'}; width: 10px; height: 20px`"
/>
<NText
strong
style="font-size: 18px"
>
{{ song.songName }}
</NText>
<template v-if="song.from == SongRequestFrom.Manual">
<!-- Manual -->
<NTag
size="small"
: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>
</template>
{{ song.user?.uid || '未知ID' }}
</NTooltip>
</template>
<NSpace
v-if="
(song.from == SongRequestFrom.Danmaku || song.from == SongRequestFrom.SC) &&
song.user?.fans_medal_wearing_status
"
>
<NTag
size="tiny"
round
>
<NTag
size="tiny"
round
:bordered="false"
>
<NText depth="3">
{{ song.user?.fans_medal_level }}
</NText>
</NTag>
<span style="color: #577fb8">
{{ song.user?.fans_medal_name }}
</span>
</NTag>
</NSpace>
<NTag
v-if="(song.user?.guard_level ?? 0) > 0"
size="small"
:bordered="false"
:color="{ textColor: 'white', color: songRequest.getGuardColor(song.user?.guard_level) }"
>
{{ song.user?.guard_level == 1 ? '总督' : song.user?.guard_level == 2 ? '提督' : '舰长' }}
</NTag>
<NTag
v-if="song.from == SongRequestFrom.SC"
size="small"
:color="{ textColor: 'white', color: songRequest.getSCColor(song.price ?? 0) }"
>
SC{{ song.price ? ` | ${song.price}` : '' }}
</NTag>
<NTag
v-if="song.from == SongRequestFrom.Gift"
size="small"
: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>
</template>
<NTime :time="song.createAt" />
</NTooltip>
</NSpace>
<NSpace
justify="end"
align="center"
>
<NTooltip v-if="hasSong">
<template #trigger>
<NButton
circle
type="success"
style="height: 30px; width: 30px"
:loading="isLrcLoading == song?.song?.key"
@click="onSelectSong"
>
<template #icon>
<NIcon :component="Play24Filled" />
</template>
</NButton>
</template>
试听
</NTooltip>
<NTooltip>
<template #trigger>
<NButton
circle
type="primary"
style="height: 30px; width: 30px"
: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,
)
"
>
<template #icon>
<NIcon :component="Mic24Filled" />
</template>
</NButton>
</template>
{{
hasOtherSingSong
? '还有其他正在演唱的歌曲'
: song.status == SongRequestStatus.Waiting && song.id
? '开始演唱'
: '停止演唱'
}}
</NTooltip>
<NTooltip>
<template #trigger>
<NButton
circle
type="primary"
style="height: 30px; width: 30px"
:loading="isLoading"
@click="onUpdateStatus(SongRequestStatus.Finish)"
>
<template #icon>
<NIcon :component="Checkmark12Regular" />
</template>
</NButton>
</template>
完成
</NTooltip>
<NPopconfirm
@positive-click="onUpdateStatus(SongRequestStatus.Cancel)"
>
<template #trigger>
<NButton
circle
type="error"
style="height: 30px; width: 30px"
:loading="isLoading"
>
<template #icon>
<NIcon :component="Dismiss16Filled" />
</template>
</NButton>
</template>
是否取消处理?
</NPopconfirm>
<NPopconfirm
v-if="
song.from == SongRequestFrom.Danmaku &&
song.user?.uid &&
song.status !== SongRequestStatus.Cancel
"
@positive-click="onBlockUser"
>
<template #trigger>
<NButton
circle
type="error"
style="height: 30px; width: 30px"
:loading="isLoading"
>
<template #icon>
<NIcon :component="PresenceBlocked16Regular" />
</template>
</NButton>
</template>
是否拉黑此用户?
</NPopconfirm>
</NSpace>
</NSpace>
</NCard>
</template>

View File

@@ -0,0 +1,157 @@
<script setup lang="ts">
import { computed, provide } from 'vue'
import { NCard, NSpace, NTag, NIcon, NInput, NInputGroup, NButton, NPopconfirm, NRadioGroup, NRadioButton, NCheckbox, NEmpty, NList, NListItem, NDivider } from 'naive-ui'
import { PeopleQueue24Filled } from '@vicons/fluent'
import { Checkmark12Regular } from '@vicons/fluent'
import { isSameDay } from 'date-fns'
import { SongRequestInfo, SongRequestStatus, QueueSortType } from '@/api/api-models'
import SongRequestItem from './SongRequestItem.vue'
import { useLiveRequest } from '@/composables/useLiveRequest'
import { useAccount } from '@/api/account'
import { SaveSetting } from '@/api/account'
// 使用useLiveRequest
const songRequest = useLiveRequest()
const accountInfo = useAccount()
// 提供activeSongs给子组件
provide('activeSongs', songRequest.activeSongs)
const todayFinishedCount = computed(() => {
return songRequest.songs.filter(s => s.status != SongRequestStatus.Cancel &&
isSameDay(s.finishAt ?? 0, Date.now())).length
})
const waitingCount = computed(() => {
return songRequest.activeSongs.filter(s => s.status === SongRequestStatus.Waiting).length
})
// 当前的排序顺序
const currentIsReverse = computed(() =>
songRequest.configCanEdit ? accountInfo.value?.settings?.songRequest?.isReverse : songRequest.isReverse
)
// 保存排序设置
async function updateSettings() {
if (accountInfo.value?.id) {
songRequest.isLoading = true
await SaveSetting('SongRequest', accountInfo.value.settings.songRequest)
.then((msg) => {
if (msg) {
window.$message.success('已保存')
return true
} else {
window.$message.error('保存失败: ' + msg)
}
})
.finally(() => {
songRequest.isLoading = false
})
} else {
window.$message.success('完成')
}
}
</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()"
>
添加
</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="暂无曲目"
/>
</template>

View File

@@ -0,0 +1,350 @@
<script setup lang="ts">
import { FunctionTypes } from '@/api/api-models';
import { useLiveRequest } from '@/composables/useLiveRequest';
import { SaveEnableFunctions, SaveSetting, useAccount } from '@/api/account'
import {
NAlert,
NButton,
NCheckbox,
NDivider,
NInput,
NInputGroup,
NInputGroupLabel,
NInputNumber,
NSpace,
NSpin,
useMessage
} from 'naive-ui';
import { computed } from 'vue';
// 使用useLiveRequest
const liveRequest = useLiveRequest()
const accountInfo = useAccount()
const message = useMessage()
const enableSongRequest = computed({
get: () => {
return accountInfo.value?.settings?.enableFunctions?.includes(FunctionTypes.SongRequest) || false
},
set: async () => {
await updateEnableFunctions()
}
})
// 控制歌曲请求功能开关
async function updateEnableFunctions() {
if (accountInfo.value.id) {
const oldValue = JSON.parse(JSON.stringify(accountInfo.value.settings.enableFunctions))
if (accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest)) {
accountInfo.value.settings.enableFunctions = accountInfo.value.settings.enableFunctions.filter(
(f: number) => f != FunctionTypes.SongRequest,
)
} else {
accountInfo.value.settings.enableFunctions.push(FunctionTypes.SongRequest)
}
if (!accountInfo.value.settings.songRequest.orderPrefix) {
accountInfo.value.settings.songRequest.orderPrefix = liveRequest.defaultPrefix
}
await SaveEnableFunctions(accountInfo.value?.settings.enableFunctions)
.then((data) => {
if (data.code == 200) {
message.success(
`${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? '启用' : '禁用'}点播功能`,
)
} else {
if (accountInfo.value.id) {
accountInfo.value.settings.enableFunctions = oldValue
}
message.error(
`点播功能${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? '启用' : '禁用'}失败: ${data.message}`,
)
}
})
.catch((err) => {
message.error(
`点播功能${accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? '启用' : '禁用'}失败: ${err}`,
)
})
}
}
// 更新歌曲请求设置
async function updateSettings() {
if (accountInfo.value.id) {
liveRequest.isLoading = true
await SaveSetting('SongRequest', accountInfo.value.settings.songRequest)
.then((msg) => {
if (msg) {
message.success('已保存')
return true
} else {
message.error('保存失败: ' + msg)
}
})
.finally(() => {
liveRequest.isLoading = false
})
} else {
message.success('完成')
}
}
</script>
<template>
<NSpin :show="liveRequest.isLoading">
<NDivider> 规则 </NDivider>
<NSpace vertical>
<NSpace align="center">
<NInputGroup style="width: 250px">
<NInputGroupLabel> 点播弹幕前缀 </NInputGroupLabel>
<template v-if="liveRequest.configCanEdit">
<NInput v-model:value="accountInfo.settings.songRequest.orderPrefix" />
<NButton
type="primary"
@click="updateSettings"
>
确定
</NButton>
</template>
<NInput
v-else
v-model:value="liveRequest.defaultPrefix"
/>
</NInputGroup>
<NAlert
v-if="accountInfo.settings.songRequest.orderPrefix && accountInfo.settings.songRequest.orderPrefix.includes(' ')"
type="info"
>
前缀包含空格
</NAlert>
</NSpace>
<NInputGroup style="width: 250px">
<NInputGroupLabel> 最大队列长度 </NInputGroupLabel>
<NInputNumber
v-model:value="accountInfo.settings.songRequest.queueMaxSize"
:disabled="!liveRequest.configCanEdit"
/>
<NButton
type="info"
:disabled="!liveRequest.configCanEdit"
@click="updateSettings"
>
确定
</NButton>
</NInputGroup>
<NSpace align="center">
<NCheckbox
v-model:checked="accountInfo.settings.songRequest.enableOnStreaming"
:disabled="!liveRequest.configCanEdit"
@update:checked="updateSettings"
>
仅在直播时才允许加入
</NCheckbox>
<NCheckbox
v-model:checked="accountInfo.settings.songRequest.allowAllDanmaku"
:disabled="!liveRequest.configCanEdit"
@update:checked="updateSettings"
>
允许所有弹幕点播
</NCheckbox>
<template v-if="!accountInfo.settings.songRequest.allowAllDanmaku">
<NCheckbox
v-model:checked="accountInfo.settings.songRequest.needWearFanMedal"
:disabled="!liveRequest.configCanEdit"
@update:checked="updateSettings"
>
需要拥有粉丝牌
</NCheckbox>
<NInputGroup
v-if="accountInfo.settings.songRequest.needWearFanMedal"
style="width: 250px"
>
<NInputGroupLabel> 最低粉丝牌等级 </NInputGroupLabel>
<NInputNumber
v-model:value="accountInfo.settings.songRequest.fanMedalMinLevel"
:disabled="!liveRequest.configCanEdit"
/>
<NButton
type="info"
:disabled="!liveRequest.configCanEdit"
@click="updateSettings"
>
确定
</NButton>
</NInputGroup>
<NCheckbox
v-if="!accountInfo.settings.songRequest.allowAllDanmaku"
v-model:checked="accountInfo.settings.songRequest.needJianzhang"
:disabled="!liveRequest.configCanEdit"
@update:checked="updateSettings"
>
只允许舰长
</NCheckbox>
<NCheckbox
v-if="!accountInfo.settings.songRequest.allowAllDanmaku"
v-model:checked="accountInfo.settings.songRequest.needTidu"
:disabled="!liveRequest.configCanEdit"
@update:checked="updateSettings"
>
只允许提督
</NCheckbox>
<NCheckbox
v-if="!accountInfo.settings.songRequest.allowAllDanmaku"
v-model:checked="accountInfo.settings.songRequest.needZongdu"
:disabled="!liveRequest.configCanEdit"
@update:checked="updateSettings"
>
只允许总督
</NCheckbox>
</template>
</NSpace>
<NSpace align="center">
<NCheckbox
v-model:checked="accountInfo.settings.songRequest.allowSC"
:disabled="!liveRequest.configCanEdit"
@update:checked="updateSettings"
>
允许通过 SuperChat 点播
</NCheckbox>
<span v-if="accountInfo.settings.songRequest.allowSC">
<NCheckbox
v-model:checked="accountInfo.settings.songRequest.scIgnoreLimit"
:disabled="!liveRequest.configCanEdit"
@update:checked="updateSettings"
>
SC 点播无视限制
</NCheckbox>
</span>
<NInputGroup
v-if="accountInfo.settings.songRequest.allowSC"
style="width: 250px"
>
<NInputGroupLabel> 最低SC价格 </NInputGroupLabel>
<NInputNumber
v-model:value="accountInfo.settings.songRequest.scMinPrice"
:disabled="!liveRequest.configCanEdit"
/>
<NButton
type="info"
:disabled="!liveRequest.configCanEdit"
@click="updateSettings"
>
确定
</NButton>
</NInputGroup>
</NSpace>
<NDivider> 点歌 </NDivider>
<NSpace>
<NCheckbox
v-model:checked="accountInfo.settings.songRequest.onlyAllowSongList"
:disabled="!liveRequest.configCanEdit"
@update:checked="updateSettings"
>
仅允许点
<NButton
text
tag="a"
href="/manage/song-list"
target="_blank"
type="info"
>
歌单
</NButton>
内的歌曲
</NCheckbox>
<NCheckbox
v-model:checked="accountInfo.settings.songRequest.allowFromWeb"
:disabled="!liveRequest.configCanEdit"
@update:checked="updateSettings"
>
允许通过网页点歌
</NCheckbox>
<NCheckbox
v-if="accountInfo.settings.songRequest.allowFromWeb"
v-model:checked="accountInfo.settings.songRequest.allowAnonymousFromWeb"
:disabled="!liveRequest.configCanEdit"
@update:checked="updateSettings"
>
允许匿名通过网页点歌
</NCheckbox>
</NSpace>
<NDivider> 冷却 (单位: ) </NDivider>
<NCheckbox
v-model:checked="accountInfo.settings.songRequest.enableCooldown"
:disabled="!liveRequest.configCanEdit"
@update:checked="updateSettings"
>
启用点播冷却
</NCheckbox>
<NSpace v-if="accountInfo.settings.songRequest.enableCooldown">
<NInputGroup style="width: 250px">
<NInputGroupLabel> 普通弹幕 </NInputGroupLabel>
<NInputNumber
v-model:value="accountInfo.settings.songRequest.cooldownSecond"
:disabled="!liveRequest.configCanEdit"
/>
<NButton
type="info"
:disabled="!liveRequest.configCanEdit"
@click="updateSettings"
>
确定
</NButton>
</NInputGroup>
<NInputGroup style="width: 220px">
<NInputGroupLabel> 舰长 </NInputGroupLabel>
<NInputNumber
v-model:value="accountInfo.settings.songRequest.jianzhangCooldownSecond"
:disabled="!liveRequest.configCanEdit"
/>
<NButton
type="info"
:disabled="!liveRequest.configCanEdit"
@click="updateSettings"
>
确定
</NButton>
</NInputGroup>
</NSpace>
<NSpace v-if="accountInfo.settings.songRequest.enableCooldown">
<NInputGroup style="width: 220px">
<NInputGroupLabel> 提督 </NInputGroupLabel>
<NInputNumber
v-model:value="accountInfo.settings.songRequest.tiduCooldownSecond"
:disabled="!liveRequest.configCanEdit"
/>
<NButton
type="info"
:disabled="!liveRequest.configCanEdit"
@click="updateSettings"
>
确定
</NButton>
</NInputGroup>
<NInputGroup style="width: 220px">
<NInputGroupLabel> 总督 </NInputGroupLabel>
<NInputNumber
v-model:value="accountInfo.settings.songRequest.zongduCooldownSecond"
:disabled="!liveRequest.configCanEdit"
/>
<NButton
type="info"
:disabled="!liveRequest.configCanEdit"
@click="updateSettings"
>
确定
</NButton>
</NInputGroup>
</NSpace>
<NDivider> 警告消息 </NDivider>
<NSpace>
<NCheckbox
:checked="liveRequest.isWarnMessageAutoClose"
@update:checked="liveRequest.isWarnMessageAutoClose = $event"
>
自动关闭警告消息
</NCheckbox>
</NSpace>
</NSpace>
</NSpin>
</template>