刘嘉昕 | 33b9f17 | 2025-06-09 17:23:06 +0800 | [diff] [blame] | 1 | import React, { useState, useEffect } from 'react'; |
| 2 | import { useNavigate } from 'react-router-dom'; |
| 3 | import { |
| 4 | Table, |
| 5 | Button, |
| 6 | Modal, |
| 7 | Image, |
| 8 | message, |
| 9 | Spin, |
| 10 | Input, |
| 11 | Select, |
| 12 | Pagination, |
| 13 | Space |
| 14 | } from 'antd'; |
| 15 | import { ExclamationCircleOutlined } from '@ant-design/icons'; |
| 16 | import axios from 'axios'; |
| 17 | |
| 18 | const { confirm } = Modal; |
| 19 | const { Option } = Select; |
| 20 | |
| 21 | const TorrentManagement = () => { |
| 22 | // 状态管理 |
| 23 | const [torrents, setTorrents] = useState([]); |
| 24 | const [isLoading, setIsLoading] = useState(false); |
| 25 | const [error, setError] = useState(null); |
| 26 | const [selectedTorrentId, setSelectedTorrentId] = useState(null); |
| 27 | const [promotionOptions, setPromotionOptions] = useState([ |
| 28 | { value: 1, label: '上传加倍' }, |
| 29 | { value: 2, label: '下载减半' }, |
| 30 | { value: 3, label: '免费下载' }, |
| 31 | { value: 0, label: '无促销' } |
| 32 | ]); |
| 33 | const [selectedPromotion, setSelectedPromotion] = useState(null); |
| 34 | const [showPromotionWarning, setShowPromotionWarning] = useState(false); |
| 35 | const [currentUserId, setCurrentUserId] = useState(null); |
| 36 | const [applyPromotionsLoading, setApplyPromotionsLoading] = useState(false); |
| 37 | const [usernames, setUsernames] = useState({}); |
| 38 | const [searchKeyword, setSearchKeyword] = useState(''); |
| 39 | const [currentPage, setCurrentPage] = useState(1); |
| 40 | const [pageSize, setPageSize] = useState(10); |
| 41 | const navigate = useNavigate(); // 用于导航到详情页 |
| 42 | |
| 43 | // 获取当前用户ID |
| 44 | useEffect(() => { |
| 45 | const userId = 1; // 示例,实际从认证系统获取 |
| 46 | setCurrentUserId(userId ? parseInt(userId) : null); |
| 47 | }, []); |
| 48 | |
| 49 | // 获取所有种子数据 |
| 50 | useEffect(() => { |
| 51 | fetchAllTorrents(); |
| 52 | }, [searchKeyword]); |
| 53 | |
| 54 | // 获取所有种子数据的函数 |
| 55 | const fetchAllTorrents = async () => { |
| 56 | setIsLoading(true); |
| 57 | setError(null); |
| 58 | try { |
| 59 | const res = await axios.get('http://localhost:8080/torrent/list'); |
| 60 | setTorrents(res.data); |
| 61 | setCurrentPage(1); // 重置为第一页 |
| 62 | } catch (err) { |
| 63 | console.error('获取种子失败', err); |
| 64 | setError('获取种子列表失败,请稍后重试'); |
| 65 | message.error('获取种子列表失败'); |
| 66 | } finally { |
| 67 | setIsLoading(false); |
| 68 | } |
| 69 | }; |
| 70 | console.log('当前种子列表:', torrents); |
| 71 | |
| 72 | // 在组件加载时,批量获取所有 uploader_id 对应的 username |
| 73 | useEffect(() => { |
| 74 | const fetchUsernames = async () => { |
| 75 | if (torrents.length === 0) return; |
| 76 | |
| 77 | const usernamePromises = torrents.map(async (torrent) => { |
| 78 | if (torrent.uploader_id && !usernames[torrent.uploader_id]) { |
| 79 | try { |
| 80 | const response = await fetch(`http://localhost:8080/torrent/${torrent.uploader_id}/username`); |
| 81 | if (response.ok) { |
| 82 | const username = await response.text(); |
| 83 | return { [torrent.uploader_id]: username }; |
| 84 | } |
| 85 | } catch (error) { |
| 86 | console.error(`Failed to fetch username for uploader_id ${torrent.uploader_id}:`, error); |
| 87 | } |
| 88 | } |
| 89 | return {}; |
| 90 | }); |
| 91 | |
| 92 | const results = await Promise.all(usernamePromises); |
| 93 | const mergedUsernames = results.reduce((acc, curr) => ({ ...acc, ...curr }), {}); |
| 94 | setUsernames((prev) => ({ ...prev, ...mergedUsernames })); |
| 95 | }; |
| 96 | |
| 97 | fetchUsernames(); |
| 98 | }, [torrents]); |
| 99 | |
| 100 | // 处理删除种子 |
| 101 | const handleDeleteTorrent = async (torrentId) => { |
| 102 | if (!currentUserId) { |
| 103 | message.warning('请先登录'); |
| 104 | return; |
| 105 | } |
| 106 | |
| 107 | confirm({ |
| 108 | title: '确认删除', |
| 109 | icon: <ExclamationCircleOutlined />, |
| 110 | content: '确定要删除这个种子吗?此操作不可恢复!', |
| 111 | onOk: async () => { |
| 112 | try { |
| 113 | await axios.delete(`http://localhost:8080/torrent/delete/${torrentId}`, { |
| 114 | params: { userid: currentUserId } |
| 115 | }); |
| 116 | setTorrents(torrents.filter(torrent => torrent.torrentid !== torrentId)); |
| 117 | message.success('种子删除成功'); |
| 118 | } catch (err) { |
| 119 | console.error('删除种子失败', err); |
| 120 | if (err.response && err.response.status === 403) { |
| 121 | message.error('无权删除此种子'); |
| 122 | } else { |
| 123 | message.error('删除种子失败'); |
| 124 | } |
| 125 | } |
| 126 | } |
| 127 | }); |
| 128 | }; |
| 129 | |
| 130 | // 搜索种子 |
| 131 | const handleSearch = async () => { |
| 132 | if (!searchKeyword.trim()) { |
| 133 | fetchAllTorrents(); |
| 134 | return; |
| 135 | } |
| 136 | |
| 137 | setIsLoading(true); |
| 138 | setError(null); |
| 139 | try { |
| 140 | const res = await axios.get(`http://localhost:8080/torrent/search`, { |
| 141 | params: { keyword: searchKeyword }, |
| 142 | }); |
| 143 | setTorrents(res.data); |
| 144 | setCurrentPage(1); // 搜索后重置为第一页 |
| 145 | } catch (err) { |
| 146 | console.error('搜索失败', err); |
| 147 | setError('搜索失败,请稍后重试'); |
| 148 | message.error('搜索失败'); |
| 149 | } finally { |
| 150 | setIsLoading(false); |
| 151 | } |
| 152 | }; |
| 153 | |
| 154 | // 处理修改促销方式 |
| 155 | const handlePromotionChange = (torrentId, newPromotion) => { |
| 156 | setSelectedTorrentId(torrentId); |
| 157 | setSelectedPromotion(newPromotion); |
| 158 | setShowPromotionWarning(true); |
| 159 | }; |
| 160 | |
| 161 | // 确认修改促销方式 |
| 162 | const confirmPromotionChange = async () => { |
| 163 | if (selectedTorrentId && selectedPromotion !== null) { |
| 164 | try { |
| 165 | await axios.post('http://localhost:8080/torrent/setPromotion', null, { |
| 166 | params: { |
| 167 | userid: currentUserId, |
| 168 | torrentId: selectedTorrentId, |
| 169 | promotionId: selectedPromotion |
| 170 | } |
| 171 | }); |
| 172 | setTorrents(torrents.map(torrent => |
| 173 | torrent.torrentid === selectedTorrentId |
| 174 | ? { ...torrent, promotionid: selectedPromotion } |
| 175 | : torrent |
| 176 | )); |
| 177 | setShowPromotionWarning(false); |
| 178 | message.success('促销方式修改成功'); |
| 179 | } catch (err) { |
| 180 | console.error('修改促销方式失败', err); |
| 181 | if (err.response && err.response.status === 403) { |
| 182 | message.error('无权修改此种子的促销方式'); |
| 183 | } else { |
| 184 | message.error('修改促销方式失败'); |
| 185 | } |
| 186 | } |
| 187 | } |
| 188 | }; |
| 189 | |
| 190 | // 取消修改促销方式 |
| 191 | const cancelPromotionChange = () => { |
| 192 | setShowPromotionWarning(false); |
| 193 | setSelectedTorrentId(null); |
| 194 | setSelectedPromotion(null); |
| 195 | }; |
| 196 | |
| 197 | // 触发检查(应用促销规则) |
| 198 | const handleApplyPromotions = async () => { |
| 199 | if (!currentUserId) { |
| 200 | message.warning('请先登录'); |
| 201 | return; |
| 202 | } |
| 203 | |
| 204 | setApplyPromotionsLoading(true); |
| 205 | try { |
| 206 | const res = await axios.post('http://localhost:8080/torrent/applyPromotions', null, { |
| 207 | params: { userid: currentUserId } |
| 208 | }); |
| 209 | |
| 210 | if (res.data.success) { |
| 211 | message.success(res.data.message); |
| 212 | fetchAllTorrents(); // 刷新种子列表 |
| 213 | } |
| 214 | } catch (err) { |
| 215 | console.error('应用促销规则失败', err); |
| 216 | if (err.response && err.response.status === 403) { |
| 217 | message.error('无权执行此操作'); |
| 218 | } else { |
| 219 | message.error('应用促销规则失败'); |
| 220 | } |
| 221 | } finally { |
| 222 | setApplyPromotionsLoading(false); |
| 223 | } |
| 224 | }; |
| 225 | |
| 226 | // 分页数据计算 |
| 227 | const getCurrentPageData = () => { |
| 228 | const start = (currentPage - 1) * pageSize; |
| 229 | const end = start + pageSize; |
| 230 | return torrents.slice(start, end); |
| 231 | }; |
| 232 | |
| 233 | // 页码变化处理 |
| 234 | const handlePageChange = (page) => { |
| 235 | setCurrentPage(page); |
| 236 | }; |
| 237 | |
| 238 | // 每页条数变化处理 |
| 239 | const handlePageSizeChange = (current, size) => { |
| 240 | setPageSize(size); |
| 241 | setCurrentPage(1); // 重置为第一页 |
| 242 | }; |
| 243 | |
| 244 | // 格式化日期 |
| 245 | const formatDate = (dateString) => { |
| 246 | const options = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }; |
| 247 | return new Date(dateString).toLocaleString('zh-CN', options); |
| 248 | }; |
| 249 | |
| 250 | // 格式化文件大小 |
| 251 | const formatFileSize = (bytes) => { |
| 252 | if (bytes === 0) return '0 Bytes'; |
| 253 | const k = 1024; |
| 254 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; |
| 255 | const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| 256 | return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
| 257 | }; |
| 258 | |
| 259 | // 获取促销方式名称 |
| 260 | const getPromotionName = (promotionId) => { |
| 261 | if (promotionId === null) return '无促销'; |
| 262 | const option = promotionOptions.find(opt => opt.value === promotionId); |
| 263 | return option ? option.label : '未知促销'; |
| 264 | }; |
| 265 | |
| 266 | const handleViewDetails = (torrentId) => { |
| 267 | navigate(`/admin/${torrentId}`); // 使用已定义的 navigate 变量 |
| 268 | }; |
| 269 | |
| 270 | return ( |
| 271 | <div className="p-4 max-w-7xl mx-auto"> |
| 272 | <h1 className="text-2xl font-bold mb-6">种子管理</h1> |
| 273 | |
| 274 | {/* 搜索框 */} |
| 275 | <div className="mb-4 flex items-center"> |
| 276 | <Input |
| 277 | placeholder="搜索种子..." |
| 278 | value={searchKeyword} |
| 279 | onChange={(e) => setSearchKeyword(e.target.value)} |
| 280 | style={{ width: 300 }} |
| 281 | onPressEnter={handleSearch} |
| 282 | /> |
| 283 | <Button |
| 284 | type="primary" |
| 285 | onClick={handleSearch} |
| 286 | style={{ marginLeft: 8 }} |
| 287 | > |
| 288 | 搜索 |
| 289 | </Button> |
| 290 | </div> |
| 291 | |
| 292 | {/* 右上角按钮 */} |
| 293 | <Button |
| 294 | type="primary" |
| 295 | loading={applyPromotionsLoading} |
| 296 | onClick={handleApplyPromotions} |
| 297 | style={{ marginBottom: 16 }} |
| 298 | > |
| 299 | 触发检查 |
| 300 | </Button> |
| 301 | |
| 302 | {/* 加载状态 */} |
| 303 | {isLoading && <Spin size="large" style={{ display: 'block', margin: '100px auto' }} />} |
| 304 | |
| 305 | {/* 错误提示 */} |
| 306 | {error && <div className="mb-4 p-3 bg-red-100 text-red-700 rounded border border-red-200">{error}</div>} |
| 307 | |
| 308 | {/* 种子列表表格 */} |
| 309 | {!isLoading && !error && ( |
| 310 | <> |
| 311 | <Table |
| 312 | columns={[ |
| 313 | { |
| 314 | title: '封面', |
| 315 | dataIndex: 'coverImagePath', |
| 316 | key: 'coverImagePath', |
| 317 | render: (text) => text ? ( |
| 318 | <Image |
| 319 | src={text} |
| 320 | width={50} |
| 321 | height={50} |
| 322 | preview={{ maskClosable: true }} |
| 323 | /> |
| 324 | ) : ( |
| 325 | <div className="w-16 h-16 bg-gray-200 flex items-center justify-center">无封面</div> |
| 326 | ) |
| 327 | }, |
| 328 | { |
| 329 | title: '名称', |
| 330 | dataIndex: 'filename', |
| 331 | key: 'filename' |
| 332 | }, |
| 333 | { |
| 334 | title: '描述', |
| 335 | dataIndex: 'description', |
| 336 | key: 'description' |
| 337 | }, |
| 338 | { |
| 339 | title: '大小', |
| 340 | dataIndex: 'torrentSize', |
| 341 | key: 'torrentSize', |
| 342 | render: (size) => formatFileSize(size) |
| 343 | }, |
| 344 | { |
| 345 | title: '上传者', |
| 346 | dataIndex: 'uploader_id', |
| 347 | key: 'uploader_id', |
| 348 | render: (id) => usernames[id] || id |
| 349 | }, |
| 350 | { |
| 351 | title: '上传时间', |
| 352 | dataIndex: 'uploadTime', |
| 353 | key: 'uploadTime', |
| 354 | render: (time) => formatDate(time) |
| 355 | }, |
| 356 | { |
| 357 | title: '下载次数', |
| 358 | dataIndex: 'downloadCount', |
| 359 | key: 'downloadCount' |
| 360 | }, |
| 361 | { |
| 362 | title: '促销', |
| 363 | dataIndex: 'promotionid', |
| 364 | key: 'promotionid', |
| 365 | render: (id) => getPromotionName(id) |
| 366 | }, |
| 367 | { |
| 368 | title: '操作', |
| 369 | key: 'action', |
| 370 | render: (_, record) => ( |
| 371 | <Space> |
| 372 | <Button |
| 373 | danger |
| 374 | onClick={() => handleDeleteTorrent(record.torrentid)} |
| 375 | loading={isLoading} |
| 376 | > |
| 377 | 删除 |
| 378 | </Button> |
| 379 | <Select |
| 380 | value={record.promotionid} |
| 381 | onChange={(value) => handlePromotionChange(record.torrentid, value)} |
| 382 | style={{ width: 120 }} |
| 383 | disabled={isLoading} |
| 384 | > |
| 385 | {promotionOptions.map(option => ( |
| 386 | <Option key={option.value} value={option.value}>{option.label}</Option> |
| 387 | ))} |
| 388 | </Select> |
| 389 | <Button |
| 390 | type="primary" |
| 391 | size="small" |
| 392 | onClick={() => handleViewDetails(record.torrentid)} // 使用处理函数 |
| 393 | > |
| 394 | 查看详情 |
| 395 | </Button> |
| 396 | </Space> |
| 397 | ) |
| 398 | } |
| 399 | ]} |
| 400 | dataSource={getCurrentPageData()} |
| 401 | rowKey="torrentid" |
| 402 | pagination={false} |
| 403 | loading={isLoading} |
| 404 | /> |
| 405 | |
| 406 | {/* 分页控件 */} |
| 407 | {torrents.length > 0 && ( |
| 408 | <div style={{ marginTop: 16, textAlign: 'center' }}> |
| 409 | <Pagination |
| 410 | current={currentPage} |
| 411 | pageSize={pageSize} |
| 412 | total={torrents.length} |
| 413 | onChange={handlePageChange} |
| 414 | onShowSizeChange={handlePageSizeChange} |
| 415 | showSizeChanger |
| 416 | showTotal={(total) => `共 ${total} 条记录`} |
| 417 | pageSizeOptions={['10', '20', '50']} |
| 418 | /> |
| 419 | </div> |
| 420 | )} |
| 421 | </> |
| 422 | )} |
| 423 | |
| 424 | {/* 促销方式修改确认弹窗 */} |
| 425 | <Modal |
| 426 | title="确认修改促销方式" |
| 427 | open={showPromotionWarning} |
| 428 | onOk={confirmPromotionChange} |
| 429 | onCancel={cancelPromotionChange} |
| 430 | okText="确认" |
| 431 | cancelText="取消" |
| 432 | > |
| 433 | <p> |
| 434 | 您确定要将种子 ID 为 |
| 435 | <span className="font-bold">{selectedTorrentId}</span> 的促销方式修改为 |
| 436 | <span className="font-bold">「{getPromotionName(selectedPromotion)}」</span> 吗? |
| 437 | </p> |
| 438 | </Modal> |
| 439 | </div> |
| 440 | ); |
| 441 | }; |
| 442 | |
| 443 | export default TorrentManagement; |