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

使用FormData上传图片失败求助(附React+Redux代码)

问题:FormData上传图片失败

我尝试通过POST请求用FormData上传图片,但一直无法正常工作。

以下是Profile.jsx中的代码,功能是从本地选择图片后,点击提交按钮将文件发送至updateCurrentUserAvatar接口完成上传:

import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getCurrentUserAvatar, setImage, updateCurrentUserAvatar } from '~/features/superAdminSlice';
import Spinner from '~/layouts/components/Spinner';

const SuperAdminAccountProfile = () => {
    const dispatch = useDispatch();
    const userAvatar = useSelector((state) => state.superadmin.avatar);
    const [imageProfile, setImageProfile] = useState(userAvatar);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        dispatch(getCurrentUserAvatar());
        setImageProfile(userAvatar);
        setLoading(false);
    }, [dispatch, userAvatar]);

    const handleButtonClick = () => {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = 'image/*';
        input.style.opacity = 0;
        input.style.height = 0;
        input.onchange = (event) => {
            const file = event.target.files[0];
            const reader = new FileReader();
            reader.onload = () => {
                setImageProfile(reader.result);
            };
            reader.readAsDataURL(file);
            setImage(file);
        };
        document.body.appendChild(input);
        input.click();
        document.body.removeChild(input);
    };

    const handleSubmit = (event) => {
        event.preventDefault();
        if (imageProfile) {
            dispatch(updateCurrentUserAvatar(imageProfile));
        }
    };
    return (
        <div style={{ position: 'relative', width: '200px', height: '200px' }}>
            {loading ? (
                <Spinner />
            ) : (
                <>
                    <img src={imageProfile} alt="" style={{ width: '100%', height: '100%' }} />
                    <button
                        type="button"
                        style={{
                            position: 'absolute',
                            top: '50%',
                            left: '50%',
                            transform: 'translate(-50%, -50%)',
                            visibility: 'visible',
                        }}
                        onClick={handleButtonClick}
                    >
                        Change Image
                    </button>
                </>
            )}
            <button type="submit" onClick={handleSubmit}>
                Submit            
            </button>
        </div>
    );
};

export default SuperAdminAccountProfile;

以下是包含updateCurrentUserAvatar方法的Slice.js代码:

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from '~/api/axios';
import { getAdmins, register, updateStatus } from '~/services/superAdminService';

const initialState = {
    data: [],
    admins: [],
    isLocked: false,
    currentPage: 1,
    pageSize: 5,
    search: '',
    adminId: '',
    count: '',
    avatar: null,
};

export const getAdminData = createAsyncThunk('superadmin/adminList', async ({ currentPage, pageSize, search }) => {
    try {
        const response = await getAdmins(currentPage, pageSize, search);
        return response;
    } catch (error) {
        console.log(error);
    }
});

export const updateAdminStatus = createAsyncThunk('superadmin/updateData', async (adminId) => {
    try {
        const response = await updateStatus(adminId);
        return response;
    } catch (error) {
        console.log(error);
    }
});

export const createAdmin = createAsyncThunk('superadmin/createAdmin', async (payload) => {
    try {
        const response = await register({
            email: payload.email,
            fullname: payload.fullname,
            username: payload.username,
            password: payload.password,
            confirmPassword: payload.confirmPassword,
        });
        return response;
    } catch (error) {
        console.log(error);
    }
});

export const getCurrentUserAvatar = createAsyncThunk('superadmin/getAvatar', async () => {
    try {
        const response = await axios.get('User/avatar');
        return response.url;
    } catch (error) {
        console.log(error);
    }
});

export const updateCurrentUserAvatar = createAsyncThunk('superadmin/updateAvatar', async (avatar) => {
    try {
        const formData = new FormData();
        formData.append('avatar', avatar);
        const response = await axios.post('User/avatar', formData, {
            headers: { 'Content-type': 'multipart/form-data' },
        });
        console.log(response);
        return response;
    } catch (error) {
        console.log(error);
    }
});

