Compare commits

6 Commits

41 changed files with 1011 additions and 2194 deletions

View File

@@ -7,8 +7,8 @@ export const ENV = process.env.NODE_ENV || 'development';
*/ */
const BASE_URL_MAP = { const BASE_URL_MAP = {
development: { development: {
MAIN: 'http://192.168.110.100:9300/pb/', // 张川川 // MAIN: 'http://192.168.110.100:9300/pb/', // 张川川
// MAIN: 'https://global.nuttyreading.com/', // 线上 MAIN: 'https://global.nuttyreading.com/', // 线上
// PAYMENT: 'https://dev-pay.example.com', // 暂时用不到 // PAYMENT: 'https://dev-pay.example.com', // 暂时用不到
// CDN: 'https://cdn-dev.example.com', // 暂时用不到 // CDN: 'https://cdn-dev.example.com', // 暂时用不到
}, },

View File

@@ -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 });
} }

View File

@@ -194,11 +194,11 @@ 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>({ const res = await mainClient.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,13 +256,45 @@ export async function getActivityDescription() {
} }
/** /**
* 获取充值列表 * 充值记录列表
*/ * @param current 当前页码
export async function getTransactionDetailsList(current: number, limit: number, userId: string,) { * @param limit 每页数量
const res = await mainClient.request<IApiResponse>({ * @param userId 用户id
url: 'common/transactionDetails/getTransactionDetailsList', * @return
method: 'POST', */
data: { current, limit, userId, } export async function getTransactionDetailsList(current : number, limit : number, userId : string) {
}) const res = await mainClient.request<IApiResponse>({
return res url: 'common/transactionDetails/getTransactionDetailsList',
method: 'POST',
data: { current, limit, userId, }
})
return res
}
/**
* 获取订单编号
* @return
*/
export async function getPlaceOrder(data: object) {
const res = await mainClient.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
} }

View File

@@ -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
@@ -23,14 +24,18 @@ export function createRequestClient(cfg: ICreateClientConfig) {
// 全局处理请求 loading // 全局处理请求 loading
const loading = !cfg.loading ? true : cfg.loading // 接口请求参数不传loading默认显示loading const loading = !cfg.loading ? true : cfg.loading // 接口请求参数不传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) {
// 委托给响应拦截器处理 // 委托给响应拦截器处理

View 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>

View File

@@ -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; display: inline-block;
left: 0;
top: 0;
font-size: 24rpx; font-size: 24rpx;
display: inline-block;
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;

View File

@@ -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>

View File

@@ -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 () => {
}) })
// 返回上一页 // 返回上一页
uni.navigateBack({ setTimeout(() => {
delta: props.backStep uni.navigateBack({
}) delta: props.backStep
})
}, 500)
} }
/** /**

View 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>

View File

@@ -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>

View File

@@ -33,6 +33,8 @@ const productImg = computed(() => {
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:
@@ -47,6 +49,8 @@ const title = computed(() => {
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:
@@ -61,6 +65,8 @@ const price = computed(() => {
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:

View File

@@ -18,7 +18,8 @@
"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"
}, },
"tabar.course": "COURSE", "tabar.course": "COURSE",
"tabar.book": "EBOOK", "tabar.book": "EBOOK",
@@ -79,7 +80,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": {
@@ -297,7 +298,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 +394,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 +445,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",

View File

@@ -18,7 +18,8 @@
"loginExpired": "登录失效,请重新登录。", "loginExpired": "登录失效,请重新登录。",
"requestException": "请求异常", "requestException": "请求异常",
"coin": "天医币", "coin": "天医币",
"days": "天" "days": "天",
"and": "和"
}, },
"tabar.course": "课程", "tabar.course": "课程",
"tabar.book": "图书", "tabar.book": "图书",
@@ -80,7 +81,7 @@
"getCode": "获取验证码", "getCode": "获取验证码",
"passwordStrengthStrong": "密码强度强", "passwordStrengthStrong": "密码强度强",
"passwordStrengthMedium": "密码强度中等", "passwordStrengthMedium": "密码强度中等",
"passwordStrengthWeak": "请使用至少包含大小写字母、数字、符号中的两种类型长度为8个字符的密码", "passwordStrengthWeak": "请使用至少包含大小写字母、数字、符号中的两种类型长度为8-20个字符的密码",
"passwordChanged": "密码修改成功" "passwordChanged": "密码修改成功"
}, },
"common": { "common": {
@@ -297,7 +298,9 @@
"listen": { "listen": {
"title": "听书", "title": "听书",
"speed": "播放速度", "speed": "播放速度",
"chapterList": "章节列表" "chapterList": "章节列表",
"isLast": "已到最后一章",
"isFirst": "已到第一章"
}, },
"workOrder": { "workOrder": {
"submit_success": "提交成功" "submit_success": "提交成功"
@@ -391,7 +394,9 @@
"courseInfo": "课程", "courseInfo": "课程",
"chapterInfo": "章节", "chapterInfo": "章节",
"videoLoadFailed": "视频加载失败", "videoLoadFailed": "视频加载失败",
"vipBenefit": "VIP畅学权益生效中" "vipBenefit": "VIP畅学权益生效中",
"audio": "音频",
"video": "视频"
}, },
"courseOrder": { "courseOrder": {
"orderTitle": "确认订单", "orderTitle": "确认订单",
@@ -440,7 +445,7 @@
"notUseCoupon": "不使用优惠券", "notUseCoupon": "不使用优惠券",
"reselect": "重新选择", "reselect": "重新选择",
"selected": "选好了", "selected": "选好了",
"maxPoints": "可用积分({max}分)", "maxPoints": "可用积分(max分)",
"pointsPlaceholder": "请输入积分", "pointsPlaceholder": "请输入积分",
"allPoints": "全部积分", "allPoints": "全部积分",
"insufficientBalance": "天医币余额不足", "insufficientBalance": "天医币余额不足",
@@ -455,7 +460,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优惠",

View File

@@ -1,7 +1,7 @@
{ {
"name" : "太湖国际", "name" : "吴门国际",
"appid" : "__UNI__1250B39", "appid" : "__UNI__1250B39",
"description" : "太湖国际", "description" : "吴门国际",
"versionName" : "1.0.4", "versionName" : "1.0.4",
"versionCode" : 104, "versionCode" : 104,
"transformPx" : false, "transformPx" : false,

View File

@@ -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": {

View File

@@ -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,70 +205,43 @@ function initScrollHeight() {
// 加载书籍详情 // 加载书籍详情
async function loadBookInfo() { async function loadBookInfo() {
try { const res = await bookApi.getBookInfo(bookId.value)
const res = await bookApi.getBookInfo(bookId.value) bookInfo.value = res.bookInfo
if (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) goodsList.value = res.productList || []
if (res.code === 0) {
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')
}
} catch (error) {
nullText.value = t('common.data_null') 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')
}
} catch (error) {
nullBookText.value = t('common.data_null') nullBookText.value = t('common.data_null')
console.error('Failed to load recommend books:', error)
} }
} }

View File

@@ -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,13 +214,9 @@ 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)
} }
} }
@@ -233,21 +224,17 @@ const getVipInfo = async () => {
* 获取我的书单 * 获取我的书单
*/ */
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 if (res.page.records && res.page.records.length > 0) {
if (res.page.records && res.page.records.length > 0) { myBooksList.value = res.page.records
myBooksList.value = res.page.records
}
} else {
// 未登录,跳转到登录页
uni.navigateTo({
url: '/pages/login/login'
})
} }
} catch (error) { } else {
console.error('获取我的书单失败:', error) // 未登录,跳转到登录页
uni.navigateTo({
url: '/pages/login/login'
})
} }
} }
@@ -255,13 +242,9 @@ const getMyBooks = async () => {
* 获取推荐图书 * 获取推荐图书
*/ */
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)
} }
} }
@@ -269,16 +252,12 @@ const getRecommendBooks = async () => {
* 获取活动标签列表 * 获取活动标签列表
*/ */
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) { activityLabelList.value = res.lableList
activityLabelList.value = res.lableList // 默认加载第一个标签的图书列表
// 默认加载第一个标签的图书列表 await getBooksByLabel(res.lableList[0].id, 'activity')
await getBooksByLabel(res.lableList[0].id, 'activity')
}
} catch (error) {
console.error('获取活动标签失败:', error)
} }
} }
@@ -286,16 +265,12 @@ const getActivityLabels = async () => {
* 获取分类标签列表 * 获取分类标签列表
*/ */
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) { categoryLevel1List.value = res.lableList
categoryLevel1List.value = res.lableList // 默认加载第一个标签的二级标签
// 默认加载第一个标签的二级标签 await getSubLabels(res.lableList[0].id, 0)
await getSubLabels(res.lableList[0].id, 0)
}
} catch (error) {
console.error('获取分类标签失败:', error)
} }
} }
@@ -303,21 +278,17 @@ const getCategoryLabels = async () => {
* 获取二级标签列表 * 获取二级标签列表
*/ */
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) { categoryLevel2List.value = res.lableList
categoryLevel2List.value = res.lableList currentLevel2Index.value = 0
currentLevel2Index.value = 0 // 加载第一个二级标签的图书列表
// 加载第一个二级标签的图书列表 await getBooksByLabel(res.lableList[0].id, 'category')
await getBooksByLabel(res.lableList[0].id, 'category') } else {
} else { // 没有二级标签,直接加载一级标签的图书列表
// 没有二级标签,直接加载一级标签的图书列表 categoryLevel2List.value = []
categoryLevel2List.value = [] await getBooksByLabel(pid, 'category')
await getBooksByLabel(pid, 'category')
}
} catch (error) {
console.error('获取二级标签失败:', error)
} }
} }
@@ -328,63 +299,22 @@ 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) { activityList.value = res.bookList
activityList.value = res.bookList
} else {
activityList.value = []
}
} else { } else {
if (res.bookList && res.bookList.length > 0) { activityList.value = []
categoryBookList.value = res.bookList }
} else { } else {
categoryBookList.value = [] if (res.bookList && res.bookList.length > 0) {
} categoryBookList.value = res.bookList
} else {
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;
} }
} }
} }

