修复:开发测试问题修改
This commit is contained in:
@@ -1,721 +0,0 @@
|
||||
<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>
|
||||
@@ -4,7 +4,7 @@
|
||||
<nav-bar :title="$t('courseDetails.chapter')" />
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<view class="page-content" :style="{ height: contentHeight }">
|
||||
<view class="page-content">
|
||||
<!-- 视频播放器 -->
|
||||
<view v-if="videoList.length > 0" class="video-section">
|
||||
<VideoPlayer
|
||||
@@ -13,19 +13,13 @@
|
||||
:video-list="videoList"
|
||||
:countdown-seconds="5"
|
||||
/>
|
||||
<!-- <AliyunPlayer
|
||||
ref="videoPlayerRef"
|
||||
:currentVideo="videoList[currentVideoIndex]"
|
||||
:currentVideoList="videoList"
|
||||
@unlockChangeVideo="changeVideoLock = false"
|
||||
/> -->
|
||||
</view>
|
||||
|
||||
<!-- 课程和章节信息 -->
|
||||
<view class="info-section">
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('courseDetails.courseInfo') }}:</text>
|
||||
<text class="value">{{ navTitle }}</text>
|
||||
<text class="value">{{ courseTitle }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('courseDetails.chapterInfo') }}:</text>
|
||||
@@ -36,126 +30,74 @@
|
||||
<!-- 视频列表 -->
|
||||
<view v-if="videoList.length > 0" class="video-list-section">
|
||||
<view class="section-title">{{ $t('courseDetails.videoTeaching') }}</view>
|
||||
<view class="video-list">
|
||||
<view
|
||||
v-for="(video, index) in videoList"
|
||||
:key="video.id"
|
||||
:class="['video-item', currentVideoIndex === index ? 'active' : '']"
|
||||
@click="selectVideo(index)"
|
||||
>
|
||||
<view class="video-info">
|
||||
<text class="video-title">【{{ video.type == "2" ? "音频" : "视频" }}】{{ index + 1 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-radio-group v-model="currentVideoIndex" shape="button" >
|
||||
<wd-radio v-for="(video, index) in videoList" :key="video.id" :value="index">
|
||||
【{{ video.type == "2" ? $t('courseDetails.audio') : $t('courseDetails.video') }}】{{ index + 1 }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
|
||||
<!-- 选项卡 -->
|
||||
<view v-if="tabList.length > 0" class="tabs-section">
|
||||
<view class="tabs">
|
||||
<view
|
||||
v-for="(tab, index) in tabList"
|
||||
:key="tab.id"
|
||||
:class="['tab-item', currentTab === index ? 'active' : '']"
|
||||
@click="switchTab(index)"
|
||||
>
|
||||
<text>{{ tab.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 选项卡内容 -->
|
||||
<view class="tab-content">
|
||||
<wd-tabs v-model="currentTab" class="tabs-section" lineWidth="30">
|
||||
<!-- 章节介绍 -->
|
||||
<view v-show="currentTab === 0" class="intro-content">
|
||||
<view class="section-title">{{ $t('courseDetails.chapterIntro') }}</view>
|
||||
<view class="intro-wrapper">
|
||||
<!-- 章节封面 -->
|
||||
<image
|
||||
v-if="chapterDetail?.imgUrl"
|
||||
:src="chapterDetail.imgUrl"
|
||||
mode="widthFix"
|
||||
class="chapter-image"
|
||||
@click="previewImage(chapterDetail.imgUrl)"
|
||||
/>
|
||||
|
||||
<!-- 章节内容 -->
|
||||
<view v-if="chapterDetail?.content" class="chapter-content" v-html="chapterDetail.content"></view>
|
||||
</view>
|
||||
<wd-tab name="chapterIntro" :title="$t('courseDetails.chapterIntro')">
|
||||
<!-- 章节封面 -->
|
||||
<image
|
||||
v-if="chapterDetail?.imgUrl"
|
||||
:src="chapterDetail.imgUrl"
|
||||
mode="widthFix"
|
||||
class="chapter-image"
|
||||
@click="previewImage(chapterDetail.imgUrl)"
|
||||
/>
|
||||
|
||||
<!-- 章节内容 -->
|
||||
<view v-if="chapterDetail?.content" v-html="chapterDetail.content"></view>
|
||||
|
||||
<view class="copyright">
|
||||
<text>{{ $t('courseDetails.copyright') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</wd-tab>
|
||||
<!-- 思考题 -->
|
||||
<view v-show="currentTab === 1" class="question-content">
|
||||
<view class="section-title">{{ $t('courseDetails.thinkingQuestion') }}</view>
|
||||
<view v-if="chapterDetail?.questions" class="question-wrapper">
|
||||
<view class="question-html" v-html="chapterDetail.questions"></view>
|
||||
</view>
|
||||
<view v-else class="no-question">
|
||||
<wd-tab v-if="chapterDetail?.questions" name="thinkingQuestion" :title="$t('courseDetails.thinkingQuestion')">
|
||||
<view v-html="chapterDetail.questions"></view>
|
||||
<!-- <view v-else class="no-question">
|
||||
<wd-divider>{{ $t('courseDetails.noQuestion') }}</wd-divider>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view> -->
|
||||
</wd-tab>
|
||||
</wd-tabs>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad, onShow, onHide } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { courseApi } from '@/api/modules/course'
|
||||
import VideoPlayer from '@/components/video-player/index.vue'
|
||||
import type { IChapterDetail, IVideo } from '@/types/course'
|
||||
|
||||
// 页面参数
|
||||
const chapterId = ref<number>(0)
|
||||
const courseId = ref<number>(0)
|
||||
const navTitle = ref('')
|
||||
const courseTitle = ref('')
|
||||
const chapterTitle = ref('')
|
||||
const noRecored = ref(false)
|
||||
|
||||
// 页面数据
|
||||
const chapterDetail = ref<IChapterDetail | null>(null)
|
||||
const videoList = ref<IVideo[]>([])
|
||||
const currentVideoIndex = ref(0)
|
||||
const activeVideoIndex = ref(0)
|
||||
const currentTab = ref(0)
|
||||
const isFullScreen = ref(false)
|
||||
const currentTab = ref('chapterIntro')
|
||||
|
||||
// 视频播放器引用
|
||||
const videoPlayerRef = ref<any>(null)
|
||||
|
||||
// 选项卡列表
|
||||
const tabList = computed(() => {
|
||||
const tabs = [
|
||||
{ id: '0', name: '章节介绍' }
|
||||
]
|
||||
|
||||
// 如果有思考题,添加思考题选项卡
|
||||
if (chapterDetail.value?.questions) {
|
||||
tabs.push({ id: '1', name: '思考题' })
|
||||
}
|
||||
|
||||
return tabs
|
||||
})
|
||||
|
||||
// 内容高度(全屏时调整)
|
||||
const contentHeight = computed(() => {
|
||||
return isFullScreen.value ? '100vh' : 'auto'
|
||||
})
|
||||
|
||||
/**
|
||||
* 页面加载
|
||||
*/
|
||||
onLoad((options: any) => {
|
||||
chapterId.value = parseInt(options.id)
|
||||
courseId.value = parseInt(options.courseId)
|
||||
navTitle.value = options.navTitle || ''
|
||||
courseTitle.value = options.courseTitle || ''
|
||||
chapterTitle.value = options.title || ''
|
||||
noRecored.value = options.noRecored === 'true'
|
||||
|
||||
loadChapterDetail()
|
||||
})
|
||||
@@ -171,7 +113,7 @@ const loadChapterDetail = async () => {
|
||||
|
||||
// 如果有历史播放记录,定位到对应视频
|
||||
if (res.data.current) {
|
||||
const index = videoList.value.findIndex(v => v.id === res.data.current)
|
||||
const index = videoList.value.findIndex((v:any) => v.id === res.data.current)
|
||||
if (index !== -1) {
|
||||
currentVideoIndex.value = index
|
||||
activeVideoIndex.value = index
|
||||
@@ -188,13 +130,6 @@ const selectVideo = async (index: number) => {
|
||||
currentVideoIndex.value = index
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换选项卡
|
||||
*/
|
||||
const switchTab = (index: number) => {
|
||||
currentTab.value = index
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览图片
|
||||
*/
|
||||
@@ -212,10 +147,6 @@ const previewImage = (url: string) => {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding-bottom: 100rpx;
|
||||
}
|
||||
|
||||
.video-section {
|
||||
background-color: #000;
|
||||
}
|
||||
@@ -254,135 +185,45 @@ const previewImage = (url: string) => {
|
||||
color: #2979ff;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.video-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
|
||||
.video-item {
|
||||
padding: 18rpx;
|
||||
margin-bottom: 10rpx;
|
||||
background-color: #f7f8f9;
|
||||
border-radius: 8rpx;
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background-color: #e8f4ff;
|
||||
border-color: #258feb;
|
||||
}
|
||||
|
||||
.video-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.video-title {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-section {
|
||||
background-color: #fff;
|
||||
margin-top: 20rpx;
|
||||
border-bottom: 2rpx solid #2979ff;
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 25rpx 0;
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
position: relative;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
color: #2979ff;
|
||||
font-weight: 500;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60rpx;
|
||||
height: 4rpx;
|
||||
background-color: #2979ff;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.wd-tabs__nav) {
|
||||
border-bottom: 1px solid #2979ff;
|
||||
}
|
||||
:deep(.wd-tab__body) {
|
||||
padding: 30rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.8;
|
||||
color: #666;
|
||||
word-break: break-all;
|
||||
}
|
||||
:deep(.wd-tabs__line) {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
background-color: #fff;
|
||||
padding: 20rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.intro-content {
|
||||
.intro-wrapper {
|
||||
.chapter-image {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.chapter-content {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.8;
|
||||
color: #666;
|
||||
text-align: justify;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.copyright {
|
||||
margin-top: 40rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
text-align: center;
|
||||
|
||||
text {
|
||||
font-size: 24rpx;
|
||||
color: #ff4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.question-content {
|
||||
.question-wrapper {
|
||||
.question-html {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.8;
|
||||
color: #666;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.no-question {
|
||||
padding: 80rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.chapter-image {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
margin-top: 20rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #ff4444;
|
||||
}
|
||||
|
||||
.no-question {
|
||||
padding: 80rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -339,7 +339,7 @@ const handleChapterClick = (chapter: IChapter) => {
|
||||
const noRecored = chapter.isAudition === 1 && currentCatalogue.value?.isBuy === 0 && !userVip.value
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/course/details/chapter?id=${chapter.id}&courseId=${courseId.value}&navTitle=${courseDetail.value?.title}&title=${chapter.title}&noRecored=${noRecored}`
|
||||
url: `/pages/course/details/chapter?id=${chapter.id}&courseId=${courseId.value}&courseTitle=${courseDetail.value?.title}&title=${chapter.title}&noRecored=${noRecored}`
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="title">{{ $t('forget.title') }}</view>
|
||||
<view class="title" :style="{ 'margin-top': getNotchHeight() + 'px' }">{{ $t('forget.title') }}</view>
|
||||
|
||||
<!-- 邮箱输入 -->
|
||||
<view class="input-box">
|
||||
@@ -53,7 +53,8 @@
|
||||
<text class="input-tit">{{ $t('forget.passwordAgain') }}</text>
|
||||
<input
|
||||
class="input-text"
|
||||
type="password"
|
||||
type="password"
|
||||
minlength="8"
|
||||
maxlength="20"
|
||||
v-model="confirmPassword"
|
||||
:placeholder="$t('forget.passwordAgainPlaceholder')"
|
||||
@@ -73,6 +74,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { commonApi } from '@/api/modules/common'
|
||||
import { resetPassword } from '@/api/modules/auth'
|
||||
import { validateEmail, checkPasswordStrength } from '@/utils/validator'
|
||||
import { getNotchHeight } from '@/utils/system'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<view class="login-page">
|
||||
<!-- Logo 背景区域 -->
|
||||
<view class="logo-bg">
|
||||
<text class="welcome-text">Hello! Welcome to<br>Amazing Limited</text>
|
||||
<text class="welcome-text">Hello! Welcome to<br>太湖国际</text>
|
||||
<image src="@/static/icon/login_icon.png" mode="aspectFit" class="icon-hua-1"></image>
|
||||
<image src="@/static/icon/login_icon.png" mode="aspectFit" class="icon-hua-2"></image>
|
||||
</view>
|
||||
@@ -87,7 +87,7 @@
|
||||
<view class="protocol-text">
|
||||
{{ $t('login.agree') }}
|
||||
<text class="highlight" @click="yhxy">《{{ $t('login.userAgreement') }}》</text>
|
||||
and
|
||||
{{ $t('global.and') }}
|
||||
<text class="highlight" @click="yszc">《{{ $t('login.privacyPolicy') }}》</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -424,11 +424,11 @@ const getAgreements = async (id: number) => {
|
||||
}
|
||||
const loadAgreements = async () => {
|
||||
// 获取用户协议
|
||||
const yhxyRes = await getAgreements(111)
|
||||
const yhxyRes = await getAgreements(116)
|
||||
yhxyText.value = yhxyRes
|
||||
|
||||
// 获取隐私政策
|
||||
const yszcRes = await getAgreements(112)
|
||||
const yszcRes = await getAgreements(117)
|
||||
yszcText.value = yszcRes
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user