From d774370a4b128b8a527e409e4922684ac5fa727a Mon Sep 17 00:00:00 2001 From: simonkoson <28867558@qq.com> Date: Mon, 25 May 2026 09:47:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BB=AA=E8=A1=A8=E7=9B=98=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E9=87=8D=E6=8E=92=EF=BC=8C=E6=96=B0=E5=A2=9E=E9=A2=98?= =?UTF-8?q?=E5=9B=BE=E4=B8=8A=E4=BC=A0=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/Dashboard/ChangeCoverModal.jsx | 151 +++++++ frontend/src/pages/Dashboard/Dashboard.css | 377 +++++++++++------ frontend/src/pages/Dashboard/Dashboard.jsx | 380 +++++++++++------- frontend/src/services/dashboardService.js | 38 ++ frontend/vite.config.js | 4 + 5 files changed, 688 insertions(+), 262 deletions(-) create mode 100644 frontend/src/pages/Dashboard/ChangeCoverModal.jsx create mode 100644 frontend/src/services/dashboardService.js diff --git a/frontend/src/pages/Dashboard/ChangeCoverModal.jsx b/frontend/src/pages/Dashboard/ChangeCoverModal.jsx new file mode 100644 index 0000000..87e2401 --- /dev/null +++ b/frontend/src/pages/Dashboard/ChangeCoverModal.jsx @@ -0,0 +1,151 @@ +import { useState, useEffect } from 'react' +import { Modal, Upload, Select, Button, message } from 'antd' +import { UploadOutlined, InboxOutlined } from '@ant-design/icons' +import { uploadCover } from '../../services/dashboardService' + +const { Dragger } = Upload + +/** + * 更换题图 Modal + * + * 权限:仅 zhipianren / zebian 可用(由父组件控制显示/隐藏) + * + * @param {boolean} open - Modal 是否显示 + * @param {Function} onClose - 关闭回调 + * @param {Function} onUploaded - 上传成功回调,传入新题图对象 + * @param {Array} episodes - 期次列表(用于下拉选择关联期次) + */ +function ChangeCoverModal({ open, onClose, onUploaded, episodes = [] }) { + const [file, setFile] = useState(null) + const [previewUrl, setPreviewUrl] = useState(null) + const [selectedEpisode, setSelectedEpisode] = useState(null) + const [uploading, setUploading] = useState(false) + + // 每次打开时重置状态 + useEffect(() => { + if (open) { + setFile(null) + setPreviewUrl(null) + setSelectedEpisode(null) + setUploading(false) + } + }, [open]) + + // 选择文件后生成预览 + const handleFileChange = (info) => { + const rawFile = info.file.originFileObj || info.file + if (!rawFile) return + setFile(rawFile) + const url = URL.createObjectURL(rawFile) + setPreviewUrl(url) + } + + const handleUpload = async () => { + if (!file) { + message.warning('请先选择图片') + return + } + if (!selectedEpisode) { + message.warning('请选择关联期次') + return + } + + setUploading(true) + try { + const ep = episodes.find(e => String(e.episode_number) === String(selectedEpisode)) + const result = await uploadCover(file, Number(selectedEpisode), ep?.program_name || `第${selectedEpisode}期`) + message.success('题图上传成功') + onUploaded({ + cover_path: result.cover_path, + episode_number: Number(selectedEpisode), + episode_title: ep?.program_name || `第${selectedEpisode}期`, + }) + } catch (err) { + const errMsg = err?.response?.data?.detail || '上传失败,请重试' + message.error(errMsg) + } finally { + setUploading(false) + } + } + + const episodeOptions = episodes.map(ep => ({ + value: String(ep.episode_number), + label: `第 ${ep.episode_number} 期 · ${ep.program_name}`, + })) + + return ( + +
+ {/* 图片上传区域 */} +
+

上传海报图片(PNG/JPG/WEBP,不超过 5MB)

+ false} + onChange={handleFileChange} + showUploadList={false} + style={{ borderRadius: 12 }} + > + {previewUrl ? ( +
+ 预览 +

点击或拖拽更换图片

+
+ ) : ( + <> +

+ +

+

点击或拖拽图片到此处上传

+ + )} +
+
+ + {/* 关联期次下拉 */} +
+

关联期次

+