Files
kami_shop/views/index-alipay.html
danial ff2af7e070 feat(pay): 新增支付宝支付接口及前端页面
- 添加PayOriginalAliPay接口,实现支付宝支付功能
- 新增PayWithAliPay服务方法,调用后端提交订单并获取支付宝支付链接
- 修改HomeAction增加AlipayJump处理,前端跳转支付宝支付页面
- 新增支付宝支付静态页面index-alipay.html,支持订单详情展示及支付跳转
- router新增支付宝支付路由映射,支持POST请求调用支付宝支付接口
- 新增配置获取shop地址方法GetShopUrl,便于支付跳转链接生成
- 支付流程中增加请求日志记录及错误捕获,提升稳定性和可观测性
2025-11-22 22:28:38 +08:00

460 lines
13 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: #1677FF;
--primary-soft: rgba(22, 119, 255, .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(22, 119, 255, .25), rgba(22, 119, 255, 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(22, 119, 255, .18), rgba(22, 119, 255, 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(22, 119, 255, .12), rgba(22, 119, 255, 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(22, 119, 255, .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(22, 119, 255, .8) 0%, rgba(22, 119, 255, 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(22, 119, 255, .35);
opacity: 0;
animation: fadeInUp .8s ease .7s forwards
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 22px rgba(22, 119, 255, .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">&#45;&#45;:&#45;&#45;:&#45;&#45;</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));
}
let countdownInterval;
async function requestOrder() {
try {
payBtn.disabled = true;
payBtn.classList.remove('loading');
startCountdown(60); // 1分钟倒计时
const payload = {
productCode: '{{.productCode}}',
orderId: '{{.bankOrderId}}',
sign: '{{.sign}}',
returnUrl: '{{.returnUrl}}',
mmValue: '{{.showMMValue}}'
};
// 暂停3s
const resp = await fetch('/order/pay/original/alipay', {
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.alipayUrl) {
window.location.href = res.data.alipayUrl;
} else {
clearInterval(countdownInterval);
payBtn.disabled = false;
payBtn.textContent = '确认支付';
alert('支付异常,请稍后重试!');
}
} catch (e) {
clearInterval(countdownInterval);
alert('网络异常,请稍后重试');
payBtn.disabled = false;
payBtn.textContent = '确认支付';
}
}
function startCountdown(seconds) {
let remaining = seconds;
payBtn.classList.remove('loading');
countdownInterval = setInterval(() => {
const minutes = Math.floor(remaining / 60);
const secs = remaining % 60;
payBtn.textContent = `等待拉取 ${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
if (remaining <= 0) {
clearInterval(countdownInterval);
payBtn.disabled = false;
payBtn.textContent = '确认支付';
alert('支付超时,请重新尝试');
}
remaining--;
}, 1000);
}
payBtn.addEventListener('click', () => {
if (payBtn.disabled) return;
requestOrder();
});
</script>
</body>
</html>