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
相关产品推荐
相关产品推荐

