feat: 更新支付模块 (Stripe/PayPal/PingPong) 和 uni-app 配置
This commit is contained in:
@@ -18,6 +18,7 @@ export const PAGES = {
|
||||
LOGIN: '/pages/login/login',
|
||||
PRODUCT: '/pages/product/product',
|
||||
UPGRADE: '/pages/upgrade/upgrade',
|
||||
CREDITS: '/pages/credits/credits',
|
||||
FEEDBACK: '/pages/feedback/feedback',
|
||||
FOLLOWUP: '/pages/followup/followup',
|
||||
NOTIFICATION: '/pages/notification/notification',
|
||||
|
||||
@@ -91,6 +91,12 @@
|
||||
"navigationBarTitleText": "升级会员"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/credits/credits",
|
||||
"style": {
|
||||
"navigationBarTitleText": "购买次数"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/followup/followup",
|
||||
"style": {
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
<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>
|
||||
@@ -9,6 +9,16 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="credit-card" v-if="user.tier !== 'guest'" @click="goCredits">
|
||||
<view class="credit-left">
|
||||
<text class="credit-label">可用次数</text>
|
||||
<text class="credit-value">{{ creditBalance }}</text>
|
||||
</view>
|
||||
<view class="credit-right">
|
||||
<text class="credit-buy">购买次数 ›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">账号设置</view>
|
||||
<view class="menu-item" v-if="user.tier !== 'guest'" @click="showProfileEdit = true">
|
||||
@@ -21,9 +31,9 @@
|
||||
<text class="menu-text">修改密码</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goUpgrade">
|
||||
<view class="menu-item" @click="goCredits">
|
||||
<text class="menu-icon">⭐</text>
|
||||
<text class="menu-text">会员升级</text>
|
||||
<text class="menu-text">购买次数</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -128,11 +138,12 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { authApi } from '@/utils/api.js'
|
||||
import { authApi, creditApi } from '@/utils/api.js'
|
||||
import AiAssistant from '@/components/ai-assistant.vue'
|
||||
import { STORAGE_KEYS, PAGES, TIER_LABELS } from '@/config.js'
|
||||
|
||||
const user = ref({})
|
||||
const creditBalance = ref(0)
|
||||
const showProfileEdit = ref(false)
|
||||
const showPassword = ref(false)
|
||||
const editForm = ref({ username: '', email: '' })
|
||||
@@ -158,6 +169,12 @@ const loadUser = async () => {
|
||||
} catch {
|
||||
user.value = { tier: 'guest' }
|
||||
}
|
||||
try {
|
||||
const cb = await creditApi.balance()
|
||||
creditBalance.value = cb.balance || 0
|
||||
} catch {
|
||||
creditBalance.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
const saveProfile = async () => {
|
||||
@@ -195,7 +212,7 @@ const changePwd = async () => {
|
||||
}
|
||||
|
||||
const goLogin = () => uni.navigateTo({ url: PAGES.LOGIN })
|
||||
const goUpgrade = () => uni.navigateTo({ url: PAGES.UPGRADE })
|
||||
const goCredits = () => uni.navigateTo({ url: PAGES.CREDITS })
|
||||
const goFeedback = () => uni.navigateTo({ url: PAGES.FEEDBACK })
|
||||
const goAgreement = (type) => uni.navigateTo({ url: `/pages/agreement/${type}` })
|
||||
const goCertification = () => uni.navigateTo({ url: PAGES.CERTIFICATION })
|
||||
@@ -280,6 +297,21 @@ onShow(loadUser)
|
||||
.tier-badge.enterprise { background: #e3f2fd; color: #1565c0; }
|
||||
.tier-badge.guest { background: #fce4ec; color: #c62828; }
|
||||
|
||||
.credit-card {
|
||||
background: linear-gradient(135deg, #f97316, #ea580c);
|
||||
border-radius: 16rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.credit-left { display: flex; flex-direction: column; }
|
||||
.credit-label { font-size: 24rpx; color: rgba(255,255,255,0.8); }
|
||||
.credit-value { font-size: 48rpx; color: #fff; font-weight: bold; }
|
||||
.credit-right { }
|
||||
.credit-buy { font-size: 28rpx; color: #fff; font-weight: 500; }
|
||||
|
||||
.section {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
|
||||
@@ -259,6 +259,20 @@ export const paymentApi = {
|
||||
request('/payment/create-order', 'POST', { plan, pay_type: payType }),
|
||||
}
|
||||
|
||||
export const creditApi = {
|
||||
balance: () => request('/credits/balance'),
|
||||
history: (page = 1, size = 20) => request(`/credits/history?page=${page}&size=${size}`),
|
||||
packages: () => request('/credits/packages'),
|
||||
subscriptionPlans: () => request('/credits/subscription-plans'),
|
||||
purchase: (packageId, payType = 'alipay') =>
|
||||
request('/credits/purchase', 'POST', { package_id: packageId, pay_type: payType }),
|
||||
stripePurchase: (packageId, gateway = 'stripe', successUrl, cancelUrl) =>
|
||||
request('/credits/stripe-purchase', 'POST', { package_id: packageId, gateway: gateway, success_url: successUrl, cancel_url: cancelUrl }),
|
||||
cancelSubscription: () => request('/credits/cancel-subscription', 'POST'),
|
||||
paypalCapture: (orderNo, token) =>
|
||||
request('/payment/paypal-capture', 'POST', { order_no: orderNo, token: token }),
|
||||
}
|
||||
|
||||
export const feedbackApi = {
|
||||
submit: (content, category = 'general', contact = '') =>
|
||||
request('/feedback', 'POST', { content, category, contact }),
|
||||
|
||||
Reference in New Issue
Block a user