更新:增加课程“复读”功能

This commit is contained in:
2025-12-08 18:05:45 +08:00
parent b671e8d76c
commit c3b84946fb
12 changed files with 268 additions and 225 deletions

View File

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

View File

@@ -159,7 +159,7 @@ export function uploadImage(filePath: string): Promise<string> {
url: `${SERVICE_MAP.MAIN}oss/fileoss`,
filePath,
name: 'file',
success: (res) => {
success: (res: any) => {
try {
const data = JSON.parse(res.data)
if (data.url) {
@@ -304,9 +304,10 @@ export async function getPointsData(current : number, limit : number, userId : s
* 迁移用户数据
* @param tel 旧账号
* @param code 迁移验证码
* @param type 未迁移数据类型
* @return
*/
export async function migrateUserData(data: { tel: string, code: string }) {
export async function migrateUserData(data: { tel: string, code: string, type: string }) {
const res = await mainClient.request<IApiResponse>({
url: 'common/user/migrationWumenData',
method: 'POST',
@@ -314,3 +315,18 @@ export async function migrateUserData(data: { tel: string, code: string }) {
})
return res
}
/**
* 获取用户迁移信息
* @return {
* alreadyMigration: 已迁移用户数
* notMigration: 未迁移用户数
* }
*/
export async function getUserMigrateInfo() {
const res = await mainClient.request<IApiResponse>({
url: 'common/user/getMigrationList',
method: 'POST',
})
return res
}

View File

@@ -16,7 +16,7 @@
<!-- 商品列表 -->
<view class="selector-header">
<text class="title">{{ isFudu ? t('order.selectFuduScheme') : t('order.selectPurchaseScheme') }}</text>
<text class="title">{{ t('order.goodsList') }}</text>
</view>
<view class="goods-list">
<view
@@ -54,7 +54,6 @@ const { t } = useI18n()
interface Props {
show: boolean
goods: IGoods[]
isFudu?: boolean // 是否为复读
}
const props = defineProps<Props>()

View File

@@ -438,8 +438,7 @@
"orderCreateFailed": "Failed to create order"
},
"order": {
"selectFuduScheme": "Select Fudu Scheme",
"selectPurchaseScheme": "Select Purchase Scheme",
"goodsList": "Product List",
"confirmTitle": "Confirm Order",
"goodsInfo": "Product Information",
"priceDetail": "Price Details",

View File

@@ -438,8 +438,7 @@
"orderCreateFailed": "订单创建失败"
},
"order": {
"selectFuduScheme": "选择复读方案",
"selectPurchaseScheme": "选择购买方案",
"goodsList": "商品列表",
"confirmTitle": "确认订单",
"goodsInfo": "商品信息",
"priceDetail": "价格明细",

View File

@@ -1,6 +1,6 @@
{
"id": "uni-taimed-international-app",
"name": "TaimedInternationalApp",
"id": "uni-wumen-international-app",
"name": "wumen-international-app",
"displayName": "太湖国际",
"version": "1.0.3",
"description": "太湖国际",

View File

@@ -55,12 +55,8 @@
<wd-button size="small" type="warning" @click="handlePurchase">
{{ $t('courseDetails.purchase') }}
</wd-button>
<wd-button
v-if="showRenewBtn"
size="small"
type="success"
@click="handleRenew"
>
<!-- 如果是复读显示复读按钮 -->
<wd-button v-if="canRenlearn" size="small" type="success" @click="handleRenlearn">
{{ $t('courseDetails.relearn') }}
</wd-button>
<wd-button size="small" type="primary" @click="goToVip">
@@ -134,28 +130,42 @@
<text>暂无章节内容</text>
</view>
</view>
<!-- 商品选择器 -->
<GoodsSelector
:show="showGoodsSelector"
:goods="goodsList"
@select="handleGoodsSelect"
@confirm="handleGoodsConfirm"
@close="closeGoodsSelector"
/>
<!-- 购买协议弹窗 -->
<Protocol :visible="showProtocol" @confirmPurchase="confirmPurchase" />
</view>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { courseApi } from '@/api/modules/course'
import GoodsSelector from '@/components/order/GoodsSelector.vue'
import Protocol from './Protocol.vue'
import type { IChapter, ICatalogue, IVipInfo } from '@/types/course'
import type { IGoods } from '@/types/order'
interface Props {
catalogues: ICatalogue[]
userVip: IVipInfo | null
showRenewBtn?: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
click: [chapter: IChapter],
purchase: [catalogue: ICatalogue],
renew: [catalogue: ICatalogue],
toVip: [catalogue: ICatalogue],
change: [index: number]
change: [index: number],
toVip: [],
toDetail: [chapter: IChapter, catalogue: ICatalogue],
loadPageData: [],
}>()
// 当前目录索引
@@ -167,14 +177,21 @@ const currentCatalogue = computed(() => {
// 当前目录的章节
const chapterList = ref<IChapter[]>([])
// 显示续费按钮
const showRenewBtn = ref<boolean>(false)
// 当前目录是否是复读
const isRelearn = ref<boolean>(false)
const canRenlearn = ref<boolean>(false)
// 判断目录是否已购买
const isPurchased = computed(() => {
return currentCatalogue.value.isBuy === 1
})
// 商品选择
const showGoodsSelector = ref(false)
const goodsList = ref<IGoods[]>([])
const selectedGoods = ref<IGoods | null>(null)
const showProtocol = ref(false)
/**
* 选择目录
*/
@@ -201,9 +218,9 @@ const getChapters = async () => {
const checkRenewPayment = async () => {
if (currentCatalogue.value.isBuy === 0 && !props.userVip) {
const renewRes = await courseApi.checkRenewPayment(currentCatalogue.value.id)
showRenewBtn.value = renewRes.canRelearn || false
canRenlearn.value = renewRes.canRelearn || false
} else {
showRenewBtn.value = false
canRenlearn.value = false
}
}
@@ -219,23 +236,98 @@ watch(() => props.catalogues, (newVal: ICatalogue[]) => {
}, { immediate: true, deep: true })
// 购买
const handlePurchase = () => {
emit('purchase', currentCatalogue.value)
const handlePurchase = async () => {
if (!currentCatalogue.value) return
isRelearn.value = false
const res = await courseApi.getProductListForCourse(currentCatalogue.value.id)
if (res.code === 0 && res.productList.length > 0) {
goodsList.value = res.productList
showGoodsSelector.value = true
} else {
uni.showToast({ title: '此课程暂无购买方式', icon: 'none' })
}
}
/**
* 选择商品
*/
const handleGoodsSelect = (goods: IGoods) => {
selectedGoods.value = goods
}
/**
* 确认购买
*/
const handleGoodsConfirm = () => {
showGoodsSelector.value = false
showProtocol.value = true
}
/**
* 关闭商品选择器
*/
const closeGoodsSelector = () => {
showGoodsSelector.value = false
}
/**
* 确认购买协议
*/
const confirmPurchase = () => {
showProtocol.value = false
if (!selectedGoods.value) return
showProtocol.value = false
if (isRelearn.value) {
uni.navigateTo({
url: `/pages/order/goodsConfirm?isRelearn=1`,
success: () => {
setTimeout(() => {
uni.$emit('selectedGoods', selectedGoods.value)
}, 100)
}
})
} else {
// 跳转到购买确认订单页
uni.navigateTo({
url: `/pages/order/goodsConfirm?goods=${selectedGoods.value.productId}`
})
}
}
// 去开通vip
const goToVip = () => {
emit('toVip', currentCatalogue.value)
emit('toVip')
}
// 续费/复读
const handleRenew = () => {
emit('renew', currentCatalogue.value)
const handleRenlearn = async () => {
if (!currentCatalogue.value) return
isRelearn.value = true
const res = await courseApi.getRenewProductList(currentCatalogue.value.id)
if (res.productList.length > 0) {
goodsList.value = res.productList
showGoodsSelector.value = true
} else {
uni.showToast({ title: '暂无复读方案', icon: 'none' })
}
}
// 领取免费课程
const handleGetFreeCourse = async () => {
emit('getFreeCourse', currentCatalogue.value)
if (!currentCatalogue.value) return
const res = await courseApi.startStudyForMF(currentCatalogue.value.id)
if (res.code === 0) {
uni.showToast({ title: '领取成功', icon: 'success' })
// 刷新页面数据
emit('loadPageData')
} else {
uni.showToast({ title: res.msg || '领取失败', icon: 'none' })
}
}
/**
@@ -260,7 +352,7 @@ const canAccess = (chapter: IChapter): boolean => {
/**
* 点击章节
*/
const handleChapterClick = (chapter: IChapter, catalogue: ICatalogue) => {
const handleChapterClick = (chapter: IChapter) => {
if (!canAccess(chapter)) {
if (currentCatalogue.value.type === 0) {
uni.showToast({

View File

@@ -0,0 +1,91 @@
<template>
<wd-popup v-model="showProtocol" position="center">
<view class="protocol-popup">
<view class="protocol-title">温馨提示</view>
<view class="protocol-content">
<text>
用户您好本软件对于一个用户名及密码仅允许一部电子设备登陆多部设备使用同一用户名操作软件的行为属于违规操作发现违规一次将提出警告再次违规您的用户名将被封号无法正常登陆如因此对您使用带来不便敬请谅解
</text>
<text>
课程购买之后一年内不打开此一年内不会计算有效学习时间一年后会自动开始计算有效学习时间
</text>
<text>
本课程一经购买暂不支持退款敬请谅解
</text>
<view style="color: red; font-weight: bold"> : </view>
<view>
1.手机pad电脑均为可登陆电子设备均有唯一标识码一个用户名仅允许在一个手机或一个ipad或一个电脑登陆请根据您的使用习惯自行选择<br />
2.如若申请变更登陆设备请联系客服<br />
客服电话:021-08371305<br />
客服微信号:yilujiankangkefu<br />
3.如因违反上述使用规定...概不退款本公司保留追究用户相关法律责任的权利<br />
4.点击同意按钮即表示您同意遵守以上条款
</view>
</view>
<view class="protocol-actions">
<wd-button type="info" plain @click="showProtocol = false">不同意</wd-button>
<wd-button type="primary" @click="confirmPurchase">同意</wd-button>
</view>
</view>
</wd-popup>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
const props = defineProps<{
visible: boolean
}>()
console.log(props.visible)
const showProtocol = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
})
const emit = defineEmits<{
'update:visible': [boolean],
confirmPurchase: []
}>()
const confirmPurchase = () => {
emit('confirmPurchase')
}
</script>
<style lang="scss" scoped>
.protocol-popup {
width: 600rpx;
padding: 40rpx;
background-color: #fff;
border-radius: 12rpx;
.protocol-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
text-align: center;
margin-bottom: 30rpx;
}
.protocol-content {
max-height: 60vh;
overflow-y: auto;
font-size: 26rpx;
line-height: 1.8;
color: #666;
margin-bottom: 30rpx;
text {
display: block;
margin-bottom: 20rpx;
}
}
.protocol-actions {
display: flex;
gap: 20rpx;
}
}
</style>

View File

@@ -20,10 +20,8 @@
v-if="catalogueList.length > 0"
:catalogues="catalogueList"
:userVip="userVip"
@getFreeCourse="handleGetFreeCourse"
@purchase="handlePurchase"
@toVip="goToVip"
@renew="handleRenew"
@loadPageData="loadPageData"
@toDetail="handleToDetail"
/>
@@ -89,55 +87,14 @@
/>
</view> -->
<!-- 商品选择器 -->
<GoodsSelector
:show="showGoodsSelector"
:goods="goodsList"
:isFudu="isFudu"
@select="handleGoodsSelect"
@confirm="handleGoodsConfirm"
@close="closeGoodsSelector"
/>
<!-- 购买协议弹窗 -->
<wd-popup v-model="showProtocol" position="center">
<view class="protocol-popup">
<view class="protocol-title">温馨提示</view>
<view class="protocol-content">
<text>
用户您好本软件对于一个用户名及密码仅允许一部电子设备登陆多部设备使用同一用户名操作软件的行为属于违规操作发现违规一次将提出警告再次违规您的用户名将被封号无法正常登陆如因此对您使用带来不便敬请谅解
</text>
<text>
课程购买之后一年内不打开此一年内不会计算有效学习时间一年后会自动开始计算有效学习时间
</text>
<text>
本课程一经购买暂不支持退款敬请谅解
</text>
<view style="color: red; font-weight: bold"> : </view>
<view>
1.手机pad电脑均为可登陆电子设备均有唯一标识码一个用户名仅允许在一个手机或一个ipad或一个电脑登陆请根据您的使用习惯自行选择<br />
2.如若申请变更登陆设备请联系客服<br />
客服电话:021-08371305<br />
客服微信号:yilujiankangkefu<br />
3.如因违反上述使用规定...概不退款本公司保留追究用户相关法律责任的权利<br />
4.点击同意按钮即表示您同意遵守以上条款
</view>
</view>
<view class="protocol-actions">
<wd-button type="info" plain @click="showProtocol = false">不同意</wd-button>
<wd-button type="primary" @click="confirmPurchase">同意</wd-button>
</view>
</view>
</wd-popup>
<!-- 评论编辑器 -->
<CommentEditor
<!-- <CommentEditor
:show="showEditor"
:parentComment="replyComment"
type="course"
@submit="handleCommentSubmit"
@close="closeCommentEditor"
/>
/> -->
<!-- 返回顶部 -->
<wd-backtop :scrollTop="scrollTop" custom-class="back-top">
@@ -153,7 +110,6 @@ import { useUserStore } from '@/stores/user'
import { courseApi } from '@/api/modules/course'
import CourseInfo from './components/CourseInfo.vue'
import CatalogueList from './components/CatalogueList.vue'
import GoodsSelector from '@/components/order/GoodsSelector.vue'
import CommentList from '@/components/comment/CommentList.vue'
import CommentEditor from '@/components/comment/CommentEditor.vue'
import type { ICourseDetail, ICatalogue, IChapter, IVipInfo } from '@/types/course'
@@ -172,14 +128,6 @@ const vipModuleList = ref<string[]>([])
const learningProgress = ref(0)
const relatedBooks = ref<IGoods[]>([])
// 商品选择
const showGoodsSelector = ref(false)
const goodsList = ref<IGoods[]>([])
const selectedGoods = ref<IGoods | null>(null)
const showProtocol = ref(false)
const isFudu = ref(false)
const fuduCatalogueId = ref<number>(0)
// 评论相关
const commentList = ref<IComment[]>([])
const commentsLoading = ref(false)
@@ -301,92 +249,6 @@ const goToVip = () => {
})
}
/**
* 领取免费课程
*/
const handleGetFreeCourse = async (catalogue: ICatalogue) => {
if (!catalogue) return
const res = await courseApi.startStudyForMF(catalogue.id)
if (res.code === 0) {
uni.showToast({ title: '领取成功', icon: 'success' })
// 刷新页面数据
loadPageData()
} else {
uni.showToast({ title: res.msg || '领取失败', icon: 'none' })
}
}
/**
* 购买课程
*/
const handlePurchase = async (catalogue: ICatalogue) => {
if (!catalogue) return
isFudu.value = false
const res = await courseApi.getProductListForCourse(catalogue.id)
if (res.code === 0 && res.productList.length > 0) {
goodsList.value = res.productList
showGoodsSelector.value = true
} else {
uni.showToast({ title: '此课程暂无购买方式', icon: 'none' })
}
}
/**
* 续费/复读
*/
const handleRenew = async () => {
// if (!currentCatalogue.value) return
// isFudu.value = true
// fuduCatalogueId.value = currentCatalogue.value.id
// const res = await courseApi.getRenewProductList(currentCatalogue.value.id)
// if (res.code === 0 && res.productList.length > 0) {
// goodsList.value = res.productList
// showGoodsSelector.value = true
// } else {
// uni.showToast({ title: '暂无复读方案', icon: 'none' })
// }
}
/**
* 选择商品
*/
const handleGoodsSelect = (goods: IGoods) => {
selectedGoods.value = goods
}
/**
* 确认购买
*/
const handleGoodsConfirm = () => {
showGoodsSelector.value = false
showProtocol.value = true
}
/**
* 关闭商品选择器
*/
const closeGoodsSelector = () => {
showGoodsSelector.value = false
}
/**
* 确认购买协议
*/
const confirmPurchase = () => {
showProtocol.value = false
if (!selectedGoods.value) return
showProtocol.value = false
// 跳转到确认订单页
uni.navigateTo({
url: `/pages/order/goodsConfirm?goods=${selectedGoods.value.productId}`
})
}
/**
* 跳转到书籍详情
*/
@@ -740,40 +602,6 @@ onReachBottom(() => {
}
}
.protocol-popup {
width: 600rpx;
padding: 40rpx;
background-color: #fff;
border-radius: 12rpx;
.protocol-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
text-align: center;
margin-bottom: 30rpx;
}
.protocol-content {
max-height: 60vh;
overflow-y: auto;
font-size: 26rpx;
line-height: 1.8;
color: #666;
margin-bottom: 30rpx;
text {
display: block;
margin-bottom: 20rpx;
}
}
.protocol-actions {
display: flex;
gap: 20rpx;
}
}
:deep(.back-top) {
background-color: #fff !important;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);

View File

@@ -91,15 +91,23 @@ const orderType = computed(() => {
* 页面加载
*/
onLoad(async (options: any) => {
if (options.goods) {
try {
if (options.isRelearn == 1) {
uni.$on('selectedGoods', async (data: IOrderGoods) => {
// 获取用户信息
await getUserInfo()
// 处理商品数据
console.log('监听到传入的商品数据:', data)
goodsList.value = [ data ]
})
} else if (options.goods) {
// 获取用户信息
await getUserInfo()
// 根据商品ID获取商品详细信息
goodsIds.value = options.goods || ''
isRelearn.value = options.isRelearn == '1'
getGoodsList()
}
} catch (error) {
console.error('解析商品数据失败:', error)
uni.showToast({
@@ -107,7 +115,6 @@ onLoad(async (options: any) => {
icon: 'none'
})
}
}
})
</script>

View File

@@ -56,10 +56,10 @@ const orderType = ref<string>('')
/**
* 页面加载
*/
onLoad(async () => {
onLoad(() => {
try {
// 获取商品列表
await uni.$on('selectedGoods', async (data: IOrderGoods) => {
uni.$on('selectedGoods', async (data: IOrderGoods) => {
// 获取用户信息
await getUserInfo()

View File

@@ -116,3 +116,15 @@ export interface IPageData<T> {
pages: number
[key: string]: any
}
/**
* 订单接口
*/
export interface IOrder {
id: number
orderType: string // 订单类型
changeAmount: number
remark: string
createTime: string
[key: string]: any
}