增加了悬赏,标签查看,评论页面,标签上传后端有问题,评论还没跟后端连,优化了一些小界面

Change-Id: I44f5ef2eb0a8ebd91a4b3b3b446f897bea41435f
diff --git a/react-ui/src/pages/Torrent/Comments/data.d.ts b/react-ui/src/pages/Torrent/Comments/data.d.ts
new file mode 100644
index 0000000..922cb29
--- /dev/null
+++ b/react-ui/src/pages/Torrent/Comments/data.d.ts
@@ -0,0 +1,15 @@
+/** 种子评论表 */
+export interface SysTorrentComment {
+    /** 评论ID */
+    commentId: number;
+    /** 种子ID */
+    torrentId: number;
+    /** 评论用户ID */
+    userId: number;
+    /** 评论内容 */
+    content: string;
+    /** 创建时间 */
+    createTime: Date;
+    /** 父评论ID,用于回复 */
+    parentId: number;
+}
diff --git a/react-ui/src/pages/Torrent/Comments/index.tsx b/react-ui/src/pages/Torrent/Comments/index.tsx
new file mode 100644
index 0000000..9a9f25a
--- /dev/null
+++ b/react-ui/src/pages/Torrent/Comments/index.tsx
@@ -0,0 +1,194 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { Button, Card, Form, Input, List, message, Avatar } from 'antd';
+import { ArrowLeftOutlined } from '@ant-design/icons';
+import { Layout } from 'antd';
+
+const { Content } = Layout;
+const { TextArea } = Input;
+
+interface CommentItem {
+    id: number;
+    content: string;
+    createTime: string;
+    createBy: string;
+    torrentId: number;
+}
+
+const TorrentComments: React.FC = () => {
+    const { torrentId } = useParams<{ torrentId: string }>();
+    const navigate = useNavigate();
+    const [form] = Form.useForm();
+    const [comments, setComments] = useState<CommentItem[]>([]);
+    const [submitting, setSubmitting] = useState(false);
+
+    // 获取评论列表
+    const fetchComments = async () => {
+        try {
+            // 模拟评论数据
+            const mockComments: CommentItem[] = [
+                {
+                    id: 1,
+                    content: '这个种子非常好,下载速度很快!',
+                    createTime: '2024-01-15 14:30:00',
+                    createBy: '张三',
+                    torrentId: Number(torrentId)
+                },
+                {
+                    id: 2,
+                    content: '资源质量很高,感谢分享。',
+                    createTime: '2024-01-15 15:20:00',
+                    createBy: '李四',
+                    torrentId: Number(torrentId)
+                },
+                {
+                    id: 3,
+                    content: '这个版本很完整,推荐下载!',
+                    createTime: '2024-01-15 16:45:00',
+                    createBy: '王五',
+                    torrentId: Number(torrentId)
+                },
+                {
+                    id: 1,
+                    content: '这个种子非常好,下载速度很快!',
+                    createTime: '2024-01-15 14:30:00',
+                    createBy: '张三',
+                    torrentId: Number(torrentId)
+                },
+                {
+                    id: 2,
+                    content: '资源质量很高,感谢分享。',
+                    createTime: '2024-01-15 15:20:00',
+                    createBy: '李四',
+                    torrentId: Number(torrentId)
+                },
+                {
+                    id: 3,
+                    content: '这个版本很完整,推荐下载!',
+                    createTime: '2024-01-15 16:45:00',
+                    createBy: '王五',
+                    torrentId: Number(torrentId)
+                }
+            ];
+            setComments(mockComments);
+        } catch (error) {
+            message.error('获取评论失败');
+        }
+    };
+
+    useEffect(() => {
+        if (torrentId) {
+            fetchComments();
+        }
+    }, [torrentId]);
+
+    // 提交评论
+    const handleSubmit = async () => {
+        try {
+            const values = await form.validateFields();
+            setSubmitting(true);
+
+            // TODO: 替换为实际的API调用
+            // await addComment({
+            //   torrentId: Number(torrentId),
+            //   content: values.content,
+            // });
+
+            message.success('评论成功');
+            form.resetFields();
+            fetchComments(); // 刷新评论列表
+        } catch (error) {
+            message.error('评论失败');
+        } finally {
+            setSubmitting(false);
+        }
+    };
+
+    return (
+        <Content style={{
+            height: '100vh',
+            display: 'flex',
+            flexDirection: 'column',
+            overflow: 'hidden' // 防止内容溢出
+        }}>
+            {/* 顶部标题栏 */}
+            <div style={{
+                padding: '16px',
+                borderBottom: '1px solid #f0f0f0',
+                display: 'flex',
+                alignItems: 'center',
+                backgroundColor: '#fff',
+                zIndex: 10
+            }}>
+                <Button
+                    type="link"
+                    icon={<ArrowLeftOutlined />}
+                    onClick={() => navigate(-1)}
+                    style={{ marginRight: '10px', padding: 0 }}
+                />
+                <span style={{ fontSize: '16px', fontWeight: 'bold' }}>种子评论</span>
+            </div>
+
+            {/* 评论列表区域 - 可滚动 */}
+            <div style={{
+                flex: 1,
+                overflowY: 'auto',
+                padding: '0 16px',
+                paddingBottom: '16px'
+            }}>
+                <List
+                    className="comment-list"
+                    itemLayout="horizontal"
+                    dataSource={comments}
+                    renderItem={(item) => (
+                        <List.Item>
+                            <List.Item.Meta
+                                avatar={<Avatar>{item.createBy[0]}</Avatar>}
+                                title={item.createBy}
+                                description={
+                                    <div>
+                                        <div>{item.content}</div>
+                                        <div style={{ color: '#8c8c8c', fontSize: '12px', marginTop: '8px' }}>
+                                            {item.createTime}
+                                        </div>
+                                    </div>
+                                }
+                            />
+                        </List.Item>
+                    )}
+                />
+            </div>
+
+            {/* 评论输入框 - 固定在父组件底部 */}
+            <div style={{
+                position: 'relative',
+                padding: '16px',
+                backgroundColor: '#fff',
+                borderTop: '1px solid #f0f0f0',
+                boxShadow: '0 -2px 8px rgba(0, 0, 0, 0.06)'
+            }}>
+                <Form form={form}>
+                    <Form.Item
+                        name="content"
+                        rules={[{ required: true, message: '请输入评论内容' }]}
+                        style={{ marginBottom: '12px' }}
+                    >
+                        <TextArea rows={3} placeholder="请输入您的评论" />
+                    </Form.Item>
+                    <Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
+                        <Button
+                            htmlType="submit"
+                            loading={submitting}
+                            onClick={handleSubmit}
+                            type="primary"
+                        >
+                            提交评论
+                        </Button>
+                    </Form.Item>
+                </Form>
+            </div>
+        </Content>
+    );
+};
+
+export default TorrentComments;
\ No newline at end of file
diff --git a/react-ui/src/pages/Torrent/Comments/service.ts b/react-ui/src/pages/Torrent/Comments/service.ts
new file mode 100644
index 0000000..f4243b6
--- /dev/null
+++ b/react-ui/src/pages/Torrent/Comments/service.ts
@@ -0,0 +1,30 @@
+import { request } from '@umijs/max';
+import type { SysTorrentComment } from '@/pages/Torrent/Comments/data';
+
+/** 获取种子评论列表 */
+export async function listComments(torrentId: number) {
+    return request<SysTorrentComment[]>(`/api/system/torrent/comment/list`, {
+        method: 'get',
+        params: { torrentId },
+    });
+}
+
+/** 新增评论 */
+export async function addComment(data: {
+    torrentId: number;
+    userId: number;
+    content: string;
+    parentId?: number;
+}) {
+    return request(`/api/system/torrent/comment`, {
+        method: 'post',
+        data,
+    });
+}
+
+/** 删除评论 */
+export async function deleteComment(commentId: number) {
+    return request(`/api/system/torrent/comment/${commentId}`, {
+        method: 'delete',
+    });
+}
diff --git a/react-ui/src/pages/Torrent/index.tsx b/react-ui/src/pages/Torrent/index.tsx
index bb058ae..30b7ef3 100644
--- a/react-ui/src/pages/Torrent/index.tsx
+++ b/react-ui/src/pages/Torrent/index.tsx
@@ -1,5 +1,5 @@
-import React, { useRef, useState } from 'react';
-import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, UploadOutlined } from '@ant-design/icons';
+import React, { useRef, useState, useEffect } from 'react';
+import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, UploadOutlined, DownloadOutlined, CommentOutlined } from '@ant-design/icons';
 import {
   Button,
   Modal,
@@ -13,21 +13,29 @@
   Layout,
   Upload,
   UploadProps,
+  Select,
+  Tag, // 导入Tag组件用于显示标签
 } from 'antd';
 import { ProTable, ActionType, ProColumns, ProDescriptions, ProDescriptionsItemProps } from '@ant-design/pro-components';
