Compare commits
27 Commits
6f0e59cd28
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
395c75d3fb | ||
|
|
aa1eabfeb7 | ||
|
|
86b286453a | ||
|
|
c76b4d9281 | ||
|
|
f5d262f4fc | ||
|
|
f0fa9cf67d | ||
|
|
bfb401cac6 | ||
|
|
c7588d3de8 | ||
|
|
4595cd59e9 | ||
|
|
b29cc352cc | ||
|
|
efccff567a | ||
|
|
f27a4cc7ec | ||
|
|
23a2090644 | ||
|
|
322ab81643 | ||
|
|
8aca3ddf75 | ||
|
|
af9bc8f21e | ||
|
|
1a00afd628 | ||
|
|
e2e5d1a3a4 | ||
|
|
b96a39ff04 | ||
|
|
d116403db4 | ||
|
|
bd5af140fe | ||
|
|
1383c4a7de | ||
|
|
63d71c01dd | ||
|
|
d214ed3d71 | ||
|
|
e46cde6daa | ||
|
|
414d214fa9 | ||
|
|
24eb4ec5d6 |
@@ -104,6 +104,13 @@
|
|||||||
<version>5.8.22</version>
|
<version>5.8.22</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- houbb 敏感词过滤 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.houbb</groupId>
|
||||||
|
<artifactId>sensitive-word</artifactId>
|
||||||
|
<version>0.29.4</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 工具类 -->
|
<!-- 工具类 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import java.time.format.DateTimeFormatter;
|
|||||||
@JsonComponent
|
@JsonComponent
|
||||||
public class JacksonConfig {
|
public class JacksonConfig {
|
||||||
|
|
||||||
|
// 全局默认使用短格式(仅日期),避免影响管理后台等只需日期的场景
|
||||||
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
public static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
|
public static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package com.test.musichouduan.controller;
|
package com.test.musichouduan.controller;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||||
import cn.dev33.satoken.annotation.SaCheckRole;
|
|
||||||
import cn.dev33.satoken.annotation.SaIgnore;
|
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.test.musichouduan.dto.CommentDTO;
|
import com.test.musichouduan.dto.CommentDTO;
|
||||||
import com.test.musichouduan.dto.PageResult;
|
import com.test.musichouduan.dto.PageResult;
|
||||||
import com.test.musichouduan.dto.Result;
|
import com.test.musichouduan.dto.Result;
|
||||||
import com.test.musichouduan.service.CommentService;
|
import com.test.musichouduan.service.CommentService;
|
||||||
|
import com.test.musichouduan.service.MusicService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@@ -21,6 +20,9 @@ public class CommentController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private CommentService commentService;
|
private CommentService commentService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MusicService musicService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据ID获取评论
|
* 根据ID获取评论
|
||||||
*
|
*
|
||||||
@@ -62,6 +64,9 @@ public class CommentController {
|
|||||||
@PostMapping("/add")
|
@PostMapping("/add")
|
||||||
public Result<CommentDTO> addComment(@RequestBody CommentDTO commentDTO) {
|
public Result<CommentDTO> addComment(@RequestBody CommentDTO commentDTO) {
|
||||||
CommentDTO result = commentService.addComment(commentDTO);
|
CommentDTO result = commentService.addComment(commentDTO);
|
||||||
|
if (commentDTO.getTargetType() != null && commentDTO.getTargetType() == 1) {
|
||||||
|
musicService.updateHotMusicCache();
|
||||||
|
}
|
||||||
return Result.success("评论成功", result);
|
return Result.success("评论成功", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +79,11 @@ public class CommentController {
|
|||||||
@SaCheckLogin
|
@SaCheckLogin
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public Result<Boolean> deleteComment(@PathVariable Long id) {
|
public Result<Boolean> deleteComment(@PathVariable Long id) {
|
||||||
|
CommentDTO commentDTO = commentService.getCommentById(id);
|
||||||
boolean result = commentService.deleteComment(id);
|
boolean result = commentService.deleteComment(id);
|
||||||
|
if (commentDTO.getTargetType() != null && commentDTO.getTargetType() == 1) {
|
||||||
|
musicService.updateHotMusicCache();
|
||||||
|
}
|
||||||
return Result.success("删除成功", result);
|
return Result.success("删除成功", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ public class MusicController {
|
|||||||
@RequestPart(value = "cover", required = false) MultipartFile cover,
|
@RequestPart(value = "cover", required = false) MultipartFile cover,
|
||||||
@RequestPart(value = "lyric", required = false) MultipartFile lyric) {
|
@RequestPart(value = "lyric", required = false) MultipartFile lyric) {
|
||||||
MusicDTO result = musicService.updateMusic(id, musicDTO, file, cover, lyric);
|
MusicDTO result = musicService.updateMusic(id, musicDTO, file, cover, lyric);
|
||||||
|
musicService.updateHotMusicCache();
|
||||||
return Result.success("更新成功", result);
|
return Result.success("更新成功", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +196,7 @@ public class MusicController {
|
|||||||
// 获取当前登录用户ID
|
// 获取当前登录用户ID
|
||||||
Long userId = StpUtil.getLoginIdAsLong();
|
Long userId = StpUtil.getLoginIdAsLong();
|
||||||
boolean isCollected = musicService.toggleCollectMusic(id, userId);
|
boolean isCollected = musicService.toggleCollectMusic(id, userId);
|
||||||
|
musicService.updateHotMusicCache();
|
||||||
String message = isCollected ? "收藏成功" : "取消收藏成功";
|
String message = isCollected ? "收藏成功" : "取消收藏成功";
|
||||||
return Result.success(message, isCollected);
|
return Result.success(message, isCollected);
|
||||||
}
|
}
|
||||||
@@ -227,6 +229,9 @@ public class MusicController {
|
|||||||
@PostMapping("/{id}/play")
|
@PostMapping("/{id}/play")
|
||||||
public Result<MusicDTO> playMusic(@PathVariable Long id) {
|
public Result<MusicDTO> playMusic(@PathVariable Long id) {
|
||||||
MusicDTO musicDTO = musicService.playMusic(id);
|
MusicDTO musicDTO = musicService.playMusic(id);
|
||||||
|
if (musicDTO != null) {
|
||||||
|
musicService.updateHotMusicCache();
|
||||||
|
}
|
||||||
return Result.success(musicDTO);
|
return Result.success(musicDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,6 +247,7 @@ public class MusicController {
|
|||||||
// 获取当前登录用户ID
|
// 获取当前登录用户ID
|
||||||
Long userId = StpUtil.getLoginIdAsLong();
|
Long userId = StpUtil.getLoginIdAsLong();
|
||||||
boolean isLiked = musicService.toggleLikeMusic(id, userId);
|
boolean isLiked = musicService.toggleLikeMusic(id, userId);
|
||||||
|
musicService.updateHotMusicCache();
|
||||||
String message = isLiked ? "点赞成功" : "取消点赞成功";
|
String message = isLiked ? "点赞成功" : "取消点赞成功";
|
||||||
return Result.success(message, isLiked);
|
return Result.success(message, isLiked);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,6 +207,27 @@ public class UserController {
|
|||||||
return Result.success("更新成功", userDTO);
|
return Result.success("更新成功", userDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员上传指定用户头像
|
||||||
|
*
|
||||||
|
* @param id 用户ID
|
||||||
|
* @param file 头像文件
|
||||||
|
* @return 响应结果
|
||||||
|
*/
|
||||||
|
@Operation(summary = "管理员上传用户头像", description = "管理员上传指定用户头像并更新用户信息,需要管理员权限")
|
||||||
|
@SaCheckRole("admin")
|
||||||
|
@PostMapping("/{id}/upload/avatar")
|
||||||
|
public Result<String> adminUploadUserAvatar(
|
||||||
|
@Parameter(description = "用户ID", example = "1") @PathVariable Long id,
|
||||||
|
@Parameter(description = "头像文件") @RequestParam("file") MultipartFile file) {
|
||||||
|
String avatarPhysicalPath = fileService.uploadImage(file);
|
||||||
|
String avatarUrl = avatarPhysicalPath.replace(System.getProperty("user.dir") + File.separator + "music-houduan", "").replace(File.separator, "/");
|
||||||
|
|
||||||
|
userService.adminUpdateUserAvatar(id, avatarUrl);
|
||||||
|
|
||||||
|
return Result.success("上传成功", avatarUrl);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传用户头像
|
* 上传用户头像
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -54,6 +54,16 @@ public class MusicDTO {
|
|||||||
*/
|
*/
|
||||||
private Long playCount;
|
private Long playCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点赞数
|
||||||
|
*/
|
||||||
|
private Integer likeCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收藏数
|
||||||
|
*/
|
||||||
|
private Integer collectCount;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 歌手列表
|
* 歌手列表
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -70,6 +70,13 @@ public class SingerDTO {
|
|||||||
*/
|
*/
|
||||||
private Boolean isCollected;
|
private Boolean isCollected;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收藏时间
|
||||||
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
|
||||||
|
private LocalDateTime collectTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建时间
|
* 创建时间
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -36,6 +36,6 @@ public class UserCollectPlaylist {
|
|||||||
* 收藏时间
|
* 收藏时间
|
||||||
*/
|
*/
|
||||||
@CreationTimestamp
|
@CreationTimestamp
|
||||||
@Column(nullable = false, updatable = false)
|
@Column(name = "create_time", nullable = false, updatable = false)
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ public interface MusicRepository extends JpaRepository<Music, Long>, JpaSpecific
|
|||||||
* @param pageable 分页
|
* @param pageable 分页
|
||||||
* @return 音乐列表
|
* @return 音乐列表
|
||||||
*/
|
*/
|
||||||
@Query("SELECT m FROM Music m ORDER BY m.createTime DESC")
|
@Query("SELECT m FROM Music m ORDER BY m.createTime DESC, m.id DESC")
|
||||||
List<Music> findLatest(Pageable pageable);
|
List<Music> findLatest(Pageable pageable);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,7 +87,7 @@ public interface MusicRepository extends JpaRepository<Music, Long>, JpaSpecific
|
|||||||
* @param pageable 分页
|
* @param pageable 分页
|
||||||
* @return 音乐列表
|
* @return 音乐列表
|
||||||
*/
|
*/
|
||||||
@Query("SELECT m FROM Music m ORDER BY m.playCount DESC")
|
@Query("SELECT m FROM Music m ORDER BY m.playCount DESC, m.createTime DESC, m.id DESC")
|
||||||
List<Music> findMostPlayed(Pageable pageable);
|
List<Music> findMostPlayed(Pageable pageable);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,19 +3,15 @@ package com.test.musichouduan.repository;
|
|||||||
import com.test.musichouduan.entity.Playlist;
|
import com.test.musichouduan.entity.Playlist;
|
||||||
import com.test.musichouduan.entity.User;
|
import com.test.musichouduan.entity.User;
|
||||||
import com.test.musichouduan.entity.UserCollectPlaylist;
|
import com.test.musichouduan.entity.UserCollectPlaylist;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.Modifying;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.data.jpa.repository.Modifying;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,7 +85,7 @@ public interface UserCollectPlaylistRepository extends JpaRepository<UserCollect
|
|||||||
|
|
||||||
@Modifying
|
@Modifying
|
||||||
@Transactional
|
@Transactional
|
||||||
@Query(value = "INSERT INTO user_collect_playlist (user_id, playlist_id, collect_time) VALUES (?1, ?2, NOW())", nativeQuery = true)
|
@Query(value = "INSERT INTO user_collect_playlist (user_id, playlist_id, create_time) VALUES (?1, ?2, NOW())", nativeQuery = true)
|
||||||
void collectPlaylist(Long userId, Long playlistId);
|
void collectPlaylist(Long userId, Long playlistId);
|
||||||
|
|
||||||
@Modifying
|
@Modifying
|
||||||
@@ -97,7 +93,10 @@ public interface UserCollectPlaylistRepository extends JpaRepository<UserCollect
|
|||||||
@Query(value = "DELETE FROM user_collect_playlist WHERE user_id = ?1 AND playlist_id = ?2", nativeQuery = true)
|
@Query(value = "DELETE FROM user_collect_playlist WHERE user_id = ?1 AND playlist_id = ?2", nativeQuery = true)
|
||||||
void uncollectPlaylist(Long userId, Long playlistId);
|
void uncollectPlaylist(Long userId, Long playlistId);
|
||||||
|
|
||||||
@Query("SELECT ucp.playlist FROM UserCollectPlaylist ucp WHERE ucp.user.id = ?1")
|
@Query(
|
||||||
|
value = "SELECT ucp.playlist FROM UserCollectPlaylist ucp WHERE ucp.user.id = ?1 ORDER BY ucp.createTime DESC",
|
||||||
|
countQuery = "SELECT COUNT(ucp) FROM UserCollectPlaylist ucp WHERE ucp.user.id = ?1"
|
||||||
|
)
|
||||||
Page<Playlist> findCollectedPlaylist(Long userId, Pageable pageable);
|
Page<Playlist> findCollectedPlaylist(Long userId, Pageable pageable);
|
||||||
|
|
||||||
Optional<UserCollectPlaylist> findByUserIdAndPlaylistId(Long userId, Long playlistId);
|
Optional<UserCollectPlaylist> findByUserIdAndPlaylistId(Long userId, Long playlistId);
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import org.springframework.data.repository.query.Param;
|
|||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户歌手关联数据访问层
|
* 用户歌手关联数据访问层
|
||||||
*/
|
*/
|
||||||
@@ -86,4 +88,13 @@ public interface UserSingerRepository extends JpaRepository<UserSinger, Long> {
|
|||||||
@Transactional
|
@Transactional
|
||||||
@Query("DELETE FROM UserSinger us WHERE us.singer.id = :singerId")
|
@Query("DELETE FROM UserSinger us WHERE us.singer.id = :singerId")
|
||||||
void deleteBySingerId(@Param("singerId") Long singerId);
|
void deleteBySingerId(@Param("singerId") Long singerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID和歌手ID查询收藏记录
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param singerId 歌手ID
|
||||||
|
* @return 收藏记录
|
||||||
|
*/
|
||||||
|
Optional<UserSinger> findByUserIdAndSingerId(Long userId, Long singerId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.test.musichouduan.service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 敏感词服务接口
|
||||||
|
*/
|
||||||
|
public interface SensitiveWordService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验评论内容是否包含敏感词
|
||||||
|
*
|
||||||
|
* @param content 评论内容
|
||||||
|
*/
|
||||||
|
void validateCommentContent(String content);
|
||||||
|
}
|
||||||
@@ -107,6 +107,15 @@ public interface UserService {
|
|||||||
*/
|
*/
|
||||||
UserDTO adminUpdateUser(Long id, UserUpdateRequest request);
|
UserDTO adminUpdateUser(Long id, UserUpdateRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员上传并更新指定用户头像
|
||||||
|
*
|
||||||
|
* @param id 用户ID
|
||||||
|
* @param avatar 头像地址
|
||||||
|
* @return 用户DTO
|
||||||
|
*/
|
||||||
|
UserDTO adminUpdateUserAvatar(Long id, String avatar);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据ID获取用户实体
|
* 根据ID获取用户实体
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.test.musichouduan.repository.PlaylistRepository;
|
|||||||
import com.test.musichouduan.repository.SingerRepository;
|
import com.test.musichouduan.repository.SingerRepository;
|
||||||
import com.test.musichouduan.repository.UserRepository;
|
import com.test.musichouduan.repository.UserRepository;
|
||||||
import com.test.musichouduan.service.CommentService;
|
import com.test.musichouduan.service.CommentService;
|
||||||
|
import com.test.musichouduan.service.SensitiveWordService;
|
||||||
import org.springframework.messaging.simp.SimpMessagingTemplate; // 添加导入
|
import org.springframework.messaging.simp.SimpMessagingTemplate; // 添加导入
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -27,7 +28,9 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import jakarta.persistence.criteria.Predicate;
|
import jakarta.persistence.criteria.Predicate;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -55,6 +58,9 @@ public class CommentServiceImpl implements CommentService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private SimpMessagingTemplate messagingTemplate; // 注入 SimpMessagingTemplate
|
private SimpMessagingTemplate messagingTemplate; // 注入 SimpMessagingTemplate
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SensitiveWordService sensitiveWordService; // 注入敏感词服务
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommentDTO getCommentById(Long id) {
|
public CommentDTO getCommentById(Long id) {
|
||||||
Comment comment = commentRepository.findById(id)
|
Comment comment = commentRepository.findById(id)
|
||||||
@@ -114,6 +120,9 @@ public class CommentServiceImpl implements CommentService {
|
|||||||
User user = userRepository.findById(userId)
|
User user = userRepository.findById(userId)
|
||||||
.orElseThrow(() -> new BusinessException("用户不存在"));
|
.orElseThrow(() -> new BusinessException("用户不存在"));
|
||||||
|
|
||||||
|
// 保存前统一校验评论内容,覆盖音乐/歌单/歌手评论及回复评论
|
||||||
|
sensitiveWordService.validateCommentContent(commentDTO.getContent());
|
||||||
|
|
||||||
// 创建评论实体
|
// 创建评论实体
|
||||||
Comment comment = new Comment();
|
Comment comment = new Comment();
|
||||||
comment.setContent(commentDTO.getContent());
|
comment.setContent(commentDTO.getContent());
|
||||||
@@ -127,33 +136,7 @@ public class CommentServiceImpl implements CommentService {
|
|||||||
Comment savedComment = commentRepository.save(comment);
|
Comment savedComment = commentRepository.save(comment);
|
||||||
|
|
||||||
|
|
||||||
// 如果评论的是音乐 (targetType=1, 这个值需要与前端约定好)
|
updateTargetCommentCount(savedComment.getTargetType(), savedComment.getTargetId());
|
||||||
// 通常可以定义一个枚举或常量类来管理这些类型值
|
|
||||||
if (savedComment.getTargetType() == 1) {
|
|
||||||
musicRepository.findById(savedComment.getTargetId()).ifPresent(music -> {
|
|
||||||
// 首先判断类型是否等于1
|
|
||||||
if (savedComment.getTargetType() == 1) {
|
|
||||||
// 查询评论表,使用COUNT(id)统计该音乐的评论数量
|
|
||||||
long commentCount = commentRepository.countByTargetIdAndTargetType(savedComment.getTargetId(), 1);
|
|
||||||
// 设置到音乐中
|
|
||||||
music.setCommentCount((int) commentCount);
|
|
||||||
// 保存音乐实体
|
|
||||||
musicRepository.save(music);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (savedComment.getTargetType() == 2) { // 2 代表歌单
|
|
||||||
playlistRepository.findById(savedComment.getTargetId()).ifPresent(playlist -> {
|
|
||||||
long commentCount = commentRepository.countByTargetIdAndTargetType(savedComment.getTargetId(), 2);
|
|
||||||
playlist.setCommentCount((int) commentCount);
|
|
||||||
playlistRepository.save(playlist);
|
|
||||||
});
|
|
||||||
} else if (savedComment.getTargetType() == 3) { // 3 代表歌手
|
|
||||||
singerRepository.findById(savedComment.getTargetId()).ifPresent(singer -> {
|
|
||||||
long commentCount = commentRepository.countByTargetIdAndTargetType(savedComment.getTargetId(), 3);
|
|
||||||
singer.setCommentCount((int) commentCount);
|
|
||||||
singerRepository.save(singer);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将新评论转换为DTO
|
// 将新评论转换为DTO
|
||||||
CommentDTO savedCommentDTO = convertToDTO(savedComment);
|
CommentDTO savedCommentDTO = convertToDTO(savedComment);
|
||||||
@@ -185,27 +168,6 @@ public class CommentServiceImpl implements CommentService {
|
|||||||
// 删除评论
|
// 删除评论
|
||||||
commentRepository.delete(comment);
|
commentRepository.delete(comment);
|
||||||
|
|
||||||
// 更新相关实体的评论数
|
|
||||||
if (comment.getTargetType() == 1) {
|
|
||||||
musicRepository.findById(comment.getTargetId()).ifPresent(music -> {
|
|
||||||
long commentCount = commentRepository.countByTargetIdAndTargetType(comment.getTargetId(), 1);
|
|
||||||
music.setCommentCount((int) commentCount);
|
|
||||||
musicRepository.save(music);
|
|
||||||
});
|
|
||||||
} else if (comment.getTargetType() == 2) {
|
|
||||||
playlistRepository.findById(comment.getTargetId()).ifPresent(playlist -> {
|
|
||||||
long commentCount = commentRepository.countByTargetIdAndTargetType(comment.getTargetId(), 2);
|
|
||||||
playlist.setCommentCount((int) commentCount);
|
|
||||||
playlistRepository.save(playlist);
|
|
||||||
});
|
|
||||||
} else if (comment.getTargetType() == 3) {
|
|
||||||
singerRepository.findById(comment.getTargetId()).ifPresent(singer -> {
|
|
||||||
long commentCount = commentRepository.countByTargetIdAndTargetType(comment.getTargetId(), 3);
|
|
||||||
singer.setCommentCount((int) commentCount);
|
|
||||||
singerRepository.save(singer);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是父评论,同时删除子评论
|
// 如果是父评论,同时删除子评论
|
||||||
if (comment.getParentId() == null) {
|
if (comment.getParentId() == null) {
|
||||||
List<Comment> children = commentRepository.findByParentIdOrderByCreateTimeAsc(id);
|
List<Comment> children = commentRepository.findByParentIdOrderByCreateTimeAsc(id);
|
||||||
@@ -214,9 +176,45 @@ public class CommentServiceImpl implements CommentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除完成后再同步评论数,避免父评论删除后子评论残留导致统计偏大
|
||||||
|
updateTargetCommentCount(comment.getTargetType(), comment.getTargetId());
|
||||||
|
|
||||||
|
// 通过 WebSocket 广播删除事件
|
||||||
|
String destination = String.format("/topic/comments/%d/%d", comment.getTargetType(), comment.getTargetId());
|
||||||
|
Map<String, Object> deleteEvent = new HashMap<>();
|
||||||
|
deleteEvent.put("deleted", true);
|
||||||
|
deleteEvent.put("id", id);
|
||||||
|
deleteEvent.put("parentId", comment.getParentId());
|
||||||
|
messagingTemplate.convertAndSend(destination, deleteEvent);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateTargetCommentCount(Integer targetType, Long targetId) {
|
||||||
|
if (targetType == null || targetId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long commentCount = commentRepository.countByTargetIdAndTargetType(targetId, targetType);
|
||||||
|
|
||||||
|
if (targetType == 1) {
|
||||||
|
musicRepository.findById(targetId).ifPresent(music -> {
|
||||||
|
music.setCommentCount((int) commentCount);
|
||||||
|
musicRepository.save(music);
|
||||||
|
});
|
||||||
|
} else if (targetType == 2) {
|
||||||
|
playlistRepository.findById(targetId).ifPresent(playlist -> {
|
||||||
|
playlist.setCommentCount((int) commentCount);
|
||||||
|
playlistRepository.save(playlist);
|
||||||
|
});
|
||||||
|
} else if (targetType == 3) {
|
||||||
|
singerRepository.findById(targetId).ifPresent(singer -> {
|
||||||
|
singer.setCommentCount((int) commentCount);
|
||||||
|
singerRepository.save(singer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageResult<CommentDTO> getUserCommentList(Long userId, Integer page, Integer size) {
|
public PageResult<CommentDTO> getUserCommentList(Long userId, Integer page, Integer size) {
|
||||||
// 获取用户
|
// 获取用户
|
||||||
@@ -336,6 +334,14 @@ public class CommentServiceImpl implements CommentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通过 WebSocket 广播删除事件
|
||||||
|
String destination = String.format("/topic/comments/%d/%d", comment.getTargetType(), comment.getTargetId());
|
||||||
|
Map<String, Object> deleteEvent = new HashMap<>();
|
||||||
|
deleteEvent.put("deleted", true);
|
||||||
|
deleteEvent.put("id", id);
|
||||||
|
deleteEvent.put("parentId", comment.getParentId());
|
||||||
|
messagingTemplate.convertAndSend(destination, deleteEvent);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ public class FileServiceImpl implements FileService {
|
|||||||
// 检查文件类型
|
// 检查文件类型
|
||||||
String originalFilename = file.getOriginalFilename();
|
String originalFilename = file.getOriginalFilename();
|
||||||
String extension = getFileExtension(originalFilename);
|
String extension = getFileExtension(originalFilename);
|
||||||
List<String> imageTypes = Arrays.asList("jpg", "jpeg", "png", "gif");
|
List<String> imageTypes = Arrays.asList("jpg", "jpeg", "png", "gif", "webp");
|
||||||
if (!imageTypes.contains(extension.toLowerCase())) {
|
if (!imageTypes.contains(extension.toLowerCase())) {
|
||||||
throw new BusinessException("只能上传jpg、jpeg、png、gif格式的图片");
|
throw new BusinessException("只能上传jpg、jpeg、png、gif、webp格式的图片");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上传文件
|
// 上传文件
|
||||||
|
|||||||
@@ -243,6 +243,14 @@ public class MusicServiceImpl implements MusicService {
|
|||||||
public CompletableFuture<List<MusicDTO>> updateHotMusicCache() {
|
public CompletableFuture<List<MusicDTO>> updateHotMusicCache() {
|
||||||
// 使用 JOIN FETCH 一次性加载所有音乐及其关联的歌手信息
|
// 使用 JOIN FETCH 一次性加载所有音乐及其关联的歌手信息
|
||||||
List<Music> allMusic = musicRepository.findAllWithSingers();
|
List<Music> allMusic = musicRepository.findAllWithSingers();
|
||||||
|
Comparator<Music> hotMusicComparator = Comparator
|
||||||
|
.comparing((Music music) -> music.getHotScore() != null ? music.getHotScore() : 0D, Comparator.reverseOrder())
|
||||||
|
.thenComparing(music -> music.getPlayCount() != null ? music.getPlayCount() : 0L, Comparator.reverseOrder())
|
||||||
|
.thenComparing(music -> music.getCollectCount() != null ? music.getCollectCount() : 0, Comparator.reverseOrder())
|
||||||
|
.thenComparing(music -> music.getLikeCount() != null ? music.getLikeCount() : 0, Comparator.reverseOrder())
|
||||||
|
.thenComparing(music -> music.getCommentCount() != null ? music.getCommentCount() : 0, Comparator.reverseOrder())
|
||||||
|
.thenComparing(Music::getCreateTime, Comparator.reverseOrder())
|
||||||
|
.thenComparing(Music::getId, Comparator.reverseOrder());
|
||||||
|
|
||||||
// 使用并行流来加速计算过程
|
// 使用并行流来加速计算过程
|
||||||
allMusic.parallelStream().forEach(music -> {
|
allMusic.parallelStream().forEach(music -> {
|
||||||
@@ -252,7 +260,7 @@ public class MusicServiceImpl implements MusicService {
|
|||||||
|
|
||||||
// 对所有音乐按计算出的热度分进行降序排序
|
// 对所有音乐按计算出的热度分进行降序排序
|
||||||
List<MusicDTO> sortedHotMusic = allMusic.stream()
|
List<MusicDTO> sortedHotMusic = allMusic.stream()
|
||||||
.sorted(Comparator.comparing(Music::getHotScore).reversed())
|
.sorted(hotMusicComparator)
|
||||||
.limit(HOT_MUSIC_LIMIT) // 取热度最高的前50
|
.limit(HOT_MUSIC_LIMIT) // 取热度最高的前50
|
||||||
.map(this::convertToDTO)
|
.map(this::convertToDTO)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
@@ -402,7 +410,10 @@ public class MusicServiceImpl implements MusicService {
|
|||||||
// 更新音乐信息
|
// 更新音乐信息
|
||||||
music.setName(musicDTO.getName());
|
music.setName(musicDTO.getName());
|
||||||
music.setAlbum(musicDTO.getAlbum());
|
music.setAlbum(musicDTO.getAlbum());
|
||||||
music.setDuration(musicDTO.getDuration());
|
// 仅当 DTO 中有值时才覆盖时长,避免编辑时被清空
|
||||||
|
if (musicDTO.getDuration() != null) {
|
||||||
|
music.setDuration(musicDTO.getDuration());
|
||||||
|
}
|
||||||
|
|
||||||
// 更新音乐类型
|
// 更新音乐类型
|
||||||
if (musicDTO.getTypeIds() != null) {
|
if (musicDTO.getTypeIds() != null) {
|
||||||
@@ -650,6 +661,10 @@ public class MusicServiceImpl implements MusicService {
|
|||||||
MusicDTO musicDTO = new MusicDTO();
|
MusicDTO musicDTO = new MusicDTO();
|
||||||
BeanUtils.copyProperties(music, musicDTO);
|
BeanUtils.copyProperties(music, musicDTO);
|
||||||
|
|
||||||
|
// 兜底互动计数,避免前端出现 null
|
||||||
|
musicDTO.setLikeCount(music.getLikeCount() != null ? music.getLikeCount() : 0);
|
||||||
|
musicDTO.setCollectCount(music.getCollectCount() != null ? music.getCollectCount() : 0);
|
||||||
|
|
||||||
// 确保专辑字段正确复制
|
// 确保专辑字段正确复制
|
||||||
musicDTO.setAlbum(music.getAlbum());
|
musicDTO.setAlbum(music.getAlbum());
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
|||||||
|
package com.test.musichouduan.service.impl;
|
||||||
|
|
||||||
|
import com.github.houbb.sensitive.word.core.SensitiveWordHelper;
|
||||||
|
import com.test.musichouduan.exception.BusinessException;
|
||||||
|
import com.test.musichouduan.service.SensitiveWordService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 敏感词服务实现
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class SensitiveWordServiceImpl implements SensitiveWordService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateCommentContent(String content) {
|
||||||
|
if (content == null || content.trim().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按官方 README 常见用法,直接使用 SensitiveWordHelper 进行检测。
|
||||||
|
if (SensitiveWordHelper.contains(content)) {
|
||||||
|
List<String> hitWords = SensitiveWordHelper.findAll(content);
|
||||||
|
if (!hitWords.isEmpty()) {
|
||||||
|
throw new BusinessException("评论包含敏感词,请修改后再发布");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -385,9 +385,15 @@ public class SingerServiceImpl implements SingerService {
|
|||||||
PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "collectTime"))
|
PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "collectTime"))
|
||||||
);
|
);
|
||||||
|
|
||||||
// 转换为DTO
|
// 转换为DTO,并填充收藏时间
|
||||||
List<SingerDTO> singerDTOs = singerPage.getContent().stream()
|
List<SingerDTO> singerDTOs = singerPage.getContent().stream()
|
||||||
.map(this::convertToDTO)
|
.map(singer -> {
|
||||||
|
SingerDTO dto = convertToDTO(singer);
|
||||||
|
// 从收藏记录中获取收藏时间
|
||||||
|
userSingerRepository.findByUserIdAndSingerId(userId, singer.getId())
|
||||||
|
.ifPresent(us -> dto.setCollectTime(us.getCollectTime()));
|
||||||
|
return dto;
|
||||||
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
return new PageResult<>(
|
return new PageResult<>(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import cn.dev33.satoken.secure.BCrypt;
|
|||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.test.musichouduan.dto.*;
|
import com.test.musichouduan.dto.*;
|
||||||
import com.test.musichouduan.entity.User;
|
import com.test.musichouduan.entity.User;
|
||||||
|
import com.test.musichouduan.exception.BusinessException;
|
||||||
import com.test.musichouduan.repository.*;
|
import com.test.musichouduan.repository.*;
|
||||||
import com.test.musichouduan.service.UserService;
|
import com.test.musichouduan.service.UserService;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
@@ -49,17 +50,17 @@ public class UserServiceImpl implements UserService {
|
|||||||
public UserDTO register(RegisterRequest request) {
|
public UserDTO register(RegisterRequest request) {
|
||||||
// 检查用户名是否已存在
|
// 检查用户名是否已存在
|
||||||
if (userRepository.existsByUsername(request.getUsername())) {
|
if (userRepository.existsByUsername(request.getUsername())) {
|
||||||
throw new RuntimeException("用户名已存在");
|
throw new BusinessException(400,"用户名已存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查邮箱是否已存在
|
// 检查邮箱是否已存在
|
||||||
if (userRepository.existsByEmail(request.getEmail())) {
|
if (userRepository.existsByEmail(request.getEmail())) {
|
||||||
throw new RuntimeException("邮箱已存在");
|
throw new BusinessException(400,"邮箱已存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查两次密码是否一致
|
// 检查两次密码是否一致
|
||||||
if (!request.getPassword().equals(request.getConfirmPassword())) {
|
if (!request.getPassword().equals(request.getConfirmPassword())) {
|
||||||
throw new RuntimeException("两次密码不一致");
|
throw new BusinessException(400,"两次密码不一致");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建用户
|
// 创建用户
|
||||||
@@ -83,16 +84,16 @@ public class UserServiceImpl implements UserService {
|
|||||||
public LoginResultDTO login(LoginRequest request) {
|
public LoginResultDTO login(LoginRequest request) {
|
||||||
// 根据用户名查找用户
|
// 根据用户名查找用户
|
||||||
User user = userRepository.findByUsername(request.getUsername())
|
User user = userRepository.findByUsername(request.getUsername())
|
||||||
.orElseThrow(() -> new RuntimeException("用户名或密码错误"));
|
.orElseThrow(() -> new BusinessException(400,"用户名或密码错误"));
|
||||||
|
|
||||||
// 检查用户状态
|
// 检查用户状态
|
||||||
if (user.getStatus() == 0) {
|
if (user.getStatus() == 0) {
|
||||||
throw new RuntimeException("账号已被禁用");
|
throw new BusinessException(400,"账号已被禁用");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查密码
|
// 检查密码
|
||||||
if (!BCrypt.checkpw(request.getPassword(), user.getPassword())) {
|
if (!BCrypt.checkpw(request.getPassword(), user.getPassword())) {
|
||||||
throw new RuntimeException("用户名或密码错误");
|
throw new BusinessException(400,"用户名或密码错误");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录
|
// 登录
|
||||||
@@ -126,7 +127,7 @@ public class UserServiceImpl implements UserService {
|
|||||||
|
|
||||||
// 根据ID查找用户
|
// 根据ID查找用户
|
||||||
User user = userRepository.findById(userId)
|
User user = userRepository.findById(userId)
|
||||||
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
.orElseThrow(() -> new BusinessException(400,"用户不存在"));
|
||||||
|
|
||||||
// 返回用户DTO
|
// 返回用户DTO
|
||||||
return getUserStats(userId);
|
return getUserStats(userId);
|
||||||
@@ -136,7 +137,7 @@ public class UserServiceImpl implements UserService {
|
|||||||
public UserDTO getUserById(Long id) {
|
public UserDTO getUserById(Long id) {
|
||||||
// 根据ID查找用户
|
// 根据ID查找用户
|
||||||
User user = userRepository.findById(id)
|
User user = userRepository.findById(id)
|
||||||
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
.orElseThrow(() -> new BusinessException(400,"用户不存在"));
|
||||||
|
|
||||||
// 返回用户DTO
|
// 返回用户DTO
|
||||||
return convertToDTO(user);
|
return convertToDTO(user);
|
||||||
@@ -146,7 +147,7 @@ public class UserServiceImpl implements UserService {
|
|||||||
public UserDTO getUserByUsername(String username) {
|
public UserDTO getUserByUsername(String username) {
|
||||||
// 根据用户名查找用户
|
// 根据用户名查找用户
|
||||||
User user = userRepository.findByUsername(username)
|
User user = userRepository.findByUsername(username)
|
||||||
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
.orElseThrow(() -> new BusinessException(400,"用户不存在"));
|
||||||
|
|
||||||
// 返回用户DTO
|
// 返回用户DTO
|
||||||
return convertToDTO(user);
|
return convertToDTO(user);
|
||||||
@@ -160,7 +161,7 @@ public class UserServiceImpl implements UserService {
|
|||||||
|
|
||||||
// 根据ID查找用户
|
// 根据ID查找用户
|
||||||
User user = userRepository.findById(userId)
|
User user = userRepository.findById(userId)
|
||||||
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
.orElseThrow(() -> new BusinessException(400,"用户不存在"));
|
||||||
|
|
||||||
// 更新用户信息
|
// 更新用户信息
|
||||||
if (request.getNickname() != null) {
|
if (request.getNickname() != null) {
|
||||||
@@ -197,16 +198,16 @@ public class UserServiceImpl implements UserService {
|
|||||||
|
|
||||||
// 根据ID查找用户
|
// 根据ID查找用户
|
||||||
User user = userRepository.findById(userId)
|
User user = userRepository.findById(userId)
|
||||||
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
.orElseThrow(() -> new BusinessException(400,"用户不存在"));
|
||||||
|
|
||||||
// 检查旧密码
|
// 检查旧密码
|
||||||
if (!BCrypt.checkpw(request.getOldPassword(), user.getPassword())) {
|
if (!BCrypt.checkpw(request.getOldPassword(), user.getPassword())) {
|
||||||
throw new RuntimeException("旧密码错误");
|
throw new BusinessException(400,"旧密码错误");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查两次密码是否一致
|
// 检查两次密码是否一致
|
||||||
if (!request.getNewPassword().equals(request.getConfirmPassword())) {
|
if (!request.getNewPassword().equals(request.getConfirmPassword())) {
|
||||||
throw new RuntimeException("两次密码不一致");
|
throw new BusinessException(400,"两次密码不一致");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新密码
|
// 更新密码
|
||||||
@@ -222,7 +223,7 @@ public class UserServiceImpl implements UserService {
|
|||||||
public UserDTO getUserStats(Long userId) {
|
public UserDTO getUserStats(Long userId) {
|
||||||
// 根据ID查找用户
|
// 根据ID查找用户
|
||||||
User user = userRepository.findById(userId)
|
User user = userRepository.findById(userId)
|
||||||
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
.orElseThrow(() -> new BusinessException(400,"用户不存在"));
|
||||||
|
|
||||||
// 转换为DTO
|
// 转换为DTO
|
||||||
UserDTO userDTO = convertToDTO(user);
|
UserDTO userDTO = convertToDTO(user);
|
||||||
@@ -274,7 +275,7 @@ public class UserServiceImpl implements UserService {
|
|||||||
public boolean deleteUser(Long id) {
|
public boolean deleteUser(Long id) {
|
||||||
// 检查用户是否存在
|
// 检查用户是否存在
|
||||||
if (!userRepository.existsById(id)) {
|
if (!userRepository.existsById(id)) {
|
||||||
throw new RuntimeException("用户不存在");
|
throw new BusinessException(400,"用户不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除用户
|
// 删除用户
|
||||||
@@ -288,7 +289,7 @@ public class UserServiceImpl implements UserService {
|
|||||||
public boolean updateUserStatus(Long id, Integer status) {
|
public boolean updateUserStatus(Long id, Integer status) {
|
||||||
// 根据ID查找用户
|
// 根据ID查找用户
|
||||||
User user = userRepository.findById(id)
|
User user = userRepository.findById(id)
|
||||||
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
.orElseThrow(() -> new BusinessException(400,"用户不存在"));
|
||||||
|
|
||||||
// 更新状态
|
// 更新状态
|
||||||
user.setStatus(status);
|
user.setStatus(status);
|
||||||
@@ -329,10 +330,19 @@ public class UserServiceImpl implements UserService {
|
|||||||
return convertToDTO(savedUser);
|
return convertToDTO(savedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public UserDTO adminUpdateUserAvatar(Long id, String avatar) {
|
||||||
|
User user = getById(id);
|
||||||
|
user.setAvatar(avatar);
|
||||||
|
User savedUser = userRepository.save(user);
|
||||||
|
return convertToDTO(savedUser);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public User getById(Long id) {
|
public User getById(Long id) {
|
||||||
return userRepository.findById(id)
|
return userRepository.findById(id)
|
||||||
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
.orElseThrow(() -> new BusinessException(400,"用户不存在"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import java.time.LocalDateTime;
|
|||||||
*/
|
*/
|
||||||
public class HotScoreUtil {
|
public class HotScoreUtil {
|
||||||
|
|
||||||
|
// 播放量采用对数缩放,避免高播放量对热门榜产生碾压
|
||||||
|
private static final double PLAY_WEIGHT = 1.2;
|
||||||
// 定义各种交互的权重
|
// 定义各种交互的权重
|
||||||
private static final double LIKE_WEIGHT = 1.0;
|
private static final double LIKE_WEIGHT = 1.0;
|
||||||
private static final double COLLECT_WEIGHT = 2.0;
|
private static final double COLLECT_WEIGHT = 2.0;
|
||||||
@@ -27,17 +29,24 @@ public class HotScoreUtil {
|
|||||||
public static double calculateHotScore(Music music) {
|
public static double calculateHotScore(Music music) {
|
||||||
// 1. 计算交互权重和
|
// 1. 计算交互权重和
|
||||||
// 如果交互数据为 null,则默认为 0
|
// 如果交互数据为 null,则默认为 0
|
||||||
|
long playCount = music.getPlayCount() != null ? music.getPlayCount() : 0;
|
||||||
long likeCount = music.getLikeCount() != null ? music.getLikeCount() : 0;
|
long likeCount = music.getLikeCount() != null ? music.getLikeCount() : 0;
|
||||||
long collectCount = music.getCollectCount() != null ? music.getCollectCount() : 0;
|
long collectCount = music.getCollectCount() != null ? music.getCollectCount() : 0;
|
||||||
long commentCount = music.getCommentCount() != null ? music.getCommentCount() : 0;
|
long commentCount = music.getCommentCount() != null ? music.getCommentCount() : 0;
|
||||||
|
|
||||||
double interactionScore = (likeCount * LIKE_WEIGHT) +
|
double playScore = Math.log1p(playCount) * PLAY_WEIGHT;
|
||||||
|
|
||||||
|
double interactionScore = playScore +
|
||||||
|
(likeCount * LIKE_WEIGHT) +
|
||||||
(collectCount * COLLECT_WEIGHT) +
|
(collectCount * COLLECT_WEIGHT) +
|
||||||
(commentCount * COMMENT_WEIGHT);
|
(commentCount * COMMENT_WEIGHT);
|
||||||
|
|
||||||
// 2. 计算时间衰减因子
|
// 2. 计算时间衰减因子
|
||||||
// (当前时间 - 创建时间)的小时数
|
// (当前时间 - 创建时间)的小时数
|
||||||
LocalDateTime createTime = music.getCreateTime();
|
LocalDateTime createTime = music.getCreateTime();
|
||||||
|
if (createTime == null) {
|
||||||
|
createTime = LocalDateTime.now();
|
||||||
|
}
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
long hoursElapsed = Duration.between(createTime, now).toHours();
|
long hoursElapsed = Duration.between(createTime, now).toHours();
|
||||||
|
|
||||||
|
|||||||
3
music-houduan/src/main/resources/sensitive-words.txt
Normal file
3
music-houduan/src/main/resources/sensitive-words.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# 敏感词词库(每行一个,支持后续自行维护)
|
||||||
|
# 示例词条:
|
||||||
|
# 坏词示例
|
||||||
@@ -67,3 +67,20 @@ export function updateUser(data) {
|
|||||||
export function adminUpdateUser(id, data) {
|
export function adminUpdateUser(id, data) {
|
||||||
return api.put(`/user/${id}`, data)
|
return api.put(`/user/${id}`, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员上传指定用户头像
|
||||||
|
* @param {number} id 用户ID
|
||||||
|
* @param {File} file 头像文件
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
export function adminUploadUserAvatar(id, file) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
return api.post(`/user/${id}/upload/avatar`, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -89,7 +89,12 @@ api.interceptors.response.use(
|
|||||||
// 其他错误
|
// 其他错误
|
||||||
ElMessage.error(res.message || '未知错误')
|
ElMessage.error(res.message || '未知错误')
|
||||||
}
|
}
|
||||||
return Promise.reject(new Error(res.message || '未知错误'))
|
const businessError = new Error(res.message || '未知错误')
|
||||||
|
businessError.code = res.code
|
||||||
|
businessError.message = res.message || '未知错误'
|
||||||
|
// 标记为已提示,避免页面 catch 再次弹窗
|
||||||
|
businessError.__handled = true
|
||||||
|
return Promise.reject(businessError)
|
||||||
} else {
|
} else {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -111,11 +116,16 @@ api.interceptors.response.use(
|
|||||||
// 处理网络错误
|
// 处理网络错误
|
||||||
if (error.message && error.message.includes('timeout')) {
|
if (error.message && error.message.includes('timeout')) {
|
||||||
ElMessage.error('请求超时,请稍后再试')
|
ElMessage.error('请求超时,请稍后再试')
|
||||||
|
error.message = '请求超时,请稍后再试'
|
||||||
} else if (error.message && error.message.includes('Network Error')) {
|
} else if (error.message && error.message.includes('Network Error')) {
|
||||||
ElMessage.error('网络错误,请检查网络连接')
|
ElMessage.error('网络错误,请检查网络连接')
|
||||||
|
error.message = '网络错误,请检查网络连接'
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(error.message || '未知错误')
|
ElMessage.error(error.message || '未知错误')
|
||||||
|
error.message = error.message || '未知错误'
|
||||||
}
|
}
|
||||||
|
// 标记为已提示,避免页面 catch 再次弹窗
|
||||||
|
error.__handled = true
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function getCurrentUser() {
|
|||||||
* @returns {Promise} 请求结果
|
* @returns {Promise} 请求结果
|
||||||
*/
|
*/
|
||||||
export function getUserInfo() {
|
export function getUserInfo() {
|
||||||
return api.get('/user/profile')
|
return api.get('/user/current')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -508,7 +508,11 @@ const toggleLike = async () => {
|
|||||||
// 播放列表相关
|
// 播放列表相关
|
||||||
const togglePlaylist = () => playerStore.setShowPlaylist(!showPlaylist.value)
|
const togglePlaylist = () => playerStore.setShowPlaylist(!showPlaylist.value)
|
||||||
const playFromPlaylist = (index) => playerStore.playFromPlaylist(index)
|
const playFromPlaylist = (index) => playerStore.playFromPlaylist(index)
|
||||||
const removeFromPlaylist = (index) => playerStore.removeFromPlaylist(index)
|
const removeFromPlaylist = (index) => {
|
||||||
|
const music = playlist.value[index]
|
||||||
|
if (!music) return
|
||||||
|
playerStore.removeFromPlaylist(music.id)
|
||||||
|
}
|
||||||
const clearPlaylist = () => playerStore.clearPlaylist()
|
const clearPlaylist = () => playerStore.clearPlaylist()
|
||||||
|
|
||||||
// 展开/收起播放器
|
// 展开/收起播放器
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ref, computed, watch } from 'vue'
|
|||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
import { usePlayerStore } from '../stores/player'
|
import { usePlayerStore } from '../stores/player'
|
||||||
import { Search } from '@element-plus/icons-vue'
|
import { Search, ArrowDown } from '@element-plus/icons-vue'
|
||||||
import MusicPlayer from '../components/MusicPlayer.vue'
|
import MusicPlayer from '../components/MusicPlayer.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -173,8 +173,17 @@ export const usePlayerStore = defineStore('player', {
|
|||||||
|
|
||||||
// 清空播放列表
|
// 清空播放列表
|
||||||
clearPlaylist() {
|
clearPlaylist() {
|
||||||
|
if (this.audio) {
|
||||||
|
this.audio.pause()
|
||||||
|
this.audio.src = ''
|
||||||
|
}
|
||||||
|
|
||||||
this.playlist = []
|
this.playlist = []
|
||||||
this.setCurrentMusic(null)
|
this.currentMusic = null
|
||||||
|
this.isPlaying = false
|
||||||
|
this.currentTime = 0
|
||||||
|
this.duration = 0
|
||||||
|
this.currentIndex = -1
|
||||||
},
|
},
|
||||||
|
|
||||||
// 播放上一首
|
// 播放上一首
|
||||||
|
|||||||
16
music-qianduan/src/utils/errorHandler.js
Normal file
16
music-qianduan/src/utils/errorHandler.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一错误提示入口,避免拦截器和页面重复弹窗
|
||||||
|
* @param {Error|Object|string} error 错误对象
|
||||||
|
* @param {string} fallbackMsg 兜底提示文案
|
||||||
|
*/
|
||||||
|
export const showErrorOnce = (error, fallbackMsg = '操作失败,请稍后重试') => {
|
||||||
|
// 已在请求拦截器中提示过,则直接跳过
|
||||||
|
if (error && error.__handled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = error?.message || fallbackMsg
|
||||||
|
ElMessage.error(message)
|
||||||
|
}
|
||||||
@@ -13,6 +13,14 @@ import { processImageUrl } from '../utils/imageUtils'
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const playerStore = usePlayerStore()
|
const playerStore = usePlayerStore()
|
||||||
|
|
||||||
|
const extractList = (data) => {
|
||||||
|
if (Array.isArray(data)) return data
|
||||||
|
if (Array.isArray(data?.list)) return data.list
|
||||||
|
if (Array.isArray(data?.records)) return data.records
|
||||||
|
if (Array.isArray(data?.items)) return data.items
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
// 新歌推荐
|
// 新歌推荐
|
||||||
const newSongs = ref([])
|
const newSongs = ref([])
|
||||||
const loadingNewSongs = ref(false)
|
const loadingNewSongs = ref(false)
|
||||||
@@ -35,7 +43,7 @@ const fetchLatestMusic = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await getLatestMusic(5) // 获取5首最新音乐
|
const res = await getLatestMusic(5) // 获取5首最新音乐
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
newSongs.value = res.data.map(item => ({
|
newSongs.value = extractList(res.data).map(item => ({
|
||||||
...item,
|
...item,
|
||||||
cover: processMusicUrl(item.cover),
|
cover: processMusicUrl(item.cover),
|
||||||
url: processMusicUrl(item.url)
|
url: processMusicUrl(item.url)
|
||||||
@@ -59,7 +67,7 @@ const fetchHotMusic = async () => {
|
|||||||
// 注意:这里调用的是新的 hot-music API
|
// 注意:这里调用的是新的 hot-music API
|
||||||
const res = await getHotMusic(5) // 获取5首热门音乐
|
const res = await getHotMusic(5) // 获取5首热门音乐
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
hotSongs.value = res.data.map(item => ({
|
hotSongs.value = extractList(res.data).map(item => ({
|
||||||
...item,
|
...item,
|
||||||
cover: processMusicUrl(item.cover),
|
cover: processMusicUrl(item.cover),
|
||||||
url: processMusicUrl(item.url)
|
url: processMusicUrl(item.url)
|
||||||
@@ -81,7 +89,7 @@ const fetchLatestPlaylist = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await getLatestPlaylist(5) // 获取5个最新歌单
|
const res = await getLatestPlaylist(5) // 获取5个最新歌单
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
newPlaylists.value = res.data
|
newPlaylists.value = extractList(res.data)
|
||||||
console.log('最新歌单数据:', newPlaylists.value)
|
console.log('最新歌单数据:', newPlaylists.value)
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || '获取最新歌单失败')
|
ElMessage.error(res.message || '获取最新歌单失败')
|
||||||
@@ -100,7 +108,7 @@ const fetchLatestSinger = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await getLatestSinger(5) // 获取5个最新歌手
|
const res = await getLatestSinger(5) // 获取5个最新歌手
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
newSingers.value = res.data.map(item => ({
|
newSingers.value = extractList(res.data).map(item => ({
|
||||||
...item,
|
...item,
|
||||||
avatar: processImageUrl(item.avatar)
|
avatar: processImageUrl(item.avatar)
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -81,7 +81,9 @@ const handleLogin = () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录失败:', error)
|
console.error('登录失败:', error)
|
||||||
ElMessage.error(error.message || '登录失败,请检查用户名和密码')
|
if (!error.__handled) {
|
||||||
|
ElMessage.error(error.message || '登录失败,请检查用户名和密码')
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { Search } from '@element-plus/icons-vue'
|
import { Search } from '@element-plus/icons-vue'
|
||||||
import { getMusicList, getHotMusic, getLatestMusic, processMusicUrl, getMusicStatus } from '../api/music'
|
import { getMusicList, getHotMusic, getLatestMusic, processMusicUrl, getMusicStatus } from '../api/music'
|
||||||
@@ -11,6 +11,22 @@ import { useUserStore } from '../stores/user'
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const extractList = (data) => {
|
||||||
|
if (Array.isArray(data)) return data
|
||||||
|
if (Array.isArray(data?.list)) return data.list
|
||||||
|
if (Array.isArray(data?.records)) return data.records
|
||||||
|
if (Array.isArray(data?.items)) return data.items
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractPageData = (data) => {
|
||||||
|
const list = extractList(data)
|
||||||
|
return {
|
||||||
|
list,
|
||||||
|
total: Number.isFinite(Number(data?.total)) ? Number(data.total) : list.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 音乐列表数据
|
// 音乐列表数据
|
||||||
const musicList = ref([])
|
const musicList = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -25,11 +41,91 @@ const musicTypes = ref([])
|
|||||||
|
|
||||||
// 标签页
|
// 标签页
|
||||||
const activeTab = ref('all')
|
const activeTab = ref('all')
|
||||||
|
const hotMusicSource = ref([])
|
||||||
const hotMusicList = ref([])
|
const hotMusicList = ref([])
|
||||||
|
const latestMusicSource = ref([])
|
||||||
const latestMusicList = ref([])
|
const latestMusicList = ref([])
|
||||||
const loadingHot = ref(false)
|
const loadingHot = ref(false)
|
||||||
const loadingLatest = ref(false)
|
const loadingLatest = ref(false)
|
||||||
|
|
||||||
|
const getMusicTypeId = (music) => {
|
||||||
|
return music?.typeId ?? music?.musicTypeId ?? music?.type?.id ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeTypeToken = (value) => {
|
||||||
|
if (value == null || value === '') return []
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.flatMap(item => normalizeTypeToken(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return normalizeTypeToken(value.id ?? value.typeId ?? value.name ?? value.typeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value
|
||||||
|
.split(/[,,、|/]/)
|
||||||
|
.map(item => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [String(value)]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMusicTypeTokens = (music) => {
|
||||||
|
const sources = [
|
||||||
|
music?.typeId,
|
||||||
|
music?.musicTypeId,
|
||||||
|
music?.type?.id,
|
||||||
|
music?.typeName,
|
||||||
|
music?.type?.name,
|
||||||
|
music?.types,
|
||||||
|
music?.typeList,
|
||||||
|
music?.typeIds,
|
||||||
|
music?.typeNames,
|
||||||
|
music?.tags,
|
||||||
|
music?.labels
|
||||||
|
]
|
||||||
|
|
||||||
|
return sources.flatMap(source => normalizeTypeToken(source))
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyMusicFilter = (list = []) => {
|
||||||
|
const normalizedKeyword = keyword.value.trim().toLowerCase()
|
||||||
|
const selectedTypeItem = musicTypes.value.find(item => item.id === selectedType.value)
|
||||||
|
const selectedTypeName = selectedTypeItem?.name || ''
|
||||||
|
|
||||||
|
return list.filter((music) => {
|
||||||
|
const musicTypeId = getMusicTypeId(music)
|
||||||
|
const musicTypeName = music?.typeName || music?.type?.name || ''
|
||||||
|
const typeTokens = getMusicTypeTokens(music)
|
||||||
|
|
||||||
|
const matchType = selectedType.value == null
|
||||||
|
|| (musicTypeId != null && Number(musicTypeId) === Number(selectedType.value))
|
||||||
|
|| typeTokens.some(token => Number(token) === Number(selectedType.value))
|
||||||
|
|| (selectedTypeName && typeTokens.includes(selectedTypeName))
|
||||||
|
|
||||||
|
if (!normalizedKeyword) {
|
||||||
|
return matchType
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchText = [
|
||||||
|
music.name,
|
||||||
|
musicTypeName,
|
||||||
|
...typeTokens,
|
||||||
|
music.album,
|
||||||
|
music.singer,
|
||||||
|
...(Array.isArray(music.singerNames) ? music.singerNames : [])
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
.toLowerCase()
|
||||||
|
|
||||||
|
return matchType && searchText.includes(normalizedKeyword)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 获取音乐列表
|
// 获取音乐列表
|
||||||
const fetchMusicList = async (params = {}) => {
|
const fetchMusicList = async (params = {}) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -39,11 +135,9 @@ const fetchMusicList = async (params = {}) => {
|
|||||||
|
|
||||||
const res = await getMusicList(page, size, keyword.value, selectedType.value)
|
const res = await getMusicList(page, size, keyword.value, selectedType.value)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
// 兼容不同的数据结构,后端可能返回 list 或 records 字段
|
const pageData = extractPageData(res.data)
|
||||||
musicList.value = res.data.list || res.data.records || []
|
musicList.value = pageData.list
|
||||||
total.value = res.data.total || 0
|
total.value = pageData.total
|
||||||
|
|
||||||
console.log('音乐列表数据:', res.data)
|
|
||||||
|
|
||||||
// 确保音乐URL和封面URL正确处理
|
// 确保音乐URL和封面URL正确处理
|
||||||
musicList.value.forEach(music => {
|
musicList.value.forEach(music => {
|
||||||
@@ -68,12 +162,30 @@ const fetchMusicList = async (params = {}) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateHotMusicView = (params = {}) => {
|
||||||
|
const filteredList = applyMusicFilter(hotMusicSource.value)
|
||||||
|
const page = params.page || currentPage.value
|
||||||
|
const size = params.size || pageSize.value
|
||||||
|
|
||||||
|
total.value = filteredList.length
|
||||||
|
const startIndex = (page - 1) * size
|
||||||
|
const endIndex = startIndex + size
|
||||||
|
hotMusicList.value = filteredList.slice(startIndex, endIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateLatestMusicView = () => {
|
||||||
|
latestMusicList.value = applyMusicFilter(latestMusicSource.value)
|
||||||
|
}
|
||||||
|
|
||||||
// 获取音乐类型列表
|
// 获取音乐类型列表
|
||||||
const fetchMusicTypes = async () => {
|
const fetchMusicTypes = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getMusicTypeList()
|
const res = await getMusicTypeList()
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
musicTypes.value = res.data || []
|
musicTypes.value = [
|
||||||
|
{ id: null, name: '全部' },
|
||||||
|
...(res.data || [])
|
||||||
|
]
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取音乐类型失败:', error)
|
console.error('获取音乐类型失败:', error)
|
||||||
@@ -83,12 +195,51 @@ const fetchMusicTypes = async () => {
|
|||||||
// 处理搜索
|
// 处理搜索
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
|
|
||||||
|
if (activeTab.value === 'hot') {
|
||||||
|
updateHotMusicView({ page: 1, size: pageSize.value })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTab.value === 'latest') {
|
||||||
|
updateLatestMusicView()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchMusicList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeywordClear = () => {
|
||||||
|
keyword.value = ''
|
||||||
|
currentPage.value = 1
|
||||||
|
|
||||||
|
if (activeTab.value === 'hot') {
|
||||||
|
updateHotMusicView({ page: 1, size: pageSize.value })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTab.value === 'latest') {
|
||||||
|
updateLatestMusicView()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
fetchMusicList()
|
fetchMusicList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理类型变化
|
// 处理类型变化
|
||||||
const handleTypeChange = () => {
|
const handleTypeChange = () => {
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
|
|
||||||
|
if (activeTab.value === 'hot') {
|
||||||
|
updateHotMusicView({ page: 1, size: pageSize.value })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTab.value === 'latest') {
|
||||||
|
updateLatestMusicView()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
fetchMusicList()
|
fetchMusicList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,25 +277,19 @@ const fetchHotMusic = async (params = {}) => {
|
|||||||
// 从缓存中获取全部50条热门音乐
|
// 从缓存中获取全部50条热门音乐
|
||||||
const res = await getHotMusic(50)
|
const res = await getHotMusic(50)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
const allHotMusic = res.data || []
|
hotMusicSource.value = extractList(res.data)
|
||||||
total.value = allHotMusic.length // 总数是50
|
|
||||||
|
|
||||||
// 处理URL
|
// 处理URL
|
||||||
allHotMusic.forEach(music => {
|
hotMusicSource.value.forEach(music => {
|
||||||
if (music.url) music.url = processMusicUrl(music.url)
|
if (music.url) music.url = processMusicUrl(music.url)
|
||||||
if (music.cover) music.cover = processMusicUrl(music.cover)
|
if (music.cover) music.cover = processMusicUrl(music.cover)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 更新状态
|
// 更新状态
|
||||||
await updateMusicStatuses(allHotMusic)
|
await updateMusicStatuses(hotMusicSource.value)
|
||||||
|
|
||||||
// 根据当前页码和每页数量进行前端分页
|
|
||||||
const page = params.page || currentPage.value
|
|
||||||
const size = params.size || pageSize.value
|
|
||||||
const startIndex = (page - 1) * size
|
|
||||||
const endIndex = startIndex + size
|
|
||||||
hotMusicList.value = allHotMusic.slice(startIndex, endIndex)
|
|
||||||
|
|
||||||
|
// 根据筛选和分页展示
|
||||||
|
updateHotMusicView(params)
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || '获取热门音乐失败')
|
ElMessage.error(res.message || '获取热门音乐失败')
|
||||||
}
|
}
|
||||||
@@ -162,10 +307,10 @@ const fetchLatestMusic = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await getLatestMusic(20) // 获取20首最新音乐
|
const res = await getLatestMusic(20) // 获取20首最新音乐
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
latestMusicList.value = res.data || []
|
latestMusicSource.value = extractList(res.data)
|
||||||
|
|
||||||
// 处理音乐URL和封面URL
|
// 处理音乐URL和封面URL
|
||||||
latestMusicList.value.forEach(music => {
|
latestMusicSource.value.forEach(music => {
|
||||||
if (music.url) {
|
if (music.url) {
|
||||||
music.url = processMusicUrl(music.url)
|
music.url = processMusicUrl(music.url)
|
||||||
}
|
}
|
||||||
@@ -175,7 +320,8 @@ const fetchLatestMusic = async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 更新状态
|
// 更新状态
|
||||||
updateMusicStatuses(latestMusicList.value)
|
await updateMusicStatuses(latestMusicSource.value)
|
||||||
|
updateLatestMusicView()
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || '获取最新音乐失败')
|
ElMessage.error(res.message || '获取最新音乐失败')
|
||||||
}
|
}
|
||||||
@@ -189,16 +335,34 @@ const fetchLatestMusic = async () => {
|
|||||||
|
|
||||||
// 切换标签页
|
// 切换标签页
|
||||||
const handleTabChange = (tab) => {
|
const handleTabChange = (tab) => {
|
||||||
currentPage.value = 1; // 重置页码
|
currentPage.value = 1 // 重置页码
|
||||||
if (tab.paneName === 'hot') {
|
if (tab.paneName === 'hot') {
|
||||||
fetchHotMusic();
|
fetchHotMusic({ page: 1, size: pageSize.value })
|
||||||
} else if (tab.paneName === 'latest') {
|
} else if (tab.paneName === 'latest') {
|
||||||
fetchLatestMusic();
|
fetchLatestMusic()
|
||||||
} else {
|
} else {
|
||||||
fetchMusicList();
|
fetchMusicList({ page: 1, size: pageSize.value })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(keyword, (val) => {
|
||||||
|
if (val === '') {
|
||||||
|
currentPage.value = 1
|
||||||
|
|
||||||
|
if (activeTab.value === 'hot') {
|
||||||
|
updateHotMusicView({ page: 1, size: pageSize.value })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTab.value === 'latest') {
|
||||||
|
updateLatestMusicView()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchMusicList({ page: 1, size: pageSize.value })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchMusicList()
|
fetchMusicList()
|
||||||
@@ -224,6 +388,7 @@ onMounted(() => {
|
|||||||
v-model="keyword"
|
v-model="keyword"
|
||||||
placeholder="搜索音乐"
|
placeholder="搜索音乐"
|
||||||
clearable
|
clearable
|
||||||
|
@clear="handleKeywordClear"
|
||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
>
|
>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
@@ -255,7 +420,7 @@ onMounted(() => {
|
|||||||
:total="total"
|
:total="total"
|
||||||
v-model:current-page="currentPage"
|
v-model:current-page="currentPage"
|
||||||
v-model:page-size="pageSize"
|
v-model:page-size="pageSize"
|
||||||
@page-change="fetchHotMusic"
|
@page-change="updateHotMusicView"
|
||||||
@collect-change="handleCollectChange"
|
@collect-change="handleCollectChange"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
<span class="label">发行日期:</span>
|
<span class="label">发行日期:</span>
|
||||||
<span class="value">{{ music.releaseDate || '-' }}</span>
|
<span class="value">{{ music.releaseDate || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="music-actions">
|
<div class="music-actions">
|
||||||
@@ -68,9 +69,9 @@
|
|||||||
<el-button
|
<el-button
|
||||||
:type="music.collected ? 'danger' : 'default'"
|
:type="music.collected ? 'danger' : 'default'"
|
||||||
@click="toggleCollect"
|
@click="toggleCollect"
|
||||||
:icon="music.collected ? 'Star' : 'StarFilled'"
|
:icon="music.collected ? 'StarFilled' : 'Star'"
|
||||||
>
|
>
|
||||||
{{ music.collected ? '已收藏' : '收藏' }}
|
{{ music.collected ? `已收藏 (${music.collectCount ?? 0})` : `收藏 (${music.collectCount ?? 0})` }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<el-button
|
<el-button
|
||||||
@@ -78,7 +79,7 @@
|
|||||||
@click="toggleLike"
|
@click="toggleLike"
|
||||||
>
|
>
|
||||||
<el-icon><component :is="music.liked ? 'Check' : 'Pointer'" /></el-icon>
|
<el-icon><component :is="music.liked ? 'Check' : 'Pointer'" /></el-icon>
|
||||||
{{ music.liked ? '已点赞' : '点赞' }}
|
{{ music.liked ? `已点赞 (${music.likeCount ?? 0})` : `点赞 (${music.likeCount ?? 0})` }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<el-button
|
<el-button
|
||||||
@@ -262,6 +263,7 @@ import { connect, subscribe, unsubscribe, disconnect } from '../utils/websocket'
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { VideoPlay, VideoPause, Star, StarFilled, Plus } from '@element-plus/icons-vue'
|
import { VideoPlay, VideoPause, Star, StarFilled, Plus } from '@element-plus/icons-vue'
|
||||||
import { processImageUrl } from '../utils/imageUtils'
|
import { processImageUrl } from '../utils/imageUtils'
|
||||||
|
import { showErrorOnce } from '../utils/errorHandler'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -321,7 +323,11 @@ const fetchMusicDetail = async () => {
|
|||||||
const res = await getMusicById(id)
|
const res = await getMusicById(id)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
music.value = res.data
|
music.value = {
|
||||||
|
...res.data,
|
||||||
|
likeCount: res.data.likeCount ?? 0,
|
||||||
|
collectCount: res.data.collectCount ?? 0
|
||||||
|
}
|
||||||
// 获取音乐状态
|
// 获取音乐状态
|
||||||
fetchMusicStatus()
|
fetchMusicStatus()
|
||||||
} else {
|
} else {
|
||||||
@@ -420,7 +426,7 @@ const submitComment = async (isReply = false) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('提交评论失败:', error);
|
console.error('提交评论失败:', error);
|
||||||
ElMessage.error('评论失败,请稍后重试');
|
showErrorOnce(error, '评论失败,请稍后重试');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -444,7 +450,7 @@ const deleteComment = async (commentId) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
if (error !== 'cancel') {
|
||||||
console.error('删除评论失败:', error);
|
console.error('删除评论失败:', error);
|
||||||
ElMessage.error('删除失败,请稍后重试');
|
showErrorOnce(error, '删除失败,请稍后重试');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -484,7 +490,25 @@ const setupWebSocket = () => {
|
|||||||
if (!music.value) return;
|
if (!music.value) return;
|
||||||
connect(() => {
|
connect(() => {
|
||||||
const destination = `/topic/comments/1/${music.value.id}`; // 1 代表音乐类型
|
const destination = `/topic/comments/1/${music.value.id}`; // 1 代表音乐类型
|
||||||
commentSubscription = subscribe(destination, (newComment) => {
|
commentSubscription = subscribe(destination, (message) => {
|
||||||
|
// 处理删除事件
|
||||||
|
if (message && message.deleted) {
|
||||||
|
const deletedId = message.id;
|
||||||
|
if (message.parentId) {
|
||||||
|
// 删除子评论
|
||||||
|
const parentComment = comments.value.find(c => c.id === message.parentId);
|
||||||
|
if (parentComment && parentComment.children) {
|
||||||
|
parentComment.children = parentComment.children.filter(c => c.id !== deletedId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 删除父评论
|
||||||
|
comments.value = comments.value.filter(c => c.id !== deletedId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理新增评论
|
||||||
|
const newComment = message;
|
||||||
if (newComment && newComment.id) {
|
if (newComment && newComment.id) {
|
||||||
if (newComment.user && newComment.user.avatar) {
|
if (newComment.user && newComment.user.avatar) {
|
||||||
newComment.user.avatar = processImageUrl(newComment.user.avatar);
|
newComment.user.avatar = processImageUrl(newComment.user.avatar);
|
||||||
@@ -543,13 +567,19 @@ const toggleCollect = async () => {
|
|||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
// 后端直接返回了最新的收藏状态 (true/false),直接赋值即可
|
// 后端直接返回了最新的收藏状态 (true/false),直接赋值即可
|
||||||
music.value.collected = res.data
|
music.value.collected = res.data
|
||||||
|
// 仅在操作成功时同步更新本地收藏数
|
||||||
|
if (music.value.collected) {
|
||||||
|
music.value.collectCount = (music.value.collectCount || 0) + 1
|
||||||
|
} else {
|
||||||
|
music.value.collectCount = Math.max(0, (music.value.collectCount || 0) - 1)
|
||||||
|
}
|
||||||
ElMessage.success(res.message || (music.value.collected ? '收藏成功' : '已取消收藏'))
|
ElMessage.success(res.message || (music.value.collected ? '收藏成功' : '已取消收藏'))
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || '操作失败')
|
ElMessage.error(res.message || '操作失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('收藏操作失败:', error)
|
console.error('收藏操作失败:', error)
|
||||||
ElMessage.error('操作失败,请稍后重试')
|
showErrorOnce(error, '操作失败,请稍后重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,7 +602,7 @@ const toggleLike = async () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('点赞操作失败:', error);
|
console.error('点赞操作失败:', error);
|
||||||
ElMessage.error('操作失败,请稍后重试');
|
showErrorOnce(error, '操作失败,请稍后重试');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { showErrorOnce } from '../utils/errorHandler'
|
||||||
import { Search, Headset } from '@element-plus/icons-vue'
|
import { Search, Headset } from '@element-plus/icons-vue'
|
||||||
import { getPlaylistList, getHotPlaylist, toggleCollectPlaylist, isPlaylistCollected, uncollectPlaylist, collectPlaylist } from '../api/playlist'
|
import { getPlaylistList, getHotPlaylist, toggleCollectPlaylist, isPlaylistCollected, uncollectPlaylist, collectPlaylist } from '../api/playlist'
|
||||||
import { getPlaylistTypeList } from '../api/playlistType'
|
import { getPlaylistTypeList } from '../api/playlistType'
|
||||||
@@ -10,6 +11,22 @@ import { getPlaylistTypeList } from '../api/playlistType'
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const extractList = (data) => {
|
||||||
|
if (Array.isArray(data)) return data
|
||||||
|
if (Array.isArray(data?.list)) return data.list
|
||||||
|
if (Array.isArray(data?.records)) return data.records
|
||||||
|
if (Array.isArray(data?.items)) return data.items
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractPageData = (data) => {
|
||||||
|
const list = extractList(data)
|
||||||
|
return {
|
||||||
|
list,
|
||||||
|
total: Number.isFinite(Number(data?.total)) ? Number(data.total) : list.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 歌单类型列表
|
// 歌单类型列表
|
||||||
const playlistTypes = ref([
|
const playlistTypes = ref([
|
||||||
{ id: null, name: '全部' }
|
{ id: null, name: '全部' }
|
||||||
@@ -88,8 +105,9 @@ const fetchPlaylists = async () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
playlists.value = res.data.list || res.data.records || []
|
const pageData = extractPageData(res.data)
|
||||||
total.value = res.data.total || 0
|
playlists.value = pageData.list
|
||||||
|
total.value = pageData.total
|
||||||
updatePlaylistStatuses(playlists.value)
|
updatePlaylistStatuses(playlists.value)
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || '获取歌单列表失败')
|
ElMessage.error(res.message || '获取歌单列表失败')
|
||||||
@@ -108,7 +126,7 @@ const fetchHotPlaylists = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await getHotPlaylist(8) // 获取8个热门歌单
|
const res = await getHotPlaylist(8) // 获取8个热门歌单
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
hotPlaylists.value = res.data || []
|
hotPlaylists.value = extractList(res.data)
|
||||||
updatePlaylistStatuses(hotPlaylists.value)
|
updatePlaylistStatuses(hotPlaylists.value)
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || '获取热门歌单失败')
|
ElMessage.error(res.message || '获取热门歌单失败')
|
||||||
@@ -128,6 +146,13 @@ const handleSearch = () => {
|
|||||||
fetchPlaylists()
|
fetchPlaylists()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleKeywordClear = () => {
|
||||||
|
keyword.value = ''
|
||||||
|
searchedKeyword.value = ''
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchPlaylists()
|
||||||
|
}
|
||||||
|
|
||||||
// 处理分页变化
|
// 处理分页变化
|
||||||
const handlePageChange = (page) => {
|
const handlePageChange = (page) => {
|
||||||
currentPage.value = page
|
currentPage.value = page
|
||||||
@@ -170,7 +195,7 @@ const toggleCollect = async (playlist) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('收藏操作失败:', error)
|
console.error('收藏操作失败:', error)
|
||||||
ElMessage.error('操作失败,请稍后重试')
|
showErrorOnce(error, '操作失败,请稍后重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +229,15 @@ const formatPlayCount = (count) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
watch(keyword, (val) => {
|
||||||
|
if (val === '' && searchedKeyword.value !== '') {
|
||||||
|
searchedKeyword.value = ''
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchPlaylists()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchPlaylistTypes()
|
fetchPlaylistTypes()
|
||||||
fetchPlaylists()
|
fetchPlaylists()
|
||||||
@@ -220,6 +254,7 @@ onMounted(() => {
|
|||||||
v-model="keyword"
|
v-model="keyword"
|
||||||
placeholder="搜索歌单"
|
placeholder="搜索歌单"
|
||||||
clearable
|
clearable
|
||||||
|
@clear="handleKeywordClear"
|
||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
>
|
>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getMusicStatus } from '../api/music'
|
|||||||
import { getCommentList, addComment, deleteComment as apiDeleteComment } from '../api/comment'
|
import { getCommentList, addComment, deleteComment as apiDeleteComment } from '../api/comment'
|
||||||
import { connect, subscribe, unsubscribe, disconnect } from '../utils/websocket' // 导入 WebSocket 工具
|
import { connect, subscribe, unsubscribe, disconnect } from '../utils/websocket' // 导入 WebSocket 工具
|
||||||
import { processImageUrl } from '../utils/imageUtils'
|
import { processImageUrl } from '../utils/imageUtils'
|
||||||
|
import { showErrorOnce } from '../utils/errorHandler'
|
||||||
import MusicList from '../components/MusicList.vue'
|
import MusicList from '../components/MusicList.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -201,7 +202,7 @@ const toggleCollect = async () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('收藏操作失败:', error)
|
console.error('收藏操作失败:', error)
|
||||||
ElMessage.error('操作失败,请稍后重试')
|
showErrorOnce(error, '操作失败,请稍后重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +227,7 @@ const deleteComment = async (commentId) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
if (error !== 'cancel') {
|
||||||
console.error('删除评论失败:', error)
|
console.error('删除评论失败:', error)
|
||||||
ElMessage.error('删除失败,请稍后重试')
|
showErrorOnce(error, '删除失败,请稍后重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,7 +320,7 @@ const submitComment = async (isReply = false) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('提交评论失败:', error)
|
console.error('提交评论失败:', error)
|
||||||
ElMessage.error('评论失败,请稍后重试')
|
showErrorOnce(error, '评论失败,请稍后重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,13 +359,18 @@ const submitEdit = async () => {
|
|||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
ElMessage.success('歌单更新成功')
|
ElMessage.success('歌单更新成功')
|
||||||
editDialogVisible.value = false
|
editDialogVisible.value = false
|
||||||
fetchPlaylistDetail()
|
await fetchPlaylistDetail()
|
||||||
|
// 如果上传了新封面,加时间戳破浏览器缓存
|
||||||
|
if (editForm.value.cover && playlist.value && playlist.value.cover) {
|
||||||
|
const separator = playlist.value.cover.includes('?') ? '&' : '?'
|
||||||
|
playlist.value.cover = playlist.value.cover + separator + 't=' + Date.now()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || '更新失败')
|
ElMessage.error(res.message || '更新失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新歌单失败:', error)
|
console.error('更新歌单失败:', error)
|
||||||
ElMessage.error('更新失败,请稍后重试')
|
showErrorOnce(error, '更新失败,请稍后重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,7 +396,7 @@ const deletePlaylist = () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除歌单失败:', error)
|
console.error('删除歌单失败:', error)
|
||||||
ElMessage.error('删除失败,请稍后重试')
|
showErrorOnce(error, '删除失败,请稍后重试')
|
||||||
}
|
}
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
}
|
}
|
||||||
@@ -510,7 +516,7 @@ const deleteFromPlaylist = async (music) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
if (error !== 'cancel') {
|
||||||
console.error('移除歌曲失败:', error)
|
console.error('移除歌曲失败:', error)
|
||||||
ElMessage.error('移除失败,请稍后重试')
|
showErrorOnce(error, '移除失败,请稍后重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -524,7 +530,28 @@ onMounted(() => {
|
|||||||
// 连接 WebSocket 并订阅评论
|
// 连接 WebSocket 并订阅评论
|
||||||
connect(() => {
|
connect(() => {
|
||||||
const destination = `/topic/comments/2/${playlistId.value}`; // 2 代表歌单类型
|
const destination = `/topic/comments/2/${playlistId.value}`; // 2 代表歌单类型
|
||||||
commentSubscription = subscribe(destination, (newComment) => {
|
commentSubscription = subscribe(destination, (message) => {
|
||||||
|
// 处理删除事件
|
||||||
|
if (message && message.deleted) {
|
||||||
|
const deletedId = message.id;
|
||||||
|
if (message.parentId) {
|
||||||
|
// 删除子评论
|
||||||
|
const parentComment = comments.value.find(c => c.id === message.parentId);
|
||||||
|
if (parentComment && parentComment.children) {
|
||||||
|
parentComment.children = parentComment.children.filter(c => c.id !== deletedId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 删除父评论
|
||||||
|
comments.value = comments.value.filter(c => c.id !== deletedId);
|
||||||
|
}
|
||||||
|
if (playlist.value && playlist.value.commentCount > 0) {
|
||||||
|
playlist.value.commentCount--;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理新增评论
|
||||||
|
const newComment = message;
|
||||||
if (newComment && newComment.id) {
|
if (newComment && newComment.id) {
|
||||||
if (newComment.user && newComment.user.avatar) {
|
if (newComment.user && newComment.user.avatar) {
|
||||||
newComment.user.avatar = processImageUrl(newComment.user.avatar);
|
newComment.user.avatar = processImageUrl(newComment.user.avatar);
|
||||||
@@ -900,21 +927,13 @@ onUnmounted(() => {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 15px;
|
gap: 10px;
|
||||||
align-items: flex-end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-input .el-button {
|
.comment-submit-actions {
|
||||||
background-color: #ff6b81;
|
display: flex;
|
||||||
color: white;
|
justify-content: flex-end;
|
||||||
border-radius: 20px;
|
gap: 10px;
|
||||||
padding: 10px 20px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-input .el-button:hover {
|
|
||||||
background-color: #ff4757;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-list {
|
.comment-list {
|
||||||
@@ -924,15 +943,8 @@ onUnmounted(() => {
|
|||||||
.comment-item {
|
.comment-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding: 15px;
|
padding-bottom: 20px;
|
||||||
background-color: #fff9f9;
|
border-bottom: 1px solid #eee;
|
||||||
border-radius: 12px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-item:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 12px rgba(255, 107, 129, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-avatar {
|
.comment-avatar {
|
||||||
@@ -951,8 +963,17 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.comment-user {
|
.comment-user {
|
||||||
font-weight: 600;
|
font-weight: bold;
|
||||||
color: #ff6b81;
|
}
|
||||||
|
|
||||||
|
.child-comment-list {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-left: 15px;
|
||||||
|
border-left: 2px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-comment-input {
|
||||||
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-text {
|
.comment-text {
|
||||||
@@ -961,18 +982,11 @@ onUnmounted(() => {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-time {
|
.comment-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-actions .el-button {
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-actions .el-button:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ import { ref, computed, onMounted, watch } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { showErrorOnce } from '../utils/errorHandler'
|
||||||
import { Search } from '@element-plus/icons-vue'
|
import { Search } from '@element-plus/icons-vue'
|
||||||
import { searchMusic, toggleCollectMusic, getMusicStatus } from '../api/music'
|
import { searchMusic, toggleCollectMusic, getMusicStatus } from '../api/music'
|
||||||
import { searchPlaylist, toggleCollectPlaylist } from '../api/playlist'
|
import { searchPlaylist, toggleCollectPlaylist } from '../api/playlist'
|
||||||
@@ -164,6 +165,22 @@ const route = useRoute()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const extractList = (data) => {
|
||||||
|
if (Array.isArray(data)) return data
|
||||||
|
if (Array.isArray(data?.list)) return data.list
|
||||||
|
if (Array.isArray(data?.records)) return data.records
|
||||||
|
if (Array.isArray(data?.items)) return data.items
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractPageData = (data) => {
|
||||||
|
const list = extractList(data)
|
||||||
|
return {
|
||||||
|
list,
|
||||||
|
total: Number.isFinite(Number(data?.total)) ? Number(data.total) : list.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 搜索关键词
|
// 搜索关键词
|
||||||
const keyword = computed(() => route.query.keyword || '')
|
const keyword = computed(() => route.query.keyword || '')
|
||||||
const searchKeyword = ref(keyword.value)
|
const searchKeyword = ref(keyword.value)
|
||||||
@@ -213,8 +230,9 @@ const fetchMusicList = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await searchMusic(keyword.value, musicPage.value, musicPageSize.value)
|
const res = await searchMusic(keyword.value, musicPage.value, musicPageSize.value)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
musicList.value = res.data.list || res.data.records || []
|
const pageData = extractPageData(res.data)
|
||||||
musicTotal.value = res.data.total || 0
|
musicList.value = pageData.list
|
||||||
|
musicTotal.value = pageData.total
|
||||||
|
|
||||||
// 处理音乐URL和封面URL
|
// 处理音乐URL和封面URL
|
||||||
musicList.value = musicList.value.map(item => ({
|
musicList.value = musicList.value.map(item => ({
|
||||||
@@ -244,8 +262,9 @@ const fetchPlaylistList = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await searchPlaylist(keyword.value, playlistPage.value, playlistPageSize.value)
|
const res = await searchPlaylist(keyword.value, playlistPage.value, playlistPageSize.value)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
playlistList.value = res.data.list || res.data.records || []
|
const pageData = extractPageData(res.data)
|
||||||
playlistTotal.value = res.data.total || 0
|
playlistList.value = pageData.list
|
||||||
|
playlistTotal.value = pageData.total
|
||||||
|
|
||||||
// 处理封面URL
|
// 处理封面URL
|
||||||
playlistList.value = playlistList.value.map(item => ({
|
playlistList.value = playlistList.value.map(item => ({
|
||||||
@@ -271,8 +290,9 @@ const fetchSingerList = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await searchSinger(keyword.value, singerPage.value, singerPageSize.value)
|
const res = await searchSinger(keyword.value, singerPage.value, singerPageSize.value)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
singerList.value = res.data.list || res.data.records || []
|
const pageData = extractPageData(res.data)
|
||||||
singerTotal.value = res.data.total || 0
|
singerList.value = pageData.list
|
||||||
|
singerTotal.value = pageData.total
|
||||||
|
|
||||||
// 处理头像URL
|
// 处理头像URL
|
||||||
singerList.value = singerList.value.map(item => ({
|
singerList.value = singerList.value.map(item => ({
|
||||||
@@ -362,7 +382,7 @@ const handleCollectChange = async (id) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('收藏操作失败:', error)
|
console.error('收藏操作失败:', error)
|
||||||
ElMessage.error('操作失败,请稍后重试')
|
showErrorOnce(error, '操作失败,请稍后重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +407,7 @@ const handleCollectPlaylist = async (id) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('收藏操作失败:', error)
|
console.error('收藏操作失败:', error)
|
||||||
ElMessage.error('操作失败,请稍后重试')
|
showErrorOnce(error, '操作失败,请稍后重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +434,7 @@ const handleCollectSinger = async (id, event) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('收藏操作失败:', error)
|
console.error('收藏操作失败:', error)
|
||||||
ElMessage.error('操作失败,请稍后重试')
|
showErrorOnce(error, '操作失败,请稍后重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { showErrorOnce } from '../utils/errorHandler'
|
||||||
import { Search } from '@element-plus/icons-vue'
|
import { Search } from '@element-plus/icons-vue'
|
||||||
import { getSingerList, getHotSinger, isSingerCollected, collectSinger, uncollectSinger } from '../api/singer'
|
import { getSingerList, getHotSinger, isSingerCollected, collectSinger, uncollectSinger } from '../api/singer'
|
||||||
import { getSingerTypeList } from '../api/singerType'
|
import { getSingerTypeList } from '../api/singerType'
|
||||||
@@ -11,6 +12,22 @@ import { processImageUrl } from '../utils/imageUtils'
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const extractList = (data) => {
|
||||||
|
if (Array.isArray(data)) return data
|
||||||
|
if (Array.isArray(data?.list)) return data.list
|
||||||
|
if (Array.isArray(data?.records)) return data.records
|
||||||
|
if (Array.isArray(data?.items)) return data.items
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractPageData = (data) => {
|
||||||
|
const list = extractList(data)
|
||||||
|
return {
|
||||||
|
list,
|
||||||
|
total: Number.isFinite(Number(data?.total)) ? Number(data.total) : list.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 歌手类型列表
|
// 歌手类型列表
|
||||||
const singerTypes = ref([{ id: null, name: '全部' }])
|
const singerTypes = ref([{ id: null, name: '全部' }])
|
||||||
|
|
||||||
@@ -74,8 +91,9 @@ const fetchSingers = async () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
singers.value = res.data.list || res.data.records || []
|
const pageData = extractPageData(res.data)
|
||||||
total.value = res.data.total || 0
|
singers.value = pageData.list
|
||||||
|
total.value = pageData.total
|
||||||
|
|
||||||
// 处理图片URL
|
// 处理图片URL
|
||||||
singers.value = singers.value.map(singer => ({
|
singers.value = singers.value.map(singer => ({
|
||||||
@@ -100,7 +118,7 @@ const fetchHotSingers = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await getHotSinger(8) // 获取8个热门歌手
|
const res = await getHotSinger(8) // 获取8个热门歌手
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
hotSingers.value = res.data || []
|
hotSingers.value = extractList(res.data)
|
||||||
|
|
||||||
// 处理图片URL
|
// 处理图片URL
|
||||||
hotSingers.value = hotSingers.value.map(singer => ({
|
hotSingers.value = hotSingers.value.map(singer => ({
|
||||||
@@ -126,6 +144,13 @@ const handleSearch = () => {
|
|||||||
fetchSingers()
|
fetchSingers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleKeywordClear = () => {
|
||||||
|
keyword.value = ''
|
||||||
|
searchedKeyword.value = ''
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchSingers()
|
||||||
|
}
|
||||||
|
|
||||||
// 处理分页变化
|
// 处理分页变化
|
||||||
const handlePageChange = (page) => {
|
const handlePageChange = (page) => {
|
||||||
currentPage.value = page
|
currentPage.value = page
|
||||||
@@ -170,7 +195,7 @@ const toggleCollect = async (singer, event) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('收藏操作失败:', error)
|
console.error('收藏操作失败:', error)
|
||||||
ElMessage.error('操作失败,请稍后重试')
|
showErrorOnce(error, '操作失败,请稍后重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +229,15 @@ const formatFans = (count) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
watch(keyword, (val) => {
|
||||||
|
if (val === '' && searchedKeyword.value !== '') {
|
||||||
|
searchedKeyword.value = ''
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchSingers()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchSingerTypes()
|
fetchSingerTypes()
|
||||||
fetchSingers()
|
fetchSingers()
|
||||||
@@ -220,6 +254,7 @@ onMounted(() => {
|
|||||||
v-model="keyword"
|
v-model="keyword"
|
||||||
placeholder="搜索歌手"
|
placeholder="搜索歌手"
|
||||||
clearable
|
clearable
|
||||||
|
@clear="handleKeywordClear"
|
||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
>
|
>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
|||||||
import { getSingerById, isSingerCollected, collectSinger, uncollectSinger } from '../api/singer'
|
import { getSingerById, isSingerCollected, collectSinger, uncollectSinger } from '../api/singer'
|
||||||
import { getCommentList, addComment, deleteComment as apiDeleteComment } from '../api/comment'
|
import { getCommentList, addComment, deleteComment as apiDeleteComment } from '../api/comment'
|
||||||
import { processImageUrl } from '../utils/imageUtils'
|
import { processImageUrl } from '../utils/imageUtils'
|
||||||
|
import { showErrorOnce } from '../utils/errorHandler'
|
||||||
import { processMusicUrl, getMusicBySinger, getMusicStatus } from '../api/music'
|
import { processMusicUrl, getMusicBySinger, getMusicStatus } from '../api/music'
|
||||||
import { connect, subscribe, unsubscribe, disconnect } from '../utils/websocket' // 导入 WebSocket 工具
|
import { connect, subscribe, unsubscribe, disconnect } from '../utils/websocket' // 导入 WebSocket 工具
|
||||||
import MusicList from '../components/MusicList.vue'
|
import MusicList from '../components/MusicList.vue'
|
||||||
@@ -219,7 +220,7 @@ const toggleCollect = async () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('收藏操作失败:', error)
|
console.error('收藏操作失败:', error)
|
||||||
ElMessage.error('操作失败,请稍后重试')
|
showErrorOnce(error, '操作失败,请稍后重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +312,7 @@ const submitComment = async (isReply = false) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('提交评论失败:', error)
|
console.error('提交评论失败:', error)
|
||||||
ElMessage.error('评论失败,请稍后重试')
|
showErrorOnce(error, '评论失败,请稍后重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,7 +337,7 @@ const deleteComment = async (commentId) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
if (error !== 'cancel') {
|
||||||
console.error('删除评论失败:', error)
|
console.error('删除评论失败:', error)
|
||||||
ElMessage.error('删除失败,请稍后重试')
|
showErrorOnce(error, '删除失败,请稍后重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,7 +441,28 @@ onMounted(() => {
|
|||||||
// 连接 WebSocket 并订阅评论
|
// 连接 WebSocket 并订阅评论
|
||||||
connect(() => {
|
connect(() => {
|
||||||
const destination = `/topic/comments/3/${singerId.value}`; // 3 代表歌手类型
|
const destination = `/topic/comments/3/${singerId.value}`; // 3 代表歌手类型
|
||||||
commentSubscription = subscribe(destination, (newComment) => {
|
commentSubscription = subscribe(destination, (message) => {
|
||||||
|
// 处理删除事件
|
||||||
|
if (message && message.deleted) {
|
||||||
|
const deletedId = message.id;
|
||||||
|
if (message.parentId) {
|
||||||
|
// 删除子评论
|
||||||
|
const parentComment = comments.value.find(c => c.id === message.parentId);
|
||||||
|
if (parentComment && parentComment.children) {
|
||||||
|
parentComment.children = parentComment.children.filter(c => c.id !== deletedId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 删除父评论
|
||||||
|
comments.value = comments.value.filter(c => c.id !== deletedId);
|
||||||
|
}
|
||||||
|
if (singer.value && singer.value.commentCount > 0) {
|
||||||
|
singer.value.commentCount--;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理新增评论
|
||||||
|
const newComment = message;
|
||||||
if (newComment && newComment.id) {
|
if (newComment && newComment.id) {
|
||||||
if (newComment.user && newComment.user.avatar) {
|
if (newComment.user && newComment.user.avatar) {
|
||||||
newComment.user.avatar = processImageUrl(newComment.user.avatar);
|
newComment.user.avatar = processImageUrl(newComment.user.avatar);
|
||||||
|
|||||||
@@ -12,6 +12,22 @@ import { processImageUrl } from '../utils/imageUtils'
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const extractList = (data) => {
|
||||||
|
if (Array.isArray(data)) return data
|
||||||
|
if (Array.isArray(data?.list)) return data.list
|
||||||
|
if (Array.isArray(data?.records)) return data.records
|
||||||
|
if (Array.isArray(data?.items)) return data.items
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractPageData = (data) => {
|
||||||
|
const list = extractList(data)
|
||||||
|
return {
|
||||||
|
list,
|
||||||
|
total: Number.isFinite(Number(data?.total)) ? Number(data.total) : list.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 用户信息
|
// 用户信息
|
||||||
const userInfo = ref(null)
|
const userInfo = ref(null)
|
||||||
|
|
||||||
@@ -43,7 +59,7 @@ const avatarUploadUrl = ref(`${import.meta.env.VITE_API_BASE_URL || ''}/api/user
|
|||||||
|
|
||||||
// 头像上传headers
|
// 头像上传headers
|
||||||
const uploadHeaders = ref({
|
const uploadHeaders = ref({
|
||||||
[localStorage.getItem('tokenName') || 'satoken']: userStore.token
|
[localStorage.getItem('tokenName') || 'satoken']: `${localStorage.getItem('tokenPrefix') || ''}${userStore.token || localStorage.getItem('token') || ''}`
|
||||||
})
|
})
|
||||||
|
|
||||||
// 当前激活的菜单
|
// 当前激活的菜单
|
||||||
@@ -168,25 +184,25 @@ const fetchUserStats = async () => {
|
|||||||
// 获取收藏的音乐数量
|
// 获取收藏的音乐数量
|
||||||
const musicRes = await getCollectedMusic()
|
const musicRes = await getCollectedMusic()
|
||||||
if (musicRes.code === 200) {
|
if (musicRes.code === 200) {
|
||||||
userStats.value.collectMusicCount = musicRes.data.total || musicRes.data.length || 0
|
userStats.value.collectMusicCount = extractPageData(musicRes.data).total
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取收藏的歌单数量
|
// 获取收藏的歌单数量
|
||||||
const playlistRes = await getCollectedPlaylist()
|
const playlistRes = await getCollectedPlaylist()
|
||||||
if (playlistRes.code === 200) {
|
if (playlistRes.code === 200) {
|
||||||
userStats.value.collectPlaylistCount = playlistRes.data.total || playlistRes.data.length || 0
|
userStats.value.collectPlaylistCount = extractPageData(playlistRes.data).total
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取收藏的歌手数量
|
// 获取收藏的歌手数量
|
||||||
const singerRes = await getCollectedSinger()
|
const singerRes = await getCollectedSinger()
|
||||||
if (singerRes.code === 200) {
|
if (singerRes.code === 200) {
|
||||||
userStats.value.collectSingerCount = singerRes.data.total || singerRes.data.length || 0
|
userStats.value.collectSingerCount = extractPageData(singerRes.data).total
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取创建的歌单数量
|
// 获取创建的歌单数量
|
||||||
const userPlaylistRes = await getUserPlaylist()
|
const userPlaylistRes = await getUserPlaylist()
|
||||||
if (userPlaylistRes.code === 200) {
|
if (userPlaylistRes.code === 200) {
|
||||||
userStats.value.createPlaylistCount = userPlaylistRes.data.total || userPlaylistRes.data.length || 0
|
userStats.value.createPlaylistCount = extractPageData(userPlaylistRes.data).total
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取用户统计数据失败:', error)
|
console.error('获取用户统计数据失败:', error)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { ElLoading, ElMessage } from 'element-plus'
|
import { ElLoading, ElMessage } from 'element-plus'
|
||||||
import * as echarts from 'echarts/core'
|
import * as echarts from 'echarts/core'
|
||||||
import { BarChart, LineChart, PieChart } from 'echarts/charts'
|
import { BarChart, LineChart, PieChart } from 'echarts/charts'
|
||||||
@@ -57,6 +58,9 @@ const userGrowthData = ref([])
|
|||||||
const musicTypeData = ref([])
|
const musicTypeData = ref([])
|
||||||
const playCountData = ref([])
|
const playCountData = ref([])
|
||||||
|
|
||||||
|
// 路由
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
// 加载状态
|
// 加载状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
@@ -484,7 +488,7 @@ onUnmounted(() => {
|
|||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>最新用户</span>
|
<span>最新用户</span>
|
||||||
<el-button text>查看更多</el-button>
|
<el-button text @click="router.push({ name: 'admin-user' })">查看更多</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -501,7 +505,7 @@ onUnmounted(() => {
|
|||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>最新音乐</span>
|
<span>最新音乐</span>
|
||||||
<el-button text>查看更多</el-button>
|
<el-button text @click="router.push({ name: 'admin-music' })">查看更多</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -270,7 +270,8 @@ const submitForm = async () => {
|
|||||||
singer: form.value.singer,
|
singer: form.value.singer,
|
||||||
album: form.value.album,
|
album: form.value.album,
|
||||||
typeIds: form.value.types,
|
typeIds: form.value.types,
|
||||||
lyric: form.value.lyric
|
lyric: form.value.lyric,
|
||||||
|
duration: form.value.duration
|
||||||
},
|
},
|
||||||
form.value.file,
|
form.value.file,
|
||||||
form.value.cover
|
form.value.cover
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { getPlaylistList, getPlaylistById, addPlaylist as apiAddPlaylist, updatePlaylist as apiUpdatePlaylist, deletePlaylist as apiDeletePlaylist } from '../../api/playlist'
|
import { getPlaylistList, getPlaylistById, addPlaylist as apiAddPlaylist, updatePlaylist as apiUpdatePlaylist, deletePlaylist as apiDeletePlaylist } from '../../api/playlist'
|
||||||
import { getPlaylistTypeList } from '../../api/playlistType'
|
import { getPlaylistTypeList } from '../../api/playlistType'
|
||||||
import { processMusicUrl } from '../../api/music'
|
import { processMusicUrl } from '../../api/music'
|
||||||
|
import { showErrorOnce } from '../../utils/errorHandler'
|
||||||
|
|
||||||
// 歌单列表
|
// 歌单列表
|
||||||
const playlistList = ref([])
|
const playlistList = ref([])
|
||||||
@@ -251,6 +252,7 @@ const musicDialogVisible = ref(false)
|
|||||||
const currentPlaylistId = ref(null)
|
const currentPlaylistId = ref(null)
|
||||||
const currentPlaylistName = ref('')
|
const currentPlaylistName = ref('')
|
||||||
const musicList = ref([])
|
const musicList = ref([])
|
||||||
|
const playlistMusics = ref([])
|
||||||
const selectedMusicIds = ref([])
|
const selectedMusicIds = ref([])
|
||||||
const musicSearchKeyword = ref('')
|
const musicSearchKeyword = ref('')
|
||||||
const musicLoading = ref(false)
|
const musicLoading = ref(false)
|
||||||
@@ -278,10 +280,17 @@ const fetchPlaylistMusic = async (playlistId) => {
|
|||||||
try {
|
try {
|
||||||
const res = await getPlaylistById(playlistId)
|
const res = await getPlaylistById(playlistId)
|
||||||
if (res.code === 200 && res.data.musics) {
|
if (res.code === 200 && res.data.musics) {
|
||||||
|
playlistMusics.value = res.data.musics
|
||||||
selectedMusicIds.value = res.data.musics.map(m => m.id)
|
selectedMusicIds.value = res.data.musics.map(m => m.id)
|
||||||
|
} else {
|
||||||
|
playlistMusics.value = []
|
||||||
|
selectedMusicIds.value = []
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取歌单音乐失败:', error)
|
console.error('获取歌单音乐失败:', error)
|
||||||
|
playlistMusics.value = []
|
||||||
|
selectedMusicIds.value = []
|
||||||
|
showErrorOnce(error, '获取歌单音乐失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,6 +299,8 @@ const managePlaylistMusic = async (row) => {
|
|||||||
currentPlaylistId.value = row.id
|
currentPlaylistId.value = row.id
|
||||||
currentPlaylistName.value = row.name
|
currentPlaylistName.value = row.name
|
||||||
musicSearchKeyword.value = ''
|
musicSearchKeyword.value = ''
|
||||||
|
playlistMusics.value = []
|
||||||
|
selectedMusicIds.value = []
|
||||||
musicDialogVisible.value = true
|
musicDialogVisible.value = true
|
||||||
|
|
||||||
// 加载音乐列表和已选音乐
|
// 加载音乐列表和已选音乐
|
||||||
@@ -305,13 +316,16 @@ const addMusicToPlaylistLocal = async (musicId) => {
|
|||||||
const res = await addMusicToPlaylist(currentPlaylistId.value, musicId)
|
const res = await addMusicToPlaylist(currentPlaylistId.value, musicId)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
ElMessage.success('添加成功')
|
ElMessage.success('添加成功')
|
||||||
await fetchPlaylistMusic(currentPlaylistId.value)
|
await Promise.all([
|
||||||
|
fetchPlaylistMusic(currentPlaylistId.value),
|
||||||
|
fetchPlaylistList()
|
||||||
|
])
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || '添加失败')
|
ElMessage.error(res.message || '添加失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('添加音乐失败:', error)
|
console.error('添加音乐失败:', error)
|
||||||
ElMessage.error('添加失败')
|
showErrorOnce(error, '添加失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,13 +335,16 @@ const removeMusicFromPlaylistLocal = async (musicId) => {
|
|||||||
const res = await removeMusicFromPlaylist(currentPlaylistId.value, musicId)
|
const res = await removeMusicFromPlaylist(currentPlaylistId.value, musicId)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
ElMessage.success('移除成功')
|
ElMessage.success('移除成功')
|
||||||
await fetchPlaylistMusic(currentPlaylistId.value)
|
await Promise.all([
|
||||||
|
fetchPlaylistMusic(currentPlaylistId.value),
|
||||||
|
fetchPlaylistList()
|
||||||
|
])
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || '移除失败')
|
ElMessage.error(res.message || '移除失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('移除音乐失败:', error)
|
console.error('移除音乐失败:', error)
|
||||||
ElMessage.error('移除失败')
|
showErrorOnce(error, '移除失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -737,7 +754,7 @@ import { addMusicToPlaylist, removeMusicFromPlaylist } from '../../api/playlist'
|
|||||||
<div class="selected-music-container">
|
<div class="selected-music-container">
|
||||||
<h3>已添加的音乐</h3>
|
<h3>已添加的音乐</h3>
|
||||||
<el-table
|
<el-table
|
||||||
:data="musicList.filter(m => selectedMusicIds.includes(m.id))"
|
:data="playlistMusics"
|
||||||
stripe
|
stripe
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
empty-text="暂无音乐"
|
empty-text="暂无音乐"
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, reactive } from 'vue'
|
import { ref, onMounted, reactive, computed } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Search, Edit } from '@element-plus/icons-vue'
|
import { Search, Edit, Plus } from '@element-plus/icons-vue'
|
||||||
import { getUserList, deleteUser, updateUserStatus, getUserById, adminUpdateUser } from '../../api/admin/user'
|
import { getUserList, deleteUser, updateUserStatus, getUserById, adminUpdateUser, adminUploadUserAvatar } from '../../api/admin/user'
|
||||||
|
import { processImageUrl } from '../../utils/imageUtils'
|
||||||
|
|
||||||
// 用户列表
|
// 用户列表
|
||||||
const userList = ref([])
|
const userList = ref([])
|
||||||
@@ -36,13 +37,74 @@ const editFormRules = {
|
|||||||
],
|
],
|
||||||
email: [
|
email: [
|
||||||
{ pattern: /^\S+@\S+\.\S+$/, message: '请输入正确的邮箱地址', trigger: 'blur' }
|
{ pattern: /^\S+@\S+\.\S+$/, message: '请输入正确的邮箱地址', trigger: 'blur' }
|
||||||
],
|
|
||||||
avatar: [
|
|
||||||
{ pattern: /^(https?:\/\/)?[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:/~+#]*[\w\-@?^=%&/~+#])?$/, message: '请输入正确的URL地址', trigger: 'blur' }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
const editFormRef = ref(null)
|
const editFormRef = ref(null)
|
||||||
const editLoading = ref(false)
|
const editLoading = ref(false)
|
||||||
|
const avatarUploading = ref(false)
|
||||||
|
const avatarPreviewVersion = ref(Date.now())
|
||||||
|
|
||||||
|
const avatarPreviewUrl = computed(() => {
|
||||||
|
if (!editForm.avatar) return ''
|
||||||
|
const avatarUrl = processImageUrl(editForm.avatar)
|
||||||
|
const separator = avatarUrl.includes('?') ? '&' : '?'
|
||||||
|
return `${avatarUrl}${separator}t=${avatarPreviewVersion.value}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolveAvatarPath = (avatarData) => {
|
||||||
|
if (!avatarData) return ''
|
||||||
|
if (typeof avatarData === 'string') return avatarData
|
||||||
|
if (typeof avatarData === 'object') {
|
||||||
|
return avatarData.avatar || avatarData.url || avatarData.path || avatarData.filePath || ''
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAvatarBeforeUpload = (file) => {
|
||||||
|
const isAllowedType = ['image/jpeg', 'image/png', 'image/webp'].includes(file.type)
|
||||||
|
const isLt2M = file.size / 1024 / 1024 < 2
|
||||||
|
|
||||||
|
if (!isAllowedType) {
|
||||||
|
ElMessage.error('头像仅支持 JPG/PNG/WEBP 格式')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLt2M) {
|
||||||
|
ElMessage.error('头像大小不能超过 2MB')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAvatarUpload = async ({ file }) => {
|
||||||
|
if (!editForm.id) {
|
||||||
|
ElMessage.error('请先选择要编辑的用户')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
avatarUploading.value = true
|
||||||
|
try {
|
||||||
|
const res = await adminUploadUserAvatar(editForm.id, file)
|
||||||
|
if (res.code === 200) {
|
||||||
|
editForm.avatar = resolveAvatarPath(res.data)
|
||||||
|
avatarPreviewVersion.value = Date.now()
|
||||||
|
ElMessage.success('头像上传成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '头像上传失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传头像失败:', error)
|
||||||
|
ElMessage.error('头像上传失败,请稍后再试')
|
||||||
|
} finally {
|
||||||
|
avatarUploading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAvatarRemove = () => {
|
||||||
|
editForm.avatar = ''
|
||||||
|
avatarPreviewVersion.value = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
// 获取用户列表
|
// 获取用户列表
|
||||||
const fetchUserList = async () => {
|
const fetchUserList = async () => {
|
||||||
@@ -144,6 +206,7 @@ const handleEdit = async (id) => {
|
|||||||
editForm.avatar = user.avatar || ''
|
editForm.avatar = user.avatar || ''
|
||||||
editForm.gender = user.gender || 0
|
editForm.gender = user.gender || 0
|
||||||
editForm.introduction = user.introduction || ''
|
editForm.introduction = user.introduction || ''
|
||||||
|
avatarPreviewVersion.value = Date.now()
|
||||||
|
|
||||||
// 显示对话框
|
// 显示对话框
|
||||||
editDialogVisible.value = true
|
editDialogVisible.value = true
|
||||||
@@ -360,11 +423,24 @@ onMounted(() => {
|
|||||||
<el-form-item label="昵称" prop="nickname">
|
<el-form-item label="昵称" prop="nickname">
|
||||||
<el-input v-model="editForm.nickname" placeholder="请输入昵称"></el-input>
|
<el-input v-model="editForm.nickname" placeholder="请输入昵称"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="头像" prop="avatar">
|
<el-form-item label="头像">
|
||||||
<el-input v-model="editForm.avatar" placeholder="请输入头像地址"></el-input>
|
<el-upload
|
||||||
<div class="avatar-preview" v-if="editForm.avatar">
|
class="avatar-upload"
|
||||||
<el-avatar :size="60" :src="editForm.avatar"></el-avatar>
|
:http-request="handleAvatarUpload"
|
||||||
|
:before-upload="handleAvatarBeforeUpload"
|
||||||
|
:show-file-list="false"
|
||||||
|
accept=".jpg,.jpeg,.png,.webp"
|
||||||
|
:disabled="avatarUploading || editLoading"
|
||||||
|
>
|
||||||
|
<div class="avatar-uploader-content" v-loading="avatarUploading">
|
||||||
|
<el-avatar v-if="editForm.avatar" :size="60" :src="avatarPreviewUrl"></el-avatar>
|
||||||
|
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
<div class="avatar-upload-actions" v-if="editForm.avatar">
|
||||||
|
<el-button link type="danger" @click="handleAvatarRemove">移除头像</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="el-upload__tip">支持 jpg/png/webp,大小不超过 2MB</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="性别">
|
<el-form-item label="性别">
|
||||||
<el-radio-group v-model="editForm.gender">
|
<el-radio-group v-model="editForm.gender">
|
||||||
@@ -505,9 +581,33 @@ onMounted(() => {
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-preview {
|
.avatar-upload :deep(.el-upload) {
|
||||||
margin-top: 10px;
|
border: 1px dashed #b3e5fc;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-upload :deep(.el-upload):hover {
|
||||||
|
border-color: #4dd0e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-uploader-content {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
background: #f8fdff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-uploader-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #8c939d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-upload-actions {
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -13,6 +13,20 @@ const collectMusicList = ref([])
|
|||||||
// 加载状态
|
// 加载状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 格式化日期时间,兼容后端返回的多种格式(ISO、yyyy-MM-dd HH:mm:ss、yyyy-MM-dd)
|
||||||
|
const formatDate = (dateStr) => {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
// 检查是否为有效日期
|
||||||
|
if (isNaN(date.getTime())) return dateStr
|
||||||
|
return date.getFullYear() + '-' +
|
||||||
|
String(date.getMonth() + 1).padStart(2, '0') + '-' +
|
||||||
|
String(date.getDate()).padStart(2, '0') + ' ' +
|
||||||
|
String(date.getHours()).padStart(2, '0') + ':' +
|
||||||
|
String(date.getMinutes()).padStart(2, '0') + ':' +
|
||||||
|
String(date.getSeconds()).padStart(2, '0')
|
||||||
|
}
|
||||||
|
|
||||||
// 获取收藏的音乐列表
|
// 获取收藏的音乐列表
|
||||||
const fetchCollectMusic = async () => {
|
const fetchCollectMusic = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -31,7 +45,7 @@ const fetchCollectMusic = async () => {
|
|||||||
duration: item.duration || 0,
|
duration: item.duration || 0,
|
||||||
cover: processImageUrl(item.cover),
|
cover: processImageUrl(item.cover),
|
||||||
url: item.url,
|
url: item.url,
|
||||||
collectTime: item.collectTime ? new Date(item.collectTime).toLocaleDateString() : '未知'
|
collectTime: item.collectTime ? formatDate(item.collectTime) : '未知'
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || '获取收藏的音乐列表失败')
|
ElMessage.error(res.message || '获取收藏的音乐列表失败')
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ const createForm = reactive({
|
|||||||
const editForm = reactive({
|
const editForm = reactive({
|
||||||
id: null,
|
id: null,
|
||||||
name: '',
|
name: '',
|
||||||
description: ''
|
description: '',
|
||||||
|
coverFile: null
|
||||||
})
|
})
|
||||||
|
|
||||||
// 表单规则
|
// 表单规则
|
||||||
@@ -52,14 +53,6 @@ const formRules = {
|
|||||||
const createFormRef = ref(null)
|
const createFormRef = ref(null)
|
||||||
const editFormRef = ref(null)
|
const editFormRef = ref(null)
|
||||||
|
|
||||||
// 头像上传URL
|
|
||||||
const coverUploadUrl = ref(`${import.meta.env.VITE_API_BASE_URL || ''}/api/file/upload/image`)
|
|
||||||
|
|
||||||
// 头像上传headers
|
|
||||||
const uploadHeaders = ref({
|
|
||||||
[userStore.tokenName]: userStore.token
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取自建歌单列表
|
// 获取自建歌单列表
|
||||||
const fetchUserPlaylists = async () => {
|
const fetchUserPlaylists = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -107,6 +100,8 @@ const openEditDialog = (playlist) => {
|
|||||||
editForm.id = playlist.id
|
editForm.id = playlist.id
|
||||||
editForm.name = playlist.name
|
editForm.name = playlist.name
|
||||||
editForm.description = playlist.description
|
editForm.description = playlist.description
|
||||||
|
editForm.coverFile = null
|
||||||
|
coverPreview.value = playlist.cover
|
||||||
editDialogVisible.value = true
|
editDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +139,7 @@ const handleUpdatePlaylist = () => {
|
|||||||
const res = await updatePlaylist(editForm.id, {
|
const res = await updatePlaylist(editForm.id, {
|
||||||
name: editForm.name,
|
name: editForm.name,
|
||||||
description: editForm.description
|
description: editForm.description
|
||||||
})
|
}, editForm.coverFile)
|
||||||
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
// 更新成功,重新获取歌单列表
|
// 更新成功,重新获取歌单列表
|
||||||
@@ -185,33 +180,13 @@ const handleDeletePlaylist = (id) => {
|
|||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 封面上传成功
|
// 封面预览URL
|
||||||
const handleCoverSuccess = async (response) => {
|
const coverPreview = ref('')
|
||||||
if (response.code === 200) {
|
|
||||||
// 上传成功,获取图片URL
|
|
||||||
const imageUrl = response.data
|
|
||||||
|
|
||||||
try {
|
// 封面选择处理(不自动上传,仅本地预览)
|
||||||
// 更新歌单封面
|
const handleCoverChange = (file) => {
|
||||||
const res = await updatePlaylist(currentPlaylist.value.id, {
|
editForm.coverFile = file.raw
|
||||||
...editForm,
|
coverPreview.value = URL.createObjectURL(file.raw)
|
||||||
cover: imageUrl
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.code === 200) {
|
|
||||||
// 更新成功,重新获取歌单列表
|
|
||||||
await fetchUserPlaylists()
|
|
||||||
ElMessage.success('封面上传成功')
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.message || '更新歌单封面失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新歌单封面失败:', error)
|
|
||||||
ElMessage.error('更新歌单封面失败')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ElMessage.error(response.message || '封面上传失败')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 封面上传前的处理
|
// 封面上传前的处理
|
||||||
@@ -354,15 +329,15 @@ onMounted(() => {
|
|||||||
<el-form-item label="封面">
|
<el-form-item label="封面">
|
||||||
<el-upload
|
<el-upload
|
||||||
class="cover-uploader"
|
class="cover-uploader"
|
||||||
:action="coverUploadUrl"
|
action="#"
|
||||||
:headers="uploadHeaders"
|
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
:on-success="handleCoverSuccess"
|
:auto-upload="false"
|
||||||
|
:on-change="handleCoverChange"
|
||||||
:before-upload="beforeCoverUpload"
|
:before-upload="beforeCoverUpload"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="currentPlaylist"
|
v-if="coverPreview"
|
||||||
:src="currentPlaylist.cover"
|
:src="coverPreview"
|
||||||
class="cover"
|
class="cover"
|
||||||
/>
|
/>
|
||||||
<el-icon v-else class="cover-uploader-icon"><plus /></el-icon>
|
<el-icon v-else class="cover-uploader-icon"><plus /></el-icon>
|
||||||
|
|||||||
Reference in New Issue
Block a user