feat(music): 添加音乐状态获取功能并优化音乐列表展示

- 新增 getMusicStatus API 接口,用于获取音乐的收藏和点赞状态
- 在音乐列表、歌单详情、搜索结果等页面中集成音乐状态获取功能
- 优化音乐播放器,主动获取并更新音乐状态
- 为每首歌曲添加收藏和点赞状态字段
This commit is contained in:
ikmkj
2025-07-26 00:02:03 +08:00
parent 4435315acf
commit 439d4b7588
11 changed files with 205 additions and 27 deletions

View File

@@ -4,6 +4,7 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.stp.StpUtil;
import com.test.musichouduan.dto.MusicDTO;
import com.test.musichouduan.dto.MusicStatusDTO;
import com.test.musichouduan.dto.PageResult;
import com.test.musichouduan.dto.Result;
import com.test.musichouduan.service.MusicService;
@@ -234,4 +235,17 @@ public class MusicController {
String message = isLiked ? "点赞成功" : "取消点赞成功";
return Result.success(message, isLiked);
}
/**
* 获取音乐的收藏和点赞状态
*
* @param id 音乐ID
* @return 响应结果
*/
@GetMapping("/{id}/status")
public Result<MusicStatusDTO> getMusicStatus(@PathVariable Long id) {
Long userId = StpUtil.getLoginIdDefaultNull() != null ? StpUtil.getLoginIdAsLong() : null;
MusicStatusDTO status = musicService.getMusicStatus(id, userId);
return Result.success(status);
}
}

View File

