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

ReactJS应用在iOS设备上无法上传图片问题求助

ReactJS 图片上传组件在 iOS 设备上失效的排查与修复

我开发的 ReactJS 应用中,图片上传功能在电脑端和 Android 设备上正常,但在 iPhone 设备上无法完成上传。以下是上传组件的代码及针对性排查修复方案:

可能的问题点及修复方案

1. iOS Safari 对 FormData 的处理差异

iOS Safari 在处理 FormData 时,若未显式指定文件名,可能导致上传失败。需要在追加文件时明确传递文件名参数。

2. Fetch 请求的 Headers 冲突

手动设置 Content-Type: multipart/form-data 会覆盖浏览器自动生成的边界标识,导致服务器无法解析表单数据。需移除手动设置的该 Header,由浏览器自动处理。

3. 错误处理不完整

原代码仅捕获网络错误,未处理 HTTP 状态码异常(如 400、500),且上传失败后未重置加载状态,用户无法重新操作。

4. 点击事件兼容性

iOS 上的非链接/按钮元素点击可能存在触发问题,需给可点击区域添加 cursor: pointer 样式确保可交互。

修改后的完整代码

"use client"
import Link from "next/link";
import React, { useRef, useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { set, get } from "local-storage";

const Page = () => {
  const { data: session } = useSession();

  const loadPhotosFromLocalStorage = () => {
    const uploadsData = JSON.parse(get("uploads")) || [null, null, null];
    const loadedPhotos = uploadsData.map((data, areaIndex) => {
      if (data !== null) {
        return `https://changebazar.chbk.run/api/changebazar/photo/view/${data[0]?.fileName || ""}`;
      }
      return null;
    });

    setPhotos(loadedPhotos);
    setLocalStorageFake(uploadsData);
  };

  const [photos, setPhotos] = useState([null, null, null]);
  const [isLoading, setIsLoading] = useState([false, false, false]);
  const [localStorageFake, setLocalStorageFake] = useState([null, null, null]);

  const handleDrop = (areaIndex, e) => {
    e.preventDefault();
    const file = e.dataTransfer.files[0];

    if (!file) {
      console.error("No file dropped.");
      return;
    }

    uploadPhoto(areaIndex, file);
  };

  const handleFileChange = (areaIndex, e) => {
    const file = e.target.files[0];
    if (file) {
      uploadPhoto(areaIndex, file);
    }
  };

  const uploadPhoto = (areaIndex, file) => {
    setIsLoading((prevLoading) => {
      const newLoading = [...prevLoading];
      newLoading[areaIndex] = true;
      return newLoading;
    });

    const formData = new FormData();
    // 显式指定文件名,兼容iOS Safari
    formData.append("file", file, file.name);

    fetch("https://changebazar.chbk.run/api/changebazar/photo", {
      method: "POST",
      body: formData,
      headers: {
        "x-auth-token": session?.authToken || "",
        // 移除手动设置的Content-Type,由浏览器自动生成正确的边界标识
      },
    })
      .then((res) => {
        // 检查HTTP状态码是否正常
        if (!res.ok) {
          throw new Error(`上传失败,状态码:${res.status}`);
        }
        return res.json();
      })
      .then((data) => {
        setIsLoading((prevLoading) => {
          const newLoading = [...prevLoading];
          newLoading[areaIndex] = false;
          return newLoading;
        });
        const obj = [{ id: data.result.id, fileName: data.result.fileName }];

        const tempLocalStorageFake = [...localStorageFake];
        tempLocalStorageFake[areaIndex] = obj;
        setLocalStorageFake(tempLocalStorageFake);

        setPhotos((prevPhotos) => {
          const newPhotos = [...prevPhotos];
          newPhotos[
            areaIndex
          ] = `https://changebazar.chbk.run/api/changebazar/photo/view/${data.result.fileName}`;
          return newPhotos;
        });

        const updatedLocalStorage = JSON.parse(localStorage.getItem("uploads")) || [null, null, null];
        updatedLocalStorage[areaIndex] = obj;
        localStorage.setItem("uploads", JSON.stringify(updatedLocalStorage));
      })
      .catch((err) => {
        console.error("上传失败:", err);
        // 上传失败后重置加载状态
        setIsLoading((prevLoading) => {
          const newLoading = [...prevLoading];
          newLoading[areaIndex] = false;
          return newLoading;
        });
      });
  };

  const fileInputRefs = [useRef(null), useRef(null), useRef(null)];

  const handleClick = (areaIndex) => {
    fileInputRefs[areaIndex].current.click();
  };

  useEffect(() => {
    loadPhotosFromLocalStorage();
  }, []);

  return (
    <div className="row justify-content-center">
      <div className="col-md-6">
        <div className="categories">
          <div className="uploaderWrapper">
            {[0, 1, 2].map((areaIndex) => (
              <div key={areaIndex}>
                <div
                  className="photoBox isLoading"
                  onDrop={(e) => handleDrop(areaIndex, e)}
                  onDragOver={(e) => e.preventDefault()}
                  onDragLeave={(e) => e.preventDefault()}
                >
                  <div
                    className={
                      "item dropzone" +
                      (isLoading[areaIndex] ? " isLoading" : "")
                    }
                    onClick={() => handleClick(areaIndex)}
                    style={{ cursor: 'pointer' }} // 添加cursor样式,确保iOS可点击
                  >
                    <input
                      type="file"
                      hidden
                      ref={fileInputRefs[areaIndex]}
                      onChange={(e) => handleFileChange(areaIndex, e)}
                      accept="image/*" // 限制选择图片类型,提升用户体验
                    />
                    <div className="loader">
                      <div
                        className={
                          "lds-ring" + (isLoading[areaIndex] ? " active" : "")
                        }
                      >
                        <div></div>
                        <div></div>
                        <div></div>
                        <div></div>
                      </div>
                    </div>
                    <div className={photos[areaIndex] ? "hide" : "show"}>
                      <i className="icon-camera"></i>
                      <h6>انتخاب عکس</h6>
                    </div>
                    <div className={photos[areaIndex] ? "show" : "hide"}>
                      <img src={photos[areaIndex]} alt="" />
                    </div>
                  </div>
                </div>
              </div>
            ))}
          </div>

          <div className="actions">
            <Link href={{ pathname: "/product/new/" }}>
              <button className="prev">مرحله قبل</button>
            </Link>
            <Link href={{ pathname: "/product/new/description" }}>
              <button className=" next">مرحله بعد</button>
            </Link>
          </div>
        </div>
      </div>
    </div>
  );
};

export default Page;

额外检查项

  • 确认服务器端支持 iOS 常用的图片格式(如 HEIC),若不支持,需在前端将图片转换为 JPG/PNG 格式后上传。
  • 检查应用是否获得 iOS 相册访问权限,无权限会导致无法选择图片。
  • 确保网站使用 HTTPS 协议,iOS 会拦截非 HTTPS 的网络请求。

内容的提问来源于stack exchange,提问作者Arman Bagheri

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 16:54:56