"use client"; import { useState, useRef, useEffect, useCallback, type FormEvent, type ChangeEvent } from "react"; import { Link } from "@/i18n/navigation"; import { getPayEnv, getRecommendedChannel, mustUseWxPay, redirectToPay, getDeviceFromEnv, } from "@/app/lib/payment"; import { usePayStatusPoll } from "@/app/lib/payment/usePayStatusPoll"; import type { PayChannel, PayEnv } from "@/app/lib/payment"; import { getStoredToken } from "@/app/lib/pocketbase"; import AuthModal from "@/app/components/AuthModal"; const genderOptions = ["男", "女"]; const singleOptions = ["单身", "恋爱中", "已婚"]; const educationOptions = ["高中及以下", "大专", "本科", "硕士", "博士"]; const graduationYears = Array.from({ length: 41 }, (_, i) => String(2026 - i)); interface FormData { nickname: string; gender: string; single: string; profession: string; intro: string; wechat: string; education: string; graduationYear: string; phone: string; } interface MediaInfo { preview: string; url: string; type: "image" | "video"; } export default function JoinPage() { const [form, setForm] = useState({ nickname: "", gender: "", single: "", profession: "", intro: "", wechat: "", education: "", graduationYear: "", phone: "", }); const [media, setMedia] = useState(null); const [uploading, setUploading] = useState(false); const fileRef = useRef(null); const [submitted, setSubmitted] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [payRedirecting, setPayRedirecting] = useState(false); const [payEnv, setPayEnv] = useState(null); const [payChannel, setPayChannel] = useState("alipay"); const [authOpen, setAuthOpen] = useState(false); const checkAuth = useCallback(async () => { if (getStoredToken()) return true; try { const res = await fetch("/api/auth/me", { credentials: "include" }); const data = await res.json(); return !!(data?.user && data?.token); } catch { return false; } }, []); useEffect(() => { if (typeof window !== "undefined" && new URLSearchParams(window.location.search).get("paid") === "1") { setSubmitted(true); } }, []); // 微信 H5 支付完成后可能不跳转,后台静默轮询,检测到已支付则显示成功页 usePayStatusPoll(() => setSubmitted(true)); useEffect(() => { if (typeof window === "undefined") return; const env = getPayEnv(); setPayEnv(env); setPayChannel(getRecommendedChannel(env)); }, []); const set = (key: keyof FormData, value: string) => setForm((prev) => ({ ...prev, [key]: value })); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setError(null); const loggedIn = await checkAuth(); if (!loggedIn) { setAuthOpen(true); setError("请先登录或注册后再提交申请"); return; } setSubmitting(true); try { const res = await fetch("/api/join", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...form, media: media?.url }), }); const data = await res.json(); if (!res.ok || !data?.ok) { throw new Error(data?.error || "提交失败,请稍后重试"); } const userId = data.user_id; if (!userId) { setSubmitted(true); return; } setSubmitting(false); setPayRedirecting(true); // Z-Pay return_url 不支持带参数,使用 /join/paid 专用路径 const returnUrl = `${window.location.origin}${window.location.pathname}/paid`; const env = getPayEnv(); const channel = mustUseWxPay(env) ? "wxpay" : payChannel; const device = getDeviceFromEnv(env); redirectToPay({ user_id: userId, return_url: returnUrl, channel, device, useJoinConfig: true, }); return; } catch (err) { setError(err instanceof Error ? err.message : "网络错误,请重试"); } finally { setSubmitting(false); } }; if (submitted) { return (

申请已提交

我们会尽快审核你的信息
审核通过后将通过微信邀请你入群

← 返回首页
); } return (
{/* ── PC: center column / H5: full width ── */}
{/* ── Banner ── */}
{/* Hero image area */}
🌍
Digital Nomad Community
{/* Decorative blobs */}
{/* Title block */}

🌍 数字游民社区报名

{"💼 结识志同道合的伙伴\n📍 线下聚会 + 线上分享\n🤝 互助成长,资源共享"}

{/* ── Form card ── */}
{/* ---- Row group: nickname + gender (PC side-by-side) ---- */}
set("nickname", e.target.value)} required className="form-input" />
{/* ---- Row group: single + profession ---- */}
set("profession", e.target.value)} required className="form-input" />
{/* ---- wechat + phone ---- */}
set("wechat", e.target.value)} required className="form-input" /> { const v = e.target.value.replace(/\D/g, "").slice(0, 11); set("phone", v); }} className="form-input" />
{/* ---- Intro (full width) ---- */}