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

如何在Next.js中使用Server Action POST发送数据至端点

问题描述

我有一个包含多步骤流程的父组件,其中一步使用了Array.from()静态方法。目前正在学习Server Action,希望通过POST请求将数据发送到指定端点。我已经把相关函数传递给子组件,也再次用到了数组方法,但不知道怎么把数据发送到目标端点。相关代码如下:

if (prev.step === 2) {
  const singleGuests = Array.from(
    { length: prev.tickets.single },
    (_, i) => ({
      firstName: formData.get(`single_firstName_${i}`),
      lastName: formData.get(`single_lastName_${i}`),
      email: formData.get(`single_email_${i}`),
      phonenumber: formData.get(`single_phonenumber_${i}`),
    })
  );

  const vipGuests = Array.from({ length: prev.tickets.vip }, (_, i) => ({
    firstName: formData.get(`vip_firstName_${i}`),
    lastName: formData.get(`vip_lastName_${i}`),
    email: formData.get(`vip_email_${i}`),
    phonenumber: formData.get(`vip_phonenumber_${i}`),
  }));

  return {
    ...prev,
    step: prev.step + 1,
    guests: { single: singleGuests, vip: vipGuests },
  };
}

子组件代码:

import { useState } from "react";
import { motion } from "framer-motion";
import { Caesar_Dressing } from "next/font/google";

const ceasarDressing = Caesar_Dressing({
  subsets: ["latin"],
  weight: "400",
  display: "swap",
});

export default function ContactInfo({ tickets, formAction }) {
  const [isFormValid, setIsFormValid] = useState(false);

  const handleInputChange = (e) => {
    const inputs = e.target.closest("form").querySelectorAll("input");
    const allValid = Array.from(inputs).every((input) => input.checkValidity());
    setIsFormValid(allValid);
  };

  return (
    <motion.form
      initial={{ opacity: 0, x: 20 }}
      animate={{ opacity: 1, x: 0 }}
      transition={{ duration: 0.3 }}
      className="text-white rounded-lg bg-gradient-to-tl border border-gray-500 from-customBlack_2 to-customBlack p-4 relative z-0"
      onChange={handleInputChange}
    >
      <fieldset className="grid gap-6 mb-6 md:grid-cols-2">
        <legend className={`${ceasarDressing.className} block mb-2 text-3xl`}>
          PERSONLIG INFORMATION
        </legend>
        {Array.from({ length: tickets.single }, (_, i) => (
          <ContactForm key={i} i={i} ticketType="single" />
        ))}
        <div className="relative z-10 group rounded-xl inline-block p-[2px] overflow-hidden shadow-lg">
          <span className="absolute inset-[-1000%] animate-[spin_3s_linear_infinite] bg-[conic-gradient(from_90deg_at_50%_50%,#EC2783_0%,#141415_50%,#EC2783_100%)]" />
          <div className="relative bg-gradient-to-tl from-customBlack_2 to-customBlack z-0 rounded-xl">
            <div className="rounded-xl overflow-hidden">
              {Array.from({ length: tickets.vip }, (_, i) => (
                <ContactForm key={i} i={i} ticketType="vip" />
              ))}
            </div>
          </div>
        </div>
      </fieldset>
      <div className="flex">
        <button
          className={`${
            isFormValid
              ? "font-bold px-8 py-2 my-8 ml-auto text-xl bg-gradient-to-bl from-customPink text-white to-customOrange text-transparent hover:transform"
              : "bg-gray-500 px-8 py-2 my-8 ml-auto text-xl font-bold text-gray-300 cursor-not-allowed"
          }`}
          formAction={formAction}
          type="submit"
          disabled={!isFormValid}
        >
          Næste
        </button>
      </div>
    </motion.form>
  );
}

