Compare commits

33 Commits

Author SHA1 Message Date
e76e6da008 修复:图书详情和课程详情无数据问题 2025-12-26 11:39:06 +08:00
89e77864a3 更新:更新版本号 2025-12-24 15:07:09 +08:00
19e8a7d033 更新:更新版本 2025-12-24 11:15:35 +08:00
88057e0805 修复:解决可以触发两次下单支付的问题; 2025-12-22 18:09:33 +08:00
cafb86cc9d 修复:修复下单可多次点击支付按钮问题;允许积分支付小数; 2025-12-22 17:52:02 +08:00
55455fa4f2 修复:添加活动充值说明 2025-12-22 15:47:42 +08:00
703ab8550b 修复:电子书VIP套餐页“白块”问题 2025-12-22 09:54:01 +08:00
ea585cdc96 修复:没有订单号不显示订单字段 2025-12-22 09:23:03 +08:00
a04d40a6d0 修复:电子书VIP套餐页“白块”问题 2025-12-19 14:38:17 +08:00
11dfd01b39 Merge branch 'main' of https://git.nuttyreading.com/zm/taimed-international-app 2025-12-19 13:59:34 +08:00
ce12470688 优化:数据迁移功能优化 2025-12-19 13:59:30 +08:00
62adfa1d4f Merge branch 'main' of https://git.nuttyreading.com/zm/taimed-international-app 2025-12-19 08:57:37 +08:00
778c6372e7 修复:后台充值和积分,app不显示订单号和跳转详情 2025-12-19 08:57:17 +08:00
54815f871f 修复:解决控制台报错 2025-12-18 16:01:09 +08:00
cf9ef2e6dd Merge branch 'main' of https://git.nuttyreading.com/zm/taimed-international-app 2025-12-18 15:57:54 +08:00
2b0e339bc9 修复:完善数据迁移;修改logo及课程首页接口文件覆盖问题; 2025-12-18 15:57:50 +08:00
4df53c611b 修改:后台操作,充值、积分功能。手机端进行限制 2025-12-18 15:30:23 +08:00
56772970e2 修复:优化访客模式 2025-12-18 14:03:01 +08:00
9e04533bdc 更新:更新打包版本 2025-12-18 09:20:51 +08:00
77fedbe877 修复:ios审核上传照片问题 2025-12-17 18:26:21 +08:00
6c2811646a 修复:修改logo图 2025-12-15 15:47:16 +08:00
9e00409e5e 修复:不同系统支付,显示支付方式不同 2025-12-15 11:18:22 +08:00
1457a24cea 修复:更新ios支付接口 2025-12-15 10:41:41 +08:00
34d6cfdf9e 更新:访客模式可以查看图书首页 2025-12-12 14:33:47 +08:00
04e2196942 优化:去掉不必要的loading 2025-12-12 11:57:43 +08:00
6e5d63febe 更新:课程首页增加骨架屏 2025-12-12 10:22:44 +08:00
3ce5e07573 Merge branch 'main' of https://git.nuttyreading.com/zm/taimed-international-app 2025-12-11 17:30:53 +08:00
d98e1ef024 更新:ios支付 2025-12-11 17:30:45 +08:00
b8dd0584aa 更新:1.课程详情增加骨架屏;2.图书首页和图书详情增加骨架屏; 2025-12-11 16:13:40 +08:00
b3d9b0c100 合并请求 2025-12-09 14:37:21 +08:00
66b004d6bf 更新:游客模式、我的湖分、我的证书 2025-12-09 14:28:02 +08:00
c3b84946fb 更新:增加课程“复读”功能 2025-12-08 18:05:45 +08:00
b671e8d76c 更新:数据迁移功能完善;(数据迁移功能已注释,暂不上线) 2025-12-08 14:34:18 +08:00
67 changed files with 3413 additions and 1941 deletions

View File

@@ -1,6 +1,7 @@
<script>
// #ifdef APP-PLUS
import update from "@/uni_modules/uni-upgrade-center-app/utils/check-update";
// #endif
export default {
onLaunch: function() {
@@ -15,6 +16,9 @@
},
onHide: function() {
console.log('App Hide')
},
onTabItemTap: function() {
console.log('点击了');
}
}
</script>

9
api/clients/index.ts Normal file
View File

@@ -0,0 +1,9 @@
import { skeletonClient } from './skeleton'
import { mainClient } from './main'
import { paymentClient } from './payment'
export {
skeletonClient,
mainClient,
paymentClient
}

View File

@@ -3,5 +3,5 @@ import { createRequestClient } from '../request';
import { SERVICE_MAP } from '../config';
export const mainClient = createRequestClient({
baseURL: SERVICE_MAP.MAIN,
baseURL: SERVICE_MAP.MAIN
});

8
api/clients/skeleton.ts Normal file
View File

@@ -0,0 +1,8 @@
// api/clients/main.ts
import { createRequestClient } from '../request';
import { SERVICE_MAP } from '../config';
export const skeletonClient = createRequestClient({
baseURL: SERVICE_MAP.MAIN,
loading: false
})

View File

