import { computed, ref } from 'vue' import { useAuth } from './useAuth' import { extractErrorCode, useServerErrors } from './useLocaleText' import { getGlobalDedup } from './useRequestDedup' import { useLocale } from '~/utils/locale' import type { PlayTime, Schedule, Song } from '~/types' export const useSongs = () => { const { isAuthenticated, user, getAuthConfig } = useAuth() const { songs: songsLocale } = useLocale() const { localize: localizeServerError } = useServerErrors() const dedup = getGlobalDedup() const songs = ref([]) const publicSchedules = ref([]) const publicSongs = ref([]) const loading = ref(false) const error = ref('') const notification = ref({ show: false, message: '', type: '' }) const playTimes = ref([]) const playTimeEnabled = ref(false) const songCount = ref(0) const requestFormLocale = computed(() => songsLocale.value?.requestForm) const actionLocale = computed(() => songsLocale.value.actions) let songsRequestVersion = 0 let publicSchedulesRequestVersion = 0 // 显示通知 const showNotification = (message: string, type: 'success' | 'error' | 'info' = 'info') => { // 使用全局通知 if (window.$showNotification) { window.$showNotification(message, type) } else { // 备用方案 notification.value = { show: true, message, type } // 3秒后自动关闭 setTimeout(() => { notification.value.show = false }, 3000) } } // 获取播放时段列表 const fetchPlayTimes = async () => { loading.value = true error.value = '' try { const response = await $fetch('/api/play-times') playTimeEnabled.value = response.enabled // 确保类型兼容性 if (response.playTimes && Array.isArray(response.playTimes)) { playTimes.value = response.playTimes.map((pt) => ({ id: pt.id, name: pt.name, startTime: pt.startTime || undefined, endTime: pt.endTime || undefined, enabled: pt.enabled, description: pt.description || undefined })) } else { playTimes.value = [] } return response } catch (err: any) { error.value = err.message || '获取播放时段失败' return { enabled: false, playTimes: [] } } finally { loading.value = false } } // 获取歌曲列表 const fetchSongs = async (silent = false, semester?: string, forceRefresh = false) => { const requestVersion = ++songsRequestVersion if (!silent) { loading.value = true } error.value = '' try { const requestUserId = user.value?.id ?? null const requestParams = { semester: semester || '', audience: requestUserId ? `user:${requestUserId}` : 'guest', role: user.value?.role || '' } const request = async () => { // 构建URL参数 const params = new URLSearchParams() if (semester) { params.append('semester', semester) } const url = `/api/songs${params.toString() ? '?' + params.toString() : ''}` // API请求 return await $fetch(url, { ...getAuthConfig() }) } const response = forceRefresh ? await request() : await dedup.dedupedRequest('songs', request, requestParams) if ( requestVersion !== songsRequestVersion || (user.value?.id ?? null) !== requestUserId || (user.value?.role ?? '') !== (requestParams.role || '') ) return // 正确解析API返回的数据结构 if (response && response.success && response.data && Array.isArray(response.data.songs)) { songs.value = response.data.songs as Song[] } else { songs.value = [] console.warn('API返回的数据格式不正确:', response) } } catch (err: any) { if (requestVersion === songsRequestVersion) { error.value = err.message || '获取歌曲列表失败' } } finally { if (!silent && requestVersion === songsRequestVersion) { loading.value = false } } } // 静默刷新歌曲列表 - 不显示加载状态 const refreshSongsSilent = async () => { return fetchSongs(true) } // 从排期数据中提取歌曲信息 const extractSongsFromSchedules = (schedules: Schedule[]): Song[] => { const songsMap = new Map() schedules.forEach((schedule) => { if (schedule.song) { const songId = String(schedule.song.id) if (!songsMap.has(songId)) { // 将排期中的歌曲信息转换为完整的Song对象 const completeSong: Song = { id: schedule.song.id, title: schedule.song.title, artist: schedule.song.artist, requester: schedule.song.requester, requesterGrade: schedule.song.requesterGrade || null, requesterClass: schedule.song.requesterClass || null, requesterId: 0, // 默认值,公共API不提供这个信息 voteCount: schedule.song.voteCount, played: schedule.song.played || false, playedAt: schedule.song.playedAt || null, semester: schedule.song.semester || null, requestedAt: schedule.song.requestedAt || new Date().toISOString(), cover: schedule.song.cover || null, musicPlatform: schedule.song.musicPlatform || null, musicId: schedule.song.musicId || null, durationSeconds: schedule.song.durationSeconds || null, usedCardCode: schedule.song.usedCardCode === true, hasSubmissionNote: schedule.song.hasSubmissionNote === true, submissionNote: schedule.song.submissionNote || null, submissionNotePublic: schedule.song.submissionNotePublic === true } songsMap.set(songId, completeSong) } } }) return Array.from(songsMap.values()) } // 获取公共排期(无需登录) const fetchPublicSchedules = async (silent = false, semester?: string, forceRefresh = false) => { const requestVersion = ++publicSchedulesRequestVersion if (!silent) { loading.value = true } error.value = '' try { const requestUserId = user.value?.id ?? null const requestParams = { semester: semester || '', audience: requestUserId ? `user:${requestUserId}` : 'guest', role: user.value?.role || '' } const request = async () => { // 构建URL参数 const params = new URLSearchParams() if (semester) { params.append('semester', semester) } const url = `/api/songs/public${params.toString() ? '?' + params.toString() : ''}` const response = await $fetch(url, { ...getAuthConfig() }) return response } const data = forceRefresh ? await request() : await dedup.dedupedRequest('public-schedules', request, requestParams) if ( requestVersion !== publicSchedulesRequestVersion || (user.value?.id ?? null) !== requestUserId || (user.value?.role ?? '') !== (requestParams.role || '') ) return // 确保每个排期的歌曲都有played属性,并处理null/undefined转换 const processedData = data.map((schedule: any) => { // 处理歌曲属性 if (schedule.song && schedule.song.played === undefined) { schedule.song.played = false } // 处理播放时间属性 let processedPlayTime = null if (schedule.playTime) { processedPlayTime = { ...schedule.playTime, startTime: schedule.playTime.startTime || undefined, endTime: schedule.playTime.endTime || undefined } } // 返回符合Schedule类型的对象 return { ...schedule, playTime: processedPlayTime } as Schedule }) publicSchedules.value = processedData // 直接从排期数据中提取歌曲信息,避免重复请求 publicSongs.value = extractSongsFromSchedules(processedData) } catch (err: any) { if (requestVersion === publicSchedulesRequestVersion) { error.value = err.message || '获取排期失败' } } finally { if (!silent && requestVersion === publicSchedulesRequestVersion) { loading.value = false } } } // 静默刷新公共排期 - 不显示加载状态 const refreshSchedulesSilent = async () => { return fetchPublicSchedules(true) } // 请求歌曲 const requestSong = async (songData: { title: string artist: string preferredPlayTimeId?: number | null cover?: string | null musicPlatform?: string | null musicId?: string | null submissionNote?: string | null submissionNotePublic?: boolean cardCode?: string | null }) => { if (!isAuthenticated.value) { showNotification(actionLocale.value.loginRequest, 'error') return null } loading.value = true error.value = '' try { // 使用认证配置 const authConfig = getAuthConfig() const data = await $fetch('/api/songs/request', { method: 'POST', body: songData, ...authConfig }) // 绕过去重,从数据库重新校准写入后的列表 await fetchSongs(true, undefined, true) return data } catch (err: any) { const errorMsg = localizeServerError(err) || '点歌失败' // 如果是重复投稿错误,只显示通知而不设置全局错误 if (errorMsg.includes('已经在列表中') || errorMsg.includes('不能重复投稿')) { showNotification(errorMsg, 'info') } else { error.value = errorMsg showNotification(errorMsg, 'error') } return null } finally { loading.value = false } } // 投票 const voteSong = async (songId: number | { id: number; unvote?: boolean }) => { if (!isAuthenticated.value) { showNotification(actionLocale.value.loginVote, 'error') return null } loading.value = true error.value = '' // 处理传入的可能是对象的情况 const actualSongId = typeof songId === 'object' ? songId.id : songId const isUnvote = typeof songId === 'object' && songId.unvote === true try { // 检查是否已经投过票(只在正常投票时检查) const targetSong = songs.value.find((s) => s.id === actualSongId) if (!isUnvote && targetSong && targetSong.voted) { showNotification(actionLocale.value.alreadyVoted, 'info') loading.value = false return null } // 使用认证配置 const authConfig = getAuthConfig() try { const data = await $fetch('/api/songs/vote', { method: 'POST', body: { songId: actualSongId, unvote: isUnvote // 添加取消投票参数 }, ...authConfig }) // 立即更新本地歌曲的投票状态 if (targetSong) { if (isUnvote) { // 取消投票 targetSong.voted = false targetSong.voteCount = Math.max(0, (targetSong.voteCount || 1) - 1) showNotification(actionLocale.value.voteCancelled(targetSong.title), 'success') } else { // 正常投票 targetSong.voted = true targetSong.voteCount = (targetSong.voteCount || 0) + 1 showNotification(actionLocale.value.voteSucceeded(targetSong.title), 'success') } } await fetchSongs(true, undefined, true) return data } catch (fetchErr: any) { // 处理API返回的错误 const errorMsg = fetchErr.data?.message || fetchErr.message || '投票失败' // 如果是已投票错误,只显示通知,不抛出异常 if (errorMsg.includes('已经为这首歌投过票')) { showNotification(actionLocale.value.alreadyVoted, 'info') // 如果API返回已投票错误,也更新本地状态 if (targetSong && !targetSong.voted) { targetSong.voted = true } return null } // 其他错误则抛出 throw fetchErr } } catch (err: any) { const errorMsg = err.data?.message || err.message || '投票失败' error.value = errorMsg showNotification(errorMsg, 'error') return null } finally { loading.value = false } } // 撤回歌曲(只能撤回自己的投稿) const withdrawSong = async (songId: number) => { if (!isAuthenticated.value) { showNotification(actionLocale.value.loginWithdraw, 'error') return null } loading.value = true error.value = '' try { // 查找歌曲信息用于通知 const targetSong = songs.value.find((s) => s.id === songId) const songTitle = targetSong ? targetSong.title : actionLocale.value.fallbackSongTitle // 使用认证配置 const authConfig = getAuthConfig() const data = await $fetch('/api/songs/withdraw', { method: 'POST', body: { songId }, ...authConfig }) // 绕过去重,从数据库重新校准写入后的列表 await fetchSongs(true, undefined, true) // 显示成功通知 let message = '' if (data.action === 'leave') { message = actionLocale.value.leaveCollaborationSucceeded(songTitle) } else if (data.message) { // 如果后端返回了特定消息,优先使用(除了撤回投稿的情况,我们要保留带标题的格式) message = data.message } else { message = data.quotaReturned ? actionLocale.value.withdrawSucceededQuota(songTitle) : actionLocale.value.withdrawSucceeded(songTitle) } if (data.action !== 'leave') { message = data.quotaReturned ? actionLocale.value.withdrawSucceededQuota(songTitle) : actionLocale.value.withdrawSucceeded(songTitle) } showNotification(message, 'success') return data } catch (err: any) { const errorMsg = err.data?.message || err.message || '撤回歌曲失败' error.value = errorMsg showNotification(errorMsg, 'error') return null } finally { loading.value = false } } // 删除歌曲(管理员专用) const deleteSong = async (songId: number) => { if (!isAuthenticated.value) { showNotification(actionLocale.value.loginDelete, 'error') return null } loading.value = true error.value = '' try { // 使用认证配置 const authConfig = getAuthConfig() const data = await $fetch('/api/admin/songs/delete', { method: 'POST', body: { songId }, ...authConfig }) // 绕过去重,从数据库重新校准写入后的列表 await fetchSongs(true, undefined, true) showNotification(actionLocale.value.deleteSucceeded, 'success') return data } catch (err: any) { const errorMsg = err.data?.message || err.message || '删除歌曲失败' error.value = errorMsg showNotification(errorMsg, 'error') return null } finally { loading.value = false } } // 标记歌曲为已播放(管理员专用) const markPlayed = async (songId: number) => { if (!isAuthenticated.value) { showNotification(actionLocale.value.loginMarkPlayed, 'error') return null } loading.value = true error.value = '' try { // 使用认证配置 const authConfig = getAuthConfig() const data = await $fetch('/api/admin/songs/mark-played', { method: 'POST', body: { songId }, ...authConfig }) // 绕过去重,从数据库重新校准写入后的列表 await fetchSongs(true, undefined, true) showNotification(actionLocale.value.markPlayedSucceeded, 'success') return data } catch (err: any) { const errorMsg = err.data?.message || err.message || '标记歌曲失败' error.value = errorMsg showNotification(errorMsg, 'error') return null } finally { loading.value = false } } // 撤回歌曲已播放状态(管理员专用) const unmarkPlayed = async (songId: number) => { if (!isAuthenticated.value) { showNotification(actionLocale.value.loginUnmarkPlayed, 'error') return null } loading.value = true error.value = '' try { // 使用认证配置 const authConfig = getAuthConfig() const data = await $fetch('/api/admin/songs/mark-played', { method: 'POST', body: { songId, unmark: true }, ...authConfig }) // 绕过去重,从数据库重新校准写入后的列表 await fetchSongs(true, undefined, true) showNotification(actionLocale.value.unmarkPlayedSucceeded, 'success') return data } catch (err: any) { const errorMsg = err.data?.message || err.message || '撤回歌曲已播放状态失败' error.value = errorMsg showNotification(errorMsg, 'error') return null } finally { loading.value = false } } // 申请重播 const requestReplay = async ( songId: number, options: { preferredPlayTimeId?: number | null submissionNote?: string | null submissionNotePublic?: boolean } = {} ) => { if (!isAuthenticated.value) { showNotification(actionLocale.value.loginRequestReplay, 'error') return null } loading.value = true error.value = '' try { const authConfig = getAuthConfig() const data = await $fetch('/api/songs/replay', { method: 'POST', body: { songId, ...options }, ...authConfig }) // 更新本地数据状态 const songIndex = songs.value.findIndex((s) => s.id === songId) if (songIndex !== -1) { songs.value[songIndex].replayRequested = true } await fetchSongs(true, undefined, true) showNotification(actionLocale.value.replaySucceeded, 'success') return data } catch (err: any) { // 按错误码本地化展示,重复申请降级为提示 if (extractErrorCode(err) === 'SONG_REPLAY_ALREADY_REQUESTED') { showNotification(actionLocale.value.replayAlreadyRequested, 'info') } else { showNotification(localizeServerError(err) || '申请重播失败', 'error') } return null } finally { loading.value = false } } /** * 撤回重播申请 * @param songId 歌曲ID */ const withdrawReplay = async (songId: number) => { const { user } = useAuth() if (!user.value) { showNotification(actionLocale.value.loginCancelReplay, 'error') return null } loading.value = true error.value = '' try { const authConfig = getAuthConfig() const data = await $fetch('/api/songs/replay', { method: 'DELETE', body: { songId }, ...authConfig }) // 更新本地数据状态 const songIndex = songs.value.findIndex((s) => s.id === songId) if (songIndex !== -1) { songs.value[songIndex].replayRequested = false } await fetchSongs(true, undefined, true) showNotification(actionLocale.value.replayCancelled, 'success') return data } catch (err: any) { showNotification(localizeServerError(err) || '取消重播申请失败', 'error') return null } finally { loading.value = false } } // 按热度排序的歌曲 const songsByPopularity = computed(() => { return [...songs.value].sort((a, b) => b.voteCount - a.voteCount) }) // 按创建时间排序的歌曲 const songsByDate = computed(() => { return [...songs.value].sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ) }) // 已播放的歌曲 const playedSongs = computed(() => { return songs.value.filter((song) => song.played) }) // 未播放的歌曲 const unplayedSongs = computed(() => { return songs.value.filter((song) => !song.played) }) // 我的投稿歌曲 const mySongs = computed(() => { if (!user.value) return [] return songs.value.filter((song) => song.requesterId === user.value?.id) }) // 我的重播申请歌曲 const myReplaySongs = computed(() => { if (!user.value) return [] return songs.value.filter((song) => song.replayRequested) }) // 所有可见的歌曲(登录用户看到的 + 公共歌曲) const visibleSongs = computed(() => { return isAuthenticated.value ? songs.value : publicSongs.value }) const clearPrivateSongs = () => { songsRequestVersion++ songs.value = [] } const clearPublicSongs = () => { publicSchedulesRequestVersion++ publicSongs.value = [] publicSchedules.value = [] } // 根据播放时间段过滤歌曲排期 const filterSchedulesByPlayTime = (schedules: Schedule[], playTimeId: number | null) => { if (playTimeId === null) { return schedules.filter((s) => s.playTimeId === null) } return schedules.filter((s) => s.playTimeId === playTimeId) } // 获取播放时间名称 const getPlayTimeName = (playTimeId: number | null) => { if (playTimeId === null) { return '未指定时段' } const foundTime = playTimes.value.find((pt) => pt.id === playTimeId) return foundTime ? foundTime.name : '未知时段' } // 格式化播放时间显示 const formatPlayTimeDisplay = (playTime: PlayTime | null) => { if (!playTime) { return '全天' } let displayText = playTime.name // 如果有开始和结束时间,添加时间信息 if (playTime.startTime && playTime.endTime) { displayText += ` (${playTime.startTime}-${playTime.endTime})` } // 如果只有开始时间 else if (playTime.startTime) { displayText += ` (${playTime.startTime}起)` } // 如果只有结束时间 else if (playTime.endTime) { displayText += ` (至${playTime.endTime})` } return displayText } // 获取歌曲总数 const fetchSongCount = async (forceRefresh = false) => { try { const request = async () => { const response = await $fetch('/api/songs/count') return response } const response = forceRefresh ? await request() : await dedup.dedupedRequest('song-count', request) // 正确解析API返回的数据结构 if (response && typeof response.count === 'number') { songCount.value = response.count return response.count } else { console.warn('歌曲总数API返回的数据格式不正确:', response) songCount.value = 0 return 0 } } catch (err: any) { console.error('获取歌曲总数失败:', err) return 0 } } // 初始化加载 const initialize = async () => { await fetchPlayTimes() if (isAuthenticated.value) { await fetchSongs() } else { await fetchPublicSchedules() } } return { songs, publicSongs, publicSchedules, visibleSongs, songCount, loading, error, notification, playTimes, playTimeEnabled, showNotification, fetchSongs, fetchPublicSchedules, fetchPlayTimes, fetchSongCount, clearPrivateSongs, clearPublicSongs, refreshSongsSilent, refreshSchedulesSilent, requestSong, voteSong, withdrawSong, deleteSong, markPlayed, unmarkPlayed, requestReplay, withdrawReplay, filterSchedulesByPlayTime, getPlayTimeName, formatPlayTimeDisplay, extractSongsFromSchedules, initialize, songsByPopularity, songsByDate, playedSongs, unplayedSongs, mySongs, myReplaySongs } }