'data'
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, type FormEvent } from "react";
|
||||
import { useState, useEffect, useRef, type FormEvent } from "react";
|
||||
import Link from "next/link";
|
||||
import ThemeToggle from "../components/ThemeToggle";
|
||||
import { DIGITAL_NOMAD_WECHAT_QR } from "@/config/home.config";
|
||||
@@ -11,9 +11,21 @@ const ageRangeOptions = ["18-22", "23-27", "28-32", "33-37", "38+"];
|
||||
/** 微信号仅允许英文字母、数字、下划线、连字符 */
|
||||
const WECHAT_REGEX = /^[a-zA-Z0-9_-]*$/;
|
||||
|
||||
const WECHAT_STORAGE_KEY = "salon_join_wechat";
|
||||
|
||||
interface UserRecord {
|
||||
hasRecord: boolean;
|
||||
record: { user_id?: string; amount?: number; order_id?: string; vip?: boolean } | null;
|
||||
record: {
|
||||
id?: string;
|
||||
wechatId?: string;
|
||||
user_id?: string;
|
||||
nickname?: string;
|
||||
media?: string;
|
||||
amount?: number;
|
||||
order_id?: string;
|
||||
vip?: boolean;
|
||||
is_volunteer?: boolean;
|
||||
} | null;
|
||||
isComplete: boolean;
|
||||
isWechatFriend: boolean;
|
||||
isApproved: boolean;
|
||||
@@ -21,6 +33,39 @@ interface UserRecord {
|
||||
vip: boolean;
|
||||
}
|
||||
|
||||
function UserProfileCard({ record, showVolunteerTag }: { record: UserRecord["record"]; showVolunteerTag?: boolean }) {
|
||||
if (!record || (!record.nickname && !record.media && !record.wechatId)) return null;
|
||||
const isVolunteer = showVolunteerTag && record.is_volunteer;
|
||||
return (
|
||||
<div className="mt-6 flex items-center justify-center gap-3 rounded-xl bg-stone-50 dark:bg-stone-800/50 px-4 py-3">
|
||||
<div className="relative shrink-0">
|
||||
{record.media ? (
|
||||
<img
|
||||
src={record.media}
|
||||
alt=""
|
||||
className="h-12 w-12 rounded-full object-cover border border-stone-200 dark:border-stone-600"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/40 text-xl">
|
||||
👤
|
||||
</div>
|
||||
)}
|
||||
{isVolunteer && (
|
||||
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 px-1.5 py-0.5 rounded text-[10px] font-medium bg-amber-500 text-white whitespace-nowrap">
|
||||
志愿者
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="font-medium text-stone-800 dark:text-stone-100">{record.nickname || "—"}</p>
|
||||
{record.wechatId && (
|
||||
<p className="text-xs text-stone-500 dark:text-stone-400 font-mono">微信号: {record.wechatId}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function JoinPage() {
|
||||
const [wechat, setWechat] = useState("");
|
||||
const [userRecord, setUserRecord] = useState<UserRecord | null>(null);
|
||||
@@ -48,9 +93,9 @@ export default function JoinPage() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [hydrated, setHydrated] = useState(false);
|
||||
|
||||
const [fromPaid, setFromPaid] = useState(() =>
|
||||
typeof window !== "undefined" && new URLSearchParams(window.location.search).get("paid") === "1"
|
||||
);
|
||||
const [fromPaid, setFromPaid] = useState(false);
|
||||
const [fromVolunteer, setFromVolunteer] = useState(false);
|
||||
const initialWechatCheckDone = useRef(false);
|
||||
|
||||
const fetchByWechat = async (w: string) => {
|
||||
if (!w.trim()) return;
|
||||
@@ -79,6 +124,11 @@ export default function JoinPage() {
|
||||
const w = wechat.trim();
|
||||
if (!w) {
|
||||
setUserRecord(null);
|
||||
try {
|
||||
typeof window !== "undefined" && localStorage.removeItem(WECHAT_STORAGE_KEY);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!WECHAT_REGEX.test(w)) {
|
||||
@@ -86,6 +136,11 @@ export default function JoinPage() {
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
try {
|
||||
typeof window !== "undefined" && localStorage.setItem(WECHAT_STORAGE_KEY, w);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
fetchByWechat(w);
|
||||
};
|
||||
|
||||
@@ -181,7 +236,7 @@ export default function JoinPage() {
|
||||
intro: "",
|
||||
age_range: ageRange,
|
||||
city: "深圳",
|
||||
session: "digital-nomad",
|
||||
session: fromVolunteer ? "volunteer" : "digital-nomad",
|
||||
education: "",
|
||||
graduation_year: "",
|
||||
agree_rules: true,
|
||||
@@ -191,6 +246,8 @@ export default function JoinPage() {
|
||||
why_join: "",
|
||||
region: "",
|
||||
available_time: "",
|
||||
qualify, // 女性且小红书帖子>10 时自动通过审核
|
||||
is_volunteer: fromVolunteer,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
@@ -199,6 +256,8 @@ export default function JoinPage() {
|
||||
}
|
||||
setQualify(qualify);
|
||||
setSubmitted(true);
|
||||
// 提交成功后立即按微信号查询,用真实记录状态展示(审核中/审核通过等),避免刷新后状态不一致
|
||||
fetchByWechat(wechat.trim());
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "网络错误,请重试");
|
||||
} finally {
|
||||
@@ -206,6 +265,32 @@ export default function JoinPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const clearWechatAndRecord = () => {
|
||||
setUserRecord(null);
|
||||
setWechat("");
|
||||
setXiaohongshuUrl("");
|
||||
setProfession("");
|
||||
setGender("");
|
||||
setAgeRange("");
|
||||
setAgreeRules(true);
|
||||
setUrlError(null);
|
||||
setProfile(null);
|
||||
setValidating(false);
|
||||
setSubmitted(false);
|
||||
setSubmitting(false);
|
||||
setQualify(false);
|
||||
setError(null);
|
||||
try {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem(WECHAT_STORAGE_KEY);
|
||||
localStorage.removeItem("join_pay_order");
|
||||
document.cookie = "join_pay_order=; path=/; max-age=0";
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
const handlePayNow = () => {
|
||||
const rec = userRecord?.record;
|
||||
if (!rec?.user_id) return;
|
||||
@@ -221,10 +306,27 @@ export default function JoinPage() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
setFromPaid(params.get("paid") === "1");
|
||||
setFromVolunteer(params.get("volunteer") === "1");
|
||||
if (!initialWechatCheckDone.current) {
|
||||
initialWechatCheckDone.current = true;
|
||||
try {
|
||||
const stored = localStorage.getItem(WECHAT_STORAGE_KEY);
|
||||
const w = (stored || "").trim();
|
||||
if (w && WECHAT_REGEX.test(w)) {
|
||||
setWechat(w);
|
||||
fetchByWechat(w);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
setHydrated(true);
|
||||
}, []);
|
||||
|
||||
// 成功报名:已支付 或 支付成功跳转 或 (好友且vip)
|
||||
// 成功报名:已支付 或 支付成功跳转 或 (好友且vip) —— 仅以数据库状态为准,不依赖 submitted
|
||||
const showSuccess = fromPaid || (userRecord?.hasRecord && userRecord.isComplete) || (userRecord?.hasRecord && userRecord.isWechatFriend && userRecord.vip);
|
||||
|
||||
// 有数据、非好友 → 只展示状态页:审核中/审核通过/审核拒绝
|
||||
@@ -233,18 +335,20 @@ export default function JoinPage() {
|
||||
// 有数据、好友、非vip、未支付 → 支付状态页
|
||||
const showPayPage = userRecord?.hasRecord && userRecord.isWechatFriend && !userRecord.vip && !userRecord.isComplete;
|
||||
|
||||
if (submitted || showSuccess) {
|
||||
if (showSuccess) {
|
||||
const baseRecord = userRecord?.record ?? null;
|
||||
const displayRecord = baseRecord
|
||||
? { ...baseRecord, wechatId: baseRecord.wechatId || wechat || undefined }
|
||||
: (wechat ? { wechatId: wechat } : null);
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-[#faf8f5] dark:bg-stone-950 px-4 py-8">
|
||||
<div className="w-full max-w-md animate-fade-in rounded-2xl bg-white dark:bg-stone-900 p-6 sm:p-10 text-center shadow-lg border border-stone-200 dark:border-stone-800">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-emerald-50 dark:bg-emerald-900/30 text-4xl">🎉</div>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-stone-800 dark:text-stone-100">报名成功</h2>
|
||||
<p className="mt-2 text-sm text-stone-500 dark:text-stone-400">感谢报名,活动前会联系你确认~</p>
|
||||
<p className="mt-6 text-xs text-stone-400 dark:text-stone-500 leading-relaxed whitespace-pre-line">
|
||||
{`本场收取少量场地分摊费29/人
|
||||
用于覆盖民宿/场地基础成本。
|
||||
通过审核后再确认名额,未确认无需支付。`}
|
||||
<p className="mt-2 text-sm text-stone-500 dark:text-stone-400">
|
||||
{displayRecord?.is_volunteer ? "要提前到场,金额可退" : "感谢报名,活动前会联系你确认~"}
|
||||
</p>
|
||||
<UserProfileCard record={displayRecord} showVolunteerTag />
|
||||
<Link href="/" className="mt-6 sm:mt-8 inline-flex items-center justify-center rounded-xl bg-amber-600 px-6 sm:px-8 py-2.5 sm:py-3 font-medium text-white text-sm sm:text-base transition-colors hover:bg-amber-700">
|
||||
🏠 返回首页
|
||||
</Link>
|
||||
@@ -281,18 +385,16 @@ export default function JoinPage() {
|
||||
<p className="mt-2 text-sm text-stone-500 dark:text-stone-400">⏰ 通常会在 24 小时内完成审核,通过后用微信联系你。</p>
|
||||
</>
|
||||
)}
|
||||
<p className="mt-6 text-xs text-stone-400 dark:text-stone-500 leading-relaxed whitespace-pre-line">
|
||||
{`本场收取少量场地分摊费29/人
|
||||
用于覆盖民宿/场地基础成本。
|
||||
通过审核后再确认名额,未确认无需支付。`}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setUserRecord(null); setWechat(""); }}
|
||||
className="mt-6 text-sm text-stone-500 hover:text-amber-600"
|
||||
>
|
||||
更换微信号
|
||||
</button>
|
||||
<UserProfileCard record={userRecord?.record ?? null} showVolunteerTag />
|
||||
{status !== "approved" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearWechatAndRecord}
|
||||
className="mt-6 text-sm text-stone-500 hover:text-amber-600"
|
||||
>
|
||||
重新填写报名表
|
||||
</button>
|
||||
)}
|
||||
<Link href="/" className="mt-4 block text-sm text-amber-600 hover:text-amber-700">
|
||||
🏠 返回首页
|
||||
</Link>
|
||||
@@ -314,7 +416,14 @@ export default function JoinPage() {
|
||||
<div className="overflow-hidden bg-white dark:bg-stone-900 shadow-sm sm:rounded-2xl p-6 sm:p-10 text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-amber-50 dark:bg-amber-900/30 text-4xl">💰</div>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-stone-800 dark:text-stone-100">支付场地茶歇费</h2>
|
||||
<p className="mt-2 text-sm text-stone-500 dark:text-stone-400">29元/人,用于覆盖民宿/场地基础成本</p>
|
||||
<p className="mt-2 text-sm text-stone-500 dark:text-stone-400">1元/人,用于覆盖民宿/场地基础成本</p>
|
||||
<UserProfileCard record={userRecord?.record ?? null} showVolunteerTag />
|
||||
{userRecord?.record?.is_volunteer && (
|
||||
<p className="mt-3 text-xs text-emerald-600 dark:text-emerald-400">
|
||||
🌿 志愿者预付活动结束可退<br />
|
||||
用于防止临时爽约
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePayNow}
|
||||
@@ -322,13 +431,6 @@ export default function JoinPage() {
|
||||
>
|
||||
立即报名
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setUserRecord(null); setWechat(""); }}
|
||||
className="mt-3 text-sm text-stone-500 hover:text-stone-700"
|
||||
>
|
||||
更换微信号
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -419,6 +521,16 @@ export default function JoinPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 sm:px-6 py-5 sm:py-6">
|
||||
{fromVolunteer && (
|
||||
<div className="mb-6 rounded-xl bg-amber-50 dark:bg-amber-900/20 p-4">
|
||||
<h3 className="font-semibold text-stone-800 dark:text-stone-100 text-sm mb-2">🙌 志愿者工作内容</h3>
|
||||
<ul className="text-xs sm:text-sm text-stone-600 dark:text-stone-400 space-y-1">
|
||||
<li>· 签到:现场签到、引导参与者</li>
|
||||
<li>· 茶歇准备:协助布置茶歇、收拾整理</li>
|
||||
<li>· 拍照摄像:记录活动现场,分享精彩瞬间</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-stone-700 dark:text-stone-300 mb-2">💬 微信号 <span className="text-red-500">*</span></label>
|
||||
@@ -460,6 +572,14 @@ export default function JoinPage() {
|
||||
<option value="男">男</option>
|
||||
</select>
|
||||
</div>
|
||||
{fromVolunteer && (
|
||||
<div>
|
||||
<label className="flex items-center gap-3 cursor-default">
|
||||
<input type="checkbox" checked readOnly disabled className="rounded border-stone-300 text-amber-600" />
|
||||
<span className="text-sm font-medium text-stone-700 dark:text-stone-300">🙌 志愿者</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
<div className="hidden">
|
||||
<select value={ageRange} onChange={(e) => setAgeRange(e.target.value)} className="form-input">
|
||||
<option value="">请选择</option>
|
||||
@@ -476,14 +596,14 @@ export default function JoinPage() {
|
||||
<p className="rounded-lg bg-red-50 dark:bg-red-900/30 px-4 py-3 text-sm text-red-600 dark:text-red-400">⚠️ {error}</p>
|
||||
)}
|
||||
<button type="submit" disabled={submitting || !!urlError || validating} className="w-full rounded-xl bg-amber-600 py-3.5 text-base font-medium text-white transition-colors hover:bg-amber-700 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50">
|
||||
{submitting ? "🔄 提交中…" : "📤 提交报名"}
|
||||
{submitting ? "🔄 提交中…" : fromVolunteer ? "📤 提交志愿者申请" : "📤 提交报名"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 text-center">
|
||||
<button type="button" onClick={() => { setUserRecord(null); setWechat(""); }} className="text-xs sm:text-sm text-stone-500 hover:text-amber-600">
|
||||
更换微信号
|
||||
<button type="button" onClick={clearWechatAndRecord} className="text-xs sm:text-sm text-stone-500 hover:text-amber-600">
|
||||
重新填写报名表
|
||||
</button>
|
||||
<span className="mx-2 text-stone-300">|</span>
|
||||
<Link href="/" className="text-xs sm:text-sm text-stone-500 hover:text-amber-600">🏠 返回首页</Link>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
const WECHAT_STORAGE_KEY = "salon_join_wechat";
|
||||
|
||||
function clearPendingOrder(): void {
|
||||
try {
|
||||
localStorage.removeItem("join_pay_order");
|
||||
@@ -14,11 +16,12 @@ export default function JoinPaidPage() {
|
||||
const [completeStatus, setCompleteStatus] = useState<"pending" | "success" | "error">("pending");
|
||||
const [errorDetail, setErrorDetail] = useState<string | null>(null);
|
||||
const [countdown, setCountdown] = useState<number | null>(null);
|
||||
const [wechatId, setWechatId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const tryComplete = async (orderId: string): Promise<{ ok: boolean; error?: string }> => {
|
||||
const tryComplete = async (orderId: string): Promise<{ ok: boolean; error?: string; record?: { wechatId?: string } }> => {
|
||||
const r = await fetch("/api/salon/complete-order", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -40,7 +43,7 @@ export default function JoinPaidPage() {
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent("auth:updated"));
|
||||
window.dispatchEvent(new CustomEvent("vip:updated"));
|
||||
return { ok: true };
|
||||
return { ok: true, record: d?.record };
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
@@ -73,7 +76,10 @@ export default function JoinPaidPage() {
|
||||
const result = await tryComplete(orderId);
|
||||
if (result.ok) {
|
||||
clearPendingOrder();
|
||||
if (!cancelled) setCompleteStatus("success");
|
||||
if (!cancelled) {
|
||||
setCompleteStatus("success");
|
||||
if (result.record?.wechatId) setWechatId(result.record.wechatId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
lastError = result.error || "unknown";
|
||||
@@ -94,6 +100,15 @@ export default function JoinPaidPage() {
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem(WECHAT_STORAGE_KEY)?.trim();
|
||||
if (stored) setWechatId(stored);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, []);
|
||||
|
||||
const COUNTDOWN_SECONDS = 5;
|
||||
useEffect(() => {
|
||||
if (completeStatus !== "success") return;
|
||||
@@ -123,12 +138,15 @@ export default function JoinPaidPage() {
|
||||
<p className="mt-3 text-slate-500 dark:text-slate-400">
|
||||
{completeStatus === "success"
|
||||
? countdown != null
|
||||
? `${countdown} 秒后跳转报名页`
|
||||
? `${countdown} 秒后跳转报名成功页`
|
||||
: "感谢您的支持"
|
||||
: completeStatus === "error"
|
||||
? "请稍后刷新页面重试"
|
||||
: "正在确认支付状态…"}
|
||||
</p>
|
||||
{wechatId && (
|
||||
<p className="mt-2 text-xs text-slate-400 dark:text-slate-500 font-mono">微信号: {wechatId}</p>
|
||||
)}
|
||||
{completeStatus === "error" && errorDetail && (
|
||||
<p className="mt-1 text-xs text-slate-400 dark:text-slate-500">{errorDetail}</p>
|
||||
)}
|
||||
@@ -141,12 +159,18 @@ export default function JoinPaidPage() {
|
||||
刷新重试
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
href="/join?paid=1"
|
||||
className="mt-8 inline-flex items-center gap-2 rounded-full bg-[#ff4d4f] px-8 py-3 font-semibold text-white transition-colors hover:bg-[#ff7a45]"
|
||||
>
|
||||
← 查看报名状态
|
||||
</Link>
|
||||
{completeStatus === "pending" ? (
|
||||
<span className="mt-8 inline-flex items-center gap-2 rounded-full bg-stone-200 dark:bg-stone-700 px-8 py-3 font-semibold text-stone-400 dark:text-stone-500 cursor-not-allowed">
|
||||
← 查看报名状态
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
href="/join?paid=1"
|
||||
className="mt-8 inline-flex items-center gap-2 rounded-full bg-[#ff4d4f] px-8 py-3 font-semibold text-white transition-colors hover:bg-[#ff7a45]"
|
||||
>
|
||||
← 查看报名状态
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user