|
- <template>
- <div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
- <!-- 标题 -->
- <div class="text-center mb-8">
- <h1 class="text-2xl sm:text-3xl font-bold text-slate-900">充值中心</h1>
- <p class="mt-2 text-sm text-slate-500">为你的 AI 创作充能</p>
- </div>
-
- <!-- 余额 -->
- <div class="max-w-sm mx-auto mb-8 px-6 py-4 bg-white rounded-2xl shadow-sm border border-slate-100 text-center">
- <span class="text-sm text-slate-500">当前余额</span>
- <div class="mt-1 text-3xl font-bold text-indigo-600">{{ userInfo.balance || 0 }} <span class="text-base font-normal text-slate-400">点</span></div>
- </div>
-
- <!-- 套餐列表 -->
- <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
- <div
- v-for="(pkg, idx) in products"
- :key="pkg.id"
- :class="[
- 'relative bg-white rounded-2xl p-6 border transition-all duration-200 cursor-pointer text-center',
- selectedId === pkg.id
- ? 'border-indigo-400 shadow-md ring-2 ring-indigo-500/20'
- : 'border-slate-100 shadow-sm hover:shadow-md hover:border-indigo-100',
- ]"
- @click="selectProduct(pkg)"
- >
- <div
- v-if="idx === 1"
- class="absolute -top-2.5 left-1/2 -translate-x-1/2 px-3 py-0.5 bg-green-500 text-white text-xs font-medium rounded-full"
- >
- 推荐
- </div>
- <div class="text-lg font-semibold text-slate-900">{{ pkg.points || pkg.point }} 点</div>
- <div class="mt-1 text-2xl font-bold text-slate-900">¥{{ pkg.price }}</div>
- <div v-if="pkg.discount" class="mt-1 text-xs text-green-600">{{ pkg.discount }}</div>
- </div>
- </div>
-
- <!-- 支付按钮 -->
- <div class="max-w-sm mx-auto">
- <button
- v-if="!isPaying"
- :disabled="!selectedId"
- class="w-full py-3 text-sm font-semibold text-white bg-indigo-500 hover:bg-indigo-600 disabled:bg-slate-300 disabled:cursor-not-allowed rounded-xl transition-colors cursor-pointer"
- @click="startPay"
- >
- 确认支付
- </button>
-
- <!-- 支付弹窗内嵌 -->
- <div v-if="isPaying" class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
- <div class="text-center">
- <p class="text-sm font-medium text-slate-700 mb-4">微信扫码支付</p>
-
- <!-- 二维码 -->
- <div class="w-40 h-40 mx-auto mb-4 flex items-center justify-center">
- <div v-if="payState === 'loading'" class="flex flex-col items-center gap-2">
- <svg class="w-8 h-8 text-indigo-500 animate-spin" fill="none" viewBox="0 0 24 24">
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
- <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
- </svg>
- <span class="text-xs text-slate-400">生成二维码...</span>
- </div>
-
- <vue-qr
- v-else-if="codeUrl"
- :text="codeUrl"
- :size="160"
- :margin="8"
- />
-
- <div v-else class="flex flex-col items-center gap-2">
- <svg class="w-8 h-8 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
- </svg>
- <span class="text-xs text-red-400">生成失败</span>
- </div>
- </div>
-
- <p v-if="payState === 'pending'" class="text-sm text-slate-500 mb-1">
- 请使用微信扫描二维码
- </p>
- <p v-else-if="payState === 'success'" class="text-sm text-green-600 font-medium">
- 支付成功!🎉
- </p>
- <p v-if="payState !== 'success'" class="text-xs text-slate-400">支付金额:¥{{ selectedPrice }}</p>
- </div>
-
- <!-- 操作按钮 -->
- <div class="flex justify-center gap-3 mt-4">
- <button
- v-if="payState !== 'success'"
- class="px-4 py-2 text-xs text-slate-400 hover:text-slate-600 cursor-pointer"
- @click="cancelPay"
- >
- 取消支付
- </button>
- <button
- v-if="payState === 'success'"
- class="px-4 py-2 text-xs text-white bg-green-500 hover:bg-green-600 rounded-lg cursor-pointer"
- @click="finishPaySuccess"
- >
- 完成
- </button>
- </div>
- </div>
- </div>
- </div>
- </template>
-
- <script setup>
- import VueQr from 'vue-qr'
-
- useSeoMeta({
- title: '充值中心 - 奇想宇宙',
- description: '选择套餐,为AI创作充值点数',
- })
-
- const { userInfo, isLogin } = useUser()
- const { post } = useApi()
-
- // 套餐列表
- const products = ref([])
- const selectedId = ref(null)
- const selectedPrice = ref(0)
-
- // 支付状态
- const isPaying = ref(false)
- const payState = ref('idle') // idle | loading | pending | success | error
- const codeUrl = ref('')
- const orderNo = ref('')
- let pollingTimer = null
-
- // 获取套餐列表
- async function loadProducts() {
- try {
- const res = await post('/vip/getproductlist')
- if (res.code === 0) {
- products.value = (res.list || res.data || []).map((p) => ({
- ...p,
- point: p.point || p.points,
- }))
- }
- } catch {
- products.value = []
- }
- }
-
- // 选择套餐
- function selectProduct(pkg) {
- selectedId.value = pkg.id
- selectedPrice.value = pkg.price
- }
-
- // 开始支付(对齐老 PC 的 buy 流程)
- async function startPay() {
- if (!selectedId.value) return
-
- payState.value = 'loading'
- isPaying.value = true
-
- try {
- const res = await post('/vip/createorder', {
- product_id: selectedId.value,
- client: 1,
- })
-
- if (res.code === 0 && res.data?.code_url) {
- codeUrl.value = res.data.code_url
- orderNo.value = res.data.order_no || ''
- payState.value = 'pending'
- startPolling()
- } else {
- payState.value = 'error'
- }
- } catch {
- payState.value = 'error'
- }
- }
-
- // 轮询支付结果(对齐老 PC:每 2 秒轮询 paysuccess)
- function startPolling() {
- clearInterval(pollingTimer)
- pollingTimer = setInterval(async () => {
- try {
- const res = await post('/vip/paysuccess', { order_no: orderNo.value })
- // status == 2 表示支付成功
- if (res.code === 0 && (res.data?.status == 2 || res.data?.paid === true)) {
- clearInterval(pollingTimer)
- payState.value = 'success'
- // 刷新余额
- await refreshUserInfo()
- }
- } catch {
- // 轮询失败不中断
- }
- }, 2000)
- }
-
- // 取消支付
- function cancelPay() {
- clearInterval(pollingTimer)
- isPaying.value = false
- payState.value = 'idle'
- codeUrl.value = ''
- orderNo.value = ''
- }
-
- // 支付成功完成
- function finishPaySuccess() {
- clearInterval(pollingTimer)
- isPaying.value = false
- payState.value = 'idle'
- codeUrl.value = ''
- orderNo.value = ''
- selectedId.value = null
- }
-
- // 刷新用户余额
- async function refreshUserInfo() {
- try {
- const res = await post('/user/getuserinfo')
- if (res.code === 0 && res.data) {
- userInfo.value.balance = res.data.balance || res.data.point || 0
- }
- } catch {
- // 忽略
- }
- }
-
- onMounted(() => {
- loadProducts()
- if (isLogin.value) {
- refreshUserInfo()
- }
- })
-
- onUnmounted(() => {
- clearInterval(pollingTimer)
- })
- </script>
|