33 Commits

Author SHA1 Message Date
b2bff1ed54 Merge branch 'main' of https://git.nuttyreading.com/zm/taimed-international-app 2025-12-26 17:12:26 +08:00
e5415a8784 feat(升级中心): 实现备用更新方案并优化版本检查逻辑 2025-12-26 17:11:58 +08:00
2f4f170fc5 Merge branch 'main' of https://git.nuttyreading.com/zm/taimed-international-app into 12.26 2025-12-26 13:12:19 +08:00
e2d6523347 更新:我的课程 2025-12-26 11:41:27 +08:00
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
66 changed files with 1759 additions and 814 deletions

22
App.vue
View File

@@ -3,31 +3,9 @@
import update from "@/uni_modules/uni-upgrade-center-app/utils/check-update";
// #endif
import { useUserStore } from '@/stores/user'
export default {
onLaunch: function() {
const userStore = useUserStore()
console.log('App Launch')
// 保存原生 switchTab 方法
const originalSwitchTab = uni.switchTab;
uni.switchTab = (options) => {
if (options.url.includes('/pages/book/index') && !userStore.token) {
uni.showModal({
title: '提示',
content: '请先登录后访问该页面',
confirmText: '去登录',
success: (res) => {
console.log(res, 'res');
if (res.confirm) uni.navigateTo({
url: '/pages/login/login'
});
}
});
return; // 拦截跳转
}
// 已登录/非拦截页 → 执行原生跳转
originalSwitchTab.call(uni, options);
}
// 检测自动更新
// #ifdef APP-PLUS
update();

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 @@ export const ENV = process.env.NODE_ENV || 'development';
const BASE_URL_MAP = {
development: {
MAIN: 'http://192.168.110.100:9300/pb/', // 张川川
//MAIN: 'https://global.nuttyreading.com/', // 线上
// MAIN: 'https://global.nuttyreading.com/', // 线上
// PAYMENT: 'https://dev-pay.example.com', // 暂时用不到
// CDN: 'https://cdn-dev.example.com', // 暂时用不到
},
@@ -21,7 +21,6 @@ const BASE_URL_MAP = {
export const APP_INFO = {
TYPE: 'abroad', // APP 名称
VERSION_CODE: '1.0.0', // APP 版本号,可能升级的时候会用,这里需要再确定?
}
export const REQUEST_TIMEOUT = 30000;

View File

@@ -1,6 +1,7 @@
import type { IRequestOptions } from '../types'
import { useUserStore } from '@/stores/user'
import { APP_INFO } from '@/api/config'
import { getCurrentVersion } from '@/uni_modules/uni-upgrade-center-app/utils/call-check-version'
export function requestInterceptor(options: IRequestOptions): IRequestOptions {
const headers = { ...(options.headers || {}) }
@@ -21,9 +22,12 @@ export function requestInterceptor(options: IRequestOptions): IRequestOptions {
headers['Content-Type'] = 'application/json;charset=UTF-8'
}
getCurrentVersion().then((version_code: string) => {
headers['version_code'] = version_code || ''
})
headers['appType'] = APP_INFO.TYPE
headers['version_code'] = APP_INFO.VERSION_CODE || '1.0.0'
return {
...options,
header: headers,

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,10 +1,7 @@
// 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'
import { useUserStore } from '@/stores/user'
export const commonApi = {
/**
@@ -24,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 }
@@ -39,9 +36,8 @@ export const commonApi = {
* @returns 消息列表
*/
getMessageList(isBook: number, isMedical: number, isSociology: number) {
const userStore = useUserStore()
return mainClient.request<IMessageListResponse>({
url: userStore.token ? 'common/message/listByPage' : '/visitor/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,
@@ -14,11 +13,6 @@ import type {
} from '@/types/course'
import type { ISearchRequest, ISearchResponse } from '@/types/search'
import type { ICommentListResponse, IAddCommentResponse, IComment } from '@/types/comment'
import { useUserStore } from '@/stores/user'
const client = createRequestClient({ baseURL: SERVICE_MAP.MAIN })
/**
* 课程相关API
@@ -29,7 +23,7 @@ export const courseApi = {
* @returns 分类数据
*/
getCourseMedicalTree() {
return client.request<ICourseMedicalTreeResponse>({
return mainClient.request<ICourseCategoryResponse>({
url: 'medical/home/getCourseMedicalTree',
method: 'POST',
data: {}
@@ -41,7 +35,7 @@ export const courseApi = {
* @returns 观看记录列表
*/
getUserLateCourseList() {
return client.request<IUserLateCourseListResponse>({
return skeletonClient.request<IUserLateCourseListResponse>({
url: 'medical/home/getUserLateCourseList',
method: 'POST',
data: {}
@@ -60,9 +54,8 @@ export const courseApi = {
page: number,
limit: number
}) {
const userStore = useUserStore()
return client.request<IMarketCourseListResponse>({
url: userStore.token ? 'medical/home/getMarketCourseList' : 'visitor/getMarketCourseList',
return skeletonClient.request<IMarketCourseListResponse>({
url: uni.getStorageSync('token') ? 'medical/home/getMarketCourseList' : 'visitor/getMarketCourseList',
method: 'POST',
data
})
@@ -74,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
@@ -86,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 }
@@ -98,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 }
@@ -110,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 }
@@ -122,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 }
@@ -134,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 }
@@ -146,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 }
@@ -158,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 }
@@ -173,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: '' }
@@ -194,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
@@ -207,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 }
@@ -220,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 }
@@ -232,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 }
@@ -244,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,16 +1,9 @@
// 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'
import { useUserStore } from '@/stores/user'
const client = createRequestClient({ baseURL: SERVICE_MAP.MAIN })
/**
* 课程分类及分类下课程相关API
@@ -22,9 +15,8 @@ export const courseSubjectClassificationApi = {
* @returns 分类数据
*/
getCourseMedicalTree() {
const userStore = useUserStore()
return client.request<ICourseCategoryResponse>({
url: userStore.token ? 'medical/home/getCourseMedicalTree' : '/visitor/getCourseMedicalTree',
return skeletonClient.request<ICourseCategoryResponse>({
url: uni.getStorageSync('token') ? 'medical/home/getCourseMedicalTree' : '/visitor/getCourseMedicalTree',
method: 'POST',
data: {}
})
@@ -36,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 }
@@ -49,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 }
@@ -62,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 }
@@ -75,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 }
@@ -93,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
@@ -106,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 }
@@ -124,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
@@ -142,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

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

14
api/modules/sys.ts Normal file
View File

@@ -0,0 +1,14 @@
import { mainClient, skeletonClient } from '@/api/clients'
import type { IApiResponse } from '@/api/types'
/**
* 请求更新包
*/
export async function requestUpdatePackage(type: string, version: string) {
const res = await skeletonClient.request<IApiResponse>({
url: 'common/apkConfig/getUpdateUrl',
method: 'POST',
data: { type, version }
})
return res
}

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'
})
@@ -324,7 +324,7 @@ export async function migrateUserData(data: { tel: string, code: string, type: s
* }
*/
export async function getUserMigrateInfo() {
const res = await mainClient.request<IApiResponse>({
const res = await skeletonClient.request<IApiResponse>({
url: 'common/user/getMigrationList',
method: 'POST',
})
@@ -336,7 +336,7 @@ export async function getUserMigrateInfo() {
* @return
*/
export async function getUserContributionData() {
const res = await mainClient.request<IApiResponse>({
const res = await skeletonClient.request<IApiResponse>({
url: 'common/userContribution/getUserContribution',
method: 'POST'
})
@@ -357,4 +357,22 @@ export async function getUserContributionByTypeList(current : number, limit : nu
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,28 +373,36 @@ const validateOrder = (): boolean => {
/**
* 提交订单
*/
const handleSubmit = async () => {
const submitLoading = ref<boolean>(false)
const handleSubmit = async () => {
// 验证订单
if (!validateOrder()) return
submitLoading.value = true
// 创建订单 此app用天医币支付创建订单成功即支付成功
await createOrder()
try {
// 创建订单 此app用天医币支付创建订单成功即支付成功
await createOrder()
// 重新获取用户信息更新store和本地缓存
const res = await getUserInfo()
userStore.setUserInfo(res.result)
// 重新获取用户信息更新store和本地缓存
const res = await getUserInfo()
userStore.setUserInfo(res.result)
uni.showToast({
title: t('order.orderSuccess'),
icon: 'success'
})
// 返回上一页
setTimeout(() => {
uni.navigateBack({
delta: props.backStep
uni.showToast({
title: t('order.orderSuccess'),
icon: 'success'
})
}, 500)
// 返回上一页
setTimeout(() => {
submitLoading.value = false
uni.navigateBack({
delta: props.backStep
})
}, 500)
} catch (error) {
submitLoading.value = false
console.error('提交订单失败:', error)
}
}
/**

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

@@ -20,7 +20,7 @@
"coin": "Coin",
"days": "Days",
"and": "and",
"call": "Call"
"queryFailed": "Query failed"
},
"tabar.course": "COURSE",
"tabar.book": "EBOOK",
@@ -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,8 +223,8 @@
"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.",
"alreadyMigrated": "You have already migrated:",
@@ -234,7 +234,10 @@
"certificate": "My certificate",
"iHufen": "My lake",
"hufenRecord": "Lake division record",
"hufen": "Hufen"
"hufen": "Hufen",
"backEnd" : "Back-end recharge",
"cannotView" : "The recharge in the background cannot be viewed",
"myCourses": "My courses"
},
"book": {
"title": "My Books",

View File

@@ -20,7 +20,7 @@
"coin": "天医币",
"days": "天",
"and": "和",
"call": "拨打电话"
"queryFailed": "查询失败"
},
"tabar.course": "课程",
"tabar.book": "图书",
@@ -146,7 +146,7 @@
"updateFailed": "更新失败",
"paymentMethod": "支付方式",
"googlePay": "Google Pay",
"applePay": "Apple Pay",
"applePay": "IAP 支付",
"agreeText": "我已同意",
"agreement": "充值协议",
"agreeFirst": "请先同意充值协议",
@@ -224,8 +224,8 @@
"migrateCodePlaceholder": "国内版账号获取的迁移验证码",
"migrateWarning": "迁移后不可恢复,请谨慎操作!",
"migrateInstructions": "迁移说明",
"instruction1": "请在吴门医述、心灵空间、众妙之门、疯子读书任意APP中获取迁移验证码获取方式【我的】-【数据迁移】-【获取迁移验证码】。",
"instruction2": "数据迁移完成后,旧账号数据将被清空,已购买的天医币、积分、课程、电子书、VIP、证书、湖分将转移到当前账号。",
"instruction1": "请在吴门医述APP中获取迁移验证码获取方式【我的】-【数据迁移】-【查看账号和迁移验证码】。",
"instruction2": "数据迁移完成后,旧账号数据将被清空,已购买的天医币、积分、课程、VIP、湖分将转移到当前账号。",
"instruction3": "迁移过程可能需要几分钟时间,请耐心等待。",
"instruction4": "如遇到问题,请联系客服获取帮助。",
"alreadyMigrated": "您已迁移过:",
@@ -235,7 +235,10 @@
"certificate": "我的证书",
"iHufen": "我的湖分",
"hufenRecord": "湖分记录",
"hufen": "湖分"
"hufen": "湖分",
"backEnd" : "后台充值",
"cannotView" : "后台充值无法查看",
"myCourses": "我的课程"
},
"book": {
"title": "我的书单",

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

@@ -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",
@@ -109,9 +112,16 @@
"navigationStyle": "custom",
"navigationBarTitleText": "%book.title%"
}
}, {
"path": "pages/user/myCourses/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "%book.title%"
}
}, {
"path": "pages/book/index",
"style": {
"enablePullDownRefresh": true,
"navigationStyle": "custom",
"navigationBarTitleText": "%book.title%"
}

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,189 +31,257 @@
<!-- 我的书单 & 推荐图书模块 -->
<view class="mine-block">
<!-- 我的书单 -->
<view class="mine-1">
<text class="mine-title">{{ $t('bookHome.block1') }}</text>
<view
v-if="myBooksList.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-for="(item, index) in myBooksList"
:key="index"
class="mine-item"
@click="handleMyBookClick(item.id)"
>
<image :src="item.images" />
<text>{{ item.name }}</text>
<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="data?.page?.records.length > 0"
class="mine-more"
@click="handleMoreClick"
>
{{ $t('bookHome.more') }}
<image src="@/static/icon/icon_right.png" />
</view>
<view v-if="data?.page?.records.length > 0" class="mine-1-list">
<view
v-for="(item, index) in data?.page?.records"
:key="index"
class="mine-item"
@click="handleMyBookClick(item.id)"
>
<image :src="item.images" />
<text>{{ item.name }}</text>
</view>
</view>
<text v-else class="zanwu">{{ $t('common.data_null') }}</text>
</view>
</view>
<text v-else class="zanwu">{{ $t('common.data_null') }}</text>
</view>
</template>
</Skeleton>
<!-- 推荐图书 -->
<view class="mine-2">
<text class="mine-title">{{ $t('bookHome.block2') }}</text>
<swiper
v-if="recommendBooksList.length > 0"
autoplay
:interval="3000"
:duration="500"
class="recommend-list"
>
<swiper-item
v-for="(item, index) in recommendBooksList"
:key="index"
class="recommend-item"
@click="handleBookClick(item.id)"
>
<image :src="item.images" width="100%" height="100%" />
<text>{{ item.name }}</text>
</swiper-item>
</swiper>
</view>
<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="data.books.length > 0"
autoplay
:interval="3000"
:duration="500"
class="recommend-list"
>
<swiper-item
v-for="(item, index) in data.books"
:key="index"
class="recommend-item"
@click="handleBookClick(item.id)"
>
<image :src="item.images" width="100%" height="100%" />
<text>{{ item.name }}</text>
</swiper-item>
</swiper>
</view>
</template>
</Skeleton>
</view>
<!-- 活动图书模块 -->
<view v-if="showActivity" class="activity-block">
<text class="activity-title">{{ $t('bookHome.activityTitle') }}</text>
<scroll-view class="scroll-view" scroll-x :show-scrollbar="false">
<view class="activity-label-list">
<view
v-for="(item, index) in activityLabelList"
:key="index"
:class="[
'activity-label-item',
currentActivityIndex === index ? 'active-label' : ''
]"
@click="handleActivityLabelClick(item.id, index)"
>
<text>{{ item.title }}</text>
</view>
</view>
</scroll-view>
<scroll-view
v-if="activityList.length > 0"
class="scroll-view"
scroll-x
:show-scrollbar="false"
<view class="activity-block">
<Skeleton
theme="image-card"
:size="Array(5).fill({ height:'28px', width: '18%' })"
:request="getActivityLabels"
@success="getActivityLabelsSuccess"
>
<view class="activity-list">
<view
v-for="(item, index) in activityList"
:key="index"
class="activity-item"
@click="handleBookClick(item.bookId)"
<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">
<view
v-for="(item, index) in activityLabelList"
:key="index"
:class="[
'activity-label-item',
currentActivityIndex === index ? 'active-label' : ''
]"
@click="handleActivityLabelClick(item.id, index)"
>
<text>{{ item.title }}</text>
</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="data.bookList.length > 0"
class="scroll-view"
scroll-x
:show-scrollbar="false"
>
<image :src="item.images" />
<text class="activity-text">{{ item.name }}</text>
</view>
</view>
</scroll-view>
<text v-else class="zanwu" style="padding: 80rpx 0">{{ $t('global.dataNull') }}</text>
<view class="activity-list">
<view
v-for="(item, index) in data.bookList"
:key="index"
class="activity-item"
@click="handleBookClick(item.bookId)"
>
<image :src="item.images" />
<text class="activity-text">{{ item.name }}</text>
</view>
</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">
<!-- 一级分类标签 -->
<scroll-view class="scroll-view" scroll-x :show-scrollbar="false">
<view class="book-tab-one">
<view
v-for="(item, index) in categoryLevel1List"
:key="index"
:class="[
'tab-one-item',
currentLevel1Index === index ? 'tab-one-active' : ''
]"
@click="handleCategoryLevel1Click(item.id, index)"
>
<text>{{ item.title }}</text>
</view>
</view>
</scroll-view>
<!-- 二级分类标签 -->
<scroll-view
v-if="categoryLevel2List.length > 0"
class="scroll-view"
scroll-x
:show-scrollbar="false"
style="background: #fff; margin-top: 15rpx"
<view class="book-block">
<Skeleton
ref="categoryLevel1LabelSkeleton"
theme="image-card"
:size="Array(3).fill({ height:'75px', width: '32%' })"
:request="getCategoryLabels"
@success="getCategoryLabelsSuccess"
>
<view class="book-tab-two">
<view
v-for="(item, index) in categoryLevel2List"
:key="index"
:class="[
'tab-two-item',
currentLevel2Index === index ? 'tab-two-active' : ''
]"
@click="handleCategoryLevel2Click(item.id, index)"
<template #content>
<!-- 一级分类标签 -->
<scroll-view class="scroll-view" scroll-x :show-scrollbar="false">
<view class="book-tab-one">
<view
v-for="(item, index) in categoryLevel1List"
:key="index"
:class="[
'tab-one-item',
currentLevel1Index === index ? 'tab-one-active' : ''
]"
@click="handleCategoryLevel1Click(item.id, index)"
>
<text>{{ item.title }}</text>
</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"
class="scroll-view"
scroll-x
:show-scrollbar="false"
style="background: #fff; margin-top: 15rpx"
>
<text>{{ item.title }}</text>
</view>
</view>
</scroll-view>
<view class="book-tab-two">
<view
v-for="(item, index) in categoryLevel2List"
:key="index"
:class="[
'tab-two-item',
currentLevel2Index === index ? 'tab-two-active' : ''
]"
@click="handleCategoryLevel2Click(item.id, index)"
>
<text>{{ item.title }}</text>
</view>
</view>
</scroll-view>
</template>
</Skeleton>
<!-- 分类图书列表 -->
<view v-if="categoryBookList.length > 0" class="book-list">
<view
v-for="(item, index) in categoryBookList"
:key="index"
class="book-item"
@click="handleBookClick(item.bookId)"
>
<image :src="item.images" />
<text class="book-text">{{ item.name }}</text>
<BookPrice :data="item" class="book-price-container" />
</view>
</view>
<text v-else class="zanwu" style="padding: 100rpx 0">{{ $t('global.dataNull') }}</text>
<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 data.bookList"
:key="index"
class="book-item"
@click="handleBookClick(item.bookId)"
>
<image :src="item.images" />
<text class="book-text">{{ item.name }}</text>
<BookPrice :data="item" class="book-price-container" />
</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

@@ -37,8 +37,6 @@ const props = defineProps<{
visible: boolean
}>()
console.log(props.visible)
const showProtocol = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)

View File

@@ -31,80 +31,87 @@
>{{ item }}</view>
</view>
<!-- 医学 -->
<view v-if="selectedFirstLevel === '医学'" class="newLeve2">
<view class="home_nar nomargin" style="padding: 0; background-color: #fff">
<view class="flexbox">
<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">
<view
:class="['hn_cl_tit', currentIndex == index ? 'active' : '']"
@click="curseClick(item, index)"
v-for="(item, index) in curseTagList"
:key="item.id"
>
<image :src="item.icon" mode="aspectFit"></image>
<text>{{ item.title }}</text>
</view>
</view>
</view>
<view
:class="['hn_cl_tit', currentIndex == index ? 'active' : '']"
@click="curseClick(item, index)"
v-for="(item, index) in curseTagList"
:key="item.id"
class="fourBox"
style="padding: 0; padding-bottom: 8rpx"
v-if="sbuMedicalTagsList?.length > 0"
>
<image :src="item.icon" mode="aspectFit"></image>
<text>{{ item.title }}</text>
<view
class="childrenBox fourIcon flexbox"
style="justify-content: space-around"
>
<view
class="item flexbox"
@click="curseClickJump(item)"
v-for="(item, index) in sbuMedicalTagsList"
:key="item.id"
>
<image
:src="item.icon"
mode="aspectFit"
v-if="item.icon != '' && item.icon != null"
></image>
<text>{{ item.title }}</text>
</view>
</view>
</view>
</view>
</view>
<view
class="fourBox"
style="padding: 0; padding-bottom: 8rpx"
v-if="sbuMedicalTagsList?.length > 0"
>
</template>
</Skeleton>
<!-- 心理学和国学 -->
<Skeleton
v-if="selectedFirstLevel === '心理学' || selectedFirstLevel === '国学'"
ref="menuSkeletonRef"
theme="menu"
:request="getMenus"
>
<template #content="{ data }">
<view
class="childrenBox fourIcon flexbox"
style="justify-content: space-around"
v-if="data.labels.length > 0"
:class="[
{'sociology_cate_box': selectedFirstLevel === '国学'},
{'soul_cate_box': selectedFirstLevel === '心理学'}
]"
>
<view
class="item flexbox"
@click="curseClickJump(item)"
v-for="(item, index) in sbuMedicalTagsList"
:key="item.id"
>
<image
:src="item.icon"
mode="aspectFit"
v-if="item.icon != '' && item.icon != null"
></image>
<text>{{ item.title }}</text>
</view>
</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">
<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>
<image
:src="item.icon"
mode="aspectFill"
style="width: 49rpx; height: 49rpx"
></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"
mode="aspectFill"
style="width: 49rpx; height: 49rpx"
></image>
</view>
<view class="cate_item_name">{{ v.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,62 +170,73 @@
</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">
<view
class="item"
v-for="(item, index) in tryListenList"
:key="item.id"
@click="onPageJump('/pages/course/details/course', item.id, item.title)"
>
<view class="imgcontainer">
<image
v-if="item.image == '' || !item.image"
src="../../static/course/nobg.jpg"
mode="aspectFit"
<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 data.courseList.records"
:key="item.id"
@click="onPageJump('/pages/course/details/course', item.id, item.title)"
>
</image>
<image v-else :src="item.image"></image>
</view>
<view class="buyItems flexbox">
<view class="txt555">
{{ item.title }}
</view>
<view class="buybtn">
<span>{{ $t('courseHome.buy') }}</span>
<view class="imgcontainer">
<image
v-if="item.image == '' || !item.image"
src="../../static/course/nobg.jpg"
mode="aspectFit"
>
</image>
<image v-else :src="item.image"></image>
</view>
<view class="buyItems flexbox">
<view class="txt555">
{{ item.title }}
</view>
<view class="buybtn">
<span>{{ $t('courseHome.buy') }}</span>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="moreBox shiting">
<text @click="onPageJump('/pages/course/list/tryListen', 1, $t('courseHome.tryListen'))">{{ $t('courseHome.moreTryListen') }}</text>
</view>
<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 userStore = useUserStore()
const { t } = useI18n()
// 系统信息
const scrollTop = ref<number>(0) // 滚动位置
// 下拉刷新状态
const isRefreshing = ref(false)
/**
* 处理搜索点击
*/
@@ -229,41 +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) => {
getPrompt()
if(!userStore.token) return
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
@@ -286,7 +288,7 @@ const getMedicalTags = async () => {
* 医学
* 一级分类点击处理
*/
const curseClick = (item: IMedicalTag, index: number) => {
const curseClick = (item: ICategory, index: number) => {
currentItem.value = item
currentIndex.value = index
@@ -304,35 +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(!userStore.token) return
if(!uni.getStorageSync('token')) return
uni.navigateTo({
url: `/pages/course/list/category?id=${item.id}&title=${item.title}&pid=${item.pid}&subject=${selectedFirstLevel.value}`
})
@@ -343,7 +364,7 @@ const curseClickJump = (item: IMedicalTag) => {
*/
const onPageJump = (url: string, id?: number, title?: string) => {
getPrompt()
if(!userStore.token) return
if(!uni.getStorageSync('token')) return
let targetUrl = url
if (id !== undefined) {
targetUrl += `?id=${id}`
@@ -391,7 +412,7 @@ const getNewsList = async () => {
*/
const newsClick = (item: INews) => {
getPrompt()
if(!userStore.token) return
if(!uni.getStorageSync('token')) return
uni.navigateTo({
url: `/pages/news/details?newsId=${item.id}&url=${item.url}&type=${item.type}`
})
@@ -399,30 +420,17 @@ const newsClick = (item: INews) => {
}
// 精彩试听
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 = () => {
if(!userStore.token) {
console.log(userStore.token);
if(!uni.getStorageSync('token')) {
uni.showModal({
title: '提示',
content: '请先登录后访问该页面',
@@ -441,47 +449,70 @@ const getTryListenList = async () => {
* 统一请求所有数据
*/
const requestAll = async () => {
if(userStore.token){
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)
}
}
/**
* 页面挂载
*/
onMounted(() => {
if(!userStore.token) {
// 重置分类索引
currentIndex.value = 0
if(!uni.getStorageSync('state') && !uni.getStorageSync('token')) {
uni.navigateTo({
url: '/pages/login/login'
});
}
// 重置分类索引
currentIndex.value = 0
if(uni.getStorageSync('state')) uni.removeStorageSync('state')
// 请求所有数据
requestAll()
console.log('进来了2');
})
/**
* 页面显示
*/
onShow(() => {
console.log('进来了1');
// 检查是否有固定的分类选择状态
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()
}
})
/**
@@ -496,15 +527,13 @@ onHide(() => {
* 下拉刷新
*/
onPullDownRefresh(() => {
requestAll().then(() => {
uni.stopPullDownRefresh()
})
handleRefresh()
})
/**
* 页面滚动
*/
onPageScroll((e) => {
onPageScroll((e: any) => {
scrollTop.value = e.scrollTop
})
</script>
@@ -521,7 +550,7 @@ $text-placeholder: #999999;
$border-color: #eeeeee;
.course-home-page {
min-height: 100vh;
height: 100vh;
background-color: $bg-color;
font-size: 28upx;
}
@@ -673,42 +702,47 @@ $border-color: #eeeeee;
box-shadow: 0rpx 0rpx 6rpx 0rpx #f9f6ea;
border-radius: 10rpx;
margin: 0 10rpx;
display: flex;
align-items: center;
justify-content: space-around;
.cate_item_box {
width: 20%;
padding: 40rpx 0 30rpx;
text-align: center;
.cate_list {
display: flex;
align-items: center;
justify-content: space-around;
.cate_item_border {
width: 60rpx;
height: 60rpx;
background-size: 100% 100%;
background-image: url("@/static/icon/cate_bg.png");
border-radius: 4rpx;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
.cate_item_box {
width: 20%;
padding: 40rpx 0 30rpx;
image {
width: 46rpx;
height: 46rpx;
}
}
.cate_item_name {
margin-top: 15rpx;
font-size: 24rpx;
line-height: 34rpx;
text-align: center;
color: #fff;
font-weight: bold;
}
.cate_item_border {
width: 60rpx;
height: 60rpx;
background-size: 100% 100%;
background-image: url("@/static/soul/cate_bg.png");
border-radius: 4rpx;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
image {
width: 46rpx;
height: 46rpx;
}
}
.cate_item_name {
margin-top: 15rpx;
font-size: 24rpx;
line-height: 34rpx;
text-align: center;
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;
}
}
}
@@ -742,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;
@@ -890,7 +924,6 @@ $border-color: #eeeeee;
.learn {
justify-content: space-between;
margin-top: 20rpx;
flex-wrap: wrap;
.item {
@@ -917,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,8 +115,8 @@
</view>
<!-- 游客体验 -->
<view class="youke-l">
<view @click="onPageJump('/pages/course/index')">
<view class="youke-l" v-if="!isAndorid">
<view @click="onPageSwitch('/pages/course/index')">
{{ $t('login.noLogin') }}
</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,7 +400,8 @@ const yszc = () => {
/**
* 页面跳转
*/
const onPageJump = (url: string) => {
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,12 +86,15 @@ const isRelearn = ref<boolean>(false)
const orderType = computed(() => {
return isRelearn.value ? 'relearn' : 'order'
})
// 是否允许积分支付
const allowPointPay = ref<boolean>(false)
/**
* 页面加载
*/
onLoad(async (options: any) => {
try {
allowPointPay.value = options.allowPointPay !== '0'
if (options.isRelearn == 1) {
uni.$on('selectedGoods', async (data: IOrderGoods) => {
// 获取用户信息

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

@@ -1,7 +1,7 @@
<template>
<view class="recharge-page">
<nav-bar :title="$t('user.iHufen')"></nav-bar>
<view class="menu-section" v-if="hufenList.list.length > 0">
<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)">

View File

@@ -1,5 +1,5 @@
<template>
<view class="user-page" :style="{ paddingTop: getNotchHeight() + 30 + 'px' }" v-if="userStore.token">
<template>
<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" />
@@ -63,7 +63,7 @@
</view>
<view class="chong_btn" @click="goRecharge"> </view>
<!-- <text class="wallet_title">{{$t('my.coin')}}<uni-icons type="help" size="19" color="#666"></uni-icons></text>
<text class="wallet_count">{{userMes.peanutCoin}}</text> -->
<text class="wallet_count">{{userMes.peanutCoin}}</text> -->
</view>
</view>
@@ -72,7 +72,7 @@
<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)">
<text v-if="item.hufenState" class="menu-list-hufen">{{hufenData.total ?? 0}}<text
<text v-if="item.hufenState" class="menu-list-hufen">{{hufenData?.total ?? 0}}<text
style="margin-left: 6rpx;">湖分</text></text>
</wd-cell>
</wd-cell-group>
@@ -85,12 +85,11 @@
import { ref, computed, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import { useSysStore } from '@/stores/sys'
import { getUserInfo, getVipInfo, getUserContributionData } 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()
@@ -98,7 +97,6 @@
// 默认头像
const defaultAvatar = '/static/logo.png'
// 用户信息
const userInfo = computed(() => userStore.userInfo)
@@ -126,6 +124,19 @@
url: '/pages/user/myBook/index',
type: 'pageJump'
},
{
id: 9,
name: t('user.myCourses'),
url: '/pages/user/myCourses/index',
type: 'pageJump'
},
{
id: 8,
name: t('user.iHufen'),
url: '/pages/user/hufen/index',
type: 'pageJump',
hufenState: true
},
{
id: 3,
name: t('user.profile'),
@@ -144,29 +155,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'),
type: 'pageJump'
},
// {
// id: 7,
// name: t('user.certificate'),
// url: '/pages/user/certificate/index',
// type: 'pageJump'
// },
{
id: 8,
name: t('user.iHufen'),
url: '/pages/user/hufen/index',
type: 'pageJump',
hufenState: true
},
])
// 湖分
const hufenData = ref('')
const hufenData = ref()
const tokenState = ref(false)
/**
* 获取平台信息
@@ -275,21 +280,34 @@
})
}
onShow(() => {
console.log(userInfo, 'userInfo');
if (userStore.token) {
getData()
/**
* 刷新数据
*/
const handleRefresh = async () => {
if (uni.getStorageSync('token')) {
tokenState.value = true
await Promise.all([
getPlatform(),
getData(),
getHufen()
])
}
}
onShow(() => {
handleRefresh()
})
onMounted(() => {
console.log(userInfo, 'userInfo');
if (userStore.token) {
getPlatform()
getHufen()
}
handleRefresh()
})
onPullDownRefresh(async () => {
handleRefresh().then(() => {
setTimeout(() => {
uni.stopPullDownRefresh()
}, 500)
})
})
</script>
@@ -299,6 +317,7 @@
.user-page {
min-height: calc(100vh - 50px);
background-color: #f7faf9;
position: relative;
}
.settings-icon {

View File

@@ -96,7 +96,7 @@ const formData = ref({
const getMigrateInfo = async () => {
const res = await getUserMigrateInfo()
migrateInfo.value.alreadyMigration = res.alreadyMigration
// migrateInfo.value.notMigration = res.notMigration
migrateInfo.value.notMigration = res.notMigration
}
onMounted(() => {
getMigrateInfo()
@@ -151,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");
uni.navigateTo({
url: '/pages/user/order/details?orderId=' + order.relationId
})
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()
@@ -403,7 +504,7 @@
color: #007bff;
padding: 30rpx 0 20rpx 30rpx;
}
.recharge {
display: flex;
flex-wrap: wrap;

View File

@@ -66,7 +66,7 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useSysStore } from '@/stores/sys'
import { useUserStore } from '@/stores/user'
import { useI18n } from 'vue-i18n'
@@ -74,6 +74,7 @@ import { useMessage } from '@/uni_modules/wot-design-uni'
import { makePhoneCall, copyToClipboard } from '@/utils/index'
// #ifdef APP-PLUS
import update from "@/uni_modules/uni-upgrade-center-app/utils/check-update";
import { getCurrentVersion } from '@/uni_modules/uni-upgrade-center-app/utils/call-check-version'
// #endif
const { t, locale } = useI18n()
@@ -89,6 +90,9 @@ const statusBarHeight = ref(0)
const showQrCode = ref(false)
const showLanguageSelect = ref(false)
// 当前版本号
const currentVersion = ref('')
// 可选语言列表
const availableLanguages = computed(() => [
{ code: 'zh-Hans', name: t('locale.zh-hans') },
@@ -131,7 +135,7 @@ const settingItems = computed(() => [
{
id: 4,
label: t('user.checkVersion'),
value: '',
value: currentVersion.value,
type: 'version'
}
])
@@ -163,14 +167,14 @@ const handleSettingClick = (item: any) => {
* 拨打电话
*/
const handlePhoneCall = (phoneNumber: string, title: string) => {
makePhoneCall(phoneNumber, title, t)
makePhoneCall(phoneNumber, title)
}
/**
* 复制到剪贴板
*/
const handleCopyEmail = (content: string, title: string) => {
copyToClipboard(content, title, t)
copyToClipboard(content, title)
}
/**
@@ -209,14 +213,27 @@ const selectLanguage = (languageCode: string) => {
*/
const checkVersion = async () => {
// #ifdef APP-PLUS
var info = await update();
console.log('版本检测信息', info)
if(info.result.code == 0){
uni.showLoading()
try {
const info = await update();
if(info.result.code == 0){
uni.showToast({
title:info.result.message,
icon:'none'
})
}
} catch (error: any) {
console.error('版本检测失败:', error)
const msg = error?.hasOwnProperty('message') && error.message
uni.showToast({
title:info.result.message,
icon:'none'
title: msg || t('user.checkVersionFailed'),
icon: 'none',
duration: 5000
})
} finally {
uni.hideLoading()
}
// #endif
// #ifndef APP-PLUS
@@ -272,6 +289,12 @@ const performLogout = () => {
url: '/pages/login/login'
})
}
onMounted(async () => {
// #ifdef APP-PLUS
currentVersion.value = await getCurrentVersion()
// #endif
})
</script>
<style lang="scss" scoped>

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");
uni.navigateTo({
url: '/pages/user/order/details?orderId=' + order.relationId
})
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;

View File

@@ -19,9 +19,9 @@
import { ref } from 'vue'
const menuItems = ref([
{
name: '分享APP'
},
// {
// name: '分享APP'
// },
{
name: '关于我们',
}
@@ -55,10 +55,10 @@
provider: "weixin",
scene: "WXSceneSession",
type: 0,
href: '',
href: 'https://a.app.qq.com/o/simple.jsp?pkgname=com.cn.medicine',
title: "吴门医述",
summary: "我正在使用吴门医述提升自己,赶紧跟我一起来体验吧!",
imageUrl: "static/icon/home_icon_logo.png",
imageUrl: "static/logo.png",
success: function (res) {
console.log("success:" + JSON.stringify(res));
},
@@ -72,10 +72,10 @@
provider: "weixin",
scene: "WXSceneTimeline",
type: 0,
href: '',
href: 'https://a.app.qq.com/o/simple.jsp?pkgname=com.cn.medicine',
title: "吴门医述",
summary: "我正在使用吴门医述提升自己,赶紧跟我一起来体验吧!",
imageUrl: "static/icon/home_icon_logo.png",
imageUrl: "static/logo.png",
success: function (res) {
console.log("success:" + JSON.stringify(res));
},
@@ -110,7 +110,7 @@
}
.visitor-block {
padding: 40rpx 20rpx;
padding: 100rpx 20rpx 40rpx 20rpx;
.visitor_img {
width: 150rpx;

View File

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 261 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -212,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;
}
@@ -248,6 +257,9 @@
.table {
display: table;
}
.w-\[50\%\] {
width: 50%;
}
.w-\[100px\] {
width: 100px;
}

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
}
/**

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

@@ -24,4 +24,16 @@ export interface IVipItemProduct {
fee: number | null, // 课程价格
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

@@ -1,31 +1,124 @@
export default function() {
// #ifdef APP-PLUS
return new Promise((resolve, reject) => {
plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) {
let data = {
action: 'checkVersion',
appid: plus.runtime.appid,
appVersion: plus.runtime.version,
wgtVersion: widgetInfo.version
}
uniCloud.callFunction({
name: 'uni-upgrade-center',
data,
success: (e) => {
resolve(e)
},
fail: (error) => {
reject(error)
}
})
})
})
// #endif
// #ifndef APP-PLUS
return new Promise((resolve, reject) => {
reject({
message: '请在App中使用'
})
})
// #endif
}
import { requestUpdatePackage } from '@/api/modules/sys'
import { getMaxVersion } from './tools'
import { getFileExtension } from '@/utils'
/**
* 统一检查更新入口
*/
export default async function checkUpdate() {
// #ifdef APP-PLUS
try {
const upgradeData = await getUpgradeCheckData()
// 优先调用 uni-upgrade-center3 秒超时)
const result = await withTimeout(
callUpgradeCenter(upgradeData),
3000
)
console.log('检查版本更新成功:', result)
return result
} catch (err) {
console.warn('uniCloud更新方案失败启用备用方案:', err?.message)
return await useBackupUpdate()
}
// #endif
// #ifndef APP-PLUS
throw { message: '请在 App 中使用' }
// #endif
}
/**
* 备用更新方案
*/
async function useBackupUpdate() {
const currentVersion = await getCurrentVersion()
if (!currentVersion) {
throw { message: '获取当前版本号失败' }
}
console.log('当前版本号:', currentVersion)
const res = await requestUpdatePackage('10', currentVersion)
if (!res || !res.updateUrl) {
throw { message: '没有匹配的更新包,当前版本无需升级,如您确定有更新,请卸载本版本后前往应用市场重新安装' }
}
// 将服务器返回的更新信息转换为 uni-upgrade-center 格式
return {
result: {
...res,
url: res.updateUrl,
platform: ['Android', 'Ios'],
type: getFileExtension(res.updateUrl),
is_mandatory: true,
is_backup_update: true,
title: "更新",
contents: "当前版本已经弃用,请立即更新",
}
}
}
/**
* uni-upgrade-center 调用 Promise 化
*/
function callUpgradeCenter(data) {
return new Promise((resolve, reject) => {
uniCloud.callFunction({
name: 'uni-upgrade-center',
data,
success: resolve,
fail: reject
})
})
}
/**
* 超时控制
*/
function withTimeout(promise, timeout) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout)
)
])
}
/**
* 获取 upgrade-center 所需参数
*/
function getUpgradeCheckData() {
return new Promise((resolve, reject) => {
plus.runtime.getProperty(
plus.runtime.appid,
(widgetInfo) => {
resolve({
action: 'checkVersion',
appid: plus.runtime.appid,
appVersion: plus.runtime.version,
wgtVersion: widgetInfo.version
})
}
)
})
}
/**
* 获取当前客户端版本app / wgt 取最大)
*/
export async function getCurrentVersion() {
// #ifdef APP-PLUS
const widgetInfo = await new Promise(resolve => {
plus.runtime.getProperty(plus.runtime.appid, resolve)
})
return getMaxVersion(
plus.runtime.version,
widgetInfo.version
)
// #endif
}

View File

@@ -10,6 +10,7 @@ export default function() {
if (!e.result) return;
const {
code,
is_backup_update, // 是否备用更新
message,
is_silently, // 是否静默更新
url, // 安装包下载地址
@@ -17,17 +18,20 @@ export default function() {
type // 安装包类型
} = e.result;
// 此处逻辑仅为实例,可自行编写
if (code > 0) {
// 腾讯云和阿里云下载链接不同,需要处理一下,阿里云会原样返回
const hasUpdate = code > 0 || is_backup_update
// 如果不是备用更新,需要处理下载链接
if (!is_backup_update) {
const {
fileList
} = await uniCloud.getTempFileURL({
fileList: [url]
});
if (fileList[0].tempFileURL)
e.result.url = fileList[0].tempFileURL;
e.result.url = fileList[0].tempFileURL;
}
// 此处逻辑仅为实例,可自行编写
if (hasUpdate) {
resolve(e)
// 静默更新只有wgt有

View File

@@ -0,0 +1,32 @@
/**
* 比较版本号
* @param {string} v1 - 版本号1
* @param {string} v2 - 版本号2
* @returns {number} - 1表示v1大于v2-1表示v1小于v20表示相等
*/
export function compareVersion(v1, v2) {
const arr1 = v1.split('.').map(Number)
const arr2 = v2.split('.').map(Number)
const maxLen = Math.max(arr1.length, arr2.length)
for (let i = 0; i < maxLen; i++) {
const n1 = arr1[i] ?? 0
const n2 = arr2[i] ?? 0
if (n1 > n2) return 1
if (n1 < n2) return -1
}
return 0
}
/**
* 获取较大版本号
* @param {string} v1 - 版本号1
* @param {string} v2 - 版本号2
* @returns {string} - 较大的版本号
*/
export function getMaxVersion(v1, v2) {
return compareVersion(v1, v2) >= 0 ? v1 : v2
}

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

View File

@@ -104,6 +104,7 @@
@include when(active) {
font-weight: 600;
font-size: 30rpx;
}
@include when(disabled) {

View File

@@ -129,4 +129,27 @@ export function parseTime(time: any, cFormat: string) {
return value.toString().padStart(2, '0')
})
return time_str
}
/**
* 获取文件扩展名
* @param {string} url - 文件的URL
* @returns {string} - 文件的扩展名(不包含点号),如果没有扩展名则返回空字符串
*/
export function getFileExtension(url: string) {
// 移除查询参数和hash
const cleanUrl = url.split(/[?#]/)[0];
// 获取文件名
const filename = cleanUrl.split('/').pop();
// 提取扩展名(支持多个点的情况)
const parts = filename?.split('.');
if (parts?.length && parts.length <= 1) {
return ''; // 没有扩展名
}
// 返回最后一个点之后的部分
return parts?.pop() || '';
}