From 258215a4e6ef38bd7ce3e3e77235f564a1894748 Mon Sep 17 00:00:00 2001 From: ikmkj <1@qq,com> Date: Sat, 26 Jul 2025 12:07:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(user):=20=E4=B8=BA=E6=94=B6=E8=97=8F?= =?UTF-8?q?=E9=9F=B3=E4=B9=90=E9=A1=B5=E9=9D=A2=E6=B7=BB=E5=8A=A0=E6=9B=B4?= =?UTF-8?q?=E5=A4=9A=E6=93=8D=E4=BD=9C=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加下拉菜单,支持多种操作 - 实现添加到播放列表、下一首播放、下载音乐、下载歌词等功能 - 优化用户交互,增加确认对话框和提示消息 --- .../controller/FileController.java | 87 ++++++++++++++++++- .../musichouduan/service/MusicService.java | 8 ++ .../service/impl/MusicServiceImpl.java | 5 ++ music-qianduan/src/components/MusicList.vue | 28 +++++- music-qianduan/src/views/PlaylistDetail.vue | 69 +++++++++++++-- music-qianduan/src/views/SingerDetail.vue | 69 +++++++++++++-- .../src/views/user/CollectMusic.vue | 78 ++++++++++++++--- 7 files changed, 310 insertions(+), 34 deletions(-) diff --git a/music-houduan/src/main/java/com/test/musichouduan/controller/FileController.java b/music-houduan/src/main/java/com/test/musichouduan/controller/FileController.java index 920e1f8..162d01a 100644 --- a/music-houduan/src/main/java/com/test/musichouduan/controller/FileController.java +++ b/music-houduan/src/main/java/com/test/musichouduan/controller/FileController.java @@ -1,12 +1,27 @@ package com.test.musichouduan.controller; import cn.dev33.satoken.annotation.SaCheckLogin; +import com.test.musichouduan.entity.Music; +import com.test.musichouduan.service.MusicService; import com.test.musichouduan.dto.Result; import com.test.musichouduan.service.FileService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + /** * 文件控制器 */ @@ -16,7 +31,8 @@ public class FileController { @Autowired private FileService fileService; - + @Autowired + private MusicService musicService; /** * 上传图片 * @@ -68,4 +84,73 @@ public class FileController { boolean result = fileService.deleteFile(url); return Result.success("删除成功", result); } + + /** + * 下载音乐 + * + * @param musicId 音乐ID + * @return 响应实体 + */ + @GetMapping("/download/music/{musicId}") + public ResponseEntity downloadMusic(@PathVariable Integer musicId) throws IOException { + // 1. 根据 musicId 获取音乐信息 + Music music = musicService.findMusicById(musicId); + if (music == null || music.getUrl() == null || music.getUrl().isEmpty()) { + return ResponseEntity.notFound().build(); + } + + // 2. 获取文件路径 + String fileUrl = music.getUrl(); // 格式: /upload/musics/xxxxxxxx.mp3 + String filePath = "music-houduan" + fileUrl; // 转换为相对项目路径 + Path path = Paths.get(filePath); + Resource resource = new InputStreamResource(Files.newInputStream(path)); + + // 3. 设置响应头 + HttpHeaders headers = new HttpHeaders(); + String filename = music.getName() + ".mp3"; // 使用音乐名作为文件名 + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name())); + headers.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); + headers.add(HttpHeaders.PRAGMA, "no-cache"); + headers.add(HttpHeaders.EXPIRES, "0"); + + return ResponseEntity.ok() + .headers(headers) + .contentLength(Files.size(path)) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(resource); + } + + /** + * 下载歌词 + * + * @param musicId 音乐ID + * @return 响应实体 + */ + @GetMapping("/download/lyric/{musicId}") + public ResponseEntity downloadLyric(@PathVariable Integer musicId) throws IOException { + // 1. 根据 musicId 获取音乐信息 + Music music = musicService.findMusicById(musicId); + if (music == null || music.getLyric() == null || music.getLyric().isEmpty()) { + return ResponseEntity.notFound().build(); + } + + // 2. 将歌词文本转换为输入流 + String lyricContent = music.getLyric(); + byte[] lyricBytes = lyricContent.getBytes(StandardCharsets.UTF_8); + Resource resource = new InputStreamResource(new ByteArrayInputStream(lyricBytes)); + + // 3. 设置响应头 + HttpHeaders headers = new HttpHeaders(); + String filename = music.getName() + ".lrc"; // 使用音乐名作为文件名 + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name())); + headers.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); + headers.add(HttpHeaders.PRAGMA, "no-cache"); + headers.add(HttpHeaders.EXPIRES, "0"); + + return ResponseEntity.ok() + .headers(headers) + .contentLength(lyricBytes.length) + .contentType(MediaType.TEXT_PLAIN) + .body(resource); + } } diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/MusicService.java b/music-houduan/src/main/java/com/test/musichouduan/service/MusicService.java index e42312c..11af4c8 100644 --- a/music-houduan/src/main/java/com/test/musichouduan/service/MusicService.java +++ b/music-houduan/src/main/java/com/test/musichouduan/service/MusicService.java @@ -161,4 +161,12 @@ public interface MusicService { * @return 音乐状态DTO */ MusicStatusDTO getMusicStatus(Long musicId, Long userId); + + /** + * 根据ID查找音乐实体 + * + * @param musicId 音乐ID + * @return 音乐实体 + */ + Music findMusicById(Integer musicId); } diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/impl/MusicServiceImpl.java b/music-houduan/src/main/java/com/test/musichouduan/service/impl/MusicServiceImpl.java index 0931965..9d198a9 100644 --- a/music-houduan/src/main/java/com/test/musichouduan/service/impl/MusicServiceImpl.java +++ b/music-houduan/src/main/java/com/test/musichouduan/service/impl/MusicServiceImpl.java @@ -615,6 +615,11 @@ public class MusicServiceImpl implements MusicService { return new MusicStatusDTO(isCollected, isLiked); } + @Override + public Music findMusicById(Integer musicId) { + return musicRepository.findById(musicId.longValue()).orElse(null); + } + /** * 将实体转换为DTO * diff --git a/music-qianduan/src/components/MusicList.vue b/music-qianduan/src/components/MusicList.vue index 7ceff0e..718a09e 100644 --- a/music-qianduan/src/components/MusicList.vue +++ b/music-qianduan/src/components/MusicList.vue @@ -99,8 +99,9 @@ 添加到播放列表 下一首播放 + 下载音乐 + 下载歌词 分享 - 下载 @@ -128,7 +129,7 @@ import { useRouter } from 'vue-router' import { VideoPlay, VideoPause, Star, More, InfoFilled, Pointer } from '@element-plus/icons-vue' import { usePlayerStore } from '../stores/player' import { toggleCollectMusic, toggleLikeMusic } from '../api/music' -import { ElMessage } from 'element-plus' +import { ElMessage, ElMessageBox } from 'element-plus' const props = defineProps({ list: { @@ -258,12 +259,31 @@ const handleCommand = (command, music) => { case 'share': ElMessage.info('分享功能开发中') break - case 'download': - ElMessage.info('下载功能开发中') + case 'downloadMusic': + downloadFile('music', music) + break + case 'downloadLyric': + downloadFile('lyric', music) break } } +// 下载文件 +const downloadFile = (type, music) => { + const typeName = type === 'music' ? '音乐' : '歌词' + ElMessageBox.confirm(`确定要下载《${music.name}》的${typeName}吗?`, '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'info' + }).then(() => { + const url = `/api/file/download/${type}/${music.id}` + window.open(url, '_blank') + ElMessage.success(`${typeName}开始下载...`) + }).catch(() => { + ElMessage.info(`已取消下载${typeName}`) + }) +} + // 格式化时长 const formatDuration = (seconds) => { if (!seconds) return '00:00' diff --git a/music-qianduan/src/views/PlaylistDetail.vue b/music-qianduan/src/views/PlaylistDetail.vue index 11a2c61..6f3eeae 100644 --- a/music-qianduan/src/views/PlaylistDetail.vue +++ b/music-qianduan/src/views/PlaylistDetail.vue @@ -331,6 +331,54 @@ const formatPlayCount = (count) => { } } +// 处理下拉菜单命令 +const handleCommand = (command, music) => { + switch (command) { + case 'addToPlaylist': + playerStore.addToPlaylist(music) + ElMessage.success('已添加到播放列表') + break + case 'playNext': + const currentIndex = playerStore.getCurrentIndex + if (currentIndex !== -1) { + const playlist = [...playerStore.getPlaylist] + const existingIndex = playlist.findIndex(item => item.id === music.id) + if (existingIndex !== -1) { + playlist.splice(existingIndex, 1) + } + playlist.splice(currentIndex + 1, 0, music) + playerStore.$patch({ playlist }) + ElMessage.success('已添加到下一首播放') + } else { + playerStore.addToPlaylist(music) + ElMessage.success('已添加到播放列表') + } + break + case 'downloadMusic': + downloadFile('music', music) + break + case 'downloadLyric': + downloadFile('lyric', music) + break + } +} + +// 下载文件 +const downloadFile = (type, music) => { + const typeName = type === 'music' ? '音乐' : '歌词' + ElMessageBox.confirm(`确定要下载《${music.name}》的${typeName}吗?`, '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'info' + }).then(() => { + const url = `/api/file/download/${type}/${music.id}` + window.open(url, '_blank') + ElMessage.success(`${typeName}开始下载...`) + }).catch(() => { + ElMessage.info(`已取消下载${typeName}`) + }) +} + // WebSocket 订阅实例 let commentSubscription = null; @@ -484,14 +532,21 @@ onUnmounted(() => { - + diff --git a/music-qianduan/src/views/SingerDetail.vue b/music-qianduan/src/views/SingerDetail.vue index 3cd7dbb..adc4d36 100644 --- a/music-qianduan/src/views/SingerDetail.vue +++ b/music-qianduan/src/views/SingerDetail.vue @@ -147,7 +147,7 @@ const playAll = () => { // 播放第一首 playerStore.setCurrentMusic({ - ...songs.value[0], + ...songs.value, singer: singer.value.name }) @@ -317,6 +317,52 @@ const deleteComment = async (commentId) => { } } +// 处理下拉菜单命令 +const handleCommand = (command, music) => { + switch (command) { + case 'addToPlaylist': + addToPlaylist(music) + break + case 'playNext': + const currentIndex = playerStore.getCurrentIndex + if (currentIndex !== -1) { + const playlist = [...playerStore.getPlaylist] + const existingIndex = playlist.findIndex(item => item.id === music.id) + if (existingIndex !== -1) { + playlist.splice(existingIndex, 1) + } + playlist.splice(currentIndex + 1, 0, music) + playerStore.$patch({ playlist }) + ElMessage.success('已添加到下一首播放') + } else { + playerStore.addToPlaylist(music) + ElMessage.success('已添加到播放列表') + } + break + case 'downloadMusic': + downloadFile('music', music) + break + case 'downloadLyric': + downloadFile('lyric', music) + break + } +} + +// 下载文件 +const downloadFile = (type, music) => { + const typeName = type === 'music' ? '音乐' : '歌词' + ElMessageBox.confirm(`确定要下载《${music.name}》的${typeName}吗?`, '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'info' + }).then(() => { + const url = `/api/file/download/${type}/${music.id}` + window.open(url, '_blank') + ElMessage.success(`${typeName}开始下载...`) + }).catch(() => { + ElMessage.info(`已取消下载${typeName}`) + }) +} // 格式化时间 @@ -497,14 +543,21 @@ onUnmounted(() => { - + diff --git a/music-qianduan/src/views/user/CollectMusic.vue b/music-qianduan/src/views/user/CollectMusic.vue index 855e144..d5cd4d2 100644 --- a/music-qianduan/src/views/user/CollectMusic.vue +++ b/music-qianduan/src/views/user/CollectMusic.vue @@ -99,6 +99,56 @@ const playAll = () => { ElMessage.success('开始播放全部收藏音乐') } +// 处理下拉菜单命令 +const handleCommand = (command, music) => { + switch (command) { + case 'addToPlaylist': + addToPlaylist(music) + break + case 'playNext': + const currentIndex = playerStore.getCurrentIndex + if (currentIndex !== -1) { + const playlist = [...playerStore.getPlaylist] + const existingIndex = playlist.findIndex(item => item.id === music.id) + if (existingIndex !== -1) { + playlist.splice(existingIndex, 1) + } + playlist.splice(currentIndex + 1, 0, music) + playerStore.$patch({ playlist }) + ElMessage.success('已添加到下一首播放') + } else { + playerStore.addToPlaylist(music) + ElMessage.success('已添加到播放列表') + } + break + case 'downloadMusic': + downloadFile('music', music) + break + case 'downloadLyric': + downloadFile('lyric', music) + break + case 'cancelCollect': + cancelCollect(music.id) + break + } +} + +// 下载文件 +const downloadFile = (type, music) => { + const typeName = type === 'music' ? '音乐' : '歌词' + ElMessageBox.confirm(`确定要下载《${music.name}》的${typeName}吗?`, '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'info' + }).then(() => { + const url = `/api/file/download/${type}/${music.id}` + window.open(url, '_blank') + ElMessage.success(`${typeName}开始下载...`) + }).catch(() => { + ElMessage.info(`已取消下载${typeName}`) + }) +} + // 格式化时间 const formatTime = (seconds) => { const mins = Math.floor(seconds / 60) @@ -167,20 +217,20 @@ onMounted(() => {