diff --git a/miniprogram/src/pages/pay/index.config.js b/miniprogram/src/pages/pay/index.config.js index a4049ce..1fd8195 100644 --- a/miniprogram/src/pages/pay/index.config.js +++ b/miniprogram/src/pages/pay/index.config.js @@ -1 +1 @@ -export default { navigationStyle: 'custom' }; +export default { navigationBarTitleText: '确认支付' }; diff --git a/miniprogram/src/pages/pay/index.jsx b/miniprogram/src/pages/pay/index.jsx index 4444a99..70d7435 100644 --- a/miniprogram/src/pages/pay/index.jsx +++ b/miniprogram/src/pages/pay/index.jsx @@ -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 ( - - + + {/* 顶栏返回 */} + + + - {!isAuthed() && ( - - 请先在「我的」中登录后再扫码支付。 - - )} - - {isAuthed() && ( - - {!scene ? ( - 缺少订单信息,请从桌面端重新生成充值码。 - ) : err ? ( - {err} - ) : !order ? ( - {busy ? '加载中…' : '下拉或点击按钮获取订单'} - ) : paid || finished ? ( - - {paid ? '支付成功' : '订单已结束'} - {paid ? '算力余额将自动到账,可在「算力管理」中查看。' : (order.status === 'credited' ? '该订单已完成充值。' : '订单已过期或取消,请重新下单。')} + {!authed ? ( + <> + {/* 登录门:与扫码登录页一致 */} + + + 请先登录小程序 + 为完成扫码支付,请先登录小程序(复用你的微信/手机号身份)。 + + setLoginMode('wx')}>微信一键登录 + setLoginMode('phone')}>手机号登录 - ) : ( - - 算力充值 - {yuan(order.quota_micro)} - 订单号 {order.order_no} - - 由微信支付提供收款服务,支付后余额自动到账 - - )} - {!order && !err && scene ? ( - - ) : null} - + {loginMode === 'wx' ? ( + + ) : ( + + setGatePhone(e.detail.value)} /> + + setGateCode(e.detail.value)} /> + + + + + )} + + + 登录后即可确认并完成本次充值 + + + ) : ( + <> + + {done ? ( + <> + + 支付成功 + 算力余额已到账,可回桌面端或「算力管理」查看 + + ) : ended ? ( + <> + + 订单已结束 + 该订单已完成充值或已过期,请在桌面端重新发起充值。 + + ) : !order ? ( + <> + {msg ? '无法获取订单' : '正在获取订单…'} + {msg || '请从桌面端「算力中心 → 充值」扫码进入'} + + ) : ( + <> + 确认支付 + 确认使用当前账号完成本次算力充值? + {yuan(order.quota_micro)} + 订单号 {order.order_no} + + )} + + + {done ? ( + + ) : ended || !order ? ( + + ) : ( + <> + 由微信支付提供收款服务,支付后余额自动到账 + + + + )} + + )} + {msg && !done && {msg}} ); } diff --git a/miniprogram/src/pages/pay/index.scss b/miniprogram/src/pages/pay/index.scss index 555831e..d6fdb05 100644 --- a/miniprogram/src/pages/pay/index.scss +++ b/miniprogram/src/pages/pay/index.scss @@ -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; }