5 Revize 06f428faa2 ... b2ee7e5a78

Autor SHA1 Zpráva Datum
  xiang b2ee7e5a78 vieo黑屏 před 1 týdnem
  xiang 891f62053b 发送评论刷新前 před 1 týdnem
  xiang 98fa5d6496 发送评论前 před 1 týdnem
  xiang c0a54ca3fb 分块未合并 před 2 týdny
  xiang 07869ce6eb 分块未合并 před 2 týdny
36 změnil soubory, kde provedl 2244 přidání a 570 odebrání
  1. 2 0
      package.json
  2. 16 0
      pnpm-lock.yaml
  3. 37 0
      src/apis/Mv.ts
  4. 21 0
      src/apis/comment.ts
  5. 24 0
      src/apis/upload.ts
  6. 1 1
      src/pages/layout/components/CreatePlaylistModal/index.tsx
  7. 3 0
      src/pages/layout/components/LoginContent/components/GiteeLogin.tsx
  8. 16 4
      src/pages/layout/pages/SongDetail/index.tsx
  9. 90 0
      src/pages/layout/pages/Video/Video_player/index.css
  10. 108 0
      src/pages/layout/pages/Video/Video_player/index.less
  11. 282 32
      src/pages/layout/pages/Video/Video_player/index.tsx
  12. 24 5
      src/pages/layout/pages/Video/index.tsx
  13. 87 13
      src/pages/layout/pages/artistinfo/MVs/MVs.tsx
  14. 65 0
      src/pages/layout/pages/artistinfo/MVs/index.css
  15. 72 0
      src/pages/layout/pages/artistinfo/MVs/index.less
  16. 69 0
      src/pages/layout/pages/find/rank/compomemts/CommentInput/index.css
  17. 72 0
      src/pages/layout/pages/find/rank/compomemts/CommentInput/index.less
  18. 90 0
      src/pages/layout/pages/find/rank/compomemts/CommentInput/index.tsx
  19. 22 0
      src/pages/layout/pages/find/rank/compomemts/CommentItem/index.css
  20. 25 0
      src/pages/layout/pages/find/rank/compomemts/CommentItem/index.less
  21. 205 0
      src/pages/layout/pages/find/rank/compomemts/CommentItem/index.tsx
  22. 3 4
      src/pages/layout/pages/find/rank/compomemts/RankPlaylist/index.tsx
  23. 1 67
      src/pages/layout/pages/find/rank/compomemts/Rank_Recommend_body_list_Comment/index.css
  24. 3 65
      src/pages/layout/pages/find/rank/compomemts/Rank_Recommend_body_list_Comment/index.less
  25. 27 48
      src/pages/layout/pages/find/rank/compomemts/Rank_Recommend_body_list_Comment/index.tsx
  26. 48 7
      src/pages/layout/pages/find/rank/compomemts/Rank_Recommend_body_list_Comment_list/indes.css
  27. 88 26
      src/pages/layout/pages/find/rank/compomemts/Rank_Recommend_body_list_Comment_list/indes.less
  28. 197 77
      src/pages/layout/pages/find/rank/compomemts/Rank_Recommend_body_list_Comment_list/index.tsx
  29. 44 153
      src/pages/layout/pages/mine/mv/index.tsx
  30. 62 16
      src/pages/layout/pages/musician/MusicianDashboardPage/works/UploadMv/index.tsx
  31. 2 3
      src/pages/layout/pages/musician/MusicianDashboardPage/works/mv/index.css
  32. 1 3
      src/pages/layout/pages/musician/MusicianDashboardPage/works/mv/index.less
  33. 300 27
      src/pages/layout/pages/musician/MusicianDashboardPage/works/mv/index.tsx
  34. 1 1
      src/router/index.tsx
  35. 41 18
      src/utils/calculateMD5.ts
  36. 95 0
      src/utils/chunkUpload.ts

+ 2 - 0
package.json

@@ -25,6 +25,7 @@
     "react-dom": "^19.1.0",
     "react-h5-audio-player": "^3.10.1",
     "react-router-dom": "^7.6.2",
+    "spark-md5": "^3.0.2",
     "zustand": "^5.0.5"
   },
   "devDependencies": {
@@ -34,6 +35,7 @@
     "@types/react": "^19.1.2",
     "@types/react-dom": "^19.1.2",
     "@types/react-router-dom": "^5.3.3",
+    "@types/spark-md5": "^3.0.5",
     "@vitejs/plugin-react": "^4.4.1",
     "eslint": "^9.25.0",
     "eslint-plugin-react-hooks": "^5.2.0",

+ 16 - 0
pnpm-lock.yaml

@@ -53,6 +53,9 @@ importers:
       react-router-dom:
         specifier: ^7.6.2
         version: 7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+      spark-md5:
+        specifier: ^3.0.2
+        version: 3.0.2
       zustand:
         specifier: ^5.0.5
         version: 5.0.5(@types/react@19.1.8)(react@19.1.0)
@@ -75,6 +78,9 @@ importers:
       '@types/react-router-dom':
         specifier: ^5.3.3
         version: 5.3.3
+      '@types/spark-md5':
+        specifier: ^3.0.5
+        version: 3.0.5
       '@vitejs/plugin-react':
         specifier: ^4.4.1
         version: 4.5.2(vite@6.3.5(@types/node@24.0.3))
@@ -685,6 +691,9 @@ packages:
   '@types/react@19.1.8':
     resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==}
 
+  '@types/spark-md5@3.0.5':
+    resolution: {integrity: sha512-lWf05dnD42DLVKQJZrDHtWFidcLrHuip01CtnC2/S6AMhX4t9ZlEUj4iuRlAnts0PQk7KESOqKxeGE/b6sIPGg==}
+
   '@typescript-eslint/eslint-plugin@8.34.1':
     resolution: {integrity: sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1607,6 +1616,9 @@ packages:
     resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
     engines: {node: '>=0.10.0'}
 
+  spark-md5@3.0.2:
+    resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
+
   string-convert@0.2.1:
     resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==}
 
@@ -2289,6 +2301,8 @@ snapshots:
     dependencies:
       csstype: 3.1.3
 
+  '@types/spark-md5@3.0.5': {}
+
   '@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0)(typescript@5.8.3))(eslint@9.29.0)(typescript@5.8.3)':
     dependencies:
       '@eslint-community/regexpp': 4.12.1
@@ -3387,6 +3401,8 @@ snapshots:
 
   source-map-js@1.2.1: {}
 
+  spark-md5@3.0.2: {}
+
   string-convert@0.2.1: {}
 
   strip-json-comments@3.1.1: {}

+ 37 - 0
src/apis/Mv.ts

@@ -0,0 +1,37 @@
+import { request } from "@/utils"
+
+export const AddMvApi = (data: any) => {
+  return request.post('/Mv/add', data)
+}
+
+export const GetMvApi = (data: any) => {
+  return request.post('/Mv/list', data)
+}
+
+export const GetFileUrlByMd5Api = (md5: string) => {
+  return request.get('/media/getFileUrlByMd5', { params: { md5 } })
+}
+
+export const GetMvApiByArtId = (id) => {
+  return request.get('/Mv/Mvlist', { params: { id } })
+}
+
+export const changeMvStatusApi = (data: { id: number; status: number }) => {
+  return request.post('/Mv/UserChangeStatus', data)
+}
+
+export const getMvById = (id: number) => {
+  return request.get('/Mv/getMvById', { params: { id } })
+}
+
+export const toggleMvFavoriteApi = (mvId: number) => {
+  return request.get('/mv-favorite/add', { params: { mvId } })
+}
+
+export const checkMvFavoriteApi = (mvId: number) => {
+  return request.get('/mv-favorite/query', { params: { mvId } })
+}
+
+export const getMvFavoritesApi = () => {
+  return request.get('/mv-favorite/list')
+}

+ 21 - 0
src/apis/comment.ts

@@ -0,0 +1,21 @@
+import { request } from "@/utils"
+
+export const getCommentListByIdType = (data) => {
+  return request.post('/comment/getCommentList', data)
+}
+
+export const addCommentByIdType = (data) => {
+  return request.post('/comment/add', data)
+}
+
+export const addLikeCount = (id: number) => {
+  return request.get(`/comment/AddlikeCount`,{params:{id}})
+}
+
+export const cancelLikeCount = (id: number) => {
+  return request.get(`/comment/CancelLikeCount`,{params:{id}})
+}
+
+export const getCommentListByPid = (id: number) => {
+  return request.get(`/comment/listByPid`,{params:{id}})
+}

+ 24 - 0
src/apis/upload.ts

@@ -21,4 +21,28 @@ export const uploadFile = (bucket: string, file: File, md5: string, onProgress?:
       }
     },
   });
+};
+
+export const chunkUploadApi = (formData) => {
+  return request('/media/upload/part', {
+    method: 'POST',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data',
+    }
+  });
+};
+
+export const mergeChunksApi = (
+  fileKey: string, 
+  totalChunks: number, 
+  fileName: string, 
+  contentType: string
+) => {
+  return request.post('/media/upload/merge', {
+    fileKey,
+    totalChunk: totalChunks,
+    fileName,
+    contentType
+  });
 };

+ 1 - 1
src/pages/layout/components/CreatePlaylistModal/index.tsx

