Compare commits
2 Commits
47c5150361
...
fb7e0edd8f
| Author | SHA1 | Date | |
|---|---|---|---|
| fb7e0edd8f | |||
| 8787e7f27c |
@@ -20,7 +20,9 @@ export default {
|
||||
'pages/my-bookings/index',
|
||||
'pages/my-tasks/index',
|
||||
'pages/my-park/index',
|
||||
'pages/my-cert/index'
|
||||
'pages/my-cert/index',
|
||||
'pages/compute/index',
|
||||
'pages/pay/index'
|
||||
],
|
||||
window: {
|
||||
backgroundTextStyle: 'light',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export default { navigationStyle: 'custom' };
|
||||
@@ -0,0 +1,222 @@
|
||||
import { View, Text, Button, Input } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { request } from '@/utils/api';
|
||||
import { isAuthed } from '@/utils/auth';
|
||||
import { PageHeader } from '@/components/TopInset';
|
||||
import './index.scss';
|
||||
|
||||
const MICRO = 1000000; // 1 元 = 1,000,000 微元(与引擎计费口径一致)
|
||||
|
||||
const yuan = (micro) => `¥${((micro || 0) / MICRO).toFixed(2)}`;
|
||||
|
||||
/** 算力管理(C端):余额 / Base URL / API Key 自服务 / 算力充值(微信 JSAPI 支付)。
|
||||
* 功能对齐桌面端「算力中心」。 */
|
||||
export default function ComputeCenter() {
|
||||
const [authed] = useState(isAuthed());
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [err, setErr] = useState('');
|
||||
const [balance, setBalance] = useState(null); // { quota, used_quota }
|
||||
const [base, setBase] = useState(null); // { base_url, username }
|
||||
const [tokens, setTokens] = useState([]); // [{ id, name, key }]
|
||||
const [usageCost, setUsageCost] = useState(null);
|
||||
|
||||
// 充值状态
|
||||
const [pkgs, setPkgs] = useState([]); // [{ id, amount, bonus, label }]
|
||||
const [payEnabled, setPayEnabled] = useState(false);
|
||||
const [pkgId, setPkgId] = useState('');
|
||||
const [custom, setCustom] = useState('');
|
||||
const [paying, setPaying] = useState(false);
|
||||
const pollRef = useRef(null);
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const [bal, b, tk, us, pk] = await Promise.all([
|
||||
request('GET', '/opc/compute/balance', {}, true).catch(() => null),
|
||||
request('GET', '/opc/compute/base', {}, true).catch(() => null),
|
||||
request('GET', '/opc/compute/tokens', {}, true).catch(() => ({ items: [] })),
|
||||
request('GET', '/opc/compute/usage', {}, true).catch(() => null),
|
||||
request('GET', '/opc/compute/recharge/packages', {}, true).catch(() => ({ enabled: false, items: [] }))
|
||||
]);
|
||||
setBalance(bal);
|
||||
setBase(b);
|
||||
setTokens(tk && tk.items ? tk.items : []);
|
||||
setUsageCost(us && us.cost_quota != null ? us.cost_quota : null);
|
||||
setPayEnabled(!!pk.enabled);
|
||||
setPkgs(pk.items || []);
|
||||
setPkgId((pk.items && pk.items[0] && pk.items[0].id) || '');
|
||||
setErr('');
|
||||
} catch (e) { setErr((e && e.message) || '加载失败'); }
|
||||
finally { setLoaded(true); }
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (authed) load();
|
||||
else setLoaded(true);
|
||||
return () => { if (pollRef.current) clearInterval(pollRef.current); };
|
||||
}, []);
|
||||
|
||||
const copy = (text, tip = '已复制') => {
|
||||
const val = text == null ? '' : String(text);
|
||||
if (!val) {
|
||||
Taro.showToast({ title: '内容为空,稍后再试', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
Taro.setClipboardData({ text: val, success: () => Taro.showToast({ title: tip, icon: 'success' }) });
|
||||
};
|
||||
|
||||
const createToken = async () => {
|
||||
try {
|
||||
await request('POST', '/opc/compute/tokens', {}, true);
|
||||
Taro.showToast({ title: '令牌已创建', icon: 'success' });
|
||||
load();
|
||||
} catch (e) { Taro.showToast({ title: (e && e.message) || '创建失败', icon: 'none' }); }
|
||||
};
|
||||
|
||||
const delToken = async (id) => {
|
||||
try {
|
||||
await request('DELETE', `/opc/compute/tokens/${id}`, {}, true);
|
||||
Taro.showToast({ title: '已删除', icon: 'success' });
|
||||
load();
|
||||
} catch (e) { Taro.showToast({ title: (e && e.message) || '删除失败', icon: 'none' }); }
|
||||
};
|
||||
|
||||
const pollStatus = (orderNo) => {
|
||||
if (pollRef.current) clearInterval(pollRef.current);
|
||||
let tries = 0;
|
||||
pollRef.current = setInterval(async () => {
|
||||
tries += 1;
|
||||
if (tries > 40) { // 约 60 秒后停止轮询(回调/对账兜底会自动到账)
|
||||
clearInterval(pollRef.current);
|
||||
pollRef.current = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const st = await request('POST', `/opc/compute/recharge/orders/${orderNo}/status`, {}, true);
|
||||
if (st.paid) {
|
||||
if (pollRef.current) clearInterval(pollRef.current);
|
||||
pollRef.current = null;
|
||||
Taro.showToast({ title: '充值成功,已到账', icon: 'success' });
|
||||
load();
|
||||
}
|
||||
} catch { /* 瞬时错误忽略,下一轮重试 */ }
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const pay = async () => {
|
||||
const amountYuan = parseFloat(custom);
|
||||
const body = pkgId
|
||||
? { client_type: 'jsapi', package_id: pkgId }
|
||||
: { client_type: 'jsapi', amount_yuan: amountYuan };
|
||||
if (!pkgId && (!amountYuan || amountYuan <= 0)) {
|
||||
Taro.showToast({ title: '请选择套餐或输入金额', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
setPaying(true);
|
||||
try {
|
||||
const order = await request('POST', '/opc/compute/recharge/orders', body, true);
|
||||
const pp = order.pay_params || {};
|
||||
Taro.requestPayment({
|
||||
timeStamp: pp.timeStamp,
|
||||
nonceStr: pp.nonceStr,
|
||||
package: pp.package,
|
||||
signType: pp.signType || 'RSA',
|
||||
paySign: pp.paySign,
|
||||
success: () => pollStatus(order.order_no),
|
||||
fail: (e) => {
|
||||
const msg = (e && e.errMsg) || '';
|
||||
if (msg.indexOf('cancel') >= 0) Taro.showToast({ title: '已取消支付', icon: 'none' });
|
||||
else Taro.showToast({ title: '支付失败,请重试', icon: 'none' });
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
Taro.showToast({ title: (e && e.message) || '下单失败', icon: 'none' });
|
||||
} finally { setPaying(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="page-in">
|
||||
<PageHeader no="04" title="算力管理" en="COMPUTE" back />
|
||||
|
||||
{!authed && <View className="card"><Text className="muted">请先登录(我的 → 登录绑定)。</Text></View>}
|
||||
|
||||
{authed && (
|
||||
<View className="cc-wrap">
|
||||
{/* 余额 */}
|
||||
<View className="card cc-balance">
|
||||
<Text className="cc-label">算力余额</Text>
|
||||
<Text className="cc-amount">{loaded && balance ? yuan(balance.quota) : '—'}</Text>
|
||||
<View className="cc-balance-row">
|
||||
<Text className="muted">已用 {balance ? yuan(balance.used_quota) : '—'}</Text>
|
||||
<Text className="muted">本月消费 {usageCost != null ? yuan(usageCost) : '—'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 接入地址 */}
|
||||
<View className="card cc-card">
|
||||
<Text className="cc-title">基础 API 地址</Text>
|
||||
<View className="cc-row" onClick={() => base && copy(base.base_url, '地址已复制')}>
|
||||
<Text className="cc-mono">{base ? base.base_url : '—'}</Text>
|
||||
<Text className="cc-act">复制</Text>
|
||||
</View>
|
||||
<Text className="muted cc-hint">填入 OpenAI 兼容客户端(base_url + 下方 API Key)即可调用平台模型。</Text>
|
||||
</View>
|
||||
|
||||
{/* API Key */}
|
||||
<View className="card cc-card">
|
||||
<View className="cc-headrow">
|
||||
<Text className="cc-title">我的 API Key</Text>
|
||||
<Button className="btn-ghost cc-btn-sm" onClick={createToken}>新建</Button>
|
||||
</View>
|
||||
{tokens.length === 0 ? <Text className="muted">暂无令牌,点击「新建」创建。</Text> : null}
|
||||
{tokens.map((tk) => (
|
||||
<View className="cc-token" key={String(tk.id)}>
|
||||
<Text className="cc-mono cc-token-key">{tk.key}</Text>
|
||||
<View className="cc-token-acts">
|
||||
<Text className="cc-act" onClick={() => copy(tk.key, 'Key 已复制')}>复制</Text>
|
||||
<Text className="cc-act cc-danger" onClick={() => delToken(tk.id)}>删除</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 充值 */}
|
||||
<View className="card cc-card">
|
||||
<Text className="cc-title">算力充值</Text>
|
||||
{!payEnabled ? (
|
||||
<Text className="muted">支付暂未配置,请联系运营或前往桌面端充值。</Text>
|
||||
) : (
|
||||
<View>
|
||||
<View className="cc-pkgs">
|
||||
{pkgs.map((p) => (
|
||||
<View
|
||||
key={p.id}
|
||||
className={`cc-pkg ${pkgId === p.id ? 'on' : ''}`}
|
||||
onClick={() => { setPkgId(p.id); setCustom(''); }}
|
||||
>
|
||||
<Text className="cc-pkg-amount">¥{p.pay != null ? p.pay : p.amount}</Text>
|
||||
{p.amount > (p.pay ?? p.amount) ? <Text className="cc-pkg-bonus">{p.discount} 折扣</Text> : null}
|
||||
{p.bonus > 0 ? <Text className="cc-pkg-bonus">送 ¥{p.bonus}</Text> : null}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<View className="cc-custom">
|
||||
<Input
|
||||
className="cc-input"
|
||||
type="digit"
|
||||
placeholder="自定义金额(元)"
|
||||
value={custom}
|
||||
onInput={(e) => { setCustom(e.detail.value); setPkgId(''); }}
|
||||
/>
|
||||
</View>
|
||||
<Button className="btn-main cc-paybtn" loading={paying} onClick={pay}>微信支付</Button>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{err && <Text className="cc-err">{err}</Text>}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/* 算力管理页:沿用全局 Soft UI 毛玻璃卡片(.card),仅补页内布局与算力专属样式 */
|
||||
@import '../../app.scss';
|
||||
|
||||
.cc-wrap {
|
||||
padding: 0 24rpx 40rpx;
|
||||
}
|
||||
|
||||
/* 余额卡:品牌渐变强调 */
|
||||
.cc-balance {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
padding: 32rpx;
|
||||
background: linear-gradient(135deg, rgba(98, 132, 255, 0.16), rgba(255, 255, 255, 0.55));
|
||||
}
|
||||
|
||||
.cc-label {
|
||||
font-size: 24rpx;
|
||||
color: #8a8a93;
|
||||
}
|
||||
|
||||
.cc-amount {
|
||||
font-size: 60rpx;
|
||||
font-weight: 700;
|
||||
color: #2b2f36;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.cc-balance-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
/* 普通卡 */
|
||||
.cc-card {
|
||||
margin-top: 24rpx;
|
||||
padding: 28rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.cc-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #2b2f36;
|
||||
}
|
||||
|
||||
.cc-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.cc-mono {
|
||||
font-family: Menlo, Consolas, monospace;
|
||||
font-size: 24rpx;
|
||||
color: #4a4f57;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.cc-act {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
color: #6284ff;
|
||||
}
|
||||
|
||||
.cc-danger {
|
||||
color: #e5484d;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.cc-hint {
|
||||
font-size: 22rpx;
|
||||
line-height: 1.6;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.cc-headrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.cc-btn-sm {
|
||||
font-size: 24rpx;
|
||||
padding: 0 24rpx;
|
||||
height: 56rpx;
|
||||
line-height: 56rpx;
|
||||
}
|
||||
|
||||
.cc-token {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
padding: 14rpx 0;
|
||||
border-top: 1rpx solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.cc-token-key {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cc-token-acts {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 充值套餐:单行等宽排列(数量多时横向滚动) */
|
||||
.cc-pkgs {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 12rpx;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.cc-pkg {
|
||||
flex: 1 0 auto;
|
||||
min-width: 120rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 2rpx solid rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2rpx;
|
||||
}
|
||||
|
||||
.cc-pkg.on {
|
||||
border-color: #6284ff;
|
||||
background: rgba(98, 132, 255, 0.1);
|
||||
}
|
||||
|
||||
.cc-pkg-amount {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #2b2f36;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cc-pkg-bonus {
|
||||
font-size: 20rpx;
|
||||
color: #e8830c;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cc-custom {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.cc-input {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 18rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.cc-paybtn {
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.cc-err {
|
||||
display: block;
|
||||
margin: 20rpx 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #e5484d;
|
||||
}
|
||||
@@ -69,11 +69,14 @@ export default function Mine() {
|
||||
const [pDebug, setPDebug] = useState('');
|
||||
|
||||
const refresh = async () => {
|
||||
if (!isAuthed()) return;
|
||||
if (!isAuthed()) { setUser(null); setLoaded(true); return; }
|
||||
try {
|
||||
const me = await getMe();
|
||||
if (me.ok && me.user) setUser(getUser());
|
||||
} catch { /* ignore */ }
|
||||
} catch (e) {
|
||||
// 401 时 api.js 已清 pine_token:同步清掉本地假登录态,显示未登录卡片
|
||||
if (!isAuthed()) { setUser(null); logout(); }
|
||||
}
|
||||
try {
|
||||
const d = await getMyBookings();
|
||||
setBookings((d && d.list) || []);
|
||||
@@ -449,6 +452,16 @@ export default function Mine() {
|
||||
<Text className="muted mk-center-hint">任务广场请到 Web 端或桌面端查看</Text>
|
||||
</View>
|
||||
|
||||
{/* 算力管理:余额 / API Key / 算力充值(对齐桌面端算力中心) */}
|
||||
<View className="mine-section-head">
|
||||
<Text className="mine-no">COMPUTE</Text>
|
||||
<Text className="mine-subtitle">算力管理</Text>
|
||||
</View>
|
||||
<View className="card mk-act">
|
||||
<Button className="btn-main mk-btn-sm" onClick={() => Taro.navigateTo({ url: '/pages/compute/index' })}>算力中心</Button>
|
||||
<Text className="muted mk-center-hint">查看余额与 API Key,支持微信充值算力</Text>
|
||||
</View>
|
||||
|
||||
{/* 我的园区:未入驻 → 申请入驻 + OPC认证;已入驻 → 园区信息 + OPC认证 + 转园 */}
|
||||
<View className="mine-section-head">
|
||||
<Text className="mine-no">PARK</Text>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export default { navigationStyle: 'custom' };
|
||||
@@ -0,0 +1,118 @@
|
||||
import { View, Text, Button } 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 './index.scss';
|
||||
|
||||
const MICRO = 1000000; // 1 元 = 1,000,000 微元
|
||||
|
||||
const yuan = (micro) => `¥${((micro || 0) / MICRO).toFixed(2)}`;
|
||||
|
||||
/** 小程序确认支付页:微信扫桌面端「算力充值」小程序码进入(scene=订单号),
|
||||
* 取 JSAPI 参数后 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 [busy, setBusy] = useState(false);
|
||||
const [paid, setPaid] = useState(false);
|
||||
|
||||
const load = async () => {
|
||||
if (!scene) { setErr('缺少订单信息(scene 为空)'); 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) || '加载订单失败'); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
|
||||
// 进入即取一次(登录态就绪时)
|
||||
useEffect(() => { if (isAuthed()) load(); }, []);
|
||||
|
||||
const pay = async () => {
|
||||
if (!info || !info.pay_params) return;
|
||||
const pp = info.pay_params;
|
||||
setBusy(true);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
Taro.requestPayment({
|
||||
timeStamp: pp.timeStamp,
|
||||
nonceStr: pp.nonceStr,
|
||||
package: pp.package,
|
||||
signType: pp.signType || 'RSA',
|
||||
paySign: pp.paySign,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
setPaid(true);
|
||||
Taro.showToast({ title: '支付成功,余额到账中', icon: 'success' });
|
||||
// 轮询确认后端到账(回调/对账兜底)
|
||||
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' }); }
|
||||
} 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' });
|
||||
} finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const order = info && info.order;
|
||||
|
||||
return (
|
||||
<View className="page-in">
|
||||
<PageHeader no="05" title="确认支付" en="PAY" back />
|
||||
|
||||
{!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>
|
||||
</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>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/* 小程序确认支付页:沿用全局 Soft UI 卡片,补支付专属样式 */
|
||||
@import '../../app.scss';
|
||||
|
||||
.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-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.pay-label {
|
||||
font-size: 26rpx;
|
||||
color: #8a8a93;
|
||||
}
|
||||
|
||||
.pay-amount {
|
||||
font-size: 72rpx;
|
||||
font-weight: 700;
|
||||
color: #2b2f36;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -27,6 +27,10 @@ export function request(method, path, data = {}, withAuth = false) {
|
||||
const d = res.data || {};
|
||||
if (res.statusCode >= 200 && res.statusCode < 300 && d.ok !== false) {
|
||||
resolve(d);
|
||||
} else if (res.statusCode === 401) {
|
||||
// 登录已失效(token 过期/被吊销/token_version 变更):清掉假登录态,引导重新登录
|
||||
Taro.removeStorageSync('pine_token');
|
||||
reject(new Error('登录已过期,请重新登录'));
|
||||
} else {
|
||||
reject(new Error(d.detail || d.error || '请求失败,请稍后重试'));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import { request } from './api';
|
||||
|
||||
/* 用户端认证(统一账号):登录/资料走平台 /auth(单一 users + 平台 JWT);
|
||||
手机绑定走培训 /api/auth(阶段3 后同样读写平台 users + 验平台 JWT)。
|
||||
/* 用户端认证(统一账号):登录/资料/手机绑定均走平台 /auth(单一 users + 平台 JWT)。
|
||||
与 web 端一致,见 小程序同步约束.md */
|
||||
|
||||
/** pine_user 契约字段(各页面读取):username/name/avatar/phone/phoneBound/status/topics/source */
|
||||
@@ -103,7 +102,7 @@ export async function bindPhone(phone, code) {
|
||||
const data = await request('POST', '/auth/bind-phone', { phone, code }, true);
|
||||
if (data.token) {
|
||||
Taro.setStorageSync('pine_token', data.token);
|
||||
Taro.setStorageSync('pine_user', JSON.stringify(pickUser(data)));
|
||||
saveUser(data); // pine_user 统一存对象(此前误存 JSON 字符串致资料页字段丢失)
|
||||
}
|
||||
return { ok: true, phone, phoneBound: true, ...data };
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.0 MiB |
Reference in New Issue
Block a user