From 00ce0fc7e1453d8594f2d869505f89382e93889c Mon Sep 17 00:00:00 2001 From: Megghy Date: Sun, 27 Apr 2025 03:33:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20LiveRequest=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=8C=E9=87=8D=E6=9E=84=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E6=AD=8C=E6=9B=B2=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加时间戳以解决缓存问题 - 重构组件结构,简化逻辑,增强可读性 - 更新歌曲请求设置和管理功能 --- .gitignore | 1 + src/api/query.ts | 4 + src/composables/useLiveRequest.ts | 522 ++++++ src/views/open_live/LiveRequest.vue | 1460 +---------------- .../components/SongRequestHistory.vue | 267 +++ .../open_live/components/SongRequestItem.vue | 307 ++++ .../open_live/components/SongRequestList.vue | 157 ++ .../components/SongRequestSettings.vue | 350 ++++ 8 files changed, 1669 insertions(+), 1399 deletions(-) create mode 100644 src/composables/useLiveRequest.ts create mode 100644 src/views/open_live/components/SongRequestHistory.vue create mode 100644 src/views/open_live/components/SongRequestItem.vue create mode 100644 src/views/open_live/components/SongRequestList.vue create mode 100644 src/views/open_live/components/SongRequestSettings.vue diff --git a/.gitignore b/.gitignore index ac6d2a8..efbe486 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ pnpm-debug.log* *.sw? env.d.ts /.specstory +/.cursor diff --git a/src/api/query.ts b/src/api/query.ts index 618913b..3c6c63b 100644 --- a/src/api/query.ts +++ b/src/api/query.ts @@ -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( diff --git a/src/composables/useLiveRequest.ts b/src/composables/useLiveRequest.ts new file mode 100644 index 0000000..017f787 --- /dev/null +++ b/src/composables/useLiveRequest.ts @@ -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([]) + const updateKey = ref(0) + const filterSongName = ref('') + const filterSongNameContains = ref(false) + const filterName = ref('') + const filterNameContains = ref(false) + const newSongName = ref('') + const selectedSong = ref() + 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(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(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(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(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 + } +}) \ No newline at end of file diff --git a/src/views/open_live/LiveRequest.vue b/src/views/open_live/LiveRequest.vue index 4d24e6d..716d193 100644 --- a/src/views/open_live/LiveRequest.vue +++ b/src/views/open_live/LiveRequest.vue @@ -1,134 +1,70 @@ +// 这是LiveRequest重构后的代码 + @@ -851,7 +208,7 @@ onUnmounted(() => { OBS 组件 - {{ configCanEdit ? '' : '登陆后才可以使用此功能' }} + {{ songRequest.configCanEdit ? '' : '登陆后才可以使用此功能' }} @@ -866,738 +223,46 @@ onUnmounted(() => { name="list" tab="列表" > - - - - - 队列 | {{ activeSongs.filter((s) => s.status == SongRequestStatus.Waiting).length }} - - - - 今日已处理 | - {{ - songs.filter((s) => s.status != SongRequestStatus.Cancel && isSameDay(s.finishAt ?? 0, Date.now())) - .length - }} - 个 - - - - - 添加 - - - - - 加入时间优先 - - - 付费价格优先 - - - 舰长优先 (按等级) - - - 粉丝牌等级优先 - - - - 倒序 - - - 倒序 - - - - 确定全部取消吗? - - - - 共 {{ activeSongs.length }} 首 +
- - - - - -
- - {{ song.songName }} - - - - - - - - {{ song.user?.fans_medal_level }} - - - - {{ song.user?.fans_medal_name }} - - - - - {{ song.user?.guard_level == 1 ? '总督' : song.user?.guard_level == 2 ? '提督' : '舰长' }} - - - SC | {{ song.price }} - - - Gift | {{ song.price }} - - - - - - - - - - 试听 - - - - {{ - songs.findIndex((s) => s.id != song.id && s.status == SongRequestStatus.Singing) > -1 - ? '还有其他正在演唱的歌曲' - : song.status == SongRequestStatus.Waiting && song.id - ? '开始演唱' - : '停止演唱' - }} - - - - 已完成演唱 - - - - 拉黑用户 - - - - 移出队列 - - - - - - - + - - - - 筛选曲名 - - - - - - 筛选用户 - - - - - - - + + - - 规则 - - - - 点播弹幕前缀 - - - - - 前缀包含空格 - - - - 最大队列长度 - - - 确定 - - - - - 仅在直播时才允许加入 - - - 允许所有弹幕点播 - - - - - - 允许通过 SuperChat 点播 - - - - SC 点播无视限制 - - - - 包含冷却时间, 队列长度, 重复点播等 - - - - 最低SC价格 - - - 确定 - - - - - - 点歌 - - - 仅允许点 - - 歌单 - - 内的歌曲 - - - 允许通过网页点歌 - - - 允许匿名通过网页点歌 - - - 冷却 (单位: 秒) - - 启用点播冷却 - - - - 普通弹幕 - - - 确定 - - - - 舰长 - - - 确定 - - - - 提督 - - - 确定 - - - - 总督 - - - 确定 - - - - OBS - - - 标题 - - - - 显示底部的需求信息 - - - 显示点播用户名 - - - 显示点播用户粉丝牌 - - - 其他 - - 自动关闭点播失败时的提示消息 - - - + +