View File

@@ -117,31 +117,20 @@ function initScrollHeight() {
// 加载书籍信息 // 加载书籍信息
async function loadBookInfo() { async function loadBookInfo() {
try { const res = await bookApi.getBookInfo(bookId.value)
const res = await bookApi.getBookInfo(bookId.value) bookInfo.value = res.bookInfo
if (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 })
})
if (res.chapterList && res.chapterList.length > 0) {
if (res.chapterList && res.chapterList.length > 0) { chapterList.value = res.chapterList
chapterList.value = res.chapterList } else {
} else {
nullText.value = t('common.data_null')
}
} catch (error) {
nullText.value = t('common.data_null') nullText.value = t('common.data_null')
console.error('Failed to load chapter list:', error)
} }
} }

View File

@@ -245,14 +245,8 @@ function initAudioContext() {
// 加载书籍信息 // 加载书籍信息
async function loadBookInfo() { async function loadBookInfo() {
try { const res = await bookApi.getBookInfo(bookId.value)
const res = await bookApi.getBookInfo(bookId.value) bookInfo.value = res.bookInfo
if (res.bookInfo) {
bookInfo.value = res.bookInfo
}
} catch (error) {
console.error('Failed to load book info:', error)
}
} }
// 加载章节列表 // 加载章节列表
@@ -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'
}) })
} }

