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

React+Next.js:实现点击Publish按钮上传图片至Cloudinary并同步Supabase

解决方案

要实现点击Publish按钮才上传图片到Cloudinary并写入Supabase,需要调整代码逻辑,把图片选择和上传操作分离,具体修改步骤如下:

1. 调整状态变量

添加状态保存选中的文件,移除原来的imageData(无需提前存储图片URL):

// 替换原imageData状态
const [selectedFile, setSelectedFile] = useState<File | null>(null);

2. 修改图片选择逻辑

将原选图即上传的逻辑改为仅保存选中的文件到状态:

// 替换原uploadImage函数
const handleFileSelect = (e: any) => {
  if (!e.target.files || e.target.files.length === 0) {
    alert('请选择要上传的图片');
    return;
  }
  setSelectedFile(e.target.files[0]);
};

3. 重构addPost函数,整合上传与数据库写入

点击Publish时,先完成Cloudinary图片上传,拿到URL后再写入Supabase:

async function addPost() {
  try {
    setLoading(true);

    // 1. 上传图片到Cloudinary(若有选中文件)
    let imageUrl = '';
    if (selectedFile) {
      const data = new FormData();
      data.append("file", selectedFile);
      data.append("upload_preset", "c_tags");
      
      const res = await fetch(`${process.env.NEXT_PUBLIC_CLOUDINARY_API}`, {
        method: "POST",
        body: data,
      });
      
      if (!res.ok) throw new Error('图片上传失败');
      const file = await res.json();
      imageUrl = file.secure_url;
    }

    // 2. 写入Supabase数据库
    const updates = {
      title,
      description,
      slug,
      images: imageUrl,
      created_at: new Date().toISOString(),
      user_id: userDetails?.id
    };

    const { error } = await supabase.from('posts').upsert(updates);
    if (error) throw error;
    
    alert('发布成功!');
  } catch (error) {
    alert('数据更新失败!');
    console.error(error);
  } finally {
    setLoading(false);
  }
}

4. 调整表单元素事件绑定

修改文件输入框的onChange事件,绑定新的文件选择函数,同时简化按钮点击逻辑:

// 文件输入框修改
<input
  id="images"
  type="file"
  className="relative block w-full appearance-none rounded-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
  accept="image/*"
  onChange={handleFileSelect}
/>

// Publish按钮修改
<Button
  className="mt-5 bg-red-500"
  onClick={addPost}
  disabled={loading} // 加载时禁用按钮,防止重复提交
>
  {loading ? '发布中...' : 'Publish'}
</Button>

完整修改后的代码

'use client';

import { useState } from 'react'
import { redirect } from 'next/navigation';
import { useUser } from '@/contexts/AuthContext';
import { Database } from '@/db_types'
import Button from "@/components/shared/Button"
import Editor from "@/components/shared/Editor"

type Posts = Database['public']['Tables']['posts']['Row']

export default function AddPost() {
  const { userDetails, supabase } = useUser();
  const [loading, setLoading] = useState(false);
  const [title, setTitle] = useState<Posts['title']>(null);
  const [description, setDescription] = useState<Posts['description']>(null);
  const [slug, setSlug] = useState<Posts['slug']>(null);
  const [selectedFile, setSelectedFile] = useState<File | null>(null);

  if (!userDetails) {
    redirect('/login');
  }

  // 仅保存选中的文件,不上传
  const handleFileSelect = (e: any) => {
    if (!e.target.files || e.target.files.length === 0) {
      alert('请选择要上传的图片');
      return;
    }
    setSelectedFile(e.target.files[0]);
  };

  // 点击Publish时执行上传+写入数据库
  async function addPost() {
    try {
      setLoading(true);

      let imageUrl = '';
      if (selectedFile) {
        const data = new FormData();
        data.append("file", selectedFile);
        data.append("upload_preset", "c_tags");
        
        const res = await fetch(`${process.env.NEXT_PUBLIC_CLOUDINARY_API}`, {
          method: "POST",
          body: data,
        });
        
        if (!res.ok) throw new Error('图片上传失败');
        const file = await res.json();
        imageUrl = file.secure_url;
      }

      const updates = {
        title,
        description,
        slug,
        images: imageUrl,
        created_at: new Date().toISOString(),
        user_id: userDetails?.id
      };

      const { error } = await supabase.from('posts').upsert(updates);
      if (error) throw error;
      
      alert('发布成功!');
    } catch (error) {
      alert('数据更新失败!');
      console.error(error);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div className="">
      <div>
        <form
          className="mt-3"
          onSubmit={(e) => {
            e.preventDefault();
            addPost();
          }}
        >
          <input
            id="images"
            type="file"
            className="relative block w-full appearance-none rounded-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
            accept="image/*"
            onChange={handleFileSelect}
          />

          <label htmlFor="title">Title</label>
          <input
            id="title"
            type="text"
            className="relative block w-full appearance-none rounded-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
            value={title || ''}
            onChange={(e) => setTitle(e.target.value)}
          />

          <label htmlFor="Description">Content</label>
          <Editor
            description={description}
            setDescription={setDescription}
          />

          <label htmlFor="slug">Slug</label>
          <input
            id="slug"
            type="text"
            className="relative block w-full appearance-none rounded-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
            value={slug || ''}
            onChange={(e) => setSlug(e.target.value)}
          />

          <div>
            <Button
              className="mt-5 bg-red-500"
              onClick={addPost}
              disabled={loading}
            >
              {loading ? '发布中...' : 'Publish'}
            </Button>
          </div>
        </form>
      </div>
    </div>
  );
}

关键修改点说明

  • 拆分图片选择与上传逻辑,选图时仅保存文件状态,不触发上传
  • 将Cloudinary上传逻辑整合到addPost函数,实现点击Publish后完成全流程
  • 移除无用的imageData和user_id状态,简化代码结构
  • 增加按钮加载状态禁用,避免重复提交操作

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 02:57:16