@@ -0,0 +1,168 @@
< 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 > Pro 月卡 3000 { { info . coinName || '星尘' } } < / 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 . showToast ( { title : '请先登录' , icon : 'none' } )
setTimeout ( ( ) => uni . navigateBack ( ) , 1500 )
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 ) {
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 : 100 vh ; padding : 0 20 px 40 px ; background : var ( -- bg - primary ) ; }
. status - bar - placeholder { height : 44 px ; }
. balance - card {
background : linear - gradient ( 135 deg , # 5 b5ff8 , # 8 b5cf6 ) ;
border - radius : var ( -- radius - lg ) ;
padding : 22 px 20 px ;
margin - bottom : 16 px ;
display : flex ; flex - direction : column ; gap : 4 px ;
. balance - label { font - size : 12 px ; color : rgba ( 255 , 255 , 255 , .7 ) ; }
. balance - num { font - size : 36 px ; font - weight : 800 ; color : # fff ; }
. balance - hint { font - size : 11 px ; color : rgba ( 255 , 255 , 255 , .5 ) ; }
}
. section - header { display : flex ; align - items : center ; margin : 16 px 0 12 px ;
. section - title { font - size : 15 px ; font - weight : 700 ; color : var ( -- text - primary ) ; } }
. pkg - grid { display : grid ; grid - template - columns : 1 fr 1 fr ; gap : 10 px ; }
. pkg - card {
background : var ( -- glass - bg ) ; border : 2 px solid var ( -- glass - border ) ;
border - radius : var ( -- radius - lg ) ; padding : 16 px 14 px ;
display : flex ; flex - direction : column ; align - items : center ; gap : 4 px ;
transition : border - color .2 s ;
& . active { border - color : var ( -- color - brand ) ; background : var ( -- color - brand - bg ) ; }
. pkg - coins { font - size : 16 px ; font - weight : 700 ; color : var ( -- text - primary ) ; }
. pkg - price { font - size : 14 px ; color : var ( -- color - brand ) ; font - weight : 600 ; }
}
. use - cases {
background : var ( -- glass - bg ) ; border : 1 px solid var ( -- glass - border ) ;
border - radius : var ( -- radius - lg ) ; padding : 14 px 16 px ; margin - top : 16 px ;
. uc - title { font - size : 13 px ; font - weight : 700 ; color : var ( -- text - primary ) ; display : block ; margin - bottom : 8 px ; }
. uc - item { display : flex ; align - items : center ; gap : 8 px ; margin - bottom : 6 px ; font - size : 12 px ; color : var ( -- text - tertiary ) ; }
. uc - tag {
background : var ( -- color - brand - bg ) ; color : var ( -- color - brand ) ;
border - radius : 4 px ; padding : 1 px 6 px ; font - size : 10 px ; font - weight : 600 ; flex - shrink : 0 ;
}
}
. pay - agreement - row { display : flex ; justify - content : center ; margin - top : 14 px ; }
. pay - agreement - text { font - size : 10 px ; color : var ( -- text - muted ) ; text - align : center ; }
. pay - agreement - link { color : var ( -- color - brand ) ; }
. recharge - btn {
width : 100 % ; height : 48 px ; margin - top : 16 px ;
background : linear - gradient ( 135 deg , var ( -- color - brand ) , var ( -- color - brand - light ) ) ;
color : # fff ; font - size : 16 px ; font - weight : 700 ; border - radius : var ( -- radius - lg ) ;
display : flex ; align - items : center ; justify - content : center ;
& [ disabled ] { opacity : .5 ; }
}
< / style >