View File

@@ -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>

View File

@@ -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 () => {

View File

@@ -164,13 +164,9 @@ 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,20 +176,15 @@ 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
if (res.commentsTree && res.commentsTree.length > 0) { if (res.commentsTree && res.commentsTree.length > 0) {
commentList.value = [...commentList.value, ...res.commentsTree] commentList.value = [...commentList.value, ...res.commentsTree]
page.value.current += 1 page.value.current += 1
} else if (commentList.value.length === 0) { } else if (commentList.value.length === 0) {
nullText.value = t('common.data_null')
}
} catch (error) {
nullText.value = t('common.data_null') nullText.value = t('common.data_null')
console.error('Failed to load comments:', error)
} }
} }
@@ -275,33 +266,29 @@ 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>') {
uni.showToast({
title: t('bookDetails.enterText'),
icon: 'none'
})
return
}
const pid = replyTarget.value?.id || 0
await bookApi.insertComment(bookId.value, content, pid)
if (!content || content === '<p><br></p>') {
uni.showToast({ uni.showToast({
title: t('workOrder.submit_success'), title: t('bookDetails.enterText'),
icon: 'success', icon: 'none'
duration: 500
}) })
return
setTimeout(() => {
editorCtx.value?.clear()
resetComments()
}, 500)
} catch (error) {
console.error('Failed to submit comment:', error)
} }
const pid = replyTarget.value?.id || 0
await bookApi.insertComment(bookId.value, content, pid)
uni.showToast({
title: t('workOrder.submit_success'),
icon: 'success',
duration: 500
})
setTimeout(() => {
editorCtx.value?.clear()
resetComments()
}, 500)
} }
// 点赞/取消点赞 // 点赞/取消点赞
@@ -314,29 +301,25 @@ 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({ title: t('bookDetails.supportSuccess'),
title: t('bookDetails.supportSuccess'), icon: 'success',
icon: 'success', duration: 1000
duration: 1000 })
}) } else {
} else { await bookApi.unlikeComment(comment.id)
await bookApi.unlikeComment(comment.id) uni.showToast({
uni.showToast({ title: t('bookDetails.supportCancel'),
title: t('bookDetails.supportCancel'), icon: 'success',
icon: 'success', duration: 1000
duration: 1000 })
})
}
setTimeout(() => {
resetComments()
}, 200)
} catch (error) {
console.error('Failed to like comment:', error)
} }
setTimeout(() => {
resetComments()
}, 200)
} }
// 删除评论 // 删除评论
@@ -348,20 +331,16 @@ 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'), icon: 'success',
icon: 'success', duration: 500
duration: 500 })
})
setTimeout(() => {
setTimeout(() => { resetComments()
resetComments() }, 500)
}, 500)
} catch (error) {
console.error('Failed to delete comment:', error)
}
} }
} }
}) })

