279 lines
9.8 KiB
Vue
279 lines
9.8 KiB
Vue
<template>
|
|
<view class="page">
|
|
<view class="balance-card">
|
|
<text class="balance-label">可用次数</text>
|
|
<text class="balance-value">{{ balance }}</text>
|
|
<text class="balance-tip" v-if="subscription">含订阅 {{ subCredits }}/月</text>
|
|
</view>
|
|
|
|
<view class="section">
|
|
<view class="tabs">
|
|
<text class="tab" :class="{ active: tab === 'packages' }" @click="tab = 'packages'">次数包</text>
|
|
<text class="tab" :class="{ active: tab === 'plans' }" @click="tab = 'plans'">订阅方案</text>
|
|
<text class="tab" :class="{ active: tab === 'history' }" @click="tab = 'history'">消费记录</text>
|
|
</view>
|
|
|
|
<view v-if="tab === 'packages'">
|
|
<view class="item-card" v-for="pkg in packages" :key="pkg.id">
|
|
<view class="item-info">
|
|
<text class="item-name">{{ pkg.name }}</text>
|
|
<text class="item-credits">{{ pkg.credits }} 次</text>
|
|
</view>
|
|
<view class="item-action">
|
|
<text class="item-price">¥{{ pkg.price }}</text>
|
|
<view class="buy-btns">
|
|
<text class="buy-btn" @click="purchase(pkg.id)">微信</text>
|
|
<text class="buy-btn overseas" @click="overseasPurchase(pkg.id, 'stripe')">Card</text>
|
|
<text class="buy-btn overseas" @click="overseasPurchase(pkg.id, 'paypal')">PayPal</text>
|
|
<text class="buy-btn pingpong" @click="overseasPurchase(pkg.id, 'pingpong')">Card</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="tab === 'plans'">
|
|
<view class="item-card" v-for="plan in subPlans" :key="plan.id"
|
|
:class="{ active: plan.id === currentSubId }">
|
|
<view class="item-info">
|
|
<text class="item-name">{{ plan.name }}</text>
|
|
<text class="item-credits">{{ plan.credits_per_month }} 次/月</text>
|
|
</view>
|
|
<view class="item-action">
|
|
<text class="item-price">¥{{ plan.price }}/月</text>
|
|
<text class="buy-btn" v-if="plan.id === currentSubId" @click="cancelSub">取消订阅</text>
|
|
<text class="buy-btn" v-else @click="subscribe(plan.id)">订阅</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="tab === 'history'">
|
|
<view class="history-item" v-for="item in history" :key="item.id">
|
|
<text class="hist-desc">{{ item.description || item.action }}</text>
|
|
<text class="hist-amount" :class="{ deduct: item.amount < 0 }">
|
|
{{ item.amount > 0 ? '+' : '' }}{{ item.amount }}
|
|
</text>
|
|
<text class="hist-date">{{ formatDate(item.created_at) }}</text>
|
|
</view>
|
|
<text class="empty" v-if="history.length === 0">暂无记录</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { creditApi, paymentApi } from '@/utils/api.js'
|
|
|
|
const tab = ref('packages')
|
|
const balance = ref(0)
|
|
const packages = ref([])
|
|
const subPlans = ref([])
|
|
const history = ref([])
|
|
const subscription = ref(null)
|
|
const currentSubId = ref('')
|
|
const subCredits = ref(0)
|
|
|
|
const handlePayPalRedirect = async () => {
|
|
const params = new URLSearchParams(window.location.search)
|
|
const gateway = params.get('gateway')
|
|
const token = params.get('token')
|
|
const orderId = params.get('order_id')
|
|
const result = params.get('result')
|
|
if (gateway === 'paypal' && result === 'success' && token && orderId) {
|
|
try {
|
|
await creditApi.paypalCapture(orderId, token)
|
|
uni.showToast({ title: '支付成功', icon: 'success' })
|
|
} catch (e) {
|
|
uni.showToast({ title: e.message || '支付处理失败', icon: 'none' })
|
|
}
|
|
const url = new URL(window.location.href)
|
|
url.search = ''
|
|
window.history.replaceState({}, '', url)
|
|
loadData()
|
|
}
|
|
}
|
|
|
|
const loadData = async () => {
|
|
try {
|
|
const cb = await creditApi.balance()
|
|
balance.value = cb.balance || 0
|
|
subscription.value = cb.subscription || null
|
|
if (subscription.value) {
|
|
currentSubId.value = subscription.value.plan_id
|
|
subCredits.value = subscription.value.credits_per_month || 0
|
|
}
|
|
} catch {}
|
|
try {
|
|
const pkgs = await creditApi.packages()
|
|
packages.value = pkgs || []
|
|
} catch {}
|
|
try {
|
|
const plans = await creditApi.subscriptionPlans()
|
|
subPlans.value = plans || []
|
|
} catch {}
|
|
try {
|
|
const h = await creditApi.history()
|
|
history.value = h.items || h || []
|
|
} catch {}
|
|
}
|
|
|
|
const purchase = async (packageId) => {
|
|
uni.showLoading({ title: '创建订单...' })
|
|
try {
|
|
const res = await creditApi.purchase(packageId, 'jsapi')
|
|
if (res.pay_params) {
|
|
uni.requestPayment({
|
|
provider: 'wxpay',
|
|
timeStamp: res.pay_params.timeStamp,
|
|
nonceStr: res.pay_params.nonceStr,
|
|
package: res.pay_params.package,
|
|
signType: res.pay_params.signType,
|
|
paySign: res.pay_params.paySign,
|
|
success: () => {
|
|
uni.showToast({ title: '购买成功', icon: 'success' })
|
|
loadData()
|
|
},
|
|
fail: () => {
|
|
uni.showToast({ title: '支付取消', icon: 'none' })
|
|
},
|
|
})
|
|
} else if (res.pay_url) {
|
|
window.location.href = res.pay_url
|
|
} else {
|
|
uni.showToast({ title: '订单创建成功', icon: 'success' })
|
|
loadData()
|
|
}
|
|
} catch (e) {
|
|
uni.showToast({ title: e.message || '购买失败', icon: 'none' })
|
|
} finally {
|
|
uni.hideLoading()
|
|
}
|
|
}
|
|
|
|
const overseasPurchase = async (packageId, gateway) => {
|
|
uni.showLoading({ title: '跳转支付...' })
|
|
try {
|
|
const baseUrl = 'https://trade.yuzhiran.com/workspace/credits'
|
|
const res = await creditApi.stripePurchase(packageId, gateway, `${baseUrl}?gateway=${gateway}&result=success`, `${baseUrl}?gateway=${gateway}&result=cancel`)
|
|
if (res.session_url) {
|
|
window.location.href = res.session_url
|
|
} else {
|
|
uni.showToast({ title: '创建订单失败', icon: 'none' })
|
|
}
|
|
} catch (e) {
|
|
uni.showToast({ title: e.message || '下单失败', icon: 'none' })
|
|
} finally {
|
|
uni.hideLoading()
|
|
}
|
|
}
|
|
|
|
const subscribe = async (planId) => {
|
|
uni.showLoading({ title: '处理中...' })
|
|
try {
|
|
const res = await paymentApi.createOrder(planId, 'jsapi')
|
|
if (res.pay_params) {
|
|
uni.requestPayment({
|
|
provider: 'wxpay',
|
|
timeStamp: res.pay_params.timeStamp,
|
|
nonceStr: res.pay_params.nonceStr,
|
|
package: res.pay_params.package,
|
|
signType: res.pay_params.signType,
|
|
paySign: res.pay_params.paySign,
|
|
success: () => {
|
|
uni.showToast({ title: '订阅成功', icon: 'success' })
|
|
loadData()
|
|
},
|
|
fail: () => {
|
|
uni.showToast({ title: '支付取消', icon: 'none' })
|
|
},
|
|
})
|
|
} else if (res.pay_url) {
|
|
window.location.href = res.pay_url
|
|
} else {
|
|
uni.showToast({ title: '订阅成功', icon: 'success' })
|
|
loadData()
|
|
}
|
|
} catch (e) {
|
|
uni.showToast({ title: e.message || '订阅失败', icon: 'none' })
|
|
} finally {
|
|
uni.hideLoading()
|
|
}
|
|
}
|
|
|
|
const cancelSub = async () => {
|
|
uni.showModal({
|
|
title: '提示',
|
|
content: '确定取消订阅?',
|
|
success: async (res) => {
|
|
if (res.confirm) {
|
|
try {
|
|
await creditApi.cancelSubscription()
|
|
uni.showToast({ title: '已取消', icon: 'success' })
|
|
loadData()
|
|
} catch (e) {
|
|
uni.showToast({ title: e.message || '取消失败', icon: 'none' })
|
|
}
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
const formatDate = (d) => {
|
|
if (!d) return ''
|
|
const date = new Date(d)
|
|
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`
|
|
}
|
|
|
|
onMounted(loadData)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.page { padding: 20rpx; background: #f5f5f5; min-height: 100vh; }
|
|
.balance-card {
|
|
background: linear-gradient(135deg, #1890ff, #096dd9);
|
|
border-radius: 16rpx;
|
|
padding: 40rpx;
|
|
text-align: center;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
.balance-label { font-size: 28rpx; color: rgba(255,255,255,0.8); display: block; }
|
|
.balance-value { font-size: 72rpx; color: #fff; font-weight: bold; display: block; margin: 10rpx 0; }
|
|
.balance-tip { font-size: 24rpx; color: rgba(255,255,255,0.7); display: block; }
|
|
.section { background: #fff; border-radius: 16rpx; overflow: hidden; }
|
|
.tabs { display: flex; border-bottom: 2rpx solid #f0f0f0; }
|
|
.tab {
|
|
flex: 1; text-align: center; padding: 24rpx 0;
|
|
font-size: 28rpx; color: #666; position: relative;
|
|
}
|
|
.tab.active { color: #1890ff; font-weight: 600; }
|
|
.tab.active::after {
|
|
content: ''; position: absolute; bottom: 0; left: 20%; right: 20%;
|
|
height: 4rpx; background: #1890ff; border-radius: 2rpx;
|
|
}
|
|
.item-card {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
padding: 28rpx 30rpx; border-bottom: 2rpx solid #f5f5f5;
|
|
}
|
|
.item-card.active { background: #f0f9ff; }
|
|
.item-info { display: flex; flex-direction: column; }
|
|
.item-name { font-size: 30rpx; font-weight: 500; color: #333; }
|
|
.item-credits { font-size: 24rpx; color: #999; margin-top: 6rpx; }
|
|
.item-action { display: flex; flex-direction: column; align-items: flex-end; }
|
|
.item-price { font-size: 32rpx; color: #f5222d; font-weight: bold; }
|
|
.buy-btn {
|
|
font-size: 24rpx; color: #1890ff; margin-top: 8rpx; padding: 4rpx 16rpx;
|
|
border: 2rpx solid #1890ff; border-radius: 8rpx;
|
|
}
|
|
.buy-btns { display: flex; gap: 8rpx; margin-top: 8rpx; }
|
|
.buy-btn.overseas { color: #52c41a; border-color: #52c41a; }
|
|
.buy-btn.pingpong { color: #722ed1; border-color: #722ed1; }
|
|
.history-item {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
padding: 24rpx 30rpx; border-bottom: 2rpx solid #f5f5f5;
|
|
}
|
|
.hist-desc { flex: 1; font-size: 26rpx; color: #333; }
|
|
.hist-amount { font-size: 28rpx; font-weight: bold; color: #52c41a; margin-left: 16rpx; }
|
|
.hist-amount.deduct { color: #f5222d; }
|
|
.hist-date { font-size: 22rpx; color: #999; margin-left: 16rpx; min-width: 100rpx; text-align: right; }
|
|
.empty { text-align: center; padding: 60rpx; color: #999; font-size: 28rpx; }
|
|
</style> |