722 lines
16 KiB
Vue
722 lines
16 KiB
Vue
<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>
|