You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 18:05:53