122 lines
3.9 KiB
TypeScript
122 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import type { FormBlockValue } from "@/types/wagtail";
|
|
|
|
const API_BASE_URL =
|
|
process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000";
|
|
|
|
export default function LeadForm({ value }: { value: FormBlockValue }) {
|
|
const [values, setValues] = useState<Record<string, string>>({});
|
|
const [website, setWebsite] = useState(""); // 蜂蜜陷阱字段,正常用户不可见,只有机器人会填写
|
|
const [status, setStatus] = useState<"idle" | "submitting" | "done" | "error">(
|
|
"idle",
|
|
);
|
|
|
|
const form = value.form;
|
|
if (!form) {
|
|
return null;
|
|
}
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setStatus("submitting");
|
|
try {
|
|
const res = await fetch(`${API_BASE_URL}/api/v1/custom/leads/`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
form_id: form!.id,
|
|
data: values,
|
|
source_url: typeof window !== "undefined" ? window.location.href : "",
|
|
website,
|
|
}),
|
|
});
|
|
if (!res.ok) throw new Error("submit failed");
|
|
setStatus("done");
|
|
} catch {
|
|
setStatus("error");
|
|
}
|
|
}
|
|
|
|
if (status === "done") {
|
|
return (
|
|
<div className="mx-auto max-w-xl px-6 py-12 text-center text-slate-700">
|
|
{form.success_message}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
className="mx-auto max-w-xl space-y-4 px-6 py-12"
|
|
>
|
|
<h3 className="text-2xl font-semibold text-slate-900">{form.name}</h3>
|
|
{form.fields.map((field) => (
|
|
<div key={field.field_key}>
|
|
<label className="mb-1 block text-sm font-medium text-slate-700">
|
|
{field.label}
|
|
{field.required && <span className="text-red-500">*</span>}
|
|
</label>
|
|
{field.field_type === "textarea" ? (
|
|
<textarea
|
|
required={field.required}
|
|
className="w-full rounded-lg border border-slate-200 px-4 py-2"
|
|
rows={4}
|
|
onChange={(e) =>
|
|
setValues((v) => ({ ...v, [field.field_key]: e.target.value }))
|
|
}
|
|
/>
|
|
) : field.field_type === "select" ? (
|
|
<select
|
|
required={field.required}
|
|
className="w-full rounded-lg border border-slate-200 px-4 py-2"
|
|
onChange={(e) =>
|
|
setValues((v) => ({ ...v, [field.field_key]: e.target.value }))
|
|
}
|
|
>
|
|
<option value="">请选择</option>
|
|
{field.choices.map((choice) => (
|
|
<option key={choice} value={choice}>
|
|
{choice}
|
|
</option>
|
|
))}
|
|
</select>
|
|
) : (
|
|
<input
|
|
type={field.field_type}
|
|
required={field.required}
|
|
className="w-full rounded-lg border border-slate-200 px-4 py-2"
|
|
onChange={(e) =>
|
|
setValues((v) => ({ ...v, [field.field_key]: e.target.value }))
|
|
}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
{/* 蜂蜜陷阱字段:离屏幕定位而非 display:none,避免部分机器人检测到隐藏属性后跳过;无需 tabIndex/aria-hidden 以外的额外依赖 */}
|
|
<input
|
|
type="text"
|
|
name="website"
|
|
value={website}
|
|
onChange={(e) => setWebsite(e.target.value)}
|
|
autoComplete="off"
|
|
tabIndex={-1}
|
|
aria-hidden="true"
|
|
className="absolute left-[-9999px] top-auto h-px w-px overflow-hidden"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={status === "submitting"}
|
|
className="w-full rounded-full bg-primary px-6 py-3 font-medium text-white disabled:opacity-50"
|
|
>
|
|
{status === "submitting" ? "提交中..." : form.submit_button_text}
|
|
</button>
|
|
{status === "error" && (
|
|
<p className="text-sm text-red-500">提交失败,请稍后重试。</p>
|
|
)}
|
|
</form>
|
|
);
|
|
}
|