Compare commits
14 Commits
Branch_706
...
1d7d00b6b6
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d7d00b6b6 | |||
| bdf1b41098 | |||
| ef2c63784f | |||
| 3d20683d76 | |||
| 0e7952ac4e | |||
| 8f890a6802 | |||
| 8edf719431 | |||
| 3587e15e7a | |||
| 79c6fb247c | |||
| f289e927bb | |||
| 85ca0c7a28 | |||
| 12de098e10 | |||
| 878e49dc2e | |||
| 435a23f995 |
@@ -6,6 +6,8 @@ import { createRequestClient } from '../request';
|
|||||||
import { SERVICE_MAP } from '../config';
|
import { SERVICE_MAP } from '../config';
|
||||||
|
|
||||||
export const paymentClient = createRequestClient({
|
export const paymentClient = createRequestClient({
|
||||||
baseURL: SERVICE_MAP.PAYMENT,
|
baseURL: SERVICE_MAP.MAIN,
|
||||||
|
// baseURL: SERVICE_MAP.PAYMENT,
|
||||||
|
loading: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function responseInterceptor(res: UniApp.RequestSuccessCallbackResult) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 可能为字符串,尝试解析(原项目也做了类似处理)
|
// 可能为字符串,尝试解析(原项目也做了类似处理)
|
||||||
let httpData: IApiResponse | string = res.data as any;
|
let httpData: IApiResponse | string = res.data;
|
||||||
if (typeof httpData === 'string') {
|
if (typeof httpData === 'string') {
|
||||||
try {
|
try {
|
||||||
httpData = JSON.parse(httpData);
|
httpData = JSON.parse(httpData);
|
||||||
@@ -44,19 +44,20 @@ export function responseInterceptor(res: UniApp.RequestSuccessCallbackResult) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 规范化 message 字段
|
// 规范化 message 字段
|
||||||
const message = (httpData as any).msg || (httpData as any).message || (httpData as any).errMsg || '';
|
const message = (httpData as IApiResponse).msg || (httpData as IApiResponse).message || (httpData as IApiResponse).errMsg || '';
|
||||||
|
|
||||||
// 成功判断:与原项目一致的条件
|
const code = (httpData as IApiResponse).code;
|
||||||
const successFlag = (httpData as any).success === true || (httpData as any).code === 0;
|
|
||||||
|
// 成功判断
|
||||||
|
const successFlag = (httpData as IApiResponse).success === true || code === 0;
|
||||||
|
|
||||||
if (successFlag) {
|
if (successFlag) {
|
||||||
// 返回原始 httpData(与原项目 dataFactory 返回 Promise.resolve(httpData) 保持一致)
|
// 返回原始 httpData
|
||||||
// 但大多数调用者更关心 data 字段,这里返回整个 httpData,调用者可取 .data
|
// 实际数据每个接口不同,调用者需根据实际情况取 .data 字段
|
||||||
return Promise.resolve(httpData);
|
return Promise.resolve(httpData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录失效或需要强制登录的一些 code(与原项目一致)
|
// 登录失效或需要强制登录的一些 code
|
||||||
const code = (httpData as any).code;
|
|
||||||
if (code === '401' || code === 401) {
|
if (code === '401' || code === 401) {
|
||||||
// 触发登出流程
|
// 触发登出流程
|
||||||
handleAuthExpired();
|
handleAuthExpired();
|
||||||
@@ -64,7 +65,7 @@ export function responseInterceptor(res: UniApp.RequestSuccessCallbackResult) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 原项目还将 1000,1001,1100,402 等视作需要强制登录
|
// 原项目还将 1000,1001,1100,402 等视作需要强制登录
|
||||||
if (code === '1000' || code === '1001' || code === 1000 || code === 1001 || code === 1100 || code === '402' || code === 402) {
|
if (code == 1000 || code == 1001 || code === 1100 || code === 402) {
|
||||||
handleAuthExpired();
|
handleAuthExpired();
|
||||||
return Promise.reject({ statusCode: 0, errMsg: message || t('global.loginExpired'), data: httpData });
|
return Promise.reject({ statusCode: 0, errMsg: message || t('global.loginExpired'), data: httpData });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import type {
|
|||||||
ICoupon,
|
ICoupon,
|
||||||
ICourseOrderCreateParams,
|
ICourseOrderCreateParams,
|
||||||
IOrderInitData,
|
IOrderInitData,
|
||||||
IGoodsDiscountParams
|
IGoodsDiscountParams,
|
||||||
|
IOrderDetail
|
||||||
} from '@/types/order'
|
} from '@/types/order'
|
||||||
import type { IUserInfo } from '@/types/user'
|
import type { IUserInfo } from '@/types/user'
|
||||||
|
|
||||||
@@ -30,19 +31,6 @@ export const orderApi = {
|
|||||||
return res
|
return res
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证 Google Pay 支付
|
|
||||||
* @param params 支付验证参数
|
|
||||||
*/
|
|
||||||
async verifyGooglePay(params: IGooglePayVerifyParams) {
|
|
||||||
const res = await mainClient.request<IApiResponse>({
|
|
||||||
url: 'pay/googlepay/googleVerify',
|
|
||||||
method: 'POST',
|
|
||||||
data: params
|
|
||||||
})
|
|
||||||
return res
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户信息(包含虚拟币余额)
|
* 获取用户信息(包含虚拟币余额)
|
||||||
*/
|
*/
|
||||||
@@ -106,7 +94,7 @@ export const orderApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取地区优惠金额
|
* 获取活动优惠金额
|
||||||
* @param productList 商品列表
|
* @param productList 商品列表
|
||||||
*/
|
*/
|
||||||
async getDistrictAmount(productList: IGoodsDiscountParams[]) {
|
async getDistrictAmount(productList: IGoodsDiscountParams[]) {
|
||||||
@@ -155,5 +143,18 @@ export const orderApi = {
|
|||||||
data
|
data
|
||||||
})
|
})
|
||||||
return res
|
return res
|
||||||
}
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单详情
|
||||||
|
* @param orderId 订单ID
|
||||||
|
*/
|
||||||
|
async getOrderDetail(orderId: string) {
|
||||||
|
const res = await mainClient.request<IApiResponse<{ buyOrder: IOrderDetail, productInfo: IOrderGoods[] }>>({
|
||||||
|
url: 'common/buyOrder/commonOrderDetail',
|
||||||
|
method: 'POST',
|
||||||
|
data: { orderId }
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// api/modules/user.ts
|
// api/modules/user.ts
|
||||||
import { mainClient } from '@/api/clients/main'
|
import { mainClient } from '@/api/clients/main'
|
||||||
|
import { paymentClient } from '@/api/clients/payment'
|
||||||
import type { IApiResponse } from '@/api/types'
|
import type { IApiResponse } from '@/api/types'
|
||||||
import type {
|
import type {
|
||||||
IUserInfo,
|
IUserInfo,
|
||||||
@@ -194,11 +195,12 @@ export async function submitFeedback(data: IFeedbackForm) {
|
|||||||
* @param orderSn 订单号
|
* @param orderSn 订单号
|
||||||
* @param productId 产品ID
|
* @param productId 产品ID
|
||||||
*/
|
*/
|
||||||
export async function verifyGooglePay(purchaseToken: string, orderSn: string, productId: string) {
|
export async function verifyGooglePay(productId: number, purchaseToken: string, orderSn: string) {
|
||||||
const res = await mainClient.request<IApiResponse>({
|
console.log(productId, purchaseToken, orderSn);
|
||||||
|
const res = await paymentClient.request<IApiResponse>({
|
||||||
url: 'pay/googlepay/googleVerify',
|
url: 'pay/googlepay/googleVerify',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { purchaseToken, orderSn, productId }
|
data: { productId, purchaseToken, orderSn }
|
||||||
})
|
})
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -256,9 +258,13 @@ export async function getActivityDescription() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取充值列表
|
* 充值记录列表
|
||||||
|
* @param current 当前页码
|
||||||
|
* @param limit 每页数量
|
||||||
|
* @param userId 用户id
|
||||||
|
* @return
|
||||||
*/
|
*/
|
||||||
export async function getTransactionDetailsList(current: number, limit: number, userId: string,) {
|
export async function getTransactionDetailsList(current : number, limit : number, userId : string) {
|
||||||
const res = await mainClient.request<IApiResponse>({
|
const res = await mainClient.request<IApiResponse>({
|
||||||
url: 'common/transactionDetails/getTransactionDetailsList',
|
url: 'common/transactionDetails/getTransactionDetailsList',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -266,3 +272,31 @@ export async function getTransactionDetailsList(current: number, limit: number,
|
|||||||
})
|
})
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单编号
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
export async function getPlaceOrder(data: object) {
|
||||||
|
|
||||||
|
const res = await paymentClient.request<IApiResponse>({
|
||||||
|
url: '/book/buyOrder/placeOrder',
|
||||||
|
method: 'POST',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取积分数据
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
export async function getPointsData(current : number, limit : number, userId : string,) {
|
||||||
|
|
||||||
|
const res = await mainClient.request<IApiResponse>({
|
||||||
|
url: 'common/jfTransactionDetails/getJfTransactionDetailsList',
|
||||||
|
method: 'POST',
|
||||||
|
data: { current, limit, userId, }
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { t } from '@/utils/i18n'
|
|||||||
export function createRequestClient(cfg: ICreateClientConfig) {
|
export function createRequestClient(cfg: ICreateClientConfig) {
|
||||||
const baseURL = cfg.baseURL;
|
const baseURL = cfg.baseURL;
|
||||||
const timeout = cfg.timeout ?? REQUEST_TIMEOUT;
|
const timeout = cfg.timeout ?? REQUEST_TIMEOUT;
|
||||||
|
let reqCount= 0
|
||||||
|
|
||||||
async function request<T = any>(options: IRequestOptions): Promise<T> {
|
async function request<T = any>(options: IRequestOptions): Promise<T> {
|
||||||
// 组装 final options
|
// 组装 final options
|
||||||
@@ -22,15 +23,19 @@ export function createRequestClient(cfg: ICreateClientConfig) {
|
|||||||
const intercepted = requestInterceptor(final as IRequestOptions);
|
const intercepted = requestInterceptor(final as IRequestOptions);
|
||||||
|
|
||||||
// 全局处理请求 loading
|
// 全局处理请求 loading
|
||||||
const loading = !cfg.loading ? true : cfg.loading // 接口请求参数不传loading,默认显示loading
|
const loading = cfg.loading ?? true // 接口请求参数不传loading,默认显示loading
|
||||||
loading && uni.showLoading()
|
if (loading) {
|
||||||
|
uni.showLoading({ mask: true })
|
||||||
|
reqCount++
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
uni.request({
|
uni.request({
|
||||||
...intercepted,
|
...intercepted,
|
||||||
complete() {
|
complete() {
|
||||||
// 请求完成关闭 loading
|
// 请求完成关闭 loading
|
||||||
loading && uni.hideLoading()
|
loading && reqCount--
|
||||||
|
reqCount <= 0 && uni.hideLoading()
|
||||||
},
|
},
|
||||||
success(res: any) {
|
success(res: any) {
|
||||||
// 委托给响应拦截器处理
|
// 委托给响应拦截器处理
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<text>{{ $t('book.listen') }}</text>
|
<text>{{ $t('book.listen') }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view
|
<!-- <view
|
||||||
v-if="!isIOS"
|
v-if="!isIOS"
|
||||||
class="action-btn review"
|
class="action-btn review"
|
||||||
@click.stop="handleReview"
|
@click.stop="handleReview"
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<image src="@/static/icon/icon_pl.png" mode="aspectFit" />
|
<image src="@/static/icon/icon_pl.png" mode="aspectFit" />
|
||||||
</view>
|
</view>
|
||||||
<text>{{ $t('book.comment') }}</text>
|
<text>{{ $t('book.comment') }}</text>
|
||||||
</view>
|
</view> -->
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
49
components/book/BookPrice.vue
Normal file
49
components/book/BookPrice.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<view class="book-price-container">
|
||||||
|
<view v-if="data.isBuy" class="book-flag">已购买</view>
|
||||||
|
<view v-else-if="data.isVip == '0'" class="book-flag">免费</view>
|
||||||
|
<view v-else-if="userHasVip && data.isVip == '1'" class="book-price">VIP免费</view>
|
||||||
|
<view v-else class="book-price">{{ item.minPrice }} {{ $t('global.coin') }}</view>
|
||||||
|
<view>
|
||||||
|
<text v-if="data.readCount" class="book-flag">{{ `${data.readCount}${$t('bookHome.readingCount')}` }}</text>
|
||||||
|
<text v-else-if="data.buyCount" class="book-flag">{{ `${data.buyCount}${$t('bookHome.purchased')}` }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { IBook } from '@/types/book'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 检查用户是否为VIP
|
||||||
|
const userHasVip = computed(() => userStore.userInfo?.userEbookVip?.length > 0)
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object as () => IBook,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.book-price-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-price {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #ff4703;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-flag {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -14,13 +14,13 @@
|
|||||||
<text v-else>
|
<text v-else>
|
||||||
课程有效期截止到:{{ catalogue.endTime }}
|
课程有效期截止到:{{ catalogue.endTime }}
|
||||||
</text>
|
</text>
|
||||||
<wd-button
|
<!-- <wd-button
|
||||||
v-if="catalogue.startTime"
|
v-if="catalogue.startTime"
|
||||||
size="small"
|
size="small"
|
||||||
@click="handleRenew"
|
@click="handleRenew"
|
||||||
>
|
>
|
||||||
续费
|
续费
|
||||||
</wd-button>
|
</wd-button> -->
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -259,11 +259,8 @@ const handleChapterClick = (chapter: IChapter) => {
|
|||||||
border-bottom-left-radius: 40rpx;
|
border-bottom-left-radius: 40rpx;
|
||||||
|
|
||||||
.vip-badge {
|
.vip-badge {
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
font-size: 24rpx;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
font-size: 24rpx;
|
||||||
background: linear-gradient(90deg, #6429db 0%, #0075ed 100%);
|
background: linear-gradient(90deg, #6429db 0%, #0075ed 100%);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 10rpx 20rpx;
|
padding: 10rpx 20rpx;
|
||||||
|
|||||||
@@ -1,394 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="ali-player-wrapper" :style="{ background: '#000' }">
|
|
||||||
<div v-if="showError" class="player-error">{{ errorText }}</div>
|
|
||||||
|
|
||||||
<div ref="playerContainer" class="player-container" :style="{ width: '100%', height: playerHeight }"></div>
|
|
||||||
|
|
||||||
<!-- 倒计时覆盖(可选,父层也可以自行实现) -->
|
|
||||||
<div v-if="showCountDown" class="countdown-overlay">
|
|
||||||
<div class="countdown-text">{{ countDownSeconds }} 秒后播放下一个视频</div>
|
|
||||||
<button class="btn-cancel" @click="cancelNext">取消下一个</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 控制按钮示例(父层应该控制 UI,我仅提供常用API按钮用于调试) -->
|
|
||||||
<div class="player-controls" style="display:none;">
|
|
||||||
<button @click="play()">播放</button>
|
|
||||||
<button @click="pause()">暂停</button>
|
|
||||||
<button @click="replay()">重播</button>
|
|
||||||
<button @click="enterFullscreen()">全屏</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, ref, onMounted, onBeforeUnmount, watch, PropType, nextTick } from 'vue';
|
|
||||||
|
|
||||||
type Platform = 'web' | 'app-ios' | 'app-android';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'AliyunPlayer',
|
|
||||||
props: {
|
|
||||||
// videoData: should include fields similar to original: type, m3u8Url, videoUrl, videoId, playAuth, firstTime, id...
|
|
||||||
videoData: { type: Object as PropType<Record<string, any>>, required: true },
|
|
||||||
// platform hint: affects screen lock behavior; default web
|
|
||||||
platform: { type: String as PropType<Platform>, default: 'web' },
|
|
||||||
// height for player area
|
|
||||||
height: { type: String, default: '200px' },
|
|
||||||
// auto start playback
|
|
||||||
autoplay: { type: Boolean, default: true },
|
|
||||||
// how often to auto-save progress in seconds (default 60)
|
|
||||||
autoSaveInterval: { type: Number, default: 60 },
|
|
||||||
// localStorage key prefix for resume data
|
|
||||||
storageKeyPrefix: { type: String, default: 'videoOssList' },
|
|
||||||
// flag: in APP environment should use WebView (true) or try to run player directly in page (false)
|
|
||||||
useWebViewForApp: { type: Boolean, default: false },
|
|
||||||
// urls for loading Aliplayer (allow overriding if needed)
|
|
||||||
playerScriptUrl: { type: String, default: 'https://g.alicdn.com/apsara-media-box/imp-web-player/2.20.3/aliplayer-min.js' },
|
|
||||||
playerComponentsUrl: { type: String, default: 'https://player.alicdn.com/aliplayer/presentation/js/aliplayercomponents.min.js' },
|
|
||||||
playerCssUrl: { type: String, default: 'https://g.alicdn.com/apsara-media-box/imp-web-player/2.20.3/skins/default/aliplayer-min.css' },
|
|
||||||
},
|
|
||||||
emits: [
|
|
||||||
'ready',
|
|
||||||
'play',
|
|
||||||
'pause',
|
|
||||||
'timeupdate',
|
|
||||||
'progress-save', // payload: { videoId, position }
|
|
||||||
'ended',
|
|
||||||
'error',
|
|
||||||
'request-playauth', // in case parent wants to fetch playAuth separately
|
|
||||||
'change-screen',
|
|
||||||
'load-next' // when ended and parent should load next
|
|
||||||
],
|
|
||||||
setup(props, { emit, expose }) {
|
|
||||||
const playerContainer = ref<HTMLElement | null>(null);
|
|
||||||
const playerInstance = ref<any | null>(null);
|
|
||||||
const scriptLoaded = ref(false);
|
|
||||||
const timerDiff = ref(0);
|
|
||||||
const currentSeconds = ref(0);
|
|
||||||
const pauseTime = ref(0);
|
|
||||||
const saveCounter = ref(0);
|
|
||||||
const autoSaveIntervalId = ref<number | null>(null);
|
|
||||||
const showCountDown = ref(false);
|
|
||||||
const countDownSeconds = ref(5);
|
|
||||||
const countdownTimerId = ref<number | null>(null);
|
|
||||||
const showError = ref(false);
|
|
||||||
const errorText = ref('');
|
|
||||||
|
|
||||||
const playerHeight = props.height;
|
|
||||||
|
|
||||||
// helper: localStorage save/load (simple array of {id, time})
|
|
||||||
function loadResumeList(): Array<any> {
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(props.storageKeyPrefix);
|
|
||||||
return raw ? JSON.parse(raw) : [];
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function saveResumeItem(videoId: any, time: number) {
|
|
||||||
try {
|
|
||||||
const list = loadResumeList();
|
|
||||||
const idx = list.findIndex((i: any) => i.id === videoId);
|
|
||||||
if (idx >= 0) list[idx].time = time;
|
|
||||||
else list.push({ id: videoId, time });
|
|
||||||
localStorage.setItem(props.storageKeyPrefix, JSON.stringify(list));
|
|
||||||
} catch (e) { /* ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// dynamic load aliplayer script + css
|
|
||||||
function loadAliplayer(): Promise<void> {
|
|
||||||
if ((window as any).Aliplayer) {
|
|
||||||
scriptLoaded.value = true;
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// css
|
|
||||||
const link = document.createElement('link');
|
|
||||||
link.rel = 'stylesheet';
|
|
||||||
link.href = props.playerCssUrl;
|
|
||||||
document.head.appendChild(link);
|
|
||||||
|
|
||||||
// main script
|
|
||||||
const s = document.createElement('script');
|
|
||||||
s.src = props.playerScriptUrl;
|
|
||||||
s.onload = () => {
|
|
||||||
// components script
|
|
||||||
const s2 = document.createElement('script');
|
|
||||||
s2.src = props.playerComponentsUrl;
|
|
||||||
s2.onload = () => {
|
|
||||||
scriptLoaded.value = true;
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
s2.onerror = () => reject(new Error('aliplayer components load failed'));
|
|
||||||
document.body.appendChild(s2);
|
|
||||||
};
|
|
||||||
s.onerror = () => reject(new Error('aliplayer load failed'));
|
|
||||||
document.body.appendChild(s);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize player with videoData
|
|
||||||
async function initPlayer() {
|
|
||||||
showError.value = false;
|
|
||||||
if (props.useWebViewForApp && props.platform !== 'web') {
|
|
||||||
// In-app recommended to use WebView. Emit event so parent can take over.
|
|
||||||
emit('error', { message: 'App environment required WebView. Set useWebViewForApp=false to attempt in-page' });
|
|
||||||
showError.value = true;
|
|
||||||
errorText.value = 'App environment recommended to use WebView for Aliplayer';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadAliplayer();
|
|
||||||
|
|
||||||
// choose options
|
|
||||||
const v = props.videoData || {};
|
|
||||||
let options: Record<string, any> = {
|
|
||||||
id: (playerContainer.value as HTMLElement).id || 'ali-player-' + Math.random().toString(36).slice(2),
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
autoplay: props.autoplay,
|
|
||||||
isLive: false,
|
|
||||||
rePlay: false,
|
|
||||||
playsinline: true,
|
|
||||||
controlBarVisibility: 'hover',
|
|
||||||
useH5Prism: true,
|
|
||||||
// skinLayout can be extended if needed
|
|
||||||
skinLayout: [
|
|
||||||
{ name: 'bigPlayButton', align: 'cc' },
|
|
||||||
{ name: 'H5Loading', align: 'cc' },
|
|
||||||
{ name: 'errorDisplay', align: 'tlabs' },
|
|
||||||
{ name: 'controlBar', align: 'blabs', children: [
|
|
||||||
{ name: 'progress', align: 'blabs' },
|
|
||||||
{ name: 'playButton', align: 'tl' },
|
|
||||||
{ name: 'timeDisplay', align: 'tl' },
|
|
||||||
{ name: 'prism-speed-selector', align: 'tr' },
|
|
||||||
{ name: 'volume', align: 'tr' }
|
|
||||||
] }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// decide source mode
|
|
||||||
if (v.type === 1) {
|
|
||||||
if (!v.m3u8Url) {
|
|
||||||
// private encrypted: require vid+playAuth
|
|
||||||
if (!v.videoId || !v.playAuth) {
|
|
||||||
// parent might need to request playAuth
|
|
||||||
emit('request-playauth', v);
|
|
||||||
showError.value = true;
|
|
||||||
errorText.value = '播放凭证缺失';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
options = {
|
|
||||||
...options,
|
|
||||||
vid: v.videoId,
|
|
||||||
playauth: v.playAuth,
|
|
||||||
encryptType: 1,
|
|
||||||
playConfig: { EncryptType: 'AliyunVoDEncryption' }
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
options = { ...options, source: v.m3u8Url };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// not encrypted
|
|
||||||
options = { ...options, source: v.videoUrl };
|
|
||||||
}
|
|
||||||
|
|
||||||
// add rate component by default
|
|
||||||
options.components = [
|
|
||||||
{ name: 'RateComponent', type: (window as any).AliPlayerComponent?.RateComponent }
|
|
||||||
];
|
|
||||||
|
|
||||||
// create player
|
|
||||||
try {
|
|
||||||
// ensure container has an id
|
|
||||||
if (playerContainer.value && !(playerContainer.value as HTMLElement).id) {
|
|
||||||
(playerContainer.value as HTMLElement).id = 'ali-player-' + Math.random().toString(36).slice(2);
|
|
||||||
}
|
|
||||||
const player = new (window as any).Aliplayer(options, function (p: any) {
|
|
||||||
// ready
|
|
||||||
});
|
|
||||||
playerInstance.value = player;
|
|
||||||
|
|
||||||
// event binds
|
|
||||||
player.on('ready', () => {
|
|
||||||
emit('ready');
|
|
||||||
if (props.autoplay) player.play();
|
|
||||||
});
|
|
||||||
|
|
||||||
player.on('play', () => {
|
|
||||||
emit('play');
|
|
||||||
});
|
|
||||||
|
|
||||||
player.on('pause', () => {
|
|
||||||
pauseTime.value = Math.floor(player.getCurrentTime() || 0);
|
|
||||||
emit('pause');
|
|
||||||
});
|
|
||||||
|
|
||||||
player.on('timeupdate', () => {
|
|
||||||
const t = Math.floor(player.getCurrentTime() || 0);
|
|
||||||
if (currentSeconds.value !== t) {
|
|
||||||
currentSeconds.value = t;
|
|
||||||
emit('timeupdate', { time: t, status: player.getStatus?.() });
|
|
||||||
saveCounter.value++;
|
|
||||||
// every autoSaveInterval seconds -> emit progress-save
|
|
||||||
if (saveCounter.value >= props.autoSaveInterval) {
|
|
||||||
saveCounter.value = 0;
|
|
||||||
emit('progress-save', { videoId: props.videoData.id, position: currentSeconds.value });
|
|
||||||
// also local save
|
|
||||||
saveResumeItem(props.videoData.id, currentSeconds.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
player.on('ended', () => {
|
|
||||||
emit('ended', { videoId: props.videoData.id });
|
|
||||||
// default behavior: start countdown then emit load-next
|
|
||||||
startNextCountdown();
|
|
||||||
});
|
|
||||||
|
|
||||||
player.on('error', (e: any) => {
|
|
||||||
showError.value = true;
|
|
||||||
errorText.value = '播放出错';
|
|
||||||
emit('error', e);
|
|
||||||
});
|
|
||||||
|
|
||||||
// seek to resume pos if present
|
|
||||||
nextTick(() => {
|
|
||||||
const list = loadResumeList();
|
|
||||||
const idx = list.findIndex(item => item.id === props.videoData.id);
|
|
||||||
const resumeTime = idx >= 0 ? list[idx].time : (props.videoData.firstTime || 0);
|
|
||||||
const dur = player.getDuration ? Math.floor(player.getDuration() || 0) : 0;
|
|
||||||
if (resumeTime && dur && resumeTime < dur) {
|
|
||||||
player.seek(resumeTime);
|
|
||||||
} else if (resumeTime && !dur) {
|
|
||||||
// if duration unknown yet, attempt seek once canplay
|
|
||||||
player.one && player.one('canplay', () => {
|
|
||||||
const d2 = Math.floor(player.getDuration() || 0);
|
|
||||||
if (resumeTime < d2) player.seek(resumeTime);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// periodic autosave fallback (in case events miss)
|
|
||||||
if (autoSaveIntervalId.value) window.clearInterval(autoSaveIntervalId.value);
|
|
||||||
autoSaveIntervalId.value = window.setInterval(() => {
|
|
||||||
if (currentSeconds.value > 0) {
|
|
||||||
emit('progress-save', { videoId: props.videoData.id, position: currentSeconds.value });
|
|
||||||
saveResumeItem(props.videoData.id, currentSeconds.value);
|
|
||||||
}
|
|
||||||
}, props.autoSaveInterval * 1000);
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
showError.value = true;
|
|
||||||
errorText.value = '播放器初始化失败';
|
|
||||||
emit('error', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// start next countdown
|
|
||||||
function startNextCountdown(seconds = 5) {
|
|
||||||
showCountDown.value = true;
|
|
||||||
countDownSeconds.value = seconds;
|
|
||||||
if (countdownTimerId.value) window.clearInterval(countdownTimerId.value);
|
|
||||||
countdownTimerId.value = window.setInterval(() => {
|
|
||||||
countDownSeconds.value -= 1;
|
|
||||||
if (countDownSeconds.value <= 0) {
|
|
||||||
// trigger parent to load next
|
|
||||||
window.clearInterval(countdownTimerId.value!);
|
|
||||||
showCountDown.value = false;
|
|
||||||
emit('load-next', { videoId: props.videoData.id });
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelNext() {
|
|
||||||
showCountDown.value = false;
|
|
||||||
if (countdownTimerId.value) window.clearInterval(countdownTimerId.value);
|
|
||||||
emit('change-screen', { action: 'cancel-next' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// control API exposed
|
|
||||||
function play() {
|
|
||||||
playerInstance.value && playerInstance.value.play && playerInstance.value.play();
|
|
||||||
}
|
|
||||||
function pause() {
|
|
||||||
playerInstance.value && playerInstance.value.pause && playerInstance.value.pause();
|
|
||||||
}
|
|
||||||
function seek(sec: number) {
|
|
||||||
playerInstance.value && playerInstance.value.seek && playerInstance.value.seek(sec);
|
|
||||||
}
|
|
||||||
function replay() {
|
|
||||||
if (playerInstance.value) {
|
|
||||||
playerInstance.value.seek(0);
|
|
||||||
playerInstance.value.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function enterFullscreen() {
|
|
||||||
if (!playerInstance.value) return;
|
|
||||||
const status = playerInstance.value.fullscreenService.getIsFullScreen && playerInstance.value.fullscreenService.getIsFullScreen();
|
|
||||||
if (status) {
|
|
||||||
playerInstance.value.fullscreenService.cancelFullScreen && playerInstance.value.fullscreenService.cancelFullScreen();
|
|
||||||
emit('change-screen', { status: false });
|
|
||||||
// example: lock portrait if in app (needs plus.* or native)
|
|
||||||
} else {
|
|
||||||
playerInstance.value.fullscreenService.requestFullScreen && playerInstance.value.fullscreenService.requestFullScreen();
|
|
||||||
emit('change-screen', { status: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// watch videoData changes
|
|
||||||
watch(() => props.videoData, async (nv) => {
|
|
||||||
// dispose old player
|
|
||||||
if (playerInstance.value && playerInstance.value.dispose) {
|
|
||||||
try { playerInstance.value.dispose(); } catch (e) { console.warn(e); }
|
|
||||||
playerInstance.value = null;
|
|
||||||
}
|
|
||||||
// reset states
|
|
||||||
currentSeconds.value = 0;
|
|
||||||
pauseTime.value = 0;
|
|
||||||
showError.value = false;
|
|
||||||
// init new
|
|
||||||
await initPlayer();
|
|
||||||
}, { immediate: true, deep: true });
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// ensure container has unique id for Aliplayer
|
|
||||||
if (playerContainer.value && !(playerContainer.value as HTMLElement).id) {
|
|
||||||
(playerContainer.value as HTMLElement).id = 'ali-player-' + Math.random().toString(36).slice(2);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (playerInstance.value && playerInstance.value.dispose) {
|
|
||||||
try { playerInstance.value.dispose(); } catch (e) { /* ignore */ }
|
|
||||||
}
|
|
||||||
if (autoSaveIntervalId.value) window.clearInterval(autoSaveIntervalId.value);
|
|
||||||
if (countdownTimerId.value) window.clearInterval(countdownTimerId.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
// expose methods to parent via ref
|
|
||||||
expose({
|
|
||||||
play, pause, seek, replay, startNextCountdown
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
playerContainer,
|
|
||||||
playerHeight,
|
|
||||||
play, pause, seek, replay,
|
|
||||||
showCountDown, countDownSeconds, cancelNext,
|
|
||||||
showError, errorText
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.ali-player-wrapper { position: relative; width: 100%; }
|
|
||||||
.player-container { background: #000; }
|
|
||||||
.countdown-overlay {
|
|
||||||
position: absolute; top: 0; right: 10px; z-index: 50;
|
|
||||||
background: rgba(0,0,0,0.6); color: #fff; padding: 10px; border-radius: 6px;
|
|
||||||
}
|
|
||||||
.btn-cancel { margin-top: 8px; background: #fff; color: #000; border: none; padding:6px 12px; border-radius:4px; }
|
|
||||||
.player-error { color: #fff; text-align:center; padding: 20px; }
|
|
||||||
</style>
|
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<!-- 积分输入 -->
|
<!-- 积分输入 -->
|
||||||
<view v-if="allowPointPay && userInfo?.jf > 0" class="points-input-section">
|
<view v-if="allowPointPay && userInfo?.jf > 0" class="points-input-section">
|
||||||
<text class="points-label">
|
<text class="points-label">
|
||||||
{{ $t('order.maxPoints', { max: pointsUsableMax }) }}
|
{{ $t('order.maxPoints').replace('max', pointsUsableMax) }}
|
||||||
</text>
|
</text>
|
||||||
<view class="points-input-box">
|
<view class="points-input-box">
|
||||||
<input
|
<input
|
||||||
@@ -136,9 +136,9 @@ const userStore = useUserStore()
|
|||||||
interface Props {
|
interface Props {
|
||||||
goodsList: IGoods[],
|
goodsList: IGoods[],
|
||||||
userInfo: object,
|
userInfo: object,
|
||||||
allowPointPay: boolean,
|
allowPointPay?: boolean,
|
||||||
orderType: string,
|
orderType?: string,
|
||||||
backStep: number // 购买完成后返回几层页面
|
backStep?: number // 购买完成后返回几层页面
|
||||||
}
|
}
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
goodsList: () => [],
|
goodsList: () => [],
|
||||||
@@ -373,9 +373,11 @@ const handleSubmit = async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 返回上一页
|
// 返回上一页
|
||||||
|
setTimeout(() => {
|
||||||
uni.navigateBack({
|
uni.navigateBack({
|
||||||
delta: props.backStep
|
delta: props.backStep
|
||||||
})
|
})
|
||||||
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
72
components/order/GoodsPrice.vue
Normal file
72
components/order/GoodsPrice.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<view class="price-info">
|
||||||
|
<text class="price">{{ goodsPrice.lowestPrice }} {{ t('global.coin') }}</text>
|
||||||
|
<text class="price-label">{{ goodsPrice.priceLabel }}</text>
|
||||||
|
<text v-if="goodsPrice.priceLabel" class="original-price">{{ props.goods.price }} {{ t('global.coin') }}</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { calculateLowestPrice } from '@/utils/index'
|
||||||
|
import { t } from '@/utils/i18n'
|
||||||
|
import type { IGoods } from '@/types/order'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
goods: IGoods
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
// 计算商品价格
|
||||||
|
const goodsPrice = computed(() => {
|
||||||
|
const { activityPrice, vipPrice, price } = props.goods
|
||||||
|
const isVipUser = userStore.userVips && userStore.userVips.length > 0
|
||||||
|
const priceLabel = {
|
||||||
|
vipPrice: 'VIP优惠价',
|
||||||
|
activityPrice: '活动价',
|
||||||
|
price: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let priceData = null
|
||||||
|
if (isVipUser) {
|
||||||
|
priceData = { activityPrice, vipPrice, price }
|
||||||
|
} else {
|
||||||
|
priceData = { activityPrice, price }
|
||||||
|
}
|
||||||
|
const lowestPrice = calculateLowestPrice(priceData)
|
||||||
|
|
||||||
|
return {
|
||||||
|
lowestPrice: parseFloat(lowestPrice.value).toFixed(2),
|
||||||
|
priceLabel: priceLabel[lowestPrice.key as keyof typeof priceLabel]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.price-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10rpx;
|
||||||
|
color: #e97512;
|
||||||
|
|
||||||
|
.price {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e97512;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #e97512;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-price {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<view class="payment-item">
|
<view class="payment-item">
|
||||||
<view class="payment-left">
|
<view class="payment-left">
|
||||||
<image src="/static/icon/pay_3.png" class="payment-icon" />
|
<image src="/static/icon/pay_3.png" class="payment-icon" />
|
||||||
<text class="">{{ $t('order.virtualCoin') }}</text>
|
<text class="">{{ $t('global.coin') }}</text>
|
||||||
<text class="text-[#7dc1f0]">
|
<text class="text-[#7dc1f0]">
|
||||||
({{ $t('order.balance') }}:{{ peanutCoin || 0 }})
|
({{ $t('order.balance') }}:{{ peanutCoin || 0 }})
|
||||||
</text>
|
</text>
|
||||||
@@ -25,19 +25,12 @@
|
|||||||
<view class="tip-title">{{ $t('order.paymentTipTitle') }}</view>
|
<view class="tip-title">{{ $t('order.paymentTipTitle') }}</view>
|
||||||
<view class="tip-item">{{ $t('order.paymentTip1') }}</view>
|
<view class="tip-item">{{ $t('order.paymentTip1') }}</view>
|
||||||
<view class="tip-item">
|
<view class="tip-item">
|
||||||
{{ $t('order.paymentTip2') }}
|
{{ $t('order.paymentTip2-1') }}
|
||||||
<text class="link-text" @click="makePhoneCall('022-24142321')">022-24142321</text>
|
<text class="link-text" @click="copyToClipboard('yilujiankangkefu')">yilujiankangkefu</text>
|
||||||
</view>
|
{{ $t('order.paymentTip2-2') }}
|
||||||
<view class="tip-item">
|
<text class="link-text" @click="copyToClipboard('AmazingLimited@163.com')">
|
||||||
{{ $t('order.paymentTip3') }}
|
AmazingLimited@163.com
|
||||||
<text class="link-text" @click="copyToClipboard('publisher@tmrjournals.com')">
|
|
||||||
publisher@tmrjournals.com
|
|
||||||
</text>
|
</text>
|
||||||
{{ $t('order.paymentTip3_1') }}
|
|
||||||
<text class="link-text" @click="copyToClipboard('yilujiankangkefu')">
|
|
||||||
yilujiankangkefu
|
|
||||||
</text>
|
|
||||||
{{ $t('order.paymentTip3_2') }}
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -53,6 +46,15 @@ const props = defineProps({
|
|||||||
default: 0
|
default: 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到充值页面
|
||||||
|
*/
|
||||||
|
const goToRecharge = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/user/recharge/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="order-item-content">
|
<view class="order-item-content" :class="size === 'large' ? 'size-large' : ''">
|
||||||
<view class="order-item-product-info">
|
<view class="order-item-product-info">
|
||||||
<image
|
<image
|
||||||
:src="productImg"
|
:src="productImg"
|
||||||
@@ -18,21 +18,25 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { t } from '@/utils/i18n'
|
import { t } from '@/utils/i18n'
|
||||||
|
import type { IGoods } from '@/types/order'
|
||||||
interface Props {
|
interface Props {
|
||||||
data: any
|
data: IGoods
|
||||||
type: string
|
type: string
|
||||||
|
size?: 'small' | 'large'
|
||||||
}
|
}
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
const productImg = computed(() => {
|
const productImg = computed(() => {
|
||||||
|
console.log(props.data)
|
||||||
switch (props.type) {
|
switch (props.type) {
|
||||||
case 'order':
|
case 'order':
|
||||||
return props.data[0]?.product?.productImages || ''
|
return props.data?.productImages || ''
|
||||||
case 'abroadBook':
|
case 'abroadBook':
|
||||||
return props.data?.images || ''
|
return props.data?.images || ''
|
||||||
case 'vip':
|
case 'vip':
|
||||||
return '/static/vip.png'
|
return '/static/vip.png'
|
||||||
|
case 'abroadVip':
|
||||||
|
return '/static/vip.png'
|
||||||
case 'point':
|
case 'point':
|
||||||
return '/static/jifen.png'
|
return '/static/jifen.png'
|
||||||
default:
|
default:
|
||||||
@@ -42,11 +46,13 @@ const productImg = computed(() => {
|
|||||||
const title = computed(() => {
|
const title = computed(() => {
|
||||||
switch (props.type) {
|
switch (props.type) {
|
||||||
case 'order':
|
case 'order':
|
||||||
return props.data[0]?.product?.productName || ''
|
return (props.data?.goodsType == '02' ? '(电子书)' : '') + props.data?.productName || ''
|
||||||
case 'abroadBook':
|
case 'abroadBook':
|
||||||
return props.data?.name || ''
|
return '(电子书)' + props.data?.name || ''
|
||||||
case 'vip':
|
case 'vip':
|
||||||
return props.data?.title + '<text style="color: #ff4703; font-weight: bold;">(' + props.data?.year + '年)</text>' || ''
|
return props.data?.title + '<text style="color: #ff4703; font-weight: bold;">(' + props.data?.year + '年)</text>' || ''
|
||||||
|
case 'abroadVip':
|
||||||
|
return '电子书VIP' + props.data?.title + '<text style="color: #ff4703; font-weight: bold;">(' + props.data?.days + '天)</text>' || ''
|
||||||
case 'point':
|
case 'point':
|
||||||
return ''
|
return ''
|
||||||
default:
|
default:
|
||||||
@@ -56,11 +62,13 @@ const title = computed(() => {
|
|||||||
const price = computed(() => {
|
const price = computed(() => {
|
||||||
switch (props.type) {
|
switch (props.type) {
|
||||||
case 'order':
|
case 'order':
|
||||||
return props.data[0]?.product?.price || 0
|
return props.data?.price || 0
|
||||||
case 'abroadBook':
|
case 'abroadBook':
|
||||||
return props.data?.abroadPrice || 0
|
return props.data?.abroadPrice || 0
|
||||||
case 'vip':
|
case 'vip':
|
||||||
return props.data?.fee || 0
|
return props.data?.fee || 0
|
||||||
|
case 'abroadVip':
|
||||||
|
return props.data?.money || 0
|
||||||
case 'point':
|
case 'point':
|
||||||
return ''
|
return ''
|
||||||
default:
|
default:
|
||||||
@@ -101,6 +109,7 @@ const price = computed(() => {
|
|||||||
color: #333;
|
color: #333;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
.count {
|
.count {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@@ -108,4 +117,17 @@ const price = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.order-item-content.size-large {
|
||||||
|
.order-item-product-cover {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
.order-item-product-name {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.order-item-product-price {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
39
hooks/useThrottle.ts
Normal file
39
hooks/useThrottle.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按钮节流Hook
|
||||||
|
* @param callback 点击后执行的业务逻辑回调
|
||||||
|
* @param delay 节流时间(默认1500ms)
|
||||||
|
* @returns 节流后的点击事件函数
|
||||||
|
* @template T 回调函数的参数类型(支持多参数元组)
|
||||||
|
*/
|
||||||
|
export const useThrottle = <T extends any[]>(
|
||||||
|
callback: (...args: T) => void | Promise<void>,
|
||||||
|
delay: number = 1500
|
||||||
|
) => {
|
||||||
|
// 标记是否可点击
|
||||||
|
const isClickable = ref(true);
|
||||||
|
|
||||||
|
// 节流后的函数,支持传递任意参数
|
||||||
|
const throttledFn = async (...args: T) => {
|
||||||
|
if (!isClickable.value) return;
|
||||||
|
|
||||||
|
// 锁定按钮
|
||||||
|
isClickable.value = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 执行业务回调,支持异步函数
|
||||||
|
await callback(...args);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('节流回调执行失败:', error);
|
||||||
|
throw error; // 抛出错误,方便组件内捕获
|
||||||
|
} finally {
|
||||||
|
// 延迟后解锁按钮(无论成功/失败都解锁)
|
||||||
|
setTimeout(() => {
|
||||||
|
isClickable.value = true;
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return throttledFn;
|
||||||
|
};
|
||||||
@@ -18,7 +18,9 @@
|
|||||||
"loginExpired": "Login expired. Please log in again.",
|
"loginExpired": "Login expired. Please log in again.",
|
||||||
"requestException": "Request exception",
|
"requestException": "Request exception",
|
||||||
"coin": "Coin",
|
"coin": "Coin",
|
||||||
"days": "Days"
|
"days": "Days",
|
||||||
|
"and": "and",
|
||||||
|
"call": "Call"
|
||||||
},
|
},
|
||||||
"tabar.course": "COURSE",
|
"tabar.course": "COURSE",
|
||||||
"tabar.book": "EBOOK",
|
"tabar.book": "EBOOK",
|
||||||
@@ -79,7 +81,7 @@
|
|||||||
"getCode": "Get Code",
|
"getCode": "Get Code",
|
||||||
"passwordStrengthStrong": "Strong password strength.",
|
"passwordStrengthStrong": "Strong password strength.",
|
||||||
"passwordStrengthMedium": "Medium password strength.",
|
"passwordStrengthMedium": "Medium password strength.",
|
||||||
"passwordStrengthWeak": "please use a password consisting of at least two types: uppercase and lowercase letters, numbers, and symbols, with a length of 8 characters.",
|
"passwordStrengthWeak": "please use a password consisting of at least two types: uppercase and lowercase letters, numbers, and symbols, with a length of 8-20 characters.",
|
||||||
"passwordChanged": "Password changed successfully"
|
"passwordChanged": "Password changed successfully"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -210,7 +212,9 @@
|
|||||||
"yearCard": "Yearly",
|
"yearCard": "Yearly",
|
||||||
"days": "days",
|
"days": "days",
|
||||||
"selectPackage": "Please select a package",
|
"selectPackage": "Please select a package",
|
||||||
"consumptionRecord": "Consumption record"
|
"consumptionRecord": "Consumption record",
|
||||||
|
"returnMine": "I'm about to return to my page"
|
||||||
|
|
||||||
},
|
},
|
||||||
"book": {
|
"book": {
|
||||||
"title": "My Books",
|
"title": "My Books",
|
||||||
@@ -297,7 +301,9 @@
|
|||||||
"listen": {
|
"listen": {
|
||||||
"title": "Audio Book",
|
"title": "Audio Book",
|
||||||
"speed": "Playback Speed",
|
"speed": "Playback Speed",
|
||||||
"chapterList": "Chapter List"
|
"chapterList": "Chapter List",
|
||||||
|
"isLast": "Last Chapter",
|
||||||
|
"isFirst": "First Chapter"
|
||||||
},
|
},
|
||||||
"workOrder": {
|
"workOrder": {
|
||||||
"submit_success": "Submitted successfully"
|
"submit_success": "Submitted successfully"
|
||||||
@@ -391,7 +397,9 @@
|
|||||||
"courseInfo": "Course",
|
"courseInfo": "Course",
|
||||||
"chapterInfo": "Chapter",
|
"chapterInfo": "Chapter",
|
||||||
"videoLoadFailed": "Video load failed",
|
"videoLoadFailed": "Video load failed",
|
||||||
"vipBenefit": "VIP benefit active"
|
"vipBenefit": "VIP benefit active",
|
||||||
|
"audio": "Audio",
|
||||||
|
"video": "Video"
|
||||||
},
|
},
|
||||||
"courseOrder": {
|
"courseOrder": {
|
||||||
"orderTitle": "Order Confirmation",
|
"orderTitle": "Order Confirmation",
|
||||||
@@ -440,7 +448,7 @@
|
|||||||
"notUseCoupon": "Don't use coupon",
|
"notUseCoupon": "Don't use coupon",
|
||||||
"reselect": "Reselect",
|
"reselect": "Reselect",
|
||||||
"selected": "Confirm",
|
"selected": "Confirm",
|
||||||
"maxPoints": "Available Points ({max} pts)",
|
"maxPoints": "Available Points (max pts)",
|
||||||
"pointsPlaceholder": "Enter points",
|
"pointsPlaceholder": "Enter points",
|
||||||
"allPoints": "Total Points",
|
"allPoints": "Total Points",
|
||||||
"insufficientBalance": "Insufficient virtual coin balance",
|
"insufficientBalance": "Insufficient virtual coin balance",
|
||||||
@@ -474,7 +482,8 @@
|
|||||||
"rechargeAmount": "Recharge amount",
|
"rechargeAmount": "Recharge amount",
|
||||||
"valueAddedServices": "Value-added services",
|
"valueAddedServices": "Value-added services",
|
||||||
"readAgree": "I have read and agreed",
|
"readAgree": "I have read and agreed",
|
||||||
"readAgreeServices": "Please read and agree to the value-added services first"
|
"readAgreeServices": "Please read and agree to the value-added services first",
|
||||||
|
"orderDetails": "Order Details"
|
||||||
},
|
},
|
||||||
"vip": {
|
"vip": {
|
||||||
"courseVip": "Course VIP",
|
"courseVip": "Course VIP",
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
"loginExpired": "登录失效,请重新登录。",
|
"loginExpired": "登录失效,请重新登录。",
|
||||||
"requestException": "请求异常",
|
"requestException": "请求异常",
|
||||||
"coin": "天医币",
|
"coin": "天医币",
|
||||||
"days": "天"
|
"days": "天",
|
||||||
|
"and": "和",
|
||||||
|
"call": "拨打电话"
|
||||||
},
|
},
|
||||||
"tabar.course": "课程",
|
"tabar.course": "课程",
|
||||||
"tabar.book": "图书",
|
"tabar.book": "图书",
|
||||||
@@ -80,7 +82,7 @@
|
|||||||
"getCode": "获取验证码",
|
"getCode": "获取验证码",
|
||||||
"passwordStrengthStrong": "密码强度强",
|
"passwordStrengthStrong": "密码强度强",
|
||||||
"passwordStrengthMedium": "密码强度中等",
|
"passwordStrengthMedium": "密码强度中等",
|
||||||
"passwordStrengthWeak": "请使用至少包含大小写字母、数字、符号中的两种类型,长度为8个字符的密码",
|
"passwordStrengthWeak": "请使用至少包含大小写字母、数字、符号中的两种类型,长度为8-20个字符的密码",
|
||||||
"passwordChanged": "密码修改成功"
|
"passwordChanged": "密码修改成功"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -211,7 +213,8 @@
|
|||||||
"yearCard": "年卡",
|
"yearCard": "年卡",
|
||||||
"days": "天",
|
"days": "天",
|
||||||
"selectPackage": "请选择套餐",
|
"selectPackage": "请选择套餐",
|
||||||
"consumptionRecord": "消费记录"
|
"consumptionRecord": "消费记录",
|
||||||
|
"returnMine": "即将返回我的页面"
|
||||||
},
|
},
|
||||||
"book": {
|
"book": {
|
||||||
"title": "我的书单",
|
"title": "我的书单",
|
||||||
@@ -297,7 +300,9 @@
|
|||||||
"listen": {
|
"listen": {
|
||||||
"title": "听书",
|
"title": "听书",
|
||||||
"speed": "播放速度",
|
"speed": "播放速度",
|
||||||
"chapterList": "章节列表"
|
"chapterList": "章节列表",
|
||||||
|
"isLast": "已到最后一章",
|
||||||
|
"isFirst": "已到第一章"
|
||||||
},
|
},
|
||||||
"workOrder": {
|
"workOrder": {
|
||||||
"submit_success": "提交成功"
|
"submit_success": "提交成功"
|
||||||
@@ -391,7 +396,9 @@
|
|||||||
"courseInfo": "课程",
|
"courseInfo": "课程",
|
||||||
"chapterInfo": "章节",
|
"chapterInfo": "章节",
|
||||||
"videoLoadFailed": "视频加载失败",
|
"videoLoadFailed": "视频加载失败",
|
||||||
"vipBenefit": "VIP畅学权益生效中"
|
"vipBenefit": "VIP畅学权益生效中",
|
||||||
|
"audio": "音频",
|
||||||
|
"video": "视频"
|
||||||
},
|
},
|
||||||
"courseOrder": {
|
"courseOrder": {
|
||||||
"orderTitle": "确认订单",
|
"orderTitle": "确认订单",
|
||||||
@@ -440,7 +447,7 @@
|
|||||||
"notUseCoupon": "不使用优惠券",
|
"notUseCoupon": "不使用优惠券",
|
||||||
"reselect": "重新选择",
|
"reselect": "重新选择",
|
||||||
"selected": "选好了",
|
"selected": "选好了",
|
||||||
"maxPoints": "可用积分({max}分)",
|
"maxPoints": "可用积分(max分)",
|
||||||
"pointsPlaceholder": "请输入积分",
|
"pointsPlaceholder": "请输入积分",
|
||||||
"allPoints": "全部积分",
|
"allPoints": "全部积分",
|
||||||
"insufficientBalance": "天医币余额不足",
|
"insufficientBalance": "天医币余额不足",
|
||||||
@@ -455,7 +462,7 @@
|
|||||||
"customerService": "客服",
|
"customerService": "客服",
|
||||||
"paymentTipTitle": "说明",
|
"paymentTipTitle": "说明",
|
||||||
"paymentTip1": "1. 1积分=1天医币",
|
"paymentTip1": "1. 1积分=1天医币",
|
||||||
"paymentTip2-1": "2. 若有疑问请加客服微信:{ customerServiceWechat } { customerServiceEmail }",
|
"paymentTip2-1": "2. 若有疑问请加客服微信:",
|
||||||
"paymentTip2-2": "或邮箱联系",
|
"paymentTip2-2": "或邮箱联系",
|
||||||
"ensureBalance": "确保您的天医币足够支付",
|
"ensureBalance": "确保您的天医币足够支付",
|
||||||
"vipLabel": "VIP优惠",
|
"vipLabel": "VIP优惠",
|
||||||
@@ -475,7 +482,8 @@
|
|||||||
"rechargeAmount": "充值金额",
|
"rechargeAmount": "充值金额",
|
||||||
"valueAddedServices": "增值服务",
|
"valueAddedServices": "增值服务",
|
||||||
"readAgree": "我已阅读并同意",
|
"readAgree": "我已阅读并同意",
|
||||||
"readAgreeServices": "请先阅读并同意增值服务"
|
"readAgreeServices": "请先阅读并同意增值服务",
|
||||||
|
"orderDetails": "订单详情"
|
||||||
},
|
},
|
||||||
"vip": {
|
"vip": {
|
||||||
"courseVip": "课程VIP",
|
"courseVip": "课程VIP",
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name" : "太湖国际",
|
"name" : "吴门国际",
|
||||||
"appid" : "__UNI__1250B39",
|
"appid" : "__UNI__1250B39",
|
||||||
"description" : "太湖国际",
|
"description" : "吴门国际",
|
||||||
"versionName" : "1.0.4",
|
"versionName" : "0.1.1",
|
||||||
"versionCode" : 104,
|
"versionCode" : 11,
|
||||||
"transformPx" : false,
|
"transformPx" : false,
|
||||||
/* 5+App特有相关 */
|
/* 5+App特有相关 */
|
||||||
"app-plus" : {
|
"app-plus" : {
|
||||||
|
|||||||
20
pages.json
20
pages.json
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
|
"pages": [
|
||||||
{
|
{
|
||||||
"path": "pages/course/index",
|
"path": "pages/course/index",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -79,6 +79,12 @@
|
|||||||
"navigationBarTitleText": "%user.virtual%",
|
"navigationBarTitleText": "%user.virtual%",
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
|
},{
|
||||||
|
"path": "pages/user/points/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "%user.points%",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
"path": "pages/user/myBook/index",
|
"path": "pages/user/myBook/index",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -127,12 +133,6 @@
|
|||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
"navigationBarTitleText": "%listen.title%"
|
"navigationBarTitleText": "%listen.title%"
|
||||||
}
|
}
|
||||||
}, {
|
|
||||||
"path": "pages/book/order",
|
|
||||||
"style": {
|
|
||||||
"navigationStyle": "custom",
|
|
||||||
"navigationBarTitleText": "%order.orderTitle%"
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
"path": "pages/course/search",
|
"path": "pages/course/search",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -187,6 +187,12 @@
|
|||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
"navigationBarTitleText": "%order.bookVip%"
|
"navigationBarTitleText": "%order.bookVip%"
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
"path": "pages/user/order/details",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "%order.orderDetails%"
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
"path": "uni_modules/uni-upgrade-center-app/pages/upgrade-popup",
|
"path": "uni_modules/uni-upgrade-center-app/pages/upgrade-popup",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 书评列表 (非iOS) -->
|
<!-- 书评列表 (非iOS) -->
|
||||||
<view v-if="!isIOS" class="comments-section">
|
<!-- <view v-if="!isIOS" class="comments-section">
|
||||||
<view class="section-header">
|
<view class="section-header">
|
||||||
<text class="section-title">{{ $t('bookDetails.message') }}</text>
|
<text class="section-title">{{ $t('bookDetails.message') }}</text>
|
||||||
<view v-if="commentList.length > 0" class="more-link" @click="goToReview">
|
<view v-if="commentList.length > 0" class="more-link" @click="goToReview">
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
/>
|
/>
|
||||||
<text v-else class="empty-text">{{ nullText }}</text>
|
<text v-else class="empty-text">{{ nullText }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view> -->
|
||||||
|
|
||||||
<!-- 相关推荐 -->
|
<!-- 相关推荐 -->
|
||||||
<view class="related-books">
|
<view class="related-books">
|
||||||
@@ -114,31 +114,6 @@
|
|||||||
@confirm="handlePurchase"
|
@confirm="handlePurchase"
|
||||||
@close="closePurchasePopup"
|
@close="closePurchasePopup"
|
||||||
/>
|
/>
|
||||||
<!-- <wd-popup v-model="purchaseVisible" position="bottom">
|
|
||||||
<view class="purchase-popup">
|
|
||||||
<view class="book-info-mini">
|
|
||||||
<image :src="bookInfo.images" mode="aspectFill" />
|
|
||||||
<view class="info">
|
|
||||||
<text class="name">{{ bookInfo.name }}</text>
|
|
||||||
<text v-if="bookInfo.priceData" class="price">
|
|
||||||
$ {{ bookInfo.priceData.dictValue }} NZD
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="spec-section">
|
|
||||||
<text class="spec-title">{{ $t('bookDetails.list') }}</text>
|
|
||||||
<view class="spec-item active">
|
|
||||||
<text>{{ bookInfo.name }}</text>
|
|
||||||
<text v-if="bookInfo.priceData" class="spec-price">
|
|
||||||
${{ bookInfo.priceData.dictValue }} NZD
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<wd-button type="primary" block @click="handlePurchase">
|
|
||||||
{{ $t('bookDetails.buy') }}
|
|
||||||
</wd-button>
|
|
||||||
</view>
|
|
||||||
</wd-popup> -->
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -230,71 +205,44 @@ function initScrollHeight() {
|
|||||||
|
|
||||||
// 加载书籍详情
|
// 加载书籍详情
|
||||||
async function loadBookInfo() {
|
async function loadBookInfo() {
|
||||||
try {
|
|
||||||
const res = await bookApi.getBookInfo(bookId.value)
|
const res = await bookApi.getBookInfo(bookId.value)
|
||||||
|
|
||||||
if (res.bookInfo) {
|
|
||||||
bookInfo.value = res.bookInfo
|
bookInfo.value = res.bookInfo
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load book info:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载购买商品信息
|
// 加载购买商品信息
|
||||||
async function loadGoodsInfo() {
|
async function loadGoodsInfo() {
|
||||||
try {
|
|
||||||
const res = await bookApi.getBookGoods(bookId.value)
|
const res = await bookApi.getBookGoods(bookId.value)
|
||||||
if (res.code === 0) {
|
|
||||||
goodsList.value = res.productList || []
|
goodsList.value = res.productList || []
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load goods info:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载统计数据
|
// 加载统计数据
|
||||||
async function loadBookCount() {
|
async function loadBookCount() {
|
||||||
try {
|
|
||||||
const res = await bookApi.getBookReadCount(bookId.value)
|
const res = await bookApi.getBookReadCount(bookId.value)
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
readCount.value = res.readCount || 0
|
readCount.value = res.readCount || 0
|
||||||
listenCount.value = res.listenCount || 0
|
listenCount.value = res.listenCount || 0
|
||||||
buyCount.value = res.buyCount || 0
|
buyCount.value = res.buyCount || 0
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load book count:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载评论
|
// 加载评论
|
||||||
async function loadComments() {
|
async function loadComments() {
|
||||||
try {
|
|
||||||
const res = await bookApi.getBookComments(bookId.value, 1, 10)
|
const res = await bookApi.getBookComments(bookId.value, 1, 10)
|
||||||
if (res.commentsTree && res.commentsTree.length > 0) {
|
if (res.commentsTree && res.commentsTree.length > 0) {
|
||||||
commentList.value = res.commentsTree
|
commentList.value = res.commentsTree
|
||||||
} else {
|
} else {
|
||||||
nullText.value = t('common.data_null')
|
nullText.value = t('common.data_null')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
nullText.value = t('common.data_null')
|
|
||||||
console.error('Failed to load comments:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载推荐书籍
|
// 加载推荐书籍
|
||||||
async function loadRecommendBooks() {
|
async function loadRecommendBooks() {
|
||||||
try {
|
|
||||||
const res = await bookApi.getRecommendBook(bookId.value)
|
const res = await bookApi.getRecommendBook(bookId.value)
|
||||||
if (res.bookList && res.bookList.length > 0) {
|
if (res.bookList && res.bookList.length > 0) {
|
||||||
relatedBooks.value = res.bookList
|
relatedBooks.value = res.bookList
|
||||||
} else {
|
} else {
|
||||||
nullBookText.value = t('common.data_null')
|
nullBookText.value = t('common.data_null')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
nullBookText.value = t('common.data_null')
|
|
||||||
console.error('Failed to load recommend books:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示购买弹窗
|
// 显示购买弹窗
|
||||||
|
|||||||
@@ -162,10 +162,7 @@
|
|||||||
>
|
>
|
||||||
<image :src="item.images" />
|
<image :src="item.images" />
|
||||||
<text class="book-text">{{ item.name }}</text>
|
<text class="book-text">{{ item.name }}</text>
|
||||||
<text class="book-price">{{ item.minPrice }} 天医币</text>
|
<BookPrice :data="item" class="book-price-container" />
|
||||||
<text v-if="formatStats(item)" class="book-flag">{{
|
|
||||||
formatStats(item)
|
|
||||||
}}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text v-else class="zanwu" style="padding: 100rpx 0">{{ $t('global.dataNull') }}</text>
|
<text v-else class="zanwu" style="padding: 100rpx 0">{{ $t('global.dataNull') }}</text>
|
||||||
@@ -177,9 +174,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { onShow } from '@dcloudio/uni-app'
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { homeApi } from '@/api/modules/book_home'
|
import { homeApi } from '@/api/modules/book_home'
|
||||||
import { getNotchHeight } from '@/utils/system'
|
import { getNotchHeight } from '@/utils/system'
|
||||||
|
import BookPrice from '@/components/book/BookPrice.vue'
|
||||||
import type {
|
import type {
|
||||||
IBook,
|
IBook,
|
||||||
IBookWithStats,
|
IBookWithStats,
|
||||||
@@ -187,8 +184,6 @@ import type {
|
|||||||
IVipInfo
|
IVipInfo
|
||||||
} from '@/types/book'
|
} from '@/types/book'
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
// 状态定义
|
// 状态定义
|
||||||
const showMyBooks = ref(false)
|
const showMyBooks = ref(false)
|
||||||
const showActivity = ref(false)
|
const showActivity = ref(false)
|
||||||
@@ -219,21 +214,16 @@ const vipInfo = ref<IVipInfo | null>(null)
|
|||||||
* 获取VIP信息
|
* 获取VIP信息
|
||||||
*/
|
*/
|
||||||
const getVipInfo = async () => {
|
const getVipInfo = async () => {
|
||||||
try {
|
|
||||||
const res = await homeApi.getVipInfo()
|
const res = await homeApi.getVipInfo()
|
||||||
if (res.vipInfo) {
|
if (res.vipInfo) {
|
||||||
vipInfo.value = res.vipInfo
|
vipInfo.value = res.vipInfo
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取VIP信息失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取我的书单
|
* 获取我的书单
|
||||||
*/
|
*/
|
||||||
const getMyBooks = async () => {
|
const getMyBooks = async () => {
|
||||||
try {
|
|
||||||
const res = await homeApi.getMyBooks(1, 10)
|
const res = await homeApi.getMyBooks(1, 10)
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
showMyBooks.value = true
|
showMyBooks.value = true
|
||||||
@@ -246,30 +236,22 @@ const getMyBooks = async () => {
|
|||||||
url: '/pages/login/login'
|
url: '/pages/login/login'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取我的书单失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取推荐图书
|
* 获取推荐图书
|
||||||
*/
|
*/
|
||||||
const getRecommendBooks = async () => {
|
const getRecommendBooks = async () => {
|
||||||
try {
|
|
||||||
const res = await homeApi.getRecommendBooks()
|
const res = await homeApi.getRecommendBooks()
|
||||||
if (res.books && res.books.length > 0) {
|
if (res.books && res.books.length > 0) {
|
||||||
recommendBooksList.value = res.books
|
recommendBooksList.value = res.books
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取推荐图书失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取活动标签列表
|
* 获取活动标签列表
|
||||||
*/
|
*/
|
||||||
const getActivityLabels = async () => {
|
const getActivityLabels = async () => {
|
||||||
try {
|
|
||||||
const res = await homeApi.getBookLabelList(1)
|
const res = await homeApi.getBookLabelList(1)
|
||||||
showActivity.value = true
|
showActivity.value = true
|
||||||
if (res.lableList && res.lableList.length > 0) {
|
if (res.lableList && res.lableList.length > 0) {
|
||||||
@@ -277,16 +259,12 @@ const getActivityLabels = async () => {
|
|||||||
// 默认加载第一个标签的图书列表
|
// 默认加载第一个标签的图书列表
|
||||||
await getBooksByLabel(res.lableList[0].id, 'activity')
|
await getBooksByLabel(res.lableList[0].id, 'activity')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取活动标签失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取分类标签列表
|
* 获取分类标签列表
|
||||||
*/
|
*/
|
||||||
const getCategoryLabels = async () => {
|
const getCategoryLabels = async () => {
|
||||||
try {
|
|
||||||
const res = await homeApi.getBookLabelList(0)
|
const res = await homeApi.getBookLabelList(0)
|
||||||
showCategory.value = true
|
showCategory.value = true
|
||||||
if (res.lableList && res.lableList.length > 0) {
|
if (res.lableList && res.lableList.length > 0) {
|
||||||
@@ -294,16 +272,12 @@ const getCategoryLabels = async () => {
|
|||||||
// 默认加载第一个标签的二级标签
|
// 默认加载第一个标签的二级标签
|
||||||
await getSubLabels(res.lableList[0].id, 0)
|
await getSubLabels(res.lableList[0].id, 0)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取分类标签失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取二级标签列表
|
* 获取二级标签列表
|
||||||
*/
|
*/
|
||||||
const getSubLabels = async (pid: number, index: number) => {
|
const getSubLabels = async (pid: number, index: number) => {
|
||||||
try {
|
|
||||||
const res = await homeApi.getSubLabelList(pid)
|
const res = await homeApi.getSubLabelList(pid)
|
||||||
currentLevel1Index.value = index
|
currentLevel1Index.value = index
|
||||||
if (res.lableList && res.lableList.length > 0) {
|
if (res.lableList && res.lableList.length > 0) {
|
||||||
@@ -316,9 +290,6 @@ const getSubLabels = async (pid: number, index: number) => {
|
|||||||
categoryLevel2List.value = []
|
categoryLevel2List.value = []
|
||||||
await getBooksByLabel(pid, 'category')
|
await getBooksByLabel(pid, 'category')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取二级标签失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -328,7 +299,6 @@ const getBooksByLabel = async (
|
|||||||
labelId: number,
|
labelId: number,
|
||||||
type: 'activity' | 'category'
|
type: 'activity' | 'category'
|
||||||
) => {
|
) => {
|
||||||
try {
|
|
||||||
const res = await homeApi.getBooksByLabel(labelId)
|
const res = await homeApi.getBooksByLabel(labelId)
|
||||||
if (type === 'activity') {
|
if (type === 'activity') {
|
||||||
if (res.bookList && res.bookList.length > 0) {
|
if (res.bookList && res.bookList.length > 0) {
|
||||||
@@ -343,46 +313,6 @@ const getBooksByLabel = async (
|
|||||||
categoryBookList.value = []
|
categoryBookList.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取图书列表失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化价格
|
|
||||||
*/
|
|
||||||
const formatPrice = (book: IBookWithStats): string => {
|
|
||||||
// 已购买不显示价格
|
|
||||||
if (book.isBuy) return ''
|
|
||||||
|
|
||||||
// VIP用户且图书为VIP专享
|
|
||||||
if (vipInfo.value?.id && book.isVip === '2') {
|
|
||||||
const price = book.sysDictData?.dictValue
|
|
||||||
return price ? `$ ${price} NZD` : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 普通用户
|
|
||||||
if (!vipInfo.value?.id) {
|
|
||||||
const price = book.sysDictData?.dictValue
|
|
||||||
return price ? `$ ${price} NZD` : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化统计信息
|
|
||||||
*/
|
|
||||||
const formatStats = (book: IBookWithStats): string => {
|
|
||||||
if (book.readCount && book.readCount > 0) {
|
|
||||||
return `${book.readCount}${t('bookHome.readingCount')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (book.buyCount && book.buyCount > 0) {
|
|
||||||
return `${book.buyCount}${t('bookHome.purchased')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -416,8 +346,8 @@ const handleBookClick = (bookId: number) => {
|
|||||||
* 处理更多按钮点击
|
* 处理更多按钮点击
|
||||||
*/
|
*/
|
||||||
const handleMoreClick = () => {
|
const handleMoreClick = () => {
|
||||||
uni.switchTab({
|
uni.navigateTo({
|
||||||
url: '/pages/book/index'
|
url: '/pages/user/myBook/index'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -807,21 +737,9 @@ onShow(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-price {
|
.book-price-container {
|
||||||
position: absolute;
|
width: 80%;
|
||||||
font-size: 28rpx;
|
margin: 15rpx auto 0;
|
||||||
color: #ff4703;
|
|
||||||
left: 30rpx;
|
|
||||||
bottom: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-flag {
|
|
||||||
display: block;
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #999;
|
|
||||||
position: absolute;
|
|
||||||
right: 6%;
|
|
||||||
bottom: 20rpx;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,19 +117,12 @@ function initScrollHeight() {
|
|||||||
|
|
||||||
// 加载书籍信息
|
// 加载书籍信息
|
||||||
async function loadBookInfo() {
|
async function loadBookInfo() {
|
||||||
try {
|
|
||||||
const res = await bookApi.getBookInfo(bookId.value)
|
const res = await bookApi.getBookInfo(bookId.value)
|
||||||
if (res.bookInfo) {
|
|
||||||
bookInfo.value = res.bookInfo
|
bookInfo.value = res.bookInfo
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load book info:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载章节列表
|
// 加载章节列表
|
||||||
async function loadChapterList() {
|
async function loadChapterList() {
|
||||||
try {
|
|
||||||
const res = await bookApi.getBookChapter({
|
const res = await bookApi.getBookChapter({
|
||||||
bookId: bookId.value
|
bookId: bookId.value
|
||||||
})
|
})
|
||||||
@@ -139,10 +132,6 @@ async function loadChapterList() {
|
|||||||
} else {
|
} else {
|
||||||
nullText.value = t('common.data_null')
|
nullText.value = t('common.data_null')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
nullText.value = t('common.data_null')
|
|
||||||
console.error('Failed to load chapter list:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 播放章节
|
// 播放章节
|
||||||
|
|||||||
@@ -245,15 +245,9 @@ function initAudioContext() {
|
|||||||
|
|
||||||
// 加载书籍信息
|
// 加载书籍信息
|
||||||
async function loadBookInfo() {
|
async function loadBookInfo() {
|
||||||
try {
|
|
||||||
const res = await bookApi.getBookInfo(bookId.value)
|
const res = await bookApi.getBookInfo(bookId.value)
|
||||||
if (res.bookInfo) {
|
|
||||||
bookInfo.value = res.bookInfo
|
bookInfo.value = res.bookInfo
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load book info:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载章节列表
|
// 加载章节列表
|
||||||
async function loadChapterList() {
|
async function loadChapterList() {
|
||||||
@@ -387,7 +381,7 @@ async function prevChapter() {
|
|||||||
playChapter(chapterList.value[currentChapterIndex.value])
|
playChapter(chapterList.value[currentChapterIndex.value])
|
||||||
} else {
|
} else {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: t('listen.earlier'),
|
title: t('listen.isFirst'),
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -418,7 +412,7 @@ async function nextChapter() {
|
|||||||
playChapter(chapterList.value[currentChapterIndex.value])
|
playChapter(chapterList.value[currentChapterIndex.value])
|
||||||
} else {
|
} else {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: t('listen.behind'),
|
title: t('listen.isLast'),
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -606,7 +606,7 @@ function changeReadMode(mode: 'scroll' | 'page') {
|
|||||||
const bookLanguages = ref([])
|
const bookLanguages = ref([])
|
||||||
const currentLanguage = ref('')
|
const currentLanguage = ref('')
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
currentLanguage.value = uni.getStorageSync('currentBookLanguage') || ''
|
currentLanguage.value = uni.getStorageSync('currentBookLanguage') || '中文'
|
||||||
console.log('currentLanguage', currentLanguage.value)
|
console.log('currentLanguage', currentLanguage.value)
|
||||||
})
|
})
|
||||||
const getBookLanguages = async () => {
|
const getBookLanguages = async () => {
|
||||||
|
|||||||
@@ -164,14 +164,10 @@ function initScrollHeight() {
|
|||||||
|
|
||||||
// 加载书籍信息
|
// 加载书籍信息
|
||||||
async function loadBookInfo() {
|
async function loadBookInfo() {
|
||||||
try {
|
|
||||||
const res = await bookApi.getBookInfo(bookId.value)
|
const res = await bookApi.getBookInfo(bookId.value)
|
||||||
if (res.bookInfo) {
|
if (res.bookInfo) {
|
||||||
bookInfo.value = res.bookInfo
|
bookInfo.value = res.bookInfo
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load book info:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载评论列表
|
// 加载评论列表
|
||||||
@@ -180,7 +176,6 @@ async function loadComments() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await bookApi.getBookComments(bookId.value, page.value.current, page.value.limit)
|
const res = await bookApi.getBookComments(bookId.value, page.value.current, page.value.limit)
|
||||||
|
|
||||||
commentsCount.value = res.commentsCount || 0
|
commentsCount.value = res.commentsCount || 0
|
||||||
@@ -191,10 +186,6 @@ async function loadComments() {
|
|||||||
} else if (commentList.value.length === 0) {
|
} else if (commentList.value.length === 0) {
|
||||||
nullText.value = t('common.data_null')
|
nullText.value = t('common.data_null')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
nullText.value = t('common.data_null')
|
|
||||||
console.error('Failed to load comments:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载更多
|
// 加载更多
|
||||||
@@ -275,7 +266,6 @@ function handleEmj(i: any) {
|
|||||||
|
|
||||||
// 提交评论
|
// 提交评论
|
||||||
async function submitComment() {
|
async function submitComment() {
|
||||||
try {
|
|
||||||
const content = await getEditorContent()
|
const content = await getEditorContent()
|
||||||
|
|
||||||
if (!content || content === '<p><br></p>') {
|
if (!content || content === '<p><br></p>') {
|
||||||
@@ -299,9 +289,6 @@ async function submitComment() {
|
|||||||
editorCtx.value?.clear()
|
editorCtx.value?.clear()
|
||||||
resetComments()
|
resetComments()
|
||||||
}, 500)
|
}, 500)
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to submit comment:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点赞/取消点赞
|
// 点赞/取消点赞
|
||||||
@@ -314,7 +301,6 @@ async function handleLike(comment: IComment) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
if (comment.isLike === 0) {
|
if (comment.isLike === 0) {
|
||||||
await bookApi.likeComment(comment.id)
|
await bookApi.likeComment(comment.id)
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
@@ -334,9 +320,6 @@ async function handleLike(comment: IComment) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resetComments()
|
resetComments()
|
||||||
}, 200)
|
}, 200)
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to like comment:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除评论
|
// 删除评论
|
||||||
@@ -348,7 +331,6 @@ function handleDelete(comment: IComment) {
|
|||||||
confirmText: t('common.confirm_text'),
|
confirmText: t('common.confirm_text'),
|
||||||
success: async (res) => {
|
success: async (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
try {
|
|
||||||
await bookApi.deleteComment(comment.id)
|
await bookApi.deleteComment(comment.id)
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: t('bookDetails.deleteSuccess'),
|
title: t('bookDetails.deleteSuccess'),
|
||||||
@@ -359,9 +341,6 @@ function handleDelete(comment: IComment) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resetComments()
|
resetComments()
|
||||||
}, 500)
|
}, 500)
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to delete comment:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -70,15 +70,9 @@ onLoad((options: any) => {
|
|||||||
* 获取VIP信息
|
* 获取VIP信息
|
||||||
*/
|
*/
|
||||||
const getVipInfo = async () => {
|
const getVipInfo = async () => {
|
||||||
try {
|
|
||||||
const res = await homeApi.getVipInfo()
|
const res = await homeApi.getVipInfo()
|
||||||
if (res.vipInfo) {
|
|
||||||
vipInfo.value = res.vipInfo
|
vipInfo.value = res.vipInfo
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取VIP信息失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理搜索
|
* 处理搜索
|
||||||
@@ -91,7 +85,6 @@ const handleSearch = async () => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
isEmpty.value = false
|
isEmpty.value = false
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await homeApi.searchBooks({
|
const res = await homeApi.searchBooks({
|
||||||
title: keyword.value.trim(),
|
title: keyword.value.trim(),
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -104,14 +97,8 @@ const handleSearch = async () => {
|
|||||||
searchResults.value = []
|
searchResults.value = []
|
||||||
isEmpty.value = true
|
isEmpty.value = true
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('搜索失败:', error)
|
|
||||||
searchResults.value = []
|
|
||||||
isEmpty.value = true
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理清空
|
* 处理清空
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<nav-bar :title="$t('courseDetails.chapter')" />
|
<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">
|
<view v-if="videoList.length > 0" class="video-section">
|
||||||
<VideoPlayer
|
<VideoPlayer
|
||||||
@@ -13,19 +13,13 @@
|
|||||||
:video-list="videoList"
|
:video-list="videoList"
|
||||||
:countdown-seconds="5"
|
:countdown-seconds="5"
|
||||||
/>
|
/>
|
||||||
<!-- <AliyunPlayer
|
|
||||||
ref="videoPlayerRef"
|
|
||||||
:currentVideo="videoList[currentVideoIndex]"
|
|
||||||
:currentVideoList="videoList"
|
|
||||||
@unlockChangeVideo="changeVideoLock = false"
|
|
||||||
/> -->
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 课程和章节信息 -->
|
<!-- 课程和章节信息 -->
|
||||||
<view class="info-section">
|
<view class="info-section">
|
||||||
<view class="info-item">
|
<view class="info-item">
|
||||||
<text class="label">{{ $t('courseDetails.courseInfo') }}:</text>
|
<text class="label">{{ $t('courseDetails.courseInfo') }}:</text>
|
||||||
<text class="value">{{ navTitle }}</text>
|
<text class="value">{{ courseTitle }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="info-item">
|
<view class="info-item">
|
||||||
<text class="label">{{ $t('courseDetails.chapterInfo') }}:</text>
|
<text class="label">{{ $t('courseDetails.chapterInfo') }}:</text>
|
||||||
@@ -36,40 +30,17 @@
|
|||||||
<!-- 视频列表 -->
|
<!-- 视频列表 -->
|
||||||
<view v-if="videoList.length > 0" class="video-list-section">
|
<view v-if="videoList.length > 0" class="video-list-section">
|
||||||
<view class="section-title">{{ $t('courseDetails.videoTeaching') }}</view>
|
<view class="section-title">{{ $t('courseDetails.videoTeaching') }}</view>
|
||||||
<view class="video-list">
|
<wd-radio-group v-model="currentVideoIndex" shape="button" >
|
||||||
<view
|
<wd-radio v-for="(video, index) in videoList" :key="video.id" :value="index">
|
||||||
v-for="(video, index) in videoList"
|
【{{ video.type == "2" ? $t('courseDetails.audio') : $t('courseDetails.video') }}】{{ index + 1 }}
|
||||||
:key="video.id"
|
</wd-radio>
|
||||||
:class="['video-item', currentVideoIndex === index ? 'active' : '']"
|
</wd-radio-group>
|
||||||
@click="selectVideo(index)"
|
|
||||||
>
|
|
||||||
<view class="video-info">
|
|
||||||
<text class="video-title">【{{ video.type == "2" ? "音频" : "视频" }}】{{ index + 1 }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 选项卡 -->
|
<!-- 选项卡 -->
|
||||||
<view v-if="tabList.length > 0" class="tabs-section">
|
<wd-tabs v-model="currentTab" class="tabs-section" lineWidth="30">
|
||||||
<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">
|
|
||||||
<!-- 章节介绍 -->
|
<!-- 章节介绍 -->
|
||||||
<view v-show="currentTab === 0" class="intro-content">
|
<wd-tab name="chapterIntro" :title="$t('courseDetails.chapterIntro')">
|
||||||
<view class="section-title">{{ $t('courseDetails.chapterIntro') }}</view>
|
|
||||||
<view class="intro-wrapper">
|
|
||||||
<!-- 章节封面 -->
|
<!-- 章节封面 -->
|
||||||
<image
|
<image
|
||||||
v-if="chapterDetail?.imgUrl"
|
v-if="chapterDetail?.imgUrl"
|
||||||
@@ -80,82 +51,53 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 章节内容 -->
|
<!-- 章节内容 -->
|
||||||
<view v-if="chapterDetail?.content" class="chapter-content" v-html="chapterDetail.content"></view>
|
<view v-if="chapterDetail?.content" v-html="chapterDetail.content"></view>
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="copyright">
|
<view class="copyright">
|
||||||
<text>{{ $t('courseDetails.copyright') }}</text>
|
<text>{{ $t('courseDetails.copyright') }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</wd-tab>
|
||||||
|
|
||||||
<!-- 思考题 -->
|
<!-- 思考题 -->
|
||||||
<view v-show="currentTab === 1" class="question-content">
|
<wd-tab v-if="chapterDetail?.questions" name="thinkingQuestion" :title="$t('courseDetails.thinkingQuestion')">
|
||||||
<view class="section-title">{{ $t('courseDetails.thinkingQuestion') }}</view>
|
<view v-html="chapterDetail.questions"></view>
|
||||||
<view v-if="chapterDetail?.questions" class="question-wrapper">
|
<!-- <view v-else class="no-question">
|
||||||
<view class="question-html" v-html="chapterDetail.questions"></view>
|
|
||||||
</view>
|
|
||||||
<view v-else class="no-question">
|
|
||||||
<wd-divider>{{ $t('courseDetails.noQuestion') }}</wd-divider>
|
<wd-divider>{{ $t('courseDetails.noQuestion') }}</wd-divider>
|
||||||
</view>
|
</view> -->
|
||||||
</view>
|
</wd-tab>
|
||||||
</view>
|
</wd-tabs>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { onLoad, onShow, onHide } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { courseApi } from '@/api/modules/course'
|
import { courseApi } from '@/api/modules/course'
|
||||||
import VideoPlayer from '@/components/video-player/index.vue'
|
import VideoPlayer from '@/components/video-player/index.vue'
|
||||||
import type { IChapterDetail, IVideo } from '@/types/course'
|
import type { IChapterDetail, IVideo } from '@/types/course'
|
||||||
|
|
||||||
// 页面参数
|
// 页面参数
|
||||||
const chapterId = ref<number>(0)
|
const chapterId = ref<number>(0)
|
||||||
const courseId = ref<number>(0)
|
const courseTitle = ref('')
|
||||||
const navTitle = ref('')
|
|
||||||
const chapterTitle = ref('')
|
const chapterTitle = ref('')
|
||||||
const noRecored = ref(false)
|
|
||||||
|
|
||||||
// 页面数据
|
// 页面数据
|
||||||
const chapterDetail = ref<IChapterDetail | null>(null)
|
const chapterDetail = ref<IChapterDetail | null>(null)
|
||||||
const videoList = ref<IVideo[]>([])
|
const videoList = ref<IVideo[]>([])
|
||||||
const currentVideoIndex = ref(0)
|
const currentVideoIndex = ref(0)
|
||||||
const activeVideoIndex = ref(0)
|
const activeVideoIndex = ref(0)
|
||||||
const currentTab = ref(0)
|
const currentTab = ref('chapterIntro')
|
||||||
const isFullScreen = ref(false)
|
|
||||||
|
|
||||||
// 视频播放器引用
|
// 视频播放器引用
|
||||||
const videoPlayerRef = ref<any>(null)
|
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) => {
|
onLoad((options: any) => {
|
||||||
chapterId.value = parseInt(options.id)
|
chapterId.value = parseInt(options.id)
|
||||||
courseId.value = parseInt(options.courseId)
|
courseTitle.value = options.courseTitle || ''
|
||||||
navTitle.value = options.navTitle || ''
|
|
||||||
chapterTitle.value = options.title || ''
|
chapterTitle.value = options.title || ''
|
||||||
noRecored.value = options.noRecored === 'true'
|
|
||||||
|
|
||||||
loadChapterDetail()
|
loadChapterDetail()
|
||||||
})
|
})
|
||||||
@@ -164,7 +106,6 @@ onLoad((options: any) => {
|
|||||||
* 加载章节详情
|
* 加载章节详情
|
||||||
*/
|
*/
|
||||||
const loadChapterDetail = async () => {
|
const loadChapterDetail = async () => {
|
||||||
try {
|
|
||||||
const res = await courseApi.getChapterDetail(chapterId.value)
|
const res = await courseApi.getChapterDetail(chapterId.value)
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
chapterDetail.value = res.data.detail
|
chapterDetail.value = res.data.detail
|
||||||
@@ -172,16 +113,13 @@ const loadChapterDetail = async () => {
|
|||||||
|
|
||||||
// 如果有历史播放记录,定位到对应视频
|
// 如果有历史播放记录,定位到对应视频
|
||||||
if (res.data.current) {
|
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) {
|
if (index !== -1) {
|
||||||
currentVideoIndex.value = index
|
currentVideoIndex.value = index
|
||||||
activeVideoIndex.value = index
|
activeVideoIndex.value = index
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('加载章节详情失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -192,13 +130,6 @@ const selectVideo = async (index: number) => {
|
|||||||
currentVideoIndex.value = index
|
currentVideoIndex.value = index
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 切换选项卡
|
|
||||||
*/
|
|
||||||
const switchTab = (index: number) => {
|
|
||||||
currentTab.value = index
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 预览图片
|
* 预览图片
|
||||||
*/
|
*/
|
||||||
@@ -216,10 +147,6 @@ const previewImage = (url: string) => {
|
|||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-content {
|
|
||||||
padding-bottom: 100rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-section {
|
.video-section {
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
}
|
}
|
||||||
@@ -258,92 +185,27 @@ const previewImage = (url: string) => {
|
|||||||
color: #2979ff;
|
color: #2979ff;
|
||||||
margin-bottom: 20rpx;
|
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 {
|
.tabs-section {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
margin-top: 20rpx;
|
margin-top: 20rpx;
|
||||||
border-bottom: 2rpx solid #2979ff;
|
|
||||||
|
|
||||||
.tabs {
|
:deep(.wd-tabs__nav) {
|
||||||
display: flex;
|
border-bottom: 1px solid #2979ff;
|
||||||
|
}
|
||||||
.tab-item {
|
:deep(.wd-tab__body) {
|
||||||
flex: 1;
|
padding: 30rpx;
|
||||||
text-align: center;
|
font-size: 28rpx;
|
||||||
padding: 25rpx 0;
|
line-height: 1.8;
|
||||||
font-size: 30rpx;
|
|
||||||
color: #666;
|
color: #666;
|
||||||
position: relative;
|
word-break: break-all;
|
||||||
transition: all 0.3s;
|
}
|
||||||
|
:deep(.wd-tabs__line) {
|
||||||
&.active {
|
|
||||||
color: #2979ff;
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 60rpx;
|
|
||||||
height: 4rpx;
|
|
||||||
background-color: #2979ff;
|
|
||||||
border-radius: 2rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
.chapter-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -351,42 +213,17 @@ const previewImage = (url: string) => {
|
|||||||
border-radius: 8rpx;
|
border-radius: 8rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chapter-content {
|
|
||||||
font-size: 28rpx;
|
|
||||||
line-height: 1.8;
|
|
||||||
color: #666;
|
|
||||||
text-align: justify;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyright {
|
.copyright {
|
||||||
margin-top: 40rpx;
|
margin-top: 20rpx;
|
||||||
padding-top: 20rpx;
|
padding-top: 20rpx;
|
||||||
border-top: 1px solid #f0f0f0;
|
border-top: 1px solid #f0f0f0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
text {
|
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #ff4444;
|
color: #ff4444;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-content {
|
|
||||||
.question-wrapper {
|
|
||||||
.question-html {
|
|
||||||
font-size: 28rpx;
|
|
||||||
line-height: 1.8;
|
|
||||||
color: #666;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-question {
|
.no-question {
|
||||||
padding: 80rpx 0;
|
padding: 80rpx 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -130,6 +130,15 @@
|
|||||||
<text>
|
<text>
|
||||||
本课程一经购买,暂不支持退款,敬请谅解。
|
本课程一经购买,暂不支持退款,敬请谅解。
|
||||||
</text>
|
</text>
|
||||||
|
<view style="color: red; font-weight: bold"> 注: </view>
|
||||||
|
<view>
|
||||||
|
1.手机、pad、电脑均为可登陆电子设备,均有唯一标识码,一个用户名仅允许在一个手机或一个ipad或一个电脑登陆,请根据您的使用习惯自行选择。<br />
|
||||||
|
2.如若申请变更登陆设备请联系客服,<br />
|
||||||
|
客服电话:021-08371305<br />
|
||||||
|
客服微信号:yilujiankangkefu<br />
|
||||||
|
3.如因违反上述使用规定...概不退款,本公司保留追究用户相关法律责任的权利。<br />
|
||||||
|
4.点击“同意”按钮即表示您同意遵守以上条款。
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="protocol-actions">
|
<view class="protocol-actions">
|
||||||
<wd-button type="info" plain @click="showProtocol = false">不同意</wd-button>
|
<wd-button type="info" plain @click="showProtocol = false">不同意</wd-button>
|
||||||
@@ -156,7 +165,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { onLoad, onPageScroll, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
import { onLoad, onPageScroll, onPullDownRefresh, onReachBottom, onShow } from '@dcloudio/uni-app'
|
||||||
import { useCourseStore } from '@/stores/course'
|
import { useCourseStore } from '@/stores/course'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { courseApi } from '@/api/modules/course'
|
import { courseApi } from '@/api/modules/course'
|
||||||
@@ -252,6 +261,12 @@ const vipTip = computed(() => {
|
|||||||
*/
|
*/
|
||||||
onLoad(async (options: any) => {
|
onLoad(async (options: any) => {
|
||||||
courseId.value = parseInt(options.id)
|
courseId.value = parseInt(options.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面显示
|
||||||
|
*/
|
||||||
|
onShow(async () => {
|
||||||
await loadPageData()
|
await loadPageData()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -339,7 +354,7 @@ const handleChapterClick = (chapter: IChapter) => {
|
|||||||
const noRecored = chapter.isAudition === 1 && currentCatalogue.value?.isBuy === 0 && !userVip.value
|
const noRecored = chapter.isAudition === 1 && currentCatalogue.value?.isBuy === 0 && !userVip.value
|
||||||
|
|
||||||
uni.navigateTo({
|
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}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -809,7 +824,7 @@ onReachBottom(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.protocol-content {
|
.protocol-content {
|
||||||
max-height: 500rpx;
|
max-height: 60vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
<view
|
<view
|
||||||
class="fourBox"
|
class="fourBox"
|
||||||
style="padding: 0; padding-bottom: 8rpx"
|
style="padding: 0; padding-bottom: 8rpx"
|
||||||
v-if="sbuMedicalTagsList && sbuMedicalTagsList.length > 0"
|
v-if="sbuMedicalTagsList?.length > 0"
|
||||||
>
|
>
|
||||||
<view
|
<view
|
||||||
class="childrenBox fourIcon flexbox"
|
class="childrenBox fourIcon flexbox"
|
||||||
@@ -257,7 +257,7 @@ const handleFirstLevelClick = (item: string) => {
|
|||||||
* 获取课程分类数据
|
* 获取课程分类数据
|
||||||
*/
|
*/
|
||||||
const getMedicalTags = async () => {
|
const getMedicalTags = async () => {
|
||||||
try {
|
sbuMedicalTagsList.value = []
|
||||||
const res = await courseSubjectClassificationApi.getCourseMedicalTree()
|
const res = await courseSubjectClassificationApi.getCourseMedicalTree()
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
if (res.labels && res.labels.length > 0) {
|
if (res.labels && res.labels.length > 0) {
|
||||||
@@ -269,8 +269,6 @@ const getMedicalTags = async () => {
|
|||||||
// 非终极分类,显示子分类
|
// 非终极分类,显示子分类
|
||||||
if (selectedTag.children && selectedTag.children.length > 0) {
|
if (selectedTag.children && selectedTag.children.length > 0) {
|
||||||
sbuMedicalTagsList.value = selectedTag.children
|
sbuMedicalTagsList.value = selectedTag.children
|
||||||
} else {
|
|
||||||
sbuMedicalTagsList.value = []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,9 +276,6 @@ const getMedicalTags = async () => {
|
|||||||
curseTagList.value = []
|
curseTagList.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取医学课程分类失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 医学
|
* 医学
|
||||||
@@ -309,14 +304,10 @@ const curseClick = (item: IMedicalTag, index: number) => {
|
|||||||
*/
|
*/
|
||||||
const soulCateList = ref<IMedicalTag[]>([])
|
const soulCateList = ref<IMedicalTag[]>([])
|
||||||
const getSoulCateList = async () => {
|
const getSoulCateList = async () => {
|
||||||
try {
|
|
||||||
const res = await courseSubjectClassificationApi.getCourseSoulTree()
|
const res = await courseSubjectClassificationApi.getCourseSoulTree()
|
||||||
if (res.labels&&res.labels.length>0) {
|
if (res.labels&&res.labels.length>0) {
|
||||||
soulCateList.value = res.labels;
|
soulCateList.value = res.labels;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取心理学课程分类失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -325,14 +316,10 @@ const getSoulCateList = async () => {
|
|||||||
*/
|
*/
|
||||||
const sociologyCateList = ref<IMedicalTag[]>([])
|
const sociologyCateList = ref<IMedicalTag[]>([])
|
||||||
const getSociologyCateList = async () => {
|
const getSociologyCateList = async () => {
|
||||||
try {
|
|
||||||
const res = await courseSubjectClassificationApi.getCourseSociologyTree()
|
const res = await courseSubjectClassificationApi.getCourseSociologyTree()
|
||||||
if (res.labels&&res.labels.length>0) {
|
if (res.labels&&res.labels.length>0) {
|
||||||
sociologyCateList.value = res.labels;
|
sociologyCateList.value = res.labels;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取国学课程分类失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -369,7 +356,6 @@ const learnList = ref<ICourse[]>([]) // 观看记录列表
|
|||||||
* 获取观看记录
|
* 获取观看记录
|
||||||
*/
|
*/
|
||||||
const getLearnCourse = async () => {
|
const getLearnCourse = async () => {
|
||||||
try {
|
|
||||||
const res = await courseApi.getUserLateCourseList()
|
const res = await courseApi.getUserLateCourseList()
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
if (res.page && res.page.length > 0) {
|
if (res.page && res.page.length > 0) {
|
||||||
@@ -378,9 +364,6 @@ const getLearnCourse = async () => {
|
|||||||
learnList.value = []
|
learnList.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取观看记录失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新闻播报
|
// 新闻播报
|
||||||
@@ -389,7 +372,6 @@ const newsList = ref<INews[]>([]) // 新闻列表
|
|||||||
* 获取新闻列表
|
* 获取新闻列表
|
||||||
*/
|
*/
|
||||||
const getNewsList = async () => {
|
const getNewsList = async () => {
|
||||||
try {
|
|
||||||
const res = await commonApi.getMessageList(0, 1, 0)
|
const res = await commonApi.getMessageList(0, 1, 0)
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
if (res.messages && res.messages.length > 0) {
|
if (res.messages && res.messages.length > 0) {
|
||||||
@@ -398,9 +380,6 @@ const getNewsList = async () => {
|
|||||||
newsList.value = []
|
newsList.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取新闻列表失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 新闻点击处理
|
* 新闻点击处理
|
||||||
@@ -423,7 +402,6 @@ const tryListenList = ref<ICourse[]>([]) // 试听课程列表
|
|||||||
* 获取试听课程列表
|
* 获取试听课程列表
|
||||||
*/
|
*/
|
||||||
const getTryListenList = async () => {
|
const getTryListenList = async () => {
|
||||||
try {
|
|
||||||
const res = await courseApi.getMarketCourseList({
|
const res = await courseApi.getMarketCourseList({
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 6,
|
limit: 6,
|
||||||
@@ -436,9 +414,6 @@ const getTryListenList = async () => {
|
|||||||
tryListenList.value = []
|
tryListenList.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取试听课程失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ const subCategoryList = ref<ICategory[]>([]) // 子级分类列表
|
|||||||
* 获取分类下的子级分类
|
* 获取分类下的子级分类
|
||||||
*/
|
*/
|
||||||
const getSubCategoryList = async () => {
|
const getSubCategoryList = async () => {
|
||||||
try {
|
|
||||||
let res: any = null
|
let res: any = null
|
||||||
switch (subject.value) {
|
switch (subject.value) {
|
||||||
case '医学':
|
case '医学':
|
||||||
@@ -74,9 +73,6 @@ const getSubCategoryList = async () => {
|
|||||||
radio_category_id.value = 0
|
radio_category_id.value = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取分类下的子级分类失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
<template>
|
|
||||||
<view class="container">
|
|
||||||
<view class="title bg-[transparent] text-center text-[#000]">这是一个等待开发的首页</view>
|
|
||||||
<view class="title bg-[blue] text-center text-[#fff]">这是一个等待开发的首页</view>
|
|
||||||
<view class="description bg-[red]">首页的内容是在线课程</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
font-size: 14px;
|
|
||||||
opacity: 0.6;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page">
|
<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">
|
<view class="input-box">
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
:placeholder="$t('login.codePlaceholder')"
|
:placeholder="$t('login.codePlaceholder')"
|
||||||
/>
|
/>
|
||||||
<wd-button type="info" :class="['code-btn', { active: !readonly }]" @click="getCode">
|
<wd-button type="info" :class="['code-btn', { active: !readonly }]" @click="getCode">
|
||||||
{{ t('login.getCode') }}
|
{{ codeText }}
|
||||||
</wd-button>
|
</wd-button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -54,6 +54,7 @@
|
|||||||
<input
|
<input
|
||||||
class="input-text"
|
class="input-text"
|
||||||
type="password"
|
type="password"
|
||||||
|
minlength="8"
|
||||||
maxlength="20"
|
maxlength="20"
|
||||||
v-model="confirmPassword"
|
v-model="confirmPassword"
|
||||||
:placeholder="$t('forget.passwordAgainPlaceholder')"
|
:placeholder="$t('forget.passwordAgainPlaceholder')"
|
||||||
@@ -69,12 +70,11 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { t } from '@/utils/i18n'
|
||||||
import { commonApi } from '@/api/modules/common'
|
import { commonApi } from '@/api/modules/common'
|
||||||
import { resetPassword } from '@/api/modules/auth'
|
import { resetPassword } from '@/api/modules/auth'
|
||||||
import { validateEmail, checkPasswordStrength } from '@/utils/validator'
|
import { validateEmail, checkPasswordStrength } from '@/utils/validator'
|
||||||
|
import { getNotchHeight } from '@/utils/system'
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const email = ref('')
|
const email = ref('')
|
||||||
@@ -83,7 +83,7 @@ const password = ref('')
|
|||||||
const confirmPassword = ref('')
|
const confirmPassword = ref('')
|
||||||
|
|
||||||
// 验证码相关
|
// 验证码相关
|
||||||
const codeText = ref('Get Code')
|
const codeText = ref(t('login.getCode'))
|
||||||
const readonly = ref(false)
|
const readonly = ref(false)
|
||||||
|
|
||||||
// 密码强度相关
|
// 密码强度相关
|
||||||
@@ -191,16 +191,12 @@ const getCode = async () => {
|
|||||||
if (!isEmailEmpty()) return
|
if (!isEmailEmpty()) return
|
||||||
if (!isEmailVerified(email.value)) return
|
if (!isEmailVerified(email.value)) return
|
||||||
|
|
||||||
try {
|
|
||||||
await commonApi.sendMailCaptcha(email.value)
|
await commonApi.sendMailCaptcha(email.value)
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: t('login.sendCodeSuccess'),
|
title: t('login.sendCodeSuccess'),
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
getCodeState()
|
getCodeState()
|
||||||
} catch (error) {
|
|
||||||
console.error('Send code error:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -264,7 +260,6 @@ const onSubmit = async () => {
|
|||||||
if (!isConfirmPasswordEmpty()) return
|
if (!isConfirmPasswordEmpty()) return
|
||||||
if (!isPasswordMatch()) return
|
if (!isPasswordMatch()) return
|
||||||
|
|
||||||
try {
|
|
||||||
await resetPassword(email.value, code.value, password.value)
|
await resetPassword(email.value, code.value, password.value)
|
||||||
|
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
@@ -275,9 +270,6 @@ const onSubmit = async () => {
|
|||||||
uni.navigateBack()
|
uni.navigateBack()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
|
||||||
console.error('Reset password error:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<view class="login-page">
|
<view class="login-page">
|
||||||
<!-- Logo 背景区域 -->
|
<!-- Logo 背景区域 -->
|
||||||
<view class="logo-bg">
|
<view class="logo-bg">
|
||||||
<text class="welcome-text">Hello! Welcome to<br>Amazing Limited</text>
|
<text class="welcome-text">Hello! Welcome to<br>WU'S INTERNATIONAL</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-1"></image>
|
||||||
<image src="@/static/icon/login_icon.png" mode="aspectFit" class="icon-hua-2"></image>
|
<image src="@/static/icon/login_icon.png" mode="aspectFit" class="icon-hua-2"></image>
|
||||||
</view>
|
</view>
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
maxlength="6"
|
maxlength="6"
|
||||||
@confirm="onSubmit"
|
@confirm="onSubmit"
|
||||||
/>
|
/>
|
||||||
<wd-button type="info" :class="['code-btn', { active: !readonly }]" @click="onSetCode">
|
<wd-button type="info" :class="['code-btn', { 'active': !readonly }]" @click="onSetCode">
|
||||||
{{ t('login.getCode') }}
|
{{ codeText }}
|
||||||
</wd-button>
|
</wd-button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -83,18 +83,18 @@
|
|||||||
|
|
||||||
<!-- 协议同意 -->
|
<!-- 协议同意 -->
|
||||||
<view class="protocol-box">
|
<view class="protocol-box">
|
||||||
<view class="select" :class="{ active: agree }" @click="agreeAgreements"></view>
|
<view class="select" :class="{ 'active': agree }" @click="agreeAgreements"></view>
|
||||||
<view class="protocol-text">
|
<view class="protocol-text">
|
||||||
{{ $t('login.agree') }}
|
{{ $t('login.agree') }}
|
||||||
<text class="highlight" @click="yhxy">《{{ $t('login.userAgreement') }}》</text>
|
<text class="highlight" @click="yhxy">《{{ $t('login.userAgreement') }}》</text>
|
||||||
and
|
{{ $t('global.and') }}
|
||||||
<text class="highlight" @click="yszc">《{{ $t('login.privacyPolicy') }}》</text>
|
<text class="highlight" @click="yszc">《{{ $t('login.privacyPolicy') }}》</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 登录按钮 -->
|
<!-- 登录按钮 -->
|
||||||
<view class="btn-box">
|
<view class="btn-box">
|
||||||
<button @click="onSubmit" class="login-btn" :class="{ active: btnShow }">
|
<button @click="onSubmit" class="login-btn" :class="{ 'active': btnShow }">
|
||||||
{{ $t('login.goLogin') }}
|
{{ $t('login.goLogin') }}
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
@@ -148,8 +148,7 @@ import { commonApi } from '@/api/modules/common'
|
|||||||
import { validateEmail } from '@/utils/validator'
|
import { validateEmail } from '@/utils/validator'
|
||||||
import { onPageJump } from '@/utils'
|
import { onPageJump } from '@/utils'
|
||||||
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { t } from '@/utils/i18n'
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
@@ -165,7 +164,7 @@ const agree = ref(false)
|
|||||||
const isSee = ref(false)
|
const isSee = ref(false)
|
||||||
|
|
||||||
// 验证码相关
|
// 验证码相关
|
||||||
const codeText = ref('Get Code')
|
const codeText = ref(t('login.getCode'))
|
||||||
const readonly = ref(false)
|
const readonly = ref(false)
|
||||||
const btnShow = ref(true)
|
const btnShow = ref(true)
|
||||||
|
|
||||||
@@ -285,13 +284,8 @@ const verifyCodeLogin = async () => {
|
|||||||
|
|
||||||
if (!isCodeEmpty()) return false
|
if (!isCodeEmpty()) return false
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await loginWithCode(email.value, code.value)
|
const res = await loginWithCode(email.value, code.value)
|
||||||
return res
|
return res || null
|
||||||
} catch (error) {
|
|
||||||
console.error('验证码登录失败:', error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 密码登录
|
// 密码登录
|
||||||
const passwordLogin = async () => {
|
const passwordLogin = async () => {
|
||||||
@@ -301,13 +295,8 @@ const passwordLogin = async () => {
|
|||||||
|
|
||||||
if (!isPasswordEmpty()) return false
|
if (!isPasswordEmpty()) return false
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await loginWithPassword(phoneEmail.value, password.value)
|
const res = await loginWithPassword(phoneEmail.value, password.value)
|
||||||
return res
|
return res || null
|
||||||
} catch (error) {
|
|
||||||
console.error('密码登录失败:', error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 提交登录
|
// 提交登录
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
@@ -434,11 +423,11 @@ const getAgreements = async (id: number) => {
|
|||||||
}
|
}
|
||||||
const loadAgreements = async () => {
|
const loadAgreements = async () => {
|
||||||
// 获取用户协议
|
// 获取用户协议
|
||||||
const yhxyRes = await getAgreements(111)
|
const yhxyRes = await getAgreements(116)
|
||||||
yhxyText.value = yhxyRes
|
yhxyText.value = yhxyRes
|
||||||
|
|
||||||
// 获取隐私政策
|
// 获取隐私政策
|
||||||
const yszcRes = await getAgreements(112)
|
const yszcRes = await getAgreements(117)
|
||||||
yszcText.value = yszcRes
|
yszcText.value = yszcRes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<nav-bar :title="$t('order.confirmTitle')" />
|
<nav-bar :title="$t('order.confirmTitle')" />
|
||||||
|
|
||||||
<!-- 确认订单组件 -->
|
<!-- 确认订单组件 -->
|
||||||
<Confirm :goodsList="goodsList" :userInfo="userInfo">
|
<Confirm :goodsList="goodsList" :userInfo="userInfo" :orderType="orderType">
|
||||||
<template #goodsList>
|
<template #goodsList>
|
||||||
<!-- 商品列表内容 -->
|
<!-- 商品列表内容 -->
|
||||||
<view
|
<view
|
||||||
@@ -26,28 +26,8 @@
|
|||||||
<!-- 商品信息 -->
|
<!-- 商品信息 -->
|
||||||
<view class="goods-info">
|
<view class="goods-info">
|
||||||
<text class="goods-name">{{ item.productName }}</text>
|
<text class="goods-name">{{ item.productName }}</text>
|
||||||
|
<!-- 商品价格组件 -->
|
||||||
<!-- 价格信息 -->
|
<GoodsPrice :goods="item" />
|
||||||
<view class="price-info">
|
|
||||||
<!-- VIP优惠价 -->
|
|
||||||
<!-- <view v-if="item.isVipPrice === 1 && item.vipPrice" class="price-row">
|
|
||||||
<text class="vip-price">¥{{ item.vipPrice.toFixed(2) }}</text>
|
|
||||||
<text class="vip-label">{{ $t('order.vipPriceLabel') }}</text>
|
|
||||||
<text class="original-price">¥{{ item.price.toFixed(2) }}</text>
|
|
||||||
</view> -->
|
|
||||||
|
|
||||||
<!-- 活动价 -->
|
|
||||||
<!-- <view v-else-if="item.activityPrice && item.activityPrice > 0" class="price-row">
|
|
||||||
<text class="activity-price">¥{{ item.activityPrice.toFixed(2) }}</text>
|
|
||||||
<text class="activity-label">{{ $t('order.activityLabel') }}</text>
|
|
||||||
<text class="original-price">¥{{ item.price.toFixed(2) }}</text>
|
|
||||||
</view> -->
|
|
||||||
|
|
||||||
<!-- 普通价格 -->
|
|
||||||
<view class="price-row">
|
|
||||||
<text class="normal-price">{{ item.price.toFixed(2) }} 天医币</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 数量 -->
|
<!-- 数量 -->
|
||||||
<!-- <view class="quantity-row">
|
<!-- <view class="quantity-row">
|
||||||
@@ -72,23 +52,18 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { orderApi } from '@/api/modules/order'
|
import { orderApi } from '@/api/modules/order'
|
||||||
import Confirm from '@/components/order/Confirm.vue';
|
|
||||||
import type { IOrderGoods } from '@/types/order'
|
import type { IOrderGoods } from '@/types/order'
|
||||||
|
import Confirm from '@/components/order/Confirm.vue';
|
||||||
|
import GoodsPrice from '@/components/order/GoodsPrice.vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户信息
|
* 获取用户信息
|
||||||
*/
|
*/
|
||||||
const userInfo = ref(null)
|
const userInfo = ref({})
|
||||||
const getUserInfo = async () => {
|
const getUserInfo = async () => {
|
||||||
try {
|
|
||||||
const res = await orderApi.getUserInfo()
|
const res = await orderApi.getUserInfo()
|
||||||
if (res.code === 0) {
|
|
||||||
userInfo.value = res.result || {}
|
userInfo.value = res.result || {}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取用户信息失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取商品列表
|
* 获取商品列表
|
||||||
@@ -96,18 +71,22 @@ const getUserInfo = async () => {
|
|||||||
const goodsIds = ref<string>('')
|
const goodsIds = ref<string>('')
|
||||||
const goodsList = ref<IOrderGoods[]>([])
|
const goodsList = ref<IOrderGoods[]>([])
|
||||||
const getGoodsList = async () => {
|
const getGoodsList = async () => {
|
||||||
try {
|
|
||||||
// 获取商品详情
|
// 获取商品详情
|
||||||
const res = await orderApi.getShopProductListByIds(goodsIds.value)
|
const res = await orderApi.getShopProductListByIds(goodsIds.value)
|
||||||
|
|
||||||
if (res.code === 0 && res.shopProductList?.length > 0) {
|
if (res.shopProductList?.length > 0) {
|
||||||
goodsList.value = res.shopProductList
|
goodsList.value = res.shopProductList
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取商品列表失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 复读
|
||||||
|
const isRelearn = ref<boolean>(false)
|
||||||
|
|
||||||
|
// 订单类型
|
||||||
|
const orderType = computed(() => {
|
||||||
|
return isRelearn.value ? 'relearn' : 'order'
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 页面加载
|
* 页面加载
|
||||||
*/
|
*/
|
||||||
@@ -119,6 +98,7 @@ onLoad(async (options: any) => {
|
|||||||
|
|
||||||
// 根据商品ID获取商品详细信息
|
// 根据商品ID获取商品详细信息
|
||||||
goodsIds.value = options.goods || ''
|
goodsIds.value = options.goods || ''
|
||||||
|
isRelearn.value = options.isRelearn == '1'
|
||||||
getGoodsList()
|
getGoodsList()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('解析商品数据失败:', error)
|
console.error('解析商品数据失败:', error)
|
||||||
@@ -181,43 +161,6 @@ onLoad(async (options: any) => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-info {
|
|
||||||
.price-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 10rpx;
|
|
||||||
|
|
||||||
.vip-price,
|
|
||||||
.activity-price {
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #e97512;
|
|
||||||
}
|
|
||||||
|
|
||||||
.normal-price {
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vip-label {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #fa2d12;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-label {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #613804;
|
|
||||||
}
|
|
||||||
|
|
||||||
.original-price {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #999;
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-row {
|
.quantity-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view> -->
|
</view> -->
|
||||||
<wd-cell-group border class="contact-info">
|
<wd-cell-group border class="contact-info">
|
||||||
<wd-cell :title="$t('user.hotline')" clickable icon="call" value="022-24142321" @click="handlePhoneCall"></wd-cell>
|
<wd-cell :title="$t('user.hotline')" clickable icon="call" value="021-08371305" @click="handlePhoneCall"></wd-cell>
|
||||||
<!-- <wd-cell :title="$t('user.wechat')" value="yilujiankangkefu" clickable @click="handlePhoneCall" /> -->
|
<!-- <wd-cell :title="$t('user.wechat')" value="yilujiankangkefu" clickable @click="handlePhoneCall" /> -->
|
||||||
</wd-cell-group>
|
</wd-cell-group>
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ const getAppVersion = () => {
|
|||||||
* 拨打电话
|
* 拨打电话
|
||||||
*/
|
*/
|
||||||
const handlePhoneCall = () => {
|
const handlePhoneCall = () => {
|
||||||
makePhoneCall('022-24142321', t('user.hotline'))
|
makePhoneCall('021-08371305', t('user.hotline'))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -89,6 +89,8 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { submitFeedback } from '@/api/modules/user'
|
import { submitFeedback } from '@/api/modules/user'
|
||||||
import type { IFeedbackForm } from '@/types/user'
|
import type { IFeedbackForm } from '@/types/user'
|
||||||
@@ -97,6 +99,14 @@ import { useI18n } from 'vue-i18n'
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
onLoad((options: any) => {
|
||||||
|
const orderSn = options.orderSn
|
||||||
|
if (orderSn) {
|
||||||
|
form.value.relation = orderSn
|
||||||
|
form.value.type = '3'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 问题类型选项
|
// 问题类型选项
|
||||||
const issueTypeOptions = computed(() => [
|
const issueTypeOptions = computed(() => [
|
||||||
{ label: t('user.issueTypeAccount'), value: '1' },
|
{ label: t('user.issueTypeAccount'), value: '1' },
|
||||||
|
|||||||
@@ -13,9 +13,6 @@
|
|||||||
<view class="user-details">
|
<view class="user-details">
|
||||||
<text class="nickname">{{ userInfo.nickname || $t('user.notSet') }}</text>
|
<text class="nickname">{{ userInfo.nickname || $t('user.notSet') }}</text>
|
||||||
<text v-if="userInfo.email" class="email">{{ userInfo.email }}</text>
|
<text v-if="userInfo.email" class="email">{{ userInfo.email }}</text>
|
||||||
<text v-if="vipInfo.endTime && platform === 'ios'" class="vip-time">
|
|
||||||
VIP {{ vipInfo.endTime.split(' ')[0] }} {{ $t('user.vipExpireTime') }}
|
|
||||||
</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -26,18 +23,18 @@
|
|||||||
<view class="vip-card-title">{{ $t('user.vip') }}</view>
|
<view class="vip-card-title">{{ $t('user.vip') }}</view>
|
||||||
<view class="vip-card-content">
|
<view class="vip-card-content">
|
||||||
<view class="vip-item-list">
|
<view class="vip-item-list">
|
||||||
<view v-if="vipInfo.length > 0" v-for="vip in vipInfo">{{ vipTypeDict[vip.type] }}(有效期到 {{ parseTime(vip.endTime, '{y}-{m}-{d}') }})</view>
|
<view v-if="vipInfo?.length > 0" v-for="vip in vipInfo">{{ vipTypeDict[vip.type] }}({{ parseTime(vip.endTime, '{y}-{m}-{d}') }} 截止)</view>
|
||||||
<view v-else>办理课程VIP,畅享更多权益</view>
|
<view v-else>办理课程VIP,畅享更多权益</view>
|
||||||
</view>
|
</view>
|
||||||
<wd-button v-if="vipInfo.length > 0" plain type="primary" size="small" @click="goSubscribe">{{ $t('vip.renewal') }}</wd-button>
|
<wd-button v-if="vipInfo?.length > 0" plain type="primary" size="small" @click="goCourseVipSub">{{ $t('vip.renewal') }}</wd-button>
|
||||||
<wd-button v-else plain type="primary" size="small" @click="goSubscribe">{{ $t('vip.openVip') }}</wd-button>
|
<wd-button v-else plain type="primary" size="small" @click="goCourseVipSub">{{ $t('vip.openVip') }}</wd-button>
|
||||||
</view>
|
</view>
|
||||||
<view class="vip-card-content">
|
<view class="vip-card-content">
|
||||||
<view class="vip-item-list">
|
<view class="vip-item-list">
|
||||||
<view v-if="vipInfoEbook.length > 0" v-for="vip in vipInfoEbook">电子书VIP{{ vipTypeDict[vip.type] }}(有效期到 {{ parseTime(vip.endTime, '{y}-{m}-{d}') }})</view>
|
<view v-if="vipInfoEbook?.length > 0" v-for="vip in vipInfoEbook">电子书VIP{{ vipTypeDict[vip.type] }}({{ parseTime(vip.endTime, '{y}-{m}-{d}') }} 截止)</view>
|
||||||
<view v-else>办理电子书VIP,畅享更多权益</view>
|
<view v-else>办理电子书VIP,畅享更多权益</view>
|
||||||
</view>
|
</view>
|
||||||
<wd-button v-if="!vipInfoEbook.length" plain type="primary" size="small" @click="goSubscribe">{{ $t('vip.openVip') }}</wd-button>
|
<wd-button v-if="!vipInfoEbook?.length" plain type="primary" size="small" @click="goSubscribe">{{ $t('vip.openVip') }}</wd-button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -50,14 +47,14 @@
|
|||||||
<view class="assets_row">{{ t('global.coin') }}</view>
|
<view class="assets_row">{{ t('global.coin') }}</view>
|
||||||
<view>{{userInfo.peanutCoin ?? 1}}</view>
|
<view>{{userInfo.peanutCoin ?? 1}}</view>
|
||||||
</view>
|
</view>
|
||||||
<view>
|
<view @click="goPointsList">
|
||||||
<view class="assets_row">积分</view>
|
<view class="assets_row">积分</view>
|
||||||
<view>{{userInfo.jf ?? 1}}</view>
|
<view>{{userInfo.jf ?? 1}}</view>
|
||||||
</view>
|
</view>
|
||||||
<view>
|
<!-- <view>
|
||||||
<view class="assets_row">优惠卷</view>
|
<view class="assets_row">优惠卷</view>
|
||||||
<view>0</view>
|
<view>0</view>
|
||||||
</view>
|
</view> -->
|
||||||
</view>
|
</view>
|
||||||
<view class="chong_btn" @click="goRecharge">充 值</view>
|
<view class="chong_btn" @click="goRecharge">充 值</view>
|
||||||
<!-- <text class="wallet_title">{{$t('my.coin')}}<uni-icons type="help" size="19" color="#666"></uni-icons></text>
|
<!-- <text class="wallet_title">{{$t('my.coin')}}<uni-icons type="help" size="19" color="#666"></uni-icons></text>
|
||||||
@@ -86,6 +83,7 @@
|
|||||||
import { getNotchHeight } from '@/utils/system'
|
import { getNotchHeight } from '@/utils/system'
|
||||||
import { parseTime } from '@/utils/index'
|
import { parseTime } from '@/utils/index'
|
||||||
import { t } from '@/utils/i18n'
|
import { t } from '@/utils/i18n'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const sysStore = useSysStore()
|
const sysStore = useSysStore()
|
||||||
@@ -178,7 +176,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 跳转到订阅页面
|
* 跳转到电子书vip订阅页面
|
||||||
*/
|
*/
|
||||||
const goSubscribe = () => {
|
const goSubscribe = () => {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
@@ -186,6 +184,15 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到课程vip订阅页面
|
||||||
|
*/
|
||||||
|
const goCourseVipSub = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/vip/course'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理菜单点击
|
* 处理菜单点击
|
||||||
*/
|
*/
|
||||||
@@ -222,11 +229,21 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转积分列表
|
||||||
|
*/
|
||||||
|
const goPointsList = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/user/points/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
getData()
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getPlatform()
|
getPlatform()
|
||||||
getData()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -323,6 +340,7 @@
|
|||||||
padding: 30rpx;
|
padding: 30rpx;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vip-item-list {
|
.vip-item-list {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -404,7 +422,7 @@
|
|||||||
border-radius: 50rpx;
|
border-radius: 50rpx;
|
||||||
color: #fffbf6;
|
color: #fffbf6;
|
||||||
padding: 10rpx 32rpx;
|
padding: 10rpx 32rpx;
|
||||||
background-image: linear-gradient(90deg, #3ab3ae 0%, #d5ecdd 200%);
|
background: #007bff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assets {
|
.assets {
|
||||||
|
|||||||
153
pages/user/order/details.vue
Normal file
153
pages/user/order/details.vue
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-wrapper">
|
||||||
|
<!-- 自定义导航栏 -->
|
||||||
|
<nav-bar :title="$t('order.orderDetails')"></nav-bar>
|
||||||
|
|
||||||
|
<!-- 订单信息 -->
|
||||||
|
<view class="order-info">
|
||||||
|
<view class="order-status">{{ sysStore.orderStatusMap[order.orderStatus] }}</view>
|
||||||
|
<!-- 三种订单类型商品信息 -->
|
||||||
|
<ProductInfo v-if="order.orderType === 'order'" size="large" :data="productList[0]" :type="order.orderType" />
|
||||||
|
<ProductInfo v-if="order.orderType === 'vip'" size="large" :data="order.vipBuyConfigEntity" :type="order.orderType" />
|
||||||
|
<ProductInfo v-if="order.orderType === 'abroadVip'" size="large" :data="order.ebookvipBuyConfig" :type="order.orderType" />
|
||||||
|
<!-- 三种订单类型商品信息 end -->
|
||||||
|
|
||||||
|
<wd-divider class="p-0!" />
|
||||||
|
|
||||||
|
<!-- 付款信息 -->
|
||||||
|
<wd-cell-group>
|
||||||
|
<wd-cell title="商品总价" :value="`${order.orderMoney} ${$t('global.coin')}`" />
|
||||||
|
<wd-cell v-if="order.districtMoney > 0" title="活动优惠" :value="`- ${order.districtMoney} ${$t('global.coin')}`" />
|
||||||
|
<wd-cell v-if="order.vipDiscountAmount > 0" title="VIP专享立减" :value="`- ${order.vipDiscountAmount} ${$t('global.coin')}`" />
|
||||||
|
<wd-cell v-if="order.jfDeduction > 0" title="积分抵扣">
|
||||||
|
<text class="text-red-500 text-lg font-bold">{{ `- ${order.jfDeduction}` }}</text>
|
||||||
|
</wd-cell>
|
||||||
|
<wd-cell title="实付金额">
|
||||||
|
<text class="text-red-500 text-lg font-bold">{{ `${order.realMoney}` }}</text> {{ $t('global.coin') }}
|
||||||
|
</wd-cell>
|
||||||
|
</wd-cell-group>
|
||||||
|
|
||||||
|
<wd-divider class="p-0!" />
|
||||||
|
|
||||||
|
<!-- 下单信息 -->
|
||||||
|
<wd-cell-group>
|
||||||
|
<wd-cell title="订单编号" title-width="4em">
|
||||||
|
<text class="text-xs">{{ order.orderSn }}</text>
|
||||||
|
<wd-icon name="file-copy" size="14px" color="#65A1FA" class="ml-1!" @click="copyToClipboard(order.orderSn)"></wd-icon>
|
||||||
|
</wd-cell>
|
||||||
|
<wd-cell title="创建时间" :value="order.createTime" />
|
||||||
|
<wd-cell title="付款时间" :value="order.paymentDate" />
|
||||||
|
</wd-cell-group>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="text-center">
|
||||||
|
<text @click="toWorkOrder" class="text-[cadetblue] text-sm">订单有问题?去申诉</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="contact-customer" @click="makePhoneCall(sysStore.customerServicePhone)">
|
||||||
|
<wd-icon name="service" size="30px"></wd-icon>
|
||||||
|
<view class="text-sm">联系客服</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { useSysStore } from '@/stores/sys'
|
||||||
|
import { orderApi } from '@/api/modules/order'
|
||||||
|
import type { IOrderDetail, IOrderGoods } from '@/types/order'
|
||||||
|
import ProductInfo from '@/components/order/ProductInfo.vue'
|
||||||
|
import { copyToClipboard, makePhoneCall } from '@/utils/index'
|
||||||
|
|
||||||
|
const sysStore = useSysStore()
|
||||||
|
// 订单详情
|
||||||
|
const order = ref<IOrderDetail>({
|
||||||
|
orderMoney: 0,
|
||||||
|
districtMoney: 0,
|
||||||
|
vipDiscountAmount: 0,
|
||||||
|
jfDeduction: 0,
|
||||||
|
realMoney: 0,
|
||||||
|
})
|
||||||
|
const productList = ref<IOrderGoods[]>([])
|
||||||
|
|
||||||
|
onLoad(async (options: { orderId: string }) => {
|
||||||
|
const orderId = options.orderId
|
||||||
|
if (orderId) {
|
||||||
|
const res = await orderApi.getOrderDetail(orderId)
|
||||||
|
const orderDetails = res.data.buyOrder
|
||||||
|
order.value = orderDetails
|
||||||
|
switch (orderDetails.orderType) {
|
||||||
|
case 'order':
|
||||||
|
productList.value = res.data.productInfo
|
||||||
|
break
|
||||||
|
case 'vip':
|
||||||
|
productList.value = orderDetails.vipBuyConfigEntity
|
||||||
|
break
|
||||||
|
case 'abroadVip':
|
||||||
|
productList.value = orderDetails
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const toWorkOrder = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/user/feedback/index?orderSn=' + order.value.orderSn
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
body {
|
||||||
|
background-color: #F8F9FA;
|
||||||
|
}
|
||||||
|
.page-wrapper {
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-info {
|
||||||
|
padding: 30rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.order-status {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
padding: 10rpx 0;
|
||||||
|
width: 6em;
|
||||||
|
background-color: #34D19D;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 0 10px 10px 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:deep(.wd-cell-group) {
|
||||||
|
padding: 0 !important;
|
||||||
|
|
||||||
|
.wd-cell__wrapper {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-customer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
margin: 40rpx auto 0;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,14 +3,14 @@
|
|||||||
<template #top>
|
<template #top>
|
||||||
<!-- 自定义导航栏 -->
|
<!-- 自定义导航栏 -->
|
||||||
<nav-bar :title="$t('user.myOrders')"></nav-bar>
|
<nav-bar :title="$t('user.myOrders')"></nav-bar>
|
||||||
<wd-tabs v-model="orderStatus" @change="handleOrderStatusTabChange">
|
<!-- <wd-tabs v-model="orderStatus" @change="handleOrderStatusTabChange">
|
||||||
<wd-tab v-for="item in ordersTabs" :key="item.value" :title="item.name" :name="item.value"></wd-tab>
|
<wd-tab v-for="item in ordersTabs" :key="item.value" :title="item.name" :name="item.value"></wd-tab>
|
||||||
</wd-tabs>
|
</wd-tabs> -->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 订单列表 -->
|
<!-- 订单列表 -->
|
||||||
<view class="order-list">
|
<view class="order-list">
|
||||||
<wd-card v-for="order in orderList" :key="order.id" type="rectangle" custom-class="order-item">
|
<wd-card v-for="order in orderList" :key="order.id" type="rectangle" custom-class="order-item" @click="toDetails(order)">
|
||||||
<template #title>
|
<template #title>
|
||||||
<view class="order-item-title">
|
<view class="order-item-title">
|
||||||
<view class="order-item-sn">
|
<view class="order-item-sn">
|
||||||
@@ -23,11 +23,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 三种订单类型商品信息 -->
|
<!-- 三种订单类型商品信息 -->
|
||||||
<ProductInfo v-if="order.orderType === 'order'" :data="order.productList" :type="order.orderType" />
|
<ProductInfo v-if="order.orderType === 'order'" :data="order.productList[0].product" :type="order.orderType" />
|
||||||
<ProductInfo v-if="order.orderType === 'abroadBook'" :data="order.bookEntity" :type="order.orderType" />
|
|
||||||
<ProductInfo v-if="order.orderType === 'vip'" :data="order.vipBuyConfigEntity" :type="order.orderType" />
|
<ProductInfo v-if="order.orderType === 'vip'" :data="order.vipBuyConfigEntity" :type="order.orderType" />
|
||||||
|
<ProductInfo v-if="order.orderType === 'abroadVip'" :data="order.ebookvipBuyConfig" :type="order.orderType" />
|
||||||
<!-- 三种订单类型商品信息 end -->
|
<!-- 三种订单类型商品信息 end -->
|
||||||
<view class="order-item-total-price">实付款:{{ order.orderMoney }} 天医币</view>
|
<view class="order-item-total-price">实付款:{{ order.orderMoney }} {{ t('global.coin') }}</view>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<view>
|
<view>
|
||||||
@@ -47,12 +47,16 @@ import type { IOrder } from '@/types/order'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { copyToClipboard } from '@/utils/index'
|
import { copyToClipboard } from '@/utils/index'
|
||||||
import ProductInfo from '@/components/order/ProductInfo.vue'
|
import ProductInfo from '@/components/order/ProductInfo.vue'
|
||||||
|
import { useSysStore } from '@/stores/sys'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const paging = ref<any>(null)
|
const paging = ref<any>(null)
|
||||||
|
|
||||||
|
// 订单状态映射
|
||||||
|
const orderStatusMap = useSysStore().orderStatusMap
|
||||||
|
|
||||||
// 订单状态
|
// 订单状态
|
||||||
const orderStatus = ref<string>('-1')
|
const orderStatus = ref<string>('3')
|
||||||
const ordersTabs = [
|
const ordersTabs = [
|
||||||
{
|
{
|
||||||
name: "全部",
|
name: "全部",
|
||||||
@@ -71,12 +75,6 @@ const ordersTabs = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// 订单状态映射
|
|
||||||
const orderStatusMap = {
|
|
||||||
'0': '待付款',
|
|
||||||
'3': '已完成',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理订单状态切换
|
* 处理订单状态切换
|
||||||
*/
|
*/
|
||||||
@@ -121,22 +119,17 @@ const getOrderImage = (order: IOrder) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOrderTitle = (order: IOrder) => {
|
/**
|
||||||
switch (order.orderType) {
|
* 跳转订单详情
|
||||||
case 'order':
|
*/
|
||||||
return order.productList[0]?.product?.productName || ''
|
const toDetails = (order: IOrder) => {
|
||||||
case 'abroadBook':
|
uni.navigateTo({
|
||||||
return order.bookEntity?.name || ''
|
url: '/pages/user/order/details?orderId=' + order.orderId
|
||||||
case 'vip':
|
})
|
||||||
return order.vipBuyConfigEntity?.title || ''
|
|
||||||
case 'point':
|
|
||||||
return ''
|
|
||||||
default:
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
135
pages/user/points/index.vue
Normal file
135
pages/user/points/index.vue
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<z-paging ref="paging" v-model="bookList" auto-show-back-to-top class="my-book-page" @query="pointsList" :default-page-size="10">
|
||||||
|
<template #top>
|
||||||
|
<!-- 自定义导航栏 -->
|
||||||
|
<nav-bar :title="$t('user.consumptionRecord')"></nav-bar>
|
||||||
|
</template>
|
||||||
|
<view class="recharge-record" v-if="(bookList && bookList.length > 0)">
|
||||||
|
<view class="go-gecharge" @click="goRecharge">
|
||||||
|
<view>{{$t('order.recharge')}}</view>
|
||||||
|
<view><wd-icon name="arrow-right" size="16px" color="#fff"/></view>
|
||||||
|
</view>
|
||||||
|
<view class="title">{{$t('order.rechargeConsumptionList')}}</view>
|
||||||
|
<view class="recharge-record-block" v-for="(item, index) in bookList" :key="index">
|
||||||
|
<view class="recharge-record-block-row">{{item.orderType}}<text class="text">{{item.changeAmount}}</text></view>
|
||||||
|
<view class="time">{{item.createTime}}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</z-paging>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { getPointsData } from '@/api/modules/user'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const paging = ref<any>()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 数据状态
|
||||||
|
const bookList = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const firstLoad = ref(true)
|
||||||
|
|
||||||
|
// 充值记录列表
|
||||||
|
async function pointsList(pageNo : number, pageSize : number) {
|
||||||
|
const userId = userStore.userInfo.id
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getPointsData(pageNo, pageSize, userId)
|
||||||
|
console.log(res, 'res');
|
||||||
|
paging.value.complete(res.transactionDetailsList.records)
|
||||||
|
} catch (error) {
|
||||||
|
paging.value.complete(false)
|
||||||
|
console.error('Failed to load book list:', error)
|
||||||
|
} finally {
|
||||||
|
firstLoad.value = false
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转充值页面
|
||||||
|
*/
|
||||||
|
const goRecharge = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/user/recharge/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.my-book-page {
|
||||||
|
background: #f7faf9;
|
||||||
|
min-height: 100vh;
|
||||||
|
|
||||||
|
.recharge-record {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||||
|
// padding: 20rpx;
|
||||||
|
margin: 20rpx;
|
||||||
|
|
||||||
|
.go-gecharge{
|
||||||
|
//height: 100rpx;
|
||||||
|
background: linear-gradient(to right, #007bff, #17a2b8);
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
padding: 20rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
display: flex;justify-content:space-between;align-items:center
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
padding-left:20rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
color: #007bff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-record-block {
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
padding: 20rpx;
|
||||||
|
|
||||||
|
.time{
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-record-block-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
|
.text{
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 200rpx;
|
||||||
|
|
||||||
|
image {
|
||||||
|
width: 400rpx;
|
||||||
|
height: 300rpx;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 50rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -199,15 +199,11 @@ const avatarUrl = ref('')
|
|||||||
*/
|
*/
|
||||||
const userInfo = ref<any>({}) // 用户信息
|
const userInfo = ref<any>({}) // 用户信息
|
||||||
const getData = async () => {
|
const getData = async () => {
|
||||||
try {
|
|
||||||
const res = await getUserInfo()
|
const res = await getUserInfo()
|
||||||
if (res.result) {
|
if (res.result) {
|
||||||
userStore.setUserInfo(res.result)
|
userStore.setUserInfo(res.result)
|
||||||
userInfo.value = res.result
|
userInfo.value = res.result
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('获取用户信息失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -324,16 +320,12 @@ const sendCode = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
await sendEmailCode(editForm.value.email)
|
await sendEmailCode(editForm.value.email)
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: t('user.sendCodeSuccess'),
|
title: t('user.sendCodeSuccess'),
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
startCountdown()
|
startCountdown()
|
||||||
} catch (error) {
|
|
||||||
console.error('发送验证码失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -398,7 +390,6 @@ const checkPasswordStrength = () => {
|
|||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const key = currentField.value?.key
|
const key = currentField.value?.key
|
||||||
|
|
||||||
try {
|
|
||||||
// 构建更新数据对象
|
// 构建更新数据对象
|
||||||
let updateData: any = Object.assign({}, userInfo.value)
|
let updateData: any = Object.assign({}, userInfo.value)
|
||||||
|
|
||||||
@@ -481,9 +472,6 @@ const handleSubmit = async () => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
getData()
|
getData()
|
||||||
}, 500)
|
}, 500)
|
||||||
} catch (error) {
|
|
||||||
console.error('更新失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
<view class="recharge_block" @click="chosPric(item)"
|
<view class="recharge_block" @click="chosPric(item)"
|
||||||
:class="aloneItem.priceTypeId === item.priceTypeId ? 'selected' : ''"
|
:class="aloneItem.priceTypeId === item.priceTypeId ? 'selected' : ''"
|
||||||
v-for="item in rechargeList.bookBuyConfigList" :key="item.priceTypeId">
|
v-for="item in rechargeList.bookBuyConfigList" :key="item.priceTypeId">
|
||||||
<view class="recharge_money">¥{{item.money}}</view>
|
<view class="recharge_money">¥{{item.realMoney}}</view>
|
||||||
<view>{{item.realMoney}}{{$t('order.virtualCoin')}}</view>
|
<view>{{item.money}}{{ t('global.coin') }}</view>
|
||||||
<!-- 红框位置的618活动标签 -->
|
<!-- 红框位置的618活动标签 -->
|
||||||
<!-- <view class="activity-tag">618活动</view> -->
|
<!-- <view class="activity-tag">618活动</view> -->
|
||||||
<span class="activity-label" v-if="item.givejf >0">618充值活动</span>
|
<span class="activity-label" v-if="item.givejf >0">618充值活动</span>
|
||||||
@@ -23,25 +23,26 @@
|
|||||||
<view class="cha_fangsh">
|
<view class="cha_fangsh">
|
||||||
<view class="cf_title PM_font">{{$t('user.paymentMethod')}}</view>
|
<view class="cf_title PM_font">{{$t('user.paymentMethod')}}</view>
|
||||||
<view class="cf_radio">
|
<view class="cf_radio">
|
||||||
<radio-group v-for="item in iosPaylist" @click="choseType(item.id)">
|
<radio-group v-for="item in iosPaylist">
|
||||||
<view style="width: 100%">
|
<view style="width: 100%">
|
||||||
<view :class="payType == item.id ? 'Tab_xf cf_xuanx' : 'cf_xuanx'">
|
<view :class="payType == item.id ? 'Tab_xf cf_xuanx' : 'cf_xuanx'">
|
||||||
<!-- <image class="pay_item_img" :src="item.imgUrl" mode="aspectFil">
|
<!-- <image class="pay_item_img" :src="item.imgUrl" mode="aspectFil">
|
||||||
</image> -->
|
</image> -->
|
||||||
<text>{{ item.title }}</text>
|
<text>{{ item.title }}</text>
|
||||||
<radio :checked="payType === item.id"></radio>
|
<radio :checked="payType === item.id" @click="choseType(item.id)"></radio>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</radio-group>
|
</radio-group>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="agree_wo flexbox">
|
<view class="agree_wo flexbox">
|
||||||
<radio-group class="agree" v-for="(item, index) in argee" :key="index" @click="radioCheck">
|
<radio-group class="agree" v-for="(item, index) in argee" :key="index">
|
||||||
<view>
|
<view>
|
||||||
<radio class="agreeRadio" :value="item.id" :checked="state" color="#007bff"></radio>
|
<radio class="agreeRadio" :value="item.id" :checked="state" color="#007bff" @click="radioCheck"></radio>
|
||||||
</view>
|
</view>
|
||||||
</radio-group>
|
</radio-group>
|
||||||
<view>{{$t('order.readAgree')}}<span class="highlight" @click="showAgreement">《{{$t('order.valueAddedServices')}}》</span></view>
|
<view>{{$t('order.readAgree')}}<span class="highlight"
|
||||||
|
@click="showAgreement">《{{$t('order.valueAddedServices')}}》</span></view>
|
||||||
</view>
|
</view>
|
||||||
<view class="bottom-button-container">
|
<view class="bottom-button-container">
|
||||||
<button class="recharge-button" @click="handleRecharge">{{$t('order.recharge')}}</button>
|
<button class="recharge-button" @click="handleRecharge">{{$t('order.recharge')}}</button>
|
||||||
@@ -63,8 +64,12 @@
|
|||||||
import { ref, computed, onMounted, toRefs, reactive } from 'vue'
|
import { ref, computed, onMounted, toRefs, reactive } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useMessage } from '@/uni_modules/wot-design-uni'
|
import { useMessage } from '@/uni_modules/wot-design-uni'
|
||||||
import { getBookBuyConfigList, getAgreement, getActivityDescription } from '@/api/modules/user'
|
import { getBookBuyConfigList, getAgreement, getActivityDescription, verifyGooglePay, getPlaceOrder } from '@/api/modules/user'
|
||||||
// const googlePay = uni.requireNativePlugin("sn-googlepay5");
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { useThrottle } from '@/hooks/useThrottle';
|
||||||
|
|
||||||
|
const googlePay = uni.requireNativePlugin("sn-googlepay5");
|
||||||
|
const userStore = useUserStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const payType = ref('1')
|
const payType = ref('1')
|
||||||
@@ -107,13 +112,15 @@
|
|||||||
|
|
||||||
const remark = ref({})
|
const remark = ref({})
|
||||||
const isConnected = ref(false)
|
const isConnected = ref(false)
|
||||||
|
const purchaseToken = ref()
|
||||||
|
// 订单编号
|
||||||
|
const orderSn = ref('')
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取使用环境
|
* 获取使用环境
|
||||||
*/
|
*/
|
||||||
const getDevName = () => {
|
const getDevName = () => {
|
||||||
|
|
||||||
if (uni.getSystemInfoSync().platform === "android") {
|
if (uni.getSystemInfoSync().platform === "android") {
|
||||||
qudao.value = 'Google'
|
qudao.value = 'Google'
|
||||||
isAndroid.value = true;
|
isAndroid.value = true;
|
||||||
@@ -125,6 +132,63 @@
|
|||||||
getData()
|
getData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取充值列表数据
|
||||||
|
*/
|
||||||
|
const getData = async () => {
|
||||||
|
try {
|
||||||
|
rechargeList.value = await getBookBuyConfigList(type.value, qudao.value)
|
||||||
|
console.log(rechargeList.value.bookBuyConfigList, '充值列表');
|
||||||
|
// 默认选择第一个金额
|
||||||
|
aloneItem.value = rechargeList.value.bookBuyConfigList[0]
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取订单列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点击支付按钮
|
||||||
|
*/
|
||||||
|
|
||||||
|
const paymentButton = async () => {
|
||||||
|
if (!state.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t('order.readAgreeServices'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getPlaceOrderObj()
|
||||||
|
}
|
||||||
|
// 节流支付按钮
|
||||||
|
const handleRecharge = useThrottle(paymentButton);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单编号
|
||||||
|
*/
|
||||||
|
const getPlaceOrderObj = async () => {
|
||||||
|
const { priceTypeId, realMoney, money } = toRefs(aloneItem.value)
|
||||||
|
const data = {
|
||||||
|
userId: userStore.userInfo.id, // 用户di
|
||||||
|
paymentMethod: '5', //支付方式4point 5google
|
||||||
|
orderMoney: money.value, //订单金额
|
||||||
|
realMoney: realMoney.value, //实际金额
|
||||||
|
come: '10', //订单来源 2医学吴门医述 10海外读书
|
||||||
|
orderType: 'point', //订单类型, point充值、order课程、书、vip 课vip、abroadVip 书vip、relearn 复读、trainingClass 培训班
|
||||||
|
productId: priceTypeId.value // 商品id
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// uni.hideLoading()
|
||||||
|
const res = await getPlaceOrder(data)
|
||||||
|
orderSn.value = res.orderSn
|
||||||
|
console.log(orderSn.value, '获取订单号');
|
||||||
|
uni.showLoading({ title: t('order.orderCreating') })
|
||||||
|
getGooglePay()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取订单号失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 点击金额
|
// 点击金额
|
||||||
const chosPric = (item : any) => {
|
const chosPric = (item : any) => {
|
||||||
console.log(item, '金额每项');
|
console.log(item, '金额每项');
|
||||||
@@ -145,17 +209,115 @@
|
|||||||
const showAgreement = () => {
|
const showAgreement = () => {
|
||||||
agreemenState.value = true
|
agreemenState.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取充值列表数据
|
* 初始化
|
||||||
*/
|
*/
|
||||||
const getData = async () => {
|
const getGooglePay = () => {
|
||||||
|
googlePay.init({
|
||||||
|
}, (e : any) => {
|
||||||
|
console.log('init', e);
|
||||||
|
if (e.code == 0) {
|
||||||
|
isConnected.value = true;
|
||||||
|
console.log('init成功了');
|
||||||
|
getQuerySku()
|
||||||
|
// 初始化成功
|
||||||
|
} else {
|
||||||
|
console.log('init失败了');
|
||||||
|
// 初始化失败
|
||||||
|
isConnected.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询sku
|
||||||
|
*/
|
||||||
|
const getQuerySku = () => {
|
||||||
|
const id = aloneItem.value.priceTypeId
|
||||||
|
console.log(id, '获取每项');
|
||||||
|
googlePay.querySku(
|
||||||
|
{
|
||||||
|
inapp: [id], // 与subs二选一, 参数为商品ID(字符串)数组
|
||||||
|
},
|
||||||
|
(e : any) => {
|
||||||
|
if (e.code == 0) {
|
||||||
|
// 查询成功.
|
||||||
|
console.log('querySku查询成功', e);
|
||||||
|
getPayAll()
|
||||||
|
uni.hideLoading()
|
||||||
|
} else {
|
||||||
|
console.log('查询失败', e);
|
||||||
|
uni.showToast({
|
||||||
|
title: t('global.networkConnectionError'),
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起支付
|
||||||
|
*/
|
||||||
|
const getPayAll = () => {
|
||||||
|
console.log(aloneItem.value.priceTypeId, orderSn.value, '发起支付传入产品id,订单id');
|
||||||
|
googlePay.payAll(
|
||||||
|
{
|
||||||
|
productId: aloneItem.value.priceTypeId, // 产品id
|
||||||
|
accountId: orderSn.value // 订单编号
|
||||||
|
},
|
||||||
|
(e : any) => {
|
||||||
|
if (e.code == 0) {
|
||||||
|
purchaseToken.value = e.data[0].original.purchaseToken
|
||||||
|
// 支付成功
|
||||||
|
console.log(e, 'payAll方法成功返参');
|
||||||
|
getConsume()
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: t('user.paymentFailed'), icon: 'error' })
|
||||||
|
console.log(e, 'e');
|
||||||
|
// 支付失败
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消耗品 确认交易
|
||||||
|
*/
|
||||||
|
const getConsume = () => {
|
||||||
|
googlePay.consume(
|
||||||
|
{
|
||||||
|
purchaseToken: purchaseToken.value, // 来自支付结果的original.purchaseToken (或 original.token)
|
||||||
|
},
|
||||||
|
(e : any) => {
|
||||||
|
if (e.code == 0) {
|
||||||
|
console.log(e, '确认交易成功');
|
||||||
|
uni.showToast({ title: t('user.returnMine'), icon: 'none' })
|
||||||
|
// 确认成功
|
||||||
|
googleVerify()
|
||||||
|
} else {
|
||||||
|
console.log(e, '确认交易失败');
|
||||||
|
// 确认失败
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验订单
|
||||||
|
*/
|
||||||
|
const googleVerify = async () => {
|
||||||
|
uni.hideLoading()
|
||||||
|
console.log(typeof aloneItem.value.priceTypeId, typeof purchaseToken.value, typeof orderSn.value);
|
||||||
try {
|
try {
|
||||||
rechargeList.value = await getBookBuyConfigList(type.value, qudao.value)
|
const obj = await verifyGooglePay(aloneItem.value.priceTypeId, purchaseToken.value, orderSn.value)
|
||||||
console.log(rechargeList.value.bookBuyConfigList, '充值列表');
|
uni.switchTab({
|
||||||
// 默认选择第一个金额
|
url: '/pages/user/index'
|
||||||
aloneItem.value = rechargeList.value.bookBuyConfigList[0]
|
})
|
||||||
|
console.log(obj, '校验订单');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取订单列表失败:', error)
|
console.error('校验订单失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,78 +357,6 @@
|
|||||||
// payType.value = val;
|
// payType.value = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRecharge = () => {
|
|
||||||
if(!state.value){
|
|
||||||
uni.showToast({
|
|
||||||
title: t('order.readAgreeServices'),
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
uni.showLoading({ title: '加载中...' })
|
|
||||||
console.log('立即充值');
|
|
||||||
getGooglePay()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化
|
|
||||||
*/
|
|
||||||
const getGooglePay = () => {
|
|
||||||
googlePay.init({
|
|
||||||
}, (e:any) => {
|
|
||||||
console.log('init', e);
|
|
||||||
if (e.code == 0) {
|
|
||||||
isConnected.value = true;
|
|
||||||
getQuerySku()
|
|
||||||
// 初始化成功
|
|
||||||
} else {
|
|
||||||
// 初始化失败
|
|
||||||
isConnected.value = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询sku
|
|
||||||
*/
|
|
||||||
const getQuerySku = () =>{
|
|
||||||
const id = aloneItem.value.priceTypeId
|
|
||||||
console.log(id, '获取每项');
|
|
||||||
googlePay.querySku(
|
|
||||||
{
|
|
||||||
inapp: [id], // 与subs二选一, 参数为商品ID(字符串)数组
|
|
||||||
},
|
|
||||||
(e:any) => {
|
|
||||||
if (e.code == 0) {
|
|
||||||
// 查询成功.
|
|
||||||
console.log('查询成功',e);
|
|
||||||
// e.list; // 查询结果, array
|
|
||||||
} else {
|
|
||||||
console.log('查询失败');
|
|
||||||
// 查询失败
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPayAll = () =>{
|
|
||||||
googlePay.payAll(
|
|
||||||
{
|
|
||||||
productId: "", // 产品id
|
|
||||||
|
|
||||||
},
|
|
||||||
(e) => {
|
|
||||||
if (e.code == 0) {
|
|
||||||
// 支付成功
|
|
||||||
e.data; //支付结果, array [ {original:{ }, signature: ''} ]
|
|
||||||
} else {
|
|
||||||
// 支付失败
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getDevName();
|
getDevName();
|
||||||
getActivityDescriptionData()
|
getActivityDescriptionData()
|
||||||
|
|||||||
@@ -102,22 +102,22 @@ const getCurrentLanguageName = () => {
|
|||||||
|
|
||||||
// 设置项列表
|
// 设置项列表
|
||||||
const settingItems = computed(() => [
|
const settingItems = computed(() => [
|
||||||
{
|
// {
|
||||||
id: 0,
|
// id: 0,
|
||||||
label: t('user.language'),
|
// label: t('user.language'),
|
||||||
value: getCurrentLanguageName(),
|
// value: getCurrentLanguageName(),
|
||||||
type: 'language'
|
// type: 'language'
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
label: t('user.hotline'),
|
label: t('user.hotline'),
|
||||||
value: '022-24142321',
|
value: '021-08371305',
|
||||||
type: 'tel'
|
type: 'tel'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
label: t('user.customerEmail'),
|
label: t('user.customerEmail'),
|
||||||
value: 'appyilujiankang@sina.com',
|
value: 'AmazingLimited@163.com',
|
||||||
type: 'email'
|
type: 'email'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -73,15 +73,6 @@ const getVipList = async () => {
|
|||||||
vipList.value = res.lableList || []
|
vipList.value = res.lableList || []
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选择套餐
|
|
||||||
const selectPackage = (vip: any) => {
|
|
||||||
// 这里可以添加跳转到订单确认页面的逻辑
|
|
||||||
uni.showToast({
|
|
||||||
title: `已选择: ${vip.title}`,
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理购买
|
// 处理购买
|
||||||
const handlePurchase = (vip: any) => {
|
const handlePurchase = (vip: any) => {
|
||||||
const selectedGoods = {
|
const selectedGoods = {
|
||||||
|
|||||||
BIN
static/logo.png
BIN
static/logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 208 KiB |
@@ -15,6 +15,11 @@ export const useSysStore = defineStore('sys', {
|
|||||||
7: '国学VIP',
|
7: '国学VIP',
|
||||||
8: '心理学VIP',
|
8: '心理学VIP',
|
||||||
9: '中西汇通学VIP',
|
9: '中西汇通学VIP',
|
||||||
|
},
|
||||||
|
customerServicePhone: '021-08371305',
|
||||||
|
orderStatusMap: {
|
||||||
|
'0': '待付款',
|
||||||
|
'3': '已完成',
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,14 @@
|
|||||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||||
"Courier New", monospace;
|
"Courier New", monospace;
|
||||||
--color-red-500: oklch(63.7% 0.237 25.331);
|
--color-red-500: oklch(63.7% 0.237 25.331);
|
||||||
|
--color-white: #fff;
|
||||||
--spacing: 0.25rem;
|
--spacing: 0.25rem;
|
||||||
|
--text-xs: 0.75rem;
|
||||||
|
--text-xs--line-height: calc(1 / 0.75);
|
||||||
|
--text-sm: 0.875rem;
|
||||||
|
--text-sm--line-height: calc(1.25 / 0.875);
|
||||||
|
--text-lg: 1.125rem;
|
||||||
|
--text-lg--line-height: calc(1.75 / 1.125);
|
||||||
--font-weight-bold: 700;
|
--font-weight-bold: 700;
|
||||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
--default-transition-duration: 150ms;
|
--default-transition-duration: 150ms;
|
||||||
@@ -244,6 +251,24 @@
|
|||||||
.table {
|
.table {
|
||||||
display: table;
|
display: table;
|
||||||
}
|
}
|
||||||
|
.h-20 {
|
||||||
|
height: calc(var(--spacing) * 20);
|
||||||
|
}
|
||||||
|
.h-\[80px\] {
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
.h-\[80rpx\] {
|
||||||
|
height: 80rpx;
|
||||||
|
}
|
||||||
|
.w-20 {
|
||||||
|
width: calc(var(--spacing) * 20);
|
||||||
|
}
|
||||||
|
.w-\[80px\] {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
.w-\[80rpx\] {
|
||||||
|
width: 80rpx;
|
||||||
|
}
|
||||||
.w-\[100px\] {
|
.w-\[100px\] {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
@@ -265,6 +290,15 @@
|
|||||||
.flex-wrap {
|
.flex-wrap {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.rounded-full {
|
||||||
|
border-radius: calc(infinity * 1px);
|
||||||
|
}
|
||||||
.border {
|
.border {
|
||||||
border-style: var(--tw-border-style);
|
border-style: var(--tw-border-style);
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
@@ -278,6 +312,15 @@
|
|||||||
.bg-\[transparent\] {
|
.bg-\[transparent\] {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
.bg-white {
|
||||||
|
background-color: var(--color-white);
|
||||||
|
}
|
||||||
|
.p-0 {
|
||||||
|
padding: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
|
.p-0\! {
|
||||||
|
padding: calc(var(--spacing) * 0) !important;
|
||||||
|
}
|
||||||
.pt-1 {
|
.pt-1 {
|
||||||
padding-top: calc(var(--spacing) * 1);
|
padding-top: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
@@ -299,6 +342,22 @@
|
|||||||
.text-right {
|
.text-right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
.text-lg {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
line-height: var(--tw-leading, var(--text-lg--line-height));
|
||||||
|
}
|
||||||
|
.text-sm {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||||
|
}
|
||||||
|
.text-xs {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
line-height: var(--tw-leading, var(--text-xs--line-height));
|
||||||
|
}
|
||||||
|
.font-\[26rpx\] {
|
||||||
|
--tw-font-weight: 26rpx;
|
||||||
|
font-weight: 26rpx;
|
||||||
|
}
|
||||||
.font-bold {
|
.font-bold {
|
||||||
--tw-font-weight: var(--font-weight-bold);
|
--tw-font-weight: var(--font-weight-bold);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
@@ -312,9 +371,24 @@
|
|||||||
.text-\[\#fff\] {
|
.text-\[\#fff\] {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
.text-\[\'10px\'\] {
|
||||||
|
color: '10px';
|
||||||
|
}
|
||||||
|
.text-\[\'12px\'\] {
|
||||||
|
color: '12px';
|
||||||
|
}
|
||||||
|
.text-\[26rpx\] {
|
||||||
|
color: 26rpx;
|
||||||
|
}
|
||||||
|
.text-\[cadetblue\] {
|
||||||
|
color: cadetblue;
|
||||||
|
}
|
||||||
.text-\[red\] {
|
.text-\[red\] {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
.text-red-500 {
|
||||||
|
color: var(--color-red-500);
|
||||||
|
}
|
||||||
.lowercase {
|
.lowercase {
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
}
|
}
|
||||||
@@ -328,6 +402,14 @@
|
|||||||
.underline {
|
.underline {
|
||||||
text-decoration-line: underline;
|
text-decoration-line: underline;
|
||||||
}
|
}
|
||||||
|
.shadow-\[0_0_10px_rgba\(0\,0\,0\,0\.1\)\] {
|
||||||
|
--tw-shadow: 0 0 10px var(--tw-shadow-color, rgba(0,0,0,0.1));
|
||||||
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
|
}
|
||||||
|
.shadow-\[0_0_10px_rgba\(0\,0\,0\,0\.05\)\] {
|
||||||
|
--tw-shadow: 0 0 10px var(--tw-shadow-color, rgba(0,0,0,0.05));
|
||||||
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
|
}
|
||||||
.ring {
|
.ring {
|
||||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
|
|||||||
@@ -114,9 +114,14 @@ uni-textarea {
|
|||||||
.wd-overlay {
|
.wd-overlay {
|
||||||
z-index: 9998 !important;
|
z-index: 9998 !important;
|
||||||
}
|
}
|
||||||
.wd-popup-wrapper .wd-popup {
|
.wd-popup-wrapper {
|
||||||
|
.wd-popup {
|
||||||
|
border-radius: 15px !important;
|
||||||
|
}
|
||||||
|
.wd-popup--bottom {
|
||||||
border-radius: 15px 15px 0 0 !important;
|
border-radius: 15px 15px 0 0 !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// uni-ui form
|
// uni-ui form
|
||||||
// .uni-forms-item {
|
// .uni-forms-item {
|
||||||
|
|||||||
22
types/order.d.ts
vendored
22
types/order.d.ts
vendored
@@ -11,6 +11,7 @@ export interface IGoods {
|
|||||||
isVipPrice?: number // 是否有VIP优惠 0-否 1-是
|
isVipPrice?: number // 是否有VIP优惠 0-否 1-是
|
||||||
productAmount?: number // 购买数量
|
productAmount?: number // 购买数量
|
||||||
delFlag?: number // 删除标记 -1-已下架
|
delFlag?: number // 删除标记 -1-已下架
|
||||||
|
goodsType?: string // 商品类型 "05": 课程 "02": 电子书
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,3 +139,24 @@ export interface IPaymentOption {
|
|||||||
value: '4' | '5'
|
value: '4' | '5'
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单详情
|
||||||
|
*/
|
||||||
|
export interface IOrderDetail {
|
||||||
|
id: number
|
||||||
|
orderSn: string
|
||||||
|
orderMoney: number
|
||||||
|
realMoney: number
|
||||||
|
paymentDate: string
|
||||||
|
createTime: string
|
||||||
|
orderType: string
|
||||||
|
districtMoney?: number
|
||||||
|
vipDiscountAmount?: number
|
||||||
|
couponId?: number
|
||||||
|
couponName?: string
|
||||||
|
couponAmount?: number
|
||||||
|
jfDeduction?: number
|
||||||
|
remark?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ export default function() {
|
|||||||
// #ifdef APP-PLUS
|
// #ifdef APP-PLUS
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) {
|
plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) {
|
||||||
console.log('哈哈哈哈', widgetInfo)
|
|
||||||
let data = {
|
let data = {
|
||||||
action: 'checkVersion',
|
action: 'checkVersion',
|
||||||
appid: plus.runtime.appid,
|
appid: plus.runtime.appid,
|
||||||
@@ -13,7 +12,6 @@ export default function() {
|
|||||||
name: 'uni-upgrade-center',
|
name: 'uni-upgrade-center',
|
||||||
data,
|
data,
|
||||||
success: (e) => {
|
success: (e) => {
|
||||||
console.log("e: ", e);
|
|
||||||
resolve(e)
|
resolve(e)
|
||||||
},
|
},
|
||||||
fail: (error) => {
|
fail: (error) => {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export default function() {
|
|||||||
// #ifdef APP-PLUS
|
// #ifdef APP-PLUS
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
callCheckVersion().then(async (e) => {
|
callCheckVersion().then(async (e) => {
|
||||||
console.log('hhhhhhhhhhhh', e)
|
|
||||||
if (!e.result) return;
|
if (!e.result) return;
|
||||||
const {
|
const {
|
||||||
code,
|
code,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const onPageBack = () => {
|
|||||||
* @param {string} phoneNumber - 要拨打的电话号码
|
* @param {string} phoneNumber - 要拨打的电话号码
|
||||||
* @param {string} title - 拨打电话提示的标题,默认值为空字符串
|
* @param {string} title - 拨打电话提示的标题,默认值为空字符串
|
||||||
*/
|
*/
|
||||||
export const makePhoneCall = (phoneNumber: string, title: string = '') => {
|
export const makePhoneCall = (phoneNumber: string, title: string = t('global.call')) => {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: title,
|
title: title,
|
||||||
content: phoneNumber,
|
content: phoneNumber,
|
||||||
|
|||||||
Reference in New Issue
Block a user