-import type { BtTorrent } from './data';
+import { useNavigate } from 'react-router-dom';
+import type { BtTorrent, BtTorrentTag } from './data';
 import {
   listBtTorrent,
   getBtTorrent,
   addBtTorrent,
   updateBtTorrent,
   removeBtTorrent,
-  uploadTorrent, // Function to handle torrent upload
+  uploadTorrent,
+  downloadTorrent,
+  addBtTorrentTag,
+  listBtTorrentTags,
+  getBtTorrentTag
 } from './service';
 
 const { Content } = Layout;
 
 const BtTorrentPage: React.FC = () => {
+  const navigate = useNavigate();
   const actionRef = useRef<ActionType>();
   const [form] = Form.useForm();
   const [modalVisible, setModalVisible] = useState(false);
@@ -35,19 +43,38 @@
   const [current, setCurrent] = useState<Partial<BtTorrent>>({});
   const [uploadModalVisible, setUploadModalVisible] = useState(false); // State for upload modal
   const [uploadFile, setUploadFile] = useState<File | null>(null); // State to store selected file
+  const [uploadForm] = Form.useForm(); // Form for upload modal
+  const [torrentTags, setTorrentTags] = useState<BtTorrentTag[]>([]); // 修改为数组类型来存储多个标签
 
   // Columns for the ProTable (the table displaying torrents)
   const columns: ProColumns<BtTorrent>[] = [
     {
       title: '种子ID',
       dataIndex: 'torrentId',
-      hideInForm: true,
       render: (dom, entity) => (
         <a
           onClick={async () => {
-            const res = await getBtTorrent(entity.torrentId!);
-            setCurrent(res);
-            setDrawerVisible(true);
+            try {
+              // 获取详细信息
+              const res = await getBtTorrent(entity.torrentId!);
+              console.log('获取的种子详情:', res); // 调试用
+
+              // 确保res是对象类型并且包含数据
+              if (res && typeof res === 'object') {
+                // 如果API返回了data包装,则提取data
+                const torrentData = res.data ? res.data : res;
+                setCurrent(torrentData);
+
+                // 先设置当前种子,然后获取标签
+                await handleGetTags(entity.torrentId!);
+                setDrawerVisible(true);
+              } else {
+                message.error('获取种子详情格式错误');
+              }
+            } catch (error) {
+              console.error('获取种子详情出错:', error);
+              message.error('获取种子详情失败');
+            }
           }}
         >
           {dom}
@@ -55,36 +82,56 @@
       ),
     },
     { title: '名称', dataIndex: 'name' },
-    { title: 'infoHash', dataIndex: 'infoHash' },
+    // { title: 'infoHash', dataIndex: 'infoHash' },
     { title: '大小 (bytes)', dataIndex: 'length', valueType: 'digit' },
-    { title: '分片大小', dataIndex: 'pieceLength', valueType: 'digit' },
-    { title: '片段数', dataIndex: 'piecesCount', valueType: 'digit' },
-    { title: '创建工具', dataIndex: 'createdBy', hideInSearch: true },
+    // { title: '分片大小', dataIndex: 'pieceLength', valueType: 'digit' },
+    // { title: '片段数', dataIndex: 'piecesCount', valueType: 'digit' },
+    { title: '创建人', dataIndex: 'createdBy', hideInSearch: true },
     { title: '上传时间', dataIndex: 'uploadTime', valueType: 'dateTime', hideInSearch: true },
     {
       title: '操作',
       valueType: 'option',
       render: (_, record) => [
-        <Button key="view" type="link" icon={<EyeOutlined />} onClick={() => {
-          setCurrent(record);
-          setDrawerVisible(true);
+        <Button key="view" type="link" icon={<EyeOutlined />} onClick={async () => {
+          try {
+            // 获取详细信息
+            const res = await getBtTorrent(record.torrentId!);
+            console.log('获取的种子详情:', res); // 调试用
+
+            // 确保res是对象类型并且包含数据
+            if (res && typeof res === 'object') {
+              // 如果API返回了data包装,则提取data
+              const torrentData = res.data ? res.data : res;
+              setCurrent(torrentData);
+
+              // 获取标签
+              await handleGetTags(record.torrentId!);
+              setDrawerVisible(true);
+            } else {
+              message.error('获取种子详情格式错误');
+            }
+          } catch (error) {
+            console.error('获取种子详情出错:', error);
+            message.error('获取详情失败');
+          }
         }}>查看</Button>,
-        <Button key="edit" type="link" icon={<EditOutlined />} onClick={() => {
-          setCurrent(record);
-          form.setFieldsValue(record);
-          setModalVisible(true);
-        }}>编辑</Button>,
-        <Button key="delete" type="link" icon={<DeleteOutlined />} danger onClick={() => {
-          Modal.confirm({
-            title: '删除确认',
-            content: '确定删除该种子?',
-            onOk: async () => {
-              await removeBtTorrent([record.torrentId!]);
-              message.success('删除成功');
-              actionRef.current?.reload();
-            },
-          });
-        }}>删除</Button>,
+        <Button key="download" type="link" icon={<DownloadOutlined />} onClick={async () => {
+          try {
+            const blob = await downloadTorrent(record.torrentId!);
+            const url = window.URL.createObjectURL(blob);
+            const link = document.createElement('a');
+            link.href = url;
+            link.download = `${record.name}.torrent`;
+            document.body.appendChild(link);
+            link.click();
+            document.body.removeChild(link);
+            window.URL.revokeObjectURL(url);
+            message.success('下载成功');
+          } catch (error: any) {
+            message.error('下载失败');
+          }
+        }}>下载</Button>,
+
       ],
     },
   ];
@@ -115,16 +162,19 @@
         throw new Error('请选择一个文件');
       }
 
-      // Call the uploadTorrent function to upload the file
+      const values = await uploadForm.validateFields();
+      console.log(file);
+      // Call the uploadTorrent function to upload the file with additional info
       await uploadTorrent(file);
 
       // Show a success message
       message.success('文件上传成功');
 
-      // Close the upload modal
+      // Close the upload modal and reset form
       setUploadModalVisible(false);
+      uploadForm.resetFields();
 
-      // Optionally reload the table or perform other actions (e.g., refresh list)
+      // Reload the table
       actionRef.current?.reload();
 
     } catch (error) {
@@ -132,6 +182,64 @@
     }
   };
 