export const superAdminSlice = createSlice({
    name: 'superadmin',
    initialState,
    reducers: {
        setPage: (state, action) => {
            state.currentPage = action.payload;
        },
        setSearch: (state, action) => {
            state.search = action.payload;
            state.currentPage = 1;
        },
        setLock: (state, action) => {
            state.isLocked = action.payload;
        },
        setImage: (state, action) => {
            console.log(action);
            state.avatar = action.payload;
        },
    },
    extraReducers: (builder) => {
        builder
            .addCase(getAdminData.fulfilled, (state, action) => {
                state.data = action.payload.data;
                state.count = action.payload.count;
            })
            .addCase(updateAdminStatus.fulfilled, (state, action) => {
                state.adminId = action.payload.adminId;
                state.isLocked = action.payload.isLocked;
            })
            .addCase(createAdmin.fulfilled, (state, action) => {
                state.admins.push(action.payload);
            })
            .addCase(getCurrentUserAvatar.fulfilled, (state, action) => {
                state.avatar = action.payload;
            })
            .addCase(updateCurrentUserAvatar.fulfilled, (state, action) => {
                state.avatar = action.payload;
            });
    },
});

export const { setPage, setSearch, setLock, setImage } = superAdminSlice.actions;

export default superAdminSlice.reducer;
问题分析与修复

核心问题是上传时传递的是DataURL字符串,而非实际的File对象:

  1. handleSubmit中传给updateCurrentUserAvatar的imageProfile是FileReader.readAsDataURL生成的base64字符串,不是用户选择的File文件。
  2. 虽然用setImage(file)把File对象存在了redux状态,但提交时并未使用该值。

修复步骤:

1. 修改Profile.jsx的提交逻辑

直接使用redux中存储的File对象提交:

const handleSubmit = (event) => {
    event.preventDefault();
    const file = useSelector((state) => state.superadmin.avatar);
    if (file) {
        dispatch(updateCurrentUserAvatar(file));
    }
};

或者在组件内维护File状态,更直观:

// 新增组件内状态
const [selectedFile, setSelectedFile] = useState(null);

// 修改handleButtonClick
const handleButtonClick = () => {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = 'image/*';
    input.style.opacity = 0;
    input.style.height = 0;
    input.onchange = (event) => {
        const file = event.target.files[0];
        const reader = new FileReader();
        reader.onload = () => {
            setImageProfile(reader.result);
        };
        reader.readAsDataURL(file);
        setSelectedFile(file); // 存储File对象
    };
    document.body.appendChild(input);
    input.click();
    document.body.removeChild(input);
};

// 修改提交方法
const handleSubmit = (event) => {
    event.preventDefault();
    if (selectedFile) {
        dispatch(updateCurrentUserAvatar(selectedFile));
    }
};

2. 优化axios请求(推荐)

传递FormData时,无需手动设置Content-type: multipart/form-data,axios会自动生成包含boundary的正确请求头,手动设置反而可能导致格式错误:

export const updateCurrentUserAvatar = createAsyncThunk('superadmin/updateAvatar', async (avatar) => {
    try {
        const formData = new FormData();
        formData.append('avatar', avatar, avatar.name); // 可选:指定文件名
        const response = await axios.post('User/avatar', formData);
        console.log(response);
        // 后端若返回图片URL,用该URL更新状态
        return response.data.url;
    } catch (error) {
        console.log(error);
    }
});

3. 修正useEffect逻辑

原逻辑可能在请求完成前执行setImageProfile,导致初始值为null,拆分后更可靠:

useEffect(() => {
    const fetchAvatar = async () => {
        await dispatch(getCurrentUserAvatar());
        setLoading(false);
    };
    fetchAvatar();
}, [dispatch]);

// 监听userAvatar变化自动更新预览图
useEffect(() => {
    setImageProfile(userAvatar);
}, [userAvatar]);

内容的提问来源于stack exchange,提问作者Quang Phú Nguyễn Hoàng

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 12:59:13