blob: 12766ee8054ab13af02e460e2b2acbe3015a3f92 [file] [log] [blame]
223010144ce05872025-06-08 22:33:28 +08001import React, { useState, useCallback } from 'react';
2import {
3 Form, Input, Select, Upload, Button, Card, message, Space, Modal,
4 Divider, Row, Col, Typography, Alert, List, Tag,
5 type FormInstance
6} from 'antd';
7import {
8 PlusOutlined, InboxOutlined, DeleteOutlined, EditOutlined,
9 CheckCircleOutlined, SendOutlined, FileTextOutlined
10} from '@ant-design/icons';
11import ReactMarkdown from 'react-markdown';
12import type {
13 UploadFile, UploadProps, UploadChangeParam, RcFile,
14} from 'antd/es/upload';
15import { type StepFormProps, type BasicInfo, ARTWORK_CATEGORIES, type VersionFormData } from './types';
16import { getBase64 } from './utils';
17
18const { TextArea } = Input;
19const { Dragger } = Upload;
20const { Title, Text, Paragraph } = Typography;
21
22// ==================== 描述组件 ====================
23interface DescriptionsProps {
24 bordered?: boolean;
25 column?: number;
26 children: React.ReactNode;
27}
28
29interface DescriptionsItemProps {
30 label: string;
31 children: React.ReactNode;
32}
33
34export const Descriptions: React.FC<DescriptionsProps> & {
35 Item: React.FC<DescriptionsItemProps>;
36} = ({ children }) => {
37 return <div style={{ marginBottom: 16 }}>{children}</div>;
38};
39
40Descriptions.Item = ({ label, children }: DescriptionsItemProps) => {
41 return (
42 <div style={{ marginBottom: 12 }}>
43 <Text strong style={{ marginRight: 16 }}>{label}:</Text>
44 <span>{children}</span>
45 </div>
46 );
47};
48
49// ==================== 步骤1:基础信息 ====================
50export const BasicInfoStep: React.FC<StepFormProps> = ({ data, onUpdate, onNext }) => {
51 const [form]: [FormInstance<BasicInfo>] = Form.useForm<BasicInfo>();
52 const [previewMode, setPreviewMode] = useState<boolean>(false);
53
54 const handleFinish = useCallback((values: BasicInfo) => {
55 onUpdate('basicInfo', values);
56 onNext?.();
57 }, [onUpdate, onNext]);
58
59 const handlePreview = useCallback(() => {
60 form.validateFields().then((values) => {
61 onUpdate('basicInfo', values);
62 setPreviewMode(true);
63 }).catch(() => {
64 message.error('请先填写完整信息');
65 });
66 }, [form, onUpdate]);
67
68 return (
69 <>
70 <Card>
71 <Form form={form} layout="vertical" initialValues={data.basicInfo} onFinish={handleFinish} autoComplete="off">
72 <Form.Item
73 label="作品名称"
74 name="artworkName"
75 rules={[
76 { required: true, message: '请输入作品名称' },
77 { min: 2, message: '作品名称至少2个字符' },
78 { max: 50, message: '作品名称最多50个字符' },
79 ]}
80 >
81 <Input placeholder="请输入作品名称,例如:未来城市概念设计" size="large" showCount maxLength={50} />
82 </Form.Item>
83
84 <Form.Item label="作品分类" name="artworkCategory" rules={[{ required: true, message: '请选择作品分类' }]}>
85 <Select placeholder="请选择作品分类" size="large" options={ARTWORK_CATEGORIES} />
86 </Form.Item>
87
88 <Form.Item
89 label="作品描述"
90 name="artworkDescription"
91 rules={[
92 { required: true, message: '请输入作品描述' },
93 { min: 20, message: '作品描述至少20个字符' },
94 { max: 2000, message: '作品描述最多2000个字符' },
95 ]}
96 extra="支持 Markdown 格式,可以使用 # 标题、**粗体**、*斜体* 等格式"
97 >
98 <TextArea
99 placeholder="请详细描述你的作品,包括创作理念、技术特点、使用说明等"
100 rows={10}
101 showCount
102 maxLength={2000}
103 />
104 </Form.Item>
105
106 <Form.Item>
107 <Space size="middle">
108 <Button type="primary" htmlType="submit" size="large">下一步</Button>
109 <Button onClick={handlePreview} size="large">预览描述</Button>
110 </Space>
111 </Form.Item>
112 </Form>
113 </Card>
114
115 <Modal title="作品描述预览" open={previewMode} onCancel={() => setPreviewMode(false)} footer={null} width={800}>
116 <div style={{ maxHeight: '60vh', overflow: 'auto' }}>
117 <ReactMarkdown>{form.getFieldValue('artworkDescription') || ''}</ReactMarkdown>
118 </div>
119 </Modal>
120 </>
121 );
122};
123
124// ==================== 步骤2:封面上传 ====================
125export const CoverUploadStep: React.FC<StepFormProps> = ({ data, onUpdate, onNext, onPrev }) => {
126 const [fileList, setFileList] = useState<UploadFile[]>(data.coverInfo.coverFile ? [data.coverInfo.coverFile] : []);
127 const [previewImage, setPreviewImage] = useState<string>('');
128 const [previewOpen, setPreviewOpen] = useState<boolean>(false);
129
130 const handleChange: UploadProps['onChange'] = useCallback((info: UploadChangeParam<UploadFile>) => {
131 const { fileList: newFileList } = info;
132 setFileList(newFileList);
133 onUpdate('coverInfo', { coverFile: newFileList[0] });
134 }, [onUpdate]);
135
136 const handlePreview = useCallback(async (file: UploadFile): Promise<void> => {
137 if (!file.url && !file.preview) {
138 file.preview = await getBase64(file.originFileObj as RcFile);
139 }
140 setPreviewImage(file.url || (file.preview as string));
141 setPreviewOpen(true);
142 }, []);
143
144 const beforeUpload = useCallback((file: RcFile): boolean => {
145 const isImage = file.type.startsWith('image/');
146 if (!isImage) {
147 message.error('只能上传图片文件!');
148 return false;
149 }
150 const isLt5M = file.size / 1024 / 1024 < 5;
151 if (!isLt5M) {
152 message.error('图片大小不能超过 5MB!');
153 return false;
154 }
155 return false;
156 }, []);
157
158 const handleNext = useCallback((): void => {
159 if (fileList.length === 0) {
160 message.error('请上传作品封面');
161 return;
162 }
163 onNext?.();
164 }, [fileList, onNext]);
165
166 return (
167 <Card>
168 <Alert
169 message="封面图片要求"
170 description={
171 <ul style={{ marginBottom: 0, paddingLeft: 20 }}>
172 <li>图片格式:JPGPNGGIF</li>
173 <li>图片大小:不超过 5MB</li>
174 <li>建议尺寸:宽高比 3:4,最小分辨率 600x800</li>
175 <li>内容要求:清晰展示作品特色,避免模糊或像素化</li>
176 </ul>
177 }
178 type="info"
179 showIcon
180 style={{ marginBottom: 24 }}
181 />
182
183 <Upload
184 listType="picture-card"
185 fileList={fileList}
186 onChange={handleChange}
187 onPreview={handlePreview}
188 beforeUpload={beforeUpload}
189 maxCount={1}
190 accept="image/*"
191 >
192 {fileList.length === 0 && (
193 <div>
194 <PlusOutlined />
195 <div style={{ marginTop: 8 }}>上传封面</div>
196 </div>
197 )}
198 </Upload>
199
200 <Modal open={previewOpen} title="封面预览" footer={null} onCancel={() => setPreviewOpen(false)}>
201 <img alt="封面预览" style={{ width: '100%' }} src={previewImage} />
202 </Modal>
203
204 <Divider />
205 <Space size="middle">
206 <Button onClick={onPrev} size="large">上一步</Button>
207 <Button type="primary" onClick={handleNext} size="large">下一步</Button>
208 </Space>
209 </Card>
210 );
211};
212
213// ==================== 版本管理相关组件 ====================
214const VersionItem: React.FC<{
215 version: VersionFormData;
216 index: number;
217 onEdit: (index: number) => void;
218 onDelete: (index: number) => void;
219}> = ({ version, index, onEdit, onDelete }) => {
220 return (
221 <Row align="middle">
222 <Col span={20}>
223 <Space direction="vertical" style={{ width: '100%' }}>
224 <Space>
225 <Tag color="blue">v{version.version}</Tag>
226 <Text strong>{version.versionDescription}</Text>
227 </Space>
228 {version.seedFile && (
229 <Space>
230 <FileTextOutlined />
231 <Text type="secondary">{version.seedFile.name}</Text>
232 </Space>
233 )}
234 </Space>
235 </Col>
236 <Col span={4} style={{ textAlign: 'right' }}>
237 <Space>
238 <Button type="text" icon={<EditOutlined />} onClick={() => onEdit(index)} />
239 <Button type="text" danger icon={<DeleteOutlined />} onClick={() => onDelete(index)} />
240 </Space>
241 </Col>
242 </Row>
243 );
244};
245
246const VersionEditForm: React.FC<{
247 form: FormInstance<VersionFormData>;
248 version: VersionFormData;
249 onSave: () => void;
250 onCancel: () => void;
251 onFileChange: (file: UploadFile | undefined) => void;
252}> = ({ form, version, onSave, onCancel, onFileChange }) => {
253 const beforeUpload = useCallback((file: RcFile): boolean => {
254 const isLt100M = file.size / 1024 / 1024 < 100;
255 if (!isLt100M) {
256 message.error('种子文件大小不能超过 100MB!');
257 return false;
258 }
259 return false;
260 }, []);
261
262 return (
263 <Form form={form} layout="vertical" initialValues={version}>
264 <Row gutter={16}>
265 <Col span={6}>
266 <Form.Item label="版本号" name="version" rules={[{ required: true, message: '请输入版本号' }]}>
267 <Input placeholder="例如:1.0" />
268 </Form.Item>
269 </Col>
270 <Col span={18}>
271 <Form.Item
272 label="版本描述"
273 name="versionDescription"
274 rules={[
275 { required: true, message: '请输入版本描述' },
276 { min: 10, message: '版本描述至少10个字符' },
277 ]}
278 >
279 <TextArea placeholder="描述此版本的更新内容、新增功能等" rows={3} showCount maxLength={500} />
280 </Form.Item>
281 </Col>
282 </Row>
283
284 <Form.Item label="种子文件">
285 <Dragger
286 maxCount={1}
287 beforeUpload={beforeUpload}
288 fileList={version.seedFile ? [version.seedFile] : []}
289 onChange={({ fileList }) => onFileChange(fileList[0])}
290 onRemove={() => onFileChange(undefined)}
291 >
292 <p className="ant-upload-drag-icon"><InboxOutlined /></p>
293 <p className="ant-upload-text">点击或拖拽文件到此区域上传</p>
294 <p className="ant-upload-hint">支持单个文件上传,文件大小不超过 100MB</p>
295 </Dragger>
296 </Form.Item>
297
298 <Space>
299 <Button type="primary" onClick={onSave}>保存</Button>
300 <Button onClick={onCancel}>取消</Button>
301 </Space>
302 </Form>
303 );
304};
305
306// ==================== 步骤3:版本管理 ====================
307export const VersionManagementStep: React.FC<StepFormProps> = ({ data, onUpdate, onNext, onPrev }) => {
308 const [versions, setVersions] = useState<VersionFormData[]>(
309 data.versions.length > 0 ? data.versions : [{ version: '1.0', versionDescription: '', seedFile: undefined }]
310 );
311 const [editingIndex, setEditingIndex] = useState<number | null>(null);
312 const [form]: [FormInstance<VersionFormData>] = Form.useForm<VersionFormData>();
313
314 const handleAddVersion = useCallback(() => {
315 const newVersion: VersionFormData = {
316 version: `${versions.length + 1}.0`,
317 versionDescription: '',
318 seedFile: undefined,
319 };
320 setVersions([...versions, newVersion]);
321 setEditingIndex(versions.length);
322 }, [versions]);
323
324 const handleSaveVersion = useCallback((index: number) => {
325 form.validateFields().then((values) => {
326 const newVersions = [...versions];
327 newVersions[index] = { ...newVersions[index], ...values };
328 setVersions(newVersions);
329 setEditingIndex(null);
330 form.resetFields();
331 message.success('版本信息已保存');
332 }).catch(() => {
333 message.error('请完整填写版本信息');
334 });
335 }, [form, versions]);
336
337 const handleDeleteVersion = useCallback((index: number) => {
338 Modal.confirm({
339 title: '确认删除',
340 content: '确定要删除这个版本吗?',
341 onOk: () => {
342 const newVersions = versions.filter((_, i) => i !== index);
343 setVersions(newVersions);
344 if (editingIndex === index) {
345 setEditingIndex(null);
346 }
347 },
348 });
349 }, [versions, editingIndex]);
350
351 const handleFileChange = useCallback((index: number, file: UploadFile | undefined) => {
352 const newVersions = [...versions];
353 newVersions[index].seedFile = file;
354 setVersions(newVersions);
355 }, [versions]);
356
357 const handleNext = useCallback(() => {
358 if (versions.length === 0) {
359 message.error('至少需要添加一个版本');
360 return;
361 }
362
363 const incompleteVersion = versions.find((v, index) =>
364 !v.version || !v.versionDescription || !v.seedFile || index === editingIndex
365 );
366
367 if (incompleteVersion) {
368 message.error('请完成所有版本的信息填写');
369 return;
370 }
371
372 onUpdate('versions', versions);
373 onNext?.();
374 }, [versions, editingIndex, onUpdate, onNext]);
375
376 return (
377 <Card>
378 <div style={{ marginBottom: 16 }}>
379 <Title level={4}>版本列表</Title>
380 <Paragraph type="secondary">每个版本需要包含版本号、版本描述和种子文件</Paragraph>
381 </div>
382
383 <List
384 dataSource={versions}
385 renderItem={(version, index) => (
386 <List.Item
387 key={index}
388 style={{
389 background: editingIndex === index ? '#fafafa' : 'transparent',
390 padding: 16,
391 marginBottom: 16,
392 border: '1px solid #f0f0f0',
393 borderRadius: 8,
394 }}
395 >
396 {editingIndex === index ? (
397 <VersionEditForm
398 form={form}
399 version={version}
400 onSave={() => handleSaveVersion(index)}
401 onCancel={() => setEditingIndex(null)}
402 onFileChange={(file) => handleFileChange(index, file)}
403 />
404 ) : (
405 <VersionItem
406 version={version}
407 index={index}
408 onEdit={(idx) => {
409 setEditingIndex(idx);
410 form.setFieldsValue(version);
411 }}
412 onDelete={handleDeleteVersion}
413 />
414 )}
415 </List.Item>
416 )}
417 />
418
419 {editingIndex === null && (
420 <Button
421 type="dashed"
422 onClick={handleAddVersion}
423 style={{ width: '100%', marginBottom: 24 }}
424 icon={<PlusOutlined />}
425 >
426 添加版本
427 </Button>
428 )}
429
430 <Divider />
431 <Space size="middle">
432 <Button onClick={onPrev} size="large">上一步</Button>
433 <Button type="primary" onClick={handleNext} size="large">下一步</Button>
434 </Space>
435 </Card>
436 );
437};
438
439// ==================== 步骤4:确认发布 ====================
440export const ConfirmPublishStep: React.FC<StepFormProps & { onPublish: () => void }> = ({ data, onPrev, onPublish }) => {
441 const [publishing, setPublishing] = useState<boolean>(false);
442
443 const handlePublish = useCallback(async () => {
444 setPublishing(true);
445 try {
446 await new Promise(resolve => setTimeout(resolve, 2000));
447 message.success('作品发布成功!');
448 onPublish();
449 } catch {
450 message.error('发布失败,请重试');
451 } finally {
452 setPublishing(false);
453 }
454 }, [onPublish]);
455
456 return (
457 <Card>
458 <Title level={3} style={{ marginBottom: 24 }}>
459 <CheckCircleOutlined style={{ color: '#52c41a', marginRight: 8 }} />
460 确认发布信息
461 </Title>
462
463 <div style={{ marginBottom: 32 }}>
464 <Title level={4}>基础信息</Title>
465 <Descriptions bordered column={1}>
466 <Descriptions.Item label="作品名称">{data.basicInfo.artworkName}</Descriptions.Item>
467 <Descriptions.Item label="作品分类">
468 <Tag color="blue">{data.basicInfo.artworkCategory}</Tag>
469 </Descriptions.Item>
470 <Descriptions.Item label="作品描述">
471 <div style={{ maxHeight: 200, overflow: 'auto' }}>
472 <ReactMarkdown>{data.basicInfo.artworkDescription}</ReactMarkdown>
473 </div>
474 </Descriptions.Item>
475 </Descriptions>
476 </div>
477
478 <div style={{ marginBottom: 32 }}>
479 <Title level={4}>作品封面</Title>
480 {data.coverInfo.coverFile && (
481 <img
482 src={data.coverInfo.coverFile.thumbUrl || data.coverInfo.coverFile.url}
483 alt="作品封面"
484 style={{ maxWidth: 300, maxHeight: 400, objectFit: 'cover' }}
485 />
486 )}
487 </div>
488
489 <div style={{ marginBottom: 32 }}>
490 <Title level={4}>版本信息</Title>
491 <List
492 dataSource={data.versions}
493 renderItem={(version) => (
494 <List.Item>
495 <List.Item.Meta
496 title={
497 <Space>
498 <Tag color="green">v{version.version}</Tag>
499 <Text>{version.versionDescription}</Text>
500 </Space>
501 }
502 description={
503 version.seedFile && (
504 <Space>
505 <FileTextOutlined />
506 <Text type="secondary">{version.seedFile.name}</Text>
507 </Space>
508 )
509 }
510 />
511 </List.Item>
512 )}
513 />
514 </div>
515
516 <Alert
517 message="发布须知"
518 description={
519 <ul style={{ marginBottom: 0, paddingLeft: 20 }}>
520 <li>发布后的作品将公开展示,所有用户都可以查看和下载</li>
521 <li>请确保作品内容符合社区规范,不包含违法违规内容</li>
522 <li>发布后您仍可以编辑作品信息和添加新版本</li>
523 <li>请尊重他人知识产权,确保作品为原创或已获得授权</li>
524 </ul>
525 }
526 type="warning"
527 showIcon
528 style={{ marginBottom: 24 }}
529 />
530
531 <Space size="middle">
532 <Button onClick={onPrev} size="large">上一步</Button>
533 <Button
534 type="primary"
535 onClick={handlePublish}
536 loading={publishing}
537 icon={<SendOutlined />}
538 size="large"
539 >
540 确认发布
541 </Button>
542 </Space>
543 </Card>
544 );
545};