'c1'
This commit is contained in:
@@ -68,36 +68,130 @@ function buildPayHtml(
|
||||
function buildQrHtml(
|
||||
imgSrc: string,
|
||||
returnUrl: string,
|
||||
orderId: string
|
||||
orderId: string,
|
||||
opts?: { channel?: string; successDesc?: string; confirmUrl?: string }
|
||||
): string {
|
||||
const ch = (opts?.channel || "wxpay").toLowerCase();
|
||||
const isWx = ch === "wxpay" || ch === "wx" || ch === "wechat";
|
||||
const title = isWx ? "微信扫码支付" : "支付宝扫码支付";
|
||||
const hint = isWx ? "请使用微信扫描下方二维码完成支付" : "请使用支付宝扫描下方二维码完成支付";
|
||||
const successDesc = opts?.successDesc || "支付成功,即将跳转";
|
||||
const confirmUrl = opts?.confirmUrl || "/api/pay/confirm";
|
||||
const esc = (s: string) =>
|
||||
s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c);
|
||||
return `<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>扫码支付</title>
|
||||
<style>body{font-family:system-ui;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f0f4f8;}h2{color:#333;margin-bottom:8px;}p{color:#64748b;font-size:14px;}img{width:220px;height:220px;margin:20px 0;border:1px solid #e2e8f0;border-radius:8px;background:#fff;}</style></head>
|
||||
<title>${esc(title)}</title>
|
||||
<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);max-width:420px;}
|
||||
h2{color:#1e293b;margin:0 0 8px;font-size:1.5rem;}
|
||||
.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: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;}
|
||||
.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;}
|
||||
.success-icon{font-size:4rem;margin-bottom:16px;animation:scaleIn .4s ease;}
|
||||
.success-title{font-size:1.25rem;color:#16a34a;font-weight:600;margin-bottom:8px;}
|
||||
.success-desc{color:#64748b;font-size:14px;margin-bottom:24px;}
|
||||
.success-countdown{font-size:1.5rem;font-weight:700;color:#667eea;}
|
||||
@keyframes scaleIn{from{transform:scale(0);opacity:0;}to{transform:scale(1);opacity:1;}}
|
||||
@keyframes pulse{0%,100%{opacity:1;}50%{opacity:.7;}}
|
||||
.pulse{animation:pulse 2s ease-in-out infinite;}
|
||||
</style></head>
|
||||
<body>
|
||||
<h2>请扫码支付</h2>
|
||||
<p>支付成功后将自动跳转</p>
|
||||
<img src="${imgSrc}" alt="支付二维码" />
|
||||
<div class="card">
|
||||
<div class="wait-wrap" id="waitWrap">
|
||||
<h2>${esc(title)}</h2>
|
||||
<p class="hint">${esc(hint)}</p>
|
||||
<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>
|
||||
</div>
|
||||
<div class="success-wrap" id="successWrap">
|
||||
<div class="success-icon">✅</div>
|
||||
<div class="success-title">支付成功</div>
|
||||
<div class="success-desc">${esc(successDesc)}</div>
|
||||
<div class="success-countdown" id="successCountdown">3 秒后跳转</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
try {
|
||||
localStorage.setItem("join_pay_order", ${JSON.stringify(
|
||||
`${orderId}|${Date.now()}`
|
||||
)});
|
||||
} catch (e) {}
|
||||
try{localStorage.setItem("join_pay_order",${JSON.stringify(`${orderId}|${Date.now()}`)});}catch(e){}
|
||||
var orderId=${JSON.stringify(orderId)};
|
||||
var returnUrl=${JSON.stringify(returnUrl)};
|
||||
if(!orderId){return;}
|
||||
var waitWrap=document.getElementById("waitWrap");
|
||||
var successWrap=document.getElementById("successWrap");
|
||||
var tip=document.getElementById("tip");
|
||||
var countdownNum=document.getElementById("countdownNum");
|
||||
var progressCircle=document.getElementById("progressCircle");
|
||||
var successCountdown=document.getElementById("successCountdown");
|
||||
var maxT=300,left=maxT;
|
||||
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>60?fmt(left):left;
|
||||
progressCircle.style.strokeDashoffset=circumference*(1-left/maxT);
|
||||
if(left<=0){clearInterval(countdownTimer);tip.textContent="支付超时,返回首页";setTimeout(function(){window.location.href="/";},800);}
|
||||
},1000);
|
||||
function showSuccess(){
|
||||
clearInterval(countdownTimer);
|
||||
waitWrap.classList.add("hide");
|
||||
successWrap.classList.add("show");
|
||||
var s=3;
|
||||
var t=setInterval(function(){
|
||||
s--;
|
||||
successCountdown.textContent=s>0?s+" 秒后跳转":"跳转中…";
|
||||
if(s<=0){clearInterval(t);window.location.href=returnUrl;}
|
||||
},1000);
|
||||
}
|
||||
var confirmUrl=${JSON.stringify(confirmUrl)};
|
||||
function doConfirm(attempt){
|
||||
if(attempt>=3){showSuccess();return;}
|
||||
fetch(confirmUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({order_id:orderId})})
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(cd){
|
||||
if(cd&&cd.ok){showSuccess();}
|
||||
else{setTimeout(function(){doConfirm(attempt+1);},800);}
|
||||
})
|
||||
.catch(function(){setTimeout(function(){doConfirm(attempt+1);},800);});
|
||||
}
|
||||
function check(){
|
||||
fetch("/api/pay/status?order_id="+encodeURIComponent(orderId), { cache: "no-store" })
|
||||
fetch("/api/pay/status?order_id="+encodeURIComponent(orderId),{cache:"no-store"})
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(d){
|
||||
if(d.ok&&d.paid){window.location.href=returnUrl;return;}
|
||||
setTimeout(check,1200);
|
||||
if(d.ok&&d.paid){doConfirm(0);return;}
|
||||
setTimeout(check,500);
|
||||
})
|
||||
.catch(function(){setTimeout(check,1200);});
|
||||
.catch(function(){setTimeout(check,500);});
|
||||
}
|
||||
check();
|
||||
setTimeout(check,500);
|
||||
})();
|
||||
</script>
|
||||
</body></html>`;
|
||||
@@ -308,7 +402,10 @@ export async function POST(request: NextRequest) {
|
||||
] || c)
|
||||
);
|
||||
const rawOrderId = String(resData.order_id || created.orderId).trim();
|
||||
const html = buildQrHtml(imgSrc, safeReturnUrl, rawOrderId);
|
||||
const html = buildQrHtml(imgSrc, safeReturnUrl, rawOrderId, {
|
||||
channel: resData.channel || "wxpay",
|
||||
successDesc: "支付成功,课程已解锁",
|
||||
});
|
||||
const response = new NextResponse(html, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/html; charset=utf-8" },
|
||||
|
||||
Reference in New Issue
Block a user