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

Next.js添加保存逻辑后Input的onChange事件未触发问题

问题描述

在Next.js项目中,添加文本保存逻辑后,Input的onChange事件不再触发;注释掉保存逻辑后功能恢复正常。当前需要实现区块文本保存与图片上传功能:文本可通过handleClickOutside函数正常保存,但图片选择后无法显示,控制台排查发现handleImageUpload函数未被触发(对应代码:<input type="file" className='hidden' onChange={handleImageUpload} ></input>)。

相关代码

图片上传代码

import {useState,useEffect,useRef} from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {faCamera} from '@fortawesome/free-solid-svg-icons'

export  function imageUpload(defaultLanguageCode,index,sectionsArray,setImageData){
    const selectedImage=useRef(null);
    const selectableImage=useRef([]);
   
 

    console.log(defaultLanguageCode);

  
   
    function showUploadImage(event){
        event.stopPropagation(); // Stop event propagation
        selectedImage.current=event.target.parentElement.parentElement.children[1];
        event.target.parentElement.parentElement.children[1].click();
        
    }
    function handleImageUpload(event){
   
      console.log(event.target.files[0])
        selectedImage.current.src=URL.createObjectURL(event.target.files[0]);
   
        console.log(event.target.files[0].name)
        const image=event.target.files[0].name;
        setImageData(image);

        
       // const apiUrl = "http://wimdrive-wimapp-service.wimsolutions.net/api/website/wbsWebsiteSection/data/"+(sectionsArray[index]).id+"?key=image&value=" + image + "&code="+defaultLanguageCode;

        //fetch(apiUrl, {
          //method: "PUT",
          //headers: {
            //"Content-Type": "application/json",
            // Add any other headers if needed
         // },
          // You can also provide a request body if needed
          //body: JSON.stringify({}),
        //})
          //.then(response => response.json())
          //.then(data => {
            // Handle API response if needed
            //console.log("API response:", data);
          //})
          //.catch(error => {
            // Handle API error
            //console.error("API error:", error);
          //});
        //  console.log("uploaded succcc")
      }
   
    //console.log("yes",imageData)

    
    

     function handleMouseLeave(event){
        if(event.target.children[0])
        event.target.children[0].classList.add('invisible');
      
     }
     function handleMouseEnter(event){
        if(event.target.children[0])
        event.target.children[0].classList.remove('invisible');
     }
     function showUploadButton(){
        selectableImage.current.forEach((element)=>{
            if(element!=null){
                element.addEventListener('mouseenter',handleMouseEnter);
                element.addEventListener('mouseleave',handleMouseLeave);
            }
        })
     }

    function UploadComponent(){
        return(
            <div className="edit-image absolute bg-transparent  mt-1 rounded w-full h-full " 
            onClick={showUploadImage}
            onMouseEnter={handleMouseEnter}
            onMouseLeave={handleMouseLeave}
            >
                <FontAwesomeIcon icon={faCamera} className='pe-3 invisible' style={{fontSize:"40px",color:"black"}} />
                <input  type="file" className='hidden' onChange={handleImageUpload} ></input>
            </div>
        )
    }
    return {
        UploadComponent,
        selectableImage,
        selectedImage,
    }
}

编辑页面元素保存代码

import { imageUpload } from "/helpers/imageUpload";
import { useEffect, useRef, useState } from "react";