@@ -5,7 +5,9 @@ import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
/**
* 用户点赞音乐实体类
*/
@Data
@Entity
@Table(name = "user_like_music", uniqueConstraints = {

View File

@@ -1,6 +1,7 @@
package com.test.musichouduan.service;
import com.test.musichouduan.dto.MusicDTO;
import com.test.musichouduan.dto.MusicStatusDTO;
import com.test.musichouduan.dto.PageResult;
import com.test.musichouduan.entity.Music;
import org.springframework.web.multipart.MultipartFile;
@@ -151,4 +152,13 @@ public interface MusicService {
* @return 是否点赞成功 (true: 点赞, false: 取消点赞)
*/
boolean toggleLikeMusic(Long musicId, Long userId);
/**
* 获取音乐的收藏和点赞状态
*
* @param musicId 音乐ID
* @param userId 用户ID
* @return 音乐状态DTO
*/
MusicStatusDTO getMusicStatus(Long musicId, Long userId);
}

View File

@@ -2,6 +2,7 @@ package com.test.musichouduan.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import com.test.musichouduan.dto.MusicDTO;
import com.test.musichouduan.dto.MusicStatusDTO;
import com.test.musichouduan.dto.PageResult;
import com.test.musichouduan.entity.Music;
import com.test.musichouduan.entity.MusicType;
@@ -599,6 +600,17 @@ public class MusicServiceImpl implements MusicService {
}
}
@Override
public MusicStatusDTO getMusicStatus(Long musicId, Long userId) {
if (userId == null) {
// 用户未登录,返回默认状态
return new MusicStatusDTO(false, false);
}
boolean isCollected = userCollectMusicRepository.existsByUserIdAndMusicId(userId, musicId);
boolean isLiked = userLikeMusicRepository.existsByUserIdAndMusicId(userId, musicId);
return new MusicStatusDTO(isCollected, isLiked);
}
/**
* 将实体转换为DTO
*

View File

@@ -44,7 +44,12 @@ export function getMusicList(page = 1, size = 10, keyword = '', typeId = null) {
* @returns {Promise<any>}
*/
export function getMusicById(id) {
return api.get(`/music/${id}`).then(res => {
return api.get(`/music/${id}`, {
params: {
// 添加时间戳以防止缓存
_t: new Date().getTime()
}
}).then(res => {
if (res.code === 200 && res.data) {
// 处理音乐URL
if (res.data.url) {
@@ -125,6 +130,15 @@ export function toggleLikeMusic(id) {
return api.post(`/music/${id}/like`)
}
/**
* 获取音乐的收藏和点赞状态
* @param {number} id 音乐ID
* @returns {Promise<any>}
*/
export function getMusicStatus(id) {
return api.get(`/music/${id}/status`);
}
/**
* 获取收藏的音乐列表
* @param {number} page 页码

View File

@@ -286,7 +286,7 @@ import { ref, computed, watch, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { usePlayerStore } from '../stores/player'
import { useUserStore } from '../stores/user'
import { processMusicUrl, toggleCollectMusic } from '../api/music'
import { processMusicUrl, toggleCollectMusic, getMusicStatus } from '../api/music'
import { ElMessage } from 'element-plus'
const router = useRouter()
@@ -490,9 +490,9 @@ const toggleCollect = async () => {
try {
const res = await toggleCollectMusic(currentMusic.value.id)
if (res.code === 200) {
// 更新收藏状态
currentMusic.value.isCollected = !currentMusic.value.isCollected
ElMessage.success(currentMusic.value.isCollected ? '收藏成功' : '已取消收藏')
// 直接使用后端返回的最新状态
playerStore.updateCurrentMusicStatus({ collected: res.data });
ElMessage.success(res.message || (res.data ? '收藏成功' : '已取消收藏'))
} else {
ElMessage.error(res.message || '操作失败')
}
@@ -637,9 +637,9 @@ const scrollToActiveLyric = async () => {
}
}
// 监听当前音乐变化处理音乐URL
// 监听当前音乐变化处理音乐URL和状态
watch(currentMusic, (newMusic) => {
if (newMusic && newMusic.url) {
if (newMusic && newMusic.id) {
// 处理音乐URL
const processedUrl = processMusicUrl(newMusic.url)
if (processedUrl !== newMusic.url) {
@@ -654,9 +654,21 @@ watch(currentMusic, (newMusic) => {
}
}
// 确保收藏状态字段存在
if (newMusic.isCollected === undefined) {
newMusic.isCollected = newMusic.collected || false
// 主动获取最新的收藏和点赞状态
if (userStore.isLoggedIn) {
getMusicStatus(newMusic.id).then(res => {
if (res.code === 200 && res.data) {
playerStore.updateCurrentMusicStatus({
collected: res.data.collected,
liked: res.data.liked
});
}
}).catch(err => {
console.error("获取播放器音乐状态失败:", err);
});
} else {
// 未登录则重置状态
playerStore.updateCurrentMusicStatus({ collected: false, liked: false });
}
// 重置歌词索引和元素引用

View File

@@ -2,12 +2,14 @@
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { Search } from '@element-plus/icons-vue'
import { getMusicList, getHotMusic, getLatestMusic, processMusicUrl } from '../api/music'
import { getMusicList, getHotMusic, getLatestMusic, processMusicUrl, getMusicStatus } from '../api/music'
import { getMusicTypeList } from '../api/musicType'
import MusicList from '../components/MusicList.vue'
import { ElMessage } from 'element-plus'
import { useUserStore } from '../stores/user'
const router = useRouter()
const userStore = useUserStore()
// 音乐列表数据
const musicList = ref([])
@@ -43,13 +45,8 @@ const fetchMusicList = async (params = {}) => {
console.log('音乐列表数据:', res.data)
// 处理收藏状态
// 确保音乐URL和封面URL正确处理
musicList.value.forEach(music => {
if (music.collected === undefined) {
music.collected = false
}
// 确保音乐URL和封面URL正确处理
if (music.url) {
music.url = processMusicUrl(music.url)
}
@@ -57,6 +54,9 @@ const fetchMusicList = async (params = {}) => {
music.cover = processMusicUrl(music.cover)
}
})
// 更新音乐状态
updateMusicStatuses(musicList.value)
} else {
ElMessage.error(res.message || '获取音乐列表失败')
}
@@ -98,6 +98,27 @@ const handleCollectChange = (music) => {
console.log('收藏状态变化:', music)
}
// 更新音乐状态
const updateMusicStatuses = async (list) => {
if (!userStore.isLoggedIn || !list || list.length === 0) {
return
}
// 为列表中的每首歌曲获取状态
const promises = list.map(music => getMusicStatus(music.id))
try {
const results = await Promise.all(promises)
results.forEach((res, index) => {
if (res.code === 200 && res.data) {
list[index].collected = res.data.collected
list[index].liked = res.data.liked
}
})
} catch (error) {
console.error('批量获取音乐状态失败:', error)
}
}
// 获取热门音乐
const fetchHotMusic = async (params = {}) => {
loadingHot.value = true
@@ -108,13 +129,15 @@ const fetchHotMusic = async (params = {}) => {
const allHotMusic = res.data || []
total.value = allHotMusic.length // 总数是50
// 处理URL和收藏状态
// 处理URL
allHotMusic.forEach(music => {
if (music.url) music.url = processMusicUrl(music.url)
if (music.cover) music.cover = processMusicUrl(music.cover)
if (music.collected === undefined) music.collected = false
})
// 更新状态
updateMusicStatuses(allHotMusic)
// 根据当前页码和每页数量进行前端分页
const page = params.page || currentPage.value
const size = params.size || pageSize.value
@@ -149,10 +172,10 @@ const fetchLatestMusic = async () => {
if (music.cover) {
music.cover = processMusicUrl(music.cover)
}
if (music.collected === undefined) {
music.collected = false
}
})
// 更新状态
updateMusicStatuses(latestMusicList.value)
} else {
ElMessage.error(res.message || '获取最新音乐失败')
}

View File

@@ -78,7 +78,7 @@
@click="toggleLike"
>
<el-icon><component :is="music.liked ? 'Check' : 'Pointer'" /></el-icon>
{{ music.liked ? '已点赞' : '点赞' }} ({{ music.likeCount || 0 }})
{{ music.liked ? '已点赞' : '点赞' }}
</el-button>
<el-button
@@ -202,7 +202,7 @@ import { ref, computed, onMounted, watch, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { usePlayerStore } from '../stores/player'
import { useUserStore } from '../stores/user'
import { getMusicById, toggleCollectMusic, getHotMusic, toggleLikeMusic } from '../api/music'
import { getMusicById, toggleCollectMusic, getHotMusic, toggleLikeMusic, getMusicStatus } from '../api/music'
import { getCommentList, addComment, deleteComment as apiDeleteComment } from '../api/comment'
import { connect, subscribe, unsubscribe, disconnect } from '../utils/websocket'
import { ElMessage, ElMessageBox } from 'element-plus'
@@ -246,6 +246,8 @@ const fetchMusicDetail = async () => {
if (res.code === 200) {
if (res.data) {
music.value = res.data
// 获取音乐状态
fetchMusicStatus()
} else {
// 如果音乐不存在,显示提示并返回音乐列表页
ElMessage.warning('音乐不存在或已被删除')
@@ -264,6 +266,22 @@ const fetchMusicDetail = async () => {
}
}
// 获取音乐状态
const fetchMusicStatus = async () => {
if (!music.value || !userStore.isLoggedIn) {
return
}
try {
const res = await getMusicStatus(music.value.id)
if (res.code === 200 && res.data) {
music.value.collected = res.data.collected
music.value.liked = res.data.liked
}
} catch (error) {
console.error('获取音乐状态失败:', error)
}
}
// 获取相关音乐
const fetchRelatedMusic = async () => {
try {

View File

@@ -5,6 +5,7 @@ import { usePlayerStore } from '../stores/player'
import { useUserStore } from '../stores/user'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getPlaylistById, toggleCollectPlaylist, deletePlaylist as apiDeletePlaylist } from '../api/playlist'
import { getMusicStatus } from '../api/music'
import { getCommentList, addComment, deleteComment as apiDeleteComment } from '../api/comment'
import { connect, subscribe, unsubscribe, disconnect } from '../utils/websocket' // 导入 WebSocket 工具
import { processImageUrl } from '../utils/imageUtils'
@@ -67,6 +68,9 @@ const fetchPlaylistDetail = async () => {
// 获取收藏状态
isCollected.value = playlist.value.collected === true
// 更新歌单内歌曲的状态
updateMusicStatuses(playlist.value.songs)
} else {
ElMessage.error(res.message || '获取歌单详情失败')
}
@@ -202,6 +206,27 @@ const cancelReply = () => {
replyContent.value = ''
}
// 更新音乐状态
const updateMusicStatuses = async (list) => {
if (!userStore.isLoggedIn || !list || list.length === 0) {
return
}
// 为列表中的每首歌曲获取状态
const promises = list.map(music => getMusicStatus(music.id))
try {
const results = await Promise.all(promises)
results.forEach((res, index) => {
if (res.code === 200 && res.data) {
list[index].collected = res.data.collected
list[index].liked = res.data.liked
}
})
} catch (error) {
console.error('批量获取音乐状态失败:', error)
}
}
// 提交评论
const submitComment = async (isReply = false) => {
if (!userStore.isLoggedIn) {

View File

@@ -153,7 +153,7 @@ import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '../stores/user'
import { ElMessage } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import { searchMusic, toggleCollectMusic } from '../api/music'
import { searchMusic, toggleCollectMusic, getMusicStatus } from '../api/music'
import { searchPlaylist, toggleCollectPlaylist } from '../api/playlist'
import { searchSinger, toggleCollectSinger } from '../api/singer'
import { processImageUrl } from '../utils/imageUtils'
@@ -222,6 +222,9 @@ const fetchMusicList = async () => {
cover: processMusicUrl(item.cover),
url: processMusicUrl(item.url)
}))
// 更新音乐状态
updateMusicStatuses(musicList.value)
} else {
ElMessage.error(res.message || '搜索音乐失败')
}
@@ -448,6 +451,27 @@ const formatFans = (count) => {
}
}
// 更新音乐状态
const updateMusicStatuses = async (list) => {
if (!userStore.isLoggedIn || !list || list.length === 0) {
return
}
// 为列表中的每首歌曲获取状态
const promises = list.map(music => getMusicStatus(music.id))
try {
const results = await Promise.all(promises)
results.forEach((res, index) => {
if (res.code === 200 && res.data) {
list[index].collected = res.data.collected
list[index].liked = res.data.liked
}
})
} catch (error) {
console.error('批量获取音乐状态失败:', error)
}
}
// 监听关键词变化
watch(() => route.query.keyword, (newKeyword) => {
searchKeyword.value = newKeyword

View File

@@ -7,7 +7,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { getSingerById, toggleCollectSinger } from '../api/singer'
import { getCommentList, addComment, deleteComment as apiDeleteComment } from '../api/comment'
import { processImageUrl } from '../utils/imageUtils'
import { processMusicUrl, getMusicBySinger } from '../api/music'
import { processMusicUrl, getMusicBySinger, getMusicStatus } from '../api/music'
import { connect, subscribe, unsubscribe, disconnect } from '../utils/websocket' // 导入 WebSocket 工具
const route = useRoute()
@@ -92,6 +92,9 @@ const fetchSingerMusic = async () => {
cover: processMusicUrl(song.cover),
url: processMusicUrl(song.url)
}))
// 更新歌曲状态
updateMusicStatuses(songs.value)
} else {
ElMessage.error(res.message || '获取歌手音乐失败')
}
@@ -221,6 +224,27 @@ const cancelReply = () => {
replyContent.value = ''
}
// 更新音乐状态
const updateMusicStatuses = async (list) => {
if (!userStore.isLoggedIn || !list || list.length === 0) {
return
}
// 为列表中的每首歌曲获取状态
const promises = list.map(music => getMusicStatus(music.id))
try {
const results = await Promise.all(promises)
results.forEach((res, index) => {
if (res.code === 200 && res.data) {
list[index].collected = res.data.collected
list[index].liked = res.data.liked
}
})
} catch (error) {
console.error('批量获取音乐状态失败:', error)
}
}
// 提交评论
const submitComment = async (isReply = false) => {
if (!userStore.isLoggedIn) {