Files
ai-dimension/client/src/pages/vpay/recharge.vue
T

184 lines
6.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="vpay-page page-layout">
<view class="status-bar-placeholder"></view>
<view class="balance-card">
<text class="balance-label">我的{{ info.coinName || '星尘' }}</text>
<text class="balance-num">{{ info.balance || 0 }}</text>
<text class="balance-hint">{{ info.env === 'sandbox' ? '(沙箱环境)' : '' }}1元 = 100 {{ info.coinName || '星尘' }}</text>
</view>
<view class="section-header">
<text class="section-title">选择充值档位</text>
</view>
<view class="pkg-grid">
<view
v-for="p in info.packages || []"
:key="p.id"
class="pkg-card"
:class="{ active: picked === p.id }"
@tap="picked = p.id"
>
<text class="pkg-coins">{{ p.label }}</text>
<text class="pkg-price">¥{{ p.priceYuan }}</text>
</view>
</view>
<view class="use-cases">
<text class="uc-title">{{ info.coinName || '星尘' }}用途</text>
<view class="uc-item"><text class="uc-tag">问答</text><text>AI 问答一次 10 {{ info.coinName || '星尘' }}</text></view>
<view class="uc-item"><text class="uc-tag">连续</text><text>更多星尘用途陆续开放</text></view>
</view>
<view class="pay-agreement-row">
<text class="pay-agreement-text">
充值即消耗型虚拟商品,不支持退款(法定情形除外),详见
<text class="pay-agreement-link" @click.stop="openAgreement">《用户协议》</text>
</text>
</view>
<button class="recharge-btn" :disabled="!picked || paying" @tap="recharge">
{{ paying ? '拉起支付中…' : '立即充值' }}
</button>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { request } from '../../utils/api'
import { useUserStore } from '../../stores/user'
const userStore = useUserStore()
const info = ref({})
const picked = ref('')
const paying = ref(false)
function openAgreement() {
uni.navigateTo({ url: '/pages/agreement/agreement?type=terms' })
}
onMounted(async () => {
if (!userStore.isLoggedIn) {
uni.showModal({
title: '请先登录',
content: '登录后即可充值星尘,每天还有 5 次免费问答',
confirmText: '去登录',
showCancel: false,
success: () => uni.redirectTo({ url: '/pages/login/login' }),
})
return
}
try {
info.value = await request('GET', '/api/vpay/info', null, userStore.token)
if ((info.value.packages || []).length) picked.value = info.value.packages[0].id
} catch (e) {
if (String(e.message || '').includes('登录')) {
uni.showModal({
title: '登录已过期',
content: '请重新登录后再充值',
confirmText: '去登录',
showCancel: false,
success: () => uni.redirectTo({ url: '/pages/login/login' }),
})
} else {
uni.showToast({ title: e.message || '获取充值信息失败', icon: 'none' })
}
}
})
async function recharge() {
if (!picked.value || paying.value) return
paying.value = true
try {
const data = await request('POST', '/api/vpay/recharge', { packageId: picked.value }, userStore.token)
const pay = data.payParams
if (!pay) throw new Error('支付参数异常')
// #ifdef MP-WEIXIN
await new Promise((resolve, reject) => {
uni.requestVirtualPayment({
mode: pay.mode,
env: pay.env,
offerId: pay.offerId,
buyQuantity: pay.buyQuantity,
currencyType: pay.currencyType,
platform: pay.platform,
outTradeNo: pay.outTradeNo,
signData: pay.signData,
paySig: pay.paySig,
signature: pay.signature,
success: resolve,
fail: (err) => reject(new Error(err.errMsg || err.errCode || '支付失败')),
})
})
// #endif
// #ifndef MP-WEIXIN
uni.showToast({ title: '请在微信小程序内充值', icon: 'none' })
return
// #endif
uni.showToast({ title: '充值成功!', icon: 'success' })
// 刷新余额
info.value = await request('GET', '/api/vpay/info', null, userStore.token)
} catch (e) {
const msg = e.message || '支付未完成'
if (msg.includes('cancel')) {
uni.showToast({ title: '已取消支付', icon: 'none' })
} else {
uni.showToast({ title: msg, icon: 'none', duration: 3000 })
}
} finally {
paying.value = false
}
}
</script>
<style lang="scss" scoped>
.vpay-page { min-height: 100vh; padding: 0 20px 40px; background: var(--bg-primary); }
.status-bar-placeholder { height: 44px; }
.balance-card {
background: linear-gradient(135deg, #5b5ff8, #8b5cf6);
border-radius: var(--radius-lg);
padding: 22px 20px;
margin-bottom: 16px;
display: flex; flex-direction: column; gap: 4px;
.balance-label { font-size: 12px; color: rgba(255,255,255,.7); }
.balance-num { font-size: 36px; font-weight: 800; color: #fff; }
.balance-hint { font-size: 11px; color: rgba(255,255,255,.5); }
}
.section-header { display: flex; align-items: center; margin: 16px 0 12px;
.section-title { font-size: 15px; font-weight: 700; color: var(--text-primary); } }
.pkg-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.pkg-card {
background: var(--glass-bg); border: 2px solid var(--glass-border);
border-radius: var(--radius-lg); padding: 16px 14px;
display: flex; flex-direction: column; align-items: center; gap: 4px;
transition: border-color .2s;
&.active { border-color: var(--color-brand); background: var(--color-brand-bg); }
.pkg-coins { font-size: 16px; font-weight: 700; color: var(--text-primary); }
.pkg-price { font-size: 14px; color: var(--color-brand); font-weight: 600; }
}
.use-cases {
background: var(--glass-bg); border: 1px solid var(--glass-border);
border-radius: var(--radius-lg); padding: 14px 16px; margin-top: 16px;
.uc-title { font-size: 13px; font-weight: 700; color: var(--text-primary); display: block; margin-bottom: 8px; }
.uc-item { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; font-size: 12px; color: var(--text-tertiary); }
.uc-tag {
background: var(--color-brand-bg); color: var(--color-brand);
border-radius: 4px; padding: 1px 6px; font-size: 10px; font-weight: 600; flex-shrink: 0;
}
}
.pay-agreement-row { display: flex; justify-content: center; margin-top: 14px; }
.pay-agreement-text { font-size: 10px; color: var(--text-muted); text-align: center; }
.pay-agreement-link { color: var(--color-brand); }
.recharge-btn {
width: 100%; height: 48px; margin-top: 16px;
background: linear-gradient(135deg, var(--color-brand), var(--color-brand-light));
color: #fff; font-size: 16px; font-weight: 700; border-radius: var(--radius-lg);
display: flex; align-items: center; justify-content: center;
&[disabled] { opacity: .5; }
}
</style>