export const useEditableElements = (sectionsArray,index,defaultLanguageCode, sectData,sectionIndex, siteInfos, imageData) => {
  const selectableElement = useRef({});
  
  const [editorPosition, setEditorPosition] = useState({ top: 0, left: 0 });
  const [isEditorVisible, setIsEditorVisible] = useState(false);
  const selectedElement = useRef(null);
  const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
  const [isMouseDown, setIsMouseDown] = useState(false);
  const [data, setData] = useState(false);
  
  const editorRef = useRef(null); //   EDITOR RELATED


  useEffect(() => {
    
    
  }, [defaultLanguageCode,sectData]);
  
 
  
 
  useEffect(() => {
    
    if (selectableElement) {
      
      Object.entries(selectableElement.current).forEach(([name, element]) => {
        element.addEventListener("click", handleElementClick);
        element.contentEditable = true;
        element.classList.add("editable-element");
      });
      
    }
  }, [index]);

  function handleElementClick(event) {
    if (
      event.target.classList.contains("editable-element") &&
      (selectedElement.current == null ||
        selectedElement.current != event.target)
    ) {
      setEditorPosition({
        x: event.currentTarget.getBoundingClientRect().left,
        y:
          event.currentTarget.getBoundingClientRect().top -
          event.currentTarget.getBoundingClientRect().height,
      });
      setIsEditorVisible(true);
      selectedElement.current = event.target;
    }
  }

  useEffect(() => {
    document.addEventListener("click", handleClickOutside);

    return () => {
      document.removeEventListener("click", handleClickOutside);
    };
  });

  function saveData(newData) {
    // Add the imageData to the newData object
    const newDataWithImage = { ...newData, image: imageData };

    setData(newDataWithImage);

    const a = {
      ...siteInfos,
      sectionIndex,
      section: {
        ...sectData,
        data: newDataWithImage,
      },
    };

    console.log("newDataWithImage:", newDataWithImage);
    console.log(defaultLanguageCode);

     if (sectData !== undefined && sectionsArray !== undefined && sectionsArray[index] !== undefined){
     const apiUrl = ("http://wimdrive-wimapp-service.wimsolutions.net/api/website/wbsWebsiteSection/updateData/"+(sectionsArray[index]).id+"?code="+defaultLanguageCode);
     console.log("apiUrl:", apiUrl);
    // PUT request headers
    const headers = {
      "Content-Type": "application/json", // Set the Content-Type to JSON
    };

    // Convert newData to JSON format
    const requestBody = JSON.stringify(newDataWithImage);

    // Perform the PUT request
    fetch(apiUrl, {
      method: "PUT",
      headers: headers,
      body: requestBody,
    })
      .then(response => {
        if (!response.ok) {
          throw new Error("Network response was not ok.");
        }
        return response.json();
      })
      .then(data => {
        // The API response data will be available here
        console.log("API response:", data);
      })
      .catch(error => {
        // Handle any errors that occurred during the API request
        console.error("Error:", error);
      });
    }
   
   }
console.log(sectData)
function handleClickOutside(event) {
  event.stopPropagation();
  if (
    !event.target.classList.contains("editable-element") &&
    !event.target.closest("#my-editor")
  ) {
    selectedElement.current = null;
   
   
     const isDataChanged = Object.entries(selectableElement.current).some(
       ([name, element]) => {
        
        if (sectData!== undefined) {
         return element.innerText !== sectData[name];

         }
         
         return false;
       }
     );

     if (isDataChanged) {
       const newData = Object.entries(selectableElement.current).reduce(
         (acc, [name, element]) => {
           acc[name] = element.innerHTML;
           return acc;
         },
         {}
       );

       saveData(newData);
     }
  }
}


  const handleMouseDown = (event) => {
    setIsMouseDown(true);
  };

  const handleMouseMove = (event) => {
    if (isMouseDown) {
      const { top, left, width, height } = 
        editorRef.current.getBoundingClientRect();
      setElementPosition({
        x: event.clientX - width / 2 + "px",
        y: event.clientY - height / 2 + "px",
      });
    }
  };

  const handleMouseUp = (event) => {
    setIsMouseDown(false);
  };

  const resetSelector = () => {
    selectedElement.current = null;
  };

  return {
    selectableElement,
    handleElementClick,
    isEditorVisible,
    editorPosition,
    setIsEditorVisible,
    selectedElement,
    resetSelector,

  };
};

布局代码

