增加了悬赏,标签查看,评论页面,标签上传后端有问题,评论还没跟后端连,优化了一些小界面
Change-Id: I44f5ef2eb0a8ebd91a4b3b3b446f897bea41435f
diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts
index 919ae3b..92f45b3 100644
--- a/react-ui/config/routes.ts
+++ b/react-ui/config/routes.ts
@@ -75,6 +75,10 @@
},
{
+ path: '/torrent/comments/:torrentId',
+ component: './Torrent/Comments',
+ },
+ {
name: 'tool',
path: '/tool',
routes: [
diff --git a/react-ui/src/pages/Reward/components/UpdateForm.tsx b/react-ui/src/pages/Reward/components/UpdateForm.tsx
new file mode 100644
index 0000000..526b946
--- /dev/null
+++ b/react-ui/src/pages/Reward/components/UpdateForm.tsx
@@ -0,0 +1,122 @@
+import { Modal, Form, Input, InputNumber, Select } from 'antd';
+import React from 'react';
+import { useIntl } from 'umi';
+import { RewardItem } from '../data';
+export type FormValueType = {
+ rewardId?: number;
+ title?: string;
+ description?: string;
+ amount?: number;
+ status?: string;
+ remark?: string;
+} & Partial<RewardItem>;
+
+export type UpdateFormProps = {
+ onCancel: (flag?: boolean, formVals?: FormValueType) => void;
+ onSubmit: (values: FormValueType) => Promise<void>;
+ open: boolean;
+ values: Partial<RewardItem>;
+ statusOptions: any;
+ readOnly?: boolean;
+};
+
+const UpdateForm: React.FC<UpdateFormProps> = (props) => {
+ const [form] = Form.useForm();
+
+ const { statusOptions } = props;
+
+ const intl = useIntl();
+
+ React.useEffect(() => {
+ form.resetFields();
+ form.setFieldsValue({
+ rewardId: props.values.rewardId,
+ title: props.values.title,
+ description: props.values.description,
+ amount: props.values.amount,
+ status: props.values.status,
+ remark: props.values.remark,
+ });
+ }, [form, props]);
+
+ const handleOk = () => {
+ form.submit();
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ form.resetFields();
+ };
+ const handleFinish = async (values: FormValueType) => {
+ props.onSubmit(values);
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={props.readOnly ? '查看悬赏' : (props.values.rewardId ? '编辑悬赏' : '新增悬赏')}
+ open={props.open}
+ onOk={handleOk}
+ footer={props.readOnly ? null : undefined}
+ onCancel={handleCancel}
+ >
+ <Form
+ form={form}
+ onFinish={handleFinish}
+ initialValues={props.values}
+ labelCol={{ span: 6 }}
+ wrapperCol={{ span: 18 }}
+ >
+ <Form.Item name="rewardId" hidden={true}>
+ <Input />
+ </Form.Item>
+ <Form.Item
+ name="title"
+ label="悬赏标题"
+ rules={[{ required: true, message: '请输入悬赏标题!' }]}
+ >
+ <Input placeholder="请输入悬赏标题" disabled={props.readOnly} />
+ </Form.Item>
+ <Form.Item
+ name="description"
+ label="悬赏描述"
+ rules={[{ required: true, message: '请输入悬赏描述!' }]}
+ >
+ <Input.TextArea rows={4} placeholder="请输入悬赏描述" disabled={props.readOnly} />
+ </Form.Item>
+ <Form.Item
+ name="amount"
+ label="悬赏金额"
+ rules={[{ required: true, message: '请输入悬赏金额!' }]}
+ >
+ <InputNumber
+ style={{ width: '100%' }}
+ min={0}
+ precision={2}
+ placeholder="请输入悬赏金额"
+ disabled={props.readOnly}
+ />
+ </Form.Item>
+ {/* <Form.Item
+ name="status"
+ label="悬赏状态"
+ >
+ <Select
+ placeholder="请选择悬赏状态"
+ options={statusOptions.map((item: any) => ({
+ value: item.dictValue,
+ label: item.dictLabel,
+ }))}
+ />
+ </Form.Item> */}
+ <Form.Item
+ name="remark"
+ label="备注"
+ >
+ <Input.TextArea rows={2} placeholder="请输入备注" disabled={props.readOnly} />
+ </Form.Item>
+ </Form>
+ </Modal>
+ );
+};
+
+export default UpdateForm;
\ No newline at end of file
diff --git a/react-ui/src/pages/Reward/data.d.ts b/react-ui/src/pages/Reward/data.d.ts
new file mode 100644
index 0000000..6dbec7b
--- /dev/null
+++ b/react-ui/src/pages/Reward/data.d.ts
@@ -0,0 +1,38 @@
+export interface RewardItem {
+ /** 悬赏ID */
+ rewardId?: number;
+ /** 悬赏标题 */
+ title: string;
+ /** 悬赏描述 */
+ description: string;
+ /** 悬赏金额 */
+ amount: number;
+ /** 悬赏状态(0进行中 1已完成 2已取消) */
+ status?: string;
+ /** 发布者ID */
+ publisherId?: number;
+ /** 接受者ID */
+ accepterId?: number;
+ /** 创建时间 */
+ createTime?: string;
+ /** 更新时间 */
+ updateTime?: string;
+ /** 备注 */
+ remark?: string;
+}
+
+export interface RewardListParams {
+ /** 当前的页码 */
+ pageNum?: number;
+ /** 页面的容量 */
+ pageSize?: number;
+ /** 悬赏标题 */
+ title?: string;
+ /** 悬赏状态 */
+ status?: string;
+ /** 开始时间 */
+ 'params[beginTime]'?: string;
+ /** 结束时间 */
+ 'params[endTime]'?: string;
+}
+
diff --git a/react-ui/src/pages/Reward/index.tsx b/react-ui/src/pages/Reward/index.tsx
new file mode 100644
index 0000000..231415c
--- /dev/null
+++ b/react-ui/src/pages/Reward/index.tsx
@@ -0,0 +1,338 @@
+import { ExclamationCircleOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons';
+import { Button, message, Modal, Switch } from 'antd';
+import React, { useRef, useState, useEffect } from 'react';
+import { FormattedMessage, useIntl } from 'umi';
+import { FooterToolbar, PageContainer } from '@ant-design/pro-layout';
+import type { ActionType, ProColumns } from '@ant-design/pro-table';
+import ProTable from '@ant-design/pro-table';
+import type { FormInstance } from 'antd';
+import { getRewardList, removeReward, updateReward, addReward } from './service';
+import UpdateForm from './components/UpdateForm';
+import { getDictValueEnum } from '@/services/system/dict';
+import DictTag from '@/components/DictTag';
+import { useAccess } from 'umi';
+import { RewardItem, RewardListParams } from './data';
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: RewardItem[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ await removeReward(selectedRows.map((row) => row.rewardId).join(','));
+ hide();
+ message.success('删除成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+const handleUpdate = async (fields: RewardItem) => {
+ const hide = message.loading('正在更新');
+ try {
+ const resp = await updateReward(fields);
+ hide();
+ if (resp.code === 200) {
+ message.success('更新成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('配置失败请重试!');
+ return false;
+ }
+};
+
+const handleAdd = async (fields: RewardItem) => {
+ const hide = message.loading('正在添加');
+ try {
+ const resp = await addReward(fields);
+ hide();
+ if (resp.code === 200) {
+ message.success('添加成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('配置失败请重试!');
+ return false;
+ }
+};
+
+const RewardTableList: React.FC = () => {
+ const formTableRef = useRef<FormInstance>();
+
+ const [modalVisible, setModalVisible] = useState<boolean>(false);
+ const [readOnly, setReadOnly] = useState<boolean>(false);
+
+ const actionRef = useRef<ActionType>();
+ const [currentRow, setCurrentRow] = useState<RewardItem>();
+ const [selectedRows, setSelectedRows] = useState<RewardItem[]>([]);
+
+ const [statusOptions, setStatusOptions] = useState<any>([]);
+
+ const access = useAccess();
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ useEffect(() => {
+ getDictValueEnum('reward_status').then((data) => {
+ setStatusOptions(data);
+ });
+ }, []);
+
+ const columns: ProColumns<RewardItem>[] = [
+ {
+ title: '悬赏ID',
+ dataIndex: 'rewardId',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: '悬赏标题',
+ dataIndex: 'title',
+ valueType: 'text',
+ },
+ {
+ title: '悬赏金额',
+ dataIndex: 'amount',
+ valueType: 'money',
+ hideInSearch: true,
+ },
+ // {
+ // title: '悬赏状态',
+ // dataIndex: 'status',
+ // valueType: 'select',
+ // valueEnum: statusOptions,
+ // render: (_, record) => {
+ // return (<DictTag enums={statusOptions} value={record.status} />);
+ // },
+ // },
+ {
+ title: '发布时间',
+ dataIndex: 'createTime',
+ valueType: 'dateRange',
+ render: (_, record) => {
+ return (<span>{record.createTime?.toString()}</span>);
+ },
+ search: {
+ transform: (value) => {
+ return {
+ 'params[beginTime]': value[0],
+ 'params[endTime]': value[1],
+ };
+ },
+ },
+ },
+ {
+ title: '备注',
+ dataIndex: 'remark',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '220px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ key="view"
+ onClick={() => {
+ setModalVisible(true);
+ setCurrentRow(record);
+ // 设置只读模式
+ setReadOnly(true);
+ }}
+ >
+ 查看
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ key="edit"
+ hidden={!access.hasPerms('reward:reward:edit')}
+ onClick={() => {
+ setModalVisible(true);
+ setCurrentRow(record);
+ setReadOnly(false);
+ }}
+ >
+ 编辑
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ danger
+ key="batchRemove"
+ hidden={!access.hasPerms('reward:reward:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove([record]);
+ if (success) {
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ },
+ });
+ }}
+ >
+ 删除
+ </Button>,
+ ],
+ },
+ ];
+
+ return (
+ <PageContainer>
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<RewardItem>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="rewardId"
+ key="rewardList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="add"
+ hidden={!access.hasPerms('reward:reward:add')}
+ onClick={async () => {
+ setCurrentRow(undefined);
+ setModalVisible(true);
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
+ </Button>,
+ <Button
+ type="primary"
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('reward:reward:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ ]}
+ request={(params) =>
+ getRewardList({ ...params } as RewardListParams).then((res) => {
+ return {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </div>
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="remove"
+ danger
+ hidden={!access.hasPerms('reward:reward:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ <UpdateForm
+ readOnly={readOnly}
+ onSubmit={async (values) => {
+ let success = false;
+ if (values.rewardId) {
+ success = await handleUpdate({ ...values } as RewardItem);
+ } else {
+ success = await handleAdd({ ...values } as RewardItem);
+ }
+ if (success) {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ }}
+ onCancel={() => {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ setReadOnly(false);
+ }}
+ open={modalVisible}
+ values={currentRow || {}}
+ statusOptions={statusOptions}
+ />
+ </PageContainer>
+ );
+};
+
+export default RewardTableList;
\ No newline at end of file
diff --git a/react-ui/src/pages/Reward/service.ts b/react-ui/src/pages/Reward/service.ts
new file mode 100644
index 0000000..7550cf9
--- /dev/null
+++ b/react-ui/src/pages/Reward/service.ts
@@ -0,0 +1,49 @@
+import { request } from '@umijs/max';
+import type {
+ RewardItem,
+ RewardListParams,
+} from '@/pages/Reward/data'; // 假设你把 data.d.ts 放这里
+
+/** 获取悬赏任务列表 */
+export async function getRewardList(params?: RewardListParams) {
+ const queryString = params
+ ? `?${new URLSearchParams(params as Record<string, any>).toString()}`
+ : '';
+ const response = await request(`/api/reward/list${queryString}`, {
+ method: 'get',
+ });
+ if (!response || response.length === 0) {
+ return [{ id: 1, name: '虚假任务1', description: '这是一个虚假的任务描述' }, { id: 2, name: '虚假任务2', description: '这是另一个虚假的任务描述' }];
+ }
+ return response;
+}
+
+/** 获取悬赏任务详细信息 */
+export async function getReward(rewardId: number) {
+ return request(`/api/reward/${rewardId}`, {
+ method: 'get',
+ });
+}
+
+/** 新增悬赏任务 */
+export async function addReward(params: RewardItem) {
+ return request('/api/reward', {
+ method: 'post',
+ data: params,
+ });
+}
+
+/** 修改悬赏任务 */
+export async function updateReward(params: RewardItem) {
+ return request('/api/reward', {
+ method: 'put',
+ data: params,
+ });
+}
+
+/** 删除悬赏任务 */
+export async function removeReward(ids: string) {
+ return request(`/api/reward/${ids}`, {
+ method: 'delete',
+ });
+}
\ No newline at end of file
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',
});
diff --git a/react-ui/src/pages/User/Center/index.tsx b/react-ui/src/pages/User/Center/index.tsx
index 2ce308c..7272fa3 100644
--- a/react-ui/src/pages/User/Center/index.tsx
+++ b/react-ui/src/pages/User/Center/index.tsx
@@ -14,6 +14,7 @@
import AvatarCropper from './components/AvatarCropper';
import { useRequest } from '@umijs/max';
import { getUserInfo } from '@/services/session';
+import { getUserRateInfo } from '@/services/system/user';
import { PageLoading } from '@ant-design/pro-components';
const operationTabList = [
@@ -38,20 +39,25 @@
export type tabKeyType = 'base' | 'password';
const Center: React.FC = () => {
-
+
const [tabKey, setTabKey] = useState<tabKeyType>('base');
-
+
const [cropperModalOpen, setCropperModalOpen] = useState<boolean>(false);
-
+
// 获取用户信息
const { data: userInfo, loading } = useRequest(async () => {
- return { data: await getUserInfo()};
+ return { data: await getUserInfo() };
+ });
+
+ const { data: userRateInfo } = useRequest(async () => {
+ return { data: await getUserRateInfo() };
});
if (loading) {
return <div>loading...</div>;
}
- const currentUser = userInfo?.user;
+ const currentUser = { ...userInfo?.user, ...userRateInfo?.data };
+
// 渲染用户信息
const renderUserInfo = ({
@@ -60,6 +66,10 @@
email,
sex,
dept,
+ uploadCount,
+ downloadCount,
+ rateCount,
+ score,
}: Partial<API.CurrentUser>) => {
return (
<List>
@@ -109,6 +119,39 @@
</List.Item>
<List.Item>
<div>
+ <MailOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 上传量
+ </div>
+ <div>{uploadCount}</div>
+ </List.Item>
+ <List.Item>
+ <div>
+ <MailOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 下载量
+ </div>
+ <div>{downloadCount}</div>
+ </List.Item>
+ <List.Item>
+ <div>
+ <MailOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 分享率
+ </div>
+ <div>{rateCount}</div>
+ </List.Item>
+ {/* <List.Item>
+ <div>
<ClusterOutlined
style={{
marginRight: 8,
@@ -117,6 +160,17 @@
部门
</div>
<div>{dept?.deptName}</div>
+ </List.Item> */}
+ <List.Item>
+ <div>
+ <MailOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 积分
+ </div>
+ <div>{score}</div>
</List.Item>
</List>
);
@@ -147,13 +201,13 @@
loading={loading}
>
{!loading && (
- <div style={{ textAlign: "center"}}>
- <div className={styles.avatarHolder} onClick={()=>{setCropperModalOpen(true)}}>
+ <div style={{ textAlign: "center" }}>
+ <div className={styles.avatarHolder} onClick={() => { setCropperModalOpen(true) }}>
<img alt="" src={currentUser.avatar} />
</div>
{renderUserInfo(currentUser)}
<Divider dashed />
- <div className={styles.team}>
+ {/* <div className={styles.team}>
<div className={styles.teamTitle}>角色</div>
<Row gutter={36}>
{currentUser.roles &&
@@ -168,7 +222,7 @@
</Col>
))}
</Row>
- </div>
+ </div> */}
</div>
)}
</Card>
@@ -188,7 +242,7 @@
</Row>
<AvatarCropper
onFinished={() => {
- setCropperModalOpen(false);
+ setCropperModalOpen(false);
}}
open={cropperModalOpen}
data={currentUser.avatar}
diff --git a/react-ui/src/services/system/user.ts b/react-ui/src/services/system/user.ts
index da997bf..9f45f12 100644
--- a/react-ui/src/services/system/user.ts
+++ b/react-ui/src/services/system/user.ts
@@ -79,6 +79,14 @@
})
}
+// 查询用户上传,下载,分享率等信息
+export function getUserRateInfo() {
+ return request('/api/system/user/profile/info', {
+ method: 'get'
+ })
+}
+
+
export function updateUserProfile(data: API.CurrentUser) {
return request<API.Result>('/api/system/user/profile', {
method: 'put',
diff --git a/react-ui/src/services/typings.d.ts b/react-ui/src/services/typings.d.ts
index 1e0b8b5..20f44dc 100644
--- a/react-ui/src/services/typings.d.ts
+++ b/react-ui/src/services/typings.d.ts
@@ -57,6 +57,10 @@
dept?: Dept;
roles?: Role[];
permissions: string[];
+ uploadCount?: number;
+ downloadCount?: number;
+ rateCount?: number;
+ score?: number;
}
interface UserInfoVO {