blob: f56d42a0c7e241dbf7937ffbc4f2f84a55550b13 [file] [log] [blame]
223010144ce05872025-06-08 22:33:28 +08001import React, { useState, useCallback } from 'react';
2import {
3 Modal, Form, Input, Upload, Button, message, Space, Card,
4 Typography, Alert, List, Tag, Popconfirm, Row, Col,
5 type FormInstance
6} from 'antd';
7import {
8 EditOutlined, DeleteOutlined, PlusOutlined, InboxOutlined,
9 SaveOutlined, FileTextOutlined, UserOutlined
10} from '@ant-design/icons';
11import ReactMarkdown from 'react-markdown';
12import type {
13 UploadFile, UploadProps, UploadChangeParam, RcFile,
14} from 'antd/es/upload';
15import type {
16 ArtworkData, VersionFormData, Comment,
17} from './types';
18
19const { TextArea } = Input;
20const { Dragger } = Upload;
21const { Text } = Typography;
22
23// ==================== 工具函数 ====================
24const getBase64 = (file: RcFile): Promise<string> =>
25 new Promise((resolve, reject) => {
26 const reader = new FileReader();
27 reader.readAsDataURL(file);
28 reader.onload = (): void => resolve(reader.result as string);
29 reader.onerror = (error): void => reject(error);
30 });
31
32// ==================== 类型定义 ====================
33interface EditCoverProps {
34 visible: boolean;
35 currentCover: string;
36 onCancel: () => void;
37 onSave: (coverUrl: string) => Promise<void>;
38}
39
40interface EditDescriptionProps {
41 visible: boolean;
42 currentDescription: string;
43 onCancel: () => void;
44 onSave: (description: string) => Promise<void>;
45}
46
47interface EditVersionsProps {
48 visible: boolean;
49 versions: VersionFormData[];
50 onCancel: () => void;
51 onSave: (versions: VersionFormData[]) => Promise<void>;
52}
53
54interface CommentItemProps {
55 comment: Comment;
56 isAuthor: boolean;
57 onDelete: (commentId: string) => Promise<void>;
58 level?: number;
59}
60
61interface EditWorkControlsProps {
62 artwork: ArtworkData;
63 isAuthor: boolean;
64 onUpdate: (updatedArtwork: Partial<ArtworkData>) => Promise<void>;
65 onDeleteComment: (commentId: string) => Promise<void>;
66}
67
68interface VersionItemProps {
69 version: VersionFormData;
70 index: number;
71 onEdit: (index: number) => void;
72 onDelete: (index: number) => void;
73}
74
75interface VersionEditFormProps {
76 form: FormInstance<VersionFormData>;
77 version: VersionFormData;
78 onSave: () => void;
79 onCancel: () => void;
80 onFileChange: (file: UploadFile | undefined) => void;
81}
82
83// ==================== 封面编辑组件 ====================
84export const EditWorkCover: React.FC<EditCoverProps> = ({
85 visible,
86 currentCover,
87 onCancel,
88 onSave
89}) => {
90 const [fileList, setFileList] = useState<UploadFile[]>([]);
91 const [previewImage, setPreviewImage] = useState<string>('');
92 const [previewOpen, setPreviewOpen] = useState<boolean>(false);
93 const [uploading, setUploading] = useState<boolean>(false);
94
95 const handleChange: UploadProps['onChange'] = useCallback((info: UploadChangeParam<UploadFile>): void => {
96 setFileList(info.fileList);
97 }, []);
98
99 const handlePreview = useCallback(async (file: UploadFile): Promise<void> => {
100 if (!file.url && !file.preview) {
101 file.preview = await getBase64(file.originFileObj as RcFile);
102 }
103 setPreviewImage(file.url || (file.preview as string));
104 setPreviewOpen(true);
105 }, []);
106
107 const beforeUpload = useCallback((file: RcFile): boolean => {
108 const isImage = file.type.startsWith('image/');
109 if (!isImage) {
110 message.error('只能上传图片文件!');
111 return false;
112 }
113 const isLt5M = file.size / 1024 / 1024 < 5;
114 if (!isLt5M) {
115 message.error('图片大小不能超过 5MB!');
116 return false;
117 }
118 return false; // 阻止自动上传
119 }, []);
120
121 const handleSave = useCallback(async (): Promise<void> => {
122 if (fileList.length === 0) {
123 message.error('请选择要上传的封面图片');
124 return;
125 }
126
127 setUploading(true);
128 try {
129 // 模拟上传过程
130 const file = fileList[0];
131 const coverUrl = file.preview || URL.createObjectURL(file.originFileObj as RcFile);
132
133 await onSave(coverUrl);
134 message.success('封面更新成功!');
135 onCancel();
136 } catch {
137 message.error('封面更新失败,请重试');
138 } finally {
139 setUploading(false);
140 }
141 }, [fileList, onSave, onCancel]);
142
143 return (
144 <>
145 <Modal
146 title="编辑作品封面"
147 open={visible}
148 onCancel={onCancel}
149 footer={[
150 <Button key="cancel" onClick={onCancel}>
151 取消
152 </Button>,
153 <Button
154 key="save"
155 type="primary"
156 loading={uploading}
157 onClick={handleSave}
158 icon={<SaveOutlined />}
159 >
160 保存
161 </Button>,
162 ]}
163 width={600}
164 >
165 <div style={{ marginBottom: 16 }}>
166 <Text strong>当前封面:</Text>
167 <div style={{ marginTop: 8 }}>
168 <img
169 src={currentCover}
170 alt="当前封面"
171 style={{ maxWidth: '100%', maxHeight: 200, objectFit: 'cover' }}
172 />
173 </div>
174 </div>
175
176 <Alert
177 message="新封面要求"
178 description="图片格式:JPG、PNG、GIF;大小不超过 5MB;建议尺寸:宽高比 3:4"
179 type="info"
180 showIcon
181 style={{ marginBottom: 16 }}
182 />
183
184 <Upload
185 listType="picture-card"
186 fileList={fileList}
187 onChange={handleChange}
188 onPreview={handlePreview}
189 beforeUpload={beforeUpload}
190 maxCount={1}
191 accept="image/*"
192 >
193 {fileList.length === 0 && (
194 <div>
195 <PlusOutlined />
196 <div style={{ marginTop: 8 }}>选择新封面</div>
197 </div>
198 )}
199 </Upload>
200 </Modal>
201
202 <Modal
203 open={previewOpen}
204 title="图片预览"
205 footer={null}
206 onCancel={(): void => setPreviewOpen(false)}
207 >
208 <img alt="预览" style={{ width: '100%' }} src={previewImage} />
209 </Modal>
210 </>
211 );
212};
213
214// ==================== 作品描述编辑组件 ====================
215export const EditWorkDescription: React.FC<EditDescriptionProps> = ({
216 visible,
217 currentDescription,
218 onCancel,
219 onSave
220}) => {
221 const [form] = Form.useForm<{ description: string }>();
222 const [previewMode, setPreviewMode] = useState<boolean>(false);
223 const [saving, setSaving] = useState<boolean>(false);
224
225 const handleSave = useCallback(async (): Promise<void> => {
226 try {
227 const values = await form.validateFields();
228 setSaving(true);
229 await onSave(values.description);
230 message.success('作品描述更新成功!');
231 onCancel();
232 } catch (error) {
233 if (error && typeof error === 'object' && 'errorFields' in error) {
234 message.error('请检查输入内容');
235 } else {
236 message.error('更新失败,请重试');
237 }
238 } finally {
239 setSaving(false);
240 }
241 }, [form, onSave, onCancel]);
242
243 const handlePreview = useCallback((): void => {
244 form.validateFields().then(() => {
245 setPreviewMode(true);
246 }).catch(() => {
247 message.error('请先填写完整信息');
248 });
249 }, [form]);
250
251 return (
252 <>
253 <Modal
254 title="编辑作品描述"
255 open={visible}
256 onCancel={onCancel}
257 footer={[
258 <Button key="preview" onClick={handlePreview}>
259 预览
260 </Button>,
261 <Button key="cancel" onClick={onCancel}>
262 取消
263 </Button>,
264 <Button
265 key="save"
266 type="primary"
267 loading={saving}
268 onClick={handleSave}
269 icon={<SaveOutlined />}
270 >
271 保存
272 </Button>,
273 ]}
274 width={800}
275 >
276 <Form
277 form={form}
278 layout="vertical"
279 initialValues={{ description: currentDescription }}
280 >
281 <Form.Item
282 label="作品描述"
283 name="description"
284 rules={[
285 { required: true, message: '请输入作品描述' },
286 { min: 20, message: '作品描述至少20个字符' },
287 { max: 2000, message: '作品描述最多2000个字符' },
288 ]}
289 extra="支持 Markdown 格式,可以使用 # 标题、**粗体**、*斜体* 等格式"
290 >
291 <TextArea
292 placeholder="请详细描述你的作品,包括创作理念、技术特点、使用说明等"
293 rows={12}
294 showCount
295 maxLength={2000}
296 />
297 </Form.Item>
298 </Form>
299 </Modal>
300
301 <Modal
302 title="作品描述预览"
303 open={previewMode}
304 onCancel={(): void => setPreviewMode(false)}
305 footer={null}
306 width={800}
307 >
308 <div style={{ maxHeight: '60vh', overflow: 'auto' }}>
309 <ReactMarkdown>{form.getFieldValue('description') || ''}</ReactMarkdown>
310 </div>
311 </Modal>
312 </>
313 );
314};
315
316// ==================== 版本编辑相关组件 ====================
317const VersionItem: React.FC<VersionItemProps> = ({
318 version,
319 index,
320 onEdit,
321 onDelete
322}) => {
323 return (
324 <Row align="middle" style={{ width: '100%' }}>
325 <Col span={20}>
326 <Space direction="vertical" style={{ width: '100%' }}>
327 <Space>
328 <Tag color="blue">v{version.version}</Tag>
329 <Text strong>{version.versionDescription}</Text>
330 </Space>
331 {version.seedFile && (
332 <Space>
333 <FileTextOutlined />
334 <Text type="secondary">{version.seedFile.name}</Text>
335 </Space>
336 )}
337 </Space>
338 </Col>
339 <Col span={4} style={{ textAlign: 'right' }}>
340 <Space>
341 <Button
342 type="text"
343 icon={<EditOutlined />}
344 onClick={(): void => onEdit(index)}
345 />
346 <Popconfirm
347 title="确定要删除这个版本吗?"
348 onConfirm={(): void => onDelete(index)}
349 okText="确定"
350 cancelText="取消"
351 >
352 <Button type="text" danger icon={<DeleteOutlined />} />
353 </Popconfirm>
354 </Space>
355 </Col>
356 </Row>
357 );
358};
359
360const VersionEditForm: React.FC<VersionEditFormProps> = ({
361 form,
362 version,
363 onSave,
364 onCancel,
365 onFileChange
366}) => {
367 const beforeUpload = useCallback((file: RcFile): boolean => {
368 const isLt100M = file.size / 1024 / 1024 < 100;
369 if (!isLt100M) {
370 message.error('种子文件大小不能超过 100MB!');
371 return false;
372 }
373 return false;
374 }, []);
375
376 return (
377 <div style={{ width: '100%' }}>
378 <Form form={form} layout="vertical" initialValues={version}>
379 <Row gutter={16}>
380 <Col span={6}>
381 <Form.Item
382 label="版本号"
383 name="version"
384 rules={[{ required: true, message: '请输入版本号' }]}
385 >
386 <Input placeholder="例如:1.0" />
387 </Form.Item>
388 </Col>
389 <Col span={18}>
390 <Form.Item
391 label="版本描述"
392 name="versionDescription"
393 rules={[
394 { required: true, message: '请输入版本描述' },
395 { min: 10, message: '版本描述至少10个字符' },
396 ]}
397 >
398 <TextArea
399 placeholder="描述此版本的更新内容、新增功能等"
400 rows={3}
401 showCount
402 maxLength={500}
403 />
404 </Form.Item>
405 </Col>
406 </Row>
407
408 <Form.Item label="种子文件">
409 <Dragger
410 maxCount={1}
411 beforeUpload={beforeUpload}
412 fileList={version.seedFile ? [version.seedFile] : []}
413 onChange={({ fileList }): void => onFileChange(fileList[0])}
414 onRemove={(): void => onFileChange(undefined)}
415 >
416 <p className="ant-upload-drag-icon"><InboxOutlined /></p>
417 <p className="ant-upload-text">点击或拖拽文件到此区域上传</p>
418 <p className="ant-upload-hint">支持单个文件上传,文件大小不超过 100MB</p>
419 </Dragger>
420 </Form.Item>
421
422 <Space>
423 <Button type="primary" onClick={onSave}>
424 保存
425 </Button>
426 <Button onClick={onCancel}>
427 取消
428 </Button>
429 </Space>
430 </Form>
431 </div>
432 );
433};
434
435// ==================== 版本管理组件 ====================
436export const EditWorkVersions: React.FC<EditVersionsProps> = ({
437 visible,
438 versions,
439 onCancel,
440 onSave
441}) => {
442 const [localVersions, setLocalVersions] = useState<VersionFormData[]>(versions);
443 const [editingIndex, setEditingIndex] = useState<number | null>(null);
444 const [form] = Form.useForm<VersionFormData>();
445 const [saving, setSaving] = useState<boolean>(false);
446
447 const handleAddVersion = useCallback((): void => {
448 const newVersion: VersionFormData = {
449 version: `${localVersions.length + 1}.0`,
450 versionDescription: '',
451 seedFile: undefined,
452 };
453 setLocalVersions([...localVersions, newVersion]);
454 setEditingIndex(localVersions.length);
455 }, [localVersions]);
456
457 const handleSaveVersion = useCallback((index: number): void => {
458 form.validateFields().then((values) => {
459 const newVersions = [...localVersions];
460 newVersions[index] = { ...newVersions[index], ...values };
461 setLocalVersions(newVersions);
462 setEditingIndex(null);
463 form.resetFields();
464 message.success('版本信息已保存');
465 }).catch(() => {
466 message.error('请完整填写版本信息');
467 });
468 }, [form, localVersions]);
469
470 const handleDeleteVersion = useCallback((index: number): void => {
471 const newVersions = localVersions.filter((_, i) => i !== index);
472 setLocalVersions(newVersions);
473 if (editingIndex === index) {
474 setEditingIndex(null);
475 }
476 }, [localVersions, editingIndex]);
477
478 const handleFileChange = useCallback((index: number, file: UploadFile | undefined): void => {
479 const newVersions = [...localVersions];
480 newVersions[index].seedFile = file;
481 setLocalVersions(newVersions);
482 }, [localVersions]);
483
484 const handleSave = useCallback(async (): Promise<void> => {
485 if (localVersions.length === 0) {
486 message.error('至少需要保留一个版本');
487 return;
488 }
489
490 const incompleteVersion = localVersions.find((v, index) =>
491 !v.version || !v.versionDescription || !v.seedFile || index === editingIndex
492 );
493
494 if (incompleteVersion) {
495 message.error('请完成所有版本的信息填写');
496 return;
497 }
498
499 setSaving(true);
500 try {
501 await onSave(localVersions);
502 message.success('版本信息更新成功!');
503 onCancel();
504 } catch {
505 message.error('更新失败,请重试');
506 } finally {
507 setSaving(false);
508 }
509 }, [localVersions, editingIndex, onSave, onCancel]);
510
511 return (
512 <Modal
513 title="编辑版本信息"
514 open={visible}
515 onCancel={onCancel}
516 footer={[
517 <Button key="cancel" onClick={onCancel}>
518 取消
519 </Button>,
520 <Button
521 key="save"
522 type="primary"
523 loading={saving}
524 onClick={handleSave}
525 icon={<SaveOutlined />}
526 >
527 保存所有更改
528 </Button>,
529 ]}
530 width={900}
531 >
532 <div style={{ maxHeight: '60vh', overflow: 'auto' }}>
533 <List
534 dataSource={localVersions}
535 renderItem={(version, index): React.ReactElement => (
536 <List.Item
537 key={index}
538 style={{
539 background: editingIndex === index ? '#fafafa' : 'transparent',
540 padding: 16,
541 marginBottom: 16,
542 border: '1px solid #f0f0f0',
543 borderRadius: 8,
544 }}
545 >
546 {editingIndex === index ? (
547 <VersionEditForm
548 form={form}
549 version={version}
550 onSave={(): void => handleSaveVersion(index)}
551 onCancel={(): void => setEditingIndex(null)}
552 onFileChange={(file): void => handleFileChange(index, file)}
553 />
554 ) : (
555 <VersionItem
556 version={version}
557 index={index}
558 onEdit={(idx): void => {
559 setEditingIndex(idx);
560 form.setFieldsValue(version);
561 }}
562 onDelete={handleDeleteVersion}
563 />
564 )}
565 </List.Item>
566 )}
567 />
568
569 {editingIndex === null && (
570 <Button
571 type="dashed"
572 onClick={handleAddVersion}
573 style={{ width: '100%', marginTop: 16 }}
574 icon={<PlusOutlined />}
575 >
576 添加新版本
577 </Button>
578 )}
579 </div>
580 </Modal>
581 );
582};
583
584// ==================== 评论管理组件 ====================
585export const EditWorkComment: React.FC<CommentItemProps> = ({
586 comment,
587 isAuthor,
588 onDelete,
589 level = 0
590}) => {
591 const [deleting, setDeleting] = useState<boolean>(false);
592
593 const handleDelete = useCallback(async (): Promise<void> => {
594 if (!comment.id) return;
595
596 setDeleting(true);
597 try {
598 await onDelete(comment.id);
599 message.success('评论删除成功');
600 } catch {
601 message.error('删除失败,请重试');
602 } finally {
603 setDeleting(false);
604 }
605 }, [comment.id, onDelete]);
606
607 return (
608 <div style={{ marginLeft: level * 24 }}>
609 <div
610 style={{
611 background: '#fafafa',
612 padding: 12,
613 borderRadius: 8,
614 marginBottom: 8,
615 }}
616 >
617 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
618 <div style={{ flex: 1 }}>
619 <Space style={{ marginBottom: 8 }}>
620 <UserOutlined />
621 <Text strong>{comment.author}</Text>
622 {comment.createdAt && (
623 <Text type="secondary" style={{ fontSize: 12 }}>
624 {new Date(comment.createdAt).toLocaleString()}
625 </Text>
626 )}
627 </Space>
628 <div>
629 <Text>{comment.content}</Text>
630 </div>
631 </div>
632
633 {isAuthor && comment.id && (
634 <Popconfirm
635 title="确定要删除这条评论吗?"
636 onConfirm={handleDelete}
637 okText="确定"
638 cancelText="取消"
639 >
640 <Button
641 type="text"
642 danger
643 size="small"
644 icon={<DeleteOutlined />}
645 loading={deleting}
646 />
647 </Popconfirm>
648 )}
649 </div>
650 </div>
651
652 {/* 递归渲染子评论 */}
653 {comment.child && comment.child.length > 0 && (
654 <div>
655 {comment.child.map((childComment, index) => (
656 <EditWorkComment
657 key={childComment.id || index}
658 comment={childComment}
659 isAuthor={isAuthor}
660 onDelete={onDelete}
661 level={level + 1}
662 />
663 ))}
664 </div>
665 )}
666 </div>
667 );
668};
669
670// ==================== 主编辑控制器组件 ====================
671export const EditWorkControls: React.FC<EditWorkControlsProps> = ({
672 artwork,
673 isAuthor,
674 onUpdate,
675}) => {
676 const [editCoverVisible, setEditCoverVisible] = useState<boolean>(false);
677 const [editDescriptionVisible, setEditDescriptionVisible] = useState<boolean>(false);
678 const [editVersionsVisible, setEditVersionsVisible] = useState<boolean>(false);
679
680 const handleUpdateCover = useCallback(async (coverUrl: string): Promise<void> => {
681 await onUpdate({ artworkCover: coverUrl });
682 }, [onUpdate]);
683
684 const handleUpdateDescription = useCallback(async (description: string): Promise<void> => {
685 await onUpdate({ artworkDescription: description });
686 }, [onUpdate]);
687
688 const handleUpdateVersions = useCallback(async (versions: VersionFormData[]): Promise<void> => {
689 // 转换为展示用的版本格式
690 const versionList = versions.map(v => ({
691 version: v.version,
692 versionDescription: v.versionDescription,
693 seedFile: v.seedFile?.name || '',
694 }));
695 await onUpdate({ versionList });
696 }, [onUpdate]);
697
698 if (!isAuthor) {
699 return null;
700 }
701
702 return (
703 <>
704 <Card title="作者管理" style={{ marginBottom: 24 }}>
705 <Space wrap>
706 <Button
707 icon={<EditOutlined />}
708 onClick={(): void => setEditCoverVisible(true)}
709 >
710 编辑封面
711 </Button>
712 <Button
713 icon={<EditOutlined />}
714 onClick={(): void => setEditDescriptionVisible(true)}
715 >
716 编辑描述
717 </Button>
718 <Button
719 icon={<EditOutlined />}
720 onClick={(): void => setEditVersionsVisible(true)}
721 >
722 管理版本
723 </Button>
724 </Space>
725 </Card>
726
727 <EditWorkCover
728 visible={editCoverVisible}
729 currentCover={artwork.artworkCover}
730 onCancel={(): void => setEditCoverVisible(false)}
731 onSave={handleUpdateCover}
732 />
733
734 <EditWorkDescription
735 visible={editDescriptionVisible}
736 currentDescription={artwork.artworkDescription}
737 onCancel={(): void => setEditDescriptionVisible(false)}
738 onSave={handleUpdateDescription}
739 />
740
741 <EditWorkVersions
742 visible={editVersionsVisible}
743 versions={artwork.versionList.map(v => ({
744 version: v.version,
745 versionDescription: v.versionDescription,
746 seedFile: { name: v.seedFile } as UploadFile,
747 }))}
748 onCancel={(): void => setEditVersionsVisible(false)}
749 onSave={handleUpdateVersions}
750 />
751 </>
752 );
753};