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

Next.js中使用Zod与React Hook Form上传图片至Supabase Storage并存储公共URL到表单的问题求助

Next.js中使用Zod与React Hook Form上传图片至Supabase Storage并存储公共URL到表单的问题求助

大家好,我最近在开发一个Next.js图书管理应用,遇到了图片上传相关的问题,折腾了好久没搞定,想请社区的朋友们帮忙看看。

我的需求

用户通过表单添加图书时,需要上传封面图到Supabase的book-pics存储桶,然后把图片的公共URL存入图书表的bookImageUrl字段,最后和其他表单数据一起提交保存。

我已经做的工作

  • 用React Hook Form + Zod处理表单验证和状态
  • 配置了Supabase Storage的book-pics桶
  • 写了单独的UploadBookImage.tsx组件处理图片上传
  • 表单提交逻辑已经对接了Supabase数据库的插入接口

遇到的问题

现在图片根本传不到book-pics桶里,表单提交时bookImageUrl字段也没有正确的URL值,导致数据库里这个字段是空的。


我的代码片段

UploadBookImage.tsx(图片上传组件)

import { createClient } from "../../../../../utils/supabase/client";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { useState } from "react";
export default function UploadBookImage({
  onUpload,
}: {
  size: number;
  url: string | null;
  onUpload: (url: string) => void;
}) {
  const supabase = createClient();
  const [uploading, setUploading] = useState(false);

  const uploadAvatar: React.ChangeEventHandler<HTMLInputElement> = async (
    event
  ) => {
    try {
      setUploading(true);

      if (!event.target.files || event.target.files.length === 0) {
        throw new Error("You must select an image to upload.");
      }

      const file = event.target.files[0];
      const fileExt = file.name.split(".").pop();
      const filePath = `books/${Date.now()}.${fileExt}`;

      const { error: uploadError } = await supabase.storage
        .from("avatars")
        .upload(filePath, file);

      if (uploadError) {
        throw uploadError;
      }

      onUpload(filePath);
    } catch (error) {
      alert(`Error uploading avatar! ${error}`);
    } finally {
      setUploading(false);
    }
  };

  return (
    <div>
      <div className="grid w-full max-w-sm items-center gap-1.5">
        <Label htmlFor="picture">
          {uploading ? "Uploading ..." : "Upload"}
        </Label>
        <Input
          id="picture"
          type="file"
          accept="image/**"
          onChange={uploadAvatar}
          disabled={uploading}
          name="bookImageUrl"
        />
      </div>
    </div>
  );
}

表单组件BookForm.tsx

const BookForm: React.FC<BookFormProps> = ({ authors }) => {
      const [state, action, pending] = useActionState(addBook, undefined);
      const [bookImageUrl, setBookImageUrl] = useState<string | null>(null);
    
  // React Hook Form with default values
  const form = useForm<BookInferSchema>({
    resolver: zodResolver(BookSchema),
    defaultValues: {
      //rest of the values
      bookImageUrl: "",
    },
  });

  //submitting the forms
  async function onSubmit(data: BookInferSchema) {
    try {
      const formData = new FormData();
      if (bookImageUrl) {
        data.bookImageUrl = bookImageUrl; // Attach uploaded image URL
      }

      Object.entries(data).forEach(([key, value]) => {
        formData.append(
          key,
          value instanceof Date ? value.toISOString() : value.toString()
        );
      });

      //sending the formData to the action.ts for submitting the forms
      const response = (await action(formData)) as {
        error?: string;
        message?: string;
      } | void;

      //Error or success messages for any submissions and any errors/success from the server
      if (response?.error) {
        toast({
          title: "Error",
          description: `An error occurred: ${response.error}`,
        });
      } else {
        form.reset();
      }
    } catch {
      toast({
        title: "Error",
        description: "An unexpected error occured.",
      });
    }
  }

  return (
        <Form {...form}>
          <form
            className="space-y-8"
            onSubmit={(e) => {
              e.preventDefault();
              startTransition(() => {
                form.handleSubmit(onSubmit)(e);
              });
            }}
          >
            <UploadBookImage
              size={150}
              url={bookImageUrl}
              onUpload={(url) => setBookImageUrl(url)}
            />

           {/* rest of the input fields */}
          </form>
        </Form>
  );
};

export default BookForm;

服务端action.ts

"use server"

export async function addBook(state: BookFormState, formData: FormData) {
  // Validate form fields
  // Log all form data to debug
  for (const pair of formData.entries()) {
    console.log(`${pair[0]}: ${pair[1]}`);
  }

  const validatedFields = BookSchema.safeParse({
    //rest of the values
    bookImageUrl: formData.get("bookImageUrl"),
  });

   // Check if validation failed
   if (!validatedFields.success) {
    console.error("Validation Errors:", validatedFields.error.format()); // Log errors
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    };
  }

 // Prepare for insertion into the new database
 const { ...restOfValues, bookImageUrl } = validatedFields.data

  // Insert the new author into the database
  const supabase = createClient();
  const {data, error} = await supabase.from('books').insert({ ...restOfValues, bookImageUrl});

  if(data){
    console.log(data,"data in the addBook function")
  }

  
  if (error) {
    return {
      error: true,
      message: error.message,
    };
  }

  return {
    error: false,
    message: 'Book updated successfully',
  };

}

数据库表定义

create table
  public.books (
   -- rest of the columns
    "bookImageUrl" text null,
    constraint books_pkey primary key (isbn),
    constraint books_author_id_fkey foreign key (author_id) references authors (id) on delete cascade
  ) tablespace pg_default;

Storage桶的RLS策略

alter policy "Enable insert for authenticated users only"
on "public"."books"
to authenticated
with check (
  true
);

Zod Schema

import { z } from "zod";

export const BookSchema = z.object({
  //rest of the values
  bookImageUrl :z.string().optional()
});

// TypeScript Type for Book
export type BookInferSchema = z.infer<typeof BookSchema>;

//Form state for adding and editing books
export type BookFormState =
  | {
      errors?: {
        //rest of the values
        bookImageUrl?: string[];
      };
      message?: string;
    }
  | undefined;

我自己排查的方向

我怀疑是存储桶的名称写错了?或者是没有正确获取公共URL?还有表单状态和上传组件的联动是不是有问题?

希望大家能帮我找出问题所在,给出修复建议,谢谢啦!

备注:内容来源于stack exchange,提问作者JS3

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.14 12:27:59