feat(user): 为收藏音乐页面添加更多操作功能

- 添加下拉菜单,支持多种操作
- 实现添加到播放列表、下一首播放、下载音乐、下载歌词等功能
- 优化用户交互,增加确认对话框和提示消息
This commit is contained in:
ikmkj
2025-07-26 12:07:45 +08:00
parent 4b4aaedeef
commit 258215a4e6
7 changed files with 310 additions and 34 deletions

View File

@@ -1,12 +1,27 @@
package com.test.musichouduan.controller; package com.test.musichouduan.controller;
import cn.dev33.satoken.annotation.SaCheckLogin; 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.dto.Result;
import com.test.musichouduan.service.FileService; import com.test.musichouduan.service.FileService;
import org.springframework.beans.factory.annotation.Autowired; 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.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; 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 @Autowired
private FileService fileService; private FileService fileService;
@Autowired
private MusicService musicService;
/** /**
* 上传图片 * 上传图片
* *
@@ -68,4 +84,73 @@ public class FileController {
boolean result = fileService.deleteFile(url); boolean result = fileService.deleteFile(url);
return Result.success("删除成功", result); return Result.success("删除成功", result);
} }
/**
* 下载音乐
*
* @param musicId 音乐ID
* @return 响应实体
*/
@GetMapping("/download/music/{musicId}")
public ResponseEntity<Resource> 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<Resource> 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);
}
} }

View File

@@ -161,4 +161,12 @@ public interface MusicService {
* @return 音乐状态DTO * @return 音乐状态DTO
*/ */
MusicStatusDTO getMusicStatus(Long musicId, Long userId); MusicStatusDTO getMusicStatus(Long musicId, Long userId);
/**
* 根据ID查找音乐实体
*
* @param musicId 音乐ID
* @return 音乐实体
*/
Music findMusicById(Integer musicId);
} }

View File

@@ -615,6 +615,11 @@ public class MusicServiceImpl implements MusicService {
return new MusicStatusDTO(isCollected, isLiked); return new MusicStatusDTO(isCollected, isLiked);
} }
@Override
public Music findMusicById(Integer musicId) {
return musicRepository.findById(musicId.longValue()).orElse(null);
}
/** /**
* 将实体转换为DTO * 将实体转换为DTO
* *

View File

@@ -99,8 +99,9 @@
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item command="addToPlaylist">添加到播放列表</el-dropdown-item> <el-dropdown-item command="addToPlaylist">添加到播放列表</el-dropdown-item>
<el-dropdown-item command="playNext">下一首播放</el-dropdown-item> <el-dropdown-item command="playNext">下一首播放</el-dropdown-item>
<el-dropdown-item command="downloadMusic">下载音乐</el-dropdown-item>
<el-dropdown-item command="downloadLyric">下载歌词</el-dropdown-item>
<el-dropdown-item command="share" v-if="false">分享</el-dropdown-item> <el-dropdown-item command="share" v-if="false">分享</el-dropdown-item>
<el-dropdown-item command="download" v-if="false">下载</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
@@ -128,7 +129,7 @@ import { useRouter } from 'vue-router'
import { VideoPlay, VideoPause, Star, More, InfoFilled, Pointer } from '@element-plus/icons-vue' import { VideoPlay, VideoPause, Star, More, InfoFilled, Pointer } from '@element-plus/icons-vue'
import { usePlayerStore } from '../stores/player' import { usePlayerStore } from '../stores/player'
import { toggleCollectMusic, toggleLikeMusic } from '../api/music' import { toggleCollectMusic, toggleLikeMusic } from '../api/music'
import { ElMessage } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
const props = defineProps({ const props = defineProps({
list: { list: {
@@ -258,12 +259,31 @@ const handleCommand = (command, music) => {
case 'share': case 'share':
ElMessage.info('分享功能开发中') ElMessage.info('分享功能开发中')
break break
case 'download': case 'downloadMusic':
ElMessage.info('下载功能开发中') downloadFile('music', music)
break
case 'downloadLyric':
downloadFile('lyric', music)
break 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) => { const formatDuration = (seconds) => {
if (!seconds) return '00:00' if (!seconds) return '00:00'

View File

@@ -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 订阅实例 // WebSocket 订阅实例
let commentSubscription = null; let commentSubscription = null;
@@ -484,14 +532,21 @@ onUnmounted(() => {
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="100" align="center"> <el-table-column label="操作" width="120" align="center">
<template #default="scope"> <template #default="scope">
<el-button <el-dropdown trigger="click" @command="handleCommand($event, scope.row)">
icon="Plus" <el-button circle size="small">
circle <el-icon><More /></el-icon>
size="small" </el-button>
@click.stop="addToPlaylist(scope.row)" <template #dropdown>
></el-button> <el-dropdown-menu>
<el-dropdown-item command="addToPlaylist">添加到播放列表</el-dropdown-item>
<el-dropdown-item command="playNext">下一首播放</el-dropdown-item>
<el-dropdown-item command="downloadMusic">下载音乐</el-dropdown-item>
<el-dropdown-item command="downloadLyric">下载歌词</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>

View File

@@ -147,7 +147,7 @@ const playAll = () => {
// 播放第一首 // 播放第一首
playerStore.setCurrentMusic({ playerStore.setCurrentMusic({
...songs.value[0], ...songs.value,
singer: singer.value.name 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(() => {
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="100" align="center"> <el-table-column label="操作" width="120" align="center">
<template #default="scope"> <template #default="scope">
<el-button <el-dropdown trigger="click" @command="handleCommand($event, scope.row)">
icon="Plus" <el-button circle size="small">
circle <el-icon><More /></el-icon>
size="small" </el-button>
@click.stop="addToPlaylist(scope.row)" <template #dropdown>
></el-button> <el-dropdown-menu>
<el-dropdown-item command="addToPlaylist">添加到播放列表</el-dropdown-item>
<el-dropdown-item command="playNext">下一首播放</el-dropdown-item>
<el-dropdown-item command="downloadMusic">下载音乐</el-dropdown-item>
<el-dropdown-item command="downloadLyric">下载歌词</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>

View File

@@ -99,6 +99,56 @@ const playAll = () => {
ElMessage.success('开始播放全部收藏音乐') 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 formatTime = (seconds) => {
const mins = Math.floor(seconds / 60) const mins = Math.floor(seconds / 60)
@@ -167,20 +217,20 @@ onMounted(() => {
<el-table-column label="操作" width="150" align="center"> <el-table-column label="操作" width="150" align="center">
<template #default="scope"> <template #default="scope">
<el-button <el-dropdown trigger="click" @command="handleCommand($event, scope.row)">
icon="Delete" <el-button circle size="small">
circle <el-icon><More /></el-icon>
size="small" </el-button>
type="danger" <template #dropdown>
@click="cancelCollect(scope.row.id)" <el-dropdown-menu>
></el-button> <el-dropdown-item command="addToPlaylist">添加到播放列表</el-dropdown-item>
<el-dropdown-item command="playNext">下一首播放</el-dropdown-item>
<el-button <el-dropdown-item command="downloadMusic">下载音乐</el-dropdown-item>
icon="Plus" <el-dropdown-item command="downloadLyric">下载歌词</el-dropdown-item>
circle <el-dropdown-item command="cancelCollect" divided>取消收藏</el-dropdown-item>
size="small" </el-dropdown-menu>
@click="addToPlaylist(scope.row)" </template>
></el-button> </el-dropdown>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>