View File

@@ -70,14 +70,8 @@ onLoad((options: any) => {
* 获取VIP信息 * 获取VIP信息
*/ */
const getVipInfo = async () => { const getVipInfo = async () => {
try { const res = await homeApi.getVipInfo()
const res = await homeApi.getVipInfo() vipInfo.value = res.vipInfo
if (res.vipInfo) {
vipInfo.value = res.vipInfo
}
} catch (error) {
console.error('获取VIP信息失败:', error)
}
} }
/** /**
@@ -91,26 +85,19 @@ 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, limit: 10,
limit: 10, })
}) if (res.bookList && res.bookList.length > 0) {
if (res.bookList && res.bookList.length > 0) { searchResults.value = res.bookList
searchResults.value = res.bookList isEmpty.value = false
isEmpty.value = false } else {
} else {
searchResults.value = []
isEmpty.value = true
}
} catch (error) {
console.error('搜索失败:', error)
searchResults.value = [] searchResults.value = []
isEmpty.value = true isEmpty.value = true
} finally {
loading.value = false
} }
loading.value = false
} }
/** /**

View File

@@ -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,126 +30,74 @@
<!-- 视频列表 --> <!-- 视频列表 -->
<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
<!-- 章节封面 --> v-if="chapterDetail?.imgUrl"
<image :src="chapterDetail.imgUrl"
v-if="chapterDetail?.imgUrl" mode="widthFix"
:src="chapterDetail.imgUrl" class="chapter-image"
mode="widthFix" @click="previewImage(chapterDetail.imgUrl)"
class="chapter-image" />
@click="previewImage(chapterDetail.imgUrl)"
/> <!-- 章节内容 -->
<view v-if="chapterDetail?.content" v-html="chapterDetail.content"></view>
<!-- 章节内容 -->
<view v-if="chapterDetail?.content" class="chapter-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,23 +106,19 @@ 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 videoList.value = res.data.videos || []
videoList.value = res.data.videos || []
// 如果有历史播放记录,定位到对应视频 // 如果有历史播放记录,定位到对应视频
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,135 +185,45 @@ 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;
:deep(.wd-tabs__nav) {
.tabs { border-bottom: 1px solid #2979ff;
display: flex; }
:deep(.wd-tab__body) {
.tab-item { padding: 30rpx;
flex: 1; font-size: 28rpx;
text-align: center; line-height: 1.8;
padding: 25rpx 0; color: #666;
font-size: 30rpx; word-break: break-all;
color: #666; }
position: relative; :deep(.wd-tabs__line) {
transition: all 0.3s; bottom: 0;
&.active {
color: #2979ff;
font-weight: 500;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background-color: #2979ff;
border-radius: 2rpx;
}
}
}
} }
} }
.tab-content { .chapter-image {
background-color: #fff; width: 100%;
padding: 20rpx; display: block;
margin-bottom: 20rpx;
.section-title { border-radius: 8rpx;
font-size: 32rpx; }
font-weight: 500;
color: #333; .copyright {
margin-bottom: 20rpx; margin-top: 20rpx;
} padding-top: 20rpx;
border-top: 1px solid #f0f0f0;
.intro-content { text-align: center;
.intro-wrapper { font-size: 24rpx;
.chapter-image { color: #ff4444;
width: 100%; }
display: block;
margin-bottom: 20rpx; .no-question {
border-radius: 8rpx; padding: 80rpx 0;
} text-align: center;
.chapter-content {
font-size: 28rpx;
line-height: 1.8;
color: #666;
text-align: justify;
word-break: break-all;
}
}
.copyright {
margin-top: 40rpx;
padding-top: 20rpx;
border-top: 1px solid #f0f0f0;
text-align: center;
text {
font-size: 24rpx;
color: #ff4444;
}
}
}
.question-content {
.question-wrapper {
.question-html {
font-size: 28rpx;
line-height: 1.8;
color: #666;
word-break: break-all;
}
}
.no-question {
padding: 80rpx 0;
text-align: center;
}
}
} }
</style> </style>

View File

@@ -130,6 +130,15 @@
<text> <text>
本课程一经购买暂不支持退款敬请谅解 本课程一经购买暂不支持退款敬请谅解
</text> </text>
<view style="color: red; font-weight: bold"> : </view>
<view>
1.手机pad电脑均为可登陆电子设备均有唯一标识码一个用户名仅允许在一个手机或一个ipad或一个电脑登陆请根据您的使用习惯自行选择<br />
2.如若申请变更登陆设备请联系客服<br />
客服电话:13110039505;022-24142321<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;

View File

@@ -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,29 +257,24 @@ 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) {
curseTagList.value = res.labels curseTagList.value = res.labels
// 根据 currentIndex 设置初始选中的分类 // 根据 currentIndex 设置初始选中的分类
if (res.labels[currentIndex.value]) { if (res.labels[currentIndex.value]) {
const selectedTag = res.labels[currentIndex.value] const selectedTag = res.labels[currentIndex.value]
if (selectedTag.isLast === 0) { if (selectedTag.isLast === 0) {
// 非终极分类,显示子分类 // 非终极分类,显示子分类
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 = []
}
} }
} }
} else {
curseTagList.value = []
} }
} else {
curseTagList.value = []
} }
} catch (error) {
console.error('获取医学课程分类失败:', error)
} }
} }
/** /**
@@ -309,13 +304,9 @@ 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,13 +316,9 @@ 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,17 +356,13 @@ 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) { learnList.value = res.page
learnList.value = res.page } else {
} else { learnList.value = []
learnList.value = []
}
} }
} catch (error) {
console.error('获取观看记录失败:', error)
} }
} }
@@ -389,17 +372,13 @@ 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) { newsList.value = res.messages
newsList.value = res.messages } else {
} else { newsList.value = []
newsList.value = []
}
} }
} catch (error) {
console.error('获取新闻列表失败:', error)
} }
} }
/** /**
@@ -423,21 +402,17 @@ 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, id: 1
id: 1 })
}) if (res && res.code === 0) {
if (res && res.code === 0) { if (res.courseList && res.courseList.records && res.courseList.records.length > 0) {
if (res.courseList && res.courseList.records && res.courseList.records.length > 0) { tryListenList.value = res.courseList.records
tryListenList.value = res.courseList.records } else {
} else { tryListenList.value = []
tryListenList.value = []
}
} }
} catch (error) {
console.error('获取试听课程失败:', error)
} }
} }

View File

@@ -52,30 +52,26 @@ 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 '医学': res = await courseSubjectClassificationApi.getCourseMedicalChildLabels(categoryId.value)
res = await courseSubjectClassificationApi.getCourseMedicalChildLabels(categoryId.value) break
break case '心理学':
case '心理学': res = await courseSubjectClassificationApi.getCourseSoulChildLabels(categoryId.value)
res = await courseSubjectClassificationApi.getCourseSoulChildLabels(categoryId.value) break
break }
if (res && res.code === 0) {
if (res.labels && res.labels.length > 0) {
subCategoryList.value = res.labels
// 默认选中第一个tab
tab_category_id.value = res.labels[0].id
radio_category_id.value = res.labels[0].children[0] && res.labels[0].children[0].id || 0
} else {
subCategoryList.value = []
tab_category_id.value = 0
radio_category_id.value = 0
} }
if (res && res.code === 0) {
if (res.labels && res.labels.length > 0) {
subCategoryList.value = res.labels
// 默认选中第一个tab
tab_category_id.value = res.labels[0].id
radio_category_id.value = res.labels[0].children[0] && res.labels[0].children[0].id || 0
} else {
subCategoryList.value = []
tab_category_id.value = 0
radio_category_id.value = 0
}
}
} catch (error) {
console.error('获取分类下的子级分类失败:', error)
} }
} }

View File

@@ -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>

View File

@@ -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">
@@ -53,7 +53,8 @@
<text class="input-tit">{{ $t('forget.passwordAgain') }}</text> <text class="input-tit">{{ $t('forget.passwordAgain') }}</text>
<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')"
@@ -73,6 +74,7 @@ import { useI18n } from 'vue-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 { t } = useI18n()
@@ -191,16 +193,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,20 +262,16 @@ 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({
title: t('global.tips'), title: t('global.tips'),
content: t('forget.passwordChanged'), content: t('forget.passwordChanged'),
showCancel: false, showCancel: false,
success: () => { success: () => {
uni.navigateBack() uni.navigateBack()
} }
}) })
} catch (error) {
console.error('Reset password error:', error)
}
} }
</script> </script>

View File

@@ -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>太湖国际</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>
@@ -87,7 +87,7 @@
<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>
@@ -285,13 +285,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 || null
return res
} catch (error) {
console.error('验证码登录失败:', error)
return null
}
} }
// 密码登录 // 密码登录
const passwordLogin = async () => { const passwordLogin = async () => {
@@ -301,13 +296,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 || null
return res
} catch (error) {
console.error('密码登录失败:', error)
return null
}
} }
// 提交登录 // 提交登录
const onSubmit = async () => { const onSubmit = async () => {
@@ -434,11 +424,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
} }

View File

@@ -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,22 +52,17 @@
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() userInfo.value = res.result || {}
if (res.code === 0) {
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.shopProductList?.length > 0) {
if (res.code === 0 && 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;

View File

@@ -26,15 +26,15 @@
<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>
@@ -50,14 +50,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 +86,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()
@@ -99,7 +100,7 @@
// VIP信息 // VIP信息
const vipInfo = computed(() => userStore.userVips) const vipInfo = computed(() => userStore.userVips)
const vipInfoEbook = computed(() => userStore.userEbookVip) const vipInfoEbook = computed(() => userStore.userEbookVip)
// VIP类型字典 // VIP类型字典
const vipTypeDict = sysStore.vipTypeDict const vipTypeDict = sysStore.vipTypeDict
@@ -178,7 +179,7 @@
} }
/** /**
* 跳转到订阅页面 * 跳转到电子书vip订阅页面
*/ */
const goSubscribe = () => { const goSubscribe = () => {
uni.navigateTo({ uni.navigateTo({
@@ -186,6 +187,15 @@
}) })
} }
/**
* 跳转到课程vip订阅页面
*/
const goCourseVipSub = () => {
uni.navigateTo({
url: '/pages/vip/course'
})
}
/** /**
* 处理菜单点击 * 处理菜单点击
*/ */
@@ -212,7 +222,7 @@
url: '/pages/user/recharge/index' url: '/pages/user/recharge/index'
}) })
} }
/** /**
* 跳转虚拟币页面 * 跳转虚拟币页面
*/ */
@@ -221,12 +231,22 @@
url: '/pages/user/virtual/index' url: '/pages/user/virtual/index'
}) })
} }
/**
* 跳转积分列表
*/
const goPointsList = () => {
uni.navigateTo({
url: '/pages/user/points/index'
})
}
onShow(() => {
getData()
})
onMounted(() => { onMounted(() => {
getPlatform() getPlatform()
getData()
}) })
</script> </script>
@@ -306,14 +326,14 @@
border-radius: 15rpx; border-radius: 15rpx;
padding: 26rpx 30rpx 10rpx; padding: 26rpx 30rpx 10rpx;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
.vip-card-title { .vip-card-title {
font-size: 32rpx; font-size: 32rpx;
color: #fff; color: #fff;
font-weight: bold; font-weight: bold;
margin-bottom: 20rpx; margin-bottom: 20rpx;
} }
.vip-card-content { .vip-card-content {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -323,6 +343,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;
@@ -412,7 +433,7 @@
flex: 1; flex: 1;
justify-content: space-around; justify-content: space-around;
text-align: center; text-align: center;
transform:translateX(-20px); transform: translateX(-20px);
.assets_row { .assets_row {
margin-bottom: 20rpx; margin-bottom: 20rpx;

View File

@@ -26,8 +26,9 @@
<ProductInfo v-if="order.orderType === 'order'" :data="order.productList" :type="order.orderType" /> <ProductInfo v-if="order.orderType === 'order'" :data="order.productList" :type="order.orderType" />
<ProductInfo v-if="order.orderType === 'abroadBook'" :data="order.bookEntity" :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>

135
pages/user/points/index.vue Normal file
View 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>

View File

@@ -199,14 +199,10 @@ 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,92 +390,88 @@ 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)
switch (key) { switch (key) {
case 'email': case 'email':
// 更新邮箱 // 更新邮箱
if (!editForm.value.email || !editForm.value.code) { if (!editForm.value.email || !editForm.value.code) {
uni.showToast({ uni.showToast({
title: t('user.pleaseInputCode'), title: t('user.pleaseInputCode'),
icon: 'none' icon: 'none'
}) })
return return
} }
await updateEmail(userInfo.value.id, editForm.value.email, editForm.value.code) await updateEmail(userInfo.value.id, editForm.value.email, editForm.value.code)
break break
case 'password': case 'password':
// 更新密码 // 更新密码
if (!passwordOk.value) { if (!passwordOk.value) {
uni.showToast({ uni.showToast({
title: passwordNote.value, title: passwordNote.value,
icon: 'none' icon: 'none'
}) })
return return
} }
if (editForm.value.password !== editForm.value.confirmPassword) { if (editForm.value.password !== editForm.value.confirmPassword) {
uni.showToast({ uni.showToast({
title: t('user.passwordNotMatch'), title: t('user.passwordNotMatch'),
icon: 'none' icon: 'none'
}) })
return return
} }
await updatePassword(userInfo.value.id, editForm.value.password) await updatePassword(userInfo.value.id, editForm.value.password)
break break
case 'avatar': case 'avatar':
// 更新头像 // 更新头像
console.log('avatarUrl.value:', avatarUrl.value) console.log('avatarUrl.value:', avatarUrl.value)
if (!avatarUrl.value) { if (!avatarUrl.value) {
uni.showToast({ uni.showToast({
title: t('common.pleaseSelect') + t('user.avatar'), title: t('common.pleaseSelect') + t('user.avatar'),
icon: 'none' icon: 'none'
}) })
return return
} }
// 如果是新上传的图片,需要先上传 // 如果是新上传的图片,需要先上传
updateData.avatar = avatarUrl.value updateData.avatar = avatarUrl.value
await updateUserInfo(updateData) await updateUserInfo(updateData)
break break
case 'sex': case 'sex':
// 更新性别 // 更新性别
updateData.sex = editValue.value updateData.sex = editValue.value
await updateUserInfo(updateData) await updateUserInfo(updateData)
break break
default: default:
// 更新其他字段 // 更新其他字段
if (!editValue.value) { if (!editValue.value) {
uni.showToast({ uni.showToast({
title: getPlaceholder(key), title: getPlaceholder(key),
icon: 'none' icon: 'none'
}) })
return return
} }
updateData[key] = editValue.value updateData[key] = editValue.value
await updateUserInfo(updateData) await updateUserInfo(updateData)
break break
}
uni.showToast({
title: t('user.updateSuccess'),
icon: 'success'
})
closeModal()
// 刷新数据
setTimeout(() => {
getData()
}, 500)
} catch (error) {
console.error('更新失败:', error)
} }
uni.showToast({
title: t('user.updateSuccess'),
icon: 'success'
})
closeModal()
// 刷新数据
setTimeout(() => {
getData()
}, 500)
} }
/** /**

View File

@@ -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>
@@ -61,10 +62,13 @@
<script setup lang="ts"> <script setup lang="ts">
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'
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 +111,40 @@
const remark = ref({}) const remark = ref({})
const isConnected = ref(false) const isConnected = ref(false)
const purchaseToken = ref()
// 订单编号
const orderSn = ref('')
/** /**
* 获取订单编号
*/
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 {
const res = await getPlaceOrder(data)
orderSn.value = res.orderSn
console.log(orderSn.value, '获取订单号');
getGooglePay()
} catch (error) {
console.error('获取订单号失败', error)
}
}
/**
* 获取使用环境 * 获取使用环境
*/ */
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;
@@ -127,7 +158,7 @@
// 点击金额 // 点击金额
const chosPric = (item : any) => { const chosPric = (item : any) => {
console.log(item,'金额每项'); console.log(item, '金额每项');
aloneItem.value = item; aloneItem.value = item;
}; };
@@ -195,25 +226,25 @@
// payType.value = val; // payType.value = val;
} }
const handleRecharge = () => { const handleRecharge = async () => {
if(!state.value){ if (!state.value) {
uni.showToast({ uni.showToast({
title: t('order.readAgreeServices'), title: t('order.readAgreeServices'),
icon: 'none' icon: 'none'
}) })
return return
} }
uni.showLoading({ title: '加载中...' }) getPlaceOrderObj()
console.log('立即充值'); uni.showLoading({ title: '生成订单中...' })
getGooglePay()
} }
/** /**
* 初始化 * 初始化
*/ */
const getGooglePay = () => { const getGooglePay = () => {
googlePay.init({ googlePay.init({
}, (e:any) => { }, (e : any) => {
console.log('init', e); console.log('init', e);
if (e.code == 0) { if (e.code == 0) {
isConnected.value = true; isConnected.value = true;
@@ -225,47 +256,94 @@
} }
}); });
} }
/** /**
* 查询sku * 查询sku
*/ */
const getQuerySku = () =>{ const getQuerySku = () => {
const id = aloneItem.value.priceTypeId const id = aloneItem.value.priceTypeId
console.log(id, '获取每项'); console.log(id, '获取每项');
googlePay.querySku( googlePay.querySku(
{ {
inapp: [id], // 与subs二选一, 参数为商品ID字符串数组 inapp: [id], // 与subs二选一, 参数为商品ID字符串数组
}, },
(e:any) => { (e : any) => {
if (e.code == 0) { if (e.code == 0) {
// 查询成功. // 查询成功.
console.log('查询成功',e); console.log('querySku查询成功', e);
// e.list; // 查询结果, array uni.hideLoading()
} else { getPayAll()
console.log('查询失败'); } else {
// 查询失败 console.log('查询失败', e);
} // 查询失败
} }
) }
}
const getPayAll = () =>{
googlePay.payAll(
{
productId: "", // 产品id
},
(e) => {
if (e.code == 0) {
// 支付成功
e.data; //支付结果, array [ {original:{ }, signature: ''} ]
} else {
// 支付失败
}
},
) )
} }
/**
* 发起支付
*/
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: '支付失败', icon: 'success' })
console.log(e, 'e');
// 支付失败
}
},
)
}
/**
* 消耗品 确认交易
*/
const getConsume = () => {
googlePay.consume(
{
purchaseToken: purchaseToken.value, // 来自支付结果的original.purchaseToken (或 original.token)
},
(e : any) => {
if (e.code == 0) {
console.log(e, '确认交易成功');
// 确认成功
googleVerify()
} else {
console.log(e, '确认交易失败');
// 确认失败
}
},
);
}
/**
* 校验订单
*/
const googleVerify = async () => {
console.log(typeof aloneItem.value.priceTypeId, typeof purchaseToken.value, typeof orderSn.value);
try {
const obj = await verifyGooglePay(aloneItem.value.priceTypeId, purchaseToken.value, orderSn.value)
uni.switchTab({
url: '/pages/user/index'
})
console.log(obj, '校验订单');
} catch (error) {
console.error('校验订单失败:', error)
}
}
onMounted(() => { onMounted(() => {
getDevName(); getDevName();
@@ -387,7 +465,7 @@
border-bottom: 1px solid #ededed; border-bottom: 1px solid #ededed;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items:center; align-items: center;
image { image {
width: 40rpx; width: 40rpx;

View File

@@ -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 = {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 208 KiB

View File

@@ -114,8 +114,13 @@ uni-textarea {
.wd-overlay { .wd-overlay {
z-index: 9998 !important; z-index: 9998 !important;
} }
.wd-popup-wrapper .wd-popup { .wd-popup-wrapper {
border-radius: 15px 15px 0 0 !important; .wd-popup {
border-radius: 15px !important;
}
.wd-popup--bottom {
border-radius: 15px 15px 0 0 !important;
}
} }
// uni-ui form // uni-ui form

View File

@@ -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) => {

View File

@@ -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,