blob: f56d42a0c7e241dbf7937ffbc4f2f84a55550b13 [file] [log] [blame]
import React, { useState, useCallback } from 'react';
import {
Modal, Form, Input, Upload, Button, message, Space, Card,
Typography, Alert, List, Tag, Popconfirm, Row, Col,
type FormInstance
} from 'antd';
import {
EditOutlined, DeleteOutlined, PlusOutlined, InboxOutlined,
SaveOutlined, FileTextOutlined, UserOutlined
} from '@ant-design/icons';
import ReactMarkdown from 'react-markdown';
import type {
UploadFile, UploadProps, UploadChangeParam, RcFile,
} from 'antd/es/upload';
import type {
ArtworkData, VersionFormData, Comment,
} from './types';
const { TextArea } = Input;
const { Dragger } = Upload;
const { Text } = Typography;
// ==================== 工具函数 ====================
const getBase64 = (file: RcFile): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (): void => resolve(reader.result as string);
reader.onerror = (error): void => reject(error);
});
// ==================== 类型定义 ====================
interface EditCoverProps {
visible: boolean;
currentCover: string;
onCancel: () => void;
onSave: (coverUrl: string) => Promise<void>;
}
interface EditDescriptionProps {
visible: boolean;
currentDescription: string;
onCancel: () => void;
onSave: (description: string) => Promise<void>;
}
interface EditVersionsProps {
visible: boolean;
versions: VersionFormData[];
onCancel: () => void;
onSave: (versions: VersionFormData[]) => Promise<void>;
}
interface CommentItemProps {
comment: Comment;
isAuthor: boolean;
onDelete: (commentId: string) => Promise<void>;
level?: number;
}
interface EditWorkControlsProps {
artwork: ArtworkData;
isAuthor: boolean;
onUpdate: (updatedArtwork: Partial<ArtworkData>) => Promise<void>;
onDeleteComment: (commentId: string) => Promise<void>;
}
interface VersionItemProps {
version: VersionFormData;
index: number;
onEdit: (index: number) => void;
onDelete: (index: number) => void;
}
interface VersionEditFormProps {
form: FormInstance<VersionFormData>;
version: VersionFormData;
onSave: () => void;
onCancel: () => void;
onFileChange: (file: UploadFile | undefined) => void;
}
// ==================== 封面编辑组件 ====================
export const EditWorkCover: React.FC<EditCoverProps> = ({
visible,
currentCover,
onCancel,
onSave
}) => {
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [previewImage, setPreviewImage] = useState<string>('');
const [previewOpen, setPreviewOpen] = useState<boolean>(false);
const [uploading, setUploading] = useState<boolean>(false);
const handleChange: UploadProps['onChange'] = useCallback((info: UploadChangeParam<UploadFile>): void => {
setFileList(info.fileList);
}, []);
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 handleSave = useCallback(async (): Promise<void> => {
if (fileList.length === 0) {
message.error('请选择要上传的封面图片');
return;
}
setUploading(true);
try {
// 模拟上传过程
const file = fileList[0];
const coverUrl = file.preview || URL.createObjectURL(file.originFileObj as RcFile);
await onSave(coverUrl);
message.success('封面更新成功!');
onCancel();
} catch {
message.error('封面更新失败,请重试');
} finally {
setUploading(false);
}
}, [fileList, onSave, onCancel]);
return (
<>
<Modal
title="编辑作品封面"
open={visible}
onCancel={onCancel}
footer={[
<Button key="cancel" onClick={onCancel}>
取消
</Button>,
<Button
key="save"
type="primary"
loading={uploading}
onClick={handleSave}
icon={<SaveOutlined />}
>
保存
</Button>,
]}
width={600}
>
<div style={{ marginBottom: 16 }}>
<Text strong>当前封面:</Text>
<div style={{ marginTop: 8 }}>
<img
src={currentCover}
alt="当前封面"
style={{ maxWidth: '100%', maxHeight: 200, objectFit: 'cover' }}
/>
</div>
</div>
<Alert
message="新封面要求"
description="图片格式:JPG、PNG、GIF;大小不超过 5MB;建议尺寸:宽高比 3:4"
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
<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>
<Modal
open={previewOpen}
title="图片预览"
footer={null}
onCancel={(): void => setPreviewOpen(false)}
>
<img alt="预览" style={{ width: '100%' }} src={previewImage} />
</Modal>
</>
);
};
// ==================== 作品描述编辑组件 ====================
export const EditWorkDescription: React.FC<EditDescriptionProps> = ({
visible,
currentDescription,
onCancel,
onSave
}) => {
const [form] = Form.useForm<{ description: string }>();
const [previewMode, setPreviewMode] = useState<boolean>(false);
const [saving, setSaving] = useState<boolean>(false);
const handleSave = useCallback(async (): Promise<void> => {
try {
const values = await form.validateFields();
setSaving(true);
await onSave(values.description);
message.success('作品描述更新成功!');
onCancel();
} catch (error) {
if (error && typeof error === 'object' && 'errorFields' in error) {
message.error('请检查输入内容');
} else {
message.error('更新失败,请重试');
}
} finally {
setSaving(false);
}
}, [form, onSave, onCancel]);
const handlePreview = useCallback((): void => {
form.validateFields().then(() => {
setPreviewMode(true);
}).catch(() => {
message.error('请先填写完整信息');
});
}, [form]);
return (
<>
<Modal
title="编辑作品描述"
open={visible}
onCancel={onCancel}
footer={[
<Button key="preview" onClick={handlePreview}>
预览
</Button>,
<Button key="cancel" onClick={onCancel}>
取消
</Button>,
<Button
key="save"
type="primary"
loading={saving}
onClick={handleSave}
icon={<SaveOutlined />}
>
保存
</Button>,
]}
width={800}
>
<Form
form={form}
layout="vertical"
initialValues={{ description: currentDescription }}
>
<Form.Item
label="作品描述"
name="description"
rules={[
{ required: true, message: '请输入作品描述' },
{ min: 20, message: '作品描述至少20个字符' },
{ max: 2000, message: '作品描述最多2000个字符' },
]}
extra="支持 Markdown 格式,可以使用 # 标题、**粗体**、*斜体* 等格式"
>
<TextArea
placeholder="请详细描述你的作品,包括创作理念、技术特点、使用说明等"
rows={12}
showCount
maxLength={2000}
/>
</Form.Item>
</Form>
</Modal>
<Modal
title="作品描述预览"
open={previewMode}
onCancel={(): void => setPreviewMode(false)}
footer={null}
width={800}
>
<div style={{ maxHeight: '60vh', overflow: 'auto' }}>
<ReactMarkdown>{form.getFieldValue('description') || ''}</ReactMarkdown>
</div>
</Modal>
</>
);
};
// ==================== 版本编辑相关组件 ====================
const VersionItem: React.FC<VersionItemProps> = ({
version,
index,
onEdit,
onDelete
}) => {
return (
<Row align="middle" style={{ width: '100%' }}>
<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={(): void => onEdit(index)}
/>
<Popconfirm
title="确定要删除这个版本吗?"
onConfirm={(): void => onDelete(index)}
okText="确定"
cancelText="取消"
>
<Button type="text" danger icon={<DeleteOutlined />} />
</Popconfirm>
</Space>
</Col>
</Row>
);
};
const VersionEditForm: React.FC<VersionEditFormProps> = ({
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 (
<div style={{ width: '100%' }}>
<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 }): void => onFileChange(fileList[0])}
onRemove={(): void => 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>
</div>
);
};
// ==================== 版本管理组件 ====================
export const EditWorkVersions: React.FC<EditVersionsProps> = ({
visible,
versions,
onCancel,
onSave
}) => {
const [localVersions, setLocalVersions] = useState<VersionFormData[]>(versions);
const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [form] = Form.useForm<VersionFormData>();
const [saving, setSaving] = useState<boolean>(false);
const handleAddVersion = useCallback((): void => {
const newVersion: VersionFormData = {
version: `${localVersions.length + 1}.0`,
versionDescription: '',
seedFile: undefined,
};
setLocalVersions([...localVersions, newVersion]);
setEditingIndex(localVersions.length);
}, [localVersions]);
const handleSaveVersion = useCallback((index: number): void => {
form.validateFields().then((values) => {
const newVersions = [...localVersions];
newVersions[index] = { ...newVersions[index], ...values };
setLocalVersions(newVersions);
setEditingIndex(null);
form.resetFields();
message.success('版本信息已保存');
}).catch(() => {
message.error('请完整填写版本信息');
});
}, [form, localVersions]);
const handleDeleteVersion = useCallback((index: number): void => {
const newVersions = localVersions.filter((_, i) => i !== index);
setLocalVersions(newVersions);
if (editingIndex === index) {
setEditingIndex(null);
}
}, [localVersions, editingIndex]);
const handleFileChange = useCallback((index: number, file: UploadFile | undefined): void => {
const newVersions = [...localVersions];
newVersions[index].seedFile = file;
setLocalVersions(newVersions);
}, [localVersions]);
const handleSave = useCallback(async (): Promise<void> => {
if (localVersions.length === 0) {
message.error('至少需要保留一个版本');
return;
}
const incompleteVersion = localVersions.find((v, index) =>
!v.version || !v.versionDescription || !v.seedFile || index === editingIndex
);
if (incompleteVersion) {
message.error('请完成所有版本的信息填写');
return;
}
setSaving(true);
try {
await onSave(localVersions);
message.success('版本信息更新成功!');
onCancel();
} catch {
message.error('更新失败,请重试');
} finally {
setSaving(false);
}
}, [localVersions, editingIndex, onSave, onCancel]);
return (
<Modal
title="编辑版本信息"
open={visible}
onCancel={onCancel}
footer={[
<Button key="cancel" onClick={onCancel}>
取消
</Button>,
<Button
key="save"
type="primary"
loading={saving}
onClick={handleSave}
icon={<SaveOutlined />}
>
保存所有更改
</Button>,
]}
width={900}
>
<div style={{ maxHeight: '60vh', overflow: 'auto' }}>
<List
dataSource={localVersions}
renderItem={(version, index): React.ReactElement => (
<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={(): void => handleSaveVersion(index)}
onCancel={(): void => setEditingIndex(null)}
onFileChange={(file): void => handleFileChange(index, file)}
/>
) : (
<VersionItem
version={version}
index={index}
onEdit={(idx): void => {
setEditingIndex(idx);
form.setFieldsValue(version);
}}
onDelete={handleDeleteVersion}
/>
)}
</List.Item>
)}
/>
{editingIndex === null && (
<Button
type="dashed"
onClick={handleAddVersion}
style={{ width: '100%', marginTop: 16 }}
icon={<PlusOutlined />}
>
添加新版本
</Button>
)}
</div>
</Modal>
);
};
// ==================== 评论管理组件 ====================
export const EditWorkComment: React.FC<CommentItemProps> = ({
comment,
isAuthor,
onDelete,
level = 0
}) => {
const [deleting, setDeleting] = useState<boolean>(false);
const handleDelete = useCallback(async (): Promise<void> => {
if (!comment.id) return;
setDeleting(true);
try {
await onDelete(comment.id);
message.success('评论删除成功');
} catch {
message.error('删除失败,请重试');
} finally {
setDeleting(false);
}
}, [comment.id, onDelete]);
return (
<div style={{ marginLeft: level * 24 }}>
<div
style={{
background: '#fafafa',
padding: 12,
borderRadius: 8,
marginBottom: 8,
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}>
<Space style={{ marginBottom: 8 }}>
<UserOutlined />
<Text strong>{comment.author}</Text>
{comment.createdAt && (
<Text type="secondary" style={{ fontSize: 12 }}>
{new Date(comment.createdAt).toLocaleString()}
</Text>
)}
</Space>
<div>
<Text>{comment.content}</Text>
</div>
</div>
{isAuthor && comment.id && (
<Popconfirm
title="确定要删除这条评论吗?"
onConfirm={handleDelete}
okText="确定"
cancelText="取消"
>
<Button
type="text"
danger
size="small"
icon={<DeleteOutlined />}
loading={deleting}
/>
</Popconfirm>
)}
</div>
</div>
{/* 递归渲染子评论 */}
{comment.child && comment.child.length > 0 && (
<div>
{comment.child.map((childComment, index) => (
<EditWorkComment
key={childComment.id || index}
comment={childComment}
isAuthor={isAuthor}
onDelete={onDelete}
level={level + 1}
/>
))}
</div>
)}
</div>
);
};
// ==================== 主编辑控制器组件 ====================
export const EditWorkControls: React.FC<EditWorkControlsProps> = ({
artwork,
isAuthor,
onUpdate,
}) => {
const [editCoverVisible, setEditCoverVisible] = useState<boolean>(false);
const [editDescriptionVisible, setEditDescriptionVisible] = useState<boolean>(false);
const [editVersionsVisible, setEditVersionsVisible] = useState<boolean>(false);
const handleUpdateCover = useCallback(async (coverUrl: string): Promise<void> => {
await onUpdate({ artworkCover: coverUrl });
}, [onUpdate]);
const handleUpdateDescription = useCallback(async (description: string): Promise<void> => {
await onUpdate({ artworkDescription: description });
}, [onUpdate]);
const handleUpdateVersions = useCallback(async (versions: VersionFormData[]): Promise<void> => {
// 转换为展示用的版本格式
const versionList = versions.map(v => ({
version: v.version,
versionDescription: v.versionDescription,
seedFile: v.seedFile?.name || '',
}));
await onUpdate({ versionList });
}, [onUpdate]);
if (!isAuthor) {
return null;
}
return (
<>
<Card title="作者管理" style={{ marginBottom: 24 }}>
<Space wrap>
<Button
icon={<EditOutlined />}
onClick={(): void => setEditCoverVisible(true)}
>
编辑封面
</Button>
<Button
icon={<EditOutlined />}
onClick={(): void => setEditDescriptionVisible(true)}
>
编辑描述
</Button>
<Button
icon={<EditOutlined />}
onClick={(): void => setEditVersionsVisible(true)}
>
管理版本
</Button>
</Space>
</Card>
<EditWorkCover
visible={editCoverVisible}
currentCover={artwork.artworkCover}
onCancel={(): void => setEditCoverVisible(false)}
onSave={handleUpdateCover}
/>
<EditWorkDescription
visible={editDescriptionVisible}
currentDescription={artwork.artworkDescription}
onCancel={(): void => setEditDescriptionVisible(false)}
onSave={handleUpdateDescription}
/>
<EditWorkVersions
visible={editVersionsVisible}
versions={artwork.versionList.map(v => ({
version: v.version,
versionDescription: v.versionDescription,
seedFile: { name: v.seedFile } as UploadFile,
}))}
onCancel={(): void => setEditVersionsVisible(false)}
onSave={handleUpdateVersions}
/>
</>
);
};