Merge "我是人,我做了悬赏中心,可以进行悬赏哦"
diff --git a/config/routes.ts b/config/routes.ts
index f9e4fcb..b468a04 100644
--- a/config/routes.ts
+++ b/config/routes.ts
@@ -90,6 +90,34 @@
]
},
{
+ path: '/bounty',
+ name: '悬赏管理',
+ icon: 'read',
+ routes: [
+ {
+ path: '/bounty/list',
+ component: '@/pages/Bounty/List',
+ },
+ {
+ path: '/bounty/detail/:id',
+ component: '@/pages/Bounty/Detail',
+ },
+ {
+ name: 'bountyPublish', // 发布悬赏页面名称
+ path: '/bounty/publish', // 访问路径
+ component: './Bounty/BountyPublish' // 对应页面文件路径(相对于 src/pages)
+ },
+ {
+ name: 'bountyReply', // 回复悬赏页面名称
+ path: '/bounty/reply', // 访问路径
+ component: './Bounty/BountyReply' // 对应页面文件路径
+ }
+
+ ],
+ },
+
+
+ {
name: '帖子中心',
icon: 'read',
path: '/post/center',
diff --git a/src/locales/zh-CN/system/menu.ts b/src/locales/zh-CN/system/menu.ts
index 8a01e58..b06ec93 100644
--- a/src/locales/zh-CN/system/menu.ts
+++ b/src/locales/zh-CN/system/menu.ts
@@ -19,4 +19,7 @@
'system.menu.update_by': '更新者',
'system.menu.update_time': '更新时间',
'system.menu.remark': '备注',
+ 'system.menu.bounty': '悬赏管理', // 父级菜单名称
+ 'system.menu.bounty.publish': '发布悬赏', // 子菜单1
+ 'system.menu.bounty.reply': '回复悬赏'
};
diff --git a/src/pages/Bounty/BountyManage.tsx b/src/pages/Bounty/BountyManage.tsx
new file mode 100644
index 0000000..609e6c6
--- /dev/null
+++ b/src/pages/Bounty/BountyManage.tsx
@@ -0,0 +1,70 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Button, message, Space } from 'antd';
+import axios from 'axios'; // 新增 axios 导入
+
+const BountyManage: React.FC = () => {
+ const [dataSource, setDataSource] = useState<API.Bounty[]>([]);
+
+ // 加载悬赏列表(修改请求方式)
+ useEffect(() => {
+ const loadBountyList = async () => {
+ try {
+ const res = await axios.get('/api/bounties', { params: { status: [0, 1, 2].join(',') } }); // 替换 get 为 axios.get
+ if (res.data.code === 200) {
+ setDataSource(res.data.data.records || []);
+ }
+ } catch (err) {
+ message.error('加载悬赏列表失败');
+ }
+ };
+ loadBountyList();
+ }, []);
+
+ // 操作列(修改请求方式)
+ const handleClose = async (id: number) => {
+ try {
+ //这个接口哪来的??
+ const res = await axios.post('/api/bounties/close', { id }); // 替换 post 为 axios.post
+ if (res.data.code === 200) {
+ message.success('悬赏已关闭');
+ setDataSource(dataSource.map(item => item.id === id ? { ...item, status: 2 } : item));
+ }
+ } catch (err) {
+ message.error('关闭失败');
+ }
+ };
+
+ const columns = [
+ { title: '我是manage标题', dataIndex: 'title' },
+ { title: '奖励', dataIndex: 'reward' },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ // 显式声明 status 为 number 类型
+ render: (status: number) => status === 0 ? '进行中' : status === 1 ? '已完成' : '已关闭'
+ },
+ {
+ title: '操作',
+ // 显式声明 record 为 API.Bounty 类型(需确保 API.Bounty 已在 types.d.ts 中定义)
+ render: (value: unknown, record: API.Bounty) => (
+ <Space>
+ <Button type="link" onClick={() => handleClose(record.id)}>关闭</Button>
+ </Space>
+ )
+ }
+ ];
+
+ return (
+ <div className="page-container">
+ <h2>悬赏管理</h2>
+ <Table
+ dataSource={dataSource}
+ columns={columns}
+ rowKey="id"
+ pagination={{ pageSize: 10 }}
+ />
+ </div>
+ );
+};
+
+export default BountyManage;
diff --git a/src/pages/Bounty/BountyPublish.tsx b/src/pages/Bounty/BountyPublish.tsx
new file mode 100644
index 0000000..b2d121b
--- /dev/null
+++ b/src/pages/Bounty/BountyPublish.tsx
@@ -0,0 +1,116 @@
+import React from 'react';
+import { Form, Input, InputNumber, DatePicker, Button, message } from 'antd';
+import axios from 'axios';
+import { publishBounty } from '@/services/bounty/bounty'; // 新增 axios 导入
+
+interface BountyPublishProps {
+ onSuccess?: () => void; // 提交成功回调
+ onCancel?: () => void; // 取消操作回调
+}
+
+
+
+const BountyPublish: React.FC<BountyPublishProps> = ({ onSuccess, onCancel }) => {
+ const [form] = Form.useForm();
+
+ // ✅ 替换 axios 请求为服务方法调用
+ const handleSubmit = async (values: {
+ title: string;
+ description: string;
+ reward: number;
+ deadline: string;
+ }) => {
+ try {
+ // 使用服务层方法
+ const res = await publishBounty(values);
+
+ if (res) {
+ //message.success('悬赏发布成功!');
+ form.resetFields();
+ onSuccess?.();
+ } else {
+ message.error(res.msg || '发布失败,请重试');
+ }
+ } catch (err) {
+ console.error('发布失败:', err);
+ message.error('网络请求失败,请检查网络');
+ }
+ };
+
+
+ return (
+ <div className="page-container">
+ <h2>发布新悬赏</h2>
+ <Form
+ form={form}
+ layout="vertical"
+ onFinish={handleSubmit}
+ requiredMark="optional"
+ style={{ maxWidth: 600 }}
+ >
+ {/* 标题 */}
+ <Form.Item
+ name="title"
+ label="悬赏标题"
+ rules={[{ required: true, message: '请输入悬赏标题' }]}
+ >
+ <Input placeholder="请输入悬赏标题" />
+ </Form.Item>
+
+ {/* 描述 */}
+ <Form.Item
+ name="description"
+ label="悬赏描述"
+ rules={[{ required: true, message: '请输入悬赏描述' }]}
+ >
+ <Input.TextArea rows={4} placeholder="请输入悬赏描述" />
+ </Form.Item>
+
+ {/* 奖励 */}
+ <Form.Item
+ name="reward"
+ label="悬赏奖励"
+ rules={[
+ { required: true, message: '请输入奖励数值' },
+ { type: 'number', min: 1, message: '奖励必须大于0' },
+ ]}
+ >
+ <InputNumber min={1} placeholder="请输入奖励数值" style={{ width: '100%' }} />
+ </Form.Item>
+
+ {/* 截止时间 */}
+ <Form.Item
+ name="deadline"
+ label="截止时间"
+ rules={[{ required: true, message: '请选择截止时间' }]}
+ >
+ <DatePicker
+ showTime
+ format="YYYY-MM-DD HH:mm:ss"
+ placeholder="选择截止时间"
+ style={{ width: '100%' }}
+ />
+ </Form.Item>
+
+ {/* 提交按钮 */}
+ <Form.Item>
+ <Button type="primary" htmlType="submit">
+ 发布悬赏
+ </Button>
+ <Button
+ type="default"
+ onClick={() => {
+ form.resetFields();
+ onCancel?.(); // 🔥 取消时触发回调
+ }}
+ style={{ marginLeft: 16 }}
+ >
+ 重置
+ </Button>
+ </Form.Item>
+ </Form>
+ </div>
+ );
+};
+
+export default BountyPublish;
diff --git a/src/pages/Bounty/BountyReply.tsx b/src/pages/Bounty/BountyReply.tsx
new file mode 100644
index 0000000..c347686
--- /dev/null
+++ b/src/pages/Bounty/BountyReply.tsx
@@ -0,0 +1,154 @@
+import React, { useState, useEffect } from 'react';
+import { Form, Input, Select, Button, Upload, message } from 'antd';
+import axios from 'axios'; // 新增 axios 导入
+import { submitBountyReply, uploadBountyAttachment } from '@/services/bounty/bounty'; // ✅ 新增导入上传接口
+const { Option } = Select;
+interface BountyReplyProps {
+ bountyId: number; // 需要回复的悬赏ID
+ onSuccess?: () => void; // 提交成功回调
+ onCancel?: () => void; // 取消操作回调
+}
+
+const BountyReply: React.FC<BountyReplyProps> = ({
+ bountyId,
+ onSuccess,
+ onCancel
+ }) => {
+ const [form] = Form.useForm();
+ const [bountyList, setBountyList] = useState<{ id: number; title: string }[]>([]);
+ const [uploading, setUploading] = useState(false); // ✅ 新增上传状态
+ // 加载可回复的悬赏列表(修改请求方式)
+ useEffect(() => {
+ const loadBountyList = async () => {
+ try {
+ const res = await axios.get('/api/bounties', { params: { status: 0 } }); // 替换 get 为 axios.get
+ if (res.data.code === 200) {
+ setBountyList(res.data.data.records || []);
+ }
+ } catch (err) {
+ message.error('加载悬赏列表失败');
+ }
+ };
+ loadBountyList();
+ }, []);
+
+ // 附件上传逻辑(修改请求方式)
+ const handleUpload = async (file: File) => {
+ try {
+ setUploading(true);
+ // 1. 上传文件获取路径
+ const filePath = await uploadBountyAttachment(file);
+ // 2. 保存文件对象到表单(用于后续提交)
+ form.setFieldsValue({ attachment: filePath, file }); // ✅ 同时保存路径和文件对象
+ return { url: filePath };
+ } catch (err) {
+ message.error('上传失败');
+ return { error: new Error('上传失败') };
+ } finally {
+ setUploading(false);
+ }
+ };
+
+ // 提交回复逻辑(修改请求方式)
+ // ✅ 替换 axios 请求为服务方法调用
+ const handleSubmit = async (values: { content: string; attachment?: string }) => {
+ try {
+ // 从表单获取文件对象(需在 handleUpload 中保存)
+ const file = form.getFieldValue('file'); // ✅ 获取文件对象
+
+ const res = await submitBountyReply({
+ bountyId,
+ ...values,
+ file, // ✅ 传递文件对象
+ });
+
+ if (res?.code === 200) {
+ message.success('提交成功');
+ form.resetFields();
+ onSuccess?.();
+ } else {
+ throw new Error('接口异常: ' + JSON.stringify(res));
+ }
+ } catch (err) {
+ console.error('提交失败:', err);
+ message.error('提交失败,请重试');
+ }
+ };
+
+ // @ts-ignore
+ // @ts-ignore
+ // @ts-ignore
+ // @ts-ignore
+ // @ts-ignore
+ // @ts-ignore
+ return (
+ <div className="page-container">
+ <h2>回复悬赏</h2>
+ <Form
+ form={form}
+ layout="vertical"
+ onFinish={handleSubmit}
+ requiredMark="optional"
+ style={{ maxWidth: 600 }}
+ >
+ {/* 选择悬赏(默认选中传入的bountyId) */}
+ <Form.Item
+ name="bountyId"
+ label="选择悬赏"
+ rules={[{ required: true, message: '请选择要回复的悬赏' }]}
+ initialValue={bountyId}
+ >
+ <Select disabled={!!bountyId} placeholder="请选择要回复的悬赏">
+ {bountyList.map((item) => (
+ <Option key={item.id} value={item.id}>
+ {item.title}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+
+ {/* 回复内容(保持原有逻辑) */}
+ <Form.Item
+ name="content"
+ label="回复内容"
+ rules={[{ required: true, message: '请输入回复内容' }]}
+ >
+ <Input.TextArea rows={4} placeholder="请输入回复内容" />
+ </Form.Item>
+
+ {/* 附件上传(保持原有逻辑) */}
+ <Form.Item name="attachment" label="附件上传">
+ <Upload
+ customRequest={({ file, onSuccess, onError }) => {
+ handleUpload(file as File); // 显式断言 file 为 File 类型
+ }} // 使用服务层上传
+ maxCount={1}
+ accept=".doc,.docx,.pdf,.zip"
+ showUploadList={false} // 隐藏默认上传列表
+ >
+ <Button loading={uploading}>上传附件</Button>
+ </Upload>
+ </Form.Item>
+
+ {/* 提交按钮 */}
+ <Form.Item>
+ <Button type="primary" htmlType="submit">
+ 提交回复
+ </Button>
+ <Button
+ type="default"
+ onClick={() => {
+ form.resetFields();
+ onCancel?.(); // 🔥 取消时触发回调
+ }}
+ style={{ marginLeft: 16 }}
+ >
+ 取消
+ </Button>
+ </Form.Item>
+ </Form>
+ </div>
+ );
+};
+
+export default BountyReply;
diff --git a/src/pages/Bounty/Detail.tsx b/src/pages/Bounty/Detail.tsx
new file mode 100644
index 0000000..41a25df
--- /dev/null
+++ b/src/pages/Bounty/Detail.tsx
@@ -0,0 +1,231 @@
+import React, { useState, useEffect } from 'react';
+import { Button, Descriptions, List, message, Tag } from 'antd';
+import axios from 'axios'; // 新增 axios 导入
+import { useParams } from 'umi';
+
+import { getBountyDetail, getProfile } from '@/services/bounty/bounty';
+import { downloadAttachment,adoptSubmission } from '@/services/bounty/bounty';
+
+
+
+const BountyDetail: React.FC = () => {
+ const [bounty, setBounty] = useState<API.BountyDetail>({} as API.BountyDetail);
+ const [loading, setLoading] = useState(false); // 新增加载状态
+ const [currentUserId, setCurrentUserId] = useState<number | null>(null);
+
+ console.log('当前用户id:',currentUserId);
+ console.log('悬赏发布用户id:',bounty.creator_id);
+
+ const handleAdoptSubmission = async (submissionId: number, currentStatus: number) => {
+ console.log('【采纳请求】', {
+ submissionId,
+ currentStatus,
+ bounty: bounty,
+ currentUserId
+ });
+
+ if (currentStatus === 1) return;
+
+ try {
+ const res = await adoptSubmission(submissionId);
+ console.log('【采纳响应】', {
+ status: res.status,
+ data: res.data,
+ headers: res.headers
+ });
+
+ if (res.code === 200) {
+ message.success('采纳成功');
+ setBounty(prev => {
+ console.log('【状态更新】', {
+ old: prev.submissions,
+ new: prev.submissions?.map(sub =>
+ sub.id === submissionId ? { ...sub, status: 1 } : sub
+ )
+ });
+
+ return {
+ ...prev,
+ submissions: prev.submissions?.map(sub =>
+ sub.id === submissionId ? { ...sub, status: 1 } : sub
+ )
+ };
+ });
+ } else {
+ message.error(`采纳失败: ${res.msg}`);
+ }
+ } catch (error: any) {
+ console.error('【采纳错误】', {
+ error: error,
+ response: error.response?.data,
+ status: error.response?.status
+ });
+
+ message.error(`网络异常: ${error.message}`);
+ }
+ };
+
+ const handleDownload = async (attachmentPath: string, submissionUserId: number) => {
+ try {
+
+ // ✅ 新增权限校验
+ if (!currentUserId || (currentUserId !== bounty.creator_id && currentUserId !== submissionUserId)) {
+ message.error('无权查看此附件');
+ return;
+ }
+
+ const blob = await downloadAttachment(attachmentPath);
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = attachmentPath.split('/').pop() || 'file';
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ window.URL.revokeObjectURL(url);
+ } catch (err) {
+ message.error('下载附件失败,请重试');
+ console.error('下载附件失败:', err);
+ }
+ };
+
+
+
+ useEffect(() => {
+ const fetchProfile = async () => {
+ try {
+ const res = await getProfile(); // 调用后端接口
+ if (res && res.code === 200) {
+ setCurrentUserId(res.data.userId); // 从接口获取 userId
+ } else {
+ message.error('获取用户信息失败');
+ }
+ } catch (err) {
+ console.error('获取用户信息失败:', err);
+ }
+ };
+ fetchProfile();
+ }, []);
+
+
+ const { id } = useParams<{ id: string }>();
+
+ // 修改加载方法(适配统一请求)✅
+ useEffect(() => {
+ if (!id) return;
+
+ const loadBountyDetail = async () => {
+ try {
+ setLoading(true);
+ const res = await getBountyDetail(id);
+ console.log('【详情响应】原始数据:', res); // 👈 关键日志
+
+ if (res && res.code === 200) {
+ setBounty(res.data);
+ // 👇 新增:检查 submissions 数据结构
+ console.log('【submissions 数据】:', res.data.submissions);
+ } else {
+ throw new Error('响应结构异常');
+ }
+ } catch (err) {
+ console.error('【详情请求】错误:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadBountyDetail();
+ }, [id]);
+
+ return (
+ <div className="page-container">
+ <h2>悬赏详情</h2>
+
+ {/* 基础信息 */}
+ <Descriptions title="悬赏信息" bordered>
+ <Descriptions.Item label="标题">{bounty.title}</Descriptions.Item>
+ <Descriptions.Item label="发布者ID">{bounty.creator_id}</Descriptions.Item> // ✅ 新增字段
+ <Descriptions.Item label="奖励">{bounty.reward}</Descriptions.Item>
+ <Descriptions.Item label="状态">
+ {bounty.status === 0 ? '进行中' : bounty.status === 1 ? '已完成' : '已关闭'}
+ </Descriptions.Item>
+ <Descriptions.Item label="截止时间">{bounty.deadline}</Descriptions.Item>
+ <Descriptions.Item label="描述" span={3}>
+ {bounty.description}
+ </Descriptions.Item>
+ </Descriptions>
+
+ {/* 回复列表 */}
+ {bounty.submissions && (
+ <div style={{ marginTop: 24 }}>
+ <h3>回复列表</h3>
+ <List
+ dataSource={bounty.submissions}
+ renderItem={(item) => (
+ <List.Item>
+ <List.Item.Meta
+ title={`回复人ID:${item.userId}`}
+ description={
+ <>
+ {item.content}
+ {/* 状态标签 */}
+ <span style={{ marginLeft: 16 }}>
+ {item.status === 1 ? (
+ <Tag color="green">已采纳</Tag>
+ ) : (
+ <Tag color="red">未被采纳</Tag>
+ )}
+ </span>
+ </>
+ }
+ />
+
+ {/* 发布者操作按钮 */}
+ {currentUserId === bounty.creator_id && (
+ <Button
+ type="primary"
+ size="small"
+ onClick={() => handleAdoptSubmission(item.id, item.status)}
+ disabled={item.status === 1}
+ >
+ {item.status === 1 ? '已采纳' : '采纳'}
+ </Button>
+ )}
+
+ {/* 附件下载 */}
+ {item.attachment && currentUserId === bounty.creator_id && (
+ <a onClick={(e) => handleDownload(item.attachment, item.userId)} style={{ marginLeft: 8 }}>
+ 查看附件
+ </a>
+ )}
+ </List.Item>
+ )}
+ />
+ </div>
+ )}
+ </div>
+ );
+};
+
+// 定义类型(建议单独放在src/types.d.ts中)
+declare namespace API {
+ export interface BountyDetail {
+ id: number;
+ title: string;
+ creator_id: number; // ✅ 新增:发布者ID
+ description: string;
+ reward: number;
+ deadline: string;
+ status: number;
+ submissions: Array<{
+ id: number;
+ userId: number; // ✅ 替换 id 为 userId
+ username: string;
+ content: string;
+ attachment: string;
+ status: number;
+ }>;
+ }
+}
+
+export default BountyDetail;
diff --git a/src/pages/Bounty/List.tsx b/src/pages/Bounty/List.tsx
new file mode 100644
index 0000000..ca789c5
--- /dev/null
+++ b/src/pages/Bounty/List.tsx
@@ -0,0 +1,183 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Button, message, Space, Modal } from 'antd';
+import axios from 'axios'; // 新增 axios 导入
+import { useNavigate } from 'umi';
+
+import { getBountyList } from '@/services/bounty/bounty';
+import BountyPublish from './BountyPublish';
+import BountyReply from './BountyReply';
+const BountyList: React.FC = () => {
+ const [dataSource, setDataSource] = useState<API.Bounty[]>([]);
+ const [loading, setLoading] = useState(false);
+ const [publishVisible, setPublishVisible] = useState(false);
+ const [replyVisible, setReplyVisible] = useState(false);
+ const [selectedId, setSelectedId] = useState<number | null>(null);
+
+ const navigate = useNavigate();
+
+ // 加载悬赏列表(修改请求方式)
+
+ const loadBountyList = async () => {
+ try {
+ setLoading(true);
+ const res = await getBountyList();
+
+ // 添加详细日志输出
+ console.log('接口原始响应:', res);
+
+ // 修改数据处理逻辑以适配实际数据结构
+ if (res && Array.isArray(res)) {
+ // 当接口直接返回数组时
+ setDataSource(res);
+ } else if (res && res.code === 200) {
+ // 当接口返回标准分页结构时
+ setDataSource(res.data.records || res.data || []);
+ } else {
+ throw new Error('接口返回异常结构');
+ }
+ } catch (err) {
+ console.error('加载数据异常:', err);
+ console.error('完整错误:', err);
+
+ // 添加网络错误处理
+ // @ts-ignore
+ if (err?.response?.status === 401) {
+ message.error('认证失效,请重新登录');
+ navigate('/login');
+ } else {
+ message.error('加载悬赏列表失败');
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadBountyList();
+ }, []);
+
+ // ✅ 发布成功回调(普通函数,无需useEffect)
+ const handlePublishSuccess = () => {
+ setPublishVisible(false);
+ loadBountyList(); // 重新加载数据
+ message.success('悬赏发布成功');
+ };
+
+ // ✅ 回复成功回调(普通函数,无需useEffect)
+ const handleReplySuccess = () => {
+ setReplyVisible(false);
+ loadBountyList(); // 重新加载数据
+ message.success('回复提交成功');
+ };
+
+
+ // 跳转详情页
+ const goDetail = (id: number) => {
+ navigate(`/bounty/detail/${id}`);
+ };
+
+ // 表格列配置
+ const columns = [
+ {
+ title: '标题',
+ dataIndex: 'title',
+ width: 200,
+ ellipsis: true,
+ },
+ {
+ title: '发布者ID',
+ dataIndex: 'creator_id', // ✅ 新增列
+ width: 100,
+ align: 'center' as const,
+ },
+ {
+ title: '奖励',
+ dataIndex: 'reward',
+ width: 100,
+ align: 'center' as const,
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ width: 100,
+ render: (status: number) => (
+ status === 0 ? '未回复' : status === 1 ? '已回复' : '已关闭'
+ )
+ },
+ {
+ title: '操作',
+ width: 160,
+ render: (value: unknown, record: API.Bounty) => (
+ <Space>
+ <Button
+ type="link"
+ onClick={() => navigate(`/bounty/detail/${record.id}`)}
+ >
+ 查看详情
+ </Button>
+ <Button
+ type="link"
+ onClick={() => {
+ setSelectedId(record.id);
+ setReplyVisible(true);
+ }}
+ >
+ 回复悬赏
+ </Button>
+ </Space>
+ )
+ }
+ ];
+
+ return (
+ <div className="page-container">
+ {/* 顶部操作区 */}
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
+ <h2>悬赏列表</h2>
+ {/* 发布按钮 */}
+ <Button
+ type="primary"
+ onClick={() => setPublishVisible(true)}
+ >
+ 发布悬赏
+ </Button>
+ </div>
+
+ {/* 悬赏列表 */}
+ <Table
+ dataSource={dataSource}
+ columns={columns}
+ rowKey="id"
+ pagination={{ pageSize: 10 }}
+ loading={loading}
+ />
+
+ {/* 发布悬赏模态框 */}
+ <Modal
+ title="发布新悬赏"
+ open={publishVisible}
+ onCancel={() => setPublishVisible(false)}
+ footer={null}
+ >
+ <BountyPublish onSuccess={handlePublishSuccess} />
+ </Modal>
+
+ {/* 回复悬赏模态框 */}
+ <Modal
+ title="回复悬赏"
+ open={replyVisible}
+ onCancel={() => setReplyVisible(false)}
+ footer={null}
+ >
+ {selectedId && (
+ <BountyReply
+ bountyId={selectedId}
+ onSuccess={handleReplySuccess}
+ />
+ )}
+ </Modal>
+ </div>
+ );
+};
+
+export default BountyList;
diff --git a/src/services/bounty/bounty.ts b/src/services/bounty/bounty.ts
new file mode 100644
index 0000000..9c1ef34
--- /dev/null
+++ b/src/services/bounty/bounty.ts
@@ -0,0 +1,120 @@
+// src/services/bounty.ts
+import { request } from '@umijs/max';
+
+// 查询悬赏列表
+export async function getBountyList(params?: Record<string, any>) {
+ return request('/api/bounties', {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ params,
+ });
+}
+
+export async function getBountyDetail(id: number | string) {
+ return request(`/api/bounties/${id}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ }).then(data => ({ code: 200, data })); // ✅ 自动包装成标准结构
+}
+
+// 新增:发布悬赏
+export async function publishBounty(params: {
+ title: string;
+ description: string;
+ reward: number;
+ deadline: string;
+}) {
+ return request('/api/bounties/publish', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ data: {
+ ...params,
+ status: 0
+ },
+ });
+}
+
+// 新增:上传附件接口
+export async function uploadBountyAttachment(file: File) {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ return request('/api/bounty-submissions/upload', {
+ method: 'POST',
+ data: formData,
+ // ✅ 移除手动设置 Content-Type
+ // headers: {
+ // 'Content-Type': 'multipart/form-data', // ❌ 浏览器会自动设置 boundary
+ // },
+ });
+}
+
+// 修改:提交悬赏回复(不再处理文件)
+// src/services/bounty/bounty.ts
+// src/services/bounty/bounty.ts
+// src/services/bounty/bounty.ts
+export async function submitBountyReply(params: {
+ bountyId: number;
+ content: string;
+ attachment?: string;
+ file?: File;
+}) {
+ console.log('【提交请求】/api/bounty-submissions 参数:', params);
+
+ const formData = new FormData();
+ formData.append('submission', new Blob([JSON.stringify(params)], { type: 'application/json' }));
+ if (params.file) {
+ formData.append('file', params.file);
+ }
+
+ return request('/api/bounty-submissions', {
+ method: 'POST',
+ data: formData,
+ }).then(res => {
+ console.log('【接口响应】/api/bounty-submissions:', res);
+ // ✅ 确保返回统一结构
+ return {
+ code: res.code || (res ? 200 : 500),
+ data: res.data || res,
+ msg: res.message || '操作成功',
+ };
+ });
+}
+
+
+export async function downloadAttachment(attachmentPath: string): Promise<Blob> {
+ // ✅ 提取文件名
+ const filename = attachmentPath.split('/').pop() || 'file';
+
+ return request(`/api/bounty-submissions/download`, {
+ method: 'GET',
+ params: {
+ filename, // ✅ 只传递文件名
+ },
+ responseType: 'blob',
+ });
+}
+
+export async function getProfile() {
+ return request('/api/system/user/profile', {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ });
+}
+
+export async function adoptSubmission(submissionId: number) {
+ return request(`/api/bounty-submissions/${submissionId}/adopt`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ });
+}
diff --git a/src/services/post/index.ts b/src/services/post/index.ts
index 3452095..13e5bae 100644
--- a/src/services/post/index.ts
+++ b/src/services/post/index.ts
@@ -1,3 +1,4 @@
+// @ts-ignore
import { request } from '@umijs/max';
export interface PostListParams {
@@ -26,7 +27,7 @@
// 获取帖子列表
export async function getPostList(params: PostListParams) {
- console.log('getPostList', params);
+ console.log('getPostList', params);
return request<API.TableDataInfo>('/api/post-center/list', {
method: 'GET',
params,
@@ -138,7 +139,7 @@
method: 'POST',
data: file,
});
-}
+}
// 删除图片
export async function deleteImage(filename: string): Promise<API.AjaxResult> {
@@ -259,4 +260,4 @@
method: 'PUT',
data: { action, postId, reason },
});
-}
\ No newline at end of file
+}
diff --git a/src/types.d.ts b/src/types.d.ts
new file mode 100644
index 0000000..d65e0e6
--- /dev/null
+++ b/src/types.d.ts
@@ -0,0 +1,14 @@
+//如果需要定义 API 类型(如接口返回的用户信息、列表数据等),推荐:
+
+declare namespace API {
+ // 补充 Bounty 类型定义(根据实际接口返回字段调整)
+ export interface Bounty {
+ id: number;
+ title: string;
+ description: string;
+ reward: number;
+ deadline: string;
+ status: number; // 0-进行中,1-已完成,2-已关闭
+ creator_id: number;
+ }
+}