style(miniprogram): 确认支付页重构对齐扫码登录确认页

整屏一页式布局(顶栏返回+居中主体+贴底CTA):未登录先走微信/手机号登录门;
确认态展示大号实付金额与订单号;支付成功为紫圈勾选+返回首页;订单结束/过期独立态。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Pine
2026-08-30 22:38:07 +08:00
parent fb7e0edd8f
commit 9fb76fe479
3 changed files with 189 additions and 120 deletions
+1 -1
View File
@@ -1 +1 @@
export default { navigationStyle: 'custom' };
export default { navigationBarTitleText: '确认支付' };
+151 -65
View File
@@ -1,49 +1,92 @@
import { View, Text, Button } from '@tarojs/components';
import { View, Text, Button, Input } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { useState, useEffect } from 'react';
import { request } from '@/utils/api';
import { isAuthed } from '@/utils/auth';
import { PageHeader } from '@/components/TopInset';
import { isAuthed, wxLogin, phoneLogin, sendCode } from '@/utils/auth';
import './index.scss';
const MICRO = 1000000; // 1 元 = 1,000,000 微元
const yuan = (micro) => `¥${((micro || 0) / MICRO).toFixed(2)}`;
/** 小程序确认支付页:微信扫桌面端「算力充值」小程序码进入(scene=订单号),
* 取 JSAPI 参数后 wx.requestPayment 拉起支付(与扫码登录同构的确认页)。 */
/**
* 小程序扫码支付确认页(对齐「扫码登录确认页」交互与视觉):
* 微信扫桌面端「算力充值」小程序码自动打开,scene=订单号。
* 规则:未登录 → 先登录(微信一键 / 手机号验证码);登录后确认订单并 wx.requestPayment 拉起支付。
*/
export default function PayConfirm() {
const [scene] = useState(() => {
const router = Taro.getCurrentInstance ? Taro.getCurrentInstance().router : null;
const raw = (router && (router.params.scene || router.params.order_no)) || '';
return decodeURIComponent(raw);
});
const [info, setInfo] = useState(null); // { order: {...}, pay_params }
const [finished, setFinished] = useState(false);
const [err, setErr] = useState('');
const [scene, setScene] = useState('');
const [authed, setAuthed] = useState(isAuthed());
const [info, setInfo] = useState(null); // { order, pay_params, finished }
const [busy, setBusy] = useState(false);
const [paid, setPaid] = useState(false);
const [msg, setMsg] = useState('');
const [done, setDone] = useState(false); // 支付成功
const [ended, setEnded] = useState(false); // 订单已结束/过期
const [loginMode, setLoginMode] = useState('wx');
const [gatePhone, setGatePhone] = useState('');
const [gateCode, setGateCode] = useState('');
const load = async () => {
if (!scene) { setErr('缺少订单信息(scene 为空)'); return; }
// 冷启动/扫码进入:读 router.params.scenewxacode 携带订单号)
useEffect(() => {
try {
const inst = Taro.getCurrentInstance();
const router = (inst && inst.router) || {};
const params = (router && (router.params || router.query)) || {};
const s = String(params.scene || params.order_no || router.scene || '');
if (s) setScene(decodeURIComponent(s));
} catch { /* 忽略取参异常 */ }
}, []);
// 登录态就绪后拉取订单
useEffect(() => {
if (!authed || !scene) return;
(async () => {
setBusy(true); setMsg('');
try {
const r = await request('GET', `/opc/compute/recharge/orders/${scene}/pay-params`, {}, true);
setInfo(r);
if (r.finished) setEnded(true);
} catch (e) { setMsg((e && e.message) || '加载订单失败'); }
finally { setBusy(false); }
})();
}, [authed, scene]);
// ── 登录门(与扫码登录页一致)──
const wxLoginNow = async () => {
setBusy(true); setMsg('');
try {
const { code } = await Taro.login();
await wxLogin({ code });
setAuthed(true);
} catch (e) { setMsg((e && e.message) || '微信登录失败'); }
finally { setBusy(false); }
};
const sendSms = async () => {
setMsg('');
if (!/^1\d{10}$/.test(gatePhone)) { setMsg('请输入正确的 11 位手机号'); return; }
setBusy(true);
try {
const r = await request('GET', `/opc/compute/recharge/orders/${scene}/pay-params`, {}, true);
setInfo(r);
setFinished(!!r.finished);
setErr('');
} catch (e) { setErr((e && e.message) || '加载订单失败'); }
const r = await sendCode(gatePhone);
setMsg(`验证码已发送${r.debugCode ? `(演示 ${r.debugCode}` : ''}`);
} catch (e) { setMsg((e && e.message) || '发送失败'); }
finally { setBusy(false); }
};
const phoneLoginNow = async () => {
setMsg('');
if (!/^1\d{10}$/.test(gatePhone) || !gateCode) { setMsg('请填写手机号与验证码'); return; }
setBusy(true);
try { await phoneLogin(gatePhone, gateCode); setAuthed(true); }
catch (e) { setMsg((e && e.message) || '登录失败'); }
finally { setBusy(false); }
};
// 进入即取一次(登录态就绪时)
useEffect(() => { if (isAuthed()) load(); }, []);
// ── 支付 ──
const pay = async () => {
if (!info || !info.pay_params) return;
const pp = info.pay_params;
setMsg('');
setBusy(true);
try {
const pp = info.pay_params;
await new Promise((resolve, reject) => {
Taro.requestPayment({
timeStamp: pp.timeStamp,
@@ -55,64 +98,107 @@ export default function PayConfirm() {
fail: reject
});
});
setPaid(true);
Taro.showToast({ title: '支付成功,余额到账中', icon: 'success' });
// 轮询确认后端到账(回调/对账兜底)
setDone(true);
// 轮询后端到账(回调/对账兜底),到账后静默结束
let tries = 0;
const timer = setInterval(async () => {
tries += 1;
if (tries > 40) { clearInterval(timer); return; }
try {
const st = await request('POST', `/opc/compute/recharge/orders/${scene}/status`, {}, true);
if (st.paid) { clearInterval(timer); Taro.showToast({ title: '充值成功,已到账', icon: 'success' }); }
if (st.paid) clearInterval(timer);
} catch { /* 忽略瞬时错误 */ }
}, 1500);
} catch (e) {
const msg = (e && e.errMsg) || '';
if (msg.indexOf('cancel') >= 0) Taro.showToast({ title: '已取消支付', icon: 'none' });
else Taro.showToast({ title: '支付失败,请重试', icon: 'none' });
const m = (e && e.errMsg) || '';
setMsg(m.indexOf('cancel') >= 0 ? '已取消支付' : '支付失败,请重试');
} finally { setBusy(false); }
};
const cancel = () => { Taro.navigateBack().catch(() => Taro.switchTab({ url: '/pages/index/index' })); };
const order = info && info.order;
return (
<View className="page-in">
<PageHeader no="05" title="确认支付" en="PAY" back />
<View className="pay">
{/* 顶栏返回 */}
<View className="pay-top">
<View className="pay-back" onClick={cancel}><View className="ic-arrow-left" /></View>
</View>
{!isAuthed() && (
<View className="card pay-card">
<Text className="muted">请先在我的中登录后再扫码支付</Text>
</View>
)}
{isAuthed() && (
<View className="card pay-card">
{!scene ? (
<Text className="muted">缺少订单信息请从桌面端重新生成充值码</Text>
) : err ? (
<Text className="pay-err">{err}</Text>
) : !order ? (
<Text className="muted">{busy ? '加载中…' : '下拉或点击按钮获取订单'}</Text>
) : paid || finished ? (
<View className="pay-done">
<Text className="pay-title">{paid ? '支付成功' : '订单已结束'}</Text>
<Text className="muted">{paid ? '算力余额将自动到账,可在「算力管理」中查看。' : (order.status === 'credited' ? '该订单已完成充值。' : '订单已过期或取消,请重新下单。')}</Text>
{!authed ? (
<>
{/* 登录门:与扫码登录页一致 */}
<View className="pay-body">
<View className="pay-ico ic-lock lock" />
<Text className="pay-title">请先登录小程序</Text>
<Text className="pay-desc">为完成扫码支付请先登录小程序复用你的微信/手机号身份</Text>
<View className="pay-tabs">
<Text className={`pay-tab${loginMode === 'wx' ? ' on' : ''}`} onClick={() => setLoginMode('wx')}>微信一键登录</Text>
<Text className={`pay-tab${loginMode === 'phone' ? ' on' : ''}`} onClick={() => setLoginMode('phone')}>手机号登录</Text>
</View>
) : (
<View className="pay-body">
<Text className="pay-label">算力充值</Text>
<Text className="pay-amount">{yuan(order.quota_micro)}</Text>
<Text className="muted">订单号 {order.order_no}</Text>
<Button className="btn-main pay-btn" loading={busy} onClick={pay}>立即支付</Button>
<Text className="muted pay-hint">由微信支付提供收款服务支付后余额自动到账</Text>
</View>
)}
{!order && !err && scene ? (
<Button className="btn-ghost pay-btn" loading={busy} onClick={load}>获取订单</Button>
) : null}
</View>
{loginMode === 'wx' ? (
<Button className="btn btn-main" loading={busy} onClick={wxLoginNow}>微信一键登录</Button>
) : (
<View className="pay-field">
<Input className="input" placeholder="11 位手机号" value={gatePhone} onInput={(e) => setGatePhone(e.detail.value)} />
<View className="pay-code">
<Input className="input" placeholder="验证码" value={gateCode} onInput={(e) => setGateCode(e.detail.value)} />
<Button className="btn btn-ghost btn-sm pay-send" disabled={busy} onClick={sendSms}>发送验证码</Button>
</View>
<Button className="btn btn-main" loading={busy} onClick={phoneLoginNow}>登录</Button>
</View>
)}
</View>
<View className="pay-foot">
<Text className="pay-note">登录后即可确认并完成本次充值</Text>
</View>
</>
) : (
<>
<View className="pay-body">
{done ? (
<>
<View className="pay-circle"><View className="ic-check" /></View>
<Text className="pay-title">支付成功</Text>
<Text className="pay-desc">算力余额已到账可回桌面端或算力管理查看</Text>
</>
) : ended ? (
<>
<View className="pay-ico lock" />
<Text className="pay-title">订单已结束</Text>
<Text className="pay-desc">该订单已完成充值或已过期请在桌面端重新发起充值</Text>
</>
) : !order ? (
<>
<Text className="pay-title">{msg ? '无法获取订单' : '正在获取订单…'}</Text>
<Text className="pay-desc">{msg || '请从桌面端「算力中心 → 充值」扫码进入'}</Text>
</>
) : (
<>
<Text className="pay-title">确认支付</Text>
<Text className="pay-desc">确认使用当前账号完成本次算力充值</Text>
<Text className="pay-amount">{yuan(order.quota_micro)}</Text>
<Text className="pay-scene">订单号 {order.order_no}</Text>
</>
)}
</View>
<View className="pay-foot">
{done ? (
<Button className="btn btn-main pay-cta" onClick={() => Taro.switchTab({ url: '/pages/index/index' })}>返回首页</Button>
) : ended || !order ? (
<Button className="btn btn-ghost pay-cta" onClick={cancel}>返回</Button>
) : (
<>
<Text className="pay-note">由微信支付提供收款服务支付后余额自动到账</Text>
<Button className="btn btn-main pay-cta" loading={busy} onClick={pay}>确认支付</Button>
<Button className="btn btn-ghost pay-cancel" disabled={busy} onClick={cancel}>取消</Button>
</>
)}
</View>
</>
)}
{msg && !done && <Text className="pay-err">{msg}</Text>}
</View>
);
}
+37 -54
View File
@@ -1,59 +1,42 @@
/* 小程序确认支付页:沿用全局 Soft UI 卡片,补支付专属样式 */
@import '../../app.scss';
/* 小程序扫码支付确认页 · 整屏一页(无滚动),样式对齐「扫码登录确认页」 */
.pay { position: relative; z-index: 2; height: 100vh; height: 100dvh; overflow: hidden;
display: flex; flex-direction: column; padding: 16rpx 40rpx 28rpx; box-sizing: border-box; }
.pay-card {
margin: 0 24rpx;
padding: 40rpx 32rpx;
min-height: 300rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16rpx;
text-align: center;
}
/* 顶栏返回 */
.pay-top { flex-shrink: 0; display: flex; align-items: center; height: 60rpx; }
.pay-back { width: 60rpx; height: 60rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center;
background: rgba(255,255,255,.55); border: 1px solid rgba(99,132,255,.16); backdrop-filter: blur(8px); }
.pay-back .ic-arrow-left { width: 30rpx; height: 30rpx; background: #23315c; }
.pay-body {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
}
/* 中间内容(垂直居中) */
.pay-body { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
text-align: center; gap: 18rpx; padding: 12rpx 0; }
.pay-ico { width: 92rpx; height: 92rpx; border-radius: 50%; flex-shrink: 0;
display: block; -webkit-mask-position: center; -webkit-mask-repeat: no-repeat; -webkit-mask-size: 46rpx;
mask-position: center; mask-repeat: no-repeat; mask-size: 46rpx; }
.pay-ico.lock { background: #9aa4c6; }
.pay-title { display: block; font-size: 42rpx; font-weight: 800; color: #23315c; letter-spacing: -0.02em; line-height: 1.2; }
.pay-desc { display: block; font-size: 26rpx; color: #4a5580; line-height: 1.6; max-width: 540rpx; }
.pay-amount { display: block; font-size: 72rpx; font-weight: 800; color: #23315c; letter-spacing: -0.02em; }
.pay-scene { display: block; font-size: 23rpx; color: #9aa4c6; }
.pay-label {
font-size: 26rpx;
color: #8a8a93;
}
/* 成功:大圆圈勾选 */
.pay-circle { width: 156rpx; height: 156rpx; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center;
border: 5rpx solid #8f7bff; box-shadow: 0 0 36rpx rgba(143,123,255,.25); }
.pay-circle .ic-check { width: 76rpx; height: 76rpx; background: #8f7bff; }
.pay-amount {
font-size: 72rpx;
font-weight: 700;
color: #2b2f36;
}
/* 底部 CTA */
.pay-foot { flex-shrink: 0; display: flex; flex-direction: column; gap: 12rpx; margin-top: auto; }
.pay-note { display: block; font-size: 22rpx; color: #9aa4c6; text-align: center; line-height: 1.5; }
.pay-cta { width: 100%; }
.pay-cancel { width: 100%; }
.pay-btn {
width: 420rpx;
margin-top: 16rpx;
}
.pay-hint {
font-size: 22rpx;
}
.pay-err {
font-size: 26rpx;
color: #e5484d;
}
.pay-done {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
}
.pay-title {
font-size: 36rpx;
font-weight: 700;
color: #2b2f36;
}
/* 登录门 tab / 表单(对齐扫码登录页) */
.pay-tabs { display: flex; gap: 8rpx; margin: 20rpx 0; padding: 6rpx; border-radius: 999px; background: #eef1ff; border: 1px solid rgba(99,132,255,.14); width: 100%; }
.pay-tab { flex: 1; text-align: center; padding: 12rpx 0; border-radius: 999px; color: #7a86b0; font-size: 26rpx; font-weight: 600; }
.pay-tab.on { background: linear-gradient(135deg, #6b8bff, #8f7bff); color: #fff; box-shadow: 0 6rpx 18rpx rgba(98,132,255,.28); }
.pay-field { display: flex; flex-direction: column; gap: 12rpx; width: 100%; }
.pay-code { display: flex; gap: 12rpx; align-items: center; }
.pay-code .input { flex: 1; margin: 0; }
.pay-send { font-size: 24rpx; padding: 0 22rpx; line-height: 2.4; flex-shrink: 0; }
.pay-err { display: block; margin-top: 10rpx; font-size: 24rpx; color: #e05252; text-align: center; }