@@ -134,7 +134,7 @@ const CreatePlaylistModal: React.FC<CreatePlaylistModalProps> = ({
 
   return (
     <>
-      {contextHolder} {/* 使用 contextHolder 替代 messageApi */}
+      {contextHolder}
       <Modal
         title="新建歌单"
         open={open}

+ 3 - 0
src/pages/layout/components/LoginContent/components/GiteeLogin.tsx

@@ -35,6 +35,9 @@ const GiteeLogin: React.FC<GiteeLoginProps> = ({ onSuccess }) => {
     <>
       <div 
         onClick={() => setIsModalOpen(true)}
+        //悬浮变手
+
+        style={{cursor: 'pointer'}}
       >
         Gitee 登录
       </div>

+ 16 - 4
src/pages/layout/pages/SongDetail/index.tsx

@@ -46,6 +46,9 @@ const SongDetail = () => {
   const [song, setSong] = useState<Song | null>(null);
   const [loading, setLoading] = useState(true);
   const [error, setError] = useState<string | null>(null);
+  
+  // 添加一个状态用于触发评论列表刷新
+  const [refreshTrigger, setRefreshTrigger] = useState(0);
 
   useEffect(() => {
     const fetchSongDetail = async () => {
@@ -69,17 +72,18 @@ const SongDetail = () => {
     fetchSongDetail();
   }, [id]);
 
+  const handleCommentSubmit = () => {
+    setRefreshTrigger(prev => prev + 1);
+  };
 
   if (loading) {
     return <div className="loading">加载中...</div>;
   }
 
-
   if (error) {
     return <div className="error">错误: {error}</div>;
   }
 
-
   if (!song) {
     return <div className="no-song">歌曲不存在</div>;
   }
@@ -87,8 +91,16 @@ const SongDetail = () => {
   return (
     <div className='SongDetail'>
       <SongDetailPage song={song} />
-      <Rank_Recommend_body_list_Comment />
-      <Rank_Recommend_body_list_Comment_list />
+      <Rank_Recommend_body_list_Comment 
+        contentId={song.id} 
+        type={0} 
+        onCommentSubmit={handleCommentSubmit} // 传递回调函数
+      />
+      <Rank_Recommend_body_list_Comment_list 
+        key={`comment-list-${refreshTrigger}`} // 使用key强制重新渲染
+        contentId={song.id} 
+        type={0} 
+      />
     </div>
   )
 }

+ 90 - 0
src/pages/layout/pages/Video/Video_player/index.css

@@ -0,0 +1,90 @@
+.video-container {
+  padding: 20px;
+  max-width: 1000px;
+  margin: 0 auto;
+  background-color: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+}
+.video-header {
+  margin-bottom: 16px;
+}
+.video-header h1 {
+  margin: 0 0 8px 0;
+  font-size: 24px;
+  font-weight: bold;
+  color: #333;
+}
+.video-header .video-meta {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 8px;
+  font-size: 14px;
+  color: #666;
+}
+.video-header .video-meta .author a {
+  color: #1890ff;
+  text-decoration: none;
+}
+.video-header .video-meta .author a:hover {
+  text-decoration: underline;
+}
+.video-header .video-stats {
+  display: flex;
+  gap: 16px;
+  color: #999;
+}
+.video-player {
+  position: relative;
+  margin-bottom: 16px;
+}
+.video-player .dplayer {
+  border-radius: 8px;
+  overflow: hidden;
+}
+.video-actions {
+  margin: 16px 0;
+  display: flex;
+  gap: 12px;
+  justify-content: center;
+}
+.video-actions .ant-btn {
+  border-radius: 20px;
+  padding: 4px 16px;
+}
+.video-description {
+  margin: 16px 0;
+  padding: 16px;
+  background-color: #f5f5f5;
+  border-radius: 4px;
+}
+.video-description h3 {
+  margin: 0 0 8px 0;
+  font-size: 16px;
+  font-weight: bold;
+}
+.video-description p {
+  margin: 0;
+  line-height: 1.6;
+  color: #333;
+}
+.copyright {
+  margin-top: 16px;
+  padding: 8px;
+  background-color: #f9f9f9;
+  border-radius: 4px;
+  text-align: center;
+  font-size: 12px;
+  color: #999;
+}
+.loading {
+  text-align: center;
+  padding: 20px;
+  color: #999;
+}
+.video-error {
+  text-align: center;
+  padding: 20px;
+  color: #ff4d4f;
+}

+ 108 - 0
src/pages/layout/pages/Video/Video_player/index.less

@@ -0,0 +1,108 @@
+.video-container {
+  padding: 20px;
+  max-width: 1000px;
+  margin: 0 auto;
+  background-color: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+}
+
+.video-header {
+  margin-bottom: 16px;
+
+  h1 {
+    margin: 0 0 8px 0;
+    font-size: 24px;
+    font-weight: bold;
+    color: #333;
+  }
+
+  .video-meta {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-top: 8px;
+    font-size: 14px;
+    color: #666;
+
+    .author {
+      a {
+        color: #1890ff;
+        text-decoration: none;
+
+        &:hover {
+          text-decoration: underline;
+        }
+      }
+    }
+  }
+
+  .video-stats {
+    display: flex;
+    gap: 16px;
+    color: #999;
+  }
+}
+
+.video-player {
+  position: relative;
+  margin-bottom: 16px;
+
+  .dplayer {
+    border-radius: 8px;
+    overflow: hidden;
+  }
+}
+
+.video-actions {
+  margin: 16px 0;
+  display: flex;
+  gap: 12px;
+  justify-content: center;
+
+  .ant-btn {
+    border-radius: 20px;
+    padding: 4px 16px;
+  }
+}
+
+.video-description {
+  margin: 16px 0;
+  padding: 16px;
+  background-color: #f5f5f5;
+  border-radius: 4px;
+
+  h3 {
+    margin: 0 0 8px 0;
+    font-size: 16px;
+    font-weight: bold;
+  }
+
+  p {
+    margin: 0;
+    line-height: 1.6;
+    color: #333;
+  }
+}
+
+.copyright {
+  margin-top: 16px;
+  padding: 8px;
+  background-color: #f9f9f9;
+  border-radius: 4px;
+  text-align: center;
+  font-size: 12px;
+  color: #999;
+}
+
+.loading {
+  text-align: center;
+  padding: 20px;
+  color: #999;
+}
+
+.video-error {
+  text-align: center;
+  padding: 20px;
+  color: #ff4d4f;
+}

+ 282 - 32
src/pages/layout/pages/Video/Video_player/index.tsx

@@ -1,54 +1,304 @@
-import React, { useEffect, useRef } from 'react';
+import { useEffect, useRef, useState } from 'react';
+import { useLocation, useParams } from 'react-router-dom';
 import DPlayer from 'dplayer';
 import './index.css';
-const Video_Player = () => {
-  // 视频地址(示例)
-  const videoUrl = 'http://117.72.120.45:9000/testbucket/123.mp4';
+import {
+  getMvById,
+  toggleMvFavoriteApi,
+  checkMvFavoriteApi,
+  GetFileUrlByMd5Api  // 添加MD5接口导入
+} from '@/apis/Mv';
+import { Button, message } from 'antd';
+import { HeartOutlined, HeartFilled } from '@ant-design/icons';
+
+interface VideoPlayerProps {
+  mvId?: number;
+}
+
+const Video_Player = ({ mvId }: VideoPlayerProps) => {
+  const location = useLocation();
+  const params = useParams<{ id: string }>();
+  const actualMvId = mvId || params.id;
   const dpContainer = useRef<HTMLDivElement>(null);
   const dp = useRef<DPlayer | null>(null);
+  const [mvData, setMvData] = useState<any>(null);
+  const [videoUrl, setVideoUrl] = useState<string | null>(null);
+  const [loading, setLoading] = useState(true);
+  const [isFavorite, setIsFavorite] = useState<boolean>(false);
+  const [favoriteLoading, setFavoriteLoading] = useState(false);
+
+  const checkFavoriteStatus = async (mvId: number) => {
+    if (!mvId) return;
+    try {
+      const response = await checkMvFavoriteApi(mvId);
+      const favStatus = response.data === 1 || response.data === true || response.data === "1" || response.data === "已收藏";
+      setIsFavorite(favStatus);
+    } catch (error) {
+      console.error('检查收藏状态失败:', error);
+      setIsFavorite(false);
+    }
+  };
+
+  // 根据MD5获取实际视频URL
+  const getVideoUrlByMd5 = async (md5: string) => {
+    try {
+      const response = await GetFileUrlByMd5Api(md5);
+      if (response.code === 200 && response.data) {
+        return response.data; // 返回实际的视频URL
+      } else {
+        console.error('获取视频URL失败:', response);
+        return null;
+      }
+    } catch (error) {
+      console.error('请求视频URL失败:', error);
+      return null;
+    }
+  };
+
   useEffect(() => {
-    if (dpContainer.current) {
-      dp.current = new DPlayer({
-        container: dpContainer.current,
-        video: {
-          url: videoUrl,
-          pic: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
-          type: 'auto'
-        },
-        autoplay: false,
-        theme: '#b74747',
-        loop: false,
-        lang: 'zh-cn',
-        screenshot: false,
-        hotkey: true,
-        preload: 'metadata',
-        volume: 0.8,
-        mutex: true,
-        height: 300 // 添加固定高度
-      });
+    const fetchMvData = async () => {
+      if (!actualMvId || isNaN(parseInt(actualMvId))) {
+        setLoading(false);
+        console.error('无效的MV ID:', actualMvId);
+        return;
+      }
+
+      try {
+        setLoading(true);
+        let targetMv: any = null;
+
+        if (location.state && (location.state as any).mv) {
+          targetMv = (location.state as any).mv;
+          setMvData(targetMv);
+        } else {
+          const response = await getMvById(parseInt(actualMvId));
+          if (response.code === 200 && response.data) {
+            targetMv = response.data;
+            setMvData(targetMv);
+          } else {
+            console.error('获取MV详情失败:', response);
+            setLoading(false);
+            return;
+          }
+        }
+
+        // 如果存在videoUrl且为MD5格式,需要通过API获取实际URL
+        if (targetMv && targetMv.videoUrl) {
+          const actualVideoUrl = await getVideoUrlByMd5(targetMv.videoUrl);
+          if (actualVideoUrl) {
+            setVideoUrl(actualVideoUrl);
+            console.log('视频URL获取成功:', actualVideoUrl);
+          } else {
+            console.warn('无法获取视频URL');
+            setVideoUrl(null);
+          }
+        } else {
+          console.warn('MV无有效视频URL:', targetMv?.videoUrl);
+          setVideoUrl(null);
+        }
+
+        if (targetMv && targetMv.id) {
+          await checkFavoriteStatus(targetMv.id);
+        }
+
+      } catch (error) {
+        console.error('获取MV数据失败:', error);
+        setVideoUrl(null);
+      } finally {
+        setLoading(false);
+      }
+    };
+
+    fetchMvData();
+  }, [actualMvId, location.state]);
+
+  // 播放器初始化
+  useEffect(() => {
+    if (dp.current) {
+      dp.current.destroy();
+      dp.current = null;
     }
 
+    const initPlayer = () => {
+      if (videoUrl && dpContainer.current && !dp.current) {
+        try {
+          dp.current = new DPlayer({
+            container: dpContainer.current,
+            video: {
+              url: videoUrl,
+              pic: mvData?.coverUrl || 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
+              type: 'video/mp4'
+            },
+            autoplay: false,
+            theme: '#b74747',
+            loop: false,
+            lang: 'zh-cn',
+            screenshot: false,
+            hotkey: true,
+            preload: 'auto',
+            volume: 0.8,
+            mutex: true,
+            height: 400
+          });
+
+          dp.current.on('ready', () => {
+            console.log('播放器初始化成功');
+          });
+
+          dp.current.on('error', (err: any) => {
+            console.error('视频播放失败:', err);
+            message.error('视频加载失败,请检查视频URL是否有效');
+          });
+        } catch (playerErr) {
+          console.error('播放器创建失败:', playerErr);
+          message.error('播放器初始化异常');
+        }
+      }
+    };
+
+    initPlayer();
+
+    const timer = setTimeout(() => {
+      initPlayer();
+    }, 100);
+
     return () => {
+      clearTimeout(timer);
       if (dp.current) {
         dp.current.destroy();
+        dp.current = null;
       }
     };
-  }, []);
+  }, [videoUrl, mvData]);
+
+  // 核心修复:精准匹配后端字符串返回值,实现双向切换
+  const handleFavorite = async () => {
+    if (!mvData || !mvData.id) {
+      message.warning('MV信息异常,无法操作收藏');
+      console.error('MV ID无效:', mvData?.id);
+      return;
+    }
+
+    setFavoriteLoading(true);
+
+    try {
+      const response = await toggleMvFavoriteApi(mvData.id);
+      // 先判断接口请求成功
+      if (response.code === 200) {
+        const resData = response.data;
+        console.log('收藏接口返回字符串:', resData);
+
+        let newFavStatus = false;
+        // 精准匹配后端返回的字符串:收藏成功
+        if (resData === "收藏成功") {
+          newFavStatus = true;
+          message.success(resData); // 直接使用后端提示语,更统一
+        }
+        // 精准匹配后端返回的字符串:取消收藏成功
+        else if (resData === "已取消收藏") {
+          newFavStatus = false;
+          message.success(resData); // 直接使用后端提示语,更统一
+        }
+        // 强制更新状态(关键)
+        setIsFavorite(newFavStatus);
+        console.log('切换后收藏状态:', newFavStatus);
+      } else {
+        message.warning(`操作异常:${response.message || '未知错误'}`);
+        console.error('收藏接口返回异常:', response);
+        // 兜底:重新拉取最新状态
+        await checkFavoriteStatus(mvData.id);
+      }
+    } catch (error) {
+      console.error('操作收藏失败:', error);
+      message.error('操作失败,请重试');
+      // 兜底:重新拉取最新状态
+      await checkFavoriteStatus(mvData.id);
+    } finally {
+      setFavoriteLoading(false);
+    }
+  };
+
+  // 格式化播放量
+  const formatPlayCount = (count: number): string => {
+    if (!count) return '0';
+    if (count >= 10000) {
+      return `${(count / 10000).toFixed(1)}万`;
+    }
+    return count.toString();
+  };
+
+  // 格式化时长
+  const formatDuration = (duration: number | null): string => {
+    if (duration === null || duration === undefined || duration === 0) return '00:00';
+    const minutes = Math.floor(duration / 60);
+    const seconds = duration % 60;
+    return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
+  };
+
+  if (loading) {
+    return <div className="video-container" style={{ padding: '20px', textAlign: 'center' }}>加载中...</div>;
+  }
+
+  if (!mvData) {
+    return <div className="video-container" style={{ padding: '20px', textAlign: 'center' }}>未找到MV信息</div>;
+  }
 
   return (
-    <div className="video-container">
-      {/* 视频标题和作者 */}
+    <div className="video-container" style={{ padding: '16px' }}>
       <div className="video-header">
-        <h1>【红茶】LOL:钻石宗师局羊刀破败强攻翠神</h1>
-        <span className="author">by <a href="#">乱斗王红茶zz</a></span>
+        <h1 style={{ margin: '0 0 16px 0', fontSize: '20px' }}>{mvData.mvName || '未知MV标题'}</h1>
+        <div className="video-meta" style={{ marginBottom: '16px' }}>
+          <span className="author" style={{ marginRight: '16px' }}>
+            by <a href="#" style={{ color: '#b74747' }}>{mvData.artistName || '未知歌手'}</a>
+          </span>
+          <div className="video-stats" style={{ display: 'inline-block' }}>
+            <span style={{ marginRight: '12px' }}>播放: {formatPlayCount(mvData.viewCount || 0)}</span>
+            <span style={{ marginRight: '12px' }}>点赞: {mvData.likeCount || 0}</span>
+            <span>时长: {formatDuration(mvData.duration)}</span>
+          </div>
+        </div>
       </div>
 
-      <div className="video-player">
-        <div ref={dpContainer} style={{ height: '400px' }} />
+      <div className="video-player" style={{ marginBottom: '16px' }}>
+        <div
+          ref={dpContainer}
+          style={{
+            height: '400px',
+            width: '100%',
+            backgroundColor: '#000',
+            borderRadius: '4px',
+            display: 'flex',
+            alignItems: 'center',
+            justifyContent: 'center'
+          }}
+        >
+          {!videoUrl && <span style={{ color: '#fff' }}>视频资源未加载</span>}
+        </div>
       </div>
 
-      {/* 版权提示 */}
-      <div className="copyright"> 
+      {/* 收藏按钮:确保样式双向切换 */}
+      <div className="video-actions" style={{ marginBottom: '16px' }}>
+        <Button
+          type={isFavorite ? 'primary' : 'default'}
+          icon={isFavorite ? <HeartFilled /> : <HeartOutlined />}
+          loading={favoriteLoading}
+          onClick={handleFavorite}
+          danger={isFavorite}
+          key={`favorite-btn-${isFavorite}`}
+          style={{ transition: 'all 0.3s ease' }}
+        >
+          {isFavorite ? '已收藏' : '收藏'}
+        </Button>
+      </div>
+
+      {mvData.description && (
+        <div className="video-description" style={{ marginBottom: '16px', padding: '16px', backgroundColor: '#f5f5f5', borderRadius: '4px' }}>
+          <h3 style={{ margin: '0 0 8px 0', fontSize: '16px' }}>简介</h3>
+          <p style={{ margin: 0, lineHeight: '1.6' }}>{mvData.description}</p>
+        </div>
+      )}
+
+      <div className="copyright" style={{ padding: '8px 0', color: '#999', fontSize: '12px' }}>
         <span>未经作者授权,禁止转载</span>
       </div>
     </div>

+ 24 - 5
src/pages/layout/pages/Video/index.tsx

@@ -1,14 +1,33 @@
+import { useParams } from 'react-router-dom';
 import Rank_Recommend_body_list_Comment from '../find/rank/compomemts/Rank_Recommend_body_list_Comment'
 import Rank_Recommend_body_list_Comment_list from '../find/rank/compomemts/Rank_Recommend_body_list_Comment_list'
 import './index.css'
 import Video_Player from './Video_player'
+import { useState } from 'react';
+
 const Video = () => {
+  const { id } = useParams<{ id: string }>();
+  const numericId = id ? parseInt(id, 10) : 0;
+  const [refreshTrigger, setRefreshTrigger] = useState(0);
+  const handleCommentSubmit = () => {
+    setRefreshTrigger(prev => prev + 1);
+  };
+
   return (
-  <div className="video">
-    <Video_Player></Video_Player>
-    <Rank_Recommend_body_list_Comment />
-    <Rank_Recommend_body_list_Comment_list />
-  </div>
+    <div className="video">
+      <Video_Player mvId={numericId}></Video_Player>
+      <Rank_Recommend_body_list_Comment 
+        contentId={numericId} 
+        type={1} 
+        onCommentSubmit={handleCommentSubmit}
+      />
+      <Rank_Recommend_body_list_Comment_list 
+        key={`comment-list-${refreshTrigger}`}
+        contentId={numericId} 
+        type={1} 
+      />
+    </div>
   )
 }
+
 export default Video

+ 87 - 13
src/pages/layout/pages/artistinfo/MVs/MVs.tsx

@@ -1,23 +1,97 @@
-import '../index.css'
+import { useEffect, useState } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { GetMvApiByArtId } from '@/apis/Mv';
+import { Image } from 'antd';
+
+
+interface MV {
+  id: number;
+  mvName: string;
+  coverUrl: string;
+  artistId: number;
+  artistName: string;
+  videoUrl: string;
+  description?: string;
+  tags?: string;
+  duration: number | null;
+  viewCount: number;
+  status: number;
+  auditReason?: string;
+  releaseTime: string | null;
+  createTime: string;
+  updateTime: string;
+}
 
 const MVs = () => {
-  const mvs = [
-    { id: 1, title: '离别总是那么突然', cover: 'https://...' },
-    { id: 2, title: '对等关系', cover: 'https://...' },
-  ]
+  const { id } = useParams<{ id: string }>();
+  const navigate = useNavigate();
+  const [mvs, setMvs] = useState<MV[]>([]);
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    const fetchMVs = async () => {
+      try {
+        if (id) {
+          const response = await GetMvApiByArtId(parseInt(id));
+
+          if (response.code === 200) {
+            setMvs(response.data);
+          }
+        }
+      } catch (error) {
+        console.error('获取MV数据失败:', error);
+      } finally {
+        setLoading(false);
+      }
+    };
+
+    fetchMVs();
+  }, [id]);
+
+  if (loading) {
+    return <div>加载中...</div>;
+  }
+
+  // 格式化播放量
+  const formatPlayCount = (count: number): string => {
+    if (count >= 10000) {
+      return `${(count / 10000).toFixed(1)}万`;
+    }
+    return count.toString();
+  };
+
+  const handleMvClick = (mv: MV) => {
+    navigate(`/find/video/${mv.id}`);
+  };
 
   return (
-    <div className="mvs">
-      <div className="mv-grid">
+    <div className="Mine_Mv">
+      <div className="mv-list">
         {mvs.map((mv) => (
-          <div key={mv.id} className="mv-item">
-            <img src={mv.cover} alt={mv.title} />
-            <div>{mv.title}</div>
+          <div
+            key={mv.id}
+            className="mv-item"
+            onClick={() => handleMvClick(mv)}
+            style={{ cursor: 'pointer' }}
+          >
+            <div className="mv-cover">
+              <Image
+                width={140}
+                height={78}
+                preview={false}
+                src={mv.coverUrl}
+              />
+              <span className="mv-play-count">{formatPlayCount(mv.viewCount)}</span>
+            </div>
+            <div className="mv-info">
+              <div className="mv-title">{mv.mvName}</div>
+              <div className="mv-artist">{mv.artistName}</div>
+            </div>
           </div>
         ))}
       </div>
     </div>
-  )
-}
+  );
+};
 
-export default MVs
+export default MVs;

+ 65 - 0
src/pages/layout/pages/artistinfo/MVs/index.css

@@ -0,0 +1,65 @@
+.Mine_Mv {
+  width: 100%;
+  padding: 20px;
+}
+.mv-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 24px;
+  justify-content: flex-start;
+}
+.mv-item {
+  width: calc(25% - 18px);
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  box-sizing: border-box;
+}
+.mv-cover {
+  width: 100%;
+  height: 0;
+  padding-bottom: 56.25%;
+  /* 16:9 比例 */
+  position: relative;
+  overflow: hidden;
+  border-radius: 4px;
+}
+.mv-cover img {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+/* 播放量 - 右上角 */
+.mv-play-count {
+  position: absolute;
+  top: 4px;
+  right: 4px;
+  background: rgba(0, 0, 0, 0.6);
+  color: #fff;
+  padding: 2px 4px;
+  border-radius: 2px;
+  font-size: 10px;
+}
+.mv-info {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  font-size: 12px;
+  color: #666;
+}
+.mv-title {
+  font-size: 14px;
+  font-weight: 500;
+  color: #333;
+  margin: 0;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.mv-artist {
+  color: #999;
+  font-size: 12px;
+}

+ 72 - 0
src/pages/layout/pages/artistinfo/MVs/index.less

@@ -0,0 +1,72 @@
+.Mine_Mv {
+  width: 100%;
+  padding: 20px;
+}
+
+.mv-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 24px;
+  justify-content: flex-start;
+}
+
+.mv-item {
+  width: calc(25% - 18px);
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  box-sizing: border-box;
+}
+
+.mv-cover {
+  width: 100%;
+  height: 0;
+  padding-bottom: 56.25%; /* 16:9 比例 */
+  position: relative;
+  overflow: hidden;
+  border-radius: 4px;
+}
+
+.mv-cover img {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+/* 播放量 - 右上角 */
+.mv-play-count {
+  position: absolute;
+  top: 4px;
+  right: 4px;
+  background: rgba(0, 0, 0, 0.6);
+  color: #fff;
+  padding: 2px 4px;
+  border-radius: 2px;
+  font-size: 10px;
+}
+
+.mv-info {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  font-size: 12px;
+  color: #666;
+}
+
+.mv-title {
+  font-size: 14px;
+  font-weight: 500;
+  color: #333;
+  margin: 0;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.mv-artist {
+  color: #999;
+  font-size: 12px;
+}

+ 69 - 0
src/pages/layout/pages/find/rank/compomemts/CommentInput/index.css

@@ -0,0 +1,69 @@
+.comment-input-container {
+  margin: 30px 0 30px 40px;
+}
+.comment-input-container .comment-input-content {
+  display: flex;
+  margin-top: 20px;
+}
+.comment-input-container .comment-input-content .comment-input-avatar {
+  width: 50px;
+  height: 50px;
+  margin-right: 10px;
+}
+.comment-input-container .comment-input-content .comment-input-box {
+  position: relative;
+  padding-left: 10px;
+  margin-left: 10px;
+  width: 600px;
+}
+.comment-input-container .comment-input-content .comment-input-box .comment-input-button-group {
+  display: flex;
+  justify-content: space-between;
+  line-height: 40px;
+  height: 40px;
+}
+.comment-input-container .comment-input-content .comment-input-box .comment-input-button-group .comment-input-button-left {
+  color: #999999;
+  display: flex;
+}
+.comment-input-container .comment-input-content .comment-input-box .comment-input-button-group .comment-input-button-left .comment-input-button-left-icon {
+  font-size: 20px;
+  margin-right: 10px;
+}
+.comment-input-container .comment-input-content .comment-input-box .comment-input-button-group .comment-input-button-left .comment-input-button-left-text {
+  font-size: 25px;
+}
+.comment-input-container .comment-input-content .comment-input-box .comment-input-button-group .comment-input-button-right {
+  display: flex;
+}
+.comment-input-container .comment-input-content .comment-input-box .comment-input-button-group .comment-input-button-right .comment-input-button-right-text {
+  font-size: 20px;
+  margin-right: 10px;
+  color: #999999;
+}
+.comment-input-container .comment-input-content .comment-input-box .comment-input-button-group .comment-input-button-right .comment-input-button-right-button {
+  margin: auto 0;
+}
+.comment-input-container .comment-input-content .comment-input-box .comment-input-button-group .comment-input-button-right .comment-input-button-right-button .comment-input-button-right-submit {
+  cursor: pointer;
+  width: 60px;
+  height: 30px;
+  background: #4491da;
+  text-align: center;
+  line-height: 30px;
+  border-radius: 3px;
+  color: #ffffff;
+}
+.comment-input-container .comment-input-content .comment-input-box .comment-input-button-group .comment-input-button-right .comment-input-button-right-button .comment-input-button-right-submit:hover {
+  background: #55a4ef;
+}
+.comment-input-container .comment-input-content .comment-input-box::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 10px;
+  border-width: 6px 8px;
+  border-style: solid;
+  border-color: transparent #ffffff transparent transparent;
+  background: none;
+}

+ 72 - 0
src/pages/layout/pages/find/rank/compomemts/CommentInput/index.less

@@ -0,0 +1,72 @@
+// src/components/CommentInput/index.less
+.comment-input-container {
+  margin: 30px 0 30px 40px;
+
+  .comment-input-content {
+    display: flex;
+    margin-top: 20px;
+    .comment-input-avatar {
+      width: 50px;
+      height: 50px;
+      margin-right: 10px;
+    }
+    .comment-input-box {
+      position: relative;
+      padding-left: 10px;
+      margin-left: 10px;
+      width: 600px;
+      .comment-input-button-group {
+        display: flex;
+        justify-content: space-between;
+        line-height: 40px;
+        height: 40px;
+        .comment-input-button-left {
+          color: rgb(153, 153, 153);
+          display: flex;
+          .comment-input-button-left-icon {
+            font-size: 20px;
+            margin-right: 10px;
+          }
+          .comment-input-button-left-text {
+            font-size: 25px;
+          }
+        }
+        .comment-input-button-right {
+          display: flex;
+          .comment-input-button-right-text {
+            font-size: 20px;
+            margin-right: 10px;
+            color: rgb(153, 153, 153);
+          }
+          .comment-input-button-right-button {
+            margin: auto 0;
+            .comment-input-button-right-submit {
+              cursor: pointer;
+              width: 60px;
+              height: 30px;
+              background: rgb(68, 145, 218);
+              text-align: center;
+              line-height: 30px;
+              border-radius: 3px;
+              color: #ffffff;
+              &:hover {
+                background: rgb(85, 164, 239);
+              }
+            }
+          }
+        }
+      }
+    }
+
+    .comment-input-box::before {
+      content: '';
+      position: absolute;
+      left: 0;
+      top: 10px;
+      border-width: 6px 8px;
+      border-style: solid;
+      border-color: transparent #ffffff transparent transparent;
+      background: none;
+    }
+  }
+}

+ 90 - 0
src/pages/layout/pages/find/rank/compomemts/CommentInput/index.tsx

@@ -0,0 +1,90 @@
+import { Image, message } from 'antd'
+import './index.css'
+import TextArea from 'antd/es/input/TextArea'
+import { useState } from 'react'
+import { SmileOutlined } from '@ant-design/icons'
+import { useLocation } from 'react-router-dom'
+import { addCommentByIdType } from '@/apis/comment'
+import { getUserinfo } from '@/utils'
+interface CommentInputProps {
+  contentId?: number
+  parentId?: number | null
+  onSubmit: () => void;
+  type?: number
+}
+
+const CommentInput = ({ contentId, parentId, onSubmit, type }: CommentInputProps) => {
+  const location = useLocation()
+  const [value, setValue] = useState('')
+  const mvData = location.state?.mv || null
+  const [messageApi, contextHolder] = message.useMessage();
+  const user = getUserinfo()
+  const handleSubmit = () => {
+    if (value.trim()) {
+      console.log("提交内容:" + value)
+      console.log("提交内容类型:" + type)
+      console.log("提交内容ID:" + contentId)
+      console.log("提交内容父ID:" + parentId)
+      const src = user.url
+      const username = user.name
+      const res = addCommentByIdType({ contentId, parentId, value, type, src, username })
+      if (res.code != 200) {
+        onSubmit()
+        setValue('')
+      } else {
+        messageApi.open({
+          type: 'error',
+          content: '发送异常',
+        })
+      }
+    }
+  }
+
+  return (
+    <div className="comment-input-container">
+      {contextHolder}
+      <div className="comment-input-content">
+        <div className="comment-input-avatar">
+          <Image
+            src={user.url || 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'}
+            width={50}
+            height={50}
+            preview={false}
+          />
+        </div>
+        <div className="comment-input-box">
+          <TextArea
+            value={value}
+            onChange={(e) => setValue(e.target.value)}
+            placeholder={`对 "${mvData?.mvName || '这个视频'}" 的评论`}
+            autoSize={{ minRows: 3, maxRows: 5 }}
+            style={{ width: '620px' }}
+          />
+          <div className="comment-input-button-group">
+            <div className="comment-input-button-left">
+              <div className="comment-input-button-left-icon">
+                <SmileOutlined />
+              </div>
+
+            </div>
+            <div className="comment-input-button-right">
+              <div className="comment-input-button-right-text">
+                {value.length}/140
+              </div>
+              <div className="comment-input-button-right-button">
+                <div
+                  className="comment-input-button-right-submit"
+                  onClick={handleSubmit}
+                >
+                  评论
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default CommentInput;

+ 22 - 0
src/pages/layout/pages/find/rank/compomemts/CommentItem/index.css

@@ -0,0 +1,22 @@
+/* comment-item.css */
+.comment-item-container {
+  position: relative;
+  margin-bottom: 10px;
+}
+.comment-content-wrapper {
+  display: flex;
+  align-items: flex-start;
+}
+.Rank_Recommend_body_list_Comment_list_content_left {
+  margin-right: 10px;
+}
+.Rank_Recommend_body_list_Comment_list_content_right {
+  flex: 1;
+}
+/* 确保子评论有正确的缩进 */
+.sub-comments-container {
+  margin-left: 20px;
+  /* 或者使用 level 计算的值 */
+  padding-left: 10px;
+  border-left: 1px solid #e8e8e8;
+}

+ 25 - 0
src/pages/layout/pages/find/rank/compomemts/CommentItem/index.less

@@ -0,0 +1,25 @@
+/* comment-item.css */
+.comment-item-container {
+  position: relative;
+  margin-bottom: 10px;
+}
+
+.comment-content-wrapper {
+  display: flex;
+  align-items: flex-start;
+}
+
+.Rank_Recommend_body_list_Comment_list_content_left {
+  margin-right: 10px;
+}
+
+.Rank_Recommend_body_list_Comment_list_content_right {
+  flex: 1;
+}
+
+/* 确保子评论有正确的缩进 */
+.sub-comments-container {
+  margin-left: 20px; /* 或者使用 level 计算的值 */
+  padding-left: 10px;
+  border-left: 1px solid #e8e8e8;
+}

+ 205 - 0
src/pages/layout/pages/find/rank/compomemts/CommentItem/index.tsx

@@ -0,0 +1,205 @@
+// CommentItem.tsx
+import { Image, Dropdown, Menu } from 'antd';
+import { LikeOutlined, DeleteOutlined } from '@ant-design/icons';
+import { useState } from 'react';
+import CommentInput from '../CommentInput';
+import { getCommentListByPid } from '@/apis/comment';
+import { request } from '@/utils';
+
+interface CommentItemProps {
+  item: {
+    id: number;
+    userId: number;
+    contentType: number;
+    src: string;
+    username: string;
+    contentId: number;
+    parentId: number | null;
+    content: string;
+    likeCount: number;
+    replyCount: number;
+    status: number;
+    createTime: string;
+    updateTime: string;
+  };
+  contentId?: number;
+  type?: number;
+  level?: number;
+  likedComments: number[];
+  activeCommentBox: { type: 'reply' | 'main', id?: number } | null;
+  onToggleLike: (commentId: number) => void;
+  onToggleReply: (id: number) => void;
+  onReplySubmit: () => void;
+  onCommentDeleted?: (commentId: number) => void;
+}
+
+const CommentItem = ({
+  item,
+  contentId,
+  type,
+  level = 0,
+  likedComments,
+  activeCommentBox,
+  onToggleLike,
+  onToggleReply,
+  onReplySubmit,
+  onCommentDeleted
+}: CommentItemProps) => {
+  // 展开状态管理
+  const [expanded, setExpanded] = useState(false);
+  const [replies, setReplies] = useState<any[]>([]);
+  const [loadingReplies, setLoadingReplies] = useState(false);
+
+  // 获取子评论
+  const fetchReplies = async () => {
+    if (loadingReplies) return;
+    
+    setLoadingReplies(true);
+    try {
+      const res = await getCommentListByPid(item.id);
+      setReplies(res.data);
+      setExpanded(true);
+    } catch (error) {
+      console.error('获取子评论失败:', error);
+    } finally {
+      setLoadingReplies(false);
+    }
+  };
+
+  // 切换展开状态
+  const toggleExpand = () => {
+    if (expanded) {
+      setExpanded(false);
+    } else {
+      fetchReplies();
+    }
+  };
+
+  // 删除评论
+  const deleteComment = async (commentId: number) => {
+    try {
+      await request.get(`/comment/delete`, { params: { id: commentId } });
+      console.log('评论删除成功');
+      if (onCommentDeleted) {
+        onCommentDeleted(commentId);
+      }
+    } catch (error) {
+      console.error('删除评论失败:', error);
+    }
+  };
+
+  // 计算缩进
+  const marginLeft = level == 1 ? `${level * 20}px` : '0px';
+
+  // 右键菜单
+  const menu = (
+    <Menu>
+      <Menu.Item key="delete" icon={<DeleteOutlined />} danger onClick={() => deleteComment(item.id)}>
+        删除评论
+      </Menu.Item>
+    </Menu>
+  );
+
+  // 阻止右键默认事件
+  const handleContextMenu = (e: React.MouseEvent) => {
+    e.preventDefault();
+  };
+
+  return (
+    <div 
+      className="comment-item-container" 
+      style={{ marginLeft }}
+      onContextMenu={handleContextMenu}
+    >
+      <Dropdown overlay={menu} trigger={['contextMenu']}>
+        <div className="comment-content-wrapper">
+          <div className="Rank_Recommend_body_list_Comment_list_content_left">
+            <Image width={50} src={item.src} preview={false} />
+          </div>
+          <div className="Rank_Recommend_body_list_Comment_list_content_right">
+            <div className="Rank_Recommend_body_list_Comment_list_content_right_top">
+              <div className="Rank_Recommend_body_list_Comment_list_content_right_top_username">
+                {item.username}
+              </div>
+              <div className="Rank_Recommend_body_list_Comment_list_content_right_top_content">
+                {item.content}
+              </div>
+            </div>
+            <div className="Rank_Recommend_body_list_Comment_list_content_right_bottom">
+              <div className="Rank_Recommend_body_list_Comment_list_content_right_bottom_time">
+                {new Date(item.createTime).toLocaleDateString('zh-CN')}
+                {item.replyCount > 0 && (
+                  <div 
+                    className='Rank_Recommend_body_list_Comment_list_content_right_bottom_time_button'
+                    onClick={toggleExpand}
+                    style={{ cursor: 'pointer', marginLeft: '10px' }}
+                  >
+                    {expanded ? `收起 (${replies.length})` : `展开 ${item.replyCount} 条回复`}
+                  </div>
+                )}
+              </div>
+
+              <div className="Rank_Recommend_body_list_Comment_list_content_right_bottom_right">
+                <div className="Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like">
+                  <div 
+                    className={`Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like_icon ${likedComments.includes(item.id) ? 'liked' : ''}`}
+                    onClick={() => onToggleLike(item.id)}
+                    style={{ cursor: 'pointer', color: likedComments.includes(item.id) ? '#1890ff' : 'inherit' }}
+                  >
+                    <LikeOutlined />
+                    {item.likeCount === 0 ? '' : item.likeCount}
+                  </div>
+                  <div
+                    className="Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like_button"
+                    onClick={() => onToggleReply(item.id)}
+                  >
+                    回复
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </Dropdown>
+
+      {/* 显示回复框 - 在评论下方 */}
+      {activeCommentBox?.type === 'reply' && activeCommentBox.id === item.id && (
+        <div className="comment-reply-box">
+          <CommentInput
+            contentId={contentId}
+            parentId={item.id}
+            onSubmit={onReplySubmit}
+            type={type}
+          />
+        </div>
+      )}
+
+      {/* 展开的子评论 */}
+      {expanded && (
+        <div className="sub-comments-container">
+          {loadingReplies ? (
+            <div>加载中...</div>
+          ) : (
+            replies.map((reply, index) => (
+              <CommentItem
+                key={`${reply.id}-${index}`}
+                item={reply}
+                contentId={contentId}
+                type={type}
+                level={level + 1}
+                likedComments={likedComments}
+                activeCommentBox={activeCommentBox}
+                onToggleLike={onToggleLike}
+                onToggleReply={onToggleReply}
+                onReplySubmit={onReplySubmit}
+                onCommentDeleted={onCommentDeleted}
+              />
+            ))
+          )}
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default CommentItem;

+ 3 - 4
src/pages/layout/pages/find/rank/compomemts/RankPlaylist/index.tsx

@@ -1,4 +1,3 @@
-// src/pages/layout/pages/find/rank/components/RankPlaylist/index.tsx
 import Rank_Recommend_body_right from '../../compomemts/Rank_Recommend_body_right'
 import Rank_Recommend_body_list from '../../compomemts/Rank_Recommend_body_list'
 import Rank_Recommend_body_list_Comment from '../../compomemts/Rank_Recommend_body_list_Comment'
@@ -6,11 +5,11 @@ import Rank_Recommend_body_list_Comment_list from '../../compomemts/Rank_Recomme
 
 const RankPlaylist = () => {
   return (
-    <div>
+    <div style={{ marginBottom: '40px' }}>
       <Rank_Recommend_body_right />
       <Rank_Recommend_body_list />
-      <Rank_Recommend_body_list_Comment />
-      <Rank_Recommend_body_list_Comment_list />
+      <Rank_Recommend_body_list_Comment contentId={"123"} type={2} />
+      <Rank_Recommend_body_list_Comment_list contentId={"111"} type={2} />
     </div>
   )
 }

+ 1 - 67
src/pages/layout/pages/find/rank/compomemts/Rank_Recommend_body_list_Comment/index.css

@@ -1,76 +1,10 @@
 .Rank_Recommend_body_list_Comment {
   margin: 30px 0 30px 40px;
 }
-.Rank_Recommend_body_list_Comment .Rank_Recommend_body_list_Comment_title {
+.Rank_Recommend_body_list_Comment .comment-input-title {
   margin-right: 30px;
   font-size: 20px;
   height: 30px;
   line-height: 30px;
   border-bottom: 2px solid red;
 }
-.Rank_Recommend_body_list_Comment .Rank_Recommend_body_list_Comment_content {
-  display: flex;
-  margin-top: 20px;
-}
-.Rank_Recommend_body_list_Comment .Rank_Recommend_body_list_Comment_content .comment-box {
-  position: relative;
-  padding-left: 10px;
-  /* 避免文字贴着箭头 */
-  margin-left: 10px;
-  width: 600px;
-}
-.Rank_Recommend_body_list_Comment .Rank_Recommend_body_list_Comment_content .comment-box .Rank_Recommend_body_list_Comment_button {
-  display: flex;
-  justify-content: space-between;
-  line-height: 40px;
-  height: 40px;
-}
-.Rank_Recommend_body_list_Comment .Rank_Recommend_body_list_Comment_content .comment-box .Rank_Recommend_body_list_Comment_button .Rank_Recommend_body_list_Comment_button_left {
-  color: #999999;
-  display: flex;
-}
-.Rank_Recommend_body_list_Comment .Rank_Recommend_body_list_Comment_content .comment-box .Rank_Recommend_body_list_Comment_button .Rank_Recommend_body_list_Comment_button_left .Rank_Recommend_body_list_Comment_button_left_icon {
-  font-size: 20px;
-  margin-right: 10px;
-}
-.Rank_Recommend_body_list_Comment .Rank_Recommend_body_list_Comment_content .comment-box .Rank_Recommend_body_list_Comment_button .Rank_Recommend_body_list_Comment_button_left .Rank_Recommend_body_list_Comment_button_left_text {
-  font-size: 25px;
-}
-.Rank_Recommend_body_list_Comment .Rank_Recommend_body_list_Comment_content .comment-box .Rank_Recommend_body_list_Comment_button .Rank_Recommend_body_list_Comment_button_right {
-  display: flex;
-}
-.Rank_Recommend_body_list_Comment .Rank_Recommend_body_list_Comment_content .comment-box .Rank_Recommend_body_list_Comment_button .Rank_Recommend_body_list_Comment_button_right .Rank_Recommend_body_list_Comment_button_right_text {
-  font-size: 20px;
-  margin-right: 10px;
-  color: #999999;
-}
-.Rank_Recommend_body_list_Comment .Rank_Recommend_body_list_Comment_content .comment-box .Rank_Recommend_body_list_Comment_button .Rank_Recommend_body_list_Comment_button_right .Rank_Recommend_body_list_Comment_button_right_button {
-  margin: auto 0;
-}
-.Rank_Recommend_body_list_Comment .Rank_Recommend_body_list_Comment_content .comment-box .Rank_Recommend_body_list_Comment_button .Rank_Recommend_body_list_Comment_button_right .Rank_Recommend_body_list_Comment_button_right_button .Rank_Recommend_body_list_Comment_button_right_button_submit {
-  cursor: pointer;
-  width: 60px;
-  height: 30px;
-  background: #4491da;
-  text-align: center;
-  line-height: 30px;
-  border-radius: 3px;
-  color: #ffffff;
-}
-.Rank_Recommend_body_list_Comment .Rank_Recommend_body_list_Comment_content .comment-box .Rank_Recommend_body_list_Comment_button .Rank_Recommend_body_list_Comment_button_right .Rank_Recommend_body_list_Comment_button_right_button .Rank_Recommend_body_list_Comment_button_right_button_submit:hover {
-  background: #55a4ef;
-}
-.Rank_Recommend_body_list_Comment .Rank_Recommend_body_list_Comment_content .comment-box::before {
-  content: '';
-  position: absolute;
-  left: 0;
-  top: 10px;
-  /* 调整垂直位置 */
-  border-width: 6px 8px;
-  /* 控制大小 */
-  border-style: solid;
-  border-color: transparent #ffffff transparent transparent;
-  /* 小尖角朝左 */
-  background: none;
-  /* 确保没有背景色干扰 */
-}

+ 3 - 65
src/pages/layout/pages/find/rank/compomemts/Rank_Recommend_body_list_Comment/index.less

@@ -1,73 +1,11 @@
 .Rank_Recommend_body_list_Comment {
   margin: 30px 0 30px 40px;
-  .Rank_Recommend_body_list_Comment_title {
+
+  .comment-input-title {
     margin-right: 30px;
     font-size: 20px;
     height: 30px;
     line-height: 30px;
     border-bottom: 2px solid red;
   }
-  .Rank_Recommend_body_list_Comment_content {
-    display: flex;
-    margin-top: 20px;
-    .comment-box {
-      position: relative;
-      padding-left: 10px; /* 避免文字贴着箭头 */
-      margin-left: 10px;
-      width: 600px;
-      .Rank_Recommend_body_list_Comment_button {
-        display: flex;
-        justify-content: space-between;
-
-        line-height: 40px;
-        height: 40px;
-        .Rank_Recommend_body_list_Comment_button_left {
-          color: rgb(153, 153, 153);
-          display: flex;
-          .Rank_Recommend_body_list_Comment_button_left_icon {
-            font-size: 20px;
-            margin-right: 10px;
-          }
-          .Rank_Recommend_body_list_Comment_button_left_text {
-            font-size: 25px;
-          }
-        }
-        .Rank_Recommend_body_list_Comment_button_right {
-          display: flex;
-          .Rank_Recommend_body_list_Comment_button_right_text {
-            font-size: 20px;
-            margin-right: 10px;
-            color: rgb(153, 153, 153);
-          }
-          .Rank_Recommend_body_list_Comment_button_right_button {
-            margin: auto 0;
-            .Rank_Recommend_body_list_Comment_button_right_button_submit {
-              cursor: pointer;
-              width: 60px;
-              height: 30px;
-              background: rgb(68, 145, 218);
-              text-align: center;
-              line-height: 30px;
-              border-radius: 3px;
-              color: #ffffff;
-              &:hover {
-                background: rgb(85, 164, 239);
-              }
-            }
-          }
-        }
-      }
-    }
-
-    .comment-box::before {
-      content: '';
-      position: absolute;
-      left: 0;
-      top: 10px; /* 调整垂直位置 */
-      border-width: 6px 8px; /* 控制大小 */
-      border-style: solid;
-      border-color: transparent #ffffff transparent transparent; /* 小尖角朝左 */
-      background: none; /* 确保没有背景色干扰 */
-    }
-  }
-}
+}

+ 27 - 48
src/pages/layout/pages/find/rank/compomemts/Rank_Recommend_body_list_Comment/index.tsx

@@ -1,51 +1,30 @@
-import {Image } from 'antd'
-import './index.css'
-import TextArea from 'antd/es/input/TextArea'
-import { useState } from 'react'
-import { SmileOutlined } from '@ant-design/icons'
-const Rank_Recommend_body_list_Comment = () => {
-  const [value, setValue] = useState('')
+import CommentInput from '../CommentInput';
+import './index.css';
+
+interface CommentProps {
+  contentId?: number;
+  type: number;
+  onCommentSubmit?: () => void; // 添加回调函数属性
+}
+
+const Rank_Recommend_body_list_Comment = ({ contentId, type, onCommentSubmit }: CommentProps) => {
+  const handleSubmitComment = () => {
+    if (onCommentSubmit) {
+      onCommentSubmit();
+    }
+  }
+
   return (
     <div className="Rank_Recommend_body_list_Comment">
-      <div className="Rank_Recommend_body_list_Comment_title">评论</div>
-      <div className="Rank_Recommend_body_list_Comment_content">
-        <div className="Rank_Recommend_body_list_Comment_content_Img">
-          <Image
-            src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
-            width={50}
-            height={50}
-            preview={false}
-          />
-        </div>
-        <div className="comment-box">
-          <TextArea
-            value={value}
-            onChange={(e) => setValue(e.target.value)}
-            placeholder="评论"
-            autoSize={{ minRows: 3, maxRows: 5 }}
-            style={{ width: '620px' }}
-          />
-          <div className="Rank_Recommend_body_list_Comment_button">
-            <div className="Rank_Recommend_body_list_Comment_button_left">
-              <div className="Rank_Recommend_body_list_Comment_button_left_icon">
-                <SmileOutlined />
-              </div>
-              <div className="Rank_Recommend_body_list_Comment_button_left_text">
-                @
-              </div>
-            </div>
-            <div className="Rank_Recommend_body_list_Comment_button_right">
-              <div className="Rank_Recommend_body_list_Comment_button_right_text">
-                123
-              </div>
-              <div className="Rank_Recommend_body_list_Comment_button_right_button">
-                <div className='Rank_Recommend_body_list_Comment_button_right_button_submit'>评论</div>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
+      <div className="comment-input-title">评论</div>
+      <CommentInput
+        contentId={contentId}
+        onSubmit={handleSubmitComment}
+        type={type}
+        parentId={null}
+      />
     </div>
-  )
-}
-export default Rank_Recommend_body_list_Comment
+  );
+};
+
+export default Rank_Recommend_body_list_Comment;

+ 48 - 7
src/pages/layout/pages/find/rank/compomemts/Rank_Recommend_body_list_Comment_list/indes.css

@@ -10,33 +10,74 @@
 }
 .Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content {
   margin-top: 20px;
-  display: flex;
   padding-bottom: 20px;
   border-bottom: 1px solid #d7d1d1;
+  position: relative;
+}
+.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .comment-content-wrapper {
+  display: flex;
 }
-.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .Rank_Recommend_body_list_Comment_list_content_right {
+.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .comment-content-wrapper .Rank_Recommend_body_list_Comment_list_content_left {
+  width: 50px;
+  flex-shrink: 0;
+}
+.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .comment-content-wrapper .Rank_Recommend_body_list_Comment_list_content_right {
   margin-left: 20px;
   display: flex;
   flex-direction: column;
   justify-content: space-between;
   width: 640px;
+  flex: 1;
 }
-.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .Rank_Recommend_body_list_Comment_list_content_right .Rank_Recommend_body_list_Comment_list_content_right_top {
+.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .comment-content-wrapper .Rank_Recommend_body_list_Comment_list_content_right .Rank_Recommend_body_list_Comment_list_content_right_top {
   display: flex;
+  flex-direction: column;
+  margin-bottom: 10px;
+}
+.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .comment-content-wrapper .Rank_Recommend_body_list_Comment_list_content_right .Rank_Recommend_body_list_Comment_list_content_right_top .Rank_Recommend_body_list_Comment_list_content_right_top_username {
+  font-weight: bold;
+  margin-bottom: 5px;
+  color: #333;
 }
-.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .Rank_Recommend_body_list_Comment_list_content_right .Rank_Recommend_body_list_Comment_list_content_right_bottom {
+.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .comment-content-wrapper .Rank_Recommend_body_list_Comment_list_content_right .Rank_Recommend_body_list_Comment_list_content_right_top .Rank_Recommend_body_list_Comment_list_content_right_top_content {
+  line-height: 1.5;
+  color: #666;
+}
+.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .comment-content-wrapper .Rank_Recommend_body_list_Comment_list_content_right .Rank_Recommend_body_list_Comment_list_content_right_bottom {
   display: flex;
   justify-content: space-between;
+  align-items: center;
+}
+.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .comment-content-wrapper .Rank_Recommend_body_list_Comment_list_content_right .Rank_Recommend_body_list_Comment_list_content_right_bottom .Rank_Recommend_body_list_Comment_list_content_right_bottom_time {
+  color: #999;
+  font-size: 12px;
+  display: flex;
+  gap: 10px;
 }
-.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .Rank_Recommend_body_list_Comment_list_content_right .Rank_Recommend_body_list_Comment_list_content_right_bottom .Rank_Recommend_body_list_Comment_list_content_right_bottom_right .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like {
+.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .comment-content-wrapper .Rank_Recommend_body_list_Comment_list_content_right .Rank_Recommend_body_list_Comment_list_content_right_bottom .Rank_Recommend_body_list_Comment_list_content_right_bottom_time .Rank_Recommend_body_list_Comment_list_content_right_bottom_time_button {
+  color: #519ff2;
+  cursor: pointer;
+}
+.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .comment-content-wrapper .Rank_Recommend_body_list_Comment_list_content_right .Rank_Recommend_body_list_Comment_list_content_right_bottom .Rank_Recommend_body_list_Comment_list_content_right_bottom_right .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like {
   display: flex;
+  align-items: center;
 }
-.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .Rank_Recommend_body_list_Comment_list_content_right .Rank_Recommend_body_list_Comment_list_content_right_bottom .Rank_Recommend_body_list_Comment_list_content_right_bottom_right .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like_icon {
+.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .comment-content-wrapper .Rank_Recommend_body_list_Comment_list_content_right .Rank_Recommend_body_list_Comment_list_content_right_bottom .Rank_Recommend_body_list_Comment_list_content_right_bottom_right .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like_icon {
   color: #0088fe;
   margin-right: 20px;
+  cursor: pointer;
+}
+.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .comment-content-wrapper .Rank_Recommend_body_list_Comment_list_content_right .Rank_Recommend_body_list_Comment_list_content_right_bottom .Rank_Recommend_body_list_Comment_list_content_right_bottom_right .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like_icon:hover {
+  color: #1890ff;
 }
-.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .Rank_Recommend_body_list_Comment_list_content_right .Rank_Recommend_body_list_Comment_list_content_right_bottom .Rank_Recommend_body_list_Comment_list_content_right_bottom_right .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like_button {
+.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .comment-content-wrapper .Rank_Recommend_body_list_Comment_list_content_right .Rank_Recommend_body_list_Comment_list_content_right_bottom .Rank_Recommend_body_list_Comment_list_content_right_bottom_right .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like_button {
   color: #6f6f6f;
+  cursor: pointer;
+  padding: 4px 8px;
+  border-radius: 4px;
+}
+.Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_content .comment-content-wrapper .Rank_Recommend_body_list_Comment_list_content_right .Rank_Recommend_body_list_Comment_list_content_right_bottom .Rank_Recommend_body_list_Comment_list_content_right_bottom_right .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like_button:hover {
+  background-color: #f0f0f0;
 }
 .Rank_Recommend_body_list_Comment_list .Rank_Recommend_body_list_Comment_list_Pagination {
   margin-top: 20px;

+ 88 - 26
src/pages/layout/pages/find/rank/compomemts/Rank_Recommend_body_list_Comment_list/indes.less

@@ -1,5 +1,7 @@
+// indes.less
 .Rank_Recommend_body_list_Comment_list {
   padding: 40px;
+
   .Rank_Recommend_body_list_Comment_list_ttitle {
     color: rgb(0, 0, 0);
     font-weight: bolder;
@@ -7,49 +9,109 @@
     height: 40px;
     border-bottom: 1px solid rgb(125, 121, 121);
   }
+
   .Rank_Recommend_body_list_Comment_list_content {
     margin-top: 20px;
-    display: flex;
     padding-bottom: 20px;
     border-bottom: 1px solid rgb(215, 209, 209);
-    .Rank_Recommend_body_list_Comment_list_content_right {
-      margin-left: 20px;
-      display: flex;
-      flex-direction: column; // 纵向排列内容
-      justify-content: space-between; // 可选:自动分配上下空间
-      width: 640px;
-      .Rank_Recommend_body_list_Comment_list_content_right_top {
-        display: flex;
-        .Rank_Recommend_body_list_Comment_list_content_right_top_username {
-        }
-        .Rank_Recommend_body_list_Comment_list_content_right_top_content {
-        }
+    position: relative; // 为回复框定位做准备
+
+    .comment-content-wrapper {
+      display: flex; // 保持左右布局
+
+      .Rank_Recommend_body_list_Comment_list_content_left {
+        width: 50px;
+        flex-shrink: 0; // 防止压缩
       }
-      .Rank_Recommend_body_list_Comment_list_content_right_bottom {
+
+      .Rank_Recommend_body_list_Comment_list_content_right {
+        margin-left: 20px;
         display: flex;
-        //水平靠下对齐
+        flex-direction: column;
         justify-content: space-between;
-        .Rank_Recommend_body_list_Comment_list_content_right_bottom_time {
+        width: 640px;
+        flex: 1; // 占据剩余空间
+
+        .Rank_Recommend_body_list_Comment_list_content_right_top {
+          display: flex;
+          flex-direction: column; // 用户名和内容垂直排列
+          margin-bottom: 10px;
+
+          .Rank_Recommend_body_list_Comment_list_content_right_top_username {
+            font-weight: bold;
+            margin-bottom: 5px;
+            color: #333;
+          }
+
+          .Rank_Recommend_body_list_Comment_list_content_right_top_content {
+            line-height: 1.5;
+            color: #666;
+          }
         }
-        .Rank_Recommend_body_list_Comment_list_content_right_bottom_right {
-          .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like {
+
+        .Rank_Recommend_body_list_Comment_list_content_right_bottom {
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+
+          .Rank_Recommend_body_list_Comment_list_content_right_bottom_time {
+            color: #999;
+            font-size: 12px;
             display: flex;
-            .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like_icon {
-              color: rgb(0, 136, 254);
-              //向左20px
-              margin-right: 20px;
+            gap: 10px;
+
+            .Rank_Recommend_body_list_Comment_list_content_right_bottom_time_button {
+              color: rgb(81, 159, 242);
+              cursor: pointer;
             }
-            .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like_button {
-              color: rgb(111, 111, 111);
+          }
+
+          .Rank_Recommend_body_list_Comment_list_content_right_bottom_right {
+            .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like {
+              display: flex;
+              align-items: center;
+
+              .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like_icon {
+                color: rgb(0, 136, 254);
+                margin-right: 20px;
+                cursor: pointer;
+
+                &:hover {
+                  color: #1890ff;
+                }
+              }
+
+              .Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like_button {
+                color: rgb(111, 111, 111);
+                cursor: pointer;
+                padding: 4px 8px;
+                border-radius: 4px;
+
+                &:hover {
+                  background-color: #f0f0f0;
+                }
+              }
             }
           }
         }
       }
     }
+
+    // // 回复框样式
+    // .comment-reply-box {
+    //   margin-top: 15px;
+    //   margin-left: 70px; // 与头像对齐
+    //   width: calc(100% - 70px); // 减去左边距
+    //   padding: 15px;
+    //   background-color: #fafafa;
+    //   border-radius: 4px;
+    //   border: 1px solid #e8e8e8;
+    // }
   }
+
   .Rank_Recommend_body_list_Comment_list_Pagination {
     margin-top: 20px;
     display: flex;
-    justify-content: center; 
+    justify-content: center;
   }
-}
+}

+ 197 - 77
src/pages/layout/pages/find/rank/compomemts/Rank_Recommend_body_list_Comment_list/index.tsx

@@ -1,87 +1,207 @@
-import { Image, Pagination, type PaginationProps } from 'antd'
-import './indes.css'
-import { LikeOutlined } from '@ant-design/icons'
-const rank_list = [
-  {
-    id: 0,
-    src: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
-    username: '爱椿的湫_',
-    content: ' 毋庸置疑。天份1551%,承蒙关照',
-    time: '2021-09-09',
-    like: 0,
-  },
-  {
-    id: 1,
-    src: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
-    username: '爱椿的湫_',
-    content: ' 毋庸置疑。天份1551%,承蒙关照',
-    time: '2021-09-09',
-    like: 2,
-  },
-]
-const Rank_Recommend_body_list_Comment_list = () => {
-  const itemRender: PaginationProps['itemRender'] = (
-    _,
-    type,
-    originalElement
-  ) => {
-    if (type === 'prev') {
-      return <a>上一页</a>
+import { Empty } from 'antd';
+import './indes.css';
+import { useEffect, useState } from 'react';
+import CommentItem from '../CommentItem';
+import { getCommentListByIdType, addLikeCount, cancelLikeCount } from '@/apis/comment';
+
+interface CommentListProps {
+  contentId?: number;
+  type?: number
+}
+
+// API 响应的数据结构
+interface ApiComment {
+  id: number;
+  userId: number;
+  contentType: number;
+  src: string;
+  username: string;
+  contentId: number;
+  parentId: number | null;
+  content: string;
+  likeCount: number;
+  replyCount: number;
+  status: number;
+  createTime: string;
+  updateTime: string;
+}
+
+interface ApiResponse {
+  code: number;
+  message: string;
+  data: ApiComment[];
+}
+
+const Rank_Recommend_body_list_Comment_list = ({ contentId, type }: CommentListProps) => {
+  const actualMvId = contentId;
+  const [comments, setComments] = useState<ApiComment[]>([]);
+  const [loading, setLoading] = useState(true);
+  const getCommentFun = async () => {
+    setLoading(true);
+    const res: ApiResponse = await getCommentListByIdType({
+      contentId: actualMvId,
+      type
+    });
+    setComments(res.data);
+  }
+  useEffect(() => {
+    const fetchComments = () => {
+      if (actualMvId) {
+        try {
+          getCommentFun()
+        } catch (error) {
+          console.error('获取评论失败:', error);
+        } finally {
+          setLoading(false);
+        }
+      } else {
+        setLoading(false); // 如果没有 contentId,也设置为非加载状态
+      }
+    };
+
+    fetchComments();
+  }, [actualMvId, type]);
+
+  const [activeCommentBox, setActiveCommentBox] = useState<{ type: 'reply' | 'main', id?: number } | null>(null);
+
+  const toggleReply = (id: number) => {
+    if (activeCommentBox && activeCommentBox.type === 'reply' && activeCommentBox.id === id) {
+      setActiveCommentBox(null);
+    } else {
+      // 否则显示这个回复框
+      setActiveCommentBox({ type: 'reply', id });
+    }
+  };
+
+  const handleReplySubmit = () => {
+    setActiveCommentBox(null);
+    // 提交回复后重新获取评论列表
+    const fetchComments = () => {
+      if (actualMvId) {
+        try {
+          getCommentFun()
+        } catch (error) {
+          console.error('获取评论失败:', error);
+        } finally {
+          setLoading(false);
+        }
+      }
+    };
+    fetchComments();
+  };
+
+  // 删除评论后的处理
+  const handleCommentDeleted = (commentId: number) => {
+    // 重新获取评论列表以更新状态
+    const fetchComments = async () => {
+      if (actualMvId) {
+        try {
+          
+          getCommentFun()
+        } catch (error) {
+          console.error('获取评论失败:', error);
+        } finally {
+          setLoading(false);
+        }
+      }
+    };
+    fetchComments();
+  };
+
+  // 处理点赞功能
+  const handleLike = async (commentId: number) => {
+    try {
+      // 调用API进行点赞
+      const res = await addLikeCount(commentId);
+
+      // 更新本地状态
+      setComments(prevComments =>
+        prevComments.map(comment =>
+          comment.id === commentId
+            ? { ...comment, likeCount: comment.likeCount + 1 }
+            : comment
+        )
+      );
+
+      console.log('点赞成功:', res);
+    } catch (error) {
+      console.error('点赞失败:', error);
     }
-    if (type === 'next') {
-      return <a>下一页</a>
+  };
+
+  // 处理取消点赞功能
+  const handleCancelLike = async (commentId: number) => {
+    try {
+      // 调用API取消点赞
+      const res = await cancelLikeCount(commentId);
+
+      // 更新本地状态
+      setComments(prevComments =>
+        prevComments.map(comment =>
+          comment.id === commentId
+            ? { ...comment, likeCount: Math.max(0, comment.likeCount - 1) }
+            : comment
+        )
+      );
+
+      console.log('取消点赞成功:', res);
+    } catch (error) {
+      console.error('取消点赞失败:', error);
     }
-    return originalElement
+  };
+
+  // 点赞按钮状态管理
+  const [likedComments, setLikedComments] = useState<number[]>([]);
+
+  const toggleLike = async (commentId: number) => {
+    if (likedComments.includes(commentId)) {
+      // 如果已经点赞,取消点赞
+      await handleCancelLike(commentId);
+      setLikedComments(prev => prev.filter(id => id !== commentId));
+    } else {
+      // 如果没有点赞,进行点赞
+      await handleLike(commentId);
+      setLikedComments(prev => [...prev, commentId]);
+    }
+  };
+
+  if (loading) {
+    return <div className="loading">加载评论中...</div>;
   }
+
   return (
     <div className="Rank_Recommend_body_list_Comment_list">
       <div className="Rank_Recommend_body_list_Comment_list_ttitle">
-        精彩评论
+        精彩评论 ({comments.length})
       </div>
-      {rank_list.map((item, index) => (
-        <div
-          className="Rank_Recommend_body_list_Comment_list_content"
-          key={index}
-        >
-          <div className="Rank_Recommend_body_list_Comment_list_content_left">
-            <Image width={50} src={item.src} preview={false} />
-          </div>
-          <div className="Rank_Recommend_body_list_Comment_list_content_right">
-            <div className="Rank_Recommend_body_list_Comment_list_content_right_top">
-              <div className="Rank_Recommend_body_list_Comment_list_content_right_top_username">
-                {item.username}
-              </div>
-              <div className="Rank_Recommend_body_list_Comment_list_content_right_top_content">
-                {item.content}
-              </div>
-            </div>
-            <div className="Rank_Recommend_body_list_Comment_list_content_right_bottom">
-              <div className="Rank_Recommend_body_list_Comment_list_content_right_bottom_time">
-                {item.time}
-              </div>
-              <div className="Rank_Recommend_body_list_Comment_list_content_right_bottom_right">
-                <div className="Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like">
-                  <div className="Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like_icon">
-                    <LikeOutlined />
-                    {item.like === 0 ? '' : item.like}
-                  </div>
-                  <div className="Rank_Recommend_body_list_Comment_list_content_right_bottom_right_like_button">
-                    回复
-                  </div>
-                </div>
-              </div>
-            </div>
+
+      {comments.length > 0 ? (
+        comments.map((item, index) => (
+          <div
+            className="Rank_Recommend_body_list_Comment_list_content"
+            key={`${item.id}-${index}`}
+          >
+            <CommentItem
+              item={item}
+              contentId={actualMvId}
+              type={type}
+              level={0}
+              likedComments={likedComments}
+              activeCommentBox={activeCommentBox}
+              onToggleLike={toggleLike}
+              onToggleReply={toggleReply}
+              onReplySubmit={handleReplySubmit}
+              onCommentDeleted={handleCommentDeleted}
+            />
           </div>
+        ))
+      ) : (
+        <div style={{ padding: '20px 0' }}>
+          <Empty description="暂无评论" />
         </div>
-      ))}
-      <div className='Rank_Recommend_body_list_Comment_list_Pagination'>
-        <Pagination
-          total={500}
-          itemRender={itemRender}
-          showSizeChanger={false}
-        />
-      </div>
+      )}
     </div>
-  )
-}
-export default Rank_Recommend_body_list_Comment_list
+  );
+};
+
+export default Rank_Recommend_body_list_Comment_list;

+ 44 - 153
src/pages/layout/pages/mine/mv/index.tsx

@@ -1,162 +1,43 @@
 import { Image } from 'antd';
 import './index.css';
 import { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom'; // 引入导航函数
+import { getMvFavoritesApi } from '@/apis/Mv'; // 假设API返回的类型
 
-// 定义 MV 类型
-interface MV {
-  id: string;
-  title: string;
-  artist: string;
-  duration: string;
-  playCount: string;
-  cover: string;
+// 定义 MV 类型 - 与API返回数据结构匹配
+interface MvFavorite {
+  id: number;
+  userId: number;
+  playlistName: string;
+  coverUrl: string;
+  description: string | null;
+  tag: string | null;
+  language: string | null;
+  style: string | null;
+  scene: string | null;
+  emotion: string | null;
+  songCount: number;
+  playCount: number;
+  createTime: string;
+  updateTime: string;
+  status: boolean;
+  theme: string | null;
 }
 
-const fetchMVs = async () => {
-  return [
-    {
-      id: '1',
-      title: 'NaKuNa',
-      artist: '郭采洁',
-      duration: '03:33',
-      playCount: '10万',
-      cover: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
-    },
-    {
-      id: '2',
-      title: '【红茶】LOL:钻石宗...',
-      artist: 'by 乱斗王红茶zz',
-      duration: '10:09',
-      playCount: '20.3万',
-      cover: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
-    },
-    {
-      id: '3',
-      title: '《你的答案》超燃混剪...',
-      artist: 'by 帐号已注销',
-      duration: '03:41',
-      playCount: '694.5万',
-      cover: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
-    },
-    {
-      id: '4',
-      title: '英雄训练营:2个平A刷...',
-      artist: 'by 兔玩电竞',
-      duration: '10:49',
-      playCount: '173048',
-      cover: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
-    },
-    {
-      id: '5',
-      title: '无论什么时候都不要失...',
-      artist: 'by 二元熊',
-      duration: '02:05',
-      playCount: '7188',
-      cover: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
-    },
-    {
-      id: '6',
-      title: '是大腿TOP10第67期: f...',
-      artist: 'by 是大腿君',
-      duration: '15:41',
-      playCount: '114.7万',
-      cover: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
-    },
-    {
-      id: '7',
-      title: '4536251到底让多少人...',
-      artist: 'by 艺起教育课堂',
-      duration: '05:15',
-      playCount: '403.6万',
-      cover: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
-    },
-    {
-      id: '8',
-      title: '百大DJ再也不敢来中国...',
-      artist: 'by 热爱音乐的大圣',
-      duration: '02:59',
-      playCount: '173.5万',
-      cover: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
-    },
-    {
-      id: '9',
-      title: '【你曾是少年】英雄联...',
-      artist: 'by 桌艺二胡',
-      duration: '04:44',
-      playCount: '29.9万',
-      cover: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
-    },
-    {
-      id: '10',
-      title: '【红茶】LOL:品如衣...',
-      artist: 'by 乱斗王红茶zz',
-      duration: '06:42',
-      playCount: '65.1万',
-      cover: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
-    },
-    {
-      id: '11',
-      title: '努力有什么用?当你遭...',
-      artist: 'by 越哥说电影',
-      duration: '14:20',
-      playCount: '78.2万',
-      cover: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
-    },
-    {
-      id: '12',
-      title: '在云村看一集《蜡笔小...',
-      artist: 'by YouTube',
-      duration: '02:29',
-      playCount: '299.9万',
-      cover: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
-    },
-    {
-      id: '13',
-      title: '钢铁侠:以凡人之躯,...',
-      artist: 'by 未知',
-      duration: '02:18',
-      playCount: '692.6万',
-      cover: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
-    },
-    {
-      id: '14',
-      title: '高考热血混剪,你能认...',
-      artist: 'by 未知',
-      duration: '03:24',
-      playCount: '32.9万',
-      cover: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
-    },
-    {
-      id: '15',
-      title: '衡水中学的一天,你认...',
-      artist: 'by 未知',
-      duration: '03:03',
-      playCount: '482.4万',
-      cover: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
-    },
-    {
-      id: '16',
-      title: '终于知道这些洗脑的神...',
-      artist: 'by 未知',
-      duration: '07:33',
-      playCount: '265.5万',
-      cover: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
-    }
-  ];
-};
-
 const Mine_Mv = () => {
-  // 明确指定类型为 MV[]
-  const [mvs, setMvs] = useState<MV[]>([]);
+  const [mvs, setMvs] = useState<MvFavorite[]>([]);
   const [loading, setLoading] = useState(true);
+  const navigate = useNavigate(); // 创建导航函数
 
-  // 获取视频数据
+  // 获取收藏的MV数据
   useEffect(() => {
     const loadMVs = async () => {
       try {
         setLoading(true);
-        const data = await fetchMVs();
-        setMvs(data);
+        const response = await getMvFavoritesApi();
+        // 根据API响应结构调整数据处理
+        const mvData = response?.data || [];
+        setMvs(mvData);
       } catch (error) {
         console.error('获取视频失败:', error);
       } finally {
@@ -167,6 +48,11 @@ const Mine_Mv = () => {
     loadMVs();
   }, []);
 
+  const handleMvClick = (id: number) => {
+    // 跳转到视频详情页
+    navigate(`/find/video/${id}`);
+  };
+
   if (loading) {
     return <div>加载中...</div>;
   }
@@ -175,21 +61,26 @@ const Mine_Mv = () => {
     <div className="Mine_Mv">
       <div className="mv-list">
         {mvs.map((mv) => (
-          <div key={mv.id} className="mv-item">
+          <div
+            key={mv.id}
+            className="mv-item"
+            onClick={() => handleMvClick(mv.id)} // 添加点击事件
+            style={{ cursor: 'pointer' }} // 添加指针样式
+          >
             <div className="mv-cover">
               <Image
                 preview={false}
                 width={130}
                 height={75}
-                src={mv.cover}
-                alt={mv.title}
+                src={mv.coverUrl}
+                alt={mv.playlistName}
               />
-              <span className="mv-duration">{mv.duration}</span>
-              <span className="mv-play-count">{mv.playCount}</span>
+              <span className="mv-duration">未知时长</span>
+              <span className="mv-play-count">{mv.playCount} 次播放</span>
             </div>
             <div className="mv-info">
-              <div className="mv-title">{mv.title}</div>
-              <div className="mv-artist">{mv.artist}</div>
+              <div className="mv-title">{mv.playlistName}</div>
+              <div className="mv-artist">作者: {mv.userId}</div>
             </div>
           </div>
         ))}

+ 62 - 16
src/pages/layout/pages/musician/MusicianDashboardPage/works/UploadMv/index.tsx

@@ -1,19 +1,53 @@
-// src/pages/layout/pages/musician/MusicianDashboardPage/works/UploadMv/index.tsx
-import { Upload, Button, Form, Input, Select, message } from 'antd';
-import { UploadOutlined } from '@ant-design/icons';
-import './index.css';
-import { uploadFile } from '@/apis/upload';
-
+import { Upload, Button, Form, Input, Select, message } from 'antd'
+import { UploadOutlined } from '@ant-design/icons'
+import './index.css'
+import { uploadFile } from '@/apis/upload'
+import { chunkUpload } from '@/utils/chunkUpload'
+import { getArtinfo } from '@/utils/artlist'
+import { AddMvApi } from '@/apis/Mv'
+import { useNavigate } from 'react-router-dom'
+import { useState } from 'react'
 const { Option } = Select;
 
 const UploadMv = () => {
   const [form] = Form.useForm();
   const [messageApi, contextHolder] = message.useMessage();
+  const artinfo = getArtinfo();
+  const navigate = useNavigate();
+  const [formLoading, setFormLoading] = useState(false);
+  
+  const onFinish = async (values: any) => {
+    setFormLoading(true); // 设置表单加载状态
+    console.log('原始表单数据:', values);
+    const finalValues = {
+      mvName: values.mvName,
+      description: values.description,
+      tags: values.tags.join(","),
+      coverUrl: values.cover?.file.response || '',
+      videoUrl: values.video?.file.response || '',
+      artistId: artinfo.id,
+      artistName: artinfo.artistName
+    };
 
-  const onFinish = (values: any) => {
-    console.log('表单数据:', values);
-    messageApi.success('MV上传成功!');
-    form.resetFields();
+    console.log('处理后需要的表单数据:', finalValues);
+    try {
+      const result = await AddMvApi(finalValues);
+      if (result.code === 200) {
+        messageApi.success('MV上传成功!');
+        form.resetFields(); // 重置表单
+        // 导航回退到上一个页面
+        setTimeout(() => {
+          navigate(-1);
+        }, 1000);
+      } else {
+        messageApi.error('MV上传失败!:' + result.message);
+      }
+    } catch (error) {
+      console.error('提交失败:', error);
+      messageApi.error('提交失败,请重试');
+    } finally {
+      setFormLoading(false); // 重置加载状态
+    }
   };
 
   const onFinishFailed = (errorInfo: any) => {
@@ -27,7 +61,11 @@ const UploadMv = () => {
     const { file, onSuccess, onError, onProgress } = options;
     try {
       const response = await uploadFile('VIDEO_HOME', file, '', onProgress);
-      onSuccess(response);
+      console.log(response);
+
+      const coverUrl = response.data; // 假设 response.data 就是 URL 字符串
+      onSuccess(coverUrl); // 这里onSuccess传入的就是纯URL
+
       messageApi.success('封面上传成功');
     } catch (error: any) {
       console.error('封面上传失败:', error);
@@ -40,12 +78,16 @@ const UploadMv = () => {
   const customUploadVideo = async (options: any) => {
     const { file, onSuccess, onError, onProgress } = options;
     try {
-      const response = await uploadFile('VIDEO_FILES', file, '', onProgress);
-      onSuccess(response);
-      messageApi.success('视频上传成功');
+      console.log(file);
+      // 使用分片上传
+      const result = await chunkUpload(file, (progress) => {
+        onProgress({ percent: progress });
+      });
+      onSuccess(result); // 这里传入的是包含MD5的对象
+      messageApi.success('视频分片上传成功');
     } catch (error: any) {
       console.error('视频上传失败:', error);
-      const errorMessage = error.response?.data?.error || '视频上传失败';
+      const errorMessage = error.message || '视频上传失败';
       messageApi.error(errorMessage);
       onError(error);
     }
@@ -123,7 +165,11 @@ const UploadMv = () => {
         </Form.Item>
 
         <Form.Item wrapperCol={{ offset: 6, span: 18 }}>
-          <Button type="primary" htmlType="submit">
+          <Button 
+            type="primary" 
+            htmlType="submit"
+            loading={formLoading} // 添加加载状态
+          >
             提交上传
           </Button>
         </Form.Item>

+ 2 - 3
src/pages/layout/pages/musician/MusicianDashboardPage/works/mv/index.css

@@ -79,14 +79,13 @@
   flex: 1;
   color: #666;
 }
-.mv-detail .mv-cover {
+.mv-detail .mv-mvdetail {
   width: 200px;
-  height: 200px;
   margin: 16px auto;
   border-radius: 8px;
   overflow: hidden;
 }
-.mv-detail .mv-cover img {
+.mv-detail .mv-mvdetail img {
   width: 100%;
   height: 100%;
   object-fit: cover;

+ 1 - 3
src/pages/layout/pages/musician/MusicianDashboardPage/works/mv/index.less

@@ -96,13 +96,11 @@
   }
 
   // 添加MV封面图片样式
-  .mv-cover {
+  .mv-mvdetail {
     width: 200px;
-    height: 200px;
     margin: 16px auto;
     border-radius: 8px;
     overflow: hidden;
-
     img {
       width: 100%;
       height: 100%;

+ 300 - 27
src/pages/layout/pages/musician/MusicianDashboardPage/works/mv/index.tsx

@@ -1,21 +1,64 @@
-// src/pages/layout/pages/musician/MusicianDashboardPage/works/mv/index.tsx
-import { Button, Drawer, Image, Modal, notification, Space, Table, Tabs } from 'antd';
+import { Button, Drawer, Image, Modal, notification, Space, Table, Tabs, Tag } from 'antd';
 import './index.css';
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useRef, useEffect as useLayoutEffect } from 'react';
 import type { NotificationPlacement } from 'antd/es/notification/interface';
 import { useNavigate } from 'react-router-dom';
+import { changeMvStatusApi, GetFileUrlByMd5Api, GetMvApi } from '@/apis/Mv';
+import { getArtinfo } from '@/utils/artlist';
+import DPlayer from 'dplayer';
 
 const Mv = () => {
   const navigate = useNavigate();
   const [mvStatus, setMvStatus] = useState(0);
   const [mvs, setMvs] = useState<any[]>([]);
   const [currentRecord, setCurrentRecord] = useState<any>(null);
+  const [currentVideoUrl, setCurrentVideoUrl] = useState<string | null>(null); // 存储实际的视频URL
   const [openDetail, setOpenDetail] = useState(false);
   const [isModalOpen, setIsModalOpen] = useState(false);
   const [deleteMvId, setDeleteMvId] = useState<number | null>(null);
-
   const [api, contextHolder] = notification.useNotification();
+  const dpContainerRef = useRef<HTMLDivElement>(null);
+  const dpRef = useRef<DPlayer | null>(null);
+  const [statusChangingMvId, setStatusChangingMvId] = useState<number | null>(null);
+  const [statusChangingModalOpen, setStatusChangingModalOpen] = useState(false);
+  const [targetStatus, setTargetStatus] = useState<number | null>(null);
+
+  // 添加上架/下架处理函数
+  const handleChangeStatus = async (record: any, newStatus: number) => {
+    setDeleteMvId(record.id);
+    setTargetStatus(newStatus);
+    setStatusChangingModalOpen(true);
+  };
+
+  // 确认状态变更
+  const handleStatusChangeConfirm = async () => {
+    if (!deleteMvId || targetStatus === null) return;
 
+    setStatusChangingMvId(deleteMvId);
+    try {
+      const result = await changeMvStatusApi({ id: deleteMvId, status: targetStatus });
+      if (result.code === 200) {
+        api.success({
+          message: targetStatus === 3 ? '上架成功' : '下架成功',
+          placement: 'topRight',
+        });
+        getMvList(); // 重新获取列表
+      } else {
+        api.error({
+          message: result.message || '操作失败',
+          placement: 'topRight',
+        });
+      }
+    } catch (error) {
+      console.error('状态变更失败:', error);
+      api.error({
+        message: '操作失败',
+        placement: 'topRight',
+      });
+    }
+    setStatusChangingModalOpen(false);
+    setStatusChangingMvId(null);
+  };
   const mvStatusContent = [
     { key: 0, label: '审核中' },
     { key: 1, label: '审核失败' },
@@ -36,15 +79,29 @@ const Mv = () => {
       key: 'mvName',
     },
     {
-      title: '专辑名',
-      dataIndex: 'albumName',
-      key: 'albumName',
-      render: (albumName: string) => albumName || '独立MV'
+      title: '封面',
+      dataIndex: 'coverUrl',
+      key: 'coverUrl',
+      render: (coverUrl: string) => (
+        coverUrl ? (
+          <Image
+            width={60}
+            height={40}
+            src={coverUrl}
+            alt="MV封面"
+            style={{ borderRadius: 4 }}
+            preview={false}
+          />
+        ) : (
+          '无封面'
+        )
+      ),
     },
     {
-      title: '发布时间',
-      dataIndex: 'releaseTime',
-      key: 'releaseTime',
+      title: '创建时间',
+      dataIndex: 'createTime',
+      key: 'createTime',
+      render: (createTime: string) => new Date(createTime).toLocaleString() || '未知'
     },
     {
       title: '状态',
@@ -69,6 +126,24 @@ const Mv = () => {
           <Button size="small" onClick={() => handleViewDetails(record)}>
             查看详情
           </Button>
+          {record.status === 2 && (
+            <Button
+              type="primary"
+              size="small"
+              onClick={() => handleChangeStatus(record, 3)}
+            >
+              上架
+            </Button>
+          )}
+          {record.status === 3 && (
+            <Button
+              danger
+              size="small"
+              onClick={() => handleChangeStatus(record, 4)}
+            >
+              下架
+            </Button>
+          )}
           <Button danger size="small" onClick={() => handleDelete(record)}>
             删除
           </Button>
@@ -77,8 +152,25 @@ const Mv = () => {
     }
   ];
 
-  const handleViewDetails = (record: any) => {
+  const handleViewDetails = async (record: any) => {
     setCurrentRecord(record);
+    if (record.videoUrl) {
+      try {
+        const result = await GetFileUrlByMd5Api(record.videoUrl);
+        if (result.code === 200) {
+          setCurrentVideoUrl(result.data); // 假设API返回的URL在data字段中
+        } else {
+          console.error('获取视频URL失败:', result.message);
+          setCurrentVideoUrl(null);
+        }
+      } catch (error) {
+        console.error('获取视频URL出错:', error);
+        setCurrentVideoUrl(null);
+      }
+    } else {
+      setCurrentVideoUrl(null);
+    }
+
     setOpenDetail(true);
   };
 
@@ -88,10 +180,21 @@ const Mv = () => {
   };
 
   const handleOk = async () => {
-    // 模拟删除操作
-    console.log('删除MV ID:', deleteMvId);
-    setIsModalOpen(false);
-    // 实际删除逻辑可以在这里添加
+    try {
+      console.log('删除MV ID:', deleteMvId);
+      setIsModalOpen(false);
+      getMvList();
+      api.success({
+        message: '删除成功',
+        placement: 'topRight',
+      });
+    } catch (error) {
+      console.error('删除失败:', error);
+      api.error({
+        message: '删除失败',
+        placement: 'topRight',
+      });
+    }
   };
 
   const handleCancel = () => {
@@ -108,17 +211,55 @@ const Mv = () => {
 
   const onClose = () => {
     setOpenDetail(false);
+    setCurrentVideoUrl(null); // 关闭详情时清空视频URL
+    // 销毁 DPlayer 实例
+    if (dpRef.current) {
+      dpRef.current.destroy();
+      dpRef.current = null;
+    }
+  };
+
+  const getMvList = async () => {
+    const id = getArtinfo().id;
+    const res = await GetMvApi({ id, status: mvStatus });
+    setMvs(res.data);
   };
 
-  // 模拟获取数据
   useEffect(() => {
-    // 获取MV列表
-    setMvs([
-      { id: 1, mvName: 'MV示例1', albumName: '专辑1', releaseTime: '2023-05-15', status: 3 },
-      { id: 2, mvName: 'MV示例2', albumName: null, releaseTime: '2023-06-20', status: 0 },
-      { id: 3, mvName: 'MV示例3', albumName: '专辑2', releaseTime: '2023-07-10', status: 2 }
-    ]);
+    getMvList();
   }, [mvStatus]);
+  useLayoutEffect(() => {
+    if (openDetail && currentVideoUrl && dpContainerRef.current) {
+      if (dpRef.current) {
+        dpRef.current.destroy();
+      }
+
+      dpRef.current = new DPlayer({
+        container: dpContainerRef.current,
+        video: {
+          url: currentVideoUrl,
+          type: 'auto'
+        },
+        autoplay: false,
+        theme: '#e63b3b',
+        loop: false,
+        lang: 'zh-cn',
+        screenshot: false,
+        hotkey: true,
+        preload: 'metadata',
+        volume: 0.7,
+        mutex: true,
+        height: 300
+      });
+    }
+
+    return () => {
+      if (dpRef.current) {
+        dpRef.current.destroy();
+        dpRef.current = null;
+      }
+    };
+  }, [currentVideoUrl, openDetail]);
 
   return (
     <div className="mv-container">
@@ -165,7 +306,18 @@ const Mv = () => {
       >
         <p>确定要删除选中MV吗?</p>
       </Modal>
-
+      <Modal
+        title={targetStatus === 3 ? '确认上架' : '确认下架'}
+        open={statusChangingModalOpen}
+        onOk={handleStatusChangeConfirm}
+        onCancel={() => setStatusChangingModalOpen(false)}
+        cancelText="取消"
+      >
+        <p>
+          确定要{targetStatus === 3 ? '上架' : '下架'}这个MV吗?
+          {targetStatus === 3 ? '上架后用户将可以访问此MV。' : '下架后用户将无法访问此MV。'}
+        </p>
+      </Modal>
       <Drawer
         title="MV详情"
         placement="right"
@@ -176,7 +328,7 @@ const Mv = () => {
       >
         {currentRecord && (
           <div className="mv-detail">
-            <div style={{ display: 'flex', marginBottom: 20 }}>
+            <div style={{ display: 'flex', marginBottom: 0 }}>
               <div style={{ flex: 1 }}>
                 <div className="detail-item">
                   <label>MV名称:</label>
@@ -186,6 +338,10 @@ const Mv = () => {
                   <label>ID:</label>
                   <span>{currentRecord.id}</span>
                 </div>
+                <div className="detail-item">
+                  <label>艺术家:</label>
+                  <span>{currentRecord.artistName}</span>
+                </div>
                 <div className="detail-item">
                   <label>状态:</label>
                   <span>
@@ -201,13 +357,30 @@ const Mv = () => {
                     })()}
                   </span>
                 </div>
+
+                {currentRecord.status === 1 && currentRecord.auditReason && (
+                  <div className="detail-item">
+                    <label>审核失败原因:</label>
+                    <span style={{ color: '#ff4d4f', fontWeight: 'bold' }}>
+                      {currentRecord.auditReason}
+                    </span>
+                  </div>
+                )}
+                <div className="detail-item">
+                  <label>时长:</label>
+                  <span>
+                    {currentRecord.duration !== null && currentRecord.duration !== undefined
+                      ? `${Math.floor(currentRecord.duration / 60)}:${(currentRecord.duration % 60).toString().padStart(2, '0')}`
+                      : '未知'}
+                  </span>
+                </div>
               </div>
-              {currentRecord.thumbnailUrl && (
+              {currentRecord.coverUrl && (
                 <div className="mv-cover">
                   <Image
                     width={180}
                     height={180}
-                    src={currentRecord.thumbnailUrl}
+                    src={currentRecord.coverUrl}
                     alt="MV封面"
                     style={{ borderRadius: 8 }}
                   />
@@ -215,6 +388,17 @@ const Mv = () => {
               )}
             </div>
 
+
+            {/* 视频播放器 */}
+            {currentVideoUrl ? (
+              <div className="detail-item" style={{ marginBottom: 16 }}>
+                <label>视频预览:</label>
+                <div style={{ marginTop: 8 }}>
+                  <div ref={dpContainerRef} style={{ height: '300px', width: '100%' }} />
+                </div>
+              </div>
+            ) : <div>加载中</div>}
+
             <div className="detail-item">
               <label>描述:</label>
               <div style={{
@@ -229,10 +413,99 @@ const Mv = () => {
               </div>
             </div>
 
+            <div className="detail-item">
+              <label>标签:</label>
+              <div style={{
+                marginTop: 4,
+                display: 'flex',
+                flexWrap: 'wrap',
+                gap: '4px'
+              }}>
+                {currentRecord.tags ? currentRecord.tags.split(',').map((tag: string, index: number) => (
+                  <Tag key={index} color='cyan' variant={'solid'}>
+                    <span color={'white'}>
+                      {tag.trim()}
+                    </span>
+                  </Tag>
+                )) : '无标签'}
+              </div>
+            </div>
+
+            <div className="detail-item">
+              <label>创建时间:</label>
+              <span>{currentRecord.createTime ? new Date(currentRecord.createTime).toLocaleString() : '未知'}</span>
+            </div>
+
+            <div className="detail-item">
+              <label>更新时间:</label>
+              <span>{currentRecord.updateTime ? new Date(currentRecord.updateTime).toLocaleString() : '未知'}</span>
+            </div>
+
             <div className="detail-item">
               <label>发布时间:</label>
               <span>{currentRecord.releaseTime || '未设置'}</span>
             </div>
+
+            <div className="detail-item">
+              <label>封面URL:</label>
+              <a href={currentRecord.coverUrl} target="_blank" rel="noopener noreferrer">
+                {currentRecord.coverUrl}
+              </a>
+            </div>
+
+            <div className="detail-item">
+              <label>视频MD5:</label>
+              <span>{currentRecord.videoUrl}</span>
+            </div>
+
+            <div className="detail-item">
+              <label>视频播放URL:</label>
+              {currentVideoUrl ? (
+                <a href={currentVideoUrl} target="_blank" rel="noopener noreferrer">
+                  {currentVideoUrl}
+                </a>
+              ) : (
+                <span>加载中...</span>
+              )}
+            </div>
+
+            <div className="detail-item">
+              <label>状态:</label>
+              <div style={{ display: 'flex', alignItems: 'center' }}>
+                <span style={{ marginRight: 16 }}>
+                  {(() => {
+                    const statusMap: Record<number, string> = {
+                      0: '审核中',
+                      1: '审核失败',
+                      2: '发布中',
+                      3: '已上架',
+                      4: '已下架'
+                    };
+                    return statusMap[currentRecord.status] || '未知';
+                  })()}
+                </span>
+                {currentRecord.status === 2 && (
+                  <Button
+                    type="primary"
+                    size="small"
+                    loading={statusChangingMvId === currentRecord.id}
+                    onClick={() => handleChangeStatus(currentRecord, 3)}
+                  >
+                    上架
+                  </Button>
+                )}
+                {currentRecord.status === 3 && (
+                  <Button
+                    danger
+                    size="small"
+                    loading={statusChangingMvId === currentRecord.id}
+                    onClick={() => handleChangeStatus(currentRecord, 4)}
+                  >
+                    下架
+                  </Button>
+                )}
+              </div>
+            </div>
           </div>
         )}
       </Drawer>

+ 1 - 1
src/router/index.tsx

@@ -81,7 +81,7 @@ const router = createBrowserRouter([
           }
           ,
           {
-            path: '/find/video',
+            path: '/find/video/:id',
             element: <Video />,
           },
         ],

+ 41 - 18
src/utils/calculateMD5.ts

@@ -1,23 +1,46 @@
-import CryptoJS from 'crypto-js'
+// src/utils/calculateMD5.ts
+import SparkMd5 from 'spark-md5';
+
 const calculateMD5 = (file: File): Promise<string> => {
   return new Promise((resolve, reject) => {
-    const reader = new FileReader()
-    reader.onload = (e) => {
-      try {
-        // 使用ArrayBuffer方式读取文件
-        const buffer = e.target?.result as ArrayBuffer
-        const wordArray = CryptoJS.lib.WordArray.create(buffer)
-        const hash = CryptoJS.MD5(wordArray)
-        resolve(hash.toString())
-      } catch (error) {
-        reject(error)
-      }
+    if (file.size === 0) {
+      resolve('');
+      return;
+    }
+
+    // 对于大文件使用spark-md5
+    const chunkSize = 2 * 1024 * 1024; // 2MB chunks
+    const chunks = Math.ceil(file.size / chunkSize);
+    let currentChunk = 0;
+    const spark = new SparkMd5(); // 直接使用导入的SparkMd5
+    const fileReader = new FileReader();
+
+    function loadNext() {
+      const start = currentChunk * chunkSize
+      const end = Math.min(start + chunkSize, file.size)
+      fileReader.readAsBinaryString(file.slice(start, end))
     }
-    reader.onerror = () => {
-      reject(new Error('文件读取失败'))
+
+    fileReader.onload = (e) => {
+      spark.appendBinary(e.target?.result as string)
+      currentChunk++
+      if (currentChunk < chunks) {
+        loadNext()
+      } else {
+        resolve(spark.end())
+      }
     }
-    reader.readAsArrayBuffer(file)
-  })
-}
 
-export default calculateMD5
+    fileReader.onerror = () => {
+      reject(new Error('文件读取失败'));
+    };
+
+    fileReader.onabort = () => {
+      reject(new Error('文件读取中断'));
+    };
+
+    loadNext();
+  });
+};
+
+export default calculateMD5;

+ 95 - 0
src/utils/chunkUpload.ts

@@ -0,0 +1,95 @@
+import { chunkUploadApi, mergeChunksApi } from '@/apis/upload';
+import calculateMD5 from './calculateMD5';
+
+interface Chunk {
+  file: Blob;
+  index: number;
+}
+
+interface UploadPartResponse {
+  code?: number;
+  message?: string;
+  data?: any;
+}
+
+const CHUNK_SIZE = 5 * 1024 * 1024;
+
+export const chunkUpload = async (
+  file: File,  // 这里是 File 类型,包含文件名和类型信息
+  onProgress?: (progress: number) => void
+): Promise<string> => {
+  const chunks: Chunk[] = [];
+  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
+  const fileKey = await calculateMD5(file);
+
+  // 创建分片
+  for (let i = 0; i < totalChunks; i++) {
+    const start = i * CHUNK_SIZE;
+    const end = Math.min(start + CHUNK_SIZE, file.size);
+    const chunk = file.slice(start, end);
+
+    chunks.push({
+      file: chunk,
+      index: i + 1
+    });
+  }
+
+  // 上传进度跟踪
+  let uploadedChunks = 0;
+
+  const uploadPromises = chunks.map(chunk => {
+    return uploadChunk(
+      chunk.file,
+      fileKey,
+      chunk.index,
+      totalChunks,
+      onProgress ? () => {
+        uploadedChunks++;
+        const progress = Math.round((uploadedChunks / totalChunks) * 100);
+        console.log(`上传进度: ${progress}%`);
+        onProgress(progress);
+      } : undefined
+    )
+  })
+  
+  // 等待所有分片上传完成
+  await Promise.all(uploadPromises)
+  
+  // 所有分片上传完成后,请求合并,传递文件名和类型
+  await mergeChunksApi(
+    fileKey,
+    totalChunks,
+    file.name,
+    file.type
+  );
+
+  return fileKey;
+};
+
+
+
+const uploadChunk = async (
+  chunk: Blob,
+  fileKey: string,
+  chunkNumber: number,
+  totalChunks: number,
+  onProgress?: () => void
+): Promise<UploadPartResponse> => {
+  const formData = new FormData();
+  formData.append('file', chunk);
+  formData.append('fileKey', fileKey);
+  formData.append('chunkNumber', chunkNumber.toString());
+  formData.append('totalChunks', totalChunks.toString());
+
+  try {
+    const response = await chunkUploadApi(formData)
+    if (onProgress) {
+      onProgress();
+    }
+
+    return { message: response.data.message || '上传成功' };
+  } catch (error: any) {
+    console.error(`分片 ${chunkNumber} 上传失败:`, error);
+    throw error;
+  }
+}