blob: 434d65bfa8829e8f06885880d527f4b5dd7fdec0 [file] [log] [blame]
Jiarenxiang36728482025-06-07 21:51:26 +08001import React, { useEffect, useState } from 'react';
2import { PlusOutlined, UploadOutlined } from '@ant-design/icons';
3import {
4Form,
5Input,
6Button,
7Select,
8Upload,
9message,
10Tag,
11Space,
12Typography,
13Modal,
14} from 'antd';
15import{
16 getCategories,
17 addTorrent,
Jiarenxiang24d681b2025-06-08 19:27:05 +080018 uploadTorrentFile,
19 deleteTorrent,
20 getTracker,
Jiarenxiang36728482025-06-07 21:51:26 +080021} from '../../services/bt/index';
22
23const { Option } = Select;
24const { TextArea } = Input;
25const { Title } = Typography;
26
27const TorrentUpload: React.FC = () => {
28const [categories, setCategories] = useState<{ id: number; name: string }[]>([]);
29const [tagOptions, setTagOptions] = useState<string[]>([]);
30const [customTags, setCustomTags] = useState<string[]>([]);
31const [fileList, setFileList] = useState<any[]>([]);
32const [uploading, setUploading] = useState(false);
33const [form] = Form.useForm();
34const [tagInputVisible, setTagInputVisible] = useState(false);
35const [tagInputValue, setTagInputValue] = useState('');
36
37useEffect(() => {
38 getCategories().then((res) => {
39 if (Array.isArray(res.data)) {
40 setCategories(res.data);
41 setTagOptions(res.data.map((cat: { name: string }) => cat.name));
42 }
43 });
44}, []);
45
46const handleTagClose = (removedTag: string) => {
47 setCustomTags(customTags.filter(tag => tag !== removedTag));
48};
49
50const showTagInput = () => setTagInputVisible(true);
51
52const handleTagInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
53 setTagInputValue(e.target.value);
54};
55
56const handleTagInputConfirm = () => {
57 if (
58 tagInputValue &&
59 !customTags.includes(tagInputValue) &&
60 !tagOptions.includes(tagInputValue)
61 ) {
62 setCustomTags([...customTags, tagInputValue]);
63 }
64 setTagInputVisible(false);
65 setTagInputValue('');
66};
67
68const beforeUpload = (file: File) => {
69 const isTorrent = file.name.endsWith('.torrent');
70 if (!isTorrent) {
71 message.error('只能上传.torrent文件');
72 }
73 setFileList([file]);
74 return false;
75};
76
77const handleSubmit = async (values: any) => {
78 if (fileList.length === 0) {
79 message.error('请上传.torrent文件');
80 return;
81 }
82 setUploading(true);
83 try {
84 // 1. 添加种子基本信息
85 const addRes = await addTorrent({
86 ...values,
87 category: Number(values.category),
88 description: values.description,
89 name: values.name,
90 title: values.title,
91 subheading: values.subheading || '',
92 remark: values.remark || '',
93 });
Jiarenxiang24d681b2025-06-08 19:27:05 +080094 if (!addRes?.data) {
Jiarenxiang36728482025-06-07 21:51:26 +080095 throw new Error('种子信息添加失败');
96 }
97 // 2. 上传.torrent文件
Jiarenxiang24d681b2025-06-08 19:27:05 +080098 let upRes = await uploadTorrentFile(fileList[0], addRes.data);
99 console.log('上传结果:', upRes);
100 if (upRes.code == 500) {
101 throw new Error(upRes.msg);
102 await deleteTorrent( addRes.data);
103 }else{
Jiarenxiang36728482025-06-07 21:51:26 +0800104 message.success('种子上传成功');
105 form.resetFields();
106 setFileList([]);
107 setCustomTags([]);
Jiarenxiang24d681b2025-06-08 19:27:05 +0800108 // 获取并显示 trackerUrl
109 const trackerRes = await getTracker();
110 if (trackerRes.data) {
111 const trackerUrl = trackerRes.data;
112 Modal.info({
113 title: 'Tracker 服务器',
114 content: (
115 <div style={{ marginTop: 12, color: '#232946', wordBreak: 'break-all' }}>
116 {trackerUrl}
117 <br />
118 <span style={{ color: '#667eea', fontWeight: 500 }}>
119 &nbsp;torrentId={addRes.data}
120 </span>
121 </div>
122 ),
123 width: 480,
124 okText: '关闭',
125 });
126 }
127 }
128
Jiarenxiang36728482025-06-07 21:51:26 +0800129 } catch (err: any) {
130 message.error(err.message || '上传失败');
131 } finally {
132 setUploading(false);
133 }
134};
135
136return (
137 <div
138 style={{
Jiarenxiang96976d82025-06-07 22:43:32 +0800139 minHeight: '100vh',
140 background: 'radial-gradient(ellipse at 50% 30%, #232946 60%, #0f1021 100%)',
Jiarenxiang36728482025-06-07 21:51:26 +0800141 position: 'relative',
142 overflow: 'hidden',
Jiarenxiang96976d82025-06-07 22:43:32 +0800143 padding: '0 0 80px 0',
Jiarenxiang36728482025-06-07 21:51:26 +0800144 }}
145 >
Jiarenxiang96976d82025-06-07 22:43:32 +0800146 {/* 星空背景装饰 */}
147 <svg
Jiarenxiang36728482025-06-07 21:51:26 +0800148 style={{
149 position: 'absolute',
Jiarenxiang96976d82025-06-07 22:43:32 +0800150 top: 0,
151 left: 0,
152 width: '100vw',
153 height: '100vh',
Jiarenxiang36728482025-06-07 21:51:26 +0800154 zIndex: 0,
Jiarenxiang96976d82025-06-07 22:43:32 +0800155 pointerEvents: 'none',
Jiarenxiang36728482025-06-07 21:51:26 +0800156 }}
Jiarenxiang96976d82025-06-07 22:43:32 +0800157 >
158 {/* 随机星星 */}
159 {Array.from({ length: 120 }).map((_, i) => (
160 <circle
161 key={i}
162 cx={Math.random() * window.innerWidth}
163 cy={Math.random() * window.innerHeight}
164 r={Math.random() * 1.2 + 0.2}
165 fill="#fff"
166 opacity={Math.random() * 0.7 + 0.3}
167 />
168 ))}
169 {/* 银河带 */}
170 <ellipse
171 cx={window.innerWidth / 2}
172 cy={window.innerHeight / 2.5}
173 rx={window.innerWidth / 2.2}
174 ry={80}
175 fill="url(#milkyway)"
176 opacity="0.18"
177 />
178 <defs>
179 <radialGradient id="milkyway" cx="50%" cy="50%" r="100%">
180 <stop offset="0%" stopColor="#fff" stopOpacity="0.8" />
181 <stop offset="100%" stopColor="#232946" stopOpacity="0" />
182 </radialGradient>
183 </defs>
184 </svg>
185
Jiarenxiang36728482025-06-07 21:51:26 +0800186 <div
187 style={{
Jiarenxiang96976d82025-06-07 22:43:32 +0800188 maxWidth: 600,
189 margin: '0 auto',
190 marginTop: 80,
191 background: 'rgba(30,34,60,0.92)',
192 borderRadius: 24,
193 boxShadow: '0 8px 32px 0 rgba(31,38,135,0.25)',
194 padding: '48px 36px 32px 36px',
Jiarenxiang36728482025-06-07 21:51:26 +0800195 position: 'relative',
Jiarenxiang96976d82025-06-07 22:43:32 +0800196 zIndex: 1,
197 border: '1.5px solid #3a3f5c',
198 backdropFilter: 'blur(2px)',
Jiarenxiang36728482025-06-07 21:51:26 +0800199 }}
200 >
Jiarenxiang96976d82025-06-07 22:43:32 +0800201 <div style={{ textAlign: 'center', marginBottom: 24 }}>
202 <svg width="64" height="64" viewBox="0 0 64 64">
203 <defs>
204 <radialGradient id="star" cx="50%" cy="50%" r="50%">
205 <stop offset="0%" stopColor="#fffbe6" />
206 <stop offset="100%" stopColor="#667eea" />
207 </radialGradient>
208 </defs>
209 <circle cx="32" cy="32" r="28" fill="url(#star)" opacity="0.7" />
210 <polygon
211 points="32,12 36,28 52,28 38,36 42,52 32,42 22,52 26,36 12,28 28,28"
212 fill="#fff"
213 opacity="0.9"
214 />
215 </svg>
216 <Title
217 level={2}
Jiarenxiang36728482025-06-07 21:51:26 +0800218 style={{
Jiarenxiang36728482025-06-07 21:51:26 +0800219 color: '#fff',
Jiarenxiang96976d82025-06-07 22:43:32 +0800220 margin: '12px 0 0 0',
221 letterSpacing: 2,
222 fontWeight: 700,
223 fontSize: 30,
224 textShadow: '0 2px 12px #667eea55',
Jiarenxiang36728482025-06-07 21:51:26 +0800225 }}
226 >
Jiarenxiang96976d82025-06-07 22:43:32 +0800227 星空PT · 种子上传
228 </Title>
229 <div style={{ color: '#bfcfff', fontSize: 16, marginTop: 4, letterSpacing: 1 }}>
230 分享你的资源,点亮星空
231 </div>
232 </div>
233 <Form
234 form={form}
235 layout="vertical"
236 onFinish={handleSubmit}
237 initialValues={{ anonymous: 0 }}
238 style={{ zIndex: 1, position: 'relative' }}
239 >
240 <Form.Item
241 label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>主标题</span>}
242 name="title"
243 rules={[{ required: true, message: '请输入主标题' }]}
244 >
245 <Input
246 placeholder="请输入主标题"
247 size="large"
248 style={{
249 background: 'rgba(255,255,255,0.06)',
250 border: '1px solid #3a3f5c',
251 color: '#fff',
252 }}
253 />
254 </Form.Item>
255 <Form.Item
256 label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>副标题</span>}
257 name="subheading"
258 >
259 <Input
260 placeholder="可选,副标题"
261 size="large"
262 style={{
263 background: 'rgba(255,255,255,0.06)',
264 border: '1px solid #3a3f5c',
265 color: '#fff',
266 }}
267 />
268 </Form.Item>
269 <Form.Item
270 label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>种子名称</span>}
271 name="name"
272 rules={[{ required: true, message: '请输入种子名称' }]}
273 >
274 <Input
275 placeholder="请输入种子名称"
276 size="large"
277 style={{
278 background: 'rgba(255,255,255,0.06)',
279 border: '1px solid #3a3f5c',
280 color: '#fff',
281 }}
282 />
283 </Form.Item>
284 <Form.Item
285 label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>分类</span>}
286 name="category"
287 rules={[{ required: true, message: '请选择分类' }]}
288 >
289 <Select
290 placeholder="请选择分类"
291 size="large"
292 dropdownStyle={{ background: '#232946', color: '#fff' }}
293 style={{
294 background: 'rgba(255,255,255,0.06)',
295 border: '1px solid #3a3f5c',
296 color: '#fff',
297 }}
298 >
299 {categories.map((cat) => (
Jiarenxiang24d681b2025-06-08 19:27:05 +0800300 <Option
301 key={cat.id}
302 value={cat.id}
303 style={{ color: '#fff', background: '#232946' }}
304 >
Jiarenxiang96976d82025-06-07 22:43:32 +0800305 {cat.name}
306 </Option>
307 ))}
308 </Select>
309 </Form.Item>
310 <Form.Item label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>标签</span>}>
311 <Space wrap>
312 {customTags.map((tag) => (
313 <Tag
314 key={tag}
315 closable
316 color="geekblue"
317 onClose={() => handleTagClose(tag)}
318 style={{
319 marginBottom: 4,
320 fontSize: 15,
321 padding: '4px 12px',
322 borderRadius: 8,
323 background: 'rgba(102,126,234,0.18)',
324 border: '1px solid #667eea',
325 color: '#fff',
326 }}
327 >
328 {tag}
329 </Tag>
330 ))}
331 {tagInputVisible ? (
332 <Input
333 size="small"
334 style={{
335 width: 120,
336 background: 'rgba(255,255,255,0.08)',
337 color: '#fff',
338 border: '1px solid #667eea',
339 }}
340 value={tagInputValue}
341 onChange={handleTagInputChange}
342 onBlur={handleTagInputConfirm}
343 onPressEnter={handleTagInputConfirm}
344 autoFocus
345 />
346 ) : (
347 <Tag
348 onClick={showTagInput}
349 style={{
350 background: 'rgba(102,126,234,0.10)',
351 border: '1px dashed #667eea',
352 cursor: 'pointer',
353 color: '#bfcfff',
354 fontSize: 15,
355 borderRadius: 8,
356 padding: '4px 12px',
357 }}
358 >
359 <PlusOutlined /> 自定义标签
360 </Tag>
361 )}
362 </Space>
363 </Form.Item>
364 <Form.Item
365 label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>描述</span>}
366 name="description"
367 rules={[{ required: true, message: '请输入描述' }]}
368 >
369 <TextArea
370 rows={6}
371 placeholder="请输入种子描述,支持Markdown"
372 style={{
373 background: 'rgba(255,255,255,0.06)',
374 border: '1px solid #3a3f5c',
375 color: '#fff',
376 fontSize: 15,
377 }}
378 />
379 </Form.Item>
380 <Form.Item
381 label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>备注</span>}
382 name="remark"
383 >
384 <Input
385 placeholder="可选,备注信息"
386 size="large"
387 style={{
388 background: 'rgba(255,255,255,0.06)',
389 border: '1px solid #3a3f5c',
390 color: '#fff',
391 }}
392 />
393 </Form.Item>
394 <Form.Item
395 label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>匿名上传</span>}
396 name="anonymous"
397 valuePropName="checked"
398 >
399 <Select
400 size="large"
401 style={{
402 background: 'rgba(255,255,255,0.06)',
403 border: '1px solid #3a3f5c',
404 color: '#fff',
405 }}
406 >
407 <Option value={0}>否</Option>
408 <Option value={1}>是</Option>
409 </Select>
410 </Form.Item>
411 <Form.Item
412 label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>上传.torrent文件</span>}
413 required
414 >
415 <Upload
416 beforeUpload={beforeUpload}
417 fileList={fileList}
418 onRemove={() => setFileList([])}
419 accept=".torrent"
420 maxCount={1}
421 showUploadList={{ showRemoveIcon: true }}
422 customRequest={() => {}}
423 >
424 <Button
425 icon={<UploadOutlined />}
426 size="large"
Jiarenxiang36728482025-06-07 21:51:26 +0800427 style={{
Jiarenxiang96976d82025-06-07 22:43:32 +0800428 background: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
429 border: 'none',
Jiarenxiang36728482025-06-07 21:51:26 +0800430 color: '#fff',
Jiarenxiang96976d82025-06-07 22:43:32 +0800431 fontWeight: 500,
432 boxShadow: '0 2px 8px #667eea33',
Jiarenxiang36728482025-06-07 21:51:26 +0800433 }}
434 >
Jiarenxiang96976d82025-06-07 22:43:32 +0800435 选择.torrent文件
436 </Button>
437 </Upload>
438 </Form.Item>
439 <Form.Item>
Jiarenxiang36728482025-06-07 21:51:26 +0800440 <Button
Jiarenxiang96976d82025-06-07 22:43:32 +0800441 type="primary"
442 htmlType="submit"
443 loading={uploading}
444 block
Jiarenxiang36728482025-06-07 21:51:26 +0800445 size="large"
446 style={{
447 background: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
448 border: 'none',
Jiarenxiang96976d82025-06-07 22:43:32 +0800449 fontWeight: 600,
450 letterSpacing: 2,
451 fontSize: 18,
452 marginTop: 8,
453 boxShadow: '0 4px 16px 0 rgba(118,75,162,0.15)',
Jiarenxiang36728482025-06-07 21:51:26 +0800454 }}
455 >
Jiarenxiang96976d82025-06-07 22:43:32 +0800456 上传
Jiarenxiang36728482025-06-07 21:51:26 +0800457 </Button>
Jiarenxiang24d681b2025-06-08 19:27:05 +0800458 <Button
459 type="default"
460 block
461 style={{
462 marginTop: 16,
463 background: 'rgba(102,126,234,0.10)',
464 border: '1px solid #667eea',
465 color: '#bfcfff',
466 fontWeight: 500,
467 letterSpacing: 1,
468 }}
469 onClick={async () => {
470 try {
471 const res = await getTracker();
472 if (res.data) {
473 Modal.info({
474 title: 'Tracker 服务器',
475 content: (
476 <div style={{ marginTop: 12, color: '#232946' }}>
477 {res.data}
478 </div>
479 ),
480 width: 480,
481 okText: '关闭',
482 });
483 } else {
484 message.info('暂无可用的 Tracker 服务器');
485 }
486 } catch (err) {
487 message.error('获取 Tracker 服务器失败');
488 }
489 }}
490 >
491 查看 Tracker 服务器
492 </Button>
Jiarenxiang96976d82025-06-07 22:43:32 +0800493 </Form.Item>
494 </Form>
495 </div>
496 <div
497 style={{
498 position: 'fixed',
499 bottom: 16,
500 width: '100%',
501 textAlign: 'center',
502 color: '#bfcfff99',
503 fontSize: 14,
504 letterSpacing: 1,
505 zIndex: 2,
506 pointerEvents: 'none',
507 textShadow: '0 2px 8px #232946',
508 }}
509 >
510 星空PT · 让每一份分享都如星辰般闪耀
511 </div>
Jiarenxiang36728482025-06-07 21:51:26 +0800512 </div>
513);
514};
515
516export default TorrentUpload;