blob: 12766ee8054ab13af02e460e2b2acbe3015a3f92 [file] [log] [blame] [edit]
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>图片格式:JPGPNGGIF</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>
);
};