Files
kami_shop/views/index-wxpay.html
danial 1a75269a61 feat(payment):重构微信支付页面并优化支付流程
- 重新设计支付页面UI,采用毛玻璃效果和动态背景- 更新支付接口调用方式,从表单提交改为JSON请求
-优化支付签名验证逻辑,统一错误处理方式
- 修改京东支付接口参数和返回结构体
- 移除OpenTelemetry追踪相关代码
- 添加支付倒计时功能和本地存储支持
- 优化支付按钮加载状态和交互反馈
- 调整HTML结构和CSS样式,提升用户体验
2025-10-11 21:56:09 +08:00

435 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>微信支付</title>
<script src="../static/js/tailwindcss.js"></script>
<script src="../static/js/framer-motion.js"></script>
<style>
:root {
--primary: #07C160;
--primary-soft: rgba(7, 193, 96, .15);
--apple-white: #ffffff;
--gray: #8e8e93;
--light: #f5f5f7;
--border: #e5e5ea
}
* {
margin: 0;
padding: 0;
box-sizing: border-box
}
html, body {
height: 100%
}
body {
font-family: 'SF Pro Text', 'SF Pro Display', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: var(--apple-white);
color: #1d1d1f;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden
}
.container {
position: relative;
text-align: center;
padding: 40px 30px;
max-width: 420px;
width: 92%;
z-index: 10
}
.bg-shapes {
position: fixed;
inset: 0;
z-index: 1;
overflow: hidden
}
.bg-shape {
position: absolute;
border-radius: 50%;
filter: blur(70px)
}
.bg-shape:nth-child(1) {
top: -12%;
left: -12%;
width: 520px;
height: 520px;
background: linear-gradient(to right, rgba(7, 193, 96, .25), rgba(7, 193, 96, 0));
animation: float 16s ease-in-out infinite
}
.bg-shape:nth-child(2) {
bottom: -18%;
right: -12%;
width: 620px;
height: 620px;
background: linear-gradient(to left, rgba(7, 193, 96, .18), rgba(7, 193, 96, 0));
animation: float 22s ease-in-out infinite reverse
}
.bg-shape:nth-child(3) {
top: 42%;
left: 62%;
width: 320px;
height: 320px;
background: linear-gradient(to top, rgba(7, 193, 96, .12), rgba(7, 193, 96, 0));
animation: float 19s ease-in-out infinite 2s
}
@keyframes float {
0% {
transform: translate(0, 0) rotate(0)
}
50% {
transform: translate(28px, 18px) rotate(4deg)
}
100% {
transform: translate(0, 0) rotate(0)
}
}
.glass {
background: rgba(255, 255, 255, .75);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, .25);
box-shadow: 0 8px 32px rgba(0, 0, 0, .06);
border-radius: 20px
}
.logo-wrap {
width: 100px;
height: 100px;
margin: 0 auto 26px;
position: relative
}
.logo {
position: relative;
z-index: 2;
width: 100px;
height: 100px;
background: var(--primary);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
box-shadow: 0 10px 25px rgba(7, 193, 96, .35);
animation: scaleIn .5s cubic-bezier(.175, .885, .32, 1.275) forwards
}
.pulse {
position: absolute;
inset: 0;
margin: auto;
width: 150px;
height: 150px;
background: radial-gradient(circle, rgba(7, 193, 96, .8) 0%, rgba(7, 193, 96, 0) 70%);
border-radius: 50%;
z-index: 1;
opacity: 0;
animation: pulse 2s ease-in-out infinite
}
.pulse:nth-child(1) {
animation-delay: .0s
}
.pulse:nth-child(2) {
width: 180px;
height: 180px;
animation-delay: .5s
}
.pulse:nth-child(3) {
width: 210px;
height: 210px;
animation-delay: 1s
}
@keyframes scaleIn {
0% {
transform: scale(0);
opacity: 0
}
100% {
transform: scale(1);
opacity: 1
}
}
@keyframes pulse {
0% {
transform: translate(-0%, -0%) scale(.8);
opacity: 0
}
50% {
transform: scale(1);
opacity: .25
}
100% {
transform: scale(1.2);
opacity: 0
}
}
h1 {
font-size: 26px;
font-weight: 700;
margin-bottom: 10px;
opacity: 0;
animation: fadeInUp .8s ease .25s forwards
}
p.desc {
font-size: 15px;
color: var(--gray);
margin-bottom: 22px;
opacity: 0;
animation: fadeInUp .8s ease .4s forwards
}
.order-card {
padding: 22px;
margin-bottom: 22px;
text-align: left;
opacity: 0;
animation: fadeInUp .8s ease .55s forwards
}
.row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
font-size: 15px;
gap: 14px;
min-height: 24px
}
.row:last-child {
margin-bottom: 0;
padding-top: 18px;
border-top: 1px solid rgba(0, 0, 0, .06)
}
.label {
color: var(--gray);
flex-shrink: 0;
min-width: 80px;
text-align: left
}
.value {
font-weight: 500;
color: #1d1d1f;
text-align: left;
flex: 1;
word-break: break-word
}
.amount {
font-size: 20px;
color: var(--primary);
font-weight: 700
}
.countdown {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
background: var(--primary-soft);
color: var(--primary)
}
.btn {
width: 100%;
padding: 15px;
border: none;
border-radius: 14px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all .25s ease;
margin-top: 8px;
font-family: 'SF Pro Text', sans-serif;
background: var(--primary);
color: #fff;
box-shadow: 0 4px 15px rgba(7, 193, 96, .35);
opacity: 0;
animation: fadeInUp .8s ease .7s forwards
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 22px rgba(7, 193, 96, .45)
}
.btn:disabled {
background: #c7c7cc;
color: #fff;
cursor: not-allowed;
box-shadow: none;
transform: none
}
.btn.loading {
background: var(--primary);
color: #fff;
cursor: not-allowed;
box-shadow: none;
transform: none;
position: relative;
padding-left: 44px;
}
.btn.loading::before {
content: '';
position: absolute;
left: 15px;
top: 50%;
width: 18px;
height: 18px;
margin-top: -9px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.safe {
margin-top: 14px;
font-size: 12px;
color: #8e8e93
}
@keyframes fadeInUp {
0% {
transform: translateY(18px);
opacity: 0
}
100% {
transform: translateY(0);
opacity: 1
}
}
</style>
</head>
<body>
<div class="bg-shapes">
<div class="bg-shape"></div>
<div class="bg-shape"></div>
<div class="bg-shape"></div>
</div>
<div class="container">
<div class="logo-wrap">
<div class="pulse"></div>
<div class="pulse"></div>
<div class="pulse"></div>
<div class="logo">
<svg width="44" height="44" viewBox="0 0 24 24" fill="none">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15l-5-5 1.41-1.41L11 14.17l7.59-7.59L20 8l-9 9z"
fill="#fff"/>
</svg>
</div>
</div>
<h1>微信支付</h1>
<p class="desc">请在有效期内完成支付,超时订单将自动取消</p>
<div class="order-card glass">
<div class="row">
<span class="label">订单金额</span>
<span class="value amount">¥{{.mmValue}}</span>
</div>
<div class="row">
<span class="label">订单编号</span>
<span class="value">{{.orderNo}}</span>
</div>
<div class="row">
<span class="label">商品名称</span>
<span class="value">微信支付</span>
</div>
<div class="row">
<span class="label">剩余时间</span>
<span class="value"><span id="countdown" class="countdown">--:--:--</span></span>
</div>
</div>
<button id="payBtn" class="btn">确认支付</button>
<div class="safe">本次交易安全可靠</div>
</div>
<script>
const payBtn = document.getElementById('payBtn');
const countdownEl = document.getElementById('countdown');
const orderId = '{{.orderNo}}';
const EXP_KEY = 'order_expiry_' + orderId;
const now = Date.now();
let expiry = parseInt(localStorage.getItem(EXP_KEY) || '0', 10);
if (!expiry || expiry < now) {
expiry = now + 24 * 60 * 60 * 1000;
localStorage.setItem(EXP_KEY, String(expiry));
}
async function requestOrder() {
try {
payBtn.disabled = true;
payBtn.classList.add('loading');
payBtn.textContent = '支付处理中…';
const payload = {
productCode: '{{.productCode}}',
orderId: '{{.orderNo}}',
sign: '{{.sign}}',
returnUrl: '{{.returnUrl}}',
factMMValue: '{{.mmValue}}'
};
const resp = await fetch('/order/pay/original/jd', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const res = await resp.json();
if (res && res.code === 0 && res.data && res.data.wxPay) {
window.location.href = res.data.wxPay;
} else {
alert(res && res.msg ? res.msg : '下单失败');
payBtn.disabled = false;
payBtn.classList.remove('loading');
payBtn.textContent = '确认支付';
}
} catch (e) {
alert('网络异常,请稍后重试');
payBtn.disabled = false;
payBtn.classList.remove('loading');
payBtn.textContent = '确认支付';
}
}
payBtn.addEventListener('click', () => {
if (payBtn.disabled) return;
requestOrder();
});
</script>
</body>
</html>