style(miniprogram): 确认支付页重构对齐扫码登录确认页
整屏一页式布局(顶栏返回+居中主体+贴底CTA):未登录先走微信/手机号登录门; 确认态展示大号实付金额与订单号;支付成功为紫圈勾选+返回首页;订单结束/过期独立态。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
export default { navigationStyle: 'custom' };
|
||||
export default { navigationBarTitleText: '确认支付' };
|
||||
|
||||
@@ -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.scene(wxacode 携带订单号)
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user