's'
This commit is contained in:
@@ -267,19 +267,20 @@ export async function POST(request: NextRequest) {
|
||||
<style>
|
||||
*{box-sizing:border-box;}
|
||||
body{font-family:system-ui,-apple-system,sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);}
|
||||
.card{background:#fff;border-radius:20px;padding:40px;box-shadow:0 25px 50px -12px rgba(0,0,0,.25);text-align:center;max-width:360px;}
|
||||
.card{background:#fff;border-radius:20px;padding:40px;box-shadow:0 25px 50px -12px rgba(0,0,0,.25);max-width:420px;}
|
||||
h2{color:#1e293b;margin:0 0 8px;font-size:1.5rem;}
|
||||
.hint{color:#64748b;font-size:14px;margin-bottom:24px;}
|
||||
.countdown-wrap{width:220px;height:220px;margin:20px auto;position:relative;}
|
||||
.hint{color:#64748b;font-size:14px;margin-bottom:20px;}
|
||||
.wait-row{display:flex;align-items:center;justify-content:center;gap:24px;margin:20px 0;}
|
||||
.qr-wrap img{width:200px;height:200px;border-radius:12px;border:2px solid #e2e8f0;background:#fff;}
|
||||
.countdown-wrap{width:72px;height:72px;flex-shrink:0;position:relative;}
|
||||
.countdown-ring{transform:rotate(-90deg);}
|
||||
.countdown-ring circle{fill:none;stroke-width:6;transition:stroke-dashoffset .5s linear;}
|
||||
.countdown-ring circle{fill:none;stroke-width:5;transition:stroke-dashoffset .5s linear;}
|
||||
.countdown-ring .bg{stroke:#e2e8f0;}
|
||||
.countdown-ring .progress{stroke:#22c55e;stroke-linecap:round;}
|
||||
.countdown-inner{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;}
|
||||
.countdown-inner img{margin-bottom:4px;}
|
||||
.countdown-num{font-size:1.1rem;font-weight:700;color:#1e293b;line-height:1;}
|
||||
.countdown-unit{font-size:10px;color:#94a3b8;}
|
||||
.tip{font-size:13px;color:#64748b;margin-top:20px;}
|
||||
.countdown-inner{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;}
|
||||
.countdown-num{font-size:1rem;font-weight:700;color:#1e293b;line-height:1;}
|
||||
.countdown-unit{font-size:9px;color:#94a3b8;}
|
||||
.tip{font-size:13px;color:#64748b;margin-top:16px;}
|
||||
.success-wrap{display:none;}
|
||||
.success-wrap.show{display:block;}
|
||||
.wait-wrap.hide{display:none;}
|
||||
@@ -296,15 +297,19 @@ h2{color:#1e293b;margin:0 0 8px;font-size:1.5rem;}
|
||||
<div class="wait-wrap" id="waitWrap">
|
||||
<h2>${esc(title)}</h2>
|
||||
<p class="hint">${esc(hint)}</p>
|
||||
<div class="countdown-wrap">
|
||||
<svg class="countdown-ring" width="220" height="220" viewBox="0 0 220 220">
|
||||
<circle class="bg" cx="110" cy="110" r="100"/>
|
||||
<circle class="progress" id="progressCircle" cx="110" cy="110" r="100" stroke-dasharray="628" stroke-dashoffset="0"/>
|
||||
</svg>
|
||||
<div class="countdown-inner">
|
||||
<img src="${imgSrc}" alt="支付二维码" style="width:140px;height:140px;border-radius:8px;background:#fff;"/>
|
||||
<span class="countdown-num" id="countdownNum">60</span>
|
||||
<span class="countdown-unit">秒后返回</span>
|
||||
<div class="wait-row">
|
||||
<div class="countdown-wrap">
|
||||
<svg class="countdown-ring" width="72" height="72" viewBox="0 0 72 72">
|
||||
<circle class="bg" cx="36" cy="36" r="32"/>
|
||||
<circle class="progress" id="progressCircle" cx="36" cy="36" r="32" stroke-dasharray="201" stroke-dashoffset="0"/>
|
||||
</svg>
|
||||
<div class="countdown-inner">
|
||||
<span class="countdown-num" id="countdownNum">5:00</span>
|
||||
<span class="countdown-unit">分</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="qr-wrap">
|
||||
<img src="${imgSrc}" alt="支付二维码" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="tip pulse" id="tip">等待支付中...</p>
|
||||
@@ -327,12 +332,13 @@ h2{color:#1e293b;margin:0 0 8px;font-size:1.5rem;}
|
||||
var countdownNum=document.getElementById("countdownNum");
|
||||
var progressCircle=document.getElementById("progressCircle");
|
||||
var successCountdown=document.getElementById("successCountdown");
|
||||
var maxT=60;
|
||||
var maxT=300;
|
||||
var left=maxT;
|
||||
var circumference=2*Math.PI*100;
|
||||
var circumference=2*Math.PI*32;
|
||||
function fmt(t){var m=Math.floor(t/60),s=t%60;return m+":"+(s<10?"0":"")+s;}
|
||||
var countdownTimer=setInterval(function(){
|
||||
left--;
|
||||
countdownNum.textContent=left;
|
||||
countdownNum.textContent=left>60?fmt(left):left;
|
||||
progressCircle.style.strokeDashoffset=circumference*(1-left/maxT);
|
||||
if(left<=0){
|
||||
clearInterval(countdownTimer);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const COOKIE_NAME = "nomadvip_pay_order";
|
||||
const POLL_INTERVAL = 2000;
|
||||
const MAX_POLLS = 30;
|
||||
const MAX_POLLS = 160;
|
||||
const MAX_AGE_MS = 15 * 60 * 1000;
|
||||
|
||||
function getCookie(name: string): string | null {
|
||||
@@ -31,7 +31,7 @@ function parsePayOrderCookie(raw: string | null): [string | null, boolean] {
|
||||
return [orderId, true];
|
||||
}
|
||||
|
||||
export function usePayStatusPoll(onPaid: () => void, onTimeout?: () => void): boolean {
|
||||
export function usePayStatusPoll(onPaid: (orderId?: string) => void, onTimeout?: () => void): boolean {
|
||||
const [polling, setPolling] = useState(false);
|
||||
const onPaidRef = useRef(onPaid);
|
||||
const onTimeoutRef = useRef(onTimeout);
|
||||
@@ -64,7 +64,7 @@ export function usePayStatusPoll(onPaid: () => void, onTimeout?: () => void): bo
|
||||
clearInterval(timer);
|
||||
clearCookie(COOKIE_NAME);
|
||||
setPolling(false);
|
||||
onPaidRef.current();
|
||||
onPaidRef.current(orderId);
|
||||
}
|
||||
} catch {
|
||||
// 继续轮询
|
||||
|
||||
12
app/page.tsx
12
app/page.tsx
@@ -14,7 +14,7 @@ const PB_URL =
|
||||
process.env.NEXT_PUBLIC_POCKETBASE_URL || "https://pocketbase.hackrobot.cn";
|
||||
|
||||
/** 微信内支付渠道:zpay | xorpay,改此参数即可切换 */
|
||||
const WECHAT_PAY_PROVIDER: "zpay" | "xorpay" = "zpay";
|
||||
const WECHAT_PAY_PROVIDER: "zpay" | "xorpay" = "xorpay";
|
||||
|
||||
/** PC/H5:form POST 到 /api/pay,有中转页,默认 zpay */
|
||||
function redirectToPayZpay(params: {
|
||||
@@ -225,9 +225,15 @@ export default function Home() {
|
||||
}, [userName]);
|
||||
|
||||
// 微信/手机 H5 支付完成后可能不跳转,轮询检测到已支付则刷新
|
||||
usePayStatusPoll(() => {
|
||||
usePayStatusPoll((orderId) => {
|
||||
let uid = getStorage("userId");
|
||||
if (!uid && orderId?.includes("_order_")) {
|
||||
const prefix = orderId.split("_order_")[0];
|
||||
const parts = prefix.split("_");
|
||||
if (parts.length >= 2) uid = parts.slice(0, -1).join("_");
|
||||
}
|
||||
if (uid) saveUserName(uid);
|
||||
savePaidType("vip");
|
||||
saveUserName(getStorage("userId") || "");
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll";
|
||||
|
||||
const COOKIE_NAME = "nomadvip_pay_order";
|
||||
|
||||
function getCookie(name: string): string | null {
|
||||
if (typeof document === "undefined") return null;
|
||||
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
}
|
||||
|
||||
function parseUserIdFromOrderId(orderId: string): string {
|
||||
if (!orderId?.includes("_order_")) return "";
|
||||
const prefix = orderId.split("_order_")[0];
|
||||
const parts = prefix.split("_");
|
||||
return parts.length >= 2 ? parts.slice(0, -1).join("_") : "";
|
||||
}
|
||||
|
||||
function SuccessWithRedirect() {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => router.replace("/"), 2000);
|
||||
const t = setTimeout(() => {
|
||||
window.location.href = "/";
|
||||
}, 2000);
|
||||
return () => clearTimeout(t);
|
||||
}, [router]);
|
||||
}, []);
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4">
|
||||
<div className="w-full max-w-md rounded-2xl bg-white p-10 text-center shadow-lg">
|
||||
@@ -20,12 +34,13 @@ function SuccessWithRedirect() {
|
||||
<h2 className="text-2xl font-bold text-slate-800">支付成功</h2>
|
||||
<p className="mt-3 text-slate-500">感谢您的支持,VIP 权益已开通</p>
|
||||
<p className="mt-2 text-xs text-slate-400">2 秒后自动返回首页</p>
|
||||
<Link
|
||||
<a
|
||||
href="/"
|
||||
onClick={(e) => { e.preventDefault(); window.location.href = "/"; }}
|
||||
className="mt-8 inline-flex items-center gap-2 rounded-full bg-sky-500 px-8 py-3 font-semibold text-white transition-colors hover:bg-sky-600"
|
||||
>
|
||||
← 立即返回首页
|
||||
</Link>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -35,8 +50,19 @@ export default function PaidPage() {
|
||||
const [status, setStatus] = useState<"pending" | "success" | "fail">("pending");
|
||||
const [fromUrl, setFromUrl] = useState<string | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
usePayStatusPoll(() => setStatus("success"), () => router.replace("/"));
|
||||
usePayStatusPoll((orderId) => {
|
||||
const uid = parseUserIdFromOrderId(orderId || "");
|
||||
const raw = getCookie(COOKIE_NAME);
|
||||
const oid = raw?.split("|")[0]?.trim() || orderId || "";
|
||||
const userId = uid || parseUserIdFromOrderId(oid);
|
||||
if (userId) {
|
||||
try {
|
||||
localStorage.setItem("paidType", "vip");
|
||||
localStorage.setItem("userName", userId);
|
||||
} catch {}
|
||||
}
|
||||
setStatus("success");
|
||||
}, () => { window.location.href = "/"; });
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
@@ -78,12 +104,13 @@ export default function PaidPage() {
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-slate-800">支付未完成</h2>
|
||||
<p className="mt-3 text-slate-500">支付已取消或未成功,请重试</p>
|
||||
<Link
|
||||
<a
|
||||
href="/"
|
||||
onClick={(e) => { e.preventDefault(); window.location.href = "/"; }}
|
||||
className="mt-8 inline-flex items-center gap-2 rounded-full border-2 border-slate-300 px-8 py-3 font-semibold text-slate-700 transition-colors hover:border-slate-400 hover:bg-slate-50"
|
||||
>
|
||||
← 返回首页
|
||||
</Link>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user