function ContactForm({ i, ticketType }) {
  // 使用staggerChildren给每个子元素添加延迟动画
  // 在父组件中定义子组件可继承的变体状态
  // 给每个子输入框定义inputSpring动画效果
  const staggerInputs = {
    hidden: { opacity: 0 },
    visible: {
      opacity: 1,
      transition: {
        staggerChildren: 0.2,
      },
    },
  };

  const inputSpring = {
    hidden: { opacity: 0, y: 20 },
    visible: {
      opacity: 1,
      y: 0,
      transition: { type: "spring", stiffness: 100 },
    },
  };

  return (
    <motion.div
      initial="hidden"
      animate="visible"
      variants={staggerInputs}
      className="p-2"
    >
      {/* 根据ticketType判断是single还是vip票,进行条件渲染 */}
      {ticketType === "single" && (
        <motion.h2 className="font-bold text-xl" variants={inputSpring}>
          Enkelt Billet
        </motion.h2>
      )}
      {ticketType === "vip" && (
        <motion.h2
          variants={inputSpring}
          className="font-bold text-xl bg-gradient-to-r from-customPink via-customRed to-customOrange bg-clip-text text-transparent"
        >
          VIP Billet
        </motion.h2>
      )}

      <motion.div className="mb-2.5" variants={inputSpring}>
        <label
          htmlFor={`${ticketType}_firstName_${i}`}
          className="block text-sm font-medium text-white"
        >
          Fornavn
        </label>
        <input
          name={`${ticketType}_firstName_${i}`}
          type="text"
          placeholder="Joe"
          autoComplete="name"
          required
          className="bg-gray-100 border border-gray-200 text-gray-900 text-sm rounded-md w-full p-2.5 focus:outline-none focus:ring-2 valid:[&:not(:placeholder-shown):not(:focus)]:bg-green-50 valid:[&:not(:placeholder-shown):not(:focus)]:border-green-500 valid:[&:not(:placeholder-shown):not(:focus)]:focus:ring-green-500 invalid:[&:not(:placeholder-shown):not(:focus)]:focus:ring-red-500 invalid:[&:not(:placeholder-shown):not(:focus)]:bg-red-50 invalid:[&:not(:placeholder-shown):not(:focus)]:border-red-400"
        />
      </motion.div>

      <motion.div className="mb-2.5" variants={inputSpring}>
        <label
          htmlFor={`${ticketType}_lastName_${i}`}
          className="block text-sm font-medium text-white"
        >
          Efternavn
        </label>
        <input
          name={`${ticketType}_lastName_${i}`}
          type="text"
          placeholder="Doe"
          autoComplete="name"
          required
          className="bg-gray-100 border border-gray-200 text-gray-900 text-sm rounded-md w-full p-2.5 focus:outline-none focus:ring-2 valid:[&:not(:placeholder-shown):not(:focus)]:bg-green-50 valid:[&:not(:placeholder-shown):not(:focus)]:border-green-500 valid:[&:not(:placeholder-shown):not(:focus)]:focus:ring-green-500 invalid:[&:not(:placeholder-shown):not(:focus)]:focus:ring-red-500 invalid:[&:not(:placeholder-shown):not(:focus)]:bg-red-50 invalid:[&:not(:placeholder-shown):not(:focus)]:border-red-400"
        />
      </motion.div>

      <motion.div className="mb-2.5" variants={inputSpring}>
        <label htmlFor={`${ticketType}_email_${i}`}>Email</label>
        <input
          className="bg-gray-100 border border-gray-200 text-gray-900 text-sm rounded-md w-full p-2.5 focus:outline-none focus:ring-2 valid:[&:not(:placeholder-shown):not(:focus)]:bg-green-50 valid:[&:not(:placeholder-shown):not(:focus)]:border-green-500 valid:[&:not(:placeholder-shown):not(:focus)]:focus:ring-green-500 invalid:[&:not(:placeholder-shown):not(:focus)]:focus:ring-red-500 invalid:[&:not(:placeholder-shown):not(:focus)]:bg-red-50 invalid:[&:not(:placeholder-shown):not(:focus)]:border-red-400"
          name={`${ticketType}_email_${i}`}
          type="email"
          placeholder="joedoe@example.com"
          autoComplete="email"
          required
        />
      </motion.div>

      <motion.div className="mb-2.5" variants={inputSpring}>
        <label htmlFor={`${ticketType}_phonenumber_${i}`}>Mobilnummer</label>
        <input
          name={`${ticketType}_phonenumber_${i}`}
          type="tel"
          placeholder="12 34 56 78"
          autoComplete="tel"
          required
          className="bg-gray-100 border border-gray-200 text-gray-900 text-sm rounded-md w-full p-2.5 focus:outline-none focus:ring-2 valid:[&:not(:placeholder-shown):not(:focus)]:bg-green-50 valid:[&:not(:placeholder-shown):not(:focus)]:border-green-500 valid:[&:not(:placeholder-shown):not(:focus)]:focus:ring-green-500 invalid:[&:not(:placeholder-shown):not(:focus)]:focus:ring-red-500 invalid:[&:not(:placeholder-shown):not(:focus)]:bg-red-50 invalid:[&:not(:placeholder-shown):not(:focus)]:border-red-400"
        />
      </motion.div>
    </motion.div>
  );
}
解决方案

