This commit is contained in:
eric
2026-03-16 11:45:38 -05:00
parent 3e73917ad1
commit c5e057f0ed
9 changed files with 482 additions and 72 deletions

View File

@@ -5,7 +5,7 @@ export default function Footer() {
<footer className="border-t border-stone-200 dark:border-stone-800 mt-12 py-8">
<div className="max-w-[900px] mx-auto px-5 sm:px-8 text-center text-sm text-stone-500 dark:text-stone-400">
<Link
href="/join"
href="/join?volunteer=1"
className="inline-flex items-center gap-1.5 mb-4 text-amber-600 dark:text-amber-400 hover:text-amber-700 dark:hover:text-amber-300 font-medium"
>
<span aria-hidden>🙋</span>

View File

@@ -10,12 +10,36 @@ import Footer from "./Footer";
import Header from "./Header";
import { getSessionsWithDates, HOSTS } from "@/config/home.config";
const WECHAT_STORAGE_KEY = "salon_join_wechat";
interface HomeContentProps {
initialVolunteer: { nickname: string; avatar?: string; xiaohongshu_url?: string } | null;
}
const VOLUNTEER_BIO = "签到、茶歇准备、拍照摄像。";
function getPendingOrderId(): string | null {
if (typeof window === "undefined") return null;
try {
const cookie = document.cookie.split(";").find((c) => c.trim().startsWith("join_pay_order="));
const raw = cookie?.split("=")[1]?.trim();
const decoded = raw ? decodeURIComponent(raw) : "";
const fromStorage = localStorage.getItem("join_pay_order") || "";
const value = decoded || fromStorage;
const orderId = value.split("|")[0]?.trim() || "";
return orderId || null;
} catch {
return null;
}
}
function clearPendingOrder(): void {
try {
localStorage.removeItem("join_pay_order");
document.cookie = "join_pay_order=; path=/; max-age=0";
} catch {}
}
export default function HomeContent({ initialVolunteer }: HomeContentProps) {
const [volunteer, setVolunteer] = useState(initialVolunteer);
@@ -26,6 +50,55 @@ export default function HomeContent({ initialVolunteer }: HomeContentProps) {
.catch(() => {});
}, []);
// 支付完成后再次打开首页后台完成订单并刷新用户信息手机浏览器唤醒微信支付后用户可能直接回首页notify 可能未到,需轮询)
useEffect(() => {
const orderId = getPendingOrderId();
if (!orderId) return;
let cancelled = false;
const tryComplete = async (): Promise<boolean> => {
const r = await fetch("/api/salon/complete-order", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ order_id: orderId }),
credentials: "include",
});
const d = await r.json();
if (!d?.ok) return false;
if (d?.token && d?.record) {
fetch("/api/auth/sync-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: d.token, record: d.record }),
credentials: "include",
}).catch(() => {});
import("@/app/lib/pocketbase").then(({ pbSaveAuth }) => pbSaveAuth(d.token, d.record));
if (d.record?.wechatId) {
try {
localStorage.setItem(WECHAT_STORAGE_KEY, d.record.wechatId);
} catch {}
}
}
clearPendingOrder();
window.dispatchEvent(new CustomEvent("auth:updated"));
window.dispatchEvent(new CustomEvent("vip:updated"));
return true;
};
const run = async () => {
const retryDelays = [1500, 2500, 3500, 4500, 5500];
for (let i = 0; i <= retryDelays.length; i++) {
if (cancelled) return;
if (await tryComplete()) return;
if (i < retryDelays.length && !cancelled) {
await new Promise((r) => setTimeout(r, retryDelays[i]));
}
}
};
void run();
return () => { cancelled = true; };
}, []);
return (
<div className="min-h-screen bg-[#faf8f5] dark:bg-stone-950 pb-20 sm:pb-12">
<Header />
@@ -138,13 +211,18 @@ export default function HomeContent({ initialVolunteer }: HomeContentProps) {
{HOSTS.map((host) => {
const isHost2 = host.id === "host-2";
const displayHost = isHost2 && volunteer
? { ...host, name: volunteer.nickname, avatar: volunteer.avatar || host.avatar, bio: VOLUNTEER_BIO }
? {
...host,
name: volunteer.nickname,
avatar: volunteer.avatar || host.avatar,
bio: VOLUNTEER_BIO,
link: volunteer.xiaohongshu_url || host.link,
}
: host;
return (
<HostLink
key={host.id}
host={displayHost}
noLink
{...(isHost2 && !volunteer && {
ctaText: "立即申请",
showFreeBadge: true,

View File

@@ -5,6 +5,8 @@ import { createPortal } from "react-dom";
import {
getPayEnv,
redirectToPay,
redirectToPayH5,
payWechatXorpaySync,
getDeviceFromEnv,
} from "@/app/lib/payment";
import type { PayChannel } from "@/app/lib/payment";
@@ -70,15 +72,20 @@ export default function JoinModal({ isOpen, onClose, initialEmail = "", vipOnly
return;
}
const returnUrl = typeof window !== "undefined" ? `${window.location.origin}/join/paid` : "/join/paid";
const env = getPayEnv();
const channel: PayChannel = "wxpay";
redirectToPay({
const device = getDeviceFromEnv(getPayEnv());
const base = {
user_id: data.user_id,
return_url: returnUrl,
channel,
device: getDeviceFromEnv(env),
channel: "wxpay" as PayChannel,
useJoinConfig: true,
});
};
if (device === "wechat") {
payWechatXorpaySync(base);
} else if (device === "h5") {
await redirectToPayH5(base);
} else {
redirectToPay({ ...base, device });
}
} catch (err) {
setError(err instanceof Error ? err.message : "操作失败");
} finally {