完成Work组件的界面和一些小修改
> 1. 修改优化路由守卫
> 2. 去除拦截器中的调试信息
> 3. 修改头部导航条下拉菜单的样式增加图标。
> 4. work组件现在使用mock数据
Change-Id: Ic602a35bb02e645a0d5253c5cbd12a68d70bfb33
diff --git a/src/feature/work/EditWork.tsx b/src/feature/work/EditWork.tsx
new file mode 100644
index 0000000..f56d42a
--- /dev/null
+++ b/src/feature/work/EditWork.tsx
@@ -0,0 +1,753 @@
+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}
+ />
+ </>
+ );
+};
\ No newline at end of file