React+Ant Design作业项文件独立展示与提交问题修复
解决React作业项目中的文件上传与提交隔离问题
问题描述
- 为单个作业项(
<li>标签)上传文件时,文件会在所有作业项的.showFile区域显示 - 点击单个作业项的发送按钮,所有作业项都会执行提交流程
问题根源
当前实现使用了全局共享的filesArray和loading状态,所有作业项共用同一组状态数据,导致无法区分不同作业项的文件和提交状态。
解决方案
1. 重构状态结构,实现作业项状态隔离
将全局的文件列表和加载状态改为按作业项ID映射的对象,每个作业项拥有独立的状态:
// 原全局状态 // const [filesArray, setFilesArray] = useState([]); // const [loading, setLoading] = useState(false); // 修改为按作业项ID隔离的状态 const [homeworkFiles, setHomeworkFiles] = useState({}); // 结构:{ [homeworkId]: 文件数组 } const [homeworkLoading, setHomeworkLoading] = useState({}); // 结构:{ [homeworkId]: 加载状态 }
2. 调整文件上传逻辑,关联对应作业项
移除selectedHomeworkId,直接在上传组件中传递当前作业项ID,确保文件关联到正确的作业项:
// 修改文件上传回调,接收作业项ID参数 const onFileChange = (selectedFile, homeworkId) => { getBase64(selectedFile.file.originFileObj, selectedFile.file.name, homeworkId); }; // 更新文件处理逻辑,仅修改对应作业项的文件列表 const getBase64 = (blob, filename, homeworkId) => { let reader = new FileReader(); reader.readAsDataURL(blob); reader.onload = function () { const file = DataURIToBlob(reader.result); setHomeworkFiles(prev => { const currentFiles = prev[homeworkId] || []; // 检查文件是否已存在,避免重复添加 const hasDuplicate = currentFiles.some(f => f.file?.filename === filename); if (hasDuplicate) return prev; return { ...prev, [homeworkId]: [...currentFiles, { homeworkId, file: { filename, data: file } }] }; }); }; reader.onerror = function (error) {}; };
3. 修改发送逻辑,仅处理当前作业项
让sendFiles接收作业项ID,只提交该作业项的文件,并更新对应作业项的加载状态:
const sendFiles = async (homeworkId) => { setHomeworkLoading(prev => ({ ...prev, [homeworkId]: true })); let requestStatus = "success"; const formData = new FormData(); const currentFiles = homeworkFiles[homeworkId] || []; for (let perFile of currentFiles) { const fileExtension = perFile.file.filename.split(".").pop(); if (["mp3", "aac", "amr", "wma"].includes(fileExtension)) { formData.append("audio_file", perFile.file.data, perFile.file.filename); } else if (["jpg", "jpeg", "png", "pdf", "doc", "docx", "mp4", "avi"].includes(fileExtension)) { formData.append("file", perFile.file.data, perFile.file.filename); } } const res = await AnswerServices.addAnswer( formData, studentId, homeworkId, { baseURL: `${PROTOCOL}${BASE_URL}api/v2/parent/` }, { "Content-Type": "multipart/form-data" }, (err) => { if (err.response?.data.message === "The assignment delivery date has been completed.") { setHomeworkLoading(prev => ({ ...prev, [homeworkId]: false })); message.warning(strings.fa.endDeliveryData); } } ); if (!res) { requestStatus = "failed"; } if (requestStatus === "success") { setHomeworkFiles(prev => ({ ...prev, [homeworkId]: [] })); setHomeworkLoading(prev => ({ ...prev, [homeworkId]: false })); message.success("文件已成功发送"); } };
4. 调整文件展示与删除逻辑,仅显示当前作业项的文件
渲染时过滤出当前作业项的文件,删除操作也仅针对该作业项的文件数组:
// 修改文件删除逻辑,接收作业项ID和索引 const removeFile = (homeworkId, index) => { setHomeworkFiles(prev => { const currentFiles = prev[homeworkId] || []; const newFiles = [...currentFiles]; newFiles.splice(index, 1); return { ...prev, [homeworkId]: newFiles }; }); }; // 渲染部分对应修改: // Upload组件传递当前作业项ID <Upload showUploadList={false} {...propss} onChange={(info) => onFileChange(info, id)} > <Button icon={<UploadOutlined />}>选择文件</Button> </Upload> // 发送按钮绑定当前作业项的发送逻辑 <Button className="send-homework-btn" loading={homeworkLoading[id] || false} disabled={(homeworkFiles[id]?.length || 0) === 0} onClick={() => sendFiles(id)} > <SendOutlined /> 发送 </Button> // 文件展示区域仅渲染当前作业项的文件 <Col xs={24} xl={24} className="showFile"> <br /> {(homeworkFiles[id] || []).length !== 0 ? (homeworkFiles[id] || []).map((i, index) => { if(i?.file.filename){ let filenameLength = i.file.filename.length; return ( <div key={index} style={{ marginBottom: " 0.5rem" }}> <List style={{ textAlign: "right" }}> {filenameLength === 37 && i.file.filename.slice(-1) === "k" ? `语音文件 ${persianToEnglishNum((index + 1).toString(), true)}` : i.file.filename} <a onClick={() => removeFile(id, index)} style={{ float: "left", color: "red", cursor: "pointer", }} > <DeleteOutlined style={{ fontSize: "20px" }} /> </a> </List> </div> ); } return null; }) : ""} </Col>
修改后的完整代码
import React, { useEffect, useState } from "react"; import { Button, Upload, Col, List } from "antd"; import { UploadOutlined, SendOutlined, DeleteOutlined } from "@ant-design/icons"; import { persianToEnglishNum } from "../../../helpers"; import "./styles.css"; import { AnswerServices, StudentsServices } from "../../../api/services"; import { strings } from "../../../common"; import { message } from "antd"; import { BASE_URL, PROTOCOL } from "../../../api/api"; import { useRedux } from "../../../hooks"; const HomeworkBox = () => { const [allHomeworks, setAllHomeworks] = useState([]); const [homeworkFiles, setHomeworkFiles] = useState({}); const [homeworkLoading, setHomeworkLoading] = useState({}); const { Get } = useRedux(); const { studentId } = Get().user; const getTodayHomeworks = async () => { await StudentsServices.getTodayHomeworks(studentId).then((res) => { if (res) { setAllHomeworks(res); } }); }; useEffect(() => { getTodayHomeworks(); }, []) const badRequestRequest = (err, homeworkId) => { if ( err.response && err.response.data.message === "The assignment delivery date has been completed." ) { setHomeworkLoading(prev => ({ ...prev, [homeworkId]: false })); message.warning(strings.fa.endDeliveryData); } }; function DataURIToBlob(dataURI) { const splitDataURI = dataURI.split(","); const byteString = splitDataURI[0].indexOf("base64") >= 0 ? atob(splitDataURI[1]) : decodeURI(splitDataURI[1]); const mimeString = splitDataURI[0].split(":")[1].split(";")[0]; const ia = new Uint8Array(byteString.length); for (let i = 0; i < byteString.length; i++) ia[i] = byteString.charCodeAt(i); return new Blob([ia], { type: mimeString }); } const getBase64 = (blob, filename, homeworkId) => { let reader = new FileReader(); reader.readAsDataURL(blob); reader.onload = function () { const file = DataURIToBlob(reader.result); setHomeworkFiles(prev => { const currentFiles = prev[homeworkId] || []; const hasDuplicate = currentFiles.some(f => f.file?.filename === filename); if (hasDuplicate) return prev; return { ...prev, [homeworkId]: [...currentFiles, { homeworkId, file: { filename, data: file } }] }; }); }; reader.onerror = function (error) {}; }; const onFileChange = (selectedFile, homeworkId) => { getBase64(selectedFile.file.originFileObj, selectedFile.file.name, homeworkId); }; const sendFiles = async (homeworkId) => { setHomeworkLoading(prev => ({ ...prev, [homeworkId]: true })); let requestStatus = "success"; const formData = new FormData(); const currentFiles = homeworkFiles[homeworkId] || []; for (let perFile of currentFiles) { const fileExtension = perFile.file.filename.split(".").pop(); if ( fileExtension === "mp3" || fileExtension === "aac" || fileExtension === "amr" || fileExtension === "wma" ) { formData.append( "audio_file", perFile.file.data, perFile.file.filename ); } else if ( fileExtension === "jpg" || fileExtension === "jpeg" || fileExtension === "png" || fileExtension === "pdf" || fileExtension === "doc" || fileExtension === "docx" || fileExtension === "mp4" || fileExtension === "avi" ) { formData.append( "file", perFile.file.data, perFile.file.filename ); } } const res = await AnswerServices.addAnswer( formData, studentId, homeworkId, { baseURL: `${PROTOCOL}${BASE_URL}api/v2/parent/` }, { "Content-Type": "multipart/form-data" }, (err) => badRequestRequest(err, homeworkId) ); if (!res) { requestStatus = "failed"; } if (requestStatus === "success") { setHomeworkFiles(prev => ({ ...prev, [homeworkId]: [] })); setHomeworkLoading(prev => ({ ...prev, [homeworkId]: false })); message.success("文件已成功发送"); } }; const removeFile = (homeworkId, index) => { setHomeworkFiles(prev => { const currentFiles = prev[homeworkId] || []; const newFiles = [...currentFiles]; newFiles.splice(index, 1); return { ...prev, [homeworkId]: newFiles }; }); }; const propss = { name: "file", progress: { strokeColor: { "0%": "#108ee9", "100%": "#87d068", }, strokeWidth: 3, format: (percent) => `${parseFloat(percent.toFixed(2))}%`, }, }; return ( <div className="homework-container"> <ul className="homeworks-list"> {allHomeworks && allHomeworks.length !== 0 && allHomeworks.map((item) => { let { id, course, homework, homework_delivery_on, homework_files } = item; return ( <li key={id}> <h3 className="course">{course.str}</h3> <p className="homework-send"> <span style={{ fontWeight: "bold" }}>用户作业</span>:" {homework} " </p> <p className="homework-send"> <span style={{ fontWeight: "bold" }}> "交付日期" </span> : {homework_delivery_on} " </p> <p className="homework-attachments"> {homework_files && homework_files.length !== 0 && ( <> <span style={{ fontWeight: "bold" }}>附件:</span> <br /> </> )} <div className="attachment-btns"> {homework_files && homework_files.length !== 0 && homework_files.map((file, index) => { let idx = index + 1; let num = persianToEnglishNum(idx.toString(), true); return ( <Button key={index}> <a href={file.file} target="_blank" rel="noreferrer" > {`我的文件 ${num}`} </a> </Button> ); })} </div> </p> <div className="upload-files-homework"> <Upload showUploadList={false} {...propss} onChange={(info) => onFileChange(info, id)} > <Button icon={<UploadOutlined />}>选择文件</Button> </Upload> <Button className="send-homework-btn" loading={homeworkLoading[id] || false} disabled={(homeworkFiles[id]?.length || 0) === 0} onClick={() => sendFiles(id)} > <SendOutlined /> 发送 </Button> </div> <Col xs={24} xl={24} className="showFile"> <br /> {(homeworkFiles[id] || []).length !== 0 ? (homeworkFiles[id] || []).map((i, index) => { if(i?.file.filename){ let filenameLength = i.file.filename.length; return ( <div key={index} style={{ marginBottom: " 0.5rem" }}> <List style={{ textAlign: "right" }}> {filenameLength === 37 && i.file.filename.slice(-1) === "k" ? `语音文件 ${persianToEnglishNum((index + 1).toString(), true)}` : i.file.filename} <a onClick={() => removeFile(id, index)} style={{ float: "left", color: "red", cursor: "pointer", }} > <DeleteOutlined style={{ fontSize: "20px" }} /> </a> </List> </div> ); } return null; }) : ""} </Col> </li> ); })} </ul> </div> ); }; export default HomeworkBox;
内容的提问来源于stack exchange,提问作者h.m.p.frontendDeveloper
相关产品推荐
相关产品推荐

