|
- <template>
- <view class="recharge">
- <!-- 余额卡 -->
- <view class="head">
- <view class="head-left">
- <text class="kicker">AI在线点数包</text>
- <text class="title">剩余 {{ balance }} 点</text>
- <text class="user">用户:{{ userStore.nickname }}</text>
- </view>
- <view class="avatar">
- <image v-if="userStore.avatar" class="avatar-img" :src="userStore.avatar" mode="aspectFill" />
- <text v-else>AI</text>
- </view>
- </view>
-
- <!-- 套餐 -->
- <view class="section-title">选择充值套餐</view>
- <view class="grid">
- <view
- v-for="item in packages"
- :key="item.id"
- class="product"
- :class="{ active: current && current.id === item.id }"
- @click="current = item"
- >
- <text v-if="item.badge" class="badge">{{ item.badge }}</text>
- <text class="points">{{ item.points }}点</text>
- <text class="price">¥{{ item.price_yuan }}</text>
- </view>
- </view>
-
- <view class="buy-btn" :class="{ disabled: !current || paying }" @click="handleBuy">
- {{ buyText }}
- </view>
-
- <!-- 权益 -->
- <view class="perks">
- <view class="perk">
- <text class="perk-title">点数长期有效</text>
- <text class="perk-desc">购买后可用于所有 AI 图像工具</text>
- </view>
- <view class="perk">
- <text class="perk-title">多端通用</text>
- <text class="perk-desc">小程序、H5、PC 端共享点数</text>
- </view>
- </view>
-
- <!-- 购买须知 -->
- <view class="notice">
- <text class="notice-title">购买须知:</text>
- <text class="notice-line">1、购买后长期有效,不支持退款;</text>
- <text class="notice-line">2、同一账号点数可在小程序、H5、PC端使用;</text>
- <text class="notice-line">3、点数适用于 AI 图片生成、高清修复、证件照、AI绘画等功能。</text>
- </view>
- </view>
- </template>
-
- <script setup>
- import { ref, computed } from 'vue'
- import { onShow } from '@dcloudio/uni-app'
- import { useUserStore } from '@/store/user.js'
- import { useApp } from '@/utils/useApp.js'
- import { pointApi } from '@/api/index.js'
-
- const userStore = useUserStore()
- const { toast } = useApp()
-
- const packages = ref([])
- const current = ref(null)
- const balance = ref(0)
- const paying = ref(false)
-
- const buyText = computed(() => {
- if (!current.value) return '请选择套餐'
- return `确认充值(${current.value.price_yuan}元)`
- })
-
- function ensureLogin() {
- if (!userStore.isLogin) {
- uni.navigateTo({ url: '/pages/login/login' })
- return false
- }
- return true
- }
-
- async function loadPackages() {
- try {
- const res = await pointApi.getPackageList()
- packages.value = res.list || []
- current.value = packages.value.find((p) => p.badge === '推荐') || packages.value[0] || null
- } catch (e) {
- toast(e.msg || '套餐加载失败')
- }
- }
-
- async function loadBalance() {
- if (!userStore.isLogin) return
- try {
- const res = await pointApi.getBalance()
- balance.value = (res.data && res.data.points) || 0
- } catch (e) {
- // 登录失效由 request 统一处理
- }
- }
-
- function getWxLoginCode() {
- return new Promise((resolve, reject) => {
- // #ifdef MP-WEIXIN
- uni.login({
- provider: 'weixin',
- success: (r) => (r.code ? resolve(r.code) : reject(new Error('获取登录凭证失败'))),
- fail: () => reject(new Error('获取登录凭证失败'))
- })
- // #endif
- // #ifndef MP-WEIXIN
- reject(new Error('当前端暂不支持点数充值'))
- // #endif
- })
- }
-
- function requestVirtualPayment(order) {
- return new Promise((resolve, reject) => {
- // #ifdef MP-WEIXIN
- if (!wx.canIUse || !wx.canIUse('requestVirtualPayment')) {
- reject(new Error('当前微信版本不支持虚拟支付,请升级微信'))
- return
- }
- // 调起虚拟支付的完整参数(供审核/排查用)
- const payParams = {
- signData: order.signData,
- paySig: order.paySig,
- signature: order.signature,
- mode: order.mode
- }
- console.log('[虚拟支付] wx.requestVirtualPayment 参数:\n' + JSON.stringify(payParams, null, 2))
- wx.requestVirtualPayment({
- ...payParams,
- success: (res) => {
- console.log('[虚拟支付] success:', JSON.stringify(res))
- resolve(res)
- },
- fail: (err) => {
- console.log('[虚拟支付] fail:', JSON.stringify(err))
- reject(err)
- }
- })
- // #endif
- // #ifndef MP-WEIXIN
- reject(new Error('当前端暂不支持点数充值'))
- // #endif
- })
- }
-
- async function pollOrder(orderNo, times = 5) {
- for (let i = 0; i < times; i++) {
- try {
- const res = await pointApi.getOrderStatus({ order_no: orderNo })
- if (res.data && res.data.status === 20) {
- balance.value = res.data.balance
- return true
- }
- } catch (e) {
- // 忽略单次失败,继续轮询
- }
- await new Promise((r) => setTimeout(r, 1500))
- }
- return false
- }
-
- async function handleBuy() {
- if (!ensureLogin()) return
- if (!current.value || paying.value) return
-
- paying.value = true
- uni.showLoading({ mask: true, title: '发起支付...' })
- try {
- const code = await getWxLoginCode()
- const res = await pointApi.createOrder({ package_id: current.value.id, code })
- const order = res.data
- uni.hideLoading()
-
- // 打印后端返回的下单结果(含 env,1=沙箱不扣费 / 0=现网真实扣费)
- console.log('[虚拟支付] createorder 返回:', JSON.stringify(order))
- // 解析 signData 里的 env,直观确认环境
- try {
- const sd = JSON.parse(order.signData || '{}')
- console.log('[虚拟支付] signData.env =', sd.env, sd.env === 1 ? '(沙箱-不扣费)' : '(现网-真实扣费!)')
- } catch (e) {}
-
- await requestVirtualPayment(order)
-
- uni.showLoading({ mask: true, title: '确认到账...' })
- const ok = await pollOrder(order.order_no)
- uni.hideLoading()
- if (ok) {
- toast('充值成功')
- } else {
- toast('支付成功,点数到账处理中')
- loadBalance()
- }
- } catch (e) {
- uni.hideLoading()
- const errCode = e && e.errCode
- if (errCode === -2) {
- // 用户取消支付,不提示
- } else {
- toast((e && (e.msg || e.errMsg || e.message)) || '支付失败')
- }
- } finally {
- paying.value = false
- }
- }
-
- onShow(() => {
- loadPackages()
- loadBalance()
- })
- </script>
-
- <style lang="scss" scoped>
- $accent: #0b8cff;
- $muted: #8b95a3;
-
- .recharge {
- padding: 36rpx 36rpx calc(48rpx + env(safe-area-inset-bottom));
- }
-
- /* 余额卡 */
- .head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 32rpx;
- min-height: 264rpx;
- padding: 44rpx 40rpx;
- border-radius: 36rpx;
- color: #fff;
- box-sizing: border-box;
- background:
- radial-gradient(circle at 82% 18%, rgba(255, 255, 255, 0.28), transparent 28%),
- linear-gradient(135deg, #0b8cff 0%, #20a7ff 48%, #20d3a2 100%);
- box-shadow: 0 28rpx 56rpx rgba(11, 140, 255, 0.18);
-
- .head-left {
- display: flex;
- flex-direction: column;
- }
- .kicker {
- font-size: 26rpx;
- font-weight: 800;
- color: rgba(255, 255, 255, 0.82);
- }
- .title {
- margin-top: 14rpx;
- font-size: 56rpx;
- line-height: 1.35;
- font-weight: 900;
- }
- .user {
- margin-top: 12rpx;
- font-size: 26rpx;
- color: rgba(255, 255, 255, 0.78);
- }
- .avatar {
- flex: 0 0 104rpx;
- width: 104rpx;
- height: 104rpx;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 32rpx;
- font-weight: 900;
- color: #fff;
- background: rgba(15, 23, 42, 0.24);
- border: 4rpx solid rgba(255, 255, 255, 0.55);
- overflow: hidden;
- }
- .avatar-img {
- width: 100%;
- height: 100%;
- }
- }
-
- .section-title {
- margin: 48rpx 0 24rpx;
- font-size: 34rpx;
- font-weight: 900;
- color: #1f2733;
- }
-
- /* 套餐宫格 */
- .grid {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 20rpx;
- }
-
- .product {
- position: relative;
- min-height: 212rpx;
- padding: 28rpx 16rpx 24rpx;
- border: 4rpx solid transparent;
- border-radius: 28rpx;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 16rpx;
- color: #1f2733;
- background: #fff;
- box-shadow: 0 20rpx 44rpx rgba(15, 23, 42, 0.06);
-
- &.active {
- border-color: $accent;
- box-shadow: 0 0 0 8rpx rgba(11, 140, 255, 0.1);
- }
- .badge {
- position: absolute;
- top: 16rpx;
- left: 50%;
- transform: translateX(-50%);
- max-width: calc(100% - 24rpx);
- padding: 6rpx 14rpx;
- border-radius: 999rpx;
- font-size: 20rpx;
- font-weight: 900;
- white-space: nowrap;
- color: $accent;
- background: rgba(11, 140, 255, 0.1);
- }
- .points {
- margin-top: 36rpx;
- font-size: 40rpx;
- font-weight: 900;
- }
- .price {
- font-size: 26rpx;
- font-weight: 800;
- color: $accent;
- }
- }
-
- /* 确认充值 */
- .buy-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 96rpx;
- margin-top: 36rpx;
- border-radius: 24rpx;
- font-size: 30rpx;
- font-weight: 900;
- color: #fff;
- background: linear-gradient(135deg, #0b8cff, #20a7ff);
- box-shadow: 0 24rpx 48rpx rgba(11, 140, 255, 0.22);
-
- &.disabled {
- opacity: 0.6;
- }
- }
-
- /* 权益 */
- .perks {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 20rpx;
- margin-top: 36rpx;
-
- .perk {
- padding: 28rpx 24rpx;
- border-radius: 28rpx;
- background: rgba(11, 140, 255, 0.08);
- }
- .perk-title {
- font-size: 28rpx;
- font-weight: 900;
- color: $accent;
- }
- .perk-desc {
- display: block;
- margin-top: 14rpx;
- font-size: 24rpx;
- line-height: 1.45;
- color: $muted;
- }
- }
-
- /* 购买须知 */
- .notice {
- margin-top: 44rpx;
- padding: 28rpx;
- border-radius: 28rpx;
- background: #fff;
- box-shadow: 0 20rpx 44rpx rgba(15, 23, 42, 0.04);
-
- .notice-title {
- display: block;
- margin-bottom: 8rpx;
- font-size: 26rpx;
- font-weight: 800;
- color: #8f98a6;
- }
- .notice-line {
- display: block;
- font-size: 26rpx;
- line-height: 1.8;
- color: $muted;
- }
- }
- </style>
|