@@ -8,7 +8,7 @@ import type { ILoginResponse } from '@/types/user'
* @param code 验证码
*/
export async function loginWithCode(tel: string, code: string) {
const res = await mainClient.request<IApiResponse<ILoginResponse>>({
const res = await mainClient.request<ILoginResponse>({
url: 'book/user/registerOrLogin',
method: 'GET',
data: { tel, code }
@@ -22,7 +22,7 @@ export async function loginWithCode(tel: string, code: string) {
* @param password 密码
*/
export async function loginWithPassword(phone: string, password: string) {
const res = await mainClient.request<IApiResponse<ILoginResponse>>({
const res = await mainClient.request<ILoginResponse>({
url: 'book/user/login',
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },

View File

@@ -1,6 +1,5 @@
// api/modules/book.ts
import { createRequestClient } from '../request'
import { SERVICE_MAP } from '../config'
import { mainClient, skeletonClient } from '@/api/clients'
import type { IApiResponse } from '../types'
import type {
IBook,
@@ -13,8 +12,6 @@ import type {
} from '@/types/book'
import type { IGoods } from '@/types/order'
const client = createRequestClient({ baseURL: SERVICE_MAP.MAIN })
/**
* 书籍相关API
*/
@@ -25,7 +22,7 @@ export const bookApi = {
* @param limit 每页数量
*/
getMyBooks(current: number, limit: number) {
return client.request<IApiResponse<{ page: IPageData<IBook> }>>({
return mainClient.request<IApiResponse<{ page: IPageData<IBook> }>>({
url: 'bookAbroad/home/getMyBooks',
method: 'POST',
data: { current, limit }
@@ -37,7 +34,7 @@ export const bookApi = {
* @param bookId 书籍ID
*/
getBookInfo(bookId: number) {
return client.request<IApiResponse<{ bookInfo: IBookDetail }>>({
return skeletonClient.request<IApiResponse<{ bookInfo: IBookDetail }>>({
url: 'bookAbroad/home/getBookInfo',
method: 'POST',
data: { bookId }
@@ -49,7 +46,7 @@ export const bookApi = {
* @param bookId 书籍ID
*/
getBookReadCount(bookId: number) {
return client.request<IApiResponse<{
return skeletonClient.request<IApiResponse<{
readCount: number
listenCount: number
buyCount: number
@@ -65,7 +62,7 @@ export const bookApi = {
* @param bookId 书籍ID
*/
getRecommendBook(bookId: number) {
return client.request<IApiResponse<{ bookList: IBook[] }>>({
return skeletonClient.request<IApiResponse<{ bookList: IBook[] }>>({
url: 'bookAbroad/home/getRecommendBook',
method: 'POST',
data: { bookId }
@@ -79,7 +76,7 @@ export const bookApi = {
* @param limit 每页数量
*/
getBookComments(bookId: number, current: number, limit: number) {
return client.request<IApiResponse<{
return mainClient.request<IApiResponse<{
commentsTree: IComment[]
commentsCount: number
}>>({
@@ -96,7 +93,7 @@ export const bookApi = {
* @param pid 父评论ID默认为0表示顶级评论
*/
insertComment(bookId: number, content: string, pid: number = 0) {
return client.request<IApiResponse>({
return mainClient.request<IApiResponse>({
url: 'bookAbroad/insertBookAbroadComment',
method: 'POST',
data: { bookId, content, pid }
@@ -108,7 +105,7 @@ export const bookApi = {
* @param commentId 评论ID
*/
likeComment(commentId: number) {
return client.request<IApiResponse>({
return mainClient.request<IApiResponse>({
url: 'bookAbroad/insertBookAbroadCommentLike',
method: 'POST',
data: { commentId }
@@ -120,7 +117,7 @@ export const bookApi = {
* @param commentId 评论ID
*/
unlikeComment(commentId: number) {
return client.request<IApiResponse>({
return mainClient.request<IApiResponse>({
url: 'bookAbroad/delBookAbroadCommentLike',
method: 'POST',
data: { commentId }
@@ -132,7 +129,7 @@ export const bookApi = {
* @param commentId 评论ID
*/
deleteComment(commentId: number) {
return client.request<IApiResponse>({
return mainClient.request<IApiResponse>({
url: 'bookAbroad/delBookAbroadComment',
method: 'POST',
data: { commentId }
@@ -144,7 +141,7 @@ export const bookApi = {
* @param bookId 书籍ID
*/
getBookChapter(data: { bookId: number; language: string }) {
return client.request<IApiResponse<{ chapterList: IChapter[] }>>({
return mainClient.request<IApiResponse<{ chapterList: IChapter[] }>>({
url: 'bookAbroad/home/getBookChapter',
method: 'POST',
data
@@ -156,7 +153,7 @@ export const bookApi = {
* @param chapterId 章节ID
*/
getChapterContent(chapterId: number) {
return client.request<IApiResponse<{ contentPage: IChapterContent[] }>>({
return mainClient.request<IApiResponse<{ contentPage: IChapterContent[] }>>({
url: 'bookAbroad/home/getBookChapterContent',
method: 'POST',
data: { chapterId }
@@ -168,7 +165,7 @@ export const bookApi = {
* @param bookId 书籍ID
*/
getReadProgress(bookId: number) {
return client.request<IApiResponse<{ bookReadRate: IReadProgress | null }>>({
return mainClient.request<IApiResponse<{ bookReadRate: IReadProgress | null }>>({
url: 'bookAbroad/home/getBookReadRate',
method: 'POST',
data: { bookId }
@@ -182,7 +179,7 @@ export const bookApi = {
* @param contentId 内容ID
*/
saveReadProgress(bookId: number, chapterId: number, contentId: number) {
return client.request<IApiResponse>({
return mainClient.request<IApiResponse>({
url: 'bookAbroad/home/insertBookReadRate',
method: 'POST',
data: { bookId, chapterId, contentId }
@@ -196,7 +193,7 @@ export const bookApi = {
* @param contentId 内容ID
*/
getBookLanguages(bookId: number) {
return client.request<IApiResponse>({
return mainClient.request<IApiResponse>({
url: 'bookAbroad/home/getBookLanguage',
method: 'POST',
data: { bookId }
@@ -208,7 +205,7 @@ export const bookApi = {
* @param chapterId 章节ID
*/
getChapterContentListen(chapterId: number) {
return client.request<IApiResponse<{ bookChapterContents: any[] }>>({
return mainClient.request<IApiResponse<{ bookChapterContents: any[] }>>({
url: 'bookAbroad/home/getBookChapterContentListen',
method: 'POST',
data: { chapterId }
@@ -220,7 +217,7 @@ export const bookApi = {
* @param bookId 书籍ID
*/
getBookGoods(bookId: number) {
return client.request<IApiResponse<{ goodsList: IGoods[] }>>({
return mainClient.request<IApiResponse<{ goodsList: IGoods[] }>>({
url: 'bookAbroad/home/getProductListForBook',
method: 'POST',
data: { bookId }

View File

@@ -1,6 +1,5 @@
// api/modules/home.ts
import { createRequestClient } from '../request'
import { SERVICE_MAP } from '../config'
import { mainClient, skeletonClient } from '@/api/clients'
import type {
IMyBooksResponse,
IRecommendBooksResponse,
@@ -10,17 +9,15 @@ import type {
ISearchResponse
} from '@/types/book'
const client = createRequestClient({ baseURL: SERVICE_MAP.MAIN })
/**
* 首页相关API
*/
export const homeApi = {
export const bookHomeApi = {
/**
* 获取VIP信息
*/
getVipInfo() {
return client.request<IVipInfoResponse>({
return mainClient.request<IVipInfoResponse>({
url: 'bookAbroad/home/getVipInfo',
method: 'POST',
data: {}
@@ -33,7 +30,7 @@ export const homeApi = {
* @param limit 每页数量
*/
getMyBooks(current: number, limit: number) {
return client.request<IMyBooksResponse>({
return skeletonClient.request<IMyBooksResponse>({
url: 'bookAbroad/home/getMyBooks',
method: 'POST',
data: { current, limit }
@@ -44,8 +41,8 @@ export const homeApi = {
* 获取推荐图书
*/
getRecommendBooks() {
return client.request<IRecommendBooksResponse>({
url: 'bookAbroad/home/getRecommendBooks',
return skeletonClient.request<IRecommendBooksResponse>({
url: uni.getStorageSync('token') ? 'bookAbroad/home/getRecommendBooks' : 'visitor/bookAbroad/getRecommendBooks',
method: 'POST',
data: {}
})
@@ -56,8 +53,8 @@ export const homeApi = {
* @param type 0: 分类标签, 1: 活动标签
*/
getBookLabelList(type: number) {
return client.request<ILabelListResponse>({
url: 'bookAbroad/home/getBookAbroadLableList',
return skeletonClient.request<ILabelListResponse>({
url: uni.getStorageSync('token') ? 'bookAbroad/home/getBookAbroadLableList' : 'visitor/bookAbroad//getBookAbroadLableList',
method: 'POST',
data: { type }
})
@@ -68,8 +65,8 @@ export const homeApi = {
* @param pid 父级标签ID
*/
getSubLabelList(pid: number) {
return client.request<ILabelListResponse>({
url: 'bookAbroad/home/getBookAbroadLableListByPid',
return skeletonClient.request<ILabelListResponse>({
url: uni.getStorageSync('token') ? 'bookAbroad/home/getBookAbroadLableListByPid' : 'visitor/bookAbroad//getBookAbroadLableListByPid',
method: 'POST',
data: { pid }
})
@@ -80,8 +77,8 @@ export const homeApi = {
* @param lableId 标签ID注意原接口参数名为 lableId
*/
getBooksByLabel(lableId: number) {
return client.request<IBookListResponse>({
url: 'bookAbroad/home/getAbroadBookListByLable',
return skeletonClient.request<IBookListResponse>({
url: uni.getStorageSync('token') ? 'bookAbroad/home/getAbroadBookListByLable' : 'visitor/bookAbroad/getAbroadBookListByLable',
method: 'POST',
data: { lableId }
})
@@ -96,7 +93,7 @@ export const homeApi = {
page: number,
limit: number,
}) {
return client.request<ISearchResponse>({
return mainClient.request<ISearchResponse>({
url: 'bookAbroad/home/searchBook',
method: 'POST',
data

View File

@@ -1,5 +1,5 @@
// api/modules/common.ts
import { mainClient } from '@/api/clients/main'
import { mainClient, skeletonClient } from '@/api/clients'
import type { IApiResponse } from '@/api/types'
import type { IAgreement } from '@/types/user'
@@ -21,7 +21,7 @@ export const commonApi = {
* @param id 协议 ID (111: 用户协议, 112: 隐私政策)
*/
getAgreement: async (id: number) => {
const res = await mainClient.request<IApiResponse<IAgreement>>({
const res = await skeletonClient.request<IApiResponse<IAgreement>>({
url: 'sys/agreement/getAgreement',
method: 'POST',
data: { id }
@@ -36,8 +36,8 @@ export const commonApi = {
* @returns 消息列表
*/
getMessageList(isBook: number, isMedical: number, isSociology: number) {
return mainClient.request<IMessageListResponse>({
url: 'common/message/listByPage',
return skeletonClient.request<IApiResponse>({
url: uni.getStorageSync('token') ? 'common/message/listByPage' : '/visitor/listByPage',
method: 'POST',
data: { isBook, isMedical, isSociology }
})

View File

@@ -1,9 +1,8 @@
// api/modules/course.ts
import { createRequestClient } from '../request'
import { SERVICE_MAP } from '../config'
import { skeletonClient, mainClient } from '@/api/clients/index'
import type { IApiResponse } from '../types'
import type {
ICourseMedicalTreeResponse,
ICourseCategoryResponse,
IUserLateCourseListResponse,
IMarketCourseListResponse,
ICourseDetailResponse,
@@ -15,8 +14,6 @@ import type {
import type { ISearchRequest, ISearchResponse } from '@/types/search'
import type { ICommentListResponse, IAddCommentResponse, IComment } from '@/types/comment'
const client = createRequestClient({ baseURL: SERVICE_MAP.MAIN })
/**
* 课程相关API
*/
@@ -26,7 +23,7 @@ export const courseApi = {
* @returns 分类数据
*/
getCourseMedicalTree() {
return client.request<ICourseMedicalTreeResponse>({
return mainClient.request<ICourseCategoryResponse>({
url: 'medical/home/getCourseMedicalTree',
method: 'POST',
data: {}
@@ -38,7 +35,7 @@ export const courseApi = {
* @returns 观看记录列表
*/
getUserLateCourseList() {
return client.request<IUserLateCourseListResponse>({
return skeletonClient.request<IUserLateCourseListResponse>({
url: 'medical/home/getUserLateCourseList',
method: 'POST',
data: {}
@@ -57,8 +54,8 @@ export const courseApi = {
page: number,
limit: number
}) {
return client.request<IMarketCourseListResponse>({
url: 'medical/home/getMarketCourseList',
return skeletonClient.request<IMarketCourseListResponse>({
url: uni.getStorageSync('token') ? 'medical/home/getMarketCourseList' : 'visitor/getMarketCourseList',
method: 'POST',
data
})
@@ -70,7 +67,7 @@ export const courseApi = {
* @returns 搜索结果
*/
searchData(data: ISearchRequest) {
return client.request<ISearchResponse>({
return mainClient.request<ISearchResponse>({
url: 'bookAbroad/home/searchCourse',
method: 'POST',
data
@@ -82,7 +79,7 @@ export const courseApi = {
* @param id 课程ID
*/
getCourseDetail(id: number) {
return client.request<ICourseDetailResponse>({
return skeletonClient.request<ICourseDetailResponse>({
url: 'sociology/course/getCourseDetail',
method: 'POST',
data: { id }
@@ -94,7 +91,7 @@ export const courseApi = {
* @param id 目录ID
*/
getCatalogueChapterList(id: number) {
return client.request<IChapterListResponse>({
return mainClient.request<IChapterListResponse>({
url: 'sociology/course/getCourseCatalogueChapterList',
method: 'POST',
data: { id }
@@ -106,7 +103,7 @@ export const courseApi = {
* @param id 章节ID
*/
getChapterDetail(id: number) {
return client.request<IChapterDetailResponse>({
return skeletonClient.request<IChapterDetailResponse>({
url: 'sociology/course/getCourseCatalogueChapterDetail',
method: 'POST',
data: { id, load: false }
@@ -118,7 +115,7 @@ export const courseApi = {
* @param catalogueId 目录ID
*/
startStudyForMF(catalogueId: number) {
return client.request<IApiResponse>({
return mainClient.request<IApiResponse>({
url: 'sociology/course/startStudyForMF',
method: 'POST',
data: { catalogueId }
@@ -130,7 +127,7 @@ export const courseApi = {
* @param id 目录ID
*/
getProductListForCourse(id: number) {
return client.request<IProductListResponse>({
return mainClient.request<IProductListResponse>({
url: 'sociology/product/getProductListForCourse',
method: 'POST',
data: { id }
@@ -142,7 +139,7 @@ export const courseApi = {
* @param courseCatalogueId 目录ID
*/
checkRenewPayment(courseCatalogueId: number) {
return client.request<IApiResponse<{ canRelearn: boolean }>>({
return skeletonClient.request<IApiResponse<{ canRelearn: boolean }>>({
url: 'common/courseRelearn/courseCatalogueCanRelearn',
method: 'POST',
data: { courseCatalogueId }
@@ -154,7 +151,7 @@ export const courseApi = {
* @param catalogueId 目录ID
*/
getRenewProductList(catalogueId: number) {
return client.request<IProductListResponse>({
return mainClient.request<IProductListResponse>({
url: 'common/courseRelearn/relearnShopProductList',
method: 'POST',
data: { catalogueId }
@@ -169,7 +166,7 @@ export const courseApi = {
* @param userId 用户ID
*/
getCourseComments(courseId: number, page: number, limit: number, userId: number) {
return client.request<ICommentListResponse>({
return mainClient.request<ICommentListResponse>({
url: 'common/courseGuestbook/getCourseGuestbookList',
method: 'POST',
data: { courseId, page, limit, userId, chapterId: '' }
@@ -190,7 +187,7 @@ export const courseApi = {
content: string
images: string
}) {
return client.request<IAddCommentResponse>({
return mainClient.request<IAddCommentResponse>({
url: 'common/courseGuestbook/addCourseGuestbook',
method: 'POST',
data
@@ -203,7 +200,7 @@ export const courseApi = {
* @param guestbookId 留言ID
*/
likeComment(userId: number, guestbookId: number) {
return client.request<IApiResponse>({
return mainClient.request<IApiResponse>({
url: 'common/courseGuestbook/addCourseGuestbookSupport',
method: 'POST',
data: { userId, guestbookId }
@@ -216,7 +213,7 @@ export const courseApi = {
* @param guestbookId 留言ID
*/
unlikeComment(userId: number, guestbookId: number) {
return client.request<IApiResponse>({
return mainClient.request<IApiResponse>({
url: 'common/courseGuestbook/cancelCourseGuestbookSupport',
method: 'POST',
data: { userId, guestbookId }
@@ -228,7 +225,7 @@ export const courseApi = {
* @param courseId 课程ID
*/
checkCourseVip(courseId: number) {
return client.request<IApiResponse<{ userVip: IVipInfo | null }>>({
return skeletonClient.request<IApiResponse<{ userVip: IVipInfo | null }>>({
url: 'common/userVip/ownCourseCatalogueByVip',
method: 'POST',
data: { courseId }
@@ -240,7 +237,7 @@ export const courseApi = {
* @param courseId 课程ID
*/
getCourseVipModule(courseId: number) {
return client.request<IApiResponse<{ list: string[] }>>({
return skeletonClient.request<IApiResponse<{ list: string[] }>>({
url: 'common/userVip/getCourseVipModule',
method: 'POST',
data: { courseId }

View File

@@ -1,15 +1,10 @@
// api/modules/course.ts
import { createRequestClient } from '../request'
import { SERVICE_MAP } from '../config'
import { mainClient, skeletonClient } from '@/api/clients'
import type {
ICourseCategoryResponse,
IUserLateCourseListResponse,
IMarketCourseListResponse,
ICourseMedicalLabelsResponse
} from '@/types/course'
const client = createRequestClient({ baseURL: SERVICE_MAP.MAIN })
/**
* 课程分类及分类下课程相关API
*/
@@ -20,8 +15,8 @@ export const courseSubjectClassificationApi = {
* @returns 分类数据
*/
getCourseMedicalTree() {
return client.request<ICourseCategoryResponse>({
url: 'medical/home/getCourseMedicalTree',
return skeletonClient.request<ICourseCategoryResponse>({
url: uni.getStorageSync('token') ? 'medical/home/getCourseMedicalTree' : '/visitor/getCourseMedicalTree',
method: 'POST',
data: {}
})
@@ -33,7 +28,7 @@ export const courseSubjectClassificationApi = {
* @returns 分类数据
*/
getCourseSoulTree() {
return client.request<ICourseCategoryResponse>({
return skeletonClient.request<ICourseCategoryResponse>({
url: 'psyche/home/getPsycheLabels',
method: 'POST',
data: { id: 0 }
@@ -46,7 +41,7 @@ export const courseSubjectClassificationApi = {
* @returns 分类数据
*/
getCourseSociologyTree() {
return client.request<ICourseCategoryResponse>({
return skeletonClient.request<ICourseCategoryResponse>({
url: 'sociology/home/getSociologyLabels',
method: 'POST',
data: { id: 0 }
@@ -59,7 +54,7 @@ export const courseSubjectClassificationApi = {
* @returns 分类数据
*/
getCourseMedicalLabels(id: number) {
return client.request<ICourseCategoryResponse>({
return mainClient.request<ICourseCategoryResponse>({
url: 'medical/home/getMedicalLabels',
method: 'POST',
data: { id }
@@ -72,7 +67,7 @@ export const courseSubjectClassificationApi = {
* @returns 分类数据
*/
getCourseMedicalChildLabels(id: number) {
return client.request<ICourseCategoryResponse>({
return mainClient.request<ICourseCategoryResponse>({
url: 'medical/home/getChildCourseMedicalTree',
method: 'POST',
data: { id }
@@ -90,7 +85,7 @@ export const courseSubjectClassificationApi = {
page: number, // 页码
limit: number // 每页数量
}) {
return client.request<IMarketCourseListResponse>({
return mainClient.request<IMarketCourseListResponse>({
url: 'medical/home/getMedicalCourseList',
method: 'POST',
data
@@ -103,7 +98,7 @@ export const courseSubjectClassificationApi = {
* @returns 分类数据
*/
getCourseSoulChildLabels(id: number) {
return client.request<ICourseCategoryResponse>({
return mainClient.request<ICourseCategoryResponse>({
url: 'psyche/home/getChildCoursePsycheTree',
method: 'POST',
data: { id }
@@ -121,7 +116,7 @@ export const courseSubjectClassificationApi = {
page: number, // 页码
limit: number // 每页数量
}) {
return client.request<IMarketCourseListResponse>({
return mainClient.request<IMarketCourseListResponse>({
url: 'psyche/home/getPsycheCourseList',
method: 'POST',
data
@@ -139,7 +134,7 @@ export const courseSubjectClassificationApi = {
page: number, // 页码
limit: number // 每页数量
}) {
return client.request<IMarketCourseListResponse>({
return mainClient.request<IMarketCourseListResponse>({
url: 'sociology/course/getSociologyCourseList',
method: 'POST',
data

View File

@@ -3,7 +3,6 @@ import { mainClient } from '@/api/clients/main'
import type { IApiResponse } from '@/api/types'
import type {
ICreateOrderParams,
IGooglePayVerifyParams,
ICreateOrderResponse,
IOrderGoods,
ICoupon,

View File

@@ -1,5 +1,5 @@
// api/modules/user.ts
import { mainClient } from '@/api/clients/main'
import { mainClient, skeletonClient } from '@/api/clients'
import { paymentClient } from '@/api/clients/payment'
import type { IApiResponse } from '@/api/types'
import type {
@@ -17,7 +17,7 @@ import { SERVICE_MAP } from '@/api/config'
* 获取用户信息
*/
export async function getUserInfo() {
const res = await mainClient.request<IApiResponse<{ user: IUserInfo }>>({
const res = await skeletonClient.request<IApiResponse<{ user: IUserInfo }>>({
url: 'common/user/getUserInfo',
method: 'POST'
})
@@ -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,64 @@ export async function migrateUserData(data: { tel: string, code: string }) {
})
return res
}
/**
* 获取用户迁移信息
* @return {
* alreadyMigration: 已迁移用户数
* notMigration: 未迁移用户数
* }
*/
export async function getUserMigrateInfo() {
const res = await skeletonClient.request<IApiResponse>({
url: 'common/user/getMigrationList',
method: 'POST',
})
return res
}
/**
* 我的湖分
* @return
*/
export async function getUserContributionData() {
const res = await skeletonClient.request<IApiResponse>({
url: 'common/userContribution/getUserContribution',
method: 'POST'
})
return res
}
/**
* 湖分列表
* @param current 当前页码
* @param limit 每页数量
* @param type 湖分类型
* @return
*/
export async function getUserContributionByTypeList(current : number, limit : number, type : string,) {
const res = await mainClient.request<IApiResponse>({
url: 'common/userContribution/getUserContributionByType',
method: 'POST',
data: { current, limit, type, }
})
return res
}
/**
* ios支付
* @param transactionId 支付交易id
* @param productId 商品id
* @param orderId 订单id
* @param receiptData 苹果返回收据
* @param customerOid 用户id
* @return
*/
export async function getIosPayment(transactionId : string, productId : string, orderId : string, receiptData : string, customerOid : string) {
const res = await mainClient.request<IApiResponse>({
url: 'Ipa/veri',
method: 'POST',
data: { transactionId, productId, orderId, receiptData, customerOid}
})
return res
}

View File

@@ -3,8 +3,7 @@ import { createRequestClient } from '../request'
import { SERVICE_MAP } from '../config'
import type { IApiResponse } from '../types'
import type { IVideoCheckResponse } from '@/types/video'
const client = createRequestClient({ baseURL: SERVICE_MAP.MAIN })
import { skeletonClient } from '@/api/clients'
/**
* 视频相关API
@@ -18,7 +17,7 @@ export const videoApi = {
checkVideo(data: {
id: number
}) {
return client.request<IVideoCheckResponse>({
return skeletonClient.request<IVideoCheckResponse>({
url: 'sociology/course/checkVideo',
method: 'POST',
data
@@ -34,7 +33,7 @@ export const videoApi = {
videoId: number
position: number
}) {
return client.request<IApiResponse>({
return skeletonClient.request<IApiResponse>({
url: 'sociology/course/saveCoursePosition',
method: 'POST',
data
@@ -51,7 +50,7 @@ export const videoApi = {
videoId: number
sort: number
}) {
return client.request<IApiResponse>({
return skeletonClient.request<IApiResponse>({
url: 'medical/course/addErrorCourse',
method: 'POST',
data

1
api/types.d.ts vendored
View File

@@ -15,6 +15,7 @@ export interface IApiResponse<T = any> {
*/
export interface IRequestOptions extends UniApp.RequestOptions {
// 允许扩展额外字段(例如 FILE 上传专用等)
headers?: Record<string, string>;
maxSize?: number;
files?: Array<{ name: string; uri: string; fileType?: string }>;
}

View File

@@ -0,0 +1,107 @@
<template>
<view>
<template v-if="loading">
<slot name="loading-top" />
<component v-if="theme" :is="components[theme]" :size="size" :count="count" />
<slot name="loading-bottom" />
</template>
<template v-else>
<slot v-if="data" name="content" :data="data" />
<slot v-else name="error" />
</template>
</view>
</template>
<script lang="ts" setup>
import { ref, onMounted, computed } from 'vue'
import ImageText from './templates/ImageText.vue'
import LineList from './templates/LineList.vue'
import BookCard from './templates/BookCard.vue'
import ImageCard from './templates/ImageCard.vue'
import BookInfo from './templates/BookInfo.vue'
import Menu from './templates/Menu.vue'
// 注册组件
const components = {
'none': () => null,
'image-text': ImageText,
'line-list': LineList,
'image-card': ImageCard,
'book-card': BookCard,
'book-info': BookInfo,
'menu': Menu
}
type RequestFn = () => Promise<any>
type ThemeType = keyof typeof components
const props = withDefaults(defineProps<{
theme?: ThemeType
request: RequestFn | RequestFn[]
auto?: boolean,
size?: 'small' | 'medium' | 'large' | any[]
count?: number
}>(), {
theme: 'none',
auto: true,
count: 1
})
const emit = defineEmits(['success', 'error', 'complete'])
const requestFnType = ref('single')
const requestList = computed<RequestFn[]>(() => {
if (Array.isArray(props.request)) {
requestFnType.value = 'multi'
return props.request
}
requestFnType.value = 'single'
return [props.request]
})
const loading = ref(true)
const data = ref<any>(null)
const run = async (methods?: RequestFn[]) => {
loading.value = true
const reqMethods = methods || requestList.value
try {
// await new Promise(resolve => setTimeout(resolve, 3000))
const results = await Promise.all(
reqMethods.map(fn => fn())
)
// 将每个请求的结果处理成 key-value 格式
const resolvedData: Record<string, any> = {}
reqMethods.forEach((fn, index) => {
const key = fn.name || `request_${index}`
resolvedData[key] = results[index]
})
data.value = requestFnType.value === 'single' ? results[0] : resolvedData
emit('success', data.value)
} catch (err) {
emit('error', err)
} finally {
loading.value = false
}
}
const reload = (methods?: RequestFn[]) => {
run(methods)
}
onMounted(() => {
if (props.auto) {
run()
}
})
defineExpose({
reload,
loading
})
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,15 @@
<template>
<view>
<wd-skeleton :row-col="info" animation="gradient" />
</view>
</template>
<script lang="ts" setup>
const count = 10
const info = Array.from({ length: count }, (_, index) => ({ height: '20px' }))
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,31 @@
<template>
<view>
<wd-skeleton :row-col="info" animation="gradient" />
</view>
</template>
<script lang="ts" setup>
const info = [
{ width: '300rpx', height: '400rpx' },
{ width: '600rpx', height: '60rpx' },
{ width: '180rpx', height: '40rpx' },
[{ width: '180rpx', height: '120rpx' }, { width: '180rpx', height: '120rpx' }, { width: '180rpx', height: '120rpx' }],
{ width: '700rpx', height: '40rpx' },
{ width: '700rpx', height: '40rpx' },
{ width: '700rpx', height: '40rpx' },
{ width: '700rpx', height: '40rpx' }
]
</script>
<style lang="scss" scoped>
:deep(.wd-skeleton__content) {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.wd-skeleton__row {
gap: 20rpx;
}
}
</style>

View File

@@ -0,0 +1,16 @@
<template>
<view>
<wd-skeleton theme="image" :row-col="list" animation="gradient" />
</view>
</template>
<script lang="ts" setup>
const props = defineProps<{
size?: any[],
count: number
}>()
const list = Array.from({ length: props.count }, (_, index) => (props.size || []))
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,17 @@
<template>
<view>
<wd-skeleton :row-col="info" animation="gradient" />
</view>
</template>
<script lang="ts" setup>
const info = [
{ height: '220px' }, 1, 1,
[{ width: '93px' }]
]
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,15 @@
<template>
<view>
<wd-skeleton :row-col="info" animation="gradient" />
</view>
</template>
<script lang="ts" setup>
const count = 10
const info = Array.from({ length: count }, (_, index) => ({ height: '20px' }))
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,33 @@
<template>
<view class="menu-box">
<wd-skeleton :row-col="grid" animation="gradient" />
</view>
</template>
<script lang="ts" setup>
const props = defineProps<{
size?: any[],
count: number
}>()
const list = Array.from({ length: props.count }, (_, index) => (props.size || []))
const grid = [
[
{ width: '96rpx', height: '96rpx' },
{ width: '96rpx', height: '96rpx' },
{ width: '96rpx', height: '96rpx' },
{ width: '96rpx', height: '96rpx' }
]
]
</script>
<style lang="scss" scoped>
.menu-box {
padding: 40rpx 80rpx;
background-color: #f5f5f5;
:deep(.wd-skeleton) {
margin: 0 auto;
}
}
</style>

View File

@@ -67,7 +67,7 @@
<view class="points-input-box">
<input
v-model="pointsDiscounted"
type="number"
type="digit"
clearable
:placeholder="$t('order.pointsPlaceholder')"
class="text-right"
@@ -90,7 +90,7 @@
<text class="label">{{ $t('order.total') }}</text>
<text class="amount">{{ finalAmount }} {{ t('global.coin') }}</text>
</view>
<wd-button type="primary" @click="handleSubmit">
<wd-button type="primary" :loading="submitLoading" @click="handleSubmit">
{{ $t('order.submit') }}
</wd-button>
</view>
@@ -128,6 +128,7 @@ import { getUserInfo } from '@/api/modules/user'
import { useUserStore } from '@/stores/user'
import { t } from '@/utils/i18n'
import type { IGoods, IGoodsDiscountParams } from '@/types/order'
import type { IUserInfo } from '@/types/user'
import PayWay from '@/components/order/PayWay.vue'
const userStore = useUserStore()
@@ -135,14 +136,13 @@ const userStore = useUserStore()
// 使用页面传参
interface Props {
goodsList: IGoods[],
userInfo: object,
userInfo: IUserInfo,
allowPointPay?: boolean,
orderType?: string,
backStep?: number // 购买完成后返回几层页面
}
const props = withDefaults(defineProps<Props>(), {
goodsList: () => [],
userInfo: () => ({}),
allowPointPay: true,
orderType: 'order',
backStep: 1
@@ -270,13 +270,24 @@ const calculatePromotionDiscounted = async () => {
const handlePointsInput = (value: any) => {
let val = String(value.detail.value)
// 允许数字字符,去掉小数点
val = val.replace(/[^0-9]/g, '')
// 允许数字字符小数点
val = val.replace(/[^0-9.]/g, '')
if (val === '0' || val === '') {
// 确保只有一个小数点
const dotIndex = val.indexOf('.')
if (dotIndex !== -1) {
// 限制小数点后最多两位
val = val.substring(0, dotIndex + 1) + val.substring(dotIndex + 1).replace(/\./g, '')
const decimalPart = val.substring(dotIndex + 1)
if (decimalPart.length > 2) {
val = val.substring(0, dotIndex + 1) + decimalPart.substring(0, 2)
}
}
if (val === '' || val === '.') {
pointsDiscounted.value = 0
} else {
let numericValue = parseInt(val, 10)
let numericValue = parseFloat(val)
if (numericValue < 0 || isNaN(numericValue)) {
numericValue = 0
}
@@ -316,7 +327,7 @@ const calculateFinalPrice = () => {
const orderAmountAfterDiscount = totalAmount.value - promotionDiscounted.value - vipDiscounted.value - couponAmount
pointsUsableMax.value = Math.min(
props?.userInfo?.jf || 0,
Math.floor(props.allowPointPay ? orderAmountAfterDiscount : 0)
props.allowPointPay ? orderAmountAfterDiscount : 0
)
pointsDiscounted.value = pointsUsableMax.value
@@ -362,10 +373,13 @@ const validateOrder = (): boolean => {
/**
* 提交订单
*/
const submitLoading = ref<boolean>(false)
const handleSubmit = async () => {
// 验证订单
if (!validateOrder()) return
submitLoading.value = true
try {
// 创建订单 此app用天医币支付创建订单成功即支付成功
await createOrder()
@@ -380,10 +394,15 @@ const handleSubmit = async () => {
// 返回上一页
setTimeout(() => {
submitLoading.value = false
uni.navigateBack({
delta: props.backStep
})
}, 500)
} catch (error) {
submitLoading.value = false
console.error('提交订单失败:', error)
}
}
/**

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

@@ -31,7 +31,7 @@ export function useVideoAPI() {
} catch (err: any) {
console.error('Failed to fetch video info:', err)
error.value = {
type: 'API_ERROR',
type: 'API_ERROR' as VideoErrorType,
message: err.message || '获取视频信息失败,请稍后重试'
}
return null

View File

@@ -145,7 +145,7 @@
"updateFailed": "Update Failed",
"paymentMethod": "Payment Method",
"googlePay": "Google Pay",
"applePay": "Apple Pay",
"applePay": "In-App Purchase",
"agreeText": "I have agreed",
"agreement": "Recharge Agreement",
"agreeFirst": "Please agree to the recharge agreement first",
@@ -223,11 +223,20 @@
"migrateCodePlaceholder": "The code obtained from chinese account",
"migrateWarning": "Migration is irreversible, please proceed with caution!",
"migrateInstructions": "Migration Instructions",
"instruction1": "Please obtain the migration code from your chinese account, get it by going to 【我的】-【数据迁移】-【获取迁移验证码】.",
"instruction2": "After data migration is complete, the chinese account data will be cleared, and all purchased Tianyi Coins, points, courses, E-book, VIP, certificate, and User Contribution will be transferred to the current account",
"instruction1": "Please obtain the migration code from your chinese account, get it by going to 【我的】-【数据迁移】-【查看账号和迁移验证码】.",
"instruction2": "After data migration is complete, the chinese account data will be cleared, and all purchased Tianyi Coins, points, courses, VIP and User Hufen will be transferred to the current account",
"instruction3": "The migration process may take a few minutes, please be patient.",
"instruction4": "If you encounter any issues, please contact customer service for assistance.",
"closeWindow": "Close the payment pop-up window"
"alreadyMigrated": "You have already migrated:",
"notMigration": "This migration:",
"migratedCompleted": "You have completed all migrations:",
"closeWindow": "Close the payment pop-up window",
"certificate": "My certificate",
"iHufen": "My lake",
"hufenRecord": "Lake division record",
"hufen": "Hufen",
"backEnd" : "Back-end recharge",
"cannotView" : "The recharge in the background cannot be viewed"
},
"book": {
"title": "My Books",
@@ -435,8 +444,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

@@ -146,7 +146,7 @@
"updateFailed": "更新失败",
"paymentMethod": "支付方式",
"googlePay": "Google Pay",
"applePay": "Apple Pay",
"applePay": "IAP 支付",
"agreeText": "我已同意",
"agreement": "充值协议",
"agreeFirst": "请先同意充值协议",
@@ -224,11 +224,20 @@
"migrateCodePlaceholder": "国内版账号获取的迁移验证码",
"migrateWarning": "迁移后不可恢复,请谨慎操作!",
"migrateInstructions": "迁移说明",
"instruction1": "请在吴门医述、心灵空间、众妙之门、疯子读书任意APP中获取迁移验证码获取方式【我的】-【数据迁移】-【获取迁移验证码】。",
"instruction2": "数据迁移完成后,旧账号数据将被清空,已购买的天医币、积分、课程、电子书、VIP、证书、湖分将转移到当前账号。",
"instruction1": "请在吴门医述APP中获取迁移验证码获取方式【我的】-【数据迁移】-【查看账号和迁移验证码】。",
"instruction2": "数据迁移完成后,旧账号数据将被清空,已购买的天医币、积分、课程、VIP、湖分将转移到当前账号。",
"instruction3": "迁移过程可能需要几分钟时间,请耐心等待。",
"instruction4": "如遇到问题,请联系客服获取帮助。",
"closeWindow": "关闭支付弹窗"
"alreadyMigrated": "您已迁移过:",
"notMigration": "本次迁移:",
"migratedCompleted": "您已迁移过所有可迁移数据:",
"closeWindow": "关闭支付弹窗",
"certificate": "我的证书",
"iHufen": "我的湖分",
"hufenRecord": "湖分记录",
"hufen": "湖分",
"backEnd" : "后台充值",
"cannotView" : "后台充值无法查看"
},
"book": {
"title": "我的书单",
@@ -435,8 +444,7 @@
"orderCreateFailed": "订单创建失败"
},
"order": {
"selectFuduScheme": "选择复读方案",
"selectPurchaseScheme": "选择购买方案",
"goodsList": "商品列表",
"confirmTitle": "确认订单",
"goodsInfo": "商品信息",
"priceDetail": "价格明细",

View File

@@ -2,8 +2,8 @@
"name" : "吴门国际",
"appid" : "__UNI__1250B39",
"description" : "吴门国际",
"versionName" : "1.0.9",
"versionCode" : 109,
"versionName" : "1.1.10",
"versionCode" : 1110,
"transformPx" : false,
/* 5+App */
"app-plus" : {
@@ -113,7 +113,7 @@
"platforms" : "Android",
"url" : "https://ext.dcloud.net.cn/plugin?id=12608",
"android_package_name" : "com.amazinglimited",
"ios_bundle_id" : "",
"ios_bundle_id" : "com.amazinglimited",
"isCloud" : true,
"bought" : 1,
"pid" : "12608",

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

@@ -3,6 +3,8 @@
{
"path": "pages/course/index",
"style": {
"enablePullDownRefresh": true,
"navigationStyle": "custom",
"navigationBarTitleText": "%index.title%"
}
}, {
@@ -23,7 +25,8 @@
"path": "pages/user/index",
"style": {
"navigationBarTitleText": "%user.title%",
"navigationStyle": "custom"
"navigationStyle": "custom",
"enablePullDownRefresh": true
}
}, {
"path": "pages/user/order/index",
@@ -67,6 +70,24 @@
"navigationBarTitleText": "%user.feedback%",
"navigationStyle": "custom"
}
},{
"path": "pages/user/certificate/index",
"style": {
"navigationBarTitleText": "%user.certificate%",
"navigationStyle": "custom"
}
},{
"path": "pages/user/hufen/index",
"style": {
"navigationBarTitleText": "%user.hufen%",
"navigationStyle": "custom"
}
},{
"path": "pages/user/hufen/forDetails",
"style": {
"navigationBarTitleText": "%user.hufen%",
"navigationStyle": "custom"
}
}, {
"path": "pages/user/recharge/index",
"style": {
@@ -94,6 +115,7 @@
}, {
"path": "pages/book/index",
"style": {
"enablePullDownRefresh": true,
"navigationStyle": "custom",
"navigationBarTitleText": "%book.title%"
}
@@ -219,6 +241,11 @@
"animationDuration": 200
}
}
}, {
"path": "pages/visitor/index",
"style": {
"navigationStyle": "custom"
}
}
],
"tabBar": {

View File

@@ -1,4 +1,11 @@
<template>
<!-- <scroll-view
class="home-page"
scroll-y
refresher-enabled
:refresher-triggered="isRefreshing"
@refresherrefresh="handleRefresh"
> -->
<view class="home-page">
<!-- 顶部背景区域 -->
<view class="home-bg" :style="{ paddingTop: getNotchHeight() + 'px' }">
@@ -12,7 +19,7 @@
/>
<view class="icon-hua">
<image
src="../../static/home_icon.png"
src="../../static/book/home_icon.png"
mode="aspectFit"
class="icon-hua-img"
/>
@@ -24,19 +31,27 @@
<!-- 我的书单 & 推荐图书模块 -->
<view class="mine-block">
<!-- 我的书单 -->
<Skeleton
ref="myBookSkeleton"
theme="image-card"
:request="getMyBooks"
:size="[{ height:'140px' }]"
class="flex-1 w-[50%]"
>
<template #content="{ data }" >
<view class="mine-1">
<text class="mine-title">{{ $t('bookHome.block1') }}</text>
<view
v-if="myBooksList.length > 0"
v-if="data?.page?.records.length > 0"
class="mine-more"
@click="handleMoreClick"
>
{{ $t('bookHome.more') }}
<image src="@/static/icon/icon_right.png" />
</view>
<view v-if="myBooksList.length > 0" class="mine-1-list">
<view v-if="data?.page?.records.length > 0" class="mine-1-list">
<view
v-for="(item, index) in myBooksList"
v-for="(item, index) in data?.page?.records"
:key="index"
class="mine-item"
@click="handleMyBookClick(item.id)"
@@ -47,19 +62,29 @@
</view>
<text v-else class="zanwu">{{ $t('common.data_null') }}</text>
</view>
</template>
</Skeleton>
<!-- 推荐图书 -->
<Skeleton
ref="recommendBooksSkeleton"
theme="image-card"
:request="getRecommendBooks"
:size="[{ height:'140px' }]"
class="flex-1 w-[50%]"
>
<template #content="{ data }" >
<view class="mine-2">
<text class="mine-title">{{ $t('bookHome.block2') }}</text>
<swiper
v-if="recommendBooksList.length > 0"
v-if="data.books.length > 0"
autoplay
:interval="3000"
:duration="500"
class="recommend-list"
>
<swiper-item
v-for="(item, index) in recommendBooksList"
v-for="(item, index) in data.books"
:key="index"
class="recommend-item"
@click="handleBookClick(item.id)"
@@ -69,10 +94,22 @@
</swiper-item>
</swiper>
</view>
</template>
</Skeleton>
</view>
<!-- 活动图书模块 -->
<view v-if="showActivity" class="activity-block">
<view class="activity-block">
<Skeleton
theme="image-card"
:size="Array(5).fill({ height:'28px', width: '18%' })"
:request="getActivityLabels"
@success="getActivityLabelsSuccess"
>
<template #loading-top>
<wd-skeleton :row-col="[{ width: '45%', height: '35px' }]" animation="gradient" class="mb-2.5!" />
</template>
<template #content>
<text class="activity-title">{{ $t('bookHome.activityTitle') }}</text>
<scroll-view class="scroll-view" scroll-x :show-scrollbar="false">
<view class="activity-label-list">
@@ -89,15 +126,26 @@
</view>
</view>
</scroll-view>
</template>
</Skeleton>
<Skeleton
ref="activityBooksSkeleton"
theme="image-card"
:auto="false"
:size="Array(4).fill({ height:'100px', width: '23%' })"
class="mt-[20rpx]!"
:request="getActivityBooks"
>
<template #content="{ data }">
<scroll-view
v-if="activityList.length > 0"
v-if="data.bookList.length > 0"
class="scroll-view"
scroll-x
:show-scrollbar="false"
>
<view class="activity-list">
<view
v-for="(item, index) in activityList"
v-for="(item, index) in data.bookList"
:key="index"
class="activity-item"
@click="handleBookClick(item.bookId)"
@@ -108,10 +156,20 @@
</view>
</scroll-view>
<text v-else class="zanwu" style="padding: 80rpx 0">{{ $t('global.dataNull') }}</text>
</template>
</Skeleton>
</view>
<!-- 分类图书模块 -->
<view v-if="showCategory" class="book-block">
<view class="book-block">
<Skeleton
ref="categoryLevel1LabelSkeleton"
theme="image-card"
:size="Array(3).fill({ height:'75px', width: '32%' })"
:request="getCategoryLabels"
@success="getCategoryLabelsSuccess"
>
<template #content>
<!-- 一级分类标签 -->
<scroll-view class="scroll-view" scroll-x :show-scrollbar="false">
<view class="book-tab-one">
@@ -128,7 +186,18 @@
</view>
</view>
</scroll-view>
</template>
</Skeleton>
<Skeleton
ref="categoryLevel2LabelSkeleton"
theme="image-card"
:auto="false"
:size="Array(4).fill({ height:'30px', width: '24%' })"
class="mt-[20rpx]!"
:request="getSubLabels"
@success="getSubLabelsSuccess"
>
<template #content>
<!-- 二级分类标签 -->
<scroll-view
v-if="categoryLevel2List.length > 0"
@@ -151,11 +220,23 @@
</view>
</view>
</scroll-view>
</template>
</Skeleton>
<!-- 分类图书列表 -->
<view v-if="categoryBookList.length > 0" class="book-list">
<Skeleton
ref="categoryBooksSkeleton"
theme="image-card"
:auto="false"
:size="Array(2).fill({ height:'240px', width: '49%' })"
:count="3"
class="mt-[20rpx]!"
:request="getBooksByLabel"
>
<template #content="{ data }">
<view v-if="data.bookList.length > 0" class="book-list">
<view
v-for="(item, index) in categoryBookList"
v-for="(item, index) in data.bookList"
:key="index"
class="book-item"
@click="handleBookClick(item.bookId)"
@@ -166,47 +247,41 @@
</view>
</view>
<text v-else class="zanwu" style="padding: 100rpx 0">{{ $t('global.dataNull') }}</text>
</template>
</Skeleton>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { homeApi } from '@/api/modules/book_home'
import { ref, computed } from 'vue'
import { onShow, onPullDownRefresh } from '@dcloudio/uni-app'
import { bookHomeApi } from '@/api/modules/book_home'
import { getNotchHeight } from '@/utils/system'
import { useUserStore } from '@/stores/user'
import BookPrice from '@/components/book/BookPrice.vue'
import type {
IBook,
IBookWithStats,
ILabel,
IVipInfo
} from '@/types/book'
import type { ILabel } from '@/types/book'
const userStore = useUserStore()
// 状态定义
const showMyBooks = ref(false)
const showActivity = ref(false)
const showCategory = ref(false)
// 我的书单
const myBooksList = ref<IBook[]>([])
const myBookSkeleton = ref()
// 推荐图书
const recommendBooksList = ref<IBook[]>([])
const recommendBooksSkeleton = ref()
// 活动图书
const activityBooksSkeleton = ref()
const activityLabelList = ref<ILabel[]>([])
const activityList = ref<IBookWithStats[]>([])
const currentActivityIndex = ref(0)
// 分类图书
const categoryLevel1LabelSkeleton = ref()
const categoryLevel2LabelSkeleton = ref()
const categoryBooksSkeleton = ref()
const categoryLevel1List = ref<ILabel[]>([])
const categoryLevel2List = ref<ILabel[]>([])
const categoryBookList = ref<IBookWithStats[]>([])
const currentLevel1Index = ref(0)
const currentLevel2Index = ref(0)
@@ -216,96 +291,62 @@ const vipInfo = computed(() => userStore.userInfo?.userEbookVip?.[0] || null)
/**
* 获取我的书单
*/
const getMyBooks = async () => {
const res = await homeApi.getMyBooks(1, 10)
if (res && res.code === 0) {
showMyBooks.value = true
if (res.page.records && res.page.records.length > 0) {
myBooksList.value = res.page.records
}
} else {
// 未登录,跳转到登录页
uni.navigateTo({
url: '/pages/login/login'
})
}
}
const getMyBooks = () => uni.getStorageSync('token') ? bookHomeApi.getMyBooks(1, 10) : []
/**
* 获取推荐图书
*/
const getRecommendBooks = async () => {
const res = await homeApi.getRecommendBooks()
if (res.books && res.books.length > 0) {
recommendBooksList.value = res.books
}
}
const getRecommendBooks = () => bookHomeApi.getRecommendBooks()
/**
* 获取活动标签列表
* 获取活动图书
*/
const getActivityLabels = async () => {
const res = await homeApi.getBookLabelList(1)
showActivity.value = true
if (res.lableList && res.lableList.length > 0) {
activityLabelList.value = res.lableList
// 默认加载第一个标签的图书列表
await getBooksByLabel(res.lableList[0].id, 'activity')
// 获取活动图书标签列表
const getActivityLabels = () => bookHomeApi.getBookLabelList(1)
const getActivityLabelsSuccess = (res: any) => {
activityLabelList.value = res.lableList || []
activityBooksSkeleton.value.reload()
}
// 获取活动图书列表
const getActivityBooks = () => {
// 获取活动标签列表
const index = currentActivityIndex.value || 0
return bookHomeApi.getBooksByLabel(activityLabelList.value[index].id)
}
/**
* 获取分类标签列表
*/
const getCategoryLabels = async () => {
const res = await homeApi.getBookLabelList(0)
showCategory.value = true
if (res.lableList && res.lableList.length > 0) {
categoryLevel1List.value = res.lableList
// 默认加载第一个标签的二级标签
await getSubLabels(res.lableList[0].id, 0)
const getCategoryLabels = () => bookHomeApi.getBookLabelList(0)
const getCategoryLabelsSuccess = (res: any) => {
categoryLevel1List.value = res.lableList || []
currentLevel2Index.value = 0
categoryLevel2LabelSkeleton.value.reload()
}
}
/**
* 获取二级标签列表
*/
const getSubLabels = async (pid: number, index: number) => {
const res = await homeApi.getSubLabelList(pid)
currentLevel1Index.value = index
if (res.lableList && res.lableList.length > 0) {
categoryLevel2List.value = res.lableList
currentLevel2Index.value = 0
// 加载第一个二级标签的图书列表
await getBooksByLabel(res.lableList[0].id, 'category')
} else {
// 没有二级标签,直接加载一级标签的图书列表
categoryLevel2List.value = []
await getBooksByLabel(pid, 'category')
const getSubLabels = async () => {
await getCategoryLabels()
const pid = categoryLevel1List.value[currentLevel1Index.value]?.id || 0
return bookHomeApi.getSubLabelList(pid)
}
const getSubLabelsSuccess = (res: any) => {
categoryLevel2List.value = res.lableList || []
categoryBooksSkeleton.value.reload()
}
/**
* 根据标签获取图书列表
* 获取分类标签图书列表
*/
const getBooksByLabel = async (
labelId: number,
type: 'activity' | 'category'
) => {
const res = await homeApi.getBooksByLabel(labelId)
if (type === 'activity') {
if (res.bookList && res.bookList.length > 0) {
activityList.value = res.bookList
} else {
activityList.value = []
}
} else {
if (res.bookList && res.bookList.length > 0) {
categoryBookList.value = res.bookList
} else {
categoryBookList.value = []
}
}
const getBooksByLabel = () => {
// 获取当前需要加载的标签id
// 判断是否有一级标签当前选中值
const level1Id = categoryLevel1List.value[currentLevel1Index.value]?.id
// 判断是否有二级标签当前选中值
const level2Id = categoryLevel2List.value[currentLevel2Index.value]?.id
const labelId = level2Id || level1Id || 0
return bookHomeApi.getBooksByLabel(labelId)
}
/**
@@ -330,6 +371,8 @@ const handleMyBookClick = (bookId: number) => {
* 处理图书点击
*/
const handleBookClick = (bookId: number) => {
getPrompt()
if(!uni.getStorageSync('token')) return
uni.navigateTo({
url: `/pages/book/detail?id=${bookId}`
})
@@ -348,15 +391,20 @@ const handleMoreClick = () => {
* 处理活动标签点击
*/
const handleActivityLabelClick = async (labelId: number, index: number) => {
getPrompt()
if(!uni.getStorageSync('token')) return
currentActivityIndex.value = index
await getBooksByLabel(labelId, 'activity')
activityBooksSkeleton.value.reload()
}
/**
* 处理一级分类标签点击
*/
const handleCategoryLevel1Click = async (labelId: number, index: number) => {
await getSubLabels(labelId, index)
currentLevel1Index.value = index
currentLevel2Index.value = 0
categoryBooksSkeleton.value.loading = true
categoryLevel2LabelSkeleton.value.reload()
}
/**
@@ -364,16 +412,54 @@ const handleCategoryLevel1Click = async (labelId: number, index: number) => {
*/
const handleCategoryLevel2Click = async (labelId: number, index: number) => {
currentLevel2Index.value = index
await getBooksByLabel(labelId, 'category')
categoryBooksSkeleton.value.reload()
}
/**
* 页面加载
* 登录提示语
*/
onMounted(() => {
// 重置活动标签选中状态
currentActivityIndex.value = 0
showActivity.value = false
const getPrompt = () => {
if(!uni.getStorageSync('token')) {
uni.showModal({
title: '提示',
content: '请先登录后访问该页面',
confirmText: '去登录',
success: (res) => {
console.log(res , 'res');
if (res.confirm) uni.navigateTo({
url: '/pages/login/login'
});
}
});
}
}
/**
* 刷新页面数据
*/
const handleRefresh = async () => {
try {
// 刷新所有数据
await Promise.all([
myBookSkeleton.value?.reload(),
recommendBooksSkeleton.value?.reload(),
categoryLevel1LabelSkeleton.value?.reload()
])
} catch (error) {
console.error('刷新数据失败:', error)
} finally {
// 延迟关闭刷新状态,避免闪烁
setTimeout(() => {
uni.stopPullDownRefresh();
}, 500)
}
}
/**
* 下拉刷新
*/
onPullDownRefresh(() => {
handleRefresh()
})
/**
@@ -381,10 +467,8 @@ onMounted(() => {
*/
onShow(() => {
// 刷新数据
getMyBooks()
getRecommendBooks()
getActivityLabels()
getCategoryLabels()
myBookSkeleton.value?.reload()
categoryLevel1LabelSkeleton.value?.reload()
})
</script>
@@ -421,17 +505,18 @@ onShow(() => {
.content-wrapper {
padding-bottom: 40rpx;
height: calc(100vh - 240px);
}
.mine-block {
padding: 20rpx;
display: flex;
gap: 20rpx;
align-items: center;
justify-content: space-between;
.mine-1,
.mine-2 {
width: 49%;
height: 290rpx;
border-radius: 15rpx;
padding: 30rpx;
@@ -584,7 +669,6 @@ onShow(() => {
.activity-list {
width: 100%;
margin-top: 20rpx;
padding-left: 10rpx;
display: flex;
align-items: center;
@@ -617,7 +701,6 @@ onShow(() => {
.book-block {
padding: 20rpx 20rpx 0;
.book-tab-one {
display: flex;
align-items: center;
@@ -700,7 +783,6 @@ onShow(() => {
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
margin-top: 20rpx;
.book-item {
width: 49%;

View File

@@ -41,7 +41,7 @@
import { ref, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import { homeApi } from '@/api/modules/book_home'
import { bookHomeApi } from '@/api/modules/book_home'
import BookPrice from '@/components/book/BookPrice.vue'
import type { IBookWithStats, IVipInfo } from '@/types/home'
@@ -66,7 +66,7 @@ onLoad((options: any) => {
* 获取VIP信息
*/
const getVipInfo = async () => {
const res = await homeApi.getVipInfo()
const res = await bookHomeApi.getVipInfo()
vipInfo.value = res.vipInfo
}
@@ -81,7 +81,7 @@ const handleSearch = async () => {
loading.value = true
isEmpty.value = false
const res = await homeApi.searchBooks({
const res = await bookHomeApi.searchBooks({
title: keyword.value.trim(),
page: 1,
limit: 10,

View File

@@ -6,8 +6,9 @@
<!-- 页面内容 -->
<view class="page-content">
<!-- 视频播放器 -->
<view v-if="videoList.length > 0" class="video-section">
<view class="video-section">
<VideoPlayer
v-if="videoList.length > 0"
ref="videoPlayerRef"
v-model:current-index="currentVideoIndex"
:video-list="videoList"
@@ -32,7 +33,7 @@
<view class="section-title">{{ $t('courseDetails.videoTeaching') }}</view>
<wd-radio-group v-model="currentVideoIndex" shape="button" >
<wd-radio v-for="(video, index) in videoList" :key="video.id" :value="index" class="mb-2!">
{{ video.type == "2" ? $t('courseDetails.audio') : $t('courseDetails.video') }}{{ index + 1 }}
{{ video.type == 2 ? $t('courseDetails.audio') : $t('courseDetails.video') }}{{ index + 1 }}
</wd-radio>
</wd-radio-group>
</view>
@@ -74,7 +75,8 @@ import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { courseApi } from '@/api/modules/course'
import VideoPlayer from '@/components/video-player/index.vue'
import type { IChapterDetail, IVideo } from '@/types/course'
import type { IChapterDetail } from '@/types/course'
import type { IVideoInfo } from '@/types/video'
// 页面参数
const chapterId = ref<number>(0)
@@ -83,7 +85,7 @@ const chapterTitle = ref('')
// 页面数据
const chapterDetail = ref<IChapterDetail | null>(null)
const videoList = ref<IVideo[]>([])
const videoList = ref<IVideoInfo[]>([])
const currentVideoIndex = ref(0)
const activeVideoIndex = ref(0)
const currentTab = ref('chapterIntro')
@@ -141,6 +143,7 @@ const previewImage = (url: string) => {
.video-section {
background-color: #000;
height: 400rpx;
}
.info-section {

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,89 @@
<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
}>()
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

@@ -31,6 +31,14 @@
>{{ item }}</view>
</view>
<!-- 医学 -->
<Skeleton
v-if="selectedFirstLevel === '医学'"
ref="medicineMenuSkeletonRef"
theme="menu"
:request="getMedicineMenus"
@success="getMedicineMenusSuccess"
>
<template #content>
<view v-if="selectedFirstLevel === '医学'" class="newLeve2">
<view class="home_nar nomargin" style="padding: 0; background-color: #fff">
<view class="flexbox">
@@ -70,41 +78,40 @@
</view>
</view>
</view>
<!-- 心理学 -->
<view v-if="selectedFirstLevel === '心理学'" class="soul_cate_box">
<scroll-view scroll-x="true">
<view class="cate_list" v-if="soulCateList.length > 0">
</template>
</Skeleton>
<!-- 心理学和国学 -->
<Skeleton
v-if="selectedFirstLevel === '心理学' || selectedFirstLevel === '国学'"
ref="menuSkeletonRef"
theme="menu"
:request="getMenus"
>
<template #content="{ data }">
<view
v-if="data.labels.length > 0"
:class="[
{'sociology_cate_box': selectedFirstLevel === '国学'},
{'soul_cate_box': selectedFirstLevel === '心理学'}
]"
>
<view
class="cate_item_box"
v-for="(item, index) in soulCateList"
:key="index"
v-for="item in data.labels"
@click="curseClickJump(item)"
>
<view class="cate_item_border">
<image :src="item.icon"></image>
</view>
<view class="cate_item_name">{{ item.title }}</view>
</view>
</view>
</scroll-view>
</view>
<!-- 国学 -->
<view v-if="selectedFirstLevel === '国学' && sociologyCateList.length > 0" class="sociology_cate_box">
<view
class="cate_item_box"
v-for="(v, i) in sociologyCateList"
@click="curseClickJump(v)"
>
<view class="cate_item_border">
<image
:src="v.icon"
:src="item.icon"
mode="aspectFill"
style="width: 49rpx; height: 49rpx"
></image>
</view>
<view class="cate_item_name">{{ v.title }}</view>
<view class="cate_item_name">{{ item.title }}</view>
</view>
</view>
</template>
</Skeleton>
<!-- 观看记录区域 -->
<view class="learnBox" v-if="learnList.length > 0">
@@ -143,7 +150,7 @@
<view class="newscoll">
<swiper
class="swiper"
interval="5000"
:interval="5000"
circular
autoplay
vertical
@@ -163,15 +170,23 @@
</view>
<!-- 精彩试听区域 -->
<view class="learnBox" v-if="tryListenList.length > 0">
<view class="learnBox">
<view class="titleBox flexbox">
<image src="../../static/course/try_listen.png" mode="aspectFit"></image>
<text>{{ $t('courseHome.tryListen') }}</text>
</view>
<view class="learn flexbox shiting">
<Skeleton
theme="image-card"
class=""
:size="Array(2).fill({ width: '48%', height: '260rpx' })"
:count="3"
:request="getTryListenList"
>
<template #content="{ data }">
<view v-if="data.courseList.records.length > 0" class="learn flexbox shiting">
<view
class="item"
v-for="(item, index) in tryListenList"
v-for="(item, index) in data.courseList.records"
:key="item.id"
@click="onPageJump('/pages/course/details/course', item.id, item.title)"
>
@@ -197,26 +212,31 @@
<view class="moreBox shiting">
<text @click="onPageJump('/pages/course/list/tryListen', 1, $t('courseHome.tryListen'))">{{ $t('courseHome.moreTryListen') }}</text>
</view>
</template>
</Skeleton>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { onShow, onHide, onPullDownRefresh, onPageScroll, onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import { onShow, onHide, onPullDownRefresh, onPageScroll } from '@dcloudio/uni-app'
import { courseApi } from '@/api/modules/course'
import { courseSubjectClassificationApi } from '@/api/modules/cousre_subject_classification'
import { commonApi } from '@/api/modules/common'
import { getNotchHeight } from '@/utils/system'
// import { onPageJump } from '@/utils'
import type { IMedicalTag, ICourse, INews } from '@/types/course'
import type { ICategory, ICourse, INews } from '@/types/course'
import { useUserStore } from '@/stores/user'
const { t } = useI18n()
const userStore = useUserStore()
// 系统信息
const scrollTop = ref<number>(0) // 滚动位置
// 下拉刷新状态
const isRefreshing = ref(false)
/**
* 处理搜索点击
*/
@@ -227,38 +247,25 @@ const handleSearch = ({ value }: { value: string }) => {
}
// 分类相关
const curseTagList = ref<IMedicalTag[]>([]) // 一级分类标签列表
const sbuMedicalTagsList = ref<IMedicalTag[]>([]) // 二级分类标签列表
const curseTagList = ref<ICategory[]>([]) // 一级分类标签列表
const sbuMedicalTagsList = ref<ICategory[]>([]) // 二级分类标签列表
const currentIndex = ref<number>(0) // 当前选中的一级分类索引
const currentItem = ref<IMedicalTag | null>(null) // 当前选中的一级分类项
const currentItem = ref<ICategory | null>(null) // 当前选中的一级分类项
// 学科数据
const firstLevelCategories = ref<string[]>(['医学', '心理学', '国学'])
const selectedFirstLevel = ref<number>('医学') // 当前选中的一级分类索引
/**
* 学科点击处理
*/
const handleFirstLevelClick = (item: string) => {
selectedFirstLevel.value = item
switch (item) {
case '医学':
getMedicalTags()
break
case '心理学':
getSoulCateList()
break
case '国学':
getSociologyCateList()
break
}
}
const selectedFirstLevel = ref('医学') // 当前选中的一级分类索引
/**
* 医学
* 获取课程分类数据
*/
const getMedicalTags = async () => {
const medicineMenuSkeletonRef = ref()
const getMedicineMenus = () => {
sbuMedicalTagsList.value = []
const res = await courseSubjectClassificationApi.getCourseMedicalTree()
return courseSubjectClassificationApi.getCourseMedicalTree()
}
const getMedicineMenusSuccess = (res: any) => {
if (res && res.code === 0) {
if (res.labels && res.labels.length > 0) {
curseTagList.value = res.labels
@@ -281,7 +288,7 @@ const getMedicalTags = async () => {
* 医学
* 一级分类点击处理
*/
const curseClick = (item: IMedicalTag, index: number) => {
const curseClick = (item: ICategory, index: number) => {
currentItem.value = item
currentIndex.value = index
@@ -299,33 +306,54 @@ const curseClick = (item: IMedicalTag, index: number) => {
}
/**
* 心理学
* 菜单
* 获取课程分类数据
*/
const soulCateList = ref<IMedicalTag[]>([])
const getSoulCateList = async () => {
const res = await courseSubjectClassificationApi.getCourseSoulTree()
if (res.labels&&res.labels.length>0) {
soulCateList.value = res.labels;
const menuSkeletonRef = ref()
const getMenus = () => {
switch (selectedFirstLevel.value) {
// case '医学':
// return getMedicalTags()
case '心理学':
return courseSubjectClassificationApi.getCourseSoulTree()
case '国学':
return courseSubjectClassificationApi.getCourseSociologyTree()
}
}
/**
*
* 获取课程分类数据
* 学科点击处理
*/
const sociologyCateList = ref<IMedicalTag[]>([])
const getSociologyCateList = async () => {
const res = await courseSubjectClassificationApi.getCourseSociologyTree()
if (res.labels&&res.labels.length>0) {
sociologyCateList.value = res.labels;
const handleFirstLevelClick = (item: string) => {
getPrompt()
if(!uni.getStorageSync('token')) return
selectedFirstLevel.value = item
if (item === '医学') {
medicineMenuSkeletonRef.value?.reload()
} else {
menuSkeletonRef.value?.reload()
}
// switch (item) {
// case '医学':
// getMedicalTags()
// break
// case '心理学':
// getSoulCateList()
// break
// case '国学':
// getSociologyCateList()
// break
// }
}
/**
* 终极分类点击处理
*/
const curseClickJump = (item: IMedicalTag) => {
const curseClickJump = (item: ICategory) => {
getPrompt()
if(!uni.getStorageSync('token')) return
uni.navigateTo({
url: `/pages/course/list/category?id=${item.id}&title=${item.title}&pid=${item.pid}&subject=${selectedFirstLevel.value}`
})
@@ -335,6 +363,8 @@ const curseClickJump = (item: IMedicalTag) => {
* 页面跳转统一处理
*/
const onPageJump = (url: string, id?: number, title?: string) => {
getPrompt()
if(!uni.getStorageSync('token')) return
let targetUrl = url
if (id !== undefined) {
targetUrl += `?id=${id}`
@@ -381,28 +411,37 @@ const getNewsList = async () => {
* 新闻点击处理
*/
const newsClick = (item: INews) => {
getPrompt()
if(!uni.getStorageSync('token')) return
uni.navigateTo({
url: `/pages/news/details?newsId=${item.id}&url=${item.url}&type=${item.type}`
})
}
// 精彩试听
const tryListenList = ref<ICourse[]>([]) // 试听课程列表
/**
* 获取试听课程列表
*/
const getTryListenList = async () => {
const res = await courseApi.getMarketCourseList({
page: 1,
limit: 6,
id: 1
})
if (res && res.code === 0) {
if (res.courseList && res.courseList.records && res.courseList.records.length > 0) {
tryListenList.value = res.courseList.records
} else {
tryListenList.value = []
const getTryListenList = () => courseApi.getMarketCourseList({ page: 1, limit: 6, id: 1 })
/**
* 登录提示语
*/
const getPrompt = () => {
console.log(userStore.token);
if(!uni.getStorageSync('token')) {
uni.showModal({
title: '提示',
content: '请先登录后访问该页面',
confirmText: '去登录',
success: (res) => {
console.log(res, 'res');
if (res.confirm) uni.navigateTo({
url: '/pages/login/login'
});
}
});
}
}
@@ -410,10 +449,34 @@ const getTryListenList = async () => {
* 统一请求所有数据
*/
const requestAll = async () => {
if(uni.getStorageSync('token')){
getLearnCourse()
getMedicalTags()
}
getTryListenList()
getNewsList()
// 刷新分类数据
if (selectedFirstLevel.value === '医学') {
medicineMenuSkeletonRef.value?.reload()
} else {
menuSkeletonRef.value?.reload()
}
}
/**
* 处理下拉刷新
*/
const handleRefresh = async () => {
try {
// 刷新所有数据
await requestAll()
} catch (error) {
console.error('刷新数据失败:', error)
} finally {
// 延迟关闭刷新状态,避免闪烁
setTimeout(() => {
uni.stopPullDownRefresh()
}, 500)
}
}
/**
@@ -422,7 +485,12 @@ const requestAll = async () => {
onMounted(() => {
// 重置分类索引
currentIndex.value = 0
if(!uni.getStorageSync('state') && !uni.getStorageSync('token')) {
uni.navigateTo({
url: '/pages/login/login'
});
}
if(uni.getStorageSync('state')) uni.removeStorageSync('state')
// 请求所有数据
requestAll()
})
@@ -432,16 +500,19 @@ onMounted(() => {
*/
onShow(() => {
// 检查是否有固定的分类选择状态
const fixed = uni.getStorageSync('fixed')
if (fixed && currentItem.value) {
curseClick(currentItem.value, currentIndex.value)
} else {
currentIndex.value = 0
currentItem.value = null
}
// const fixed = uni.getStorageSync('fixed')
// if (fixed && currentItem.value) {
// curseClick(currentItem.value, currentIndex.value)
// } else {
// currentIndex.value = 0
// currentItem.value = null
// }
// 刷新数据
requestAll()
// requestAll()
if(uni.getStorageSync('token')){
getLearnCourse()
}
})
/**
@@ -456,15 +527,13 @@ onHide(() => {
* 下拉刷新
*/
onPullDownRefresh(() => {
requestAll().then(() => {
uni.stopPullDownRefresh()
})
handleRefresh()
})
/**
* 页面滚动
*/
onPageScroll((e) => {
onPageScroll((e: any) => {
scrollTop.value = e.scrollTop
})
</script>
@@ -481,7 +550,7 @@ $text-placeholder: #999999;
$border-color: #eeeeee;
.course-home-page {
min-height: 100vh;
height: 100vh;
background-color: $bg-color;
font-size: 28upx;
}
@@ -634,11 +703,9 @@ $border-color: #eeeeee;
border-radius: 10rpx;
margin: 0 10rpx;
.cate_list {
display: flex;
align-items: center;
justify-content: space-around;
.cate_item_box {
width: 20%;
padding: 40rpx 0 30rpx;
@@ -648,7 +715,7 @@ $border-color: #eeeeee;
width: 60rpx;
height: 60rpx;
background-size: 100% 100%;
background-image: url("@/static/soul/cate_bg.png");
background-image: url("@/static/icon/cate_bg.png");
border-radius: 4rpx;
display: flex;
align-items: center;
@@ -669,6 +736,13 @@ $border-color: #eeeeee;
color: #fff;
font-weight: bold;
}
.child-menu {
display: none;
background-color: #000;
box-shadow: 0 4rpx 12rpx rgba(51, 97, 165, 0.08);
border-radius: 0 0 20rpx 20rpx;
padding: 10rpx 0;
}
}
}
@@ -702,7 +776,7 @@ $border-color: #eeeeee;
width: 65rpx;
height: 78rpx;
background-size: 100% 100%;
background-image: url("@/static/soul/cate_bg.png");
background-image: url("@/static/icon/cate_bg.png");
border-radius: 4rpx;
display: flex;
align-items: center;
@@ -850,7 +924,6 @@ $border-color: #eeeeee;
.learn {
justify-content: space-between;
margin-top: 20rpx;
flex-wrap: wrap;
.item {
@@ -877,6 +950,7 @@ $border-color: #eeeeee;
.titleBox {
align-items: center;
margin-bottom: 20rpx;
image {
width: 50rpx;

View File

@@ -11,7 +11,12 @@
<!-- 子级分类 -->
<view v-if="selectedTab && selectedTab.children && selectedTab.children.length > 0" class="sub-category-list">
<view v-for="child in selectedTab.children" :key="child.id" :class="{'active': child.id === radio_category_id}" class="sub-category-child-item">{{ child.title }}</view>
<view
v-for="child in selectedTab.children"
:key="child.id" :class="{'active': child.id === radio_category_id}"
class="sub-category-child-item"
@click="changeRadioCategoryId(child.id)"
>{{ child.title }}</view>
</view>
<!-- 课程列表 -->
@@ -86,6 +91,17 @@ const changeCategory = ({ name }: { name: number}) => {
radio_category_id.value = selectedTab.value && selectedTab.value.children[0] && selectedTab.value.children[0].id || 0
}
/**
* 子级分类切换
*/
const changeRadioCategoryId = (id: number) => {
radio_category_id.value = id
// 重新加载分页数据
paging.value && paging.value.reload();
}
// 分类下的课程列表
const courseList = ref<ICourse[]>([]) // 课程列表
const selectedCategoryId = computed(() => {

139
pages/index.vue Normal file
View File

@@ -0,0 +1,139 @@
<template>
<div class="menu-container">
<!-- 一级导航 -->
<div class="menu-level-1">
<div
v-for="item in level1"
:key="item.id"
class="menu-item"
:class="{ active: selectedParentId === item.id }"
@click="selectParent(item)"
>
{{ item.name }}
</div>
</div>
<!-- 二级导航区域 -->
<transition name="fade">
<div
v-if="childList.length"
class="menu-level-2"
:style="{ gridRowStart: childRowIndex }"
>
<div
v-for="child in childList"
:key="child.id"
class="menu-item child"
>
{{ child.name }}
</div>
</div>
</transition>
</div>
</template>
<script setup>
import { ref, computed } from "vue"
// 模拟接口返回的一级导航
const level1 = ref([
{ id: 1, name: "导航1", children: [] },
{ id: 2, name: "导航2", children: [
{ id: 21, name: "子2-1" },
{ id: 22, name: "子2-2" },
{ id: 23, name: "子2-3" }
]},
{ id: 3, name: "导航3", children: [] },
{ id: 4, name: "导航4", children: [
{ id: 41, name: "子4-1" },
{ id: 42, name: "子4-2" }
]},
{ id: 5, name: "导航5", children: [] },
{ id: 6, name: "导航6", children: [
{ id: 61, name: "子6-1" },
{ id: 62, name: "子6-2" },
{ id: 63, name: "子6-3" },
{ id: 64, name: "子6-4" },
]},
{ id: 7, name: "导航7", children: [] },
{ id: 8, name: "导航8", children: [] }
])
// 选择的一级导航
const selectedParentId = ref(null)
// 当前一级导航的子级
const childList = computed(() => {
const parent = level1.value.find(i => i.id === selectedParentId.value)
return parent ? parent.children : []
})
// 计算二级导航应该显示在哪一行(每行 4 个)
const childRowIndex = computed(() => {
if (!selectedParentId.value) return 3
const index = level1.value.findIndex(i => i.id === selectedParentId.value)
return Math.floor(index / 4) + 2 // 第一行 row=1二级导航从 row=2 或 row=3
})
const selectParent = (item) => {
// 点击同一个时关闭
if (selectedParentId.value === item.id) {
selectedParentId.value = null
} else {
selectedParentId.value = item.id
}
}
</script>
<style scoped>
.menu-container {
display: grid;
grid-template-rows: auto auto auto;
gap: 10px;
}
/* 一级导航4列布局 */
.menu-level-1 {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.menu-item {
padding: 12px;
text-align: center;
background: #4a90e2;
color: white;
border-radius: 6px;
cursor: pointer;
}
.menu-item.active {
background: #2d73c7;
}
/* 二级导航:自动换行 */
.menu-level-2 {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
background: #2d73c7;
padding: 10px;
border-radius: 6px;
}
.menu-item.child {
background: #1e4f8a;
}
/* 动画 */
.fade-enter-active, .fade-leave-active {
transition: all .2s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
transform: translateY(-5px);
}
</style>

View File

@@ -1,6 +1,8 @@
<template>
<view class="page">
<view class="title" :style="{ 'margin-top': getNotchHeight() + 'px' }">{{ $t('forget.title') }}</view>
<!-- 自定义导航栏 -->
<nav-bar :title="$t('forget.title')" />
<!-- <view class="title" :style="{ 'margin-top': getNotchHeight() + 'px' }">{{ $t('forget.title') }}</view> -->
<!-- 邮箱输入 -->
<view class="input-box">
@@ -33,7 +35,7 @@
<input
class="input-text"
type="password"
maxlength="20"
:maxlength="20"
v-model="password"
:placeholder="$t('forget.passwordPlaceholder')"
@input="inputMethod(password)"
@@ -54,8 +56,8 @@
<input
class="input-text"
type="password"
minlength="8"
maxlength="20"
:minlength="8"
:maxlength="20"
v-model="confirmPassword"
:placeholder="$t('forget.passwordAgainPlaceholder')"
/>

View File

@@ -34,7 +34,7 @@
v-model="code"
:placeholder="$t('login.codePlaceholder')"
placeholder-class="grey"
maxlength="6"
:maxlength="6"
@confirm="onSubmit"
/>
<wd-button type="info" :class="['code-btn', { 'active': !readonly }]" @click="onSetCode">
@@ -115,11 +115,11 @@
</view>
<!-- 游客体验 -->
<!-- <view class="youke-l">
<view @click="onPageJump('/pages/visitor/visitor')">
<view class="youke-l" v-if="!isAndorid">
<view @click="onPageSwitch('/pages/course/index')">
{{ $t('login.noLogin') }}
</view>
</view> -->
</view>
</view>
<!-- 用户协议弹窗 -->
@@ -147,6 +147,7 @@ import { loginWithCode, loginWithPassword } from '@/api/modules/auth'
import { commonApi } from '@/api/modules/common'
import { validateEmail } from '@/utils/validator'
import { onPageJump } from '@/utils'
import { ILoginResponse } from '@/types/user'
import { t } from '@/utils/i18n'
@@ -184,6 +185,7 @@ let codeTimer: any = null
// 提交点击次数
const submitClickNum = ref(0)
const isAndorid = ref(false)
/**
* 切换登录方式
@@ -302,14 +304,14 @@ const passwordLogin = async () => {
const onSubmit = async () => {
if(!isAgree()) return false
let res = null
let res: ILoginResponse | null = null
switch (loginType.value) {
case 2000:
res = await verifyCodeLogin()
res = await verifyCodeLogin() as ILoginResponse
break
case 1000:
res = await passwordLogin()
res = await passwordLogin() as ILoginResponse
break
}
@@ -398,8 +400,9 @@ const yszc = () => {
/**
* 页面跳转
*/
const onPageJump = (url: string) => {
uni.navigateTo({
const onPageSwitch = (url: string) => {
uni.setStorageSync('state', 'true');
uni.switchTab({
url: url,
})
}
@@ -437,7 +440,17 @@ const agreeAgreements = () => {
uni.setStorageSync('Agreements_agreed', agree.value);
}
/**
* 判断当前系统
*/
const getOS = () =>{
const oprateOs = uni.getSystemInfoSync().platform
console.log(oprateOs, 'oprateOs');
isAndorid.value = oprateOs === "android" ? true : false
}
onMounted(() => {
getOS()
loadAgreements()
})
</script>

View File

@@ -4,7 +4,7 @@
<nav-bar :title="$t('order.confirmTitle')" />
<!-- 确认订单组件 -->
<Confirm :goodsList="goodsList" :userInfo="userInfo" :orderType="orderType">
<Confirm :goodsList="goodsList" :userInfo="userInfo" :orderType="orderType" :allow-point-pay="allowPointPay">
<template #goodsList>
<!-- 商品列表内容 -->
<view
@@ -86,20 +86,31 @@ const isRelearn = ref<boolean>(false)
const orderType = computed(() => {
return isRelearn.value ? 'relearn' : 'order'
})
// 是否允许积分支付
const allowPointPay = ref<boolean>(false)
/**
* 页面加载
*/
onLoad(async (options: any) => {
if (options.goods) {
try {
allowPointPay.value = options.allowPointPay !== '0'
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 +118,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

@@ -7,7 +7,7 @@
<!-- 应用信息 -->
<view class="app-info">
<image
src="/static/icon/home_icon_logo.jpg"
src="/static/logo.png"
mode="aspectFit"
class="app-logo"
/>
@@ -52,11 +52,9 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { t } from '@/utils/i18n'
import { makePhoneCall } from '@/utils/index'
const { t } = useI18n()
// 导航栏高度
const statusBarHeight = ref(0)
const navbarHeight = ref('44px')

View File

@@ -0,0 +1,134 @@
<template>
<view class="certificate-page">
<nav-bar :title="$t('user.certificate')"></nav-bar>
<view v-if="certificateList.length > 0">
<view style="margin: 10rpx;" >{{certificateList.length}}个证书</view>
<view class="certificate-list" v-for="(item,index) in certificateList" :key="index">
<view class="certificate-list-row">
<h3>证书编号{{item.bh}}</h3>
<text style="font-size: 26rpx; color: #999;">获得时间{{item.time}}</text>
</view>
<view class="certificate-certificate">
<view class="img" v-for="(i,index) in item.certificateUrl" :key="index">
<image @click="preveImg(i.url)" :src="i.url" mode="heightFix"></image>
</view>
<view class="certificate-detailed" @click="detailed(item)">详细信息</view>
</view>
</view>
</view>
<view v-else><wd-divider>您还未获得证书</wd-divider></view>
</view>
<wd-popup v-model="detailedState" position="bottom" :closeable="true">
<view class="detailed">
<view class="detailed-text">
证书详情
</view>
<view class="detailed-row">证书类型<text class="text">{{detailedData.a}}</text></view>
<view class="detailed-row">获得时间<text class="text">{{detailedData.time}}</text></view>
<view class="detailed-row">获得途径<text class="text">{{detailedData.b}}</text></view>
</view>
</wd-popup>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const detailedState = ref(false)
const detailedData = ref({})
// 模拟的 certificateList 数据
const certificateList = ref([
{ a: 'ZH', b: '吴门', bh: 1, time: '2025-6-20', certificateUrl: "https://ehh-private-01.oss-cn-beijing.aliyuncs.com/certificate/ca2140c3-d212-4d4e-9203-ddc161d50470.jpg,https://ehh-private-01.oss-cn-beijing.aliyuncs.com/certificate/18a7ea22-b75a-4ef6-9109-f448f45e424f.jpg" },
{ bh: 2, time: '2025-6-22', certificateUrl: "https://ehh-private-01.oss-cn-beijing.aliyuncs.com/certificate/ca2140c3-d212-4d4e-9203-ddc161d50470.jpg,https://ehh-private-01.oss-cn-beijing.aliyuncs.com/certificate/18a7ea22-b75a-4ef6-9109-f448f45e424f.jpg" }
]);
// 重新获取数据
certificateList.value = certificateList.value.map(item => {
return { ...item, certificateUrl: item.certificateUrl.split(',').map(url => ({ url })) };
});
/**
* 查看证书详情信息
*/
const detailed = (item) => {
detailedState.value = true
detailedData.value = item
}
/**
* 查看照片
*/
const preveImg = (url : any) => {
uni.previewImage({
urls: [url],
current: 0
});
}
</script>
<style lang="scss" scoped>
.certificate-page {
min-height: 100vh;
background-color: #f7faf9;
}
.certificate-list {
background: #fff;
border-radius: 15rpx;
overflow: hidden;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
margin: 20rpx;
padding: 20rpx;
}
.certificate-list-row {
display: flex;
justify-content: space-between;
}
.certificate-certificate {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 20rpx;
.img {
width: 36%;
overflow: hidden;
height: 300rpx;
image {
width: 100%;
height: 100%;
}
}
.certificate-detailed {
color: #55aaff;
border: #55aaff 1px solid;
padding: 10rpx 20rpx;
border-radius: 15rpx;
}
}
.detailed {
padding: 20rpx;
.detailed-text {
text-align: center;
margin-bottom: 40rpx;
}
.detailed-row {
color: #999;
margin: 20rpx 0;
font-size: 26rpx;
.text {
color: #000;
}
}
}
</style>

View File

@@ -0,0 +1,113 @@
<template>
<z-paging ref="paging" v-model="bookList" auto-show-back-to-top class="my-book-page" @query="hufenList"
:default-page-size="10">
<template #top>
<!-- 自定义导航栏 -->
<nav-bar :title="$t('user.hufenRecord')"></nav-bar>
</template>
<view class="recharge-record" v-if="(bookList && bookList.length > 0)">
<view class="go-gecharge">{{hufenData.nameValue}} {{$t('user.hufenRecord')}}</view>
<view class="recharge-record-block" v-for="(item, index) in bookList" :key="index">
<view class="recharge-record-block-row">{{item.createTime}}<text class="text">{{item.score}}</text>
</view>
<view class="time">{{item.detail}}</view>
</view>
</view>
</z-paging>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user'
import { getUserContributionByTypeList } from '@/api/modules/user'
import { copyToClipboard } from '@/utils/index'
const { t } = useI18n()
const paging = ref<any>()
const userStore = useUserStore()
// 数据状态
const bookList = ref([])
const loading = ref(false)
const firstLoad = ref(true)
const hufenData = ref('')
// 湖分记录
async function hufenList(pageNo : number, pageSize : number) {
loading.value = true
try {
const res = await getUserContributionByTypeList(pageNo, pageSize, hufenData.value.type)
console.log(res, 'res');
paging.value.complete(res.list.records)
} catch (error) {
paging.value.complete(false)
console.error('Failed to load book list:', error)
} finally {
firstLoad.value = false
loading.value = false
}
}
onLoad((options) => {
hufenData.value = options
console.log(hufenData);
});
</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 {
text-align: center;
background: linear-gradient(to right, #007bff, #17a2b8);
font-size: 30rpx;
font-weight: bold;
color: #fff;
padding: 20rpx;
margin-bottom: 20rpx;
}
.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: 24rpx;
margin-bottom: 20rpx;
color: #343434
}
.recharge-record-block-row {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
// font-weight: 700;
color: #909090;
.text {
color: #007bff;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<view class="recharge-page">
<nav-bar :title="$t('user.iHufen')"></nav-bar>
<view class="menu-section" v-if="hufenList.list.length > 0">
<wd-cell-group border class="menu-list">
<wd-cell v-for="item in hufenList.list" :key="item.type" :title="item.dict_value" is-link
@click="handleMenuClick(item)">
<text class="menu-list-hufen">{{item.score}}</text><text class="menu-list-hufen-text">{{$t('user.hufen')}}</text>
</wd-cell>
</wd-cell-group>
</view>
<view v-else><wd-divider>您还未获得湖分</wd-divider></view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { getUserContributionData } from '@/api/modules/user'
const { t } = useI18n()
const hufenList = ref([])
/**
* 获取用户湖分
*/
const getHufen = async () => {
hufenList.value = await getUserContributionData()
console.log(hufenList.value.list)
}
const handleMenuClick = (item) => {
uni.navigateTo({
url: `/pages/user/hufen/forDetails?type=${item.type}&nameValue=${item.dict_value}`
})
}
onMounted(() => {
getHufen()
})
</script>
<style lang="scss" scoped>
.recharge-page {
min-height: 100vh;
background-color: #f7faf9;
}
.menu-section {
padding: 20rpx 20rpx;
}
.menu-list {
background: #fff;
border-radius: 15rpx;
overflow: hidden;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
.menu-list-hufen {
font-size: 36rpx;
color: #007bff;
margin-right: 6rpx;
}
.menu-list-hufen-text {
color: #007bff;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<view class="user-page" :style="{ paddingTop: getNotchHeight() + 30 + 'px' }">
<view class="user-page" :style="{ paddingTop: getNotchHeight() + 30 + 'px' }" v-if="tokenState">
<!-- 设置图标 -->
<view class="settings-icon" :style="{ top: getNotchHeight() + 30 + 'px' }" @click="goSettings">
<wd-icon name="setting1" size="24px" color="#666" />
@@ -23,18 +23,23 @@
<view class="vip-card-title">{{ $t('user.vip') }}</view>
<view class="vip-card-content">
<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>
<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="goCourseVipSub">{{ $t('vip.openVip') }}</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="goCourseVipSub">{{ $t('vip.openVip') }}</wd-button>
</view>
<view class="vip-card-content">
<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>
<wd-button v-if="!vipInfoEbook?.length" plain type="primary" size="small" @click="goSubscribe">{{ $t('vip.openVip') }}</wd-button>
<wd-button v-if="!vipInfoEbook?.length" plain type="primary" size="small"
@click="goSubscribe">{{ $t('vip.openVip') }}</wd-button>
</view>
</view>
</view>
@@ -65,29 +70,33 @@
<!-- 功能菜单列表 -->
<view class="menu-section">
<wd-cell-group border class="menu-list">
<wd-cell v-for="item in menuItems" :key="item.id" :title="item.name" :label="item.desc" is-link @click="handleMenuClick(item)" />
<wd-cell v-for="item in menuItems" :key="item.id" :title="item.name" :label="item.desc" is-link
@click="handleMenuClick(item)">
<text v-if="item.hufenState" class="menu-list-hufen">{{hufenData?.total ?? 0}}<text
style="margin-left: 6rpx;">湖分</text></text>
</wd-cell>
</wd-cell-group>
</view>
</view>
<visitor v-else></visitor>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import { useSysStore } from '@/stores/sys'
import { getUserInfo, getVipInfo } from '@/api/modules/user'
import type { IVipInfo } from '@/types/user'
import { getUserInfo, getUserContributionData } from '@/api/modules/user'
import { getNotchHeight } from '@/utils/system'
import { parseTime } from '@/utils/index'
import { t } from '@/utils/i18n'
import { onShow } from '@dcloudio/uni-app'
import { onShow, onPullDownRefresh } from '@dcloudio/uni-app'
import visitor from '@/pages/visitor/index.vue';
const userStore = useUserStore()
const sysStore = useSysStore()
// 默认头像
const defaultAvatar = '/static/logo.png'
// 用户信息
const userInfo = computed(() => userStore.userInfo)
@@ -115,6 +124,13 @@
url: '/pages/user/myBook/index',
type: 'pageJump'
},
{
id: 8,
name: t('user.iHufen'),
url: '/pages/user/hufen/index',
type: 'pageJump',
hufenState: true
},
{
id: 3,
name: t('user.profile'),
@@ -133,14 +149,23 @@
url: '/pages/user/feedback/index',
type: 'pageJump'
},
{
id: 6,
name: t('user.dataMigrate'),
url: '/pages/user/migrate/index',
desc: t('user.migrateSubtitle'),
type: 'pageJump'
},
// {
// id: 6,
// name: t('user.dataMigrate'),
// url: '/pages/user/migrate/index',
// desc: t('user.migrateSubtitle'),
// id: 7,
// name: t('user.certificate'),
// url: '/pages/user/certificate/index',
// type: 'pageJump'
// }
// },
])
// 湖分
const hufenData = ref()
const tokenState = ref(false)
/**
* 获取平台信息
@@ -161,6 +186,13 @@
}
}
/**
* 获取用户湖分
*/
const getHufen = async () => {
hufenData.value = await getUserContributionData()
}
/**
* 跳转到设置页面
*/
@@ -242,12 +274,34 @@
})
}
/**
* 刷新数据
*/
const handleRefresh = async () => {
if (uni.getStorageSync('token')) {
tokenState.value = true
await Promise.all([
getPlatform(),
getData(),
getHufen()
])
}
}
onShow(() => {
getData()
handleRefresh()
})
onMounted(() => {
getPlatform()
handleRefresh()
})
onPullDownRefresh(async () => {
handleRefresh().then(() => {
setTimeout(() => {
uni.stopPullDownRefresh()
}, 500)
})
})
</script>
@@ -255,8 +309,9 @@
$theme-color: #54a966;
.user-page {
min-height: 100vh;
min-height: calc(100vh - 50px);
background-color: #f7faf9;
position: relative;
}
.settings-icon {
@@ -394,6 +449,16 @@
border-radius: 15rpx;
overflow: hidden;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
.menu-list-hufen {
font-size: 36rpx;
color: #007bff;
text {
font-size: 26rpx;
}
}
}
.chong_btn {

View File

@@ -3,10 +3,20 @@
<!-- 自定义导航栏 -->
<nav-bar :title="$t('user.dataMigrate')"></nav-bar>
<view class="text-red-500 text-center mb-[20rpx]! font-bold">{{ $t('user.migrateWarning') }}</view>
<view v-if="!!migrateInfo.notMigration" class="text-center mb-[20rpx]!">
<view v-if="!!migrateInfo.alreadyMigration">{{ $t('user.alreadyMigrated') }}{{ migrateInfo.alreadyMigration }}</view>
<view>{{ $t('user.notMigration') }}<text class="font-bold">{{ migrateInfo.notMigration }}</text></view>
</view>
<view v-else class="text-center mb-[20rpx]! bg-white p-[20rpx] rounded-[10rpx] shadow-[0_4rpx_12rpx_rgba(0,0,0,0.05)]">
<wd-text :text="$t('user.migratedCompleted')" type="warning" />
<wd-text :text="migrateInfo.alreadyMigration" type="warning" bold />
</view>
<view v-if="!!migrateInfo.notMigration" class="text-red-500 text-center mb-[20rpx]! font-bold">{{ $t('user.migrateWarning') }}</view>
<!-- 主要内容区域 -->
<wd-form ref="migrateForm" :model="formData" :rules="rules" :label-width="120" class="migrate-card p-[10rpx]">
<wd-form v-if="!!migrateInfo.notMigration" ref="migrateForm" :model="formData" :rules="rules" :label-width="120" class="migrate-card p-[10rpx]">
<wd-cell-group border>
<wd-input
v-model="formData.tel"
@@ -60,22 +70,40 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import { t } from '@/utils/i18n'
import { migrateUserData } from '@/api/modules/user'
import { migrateUserData, getUserMigrateInfo } from '@/api/modules/user'
import { useMessage } from '@/uni_modules/wot-design-uni'
const message = useMessage()
const migrateInfo = ref({
alreadyMigration: '',
notMigration: ''
})
// 表单引用
const migrateForm = ref()
// 表单数据
const formData = ref({
tel: '',
code: ''
code: '',
type: ''
})
// 获取用户迁移信息
const getMigrateInfo = async () => {
const res = await getUserMigrateInfo()
migrateInfo.value.alreadyMigration = res.alreadyMigration
migrateInfo.value.notMigration = res.notMigration
}
onMounted(() => {
getMigrateInfo()
})
// 表单验证规则
const rules = ref({
tel: [
@@ -113,6 +141,7 @@ const handleSubmit = async () => {
}
// 处理迁移
const submitMigrate = async () => {
formData.value.type = migrateInfo.value.notMigration
await migrateUserData(formData.value)
uni.showToast({
title: t('user.migrateSuccess'),
@@ -122,6 +151,9 @@ const submitMigrate = async () => {
// 清空表单
formData.value.tel = ''
formData.value.code = ''
// 刷新用户迁移信息
getMigrateInfo()
}
</script>

View File

@@ -16,7 +16,7 @@
<!-- 付款信息 -->
<wd-cell-group>
<wd-cell title="商品总价" :value="`${order.orderMoney} ${$t('global.coin')}`" />
<wd-cell title="商品总价" :value="`${order.bookBuyConfigEntity?.description || ''} ${order.orderMoney} ${$t('global.coin')}`" />
<wd-cell v-if="order.districtMoney > 0" title="活动优惠" :value="`- ${order.districtMoney} ${$t('global.coin')}`" />
<wd-cell v-if="order.vipDiscountAmount > 0" title="VIP专享立减" :value="`- ${order.vipDiscountAmount} ${$t('global.coin')}`" />
<wd-cell v-if="order.jfDeduction > 0" title="积分抵扣">

View File

@@ -12,11 +12,11 @@
</view>
<view class="title">{{$t('order.pointsRecord')}}</view>
<view class="recharge-record-block" v-for="(item, index) in bookList" :key="index" @click="toDetails(item)">
<view class="recharge-record-block-row">{{item.remark.slice(0, (item.remark.indexOf(',')))}}<text
:class="item.actType === 1 ? 'text1' : 'text2'">{{item.actType === 1 ? '' : '+'}}{{item.changeAmount}}</text>
<view class="recharge-record-block-row">{{item.relationId ? item.remark.slice(0, (item.remark.indexOf(','))) : $t('user.backEnd')}}<text
:class="item.changeAmount < 0 ? 'text1' : 'text2'">{{item.changeAmount < 0 ? '' : '+'}}{{item.changeAmount}}</text>
</view>
<view class="time">{{item.createTime}}</view>
<view style="font-size: 24rpx;">{{item.remark.slice((item.remark.indexOf(','))+1)}}<wd-icon name="file-copy"
<view style="font-size: 24rpx;" >{{item.remark.slice((item.remark.indexOf(','))+1)}}<wd-icon v-if="item.relationId" name="file-copy"
size="14px" color="#65A1FA" style="margin-left: 10rpx;" @click="copyToClipboard()"></wd-icon></view>
</view>
</view>
@@ -69,10 +69,14 @@
* 跳转订单详情
*/
const toDetails = (order: IOrder) => {
console.log(order.relationId, "order");
if (order.relationId) {
uni.navigateTo({
url: '/pages/user/order/details?orderId=' + order.relationId
})
} else {
uni.showToast({ title: t('user.cannotView'), icon: 'none' });
}
}
</script>
@@ -116,7 +120,7 @@
.time {
font-size: 24rpx;
margin-bottom: 20rpx;
}
.recharge-record-block-row {

View File

@@ -3,13 +3,13 @@
<!-- 自定义导航栏 -->
<nav-bar :title="$t('order.recharge')"></nav-bar>
<!-- 活动充值金额 -->
<view class="block" v-if="eventAmountList.length > 0">
<view class="block" v-if="eventAmountList?.length > 0">
<!-- <view class="text">{{$t('order.rechargeAmount')}}</view> -->
<view class="text">活动充值金额</view>
<view class="recharge">
<view class="recharge_block" @click="chosPric(item)"
:class="aloneItem.priceTypeId === item.priceTypeId ? 'selected' : ''"
v-for="item in eventAmountList" :key="item.priceTypeId">
:class="aloneItem.priceTypeId === item.priceTypeId ? 'selected' : ''" v-for="item in eventAmountList"
:key="item.priceTypeId">
<view class="recharge_money">NZ${{item.realMoney}}</view>
<view style="font-size: 26rpx;">{{item.money}}{{ $t('global.coin') }}</view>
<span class="activity-label" v-if="item.givejf >0">{{item.description}}</span>
@@ -23,8 +23,8 @@
<view class="text">{{$t('order.rechargeAmount')}}</view>
<view class="recharge">
<view class="recharge_block" @click="chosPric(item)"
:class="aloneItem.priceTypeId === item.priceTypeId ? 'selected' : ''"
v-for="item in standardAmountList" :key="item.priceTypeId">
:class="aloneItem.priceTypeId === item.priceTypeId ? 'selected' : ''" v-for="item in standardAmountList"
:key="item.priceTypeId">
<view class="recharge_money">NZ${{item.realMoney}}</view>
<view style="font-size: 26rpx;">{{item.money}}{{ $t('global.coin') }}</view>
<span class="activity-label" v-if="item.givejf >0">{{item.description}}</span>
@@ -39,13 +39,11 @@
<view class="cha_fangsh">
<view class="cf_title">{{$t('user.paymentMethod')}}</view>
<view class="cf_radio">
<radio-group v-for="item in iosPaylist">
<radio-group>
<view>
<view :class="payType == item.id ? 'Tab_xf cf_xuanx' : 'cf_xuanx'">
<!-- <image class="pay_item_img" :src="item.imgUrl" mode="aspectFil">
</image> -->
<text>{{ item.title }}</text>
<radio :checked="payType === item.id" @click="choseType(item.id)"></radio>
<view class="cf_xuanx">
<text>{{ isAndroid ? $t('user.googlePay') : $t('user.applePay')}}</text>
<radio checked="true;" @click="choseType(item.id)"></radio>
</view>
</view>
</radio-group>
@@ -80,7 +78,7 @@
import { ref, computed, onMounted, toRefs, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import { useMessage } from '@/uni_modules/wot-design-uni'
import { getBookBuyConfigList, getAgreement, getActivityDescription, verifyGooglePay, getPlaceOrder } from '@/api/modules/user'
import { getBookBuyConfigList, getAgreement, getActivityDescription, verifyGooglePay, getPlaceOrder, getIosPayment } from '@/api/modules/user'
import { useUserStore } from '@/stores/user'
import { useThrottle } from '@/hooks/useThrottle';
@@ -88,14 +86,6 @@
const userStore = useUserStore()
const { t } = useI18n()
const message = useMessage()
const payType = ref('1')
const iosPaylist = ref([
{
title: "Google Pay",
id: '1',
// imgUrl: "/static/icon/currency.png"
}
])
// 充值列表
const rechargeList = ref([])
// 金额列表单独每项
@@ -135,6 +125,8 @@
const eventAmountList = ref([])
//正常金额数据
const standardAmountList = ref([])
// 声明ios实例
const iapChannel = ref(null)
/**
* 获取使用环境
@@ -145,7 +137,8 @@
isAndroid.value = true;
console.log('运行Android上')
} else {
qudao.value = 'Google'
isAndroid.value = false;
qudao.value = 'IOS'
console.log('运行iOS上')
}
getData()
@@ -189,10 +182,11 @@
* 获取订单编号
*/
const getPlaceOrderObj = async () => {
console.log(isAndroid.value);
const { priceTypeId, realMoney, money } = toRefs(aloneItem.value)
const data = {
userId: userStore.userInfo.id, // 用户di
paymentMethod: '5', //支付方式4point 5google
paymentMethod: isAndroid.value ? '5' : '3', //支付方式3ios 5google
orderMoney: money.value, //订单金额
realMoney: realMoney.value, //实际金额
come: '10', //订单来源 2医学吴门医述 10海外读书
@@ -200,12 +194,12 @@
productId: priceTypeId.value // 商品id
}
try {
// uni.hideLoading()
const res = await getPlaceOrder(data)
orderSn.value = res.orderSn
console.log(orderSn.value, '获取订单号');
uni.showLoading({ title: t('order.orderCreating') })
getGooglePay()
isAndroid.value ? getGooglePay() : checkProvider()
// getGooglePay()
} catch (error) {
console.error('获取订单号失败', error)
}
@@ -233,7 +227,121 @@
}
/**
* 初始化
* 检测支付提供商判断是否支持applepay
*/
const checkProvider = () => {
uni.getProvider({
service: 'payment',
success: (res) => {
console.log('getProvider返回结果', res); // 关键日志看是否包含applepay
iapChannel.value = res.providers.find((channel) => {
return (channel.id === 'appleiap')
})
getProductInfo()
}
});
}
/**
* 查询后台配置的商品信息
*/
const getProductInfo = () => {
const id = String(aloneItem.value.priceTypeId)
iapChannel.value.requestProduct([id], (res : any) => {
console.log(res, '查询苹果后台配置的商品id');
topay(id)
}, (err : any) => {
uni.showToast({ title: '未获取到产品信息,请联系管理员', icon: 'none' });
console.error('失败', err);
});
}
/**
* 准备支付-调出支付窗口
*/
const topay = (id : string) => {
return new Promise((resolve, reject) => {
uni.hideLoading()
uni.requestPayment({
provider: 'appleiap',
orderInfo: {
productid: id,
username: orderSn.value, // 订单id
quantity: 1,
manualFinishTransaction: true
},
success: (res) => {
console.log(res, 'res-topay');
iapCheck(res);
resolve(res);
},
fail: (err) => {
console.log('支付错误', err);
restoreComplateRequest()
uni.showToast({ title: t('user.closeWindow'), icon: 'error' })
reject(err);
}
});
})
}
/**
* 调用后台支付-ios
*/
const iapCheck = async (res : { transactionIdentifier : string; transactionReceipt : string }) => {
// console.log(res.transactionIdentifier,res.payment.productid,res.payment.username,res.transactionReceipt,userStore.userInfo.id);
try {
const obj = await getIosPayment(res.transactionIdentifier, res.payment.productid, res.payment.username, res.transactionReceipt, userStore.userInfo.id)
console.log(obj, '校验订单')
finishTransaction(res)
uni.switchTab({
url: '/pages/user/index'
})
} catch (error) {
console.error('校验订单失败:', error)
// 也需要释放订单,防止失败再次提交支付窗口拉不起来
finishTransaction(res)
}
}
/**
* 检查是否存在未关闭的订单
*/
const restoreComplateRequest = () => {
return new Promise((resolve, reject) => {
iapChannel.value.restoreCompletedTransactions({
manualFinishTransaction: true,
}, (res : unknown) => {
console.log(res, '成功-restoreCompletedTransactions');
res.map((item : any) => {
finishTransaction(item)
})
resolve(res);
}, (err : any) => {
console.log(err, '失败-restoreCompletedTransactions');
reject(err);
})
});
}
/**
* 关闭订单
*/
const finishTransaction = (trans : any) => {
iapChannel.value.finishTransaction(
trans,
(success : any) => {
console.log("关闭订单成功", success);
},
(fail : any) => {
console.log("关闭订单失败", fail);
}
);
}
/**
* 谷歌初始化
*/
const getGooglePay = () => {
googlePay.init({
@@ -375,13 +483,6 @@
}
}
/**
* 切换支付方式
*/
const choseType = () => {
// payType.value = val;
}
onMounted(() => {
getDevName();
getActivityDescriptionData()

View File

@@ -15,11 +15,11 @@
</view>
<view class="title">{{$t('order.rechargeConsumptionList')}}</view>
<view class="recharge-record-block" v-for="(item, index) in bookList" :key="index" @click="toDetails(item)">
<view class="recharge-record-block-row">{{item.orderType}}<text
:class="item.orderType !== '充值' ? 'text1' : 'text2'">{{item.orderType !== '充值' ? '' : '+'}}{{item.changeAmount}}</text>
<view class="recharge-record-block-row">{{item.description || item.orderType}}<text
:class="item.changeAmount < 0 ? 'text1' : 'text2'">{{item.changeAmount < 0 ? '' : '+'}}{{item.changeAmount}}</text>
</view>
<view class="recharge-record-block-row_">{{item.productName}}</view>
<view class="recharge-record-block-row_">{{$t('user.orderSn')}}{{item.payNo}}<wd-icon name="file-copy"
<view class="recharge-record-block-row_">{{ item.note ?? item.productName }}</view>
<view class="recharge-record-block-row_" v-if="item.relationId">{{$t('user.orderSn')}}{{item.payNo}}<wd-icon name="file-copy"
size="14px" color="#65A1FA" style="margin-left: 10rpx;" @click="copyToClipboard()"></wd-icon>
</view>
<view class="time">{{item.createTime}}</view>
@@ -75,10 +75,14 @@
* 跳转订单详情
*/
const toDetails = (order: IOrder) => {
console.log(order.relationId, "order");
if (order.relationId) {
uni.navigateTo({
url: '/pages/user/order/details?orderId=' + order.relationId
})
} else {
uni.showToast({ title: t('user.cannotView'), icon: 'none' });
}
}
</script>
@@ -122,7 +126,7 @@
.recharge-record-block-row_ {
font-size: 24rpx;
margin-bottom: 20rpx;
// margin-bottom: 20rpx;
}
.time {

View File

@@ -4,7 +4,7 @@
<nav-bar :title="$t('vip.bookVip')" />
<!-- VIP介绍卡片 -->
<view class="vip-intro-card">
<view class="vip-intro-card mt-2!">
<view class="vip-intro-header">
<text class="vip-intro-title">📚 读书VIP特权</text>
<!-- <text class="vip-intro-subtitle">畅享海量电子图书</text> -->
@@ -12,7 +12,7 @@
<view class="vip-intro-features">
<view class="feature-item">
<wd-icon name="check-circle" size="16px" color="#258feb" />
<text>畅享海量电子图书</text>
<text>畅享App内电子图书</text>
</view>
</view>
</view>
@@ -58,14 +58,15 @@
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useUserStore } from '@/stores/user'
import { vipApi } from '@/api/modules/vip'
import type { IVipBook } from '@/types/vip'
const userStore = useUserStore()
const vipList = ref([])
const vipList = ref<IVipBook[]>([])
const getVipList = async () => {
const res = await vipApi.getBookVipList()
// 模拟推荐标识,实际项目中应该从后端获取
@@ -79,7 +80,7 @@ const handlePurchase = (vip: any) => {
productName: `${vip.title}`,
price: vip.money,
productImages: '/static/vip.png',
state: null,
state: 1,
orderType: 'abroadVip'
}
uni.navigateTo({
@@ -100,7 +101,10 @@ onShow(() => {
<style lang="scss" scoped>
.page-wrapper {
padding: 20rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
// background-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background-color: #6E63C4;
background-size: cover;
background-position: center;
min-height: 100vh;
}
@@ -111,7 +115,6 @@ onShow(() => {
padding: 30rpx 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10rpx);
}
.vip-intro-header {
@@ -177,8 +180,7 @@ onShow(() => {
background: rgba(255, 255, 255, 0.95);
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 8rpx 25rpx rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10rpx);
// box-shadow: 0 8rpx 25rpx rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
position: relative;
overflow: hidden;

138
pages/visitor/index.vue Normal file
View File

@@ -0,0 +1,138 @@
<template>
<view class="visitor">
<view class="visitor-block">
<view style="display: flex;">
<image class="visitor_img" src="/static/logo.png" mode="aspectFil">
</image>
<text class="visitor-text" @click="gologin">立即登录</text>
</view>
<wd-cell-group border class="visitor-list">
<wd-cell v-for="item in menuItems" :title="item.name" is-link @click="handleMenuClick(item)">
</wd-cell>
</wd-cell-group>
</view>
</view>
<wd-action-sheet v-model="isShareSheetOpen" title="选择分享渠道" :panels="panels" @select="handleShare" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
const menuItems = ref([
// {
// name: '分享APP'
// },
{
name: '关于我们',
}
])
const isShareSheetOpen = ref(false)
const panels = ref([
{
iconUrl: '/static/contact-person.png',
title: '微信消息'
},
{
iconUrl: '/static/moments.png',
title: '朋友圈'
}
])
// 打开分享菜单
const openShareSheet = () => {
isShareSheetOpen.value = true
}
// 选择分享渠道后执行分享逻辑
const handleShare = (action) => {
console.log(action, 'action');
isShareSheetOpen.value = false // 关闭菜单
if (action.index == 0) {
// 分享到好友
uni.share({
provider: "weixin",
scene: "WXSceneSession",
type: 0,
href: 'https://a.app.qq.com/o/simple.jsp?pkgname=com.cn.medicine',
title: "吴门医述",
summary: "我正在使用吴门医述提升自己,赶紧跟我一起来体验吧!",
imageUrl: "static/logo.png",
success: function (res) {
console.log("success:" + JSON.stringify(res));
},
fail: function (err) {
console.log("fail:" + JSON.stringify(err));
},
});
} else if (action.index == 1) {
// 分享到朋友圈
uni.share({
provider: "weixin",
scene: "WXSceneTimeline",
type: 0,
href: 'https://a.app.qq.com/o/simple.jsp?pkgname=com.cn.medicine',
title: "吴门医述",
summary: "我正在使用吴门医述提升自己,赶紧跟我一起来体验吧!",
imageUrl: "static/logo.png",
success: function (res) {
console.log("success:" + JSON.stringify(res));
},
fail: function (err) {
console.log("fail:" + JSON.stringify(err));
},
});
}
}
const handleMenuClick = (item : { name : string }) => {
if (item.name === '关于我们') {
uni.navigateTo({
url: '/pages/user/about/index'
})
} else {
isShareSheetOpen.value = true
}
}
const gologin = () => {
uni.navigateTo({
url: '/pages/login/login'
})
}
</script>
<style lang="scss" scoped>
.visitor {
background: #f4f7ff;
min-height: 100vh;
}
.visitor-block {
padding: 100rpx 20rpx 40rpx 20rpx;
.visitor_img {
width: 150rpx;
height: 150rpx;
background-color: #fff;
border-radius: 60px;
}
.visitor-text {
margin-top: 30rpx;
margin-left: 20rpx;
font-weight: bold;
font-size: 36rpx;
}
}
.visitor-list {
background: #fff;
border-radius: 15rpx;
overflow: hidden;
margin-top: 40rpx;
font-weight: bold;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
</style>

View File

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 261 KiB

BIN
static/contact-person.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
static/moments.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -8,6 +8,7 @@
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--color-red-500: oklch(63.7% 0.237 25.331);
--color-white: #fff;
--spacing: 0.25rem;
--text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.75);
@@ -211,9 +212,18 @@
max-width: 96rem;
}
}
.mt-2\! {
margin-top: calc(var(--spacing) * 2) !important;
}
.mt-\[20rpx\]\! {
margin-top: 20rpx !important;
}
.mb-2\! {
margin-bottom: calc(var(--spacing) * 2) !important;
}
.mb-2\.5\! {
margin-bottom: calc(var(--spacing) * 2.5) !important;
}
.mb-\[20rpx\]\! {
margin-bottom: 20rpx !important;
}
@@ -247,6 +257,9 @@
.table {
display: table;
}
.w-\[50\%\] {
width: 50%;
}
.w-\[100px\] {
width: 100px;
}
@@ -265,16 +278,25 @@
.flex-wrap {
flex-wrap: wrap;
}
.rounded-\[10rpx\] {
border-radius: 10rpx;
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
}
.bg-white {
background-color: var(--color-white);
}
.p-0\! {
padding: calc(var(--spacing) * 0) !important;
}
.p-\[10rpx\] {
padding: 10rpx;
}
.p-\[20rpx\] {
padding: 20rpx;
}
.p-\[30rpx\] {
padding: 30rpx;
}
@@ -337,6 +359,10 @@
--tw-ordinal: ordinal;
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
}
.shadow-\[0_4rpx_12rpx_rgba\(0\,0\,0\,0\.05\)\] {
--tw-shadow: 0 4rpx 12rpx var(--tw-shadow-color, rgba(0,0,0,0.05));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);

1
types/book.d.ts vendored
View File

@@ -129,7 +129,6 @@ export interface IPageData<T> {
export interface IApiResponse<T = any> {
code: number
msg?: string
info?: string
[key: string]: any
}

18
types/course.d.ts vendored
View File

@@ -3,7 +3,8 @@
* 课程相关类型定义
*/
import type { IApiResponse } from './book'
import type { IApiResponse } from '@/api/types'
import type { IVideoInfo } from './video'
import type { IGoods } from './order'
/** 医学标签(课程分类) */
@@ -49,11 +50,6 @@ export interface IMarketCourseListResponse extends IApiResponse {
}
}
/** 消息列表响应 */
export interface IMessageListResponse extends IApiResponse {
messages: INews[] // 消息列表
}
/** 课程详情 */
export interface ICourseDetail {
id: number
@@ -93,14 +89,6 @@ export interface IChapterDetail {
questions: string // 思考题内容
}
/** 视频信息 */
export interface IVideo {
id: number
title: string
url: string
duration?: number
}
/** VIP信息 */
export interface IVipInfo {
type: number // VIP类型 4-中医学 5-针灸学 6-肿瘤学 7-国学 8-心理学 9-中西汇通学
@@ -140,7 +128,7 @@ export interface IChapterListResponse extends IApiResponse {
export interface IChapterDetailResponse extends IApiResponse {
data: {
detail: IChapterDetail
videos: IVideo[]
videos: IVideoInfo[]
current?: number // 当前播放视频ID
}
}

58
types/order.d.ts vendored
View File

@@ -160,3 +160,61 @@ export interface IOrderDetail {
remark?: string
[key: string]: any
}
/**
* 创建订单参数
*/
export interface ICreateOrderParams {
buyType: number // 0-商品页直接下单 1-购物车结算
userId: number
paymentMethod: number // 4-天医币
orderMoney: number // 订单金额
realMoney: number // 实收金额
jfDeduction: number // 积分抵扣
couponId?: number // 优惠券ID
couponName?: string // 优惠券名称
vipDiscountAmount: number // VIP折扣金额
districtMoney: number // 地区优惠金额
remark?: string // 备注
productList: Array<{
productId: number
quantity: number
}>
orderType: string // "order"
addressId: number // 0 for course products
appName: string // "wumen"
come: number // 2
}
/**
* 创建订单响应
*/
export interface ICreateOrderResponse {
orderSn: string
money: number
[key: string]: any
}
/**
* 订单初始化数据
*/
export interface IOrderInitData {
goodsList: IOrderGoods[]
totalPrice: number
vipPrice: number
districtAmount: number
actualPayment: number
jfNumber: number
jfNumberMax: number
jfNumberShow: string
couponList: ICoupon[]
selectedCoupon: ICoupon | null
showCouponPopup: boolean
remark: string
showRemarkPopup: boolean
payType: number
loading: boolean
submitting: boolean
buyingFlag: boolean
[key: string]: any
}

View File

@@ -1,4 +1,5 @@
// types/user.ts
import type { IApiResponse } from '@/api/types'
/**
*
@@ -16,12 +17,12 @@ export interface IUserInfo {
/**
*
*/
export interface ILoginResponse {
userInfo: IUserInfo
export interface ILoginResponse<T = IUserInfo> extends IApiResponse {
token: {
token: string
[key: string]: any
}
},
userInfo: T
}
/**
@@ -116,3 +117,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
}

11
types/video.d.ts vendored
View File

@@ -6,7 +6,7 @@
export interface IVideoInfo {
id: number
chapterId: number
type: 0 | 1 // 0: MP4, 1: M3U8
type: 0 | 1 | 2 // 0: MP4, 1: M3U8, 2: 音频
video: string // 视频ID
sort: number
duration: number // 视频时长(秒)
@@ -46,14 +46,7 @@ export interface IVideoCheckResponse {
* 视频播放器组件 Props
*/
export interface IVideoPlayerProps {
videoList: Array<{
id: number
chapterId: number
video: string
sort: number
type?: 0 | 1
duration?: number
}>
videoList: Array<IVideoInfo>
currentIndex: number
countdownSeconds?: number
showWatermark?: boolean

12
types/vip.d.ts vendored
View File

@@ -25,3 +25,15 @@ export interface IVipItemProduct {
lastFee?: number | null, // 未使用字段
[key: string]: any
}
/**
* 电子书VIP套餐
*/
export interface IVipBook {
id: number
type: number
title: string
money: number // vip金额
days: number // vip天数
[key: string]: any
}

View File

@@ -232,6 +232,18 @@ export function useUpload(): UseUploadReturn {
extension
}: ChooseFileOption): Promise<ChooseFile[]> {
return new Promise((resolve, reject) => {
if(uni.getSystemInfoSync().platform === 'ios') {
uni.chooseImage({
count: multiple ? maxCount : 1,
mediaType: ['image'],
sizeType,
sourceType,
extension,
success: (res) => resolve(formatImage(res)),
fail: reject
})
return
}
switch (accept) {
case 'image':
// #ifdef MP-WEIXIN