+  // 修改获取标签的函数,处理API特定的响应格式
+  const handleGetTags = async (id: number) => {
+    try {
+      // 根据API的响应格式,获取rows数组中的标签
+      const response = await listBtTorrentTags({ torrentId: id });
+      console.log('API标签响应:', response);
+
+      // 检查响应格式并提取rows数组
+      if (response && response.rows && Array.isArray(response.rows)) {
+        setTorrentTags(response.rows);
+        console.log('设置标签:', response.rows);
+      } else {
+        console.log('未找到标签或格式不符');
+        setTorrentTags([]);
+      }
+    } catch (error) {
+      console.error('获取标签失败:', error);
+      message.error('获取标签失败');
+      setTorrentTags([]);
+    }
+  };
+
+  useEffect(() => {
+    if (current?.torrentId) {
+      handleGetTags(current.torrentId);
+    } else {
+      setTorrentTags([]); // 清空标签当没有选中种子时
+    }
+  }, [current]);
+
+  // 渲染标签列表的函数
+  const renderTags = () => {
+    if (!torrentTags || torrentTags.length === 0) {
+      return <span style={{ color: '#999' }}>暂无标签</span>;
+    }
+
+    // 定义一些可能的标签颜色
+    const tagColors = ['blue', 'green', 'cyan', 'purple', 'magenta', 'orange', 'gold', 'lime'];
+
+    return (
+      <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
+        {torrentTags.map((tag, index) => {
+          // 根据索引轮换颜色
+          const colorIndex = index % tagColors.length;
+          return (
+            <Tag
+              key={tag.id || tag.torrentId || index}
+              color={tagColors[colorIndex]}
+              style={{ margin: '0 4px 4px 0', padding: '2px 8px' }}
+            >
+              {tag.tag}
+            </Tag>
+          );
+        })}
+      </div>
+    );
+  };
+
   return (
     <Content>
       <Card bordered={false}>
@@ -142,22 +250,10 @@
           search={{ labelWidth: 100 }}
           toolBarRender={() => [
             <Button
-              key="add"
-              type="primary"
-              icon={<PlusOutlined />}
-              onClick={() => {
-                form.resetFields();
-                setCurrent({});
-                setModalVisible(true);
-              }}
-            >
-              新增
-            </Button>,
-            <Button
               key="upload"
               type="primary"
               icon={<UploadOutlined />}
-              onClick={() => setUploadModalVisible(true)} // Show the upload modal
+              onClick={() => setUploadModalVisible(true)}
             >
               上传种子文件
             </Button>
@@ -202,37 +298,155 @@
         {/* 上传种子文件的Modal */}
         <Modal
           title="上传种子文件"
-          visible={uploadModalVisible}
-          onCancel={() => setUploadModalVisible(false)}
-          footer={null}
+          open={uploadModalVisible}
+          onCancel={() => {
+            setUploadModalVisible(false);
+            uploadForm.resetFields();
+          }}
+          onOk={() => {
+            if (uploadFile) {
+              handleFileUpload(uploadFile);
+            } else {
+              message.error('请选择文件');
+            }
+          }}
         >
-          <Upload
-            customRequest={({ file, onSuccess, onError }) => {
-              setUploadFile(file);
-              handleFileUpload(file);
-              onSuccess?.();
-            }}
-            showUploadList={false}
-            accept=".torrent"
-          >
-            <Button icon={<UploadOutlined />}>点击上传 .torrent 文件</Button>
-          </Upload>
+          <Form form={uploadForm} layout="vertical">
+            <Form.Item
+              name="file"
+              label="种子文件"
+              rules={[{ required: true, message: '请选择种子文件' }]}
+            >
+              <Upload
+                customRequest={({ file, onSuccess }) => {
+                  setUploadFile(file as File);
+                  onSuccess?.();
+                }}
+                showUploadList={true}
+                maxCount={1}
+                accept=".torrent"
+                onRemove={() => setUploadFile(null)}
+              >
+                <Button icon={<UploadOutlined />}>选择 .torrent 文件</Button>
+              </Upload>
+            </Form.Item>
+            <Form.Item
+              name="description"
+              label="介绍"
+              rules={[{ required: true, message: '请输入种子介绍' }]}
+            >
+              <Input.TextArea rows={4} placeholder="请输入种子文件的详细介绍" />
+            </Form.Item>
+            <Form.Item
+              name="tags"
+              label="标签"
+              rules={[{ required: true, message: '请输入标签' }]}
+            >
+              <Select
+                mode="tags"
+                style={{ width: '100%' }}
+                placeholder="请输入标签,按回车键确认"
+                tokenSeparators={[',']}
+              />
+            </Form.Item>
+          </Form>
         </Modal>
 
         {/* 详情抽屉 */}
         <Drawer
-          width={500}
+          width={600}
           open={drawerVisible}
           onClose={() => setDrawerVisible(false)}
           title="种子详情"
+          extra={
+            <Button
+              type="primary"
+              icon={<CommentOutlined />}
+              onClick={() => navigate(`/torrent/comments/${current.torrentId}`)}
+            >
+              查看评论
+            </Button>
+          }
         >
           {current && (
-            <ProDescriptions<BtTorrent>
-              column={1}
-              title={current.name}
-              request={async () => ({ data: current })}
-              columns={columns as ProDescriptionsItemProps<BtTorrent>[]}
-            />
+            <>
+              {/* 不要使用request属性,直接使用dataSource */}
+              <ProDescriptions<BtTorrent>
+                column={1}
+                title={current.name}
+                dataSource={current}
+                columns={[
+                  {
+                    title: '种子ID',
+                    dataIndex: 'torrentId',
+                    valueType: 'text'
+                  },
+                  {
+                    title: '名称',
+                    dataIndex: 'name',
+                    valueType: 'text'
+                  },
+                  {
+                    title: 'infoHash',
+                    dataIndex: 'infoHash',
+                    valueType: 'text',
+                    copyable: true
+                  },
+                  {
+                    title: '大小 (bytes)',
+                    dataIndex: 'length',
+                    valueType: 'digit',
+
+                  },
+                  {
+                    title: '分片大小',
+                    dataIndex: 'pieceLength',
+                    valueType: 'digit'
+                  },
+                  {
+                    title: '片段数',
+                    dataIndex: 'piecesCount',
+                    valueType: 'digit'
+                  },
+                  {
+                    title: '创建人',
+                    dataIndex: 'createdBy',
+                    valueType: 'text'
+                  },
+                  {
+                    title: '上传时间',
+                    dataIndex: 'uploadTime',
+                    valueType: 'dateTime'
+                  },
+                  {
+                    title: '标签',
+                    dataIndex: 'tags',
+                    render: () => renderTags()
+                  }
+                ] as ProDescriptionsItemProps<BtTorrent>[]}
+              />
+
+              {/* 如果需要显示额外信息,可以在这里添加 */}
+              {current.description && (
+                <div style={{ marginTop: 16 }}>
+                  <h3>介绍</h3>
+                  <p>{current.description}</p>
+                </div>
+              )}
+
+              {/* 添加调试信息 - 开发时使用,生产环境可以移除 */}
+              {/* <div style={{ marginTop: 20, background: '#f5f5f5', padding: 10, borderRadius: 4 }}>
+                <h4>调试信息:</h4>
+                <p>当前种子ID: {current.torrentId}</p>
+                <p>标签数量: {torrentTags?.length || 0}</p>
+                <details>
+                  <summary>查看完整数据</summary>
+                  <pre style={{ maxHeight: 200, overflow: 'auto' }}>{JSON.stringify(current, null, 2)}</pre>
+                  <h5>标签数据:</h5>
+                  <pre style={{ maxHeight: 200, overflow: 'auto' }}>{JSON.stringify(torrentTags, null, 2)}</pre>
+                </details>
+              </div> */}
+            </>
           )}
         </Drawer>
       </Card>
@@ -240,4 +454,4 @@
   );
 };
 
-export default BtTorrentPage;
+export default BtTorrentPage;
\ No newline at end of file
diff --git a/react-ui/src/pages/Torrent/service.ts b/react-ui/src/pages/Torrent/service.ts
index 8e15618..e526151 100644
--- a/react-ui/src/pages/Torrent/service.ts
+++ b/react-ui/src/pages/Torrent/service.ts
@@ -6,6 +6,14 @@
   BtTorrentTag,
 } from '@/pages/Torrent/data'; // 假设你把 data.d.ts 放这里
 
+/** 下载种子文件 */
+export async function downloadTorrent(torrentId: number) {
+  return request(`/api/system/torrent/download/${torrentId}`, {
+    method: 'get',
+    responseType: 'blob',
+  });
+}
+
 // ================================
 // 种子主表(bt_torrent)接口
 // ================================
@@ -62,6 +70,7 @@
 
 /** 获取文件详情 */
 export async function getBtTorrentFile(id: number) {
+
   return request<BtTorrentFile>(`/api/system/file/${id}`, {
     method: 'get',
   });