1. 创建Server Action文件

在Next.js项目中新建actions.js文件,定义处理表单提交的Server Action:

"use server";

export async function submitGuestData(formData) {
  // 解析表单中携带的票数量
  const ticketCounts = JSON.parse(formData.get("ticketCounts"));
  
  // 解析嘉宾数据,复用原有的Array.from逻辑
  const singleGuests = Array.from(
    { length: ticketCounts.single },
    (_, i) => ({
      firstName: formData.get(`single_firstName_${i}`),
      lastName: formData.get(`single_lastName_${i}`),
      email: formData.get(`single_email_${i}`),
      phonenumber: formData.get(`single_phonenumber_${i}`),
    })
  );

  const vipGuests = Array.from(
    { length: ticketCounts.vip },
    (_, i) => ({
      firstName: formData.get(`vip_firstName_${i}`),
      lastName: formData.get(`vip_lastName_${i}`),
      email: formData.get(`vip_email_${i}`),
      phonenumber: formData.get(`vip_phonenumber_${i}`),
    })
  );

  // 发送POST请求到目标端点
  try {
    const response = await fetch("你的目标端点URL", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ singleGuests, vipGuests }),
    });

    if (!response.ok) throw new Error("数据发送失败");
    const result = await response.json();
    return { success: true, data: result };
  } catch (error) {
    return { success: false, error: error.message };
  }
}

2. 修改父组件传递formAction的逻辑

在父组件中导入Server Action,将票数量作为隐藏字段传入表单,并处理提交结果:

// 父组件中导入Server Action
import { submitGuestData } from "@/actions";

// 渲染ContactInfo组件时传递formAction
<ContactInfo 
  tickets={tickets} 
  formAction={async (formData) => {
    // 将票数量添加到formData中
    formData.append('ticketCounts', JSON.stringify(tickets));
    // 调用Server Action
    const result = await submitGuestData(formData);
    
    // 根据结果更新流程步骤或处理错误
    if (result.success) {
      setStep(prev => prev + 1);
    } else {
      console.error("提交失败:", result.error);
      // 可添加错误提示逻辑
    }
  }} 
/>

3. 修正子组件表单配置

给子组件的表单添加POST方法,确保符合Server Action的提交要求:

// ContactInfo组件中的form标签修改为:
<motion.form
  initial={{ opacity: 0, x: 20 }}
  animate={{ opacity: 1, x: 0 }}
  transition={{ duration: 0.3 }}
  className="text-white rounded-lg bg-gradient-to-tl border border-gray-500 from-customBlack_2 to-customBlack p-4 relative z-0"
  onChange={handleInputChange}
  method="POST"
>

关键说明

  • Server Action必须标记"use server",确保在服务器端执行,避免暴露敏感信息。
  • 票数量需要通过表单传递给Server Action,否则无法正确解析对应数量的嘉宾数据。
  • 请求发送时要设置Content-Type: application/json,将数据序列化为JSON格式。
  • 必须处理请求的成功和失败状态,以便更新流程或给用户反馈。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 12:18:09