return (
    <section ref={mySection} class="relative md:py-24 py-16">
    <div class="container">
        <div class="grid md:grid-cols-12 grid-cols-1 items-center gap-[30px]">
       
            <div  class="lg:col-span-5 md:col-span-6 editable-image-container">
      
            <UploadComponent  />
                <img src= {sectData.image}   class="rounded-md shadow dark:shadow-gray-800" alt="defaultimage"/>
                    
            </div>

            <div class="lg:col-span-7 md:col-span-6">
                <div class="ms-6">
                    
                
                    <h4  ref={(ref) => (selectableElement.current["title"] = ref)} class="my-6 md:text-3xl text-2xl md:leading-normal leading-normal font-semibold">{sectData.title}</h4>

                    <p ref={(ref) => (selectableElement.current["paragraph"] = ref)} class="text-slate-400">{sectData.paragraph}.</p>
                
                    
                </div>
            </div>
        </div>
    </div>
  )

解决方案

1. 修复文件Input onChange事件不触发问题

全局绑定的handleClickOutside存在两个问题:无依赖数组导致重复绑定事件,以及事件处理逻辑干扰了文件选择后的事件传递。

修改useEditableElements中的事件监听逻辑:

// 原代码
useEffect(() => {
  document.addEventListener("click", handleClickOutside);
  return () => document.removeEventListener("click", handleClickOutside);
});

// 修改后:添加空依赖数组,仅绑定一次
useEffect(() => {
  document.addEventListener("click", handleClickOutside);
  return () => document.removeEventListener("click", handleClickOutside);
}, []);

同时更新handleClickOutside,排除文件上传相关元素:

function handleClickOutside(event) {
  // 跳过文件上传相关元素的点击事件
  if (event.target.type === 'file' || event.target.closest('.edit-image')) {
    return;
  }

  if (
    !event.target.classList.contains("editable-element") &&
    !event.target.closest("#my-editor")
  ) {
    selectedElement.current = null;
   
    const isDataChanged = Object.entries(selectableElement.current).some(
      ([name, element]) => {
        if (sectData!== undefined) {
          return element.innerText !== sectData[name];
        }
        return false;
      }
    );

    if (isDataChanged) {
      const newData = Object.entries(selectableElement.current).reduce(
        (acc, [name, element]) => {
          acc[name] = element.innerHTML;
          return acc;
        },
        {}
      );

      saveData(newData);
    }
  }
}

2. 修复handleImageUpload未触发问题

原代码通过DOM遍历获取input元素的方式极易出错,改用ref直接引用:

修改imageUpload函数:

export function imageUpload(defaultLanguageCode, index, sectionsArray, setImageData) {
  const selectedImage = useRef(null);
  const selectableImage = useRef([]);
  const fileInputRef = useRef(null); // 新增文件input的ref

  function showUploadImage(event) {
    event.stopPropagation();
    // 直接通过父容器获取img元素
    selectedImage.current = event.currentTarget.parentElement.querySelector('img');
    // 通过ref触发文件选择框点击
    fileInputRef.current?.click();
  }

  // ... 其他函数保持不变

  function UploadComponent() {
    return (
      <div className="edit-image absolute bg-transparent mt-1 rounded w-full h-full " 
        onClick={showUploadImage}
        onMouseEnter={handleMouseEnter}
        onMouseLeave={handleMouseLeave}
      >
        <FontAwesomeIcon icon={faCamera} className='pe-3 invisible' style={{fontSize:"40px",color:"black"}} />
        {/* 绑定ref到input */}
        <input ref={fileInputRef} type="file" className='hidden' onChange={handleImageUpload} />
      </div>
    )
  }

  return {
    UploadComponent,
    selectableImage,
    selectedImage,
  }
}

3. 优化事件重复绑定问题

原代码在index变化时会重复给元素添加点击事件,添加清理逻辑避免重复绑定:

useEffect(() => {
  if (selectableElement.current) {
    // 先移除旧事件
    Object.entries(selectableElement.current).forEach(([name, element]) => {
      element.removeEventListener("click", handleElementClick);
    });
    // 再添加新事件
    Object.entries(selectableElement.current).forEach(([name, element]) => {
      element.addEventListener("click", handleElementClick);
      element.contentEditable = true;
      element.classList.add("editable-element");
    });
  }

  // 清理函数:组件卸载或index变化时移除事件
  return () => {
    if (selectableElement.current) {
      Object.entries(selectableElement.current).forEach(([name, element]) => {
        element.removeEventListener("click", handleElementClick);
      });
    }
  };
}, [index]);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 00:29:52