blob: b5d45bcf2d4dcda7c03bc09e89274f669850af3c [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,
18 uploadTorrentFile
19} from '../../services/bt/index';
20
21const { Option } = Select;
22const { TextArea } = Input;
23const { Title } = Typography;
24
25const TorrentUpload: React.FC = () => {
26const [categories, setCategories] = useState<{ id: number; name: string }[]>([]);
27const [tagOptions, setTagOptions] = useState<string[]>([]);
28const [customTags, setCustomTags] = useState<string[]>([]);
29const [fileList, setFileList] = useState<any[]>([]);
30const [uploading, setUploading] = useState(false);
31const [form] = Form.useForm();
32const [tagInputVisible, setTagInputVisible] = useState(false);
33const [tagInputValue, setTagInputValue] = useState('');
34
35useEffect(() => {
36 getCategories().then((res) => {
37 if (Array.isArray(res.data)) {
38 setCategories(res.data);
39 setTagOptions(res.data.map((cat: { name: string }) => cat.name));
40 }
41 });
42}, []);
43
44const handleTagClose = (removedTag: string) => {
45 setCustomTags(customTags.filter(tag => tag !== removedTag));
46};
47
48const showTagInput = () => setTagInputVisible(true);
49
50const handleTagInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
51 setTagInputValue(e.target.value);
52};
53
54const handleTagInputConfirm = () => {
55 if (
56 tagInputValue &&
57 !customTags.includes(tagInputValue) &&
58 !tagOptions.includes(tagInputValue)
59 ) {
60 setCustomTags([...customTags, tagInputValue]);
61 }
62 setTagInputVisible(false);
63 setTagInputValue('');
64};
65
66const beforeUpload = (file: File) => {
67 const isTorrent = file.name.endsWith('.torrent');
68 if (!isTorrent) {
69 message.error('只能上传.torrent文件');
70 }
71 setFileList([file]);
72 return false;
73};
74
75const handleSubmit = async (values: any) => {
76 if (fileList.length === 0) {
77 message.error('请上传.torrent文件');
78 return;
79 }
80 setUploading(true);
81 try {
82 // 1. 添加种子基本信息
83 const addRes = await addTorrent({
84 ...values,
85 category: Number(values.category),
86 description: values.description,
87 name: values.name,
88 title: values.title,
89 subheading: values.subheading || '',
90 remark: values.remark || '',
91 });
92 if (!addRes?.id) {
93 throw new Error('种子信息添加失败');
94 }
95 // 2. 上传.torrent文件
96 await uploadTorrentFile(fileList[0], addRes.id);
97
98 message.success('种子上传成功');
99 form.resetFields();
100 setFileList([]);
101 setCustomTags([]);
102 } catch (err: any) {
103 message.error(err.message || '上传失败');
104 } finally {
105 setUploading(false);
106 }
107};
108
109return (
110 <div
111 style={{
Jiarenxiang96976d82025-06-07 22:43:32 +0800112 minHeight: '100vh',
113 background: 'radial-gradient(ellipse at 50% 30%, #232946 60%, #0f1021 100%)',
Jiarenxiang36728482025-06-07 21:51:26 +0800114 position: 'relative',
115 overflow: 'hidden',
Jiarenxiang96976d82025-06-07 22:43:32 +0800116 padding: '0 0 80px 0',
Jiarenxiang36728482025-06-07 21:51:26 +0800117 }}
118 >
Jiarenxiang96976d82025-06-07 22:43:32 +0800119 {/* 星空背景装饰 */}
120 <svg
Jiarenxiang36728482025-06-07 21:51:26 +0800121 style={{
122 position: 'absolute',
Jiarenxiang96976d82025-06-07 22:43:32 +0800123 top: 0,
124 left: 0,
125 width: '100vw',
126 height: '100vh',
Jiarenxiang36728482025-06-07 21:51:26 +0800127 zIndex: 0,
Jiarenxiang96976d82025-06-07 22:43:32 +0800128 pointerEvents: 'none',
Jiarenxiang36728482025-06-07 21:51:26 +0800129 }}
Jiarenxiang96976d82025-06-07 22:43:32 +0800130 >
131 {/* 随机星星 */}
132 {Array.from({ length: 120 }).map((_, i) => (
133 <circle
134 key={i}
135 cx={Math.random() * window.innerWidth}
136 cy={Math.random() * window.innerHeight}
137 r={Math.random() * 1.2 + 0.2}
138 fill="#fff"
139 opacity={Math.random() * 0.7 + 0.3}
140 />
141 ))}
142 {/* 银河带 */}
143 <ellipse
144 cx={window.innerWidth / 2}
145 cy={window.innerHeight / 2.5}
146 rx={window.innerWidth / 2.2}
147 ry={80}
148 fill="url(#milkyway)"
149 opacity="0.18"
150 />
151 <defs>
152 <radialGradient id="milkyway" cx="50%" cy="50%" r="100%">
153 <stop offset="0%" stopColor="#fff" stopOpacity="0.8" />
154 <stop offset="100%" stopColor="#232946" stopOpacity="0" />
155 </radialGradient>
156 </defs>
157 </svg>
158
Jiarenxiang36728482025-06-07 21:51:26 +0800159 <div
160 style={{
Jiarenxiang96976d82025-06-07 22:43:32 +0800161 maxWidth: 600,
162 margin: '0 auto',
163 marginTop: 80,
164 background: 'rgba(30,34,60,0.92)',
165 borderRadius: 24,
166 boxShadow: '0 8px 32px 0 rgba(31,38,135,0.25)',
167 padding: '48px 36px 32px 36px',
Jiarenxiang36728482025-06-07 21:51:26 +0800168 position: 'relative',
Jiarenxiang96976d82025-06-07 22:43:32 +0800169 zIndex: 1,
170 border: '1.5px solid #3a3f5c',
171 backdropFilter: 'blur(2px)',
Jiarenxiang36728482025-06-07 21:51:26 +0800172 }}
173 >
Jiarenxiang96976d82025-06-07 22:43:32 +0800174 <div style={{ textAlign: 'center', marginBottom: 24 }}>
175 <svg width="64" height="64" viewBox="0 0 64 64">
176 <defs>
177 <radialGradient id="star" cx="50%" cy="50%" r="50%">
178 <stop offset="0%" stopColor="#fffbe6" />
179 <stop offset="100%" stopColor="#667eea" />
180 </radialGradient>
181 </defs>
182 <circle cx="32" cy="32" r="28" fill="url(#star)" opacity="0.7" />
183 <polygon
184 points="32,12 36,28 52,28 38,36 42,52 32,42 22,52 26,36 12,28 28,28"
185 fill="#fff"
186 opacity="0.9"
187 />
188 </svg>
189 <Title
190 level={2}
Jiarenxiang36728482025-06-07 21:51:26 +0800191 style={{
Jiarenxiang36728482025-06-07 21:51:26 +0800192 color: '#fff',
Jiarenxiang96976d82025-06-07 22:43:32 +0800193 margin: '12px 0 0 0',
194 letterSpacing: 2,
195 fontWeight: 700,
196 fontSize: 30,
197 textShadow: '0 2px 12px #667eea55',
Jiarenxiang36728482025-06-07 21:51:26 +0800198 }}
199 >
Jiarenxiang96976d82025-06-07 22:43:32 +0800200 星空PT · 种子上传
201 </Title>
202 <div style={{ color: '#bfcfff', fontSize: 16, marginTop: 4, letterSpacing: 1 }}>
203 分享你的资源,点亮星空
204 </div>
205 </div>
206 <Form
207 form={form}
208 layout="vertical"
209 onFinish={handleSubmit}
210 initialValues={{ anonymous: 0 }}
211 style={{ zIndex: 1, position: 'relative' }}
212 >
213 <Form.Item
214 label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>主标题</span>}
215 name="title"
216 rules={[{ required: true, message: '请输入主标题' }]}
217 >
218 <Input
219 placeholder="请输入主标题"
220 size="large"
221 style={{
222 background: 'rgba(255,255,255,0.06)',
223 border: '1px solid #3a3f5c',
224 color: '#fff',
225 }}
226 />
227 </Form.Item>
228 <Form.Item
229 label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>副标题</span>}
230 name="subheading"
231 >
232 <Input
233 placeholder="可选,副标题"
234 size="large"
235 style={{
236 background: 'rgba(255,255,255,0.06)',
237 border: '1px solid #3a3f5c',
238 color: '#fff',
239 }}
240 />
241 </Form.Item>
242 <Form.Item
243 label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>种子名称</span>}
244 name="name"
245 rules={[{ required: true, message: '请输入种子名称' }]}
246 >
247 <Input
248 placeholder="请输入种子名称"
249 size="large"
250 style={{
251 background: 'rgba(255,255,255,0.06)',
252 border: '1px solid #3a3f5c',
253 color: '#fff',
254 }}
255 />
256 </Form.Item>
257 <Form.Item
258 label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>分类</span>}
259 name="category"
260 rules={[{ required: true, message: '请选择分类' }]}
261 >
262 <Select
263 placeholder="请选择分类"
264 size="large"
265 dropdownStyle={{ background: '#232946', color: '#fff' }}
266 style={{
267 background: 'rgba(255,255,255,0.06)',
268 border: '1px solid #3a3f5c',
269 color: '#fff',
270 }}
271 >
272 {categories.map((cat) => (
273 <Option key={cat.id} value={cat.id}>
274 {cat.name}
275 </Option>
276 ))}
277 </Select>
278 </Form.Item>
279 <Form.Item label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>标签</span>}>
280 <Space wrap>
281 {customTags.map((tag) => (
282 <Tag
283 key={tag}
284 closable
285 color="geekblue"
286 onClose={() => handleTagClose(tag)}
287 style={{
288 marginBottom: 4,
289 fontSize: 15,
290 padding: '4px 12px',
291 borderRadius: 8,
292 background: 'rgba(102,126,234,0.18)',
293 border: '1px solid #667eea',
294 color: '#fff',
295 }}
296 >
297 {tag}
298 </Tag>
299 ))}
300 {tagInputVisible ? (
301 <Input
302 size="small"
303 style={{
304 width: 120,
305 background: 'rgba(255,255,255,0.08)',
306 color: '#fff',
307 border: '1px solid #667eea',
308 }}
309 value={tagInputValue}
310 onChange={handleTagInputChange}
311 onBlur={handleTagInputConfirm}
312 onPressEnter={handleTagInputConfirm}
313 autoFocus
314 />
315 ) : (
316 <Tag
317 onClick={showTagInput}
318 style={{
319 background: 'rgba(102,126,234,0.10)',
320 border: '1px dashed #667eea',
321 cursor: 'pointer',
322 color: '#bfcfff',
323 fontSize: 15,
324 borderRadius: 8,
325 padding: '4px 12px',
326 }}
327 >
328 <PlusOutlined /> 自定义标签
329 </Tag>
330 )}
331 </Space>
332 </Form.Item>
333 <Form.Item
334 label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>描述</span>}
335 name="description"
336 rules={[{ required: true, message: '请输入描述' }]}
337 >
338 <TextArea
339 rows={6}
340 placeholder="请输入种子描述,支持Markdown"
341 style={{
342 background: 'rgba(255,255,255,0.06)',
343 border: '1px solid #3a3f5c',
344 color: '#fff',
345 fontSize: 15,
346 }}
347 />
348 </Form.Item>
349 <Form.Item
350 label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>备注</span>}
351 name="remark"
352 >
353 <Input
354 placeholder="可选,备注信息"
355 size="large"
356 style={{
357 background: 'rgba(255,255,255,0.06)',
358 border: '1px solid #3a3f5c',
359 color: '#fff',
360 }}
361 />
362 </Form.Item>
363 <Form.Item
364 label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>匿名上传</span>}
365 name="anonymous"
366 valuePropName="checked"
367 >
368 <Select
369 size="large"
370 style={{
371 background: 'rgba(255,255,255,0.06)',
372 border: '1px solid #3a3f5c',
373 color: '#fff',
374 }}
375 >
376 <Option value={0}>否</Option>
377 <Option value={1}>是</Option>
378 </Select>
379 </Form.Item>
380 <Form.Item
381 label={<span style={{ color: '#bfcfff', fontWeight: 500 }}>上传.torrent文件</span>}
382 required
383 >
384 <Upload
385 beforeUpload={beforeUpload}
386 fileList={fileList}
387 onRemove={() => setFileList([])}
388 accept=".torrent"
389 maxCount={1}
390 showUploadList={{ showRemoveIcon: true }}
391 customRequest={() => {}}
392 >
393 <Button
394 icon={<UploadOutlined />}
395 size="large"
Jiarenxiang36728482025-06-07 21:51:26 +0800396 style={{
Jiarenxiang96976d82025-06-07 22:43:32 +0800397 background: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
398 border: 'none',
Jiarenxiang36728482025-06-07 21:51:26 +0800399 color: '#fff',
Jiarenxiang96976d82025-06-07 22:43:32 +0800400 fontWeight: 500,
401 boxShadow: '0 2px 8px #667eea33',
Jiarenxiang36728482025-06-07 21:51:26 +0800402 }}
403 >
Jiarenxiang96976d82025-06-07 22:43:32 +0800404 选择.torrent文件
405 </Button>
406 </Upload>
407 </Form.Item>
408 <Form.Item>
Jiarenxiang36728482025-06-07 21:51:26 +0800409 <Button
Jiarenxiang96976d82025-06-07 22:43:32 +0800410 type="primary"
411 htmlType="submit"
412 loading={uploading}
413 block
Jiarenxiang36728482025-06-07 21:51:26 +0800414 size="large"
415 style={{
416 background: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
417 border: 'none',
Jiarenxiang96976d82025-06-07 22:43:32 +0800418 fontWeight: 600,
419 letterSpacing: 2,
420 fontSize: 18,
421 marginTop: 8,
422 boxShadow: '0 4px 16px 0 rgba(118,75,162,0.15)',
Jiarenxiang36728482025-06-07 21:51:26 +0800423 }}
424 >
Jiarenxiang96976d82025-06-07 22:43:32 +0800425 上传
Jiarenxiang36728482025-06-07 21:51:26 +0800426 </Button>
Jiarenxiang96976d82025-06-07 22:43:32 +0800427 </Form.Item>
428 </Form>
429 </div>
430 <div
431 style={{
432 position: 'fixed',
433 bottom: 16,
434 width: '100%',
435 textAlign: 'center',
436 color: '#bfcfff99',
437 fontSize: 14,
438 letterSpacing: 1,
439 zIndex: 2,
440 pointerEvents: 'none',
441 textShadow: '0 2px 8px #232946',
442 }}
443 >
444 星空PT · 让每一份分享都如星辰般闪耀
445 </div>
Jiarenxiang36728482025-06-07 21:51:26 +0800446 </div>
447);
448};
449
450export default TorrentUpload;