Files
taimed-international-app/pages/book/order.vue

722 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="order-page">
<!-- 导航栏 -->
<nav-bar :title="$t('bookOrder.orderTitle')"></nav-bar>
<!-- 图书信息区域 -->
<view class="order-block">
<view class="order-info">
<image :src="bookInfo.images" class="order-img" mode="aspectFill"></image>
<text class="order-name">{{ bookInfo.name }}</text>
</view>
<view class="order-price">
<text class="order-title">{{ $t('bookOrder.amount') }}</text>
<view v-if="paymentMethod === '5'" class="price-display">
<image src="/static/icon/currency.png" class="coin-img"></image>
<text class="coin-text">{{ displayPrice }} NZD</text>
</view>
<view v-if="paymentMethod === '4'" class="price-display">
<image src="/static/icon/coin.png" class="coin-img"></image>
<text class="coin-text">{{ displayPrice }}</text>
</view>
</view>
</view>
<!-- 支付方式选择区域 -->
<view class="order-type">
<text class="order-title">{{ $t('bookOrder.paymentMethod') }}</text>
<radio-group @change="handlePaymentChange" class="radio-group">
<label
v-for="(item, index) in paymentOptions"
:key="index"
class="type-label"
>
<view class="type-view">
<radio
:value="item.value"
:checked="paymentMethod === item.value"
color="#54a966"
></radio>
<text>
{{ item.name }}
<text v-if="item.value === '4'" class="balance-text">
{{ $t('bookOrder.balance') }}{{ userCoinBalance }}
</text>
</text>
</view>
</label>
</radio-group>
</view>
<!-- 底部确认按钮 -->
<view class="order-btn">
<view class="btn-spacer"></view>
<button
class="confirm-btn"
:disabled="isSubmitting"
@click="handleConfirmOrder"
>
{{ $t('bookOrder.confirm') }}
</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
import { orderApi } from '@/api/modules/order'
import { bookApi } from '@/api/modules/book'
import type { IBookDetail } from '@/types/book'
import type { IPaymentOption } from '@/types/order'
const { t } = useI18n()
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
// 页面参数
const bookId = ref(0)
// 图书信息
const bookInfo = ref<IBookDetail>({
id: 0,
name: '',
images: '',
author: { authorName: '', introduction: '' },
priceData: { dictType: '', dictValue: '' },
abroadPrice: 0,
isBuy: false,
freeChapterCount: 0
})
// 支付相关
const paymentMethod = ref<'4' | '5'>('5') // 默认 Google Pay
const paymentOptions = ref<IPaymentOption[]>([])
const userCoinBalance = ref(0)
const orderSn = ref('')
const isSubmitting = ref(false)
// Google Pay 相关
const googlePayConnected = ref(false)
let googlePayPlugin: any = null
// 计算显示价格
const displayPrice = computed(() => {
if (paymentMethod.value === '4') {
// 虚拟币价格
return (bookInfo.value.abroadPrice || 0) * 10
} else {
// Google Pay 价格
return bookInfo.value.priceData?.dictValue || '0'
}
})
/**
* 页面加载
*/
onLoad((options: any) => {
if (options.id) {
bookId.value = Number(options.id)
}
// 初始化支付方式
initPaymentOptions()
// 初始化 Google Pay
initGooglePay()
// 加载图书信息
loadBookInfo()
})
/**
* 页面显示
*/
onShow(() => {
// 刷新用户信息
loadUserInfo()
})
/**
* 页面卸载
*/
onUnload(() => {
// 清理资源
uni.hideLoading()
})
/**
* 初始化支付方式列表
*/
function initPaymentOptions() {
const platform = uni.getSystemInfoSync().platform
if (platform === 'android') {
// Android 平台显示 Google Pay
paymentOptions.value = [
{ value: '5', name: t('bookOrder.googlePay') }
]
paymentMethod.value = '5'
} else {
// iOS 平台暂不支持支付
paymentOptions.value = []
paymentMethod.value = '5'
}
}
/**
* 初始化 Google Pay 插件
*/
function initGooglePay() {
// #ifdef APP-PLUS
try {
googlePayPlugin = uni.requireNativePlugin('sn-googlepay5')
if (googlePayPlugin) {
googlePayPlugin.init({}, (result: any) => {
console.log('[Google Pay] Init result:', result)
if (result.code === 0) {
googlePayConnected.value = true
console.log('[Google Pay] Connected successfully')
} else {
googlePayConnected.value = false
console.log('[Google Pay] Connection failed')
}
})
}
} catch (error) {
console.error('[Google Pay] Init error:', error)
googlePayConnected.value = false
}
// #endif
}
/**
* 加载图书信息
*/
async function loadBookInfo() {
try {
uni.showLoading({ title: t('common.loading') })
const res = await bookApi.getBookInfo(bookId.value)
uni.hideLoading()
if (res.bookInfo) {
bookInfo.value = res.bookInfo
}
} catch (error) {
uni.hideLoading()
console.error('[Order] Load book info error:', error)
uni.showToast({
title: t('common.networkError'),
icon: 'none'
})
}
}
/**
* 加载用户信息
*/
async function loadUserInfo() {
if (!userInfo.value.id) return
try {
const res = await orderApi.getUserInfo()
if (res.result) {
userCoinBalance.value = res.result.peanutCoin || 0
}
} catch (error) {
console.error('[Order] Load user info error:', error)
}
}
/**
* 切换支付方式
*/
function handlePaymentChange(e: any) {
paymentMethod.value = e.detail.value
}
/**
* 确认下单
*/
async function handleConfirmOrder() {
// 防止重复提交
if (isSubmitting.value) {
uni.showToast({
title: t('bookOrder.doNotRepeat'),
icon: 'none'
})
return
}
// 验证订单
if (!validateOrder()) {
return
}
isSubmitting.value = true
try {
// 创建订单
await createOrder()
} catch (error) {
isSubmitting.value = false
console.error('[Order] Confirm order error:', error)
}
}
/**
* 验证订单
*/
function validateOrder(): boolean {
// 检查是否登录
if (!userInfo.value.id) {
uni.showToast({
title: t('login.agreeFirst'),
icon: 'none'
})
return false
}
// 虚拟币支付时检查余额
if (paymentMethod.value === '4') {
const coinPrice = (bookInfo.value.abroadPrice || 0) * 10
if (userCoinBalance.value < coinPrice) {
uni.showToast({
title: t('bookOrder.insufficientBalance'),
icon: 'none'
})
return false
}
}
return true
}
/**
* 创建订单
*/
async function createOrder() {
try {
uni.showLoading({ title: t('bookOrder.creating') })
const res = await orderApi.createOrder({
paymentMethod: paymentMethod.value,
orderMoney: displayPrice.value,
abroadBookId: bookId.value,
orderType: 'abroadBook'
})
uni.hideLoading()
if (res.code === 0 && res.orderSn) {
orderSn.value = res.orderSn
// 根据支付方式执行相应流程
if (paymentMethod.value === '4') {
// 虚拟币支付
await processVirtualCoinPayment()
} else if (paymentMethod.value === '5') {
// Google Pay 支付
await processGooglePayment()
}
} else {
throw new Error(res.msg || t('bookOrder.orderCreateFailed'))
}
} catch (error: any) {
uni.hideLoading()
isSubmitting.value = false
uni.showToast({
title: error.message || t('bookOrder.orderCreateFailed'),
icon: 'none'
})
}
}
/**
* 处理虚拟币支付
*/
async function processVirtualCoinPayment() {
try {
// 虚拟币支付在订单创建时已完成
// 刷新用户信息
await refreshUserInfo()
// 显示支付成功
uni.showToast({
title: t('bookOrder.paymentSuccess'),
icon: 'success'
})
// 延迟跳转
setTimeout(() => {
navigateToBookDetail()
}, 1000)
} catch (error) {
isSubmitting.value = false
console.error('[Order] Virtual coin payment error:', error)
uni.showToast({
title: t('bookOrder.paymentFailed'),
icon: 'none'
})
}
}
/**
* 处理 Google Pay 支付
*/
async function processGooglePayment() {
try {
// 检查 Google Pay 连接状态
if (!googlePayConnected.value) {
throw new Error(t('bookOrder.googlePayNotAvailable'))
}
uni.showLoading({ title: t('bookOrder.processing') })
// 1. 查询商品 SKU
const skuList = await queryGooglePaySku()
if (skuList.length === 0) {
throw new Error(t('bookOrder.productNotFound'))
}
// 2. 调起 Google Pay 支付
const paymentResult = await initiateGooglePayment()
// 3. 消费购买凭证
await consumeGooglePayPurchase(paymentResult.purchaseToken)
// 4. 后端验证支付
await verifyGooglePayment(paymentResult)
// 5. 支付成功处理
await handlePaymentSuccess()
} catch (error: any) {
uni.hideLoading()
isSubmitting.value = false
console.error('[Order] Google Pay payment error:', error)
uni.showToast({
title: error.message || t('bookOrder.paymentFailed'),
icon: 'none',
duration: 2000
})
}
}
/**
* 查询 Google Pay SKU
*/
function queryGooglePaySku(): Promise<any[]> {
return new Promise((resolve, reject) => {
if (!googlePayPlugin) {
reject(new Error(t('bookOrder.googlePayNotAvailable')))
return
}
const productId = bookInfo.value.priceData?.dictType
if (!productId) {
reject(new Error(t('bookOrder.productNotFound')))
return
}
googlePayPlugin.querySku(
{ inapp: [productId] },
(result: any) => {
console.log('[Google Pay] Query SKU result:', result)
if (result.code === 0 && result.list && result.list.length > 0) {
resolve(result.list)
} else {
reject(new Error(t('bookOrder.productNotFound')))
}
}
)
})
}
/**
* 调起 Google Pay 支付
*/
function initiateGooglePayment(): Promise<{ purchaseToken: string; productId: string }> {
return new Promise((resolve, reject) => {
if (!googlePayPlugin) {
reject(new Error(t('bookOrder.googlePayNotAvailable')))
return
}
const productId = bookInfo.value.priceData?.dictType
googlePayPlugin.payAll(
{
accountId: orderSn.value,
productId: productId
},
(result: any) => {
console.log('[Google Pay] Payment result:', result)
if (result.code === 0 && result.data && result.data.length > 0) {
const purchaseToken = result.data[0].original.purchaseToken
resolve({ purchaseToken, productId })
} else {
// 支付失败或取消
reject(new Error(t('bookOrder.paymentCancelled')))
}
}
)
})
}
/**
* 消费 Google Pay 购买凭证
*/
function consumeGooglePayPurchase(purchaseToken: string): Promise<void> {
return new Promise((resolve, reject) => {
if (!googlePayPlugin) {
reject(new Error(t('bookOrder.googlePayNotAvailable')))
return
}
googlePayPlugin.consume(
{ purchaseToken },
(result: any) => {
console.log('[Google Pay] Consume result:', result)
if (result.code === 0) {
resolve()
} else {
reject(new Error(t('bookOrder.verificationFailed')))
}
}
)
})
}
/**
* 验证 Google Pay 支付
*/
async function verifyGooglePayment(paymentResult: { purchaseToken: string; productId: string }) {
try {
const res = await orderApi.verifyGooglePay({
purchaseToken: paymentResult.purchaseToken,
orderSn: orderSn.value,
productId: paymentResult.productId
})
if (res.code !== 0) {
throw new Error(res.msg || t('bookOrder.verificationFailed'))
}
} catch (error: any) {
throw new Error(error.message || t('bookOrder.verificationFailed'))
}
}
/**
* 支付成功处理
*/
async function handlePaymentSuccess() {
try {
// 刷新用户信息
await refreshUserInfo()
uni.hideLoading()
// 显示支付成功
uni.showToast({
title: t('bookOrder.paymentSuccess'),
icon: 'success'
})
// 延迟跳转
setTimeout(() => {
navigateToBookDetail()
}, 1000)
} catch (error) {
console.error('[Order] Payment success handler error:', error)
// 即使刷新用户信息失败,也跳转到详情页
setTimeout(() => {
navigateToBookDetail()
}, 1000)
}
}
/**
* 刷新用户信息
*/
async function refreshUserInfo() {
if (!userInfo.value.id) return
try {
const res = await orderApi.refreshUserInfo(userInfo.value.id)
if (res.code === 0 && res.user) {
userStore.setUserInfo(res.user)
}
} catch (error) {
console.error('[Order] Refresh user info error:', error)
}
}
/**
* 跳转到图书详情页
*/
function navigateToBookDetail() {
uni.navigateTo({
url: `/pages/book/detail?id=${bookId.value}&page=order`
})
}
</script>
<style lang="scss" scoped>
.order-page {
min-height: 100vh;
background: #f7faf9;
}
.order-block {
margin: 20rpx;
padding: 30rpx;
background: #fff;
border-radius: 15rpx;
}
.order-info {
margin-top: 20rpx;
display: flex;
align-items: center;
.order-img {
width: 180rpx;
height: 240rpx;
border-radius: 10rpx;
flex-shrink: 0;
}
.order-name {
flex: 1;
color: #333;
font-size: 36rpx;
padding-left: 40rpx;
line-height: 44rpx;
max-height: 88rpx;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
.order-price {
padding-top: 40rpx;
display: flex;
align-items: center;
justify-content: space-between;
.price-display {
display: flex;
align-items: center;
}
.coin-img {
width: 40rpx;
height: 40rpx;
margin-right: 10rpx;
}
.coin-text {
font-size: 38rpx;
color: #ff4703;
font-weight: bold;
}
}
.order-type {
background: #fff;
border-radius: 15rpx;
margin: 20rpx;
padding: 30rpx;
.radio-group {
margin-top: 30rpx;
padding-bottom: 10rpx;
}
.type-label {
display: flex;
align-items: center;
margin-top: 10rpx;
.type-view {
width: 100%;
display: flex;
align-items: center;
line-height: 30rpx;
radio {
transform: scale(0.95);
}
text {
font-size: 30rpx;
padding-left: 5rpx;
color: #333;
.balance-text {
padding-left: 10rpx;
color: #54a966;
font-weight: normal;
}
}
}
}
}
.order-title {
font-size: 34rpx;
line-height: 50rpx;
padding-bottom: 10rpx;
color: #333;
}
.order-btn {
width: 100%;
height: 110rpx;
background: #fff;
position: fixed;
bottom: 0;
left: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
.btn-spacer {
flex: 1;
}
.confirm-btn {
margin: 20rpx 0;
padding: 0 35rpx;
height: 70rpx;
line-height: 70rpx;
background-color: #ff4703;
color: #fff;
font-size: 30rpx;
border-radius: 10rpx;
border: none;
&[disabled] {
opacity: 0.6;
}
}
}
</style>