diff --git a/miniprogram/src/app.config.js b/miniprogram/src/app.config.js
index bfefb59..67137bf 100644
--- a/miniprogram/src/app.config.js
+++ b/miniprogram/src/app.config.js
@@ -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',
diff --git a/miniprogram/src/pages/compute/index.config.js b/miniprogram/src/pages/compute/index.config.js
new file mode 100644
index 0000000..a4049ce
--- /dev/null
+++ b/miniprogram/src/pages/compute/index.config.js
@@ -0,0 +1 @@
+export default { navigationStyle: 'custom' };
diff --git a/miniprogram/src/pages/compute/index.jsx b/miniprogram/src/pages/compute/index.jsx
new file mode 100644
index 0000000..8704a03
--- /dev/null
+++ b/miniprogram/src/pages/compute/index.jsx
@@ -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 (
+
+
+
+ {!authed && 请先登录(我的 → 登录绑定)。}
+
+ {authed && (
+
+ {/* 余额 */}
+
+ 算力余额
+ {loaded && balance ? yuan(balance.quota) : '—'}
+
+ 已用 {balance ? yuan(balance.used_quota) : '—'}
+ 本月消费 {usageCost != null ? yuan(usageCost) : '—'}
+
+
+
+ {/* 接入地址 */}
+
+ 基础 API 地址
+ base && copy(base.base_url, '地址已复制')}>
+ {base ? base.base_url : '—'}
+ 复制
+
+ 填入 OpenAI 兼容客户端(base_url + 下方 API Key)即可调用平台模型。
+
+
+ {/* API Key */}
+
+
+ 我的 API Key
+
+
+ {tokens.length === 0 ? 暂无令牌,点击「新建」创建。 : null}
+ {tokens.map((tk) => (
+
+ {tk.key}
+
+ copy(tk.key, 'Key 已复制')}>复制
+ delToken(tk.id)}>删除
+
+
+ ))}
+
+
+ {/* 充值 */}
+
+ 算力充值
+ {!payEnabled ? (
+ 支付暂未配置,请联系运营或前往桌面端充值。
+ ) : (
+
+
+ {pkgs.map((p) => (
+ { setPkgId(p.id); setCustom(''); }}
+ >
+ ¥{p.pay != null ? p.pay : p.amount}
+ {p.amount > (p.pay ?? p.amount) ? {p.discount} 折扣 : null}
+ {p.bonus > 0 ? 送 ¥{p.bonus} : null}
+
+ ))}
+
+
+ { setCustom(e.detail.value); setPkgId(''); }}
+ />
+
+
+
+ )}
+
+
+ {err && {err}}
+
+ )}
+
+ );
+}
diff --git a/miniprogram/src/pages/compute/index.scss b/miniprogram/src/pages/compute/index.scss
new file mode 100644
index 0000000..addc90a
--- /dev/null
+++ b/miniprogram/src/pages/compute/index.scss
@@ -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;
+}
diff --git a/miniprogram/src/pages/mine/index.jsx b/miniprogram/src/pages/mine/index.jsx
index 33a3875..29b462f 100644
--- a/miniprogram/src/pages/mine/index.jsx
+++ b/miniprogram/src/pages/mine/index.jsx
@@ -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() {
任务广场请到 Web 端或桌面端查看
+ {/* 算力管理:余额 / API Key / 算力充值(对齐桌面端算力中心) */}
+
+ COMPUTE
+ 算力管理
+
+
+
+ 查看余额与 API Key,支持微信充值算力
+
+
{/* 我的园区:未入驻 → 申请入驻 + OPC认证;已入驻 → 园区信息 + OPC认证 + 转园 */}
PARK
diff --git a/miniprogram/src/pages/pay/index.config.js b/miniprogram/src/pages/pay/index.config.js
new file mode 100644
index 0000000..a4049ce
--- /dev/null
+++ b/miniprogram/src/pages/pay/index.config.js
@@ -0,0 +1 @@
+export default { navigationStyle: 'custom' };
diff --git a/miniprogram/src/pages/pay/index.jsx b/miniprogram/src/pages/pay/index.jsx
new file mode 100644
index 0000000..4444a99
--- /dev/null
+++ b/miniprogram/src/pages/pay/index.jsx
@@ -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 (
+
+
+
+ {!isAuthed() && (
+
+ 请先在「我的」中登录后再扫码支付。
+
+ )}
+
+ {isAuthed() && (
+
+ {!scene ? (
+ 缺少订单信息,请从桌面端重新生成充值码。
+ ) : err ? (
+ {err}
+ ) : !order ? (
+ {busy ? '加载中…' : '下拉或点击按钮获取订单'}
+ ) : paid || finished ? (
+
+ {paid ? '支付成功' : '订单已结束'}
+ {paid ? '算力余额将自动到账,可在「算力管理」中查看。' : (order.status === 'credited' ? '该订单已完成充值。' : '订单已过期或取消,请重新下单。')}
+
+ ) : (
+
+ 算力充值
+ {yuan(order.quota_micro)}
+ 订单号 {order.order_no}
+
+ 由微信支付提供收款服务,支付后余额自动到账
+
+ )}
+ {!order && !err && scene ? (
+
+ ) : null}
+
+ )}
+
+ );
+}
diff --git a/miniprogram/src/pages/pay/index.scss b/miniprogram/src/pages/pay/index.scss
new file mode 100644
index 0000000..555831e
--- /dev/null
+++ b/miniprogram/src/pages/pay/index.scss
@@ -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;
+}