| import React, { useState, useCallback } from 'react'; |
| import { |
| Form, Input, Select, Upload, Button, Card, message, Space, Modal, |
| Divider, Row, Col, Typography, Alert, List, Tag, |
| type FormInstance |
| } from 'antd'; |
| import { |
| PlusOutlined, InboxOutlined, DeleteOutlined, EditOutlined, |
| CheckCircleOutlined, SendOutlined, FileTextOutlined |
| } from '@ant-design/icons'; |
| import ReactMarkdown from 'react-markdown'; |
| import type { |
| UploadFile, UploadProps, UploadChangeParam, RcFile, |
| } from 'antd/es/upload'; |
| import { type StepFormProps, type BasicInfo, ARTWORK_CATEGORIES, type VersionFormData } from './types'; |
| import { getBase64 } from './utils'; |
| |
| const { TextArea } = Input; |
| const { Dragger } = Upload; |
| const { Title, Text, Paragraph } = Typography; |
| |
| // ==================== 描述组件 ==================== |
| interface DescriptionsProps { |
| bordered?: boolean; |
| column?: number; |
| children: React.ReactNode; |
| } |
| |
| interface DescriptionsItemProps { |
| label: string; |
| children: React.ReactNode; |
| } |
| |
| export const Descriptions: React.FC<DescriptionsProps> & { |
| Item: React.FC<DescriptionsItemProps>; |
| } = ({ children }) => { |
| return <div style={{ marginBottom: 16 }}>{children}</div>; |
| }; |
| |
| Descriptions.Item = ({ label, children }: DescriptionsItemProps) => { |
| return ( |
| <div style={{ marginBottom: 12 }}> |
| <Text strong style={{ marginRight: 16 }}>{label}:</Text> |
| <span>{children}</span> |
| </div> |
| ); |
| }; |
| |
| // ==================== 步骤1:基础信息 ==================== |
| export const BasicInfoStep: React.FC<StepFormProps> = ({ data, onUpdate, onNext }) => { |
| const [form]: [FormInstance<BasicInfo>] = Form.useForm<BasicInfo>(); |
| const [previewMode, setPreviewMode] = useState<boolean>(false); |
| |
| const handleFinish = useCallback((values: BasicInfo) => { |
| onUpdate('basicInfo', values); |
| onNext?.(); |
| }, [onUpdate, onNext]); |
| |
| const handlePreview = useCallback(() => { |
| form.validateFields().then((values) => { |
| onUpdate('basicInfo', values); |
| setPreviewMode(true); |
| }).catch(() => { |
| message.error('请先填写完整信息'); |
| }); |
| }, [form, onUpdate]); |
| |
| return ( |
| <> |
| <Card> |
| <Form form={form} layout="vertical" initialValues={data.basicInfo} onFinish={handleFinish} autoComplete="off"> |
| <Form.Item |
| label="作品名称" |
| name="artworkName" |
| rules={[ |
| { required: true, message: '请输入作品名称' }, |
| { min: 2, message: '作品名称至少2个字符' }, |
| { max: 50, message: '作品名称最多50个字符' }, |
| ]} |
| > |
| <Input placeholder="请输入作品名称,例如:未来城市概念设计" size="large" showCount maxLength={50} /> |
| </Form.Item> |
| |
| <Form.Item label="作品分类" name="artworkCategory" rules={[{ required: true, message: '请选择作品分类' }]}> |
| <Select placeholder="请选择作品分类" size="large" options={ARTWORK_CATEGORIES} /> |
| </Form.Item> |
| |
| <Form.Item |
| label="作品描述" |
| name="artworkDescription" |
| rules={[ |
| { required: true, message: '请输入作品描述' }, |
| { min: 20, message: '作品描述至少20个字符' }, |
| { max: 2000, message: '作品描述最多2000个字符' }, |
| ]} |
| extra="支持 Markdown 格式,可以使用 # 标题、**粗体**、*斜体* 等格式" |
| > |
| <TextArea |
| placeholder="请详细描述你的作品,包括创作理念、技术特点、使用说明等" |
| rows={10} |
| showCount |
| maxLength={2000} |
| /> |
| </Form.Item> |
| |
| <Form.Item> |
| <Space size="middle"> |
| <Button type="primary" htmlType="submit" size="large">下一步</Button> |
| <Button onClick={handlePreview} size="large">预览描述</Button> |
| </Space> |
| </Form.Item> |
| </Form> |
| </Card> |
| |
| <Modal title="作品描述预览" open={previewMode} onCancel={() => setPreviewMode(false)} footer={null} width={800}> |
| <div style={{ maxHeight: '60vh', overflow: 'auto' }}> |
| <ReactMarkdown>{form.getFieldValue('artworkDescription') || ''}</ReactMarkdown> |
| </div> |
| </Modal> |
| </> |
| ); |
| }; |
| |
| // ==================== 步骤2:封面上传 ==================== |
| export const CoverUploadStep: React.FC<StepFormProps> = ({ data, onUpdate, onNext, onPrev }) => { |
| const [fileList, setFileList] = useState<UploadFile[]>(data.coverInfo.coverFile ? [data.coverInfo.coverFile] : []); |
| const [previewImage, setPreviewImage] = useState<string>(''); |
| const [previewOpen, setPreviewOpen] = useState<boolean>(false); |
| |
| const handleChange: UploadProps['onChange'] = useCallback((info: UploadChangeParam<UploadFile>) => { |
| const { fileList: newFileList } = info; |
| setFileList(newFileList); |
| onUpdate('coverInfo', { coverFile: newFileList[0] }); |
| }, [onUpdate]); |
| |
| const handlePreview = useCallback(async (file: UploadFile): Promise<void> => { |
| if (!file.url && !file.preview) { |
| file.preview = await getBase64(file.originFileObj as RcFile); |
| } |
| setPreviewImage(file.url || (file.preview as string)); |
| setPreviewOpen(true); |
| }, []); |
| |
| const beforeUpload = useCallback((file: RcFile): boolean => { |
| const isImage = file.type.startsWith('image/'); |
| if (!isImage) { |
| message.error('只能上传图片文件!'); |
| return false; |
| } |
| const isLt5M = file.size / 1024 / 1024 < 5; |
| if (!isLt5M) { |
| message.error('图片大小不能超过 5MB!'); |
| return false; |
| } |
| return false; |
| }, []); |
| |
| const handleNext = useCallback((): void => { |
| if (fileList.length === 0) { |
| message.error('请上传作品封面'); |
| return; |
| } |
| onNext?.(); |
| }, [fileList, onNext]); |
| |
| return ( |
| <Card> |
| <Alert |
| message="封面图片要求" |
| description={ |
| <ul style={{ marginBottom: 0, paddingLeft: 20 }}> |
| <li>图片格式:JPG、PNG、GIF</li> |
| <li>图片大小:不超过 5MB</li> |
| <li>建议尺寸:宽高比 3:4,最小分辨率 600x800</li> |
| <li>内容要求:清晰展示作品特色,避免模糊或像素化</li> |
| </ul> |
| } |
| type="info" |
| showIcon |
| style={{ marginBottom: 24 }} |
| /> |
| |
| <Upload |
| listType="picture-card" |
| fileList={fileList} |
| onChange={handleChange} |
| onPreview={handlePreview} |
| beforeUpload={beforeUpload} |
| maxCount={1} |
| accept="image/*" |
| > |
| {fileList.length === 0 && ( |
| <div> |
| <PlusOutlined /> |
| <div style={{ marginTop: 8 }}>上传封面</div> |
| </div> |
| )} |
| </Upload> |
| |
| <Modal open={previewOpen} title="封面预览" footer={null} onCancel={() => setPreviewOpen(false)}> |
| <img alt="封面预览" style={{ width: '100%' }} src={previewImage} /> |
| </Modal> |
| |
| <Divider /> |
| <Space size="middle"> |
| <Button onClick={onPrev} size="large">上一步</Button> |
| <Button type="primary" onClick={handleNext} size="large">下一步</Button> |
| </Space> |
| </Card> |
| ); |
| }; |
| |
| // ==================== 版本管理相关组件 ==================== |
| const VersionItem: React.FC<{ |
| version: VersionFormData; |
| index: number; |
| onEdit: (index: number) => void; |
| onDelete: (index: number) => void; |
| }> = ({ version, index, onEdit, onDelete }) => { |
| return ( |
| <Row align="middle"> |
| <Col span={20}> |
| <Space direction="vertical" style={{ width: '100%' }}> |
| <Space> |
| <Tag color="blue">v{version.version}</Tag> |
| <Text strong>{version.versionDescription}</Text> |
| </Space> |
| {version.seedFile && ( |
| <Space> |
| <FileTextOutlined /> |
| <Text type="secondary">{version.seedFile.name}</Text> |
| </Space> |
| )} |
| </Space> |
| </Col> |
| <Col span={4} style={{ textAlign: 'right' }}> |
| <Space> |
| <Button type="text" icon={<EditOutlined />} onClick={() => onEdit(index)} /> |
| <Button type="text" danger icon={<DeleteOutlined />} onClick={() => onDelete(index)} /> |
| </Space> |
| </Col> |
| </Row> |
| ); |
| }; |
| |
| const VersionEditForm: React.FC<{ |
| form: FormInstance<VersionFormData>; |
| version: VersionFormData; |
| onSave: () => void; |
| onCancel: () => void; |
| onFileChange: (file: UploadFile | undefined) => void; |
| }> = ({ form, version, onSave, onCancel, onFileChange }) => { |
| const beforeUpload = useCallback((file: RcFile): boolean => { |
| const isLt100M = file.size / 1024 / 1024 < 100; |
| if (!isLt100M) { |
| message.error('种子文件大小不能超过 100MB!'); |
| return false; |
| } |
| return false; |
| }, []); |
| |
| return ( |
| <Form form={form} layout="vertical" initialValues={version}> |
| <Row gutter={16}> |
| <Col span={6}> |
| <Form.Item label="版本号" name="version" rules={[{ required: true, message: '请输入版本号' }]}> |
| <Input placeholder="例如:1.0" /> |
| </Form.Item> |
| </Col> |
| <Col span={18}> |
| <Form.Item |
| label="版本描述" |
| name="versionDescription" |
| rules={[ |
| { required: true, message: '请输入版本描述' }, |
| { min: 10, message: '版本描述至少10个字符' }, |
| ]} |
| > |
| <TextArea placeholder="描述此版本的更新内容、新增功能等" rows={3} showCount maxLength={500} /> |
| </Form.Item> |
| </Col> |
| </Row> |
| |
| <Form.Item label="种子文件"> |
| <Dragger |
| maxCount={1} |
| beforeUpload={beforeUpload} |
| fileList={version.seedFile ? [version.seedFile] : []} |
| onChange={({ fileList }) => onFileChange(fileList[0])} |
| onRemove={() => onFileChange(undefined)} |
| > |
| <p className="ant-upload-drag-icon"><InboxOutlined /></p> |
| <p className="ant-upload-text">点击或拖拽文件到此区域上传</p> |
| <p className="ant-upload-hint">支持单个文件上传,文件大小不超过 100MB</p> |
| </Dragger> |
| </Form.Item> |
| |
| <Space> |
| <Button type="primary" onClick={onSave}>保存</Button> |
| <Button onClick={onCancel}>取消</Button> |
| </Space> |
| </Form> |
| ); |
| }; |
| |
| // ==================== 步骤3:版本管理 ==================== |
| export const VersionManagementStep: React.FC<StepFormProps> = ({ data, onUpdate, onNext, onPrev }) => { |
| const [versions, setVersions] = useState<VersionFormData[]>( |
| data.versions.length > 0 ? data.versions : [{ version: '1.0', versionDescription: '', seedFile: undefined }] |
| ); |
| const [editingIndex, setEditingIndex] = useState<number | null>(null); |
| const [form]: [FormInstance<VersionFormData>] = Form.useForm<VersionFormData>(); |
| |
| const handleAddVersion = useCallback(() => { |
| const newVersion: VersionFormData = { |
| version: `${versions.length + 1}.0`, |
| versionDescription: '', |
| seedFile: undefined, |
| }; |
| setVersions([...versions, newVersion]); |
| setEditingIndex(versions.length); |
| }, [versions]); |
| |
| const handleSaveVersion = useCallback((index: number) => { |
| form.validateFields().then((values) => { |
| const newVersions = [...versions]; |
| newVersions[index] = { ...newVersions[index], ...values }; |
| setVersions(newVersions); |
| setEditingIndex(null); |
| form.resetFields(); |
| message.success('版本信息已保存'); |
| }).catch(() => { |
| message.error('请完整填写版本信息'); |
| }); |
| }, [form, versions]); |
| |
| const handleDeleteVersion = useCallback((index: number) => { |
| Modal.confirm({ |
| title: '确认删除', |
| content: '确定要删除这个版本吗?', |
| onOk: () => { |
| const newVersions = versions.filter((_, i) => i !== index); |
| setVersions(newVersions); |
| if (editingIndex === index) { |
| setEditingIndex(null); |
| } |
| }, |
| }); |
| }, [versions, editingIndex]); |
| |
| const handleFileChange = useCallback((index: number, file: UploadFile | undefined) => { |
| const newVersions = [...versions]; |
| newVersions[index].seedFile = file; |
| setVersions(newVersions); |
| }, [versions]); |
| |
| const handleNext = useCallback(() => { |
| if (versions.length === 0) { |
| message.error('至少需要添加一个版本'); |
| return; |
| } |
| |
| const incompleteVersion = versions.find((v, index) => |
| !v.version || !v.versionDescription || !v.seedFile || index === editingIndex |
| ); |
| |
| if (incompleteVersion) { |
| message.error('请完成所有版本的信息填写'); |
| return; |
| } |
| |
| onUpdate('versions', versions); |
| onNext?.(); |
| }, [versions, editingIndex, onUpdate, onNext]); |
| |
| return ( |
| <Card> |
| <div style={{ marginBottom: 16 }}> |
| <Title level={4}>版本列表</Title> |
| <Paragraph type="secondary">每个版本需要包含版本号、版本描述和种子文件</Paragraph> |
| </div> |
| |
| <List |
| dataSource={versions} |
| renderItem={(version, index) => ( |
| <List.Item |
| key={index} |
| style={{ |
| background: editingIndex === index ? '#fafafa' : 'transparent', |
| padding: 16, |
| marginBottom: 16, |
| border: '1px solid #f0f0f0', |
| borderRadius: 8, |
| }} |
| > |
| {editingIndex === index ? ( |
| <VersionEditForm |
| form={form} |
| version={version} |
| onSave={() => handleSaveVersion(index)} |
| onCancel={() => setEditingIndex(null)} |
| onFileChange={(file) => handleFileChange(index, file)} |
| /> |
| ) : ( |
| <VersionItem |
| version={version} |
| index={index} |
| onEdit={(idx) => { |
| setEditingIndex(idx); |
| form.setFieldsValue(version); |
| }} |
| onDelete={handleDeleteVersion} |
| /> |
| )} |
| </List.Item> |
| )} |
| /> |
| |
| {editingIndex === null && ( |
| <Button |
| type="dashed" |
| onClick={handleAddVersion} |
| style={{ width: '100%', marginBottom: 24 }} |
| icon={<PlusOutlined />} |
| > |
| 添加版本 |
| </Button> |
| )} |
| |
| <Divider /> |
| <Space size="middle"> |
| <Button onClick={onPrev} size="large">上一步</Button> |
| <Button type="primary" onClick={handleNext} size="large">下一步</Button> |
| </Space> |
| </Card> |
| ); |
| }; |
| |
| // ==================== 步骤4:确认发布 ==================== |
| export const ConfirmPublishStep: React.FC<StepFormProps & { onPublish: () => void }> = ({ data, onPrev, onPublish }) => { |
| const [publishing, setPublishing] = useState<boolean>(false); |
| |
| const handlePublish = useCallback(async () => { |
| setPublishing(true); |
| try { |
| await new Promise(resolve => setTimeout(resolve, 2000)); |
| message.success('作品发布成功!'); |
| onPublish(); |
| } catch { |
| message.error('发布失败,请重试'); |
| } finally { |
| setPublishing(false); |
| } |
| }, [onPublish]); |
| |
| return ( |
| <Card> |
| <Title level={3} style={{ marginBottom: 24 }}> |
| <CheckCircleOutlined style={{ color: '#52c41a', marginRight: 8 }} /> |
| 确认发布信息 |
| </Title> |
| |
| <div style={{ marginBottom: 32 }}> |
| <Title level={4}>基础信息</Title> |
| <Descriptions bordered column={1}> |
| <Descriptions.Item label="作品名称">{data.basicInfo.artworkName}</Descriptions.Item> |
| <Descriptions.Item label="作品分类"> |
| <Tag color="blue">{data.basicInfo.artworkCategory}</Tag> |
| </Descriptions.Item> |
| <Descriptions.Item label="作品描述"> |
| <div style={{ maxHeight: 200, overflow: 'auto' }}> |
| <ReactMarkdown>{data.basicInfo.artworkDescription}</ReactMarkdown> |
| </div> |
| </Descriptions.Item> |
| </Descriptions> |
| </div> |
| |
| <div style={{ marginBottom: 32 }}> |
| <Title level={4}>作品封面</Title> |
| {data.coverInfo.coverFile && ( |
| <img |
| src={data.coverInfo.coverFile.thumbUrl || data.coverInfo.coverFile.url} |
| alt="作品封面" |
| style={{ maxWidth: 300, maxHeight: 400, objectFit: 'cover' }} |
| /> |
| )} |
| </div> |
| |
| <div style={{ marginBottom: 32 }}> |
| <Title level={4}>版本信息</Title> |
| <List |
| dataSource={data.versions} |
| renderItem={(version) => ( |
| <List.Item> |
| <List.Item.Meta |
| title={ |
| <Space> |
| <Tag color="green">v{version.version}</Tag> |
| <Text>{version.versionDescription}</Text> |
| </Space> |
| } |
| description={ |
| version.seedFile && ( |
| <Space> |
| <FileTextOutlined /> |
| <Text type="secondary">{version.seedFile.name}</Text> |
| </Space> |
| ) |
| } |
| /> |
| </List.Item> |
| )} |
| /> |
| </div> |
| |
| <Alert |
| message="发布须知" |
| description={ |
| <ul style={{ marginBottom: 0, paddingLeft: 20 }}> |
| <li>发布后的作品将公开展示,所有用户都可以查看和下载</li> |
| <li>请确保作品内容符合社区规范,不包含违法违规内容</li> |
| <li>发布后您仍可以编辑作品信息和添加新版本</li> |
| <li>请尊重他人知识产权,确保作品为原创或已获得授权</li> |
| </ul> |
| } |
| type="warning" |
| showIcon |
| style={{ marginBottom: 24 }} |
| /> |
| |
| <Space size="middle"> |
| <Button onClick={onPrev} size="large">上一步</Button> |
| <Button |
| type="primary" |
| onClick={handlePublish} |
| loading={publishing} |
| icon={<SendOutlined />} |
| size="large" |
| > |
| 确认发布 |
| </Button> |
| </Space> |
| </Card> |
| ); |
| }; |