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"; import update from "@/uni_modules/uni-upgrade-center-app/utils/check-update";
// #endif // #endif
import { useUserStore } from '@/stores/user'
export default { export default {
onLaunch: function() { onLaunch: function() {
const userStore = useUserStore()
console.log('App Launch') 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 // #ifdef APP-PLUS
update(); 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'; import { SERVICE_MAP } from '../config';
export const mainClient = createRequestClient({ 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 = { const BASE_URL_MAP = {
development: { development: {
MAIN: 'http://192.168.110.100:9300/pb/', // 张川川 MAIN: 'http://192.168.110.100:9300/pb/', // 张川川
//MAIN: 'https://global.nuttyreading.com/', // 线上 // MAIN: 'https://global.nuttyreading.com/', // 线上
// PAYMENT: 'https://dev-pay.example.com', // 暂时用不到 // PAYMENT: 'https://dev-pay.example.com', // 暂时用不到
// CDN: 'https://cdn-dev.example.com', // 暂时用不到 // CDN: 'https://cdn-dev.example.com', // 暂时用不到
}, },
@@ -21,7 +21,6 @@ const BASE_URL_MAP = {
export const APP_INFO = { export const APP_INFO = {
TYPE: 'abroad', // APP 名称 TYPE: 'abroad', // APP 名称
VERSION_CODE: '1.0.0', // APP 版本号,可能升级的时候会用,这里需要再确定?
} }
export const REQUEST_TIMEOUT = 30000; export const REQUEST_TIMEOUT = 30000;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,7 @@
import { mainClient } from '@/api/clients/main' import { mainClient } from '@/api/clients/main'
import type { IApiResponse } from '@/api/types' import type { IApiResponse } from '@/api/types'
import type { import type {
ICreateOrderParams, ICreateOrderParams,
IGooglePayVerifyParams,
ICreateOrderResponse, ICreateOrderResponse,
IOrderGoods, IOrderGoods,
ICoupon, 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 // api/modules/user.ts
import { mainClient } from '@/api/clients/main' import { mainClient, skeletonClient } from '@/api/clients'
import { paymentClient } from '@/api/clients/payment' import { paymentClient } from '@/api/clients/payment'
import type { IApiResponse } from '@/api/types' import type { IApiResponse } from '@/api/types'
import type { import type {
@@ -17,7 +17,7 @@ import { SERVICE_MAP } from '@/api/config'
* 获取用户信息 * 获取用户信息
*/ */
export async function getUserInfo() { export async function getUserInfo() {
const res = await mainClient.request<IApiResponse<{ user: IUserInfo }>>({ const res = await skeletonClient.request<IApiResponse<{ user: IUserInfo }>>({
url: 'common/user/getUserInfo', url: 'common/user/getUserInfo',
method: 'POST' method: 'POST'
}) })
@@ -324,7 +324,7 @@ export async function migrateUserData(data: { tel: string, code: string, type: s
* } * }
*/ */
export async function getUserMigrateInfo() { export async function getUserMigrateInfo() {
const res = await mainClient.request<IApiResponse>({ const res = await skeletonClient.request<IApiResponse>({
url: 'common/user/getMigrationList', url: 'common/user/getMigrationList',
method: 'POST', method: 'POST',
}) })
@@ -336,7 +336,7 @@ export async function getUserMigrateInfo() {
* @return * @return
*/ */
export async function getUserContributionData() { export async function getUserContributionData() {
const res = await mainClient.request<IApiResponse>({ const res = await skeletonClient.request<IApiResponse>({
url: 'common/userContribution/getUserContribution', url: 'common/userContribution/getUserContribution',
method: 'POST' method: 'POST'
}) })
@@ -357,4 +357,22 @@ export async function getUserContributionByTypeList(current : number, limit : nu
data: { current, limit, type, } data: { current, limit, type, }
}) })
return res 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 { SERVICE_MAP } from '../config'
import type { IApiResponse } from '../types' import type { IApiResponse } from '../types'
import type { IVideoCheckResponse } from '@/types/video' import type { IVideoCheckResponse } from '@/types/video'
import { skeletonClient } from '@/api/clients'
const client = createRequestClient({ baseURL: SERVICE_MAP.MAIN })
/** /**
* 视频相关API * 视频相关API
@@ -18,7 +17,7 @@ export const videoApi = {
checkVideo(data: { checkVideo(data: {
id: number id: number
}) { }) {
return client.request<IVideoCheckResponse>({ return skeletonClient.request<IVideoCheckResponse>({
url: 'sociology/course/checkVideo', url: 'sociology/course/checkVideo',
method: 'POST', method: 'POST',
data data
@@ -34,7 +33,7 @@ export const videoApi = {
videoId: number videoId: number
position: number position: number
}) { }) {
return client.request<IApiResponse>({ return skeletonClient.request<IApiResponse>({
url: 'sociology/course/saveCoursePosition', url: 'sociology/course/saveCoursePosition',
method: 'POST', method: 'POST',
data data
@@ -51,7 +50,7 @@ export const videoApi = {
videoId: number videoId: number
sort: number sort: number
}) { }) {
return client.request<IApiResponse>({ return skeletonClient.request<IApiResponse>({
url: 'medical/course/addErrorCourse', url: 'medical/course/addErrorCourse',
method: 'POST', method: 'POST',
data data

1
api/types.d.ts vendored
View File

@@ -15,6 +15,7 @@ export interface IApiResponse<T = any> {
*/ */
export interface IRequestOptions extends UniApp.RequestOptions { export interface IRequestOptions extends UniApp.RequestOptions {
// 允许扩展额外字段(例如 FILE 上传专用等) // 允许扩展额外字段(例如 FILE 上传专用等)
headers?: Record<string, string>;
maxSize?: number; maxSize?: number;
files?: Array<{ name: string; uri: string; fileType?: string }>; 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"> <view class="points-input-box">
<input <input
v-model="pointsDiscounted" v-model="pointsDiscounted"
type="number" type="digit"
clearable clearable
:placeholder="$t('order.pointsPlaceholder')" :placeholder="$t('order.pointsPlaceholder')"
class="text-right" class="text-right"
@@ -90,7 +90,7 @@
<text class="label">{{ $t('order.total') }}</text> <text class="label">{{ $t('order.total') }}</text>
<text class="amount">{{ finalAmount }} {{ t('global.coin') }}</text> <text class="amount">{{ finalAmount }} {{ t('global.coin') }}</text>
</view> </view>
<wd-button type="primary" @click="handleSubmit"> <wd-button type="primary" :loading="submitLoading" @click="handleSubmit">
{{ $t('order.submit') }} {{ $t('order.submit') }}
</wd-button> </wd-button>
</view> </view>
@@ -128,6 +128,7 @@ import { getUserInfo } from '@/api/modules/user'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { t } from '@/utils/i18n' import { t } from '@/utils/i18n'
import type { IGoods, IGoodsDiscountParams } from '@/types/order' import type { IGoods, IGoodsDiscountParams } from '@/types/order'
import type { IUserInfo } from '@/types/user'
import PayWay from '@/components/order/PayWay.vue' import PayWay from '@/components/order/PayWay.vue'
const userStore = useUserStore() const userStore = useUserStore()
@@ -135,14 +136,13 @@ const userStore = useUserStore()
// 使用页面传参 // 使用页面传参
interface Props { interface Props {
goodsList: IGoods[], goodsList: IGoods[],
userInfo: object, userInfo: IUserInfo,
allowPointPay?: boolean, allowPointPay?: boolean,
orderType?: string, orderType?: string,
backStep?: number // 购买完成后返回几层页面 backStep?: number // 购买完成后返回几层页面
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
goodsList: () => [], goodsList: () => [],
userInfo: () => ({}),
allowPointPay: true, allowPointPay: true,
orderType: 'order', orderType: 'order',
backStep: 1 backStep: 1
@@ -270,13 +270,24 @@ const calculatePromotionDiscounted = async () => {
const handlePointsInput = (value: any) => { const handlePointsInput = (value: any) => {
let val = String(value.detail.value) 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 pointsDiscounted.value = 0
} else { } else {
let numericValue = parseInt(val, 10) let numericValue = parseFloat(val)
if (numericValue < 0 || isNaN(numericValue)) { if (numericValue < 0 || isNaN(numericValue)) {
numericValue = 0 numericValue = 0
} }
@@ -316,7 +327,7 @@ const calculateFinalPrice = () => {
const orderAmountAfterDiscount = totalAmount.value - promotionDiscounted.value - vipDiscounted.value - couponAmount const orderAmountAfterDiscount = totalAmount.value - promotionDiscounted.value - vipDiscounted.value - couponAmount
pointsUsableMax.value = Math.min( pointsUsableMax.value = Math.min(
props?.userInfo?.jf || 0, props?.userInfo?.jf || 0,
Math.floor(props.allowPointPay ? orderAmountAfterDiscount : 0) props.allowPointPay ? orderAmountAfterDiscount : 0
) )
pointsDiscounted.value = pointsUsableMax.value 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 if (!validateOrder()) return
submitLoading.value = true
// 创建订单 此app用天医币支付创建订单成功即支付成功 try {
await createOrder() // 创建订单 此app用天医币支付创建订单成功即支付成功
await createOrder()
// 重新获取用户信息更新store和本地缓存 // 重新获取用户信息更新store和本地缓存
const res = await getUserInfo() const res = await getUserInfo()
userStore.setUserInfo(res.result) userStore.setUserInfo(res.result)
uni.showToast({ uni.showToast({
title: t('order.orderSuccess'), title: t('order.orderSuccess'),
icon: 'success' icon: 'success'
})
// 返回上一页
setTimeout(() => {
uni.navigateBack({
delta: props.backStep
}) })
}, 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) { } catch (err: any) {
console.error('Failed to fetch video info:', err) console.error('Failed to fetch video info:', err)
error.value = { error.value = {
type: 'API_ERROR', type: 'API_ERROR' as VideoErrorType,
message: err.message || '获取视频信息失败,请稍后重试' message: err.message || '获取视频信息失败,请稍后重试'
} }
return null return null

View File

@@ -20,7 +20,7 @@
"coin": "Coin", "coin": "Coin",
"days": "Days", "days": "Days",
"and": "and", "and": "and",
"call": "Call" "queryFailed": "Query failed"
}, },
"tabar.course": "COURSE", "tabar.course": "COURSE",
"tabar.book": "EBOOK", "tabar.book": "EBOOK",
@@ -145,7 +145,7 @@
"updateFailed": "Update Failed", "updateFailed": "Update Failed",
"paymentMethod": "Payment Method", "paymentMethod": "Payment Method",
"googlePay": "Google Pay", "googlePay": "Google Pay",
"applePay": "Apple Pay", "applePay": "In-App Purchase",
"agreeText": "I have agreed", "agreeText": "I have agreed",
"agreement": "Recharge Agreement", "agreement": "Recharge Agreement",
"agreeFirst": "Please agree to the recharge agreement first", "agreeFirst": "Please agree to the recharge agreement first",
@@ -223,8 +223,8 @@
"migrateCodePlaceholder": "The code obtained from chinese account", "migrateCodePlaceholder": "The code obtained from chinese account",
"migrateWarning": "Migration is irreversible, please proceed with caution!", "migrateWarning": "Migration is irreversible, please proceed with caution!",
"migrateInstructions": "Migration Instructions", "migrateInstructions": "Migration Instructions",
"instruction1": "Please obtain the migration code from your chinese account, get it by going to 【我的】-【数据迁移】-【获取迁移验证码】.", "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", "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.", "instruction3": "The migration process may take a few minutes, please be patient.",
"instruction4": "If you encounter any issues, please contact customer service for assistance.", "instruction4": "If you encounter any issues, please contact customer service for assistance.",
"alreadyMigrated": "You have already migrated:", "alreadyMigrated": "You have already migrated:",
@@ -234,7 +234,10 @@
"certificate": "My certificate", "certificate": "My certificate",
"iHufen": "My lake", "iHufen": "My lake",
"hufenRecord": "Lake division record", "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": { "book": {
"title": "My Books", "title": "My Books",

View File

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

View File

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

View File

@@ -3,6 +3,8 @@
{ {
"path": "pages/course/index", "path": "pages/course/index",
"style": { "style": {
"enablePullDownRefresh": true,
"navigationStyle": "custom",
"navigationBarTitleText": "%index.title%" "navigationBarTitleText": "%index.title%"
} }
}, { }, {
@@ -23,7 +25,8 @@
"path": "pages/user/index", "path": "pages/user/index",
"style": { "style": {
"navigationBarTitleText": "%user.title%", "navigationBarTitleText": "%user.title%",
"navigationStyle": "custom" "navigationStyle": "custom",
"enablePullDownRefresh": true
} }
}, { }, {
"path": "pages/user/order/index", "path": "pages/user/order/index",
@@ -109,9 +112,16 @@
"navigationStyle": "custom", "navigationStyle": "custom",
"navigationBarTitleText": "%book.title%" "navigationBarTitleText": "%book.title%"
} }
}, {
"path": "pages/user/myCourses/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "%book.title%"
}
}, { }, {
"path": "pages/book/index", "path": "pages/book/index",
"style": { "style": {
"enablePullDownRefresh": true,
"navigationStyle": "custom", "navigationStyle": "custom",
"navigationBarTitleText": "%book.title%" "navigationBarTitleText": "%book.title%"
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,80 +31,87 @@
>{{ item }}</view> >{{ item }}</view>
</view> </view>
<!-- 医学 --> <!-- 医学 -->
<view v-if="selectedFirstLevel === '医学'" class="newLeve2"> <Skeleton
<view class="home_nar nomargin" style="padding: 0; background-color: #fff"> v-if="selectedFirstLevel === '医学'"
<view class="flexbox"> 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 <view
:class="['hn_cl_tit', currentIndex == index ? 'active' : '']" class="fourBox"
@click="curseClick(item, index)" style="padding: 0; padding-bottom: 8rpx"
v-for="(item, index) in curseTagList" v-if="sbuMedicalTagsList?.length > 0"
:key="item.id"
> >
<image :src="item.icon" mode="aspectFit"></image> <view
<text>{{ item.title }}</text> 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>
</view> </template>
<view </Skeleton>
class="fourBox" <!-- 心理学和国学 -->
style="padding: 0; padding-bottom: 8rpx" <Skeleton
v-if="sbuMedicalTagsList?.length > 0" v-if="selectedFirstLevel === '心理学' || selectedFirstLevel === '国学'"
> ref="menuSkeletonRef"
theme="menu"
:request="getMenus"
>
<template #content="{ data }">
<view <view
class="childrenBox fourIcon flexbox" v-if="data.labels.length > 0"
style="justify-content: space-around" :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 <view
class="cate_item_box" class="cate_item_box"
v-for="(item, index) in soulCateList" v-for="item in data.labels"
:key="index"
@click="curseClickJump(item)" @click="curseClickJump(item)"
> >
<view class="cate_item_border"> <view class="cate_item_border">
<image :src="item.icon"></image> <image
:src="item.icon"
mode="aspectFill"
style="width: 49rpx; height: 49rpx"
></image>
</view> </view>
<view class="cate_item_name">{{ item.title }}</view> <view class="cate_item_name">{{ item.title }}</view>
</view> </view>
</view> </view>
</scroll-view> </template>
</view> </Skeleton>
<!-- 国学 -->
<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>
<!-- 观看记录区域 --> <!-- 观看记录区域 -->
<view class="learnBox" v-if="learnList.length > 0"> <view class="learnBox" v-if="learnList.length > 0">
@@ -143,7 +150,7 @@
<view class="newscoll"> <view class="newscoll">
<swiper <swiper
class="swiper" class="swiper"
interval="5000" :interval="5000"
circular circular
autoplay autoplay
vertical vertical
@@ -163,62 +170,73 @@
</view> </view>
<!-- 精彩试听区域 --> <!-- 精彩试听区域 -->
<view class="learnBox" v-if="tryListenList.length > 0"> <view class="learnBox">
<view class="titleBox flexbox"> <view class="titleBox flexbox">
<image src="../../static/course/try_listen.png" mode="aspectFit"></image> <image src="../../static/course/try_listen.png" mode="aspectFit"></image>
<text>{{ $t('courseHome.tryListen') }}</text> <text>{{ $t('courseHome.tryListen') }}</text>
</view> </view>
<view class="learn flexbox shiting"> <Skeleton
<view theme="image-card"
class="item" class=""
v-for="(item, index) in tryListenList" :size="Array(2).fill({ width: '48%', height: '260rpx' })"
:key="item.id" :count="3"
@click="onPageJump('/pages/course/details/course', item.id, item.title)" :request="getTryListenList"
> >
<view class="imgcontainer"> <template #content="{ data }">
<image <view v-if="data.courseList.records.length > 0" class="learn flexbox shiting">
v-if="item.image == '' || !item.image" <view
src="../../static/course/nobg.jpg" class="item"
mode="aspectFit" v-for="(item, index) in data.courseList.records"
:key="item.id"
@click="onPageJump('/pages/course/details/course', item.id, item.title)"
> >
</image> <view class="imgcontainer">
<image v-else :src="item.image"></image> <image
</view> v-if="item.image == '' || !item.image"
<view class="buyItems flexbox"> src="../../static/course/nobg.jpg"
<view class="txt555"> mode="aspectFit"
{{ item.title }} >
</view> </image>
<view class="buybtn"> <image v-else :src="item.image"></image>
<span>{{ $t('courseHome.buy') }}</span> </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> <view class="moreBox shiting">
</view> <text @click="onPageJump('/pages/course/list/tryListen', 1, $t('courseHome.tryListen'))">{{ $t('courseHome.moreTryListen') }}</text>
<view class="moreBox shiting"> </view>
<text @click="onPageJump('/pages/course/list/tryListen', 1, $t('courseHome.tryListen'))">{{ $t('courseHome.moreTryListen') }}</text> </template>
</view> </Skeleton>
</view> </view>
</view> </view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { onShow, onHide, onPullDownRefresh, onPageScroll, onLoad } from '@dcloudio/uni-app' import { onShow, onHide, onPullDownRefresh, onPageScroll } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import { courseApi } from '@/api/modules/course' import { courseApi } from '@/api/modules/course'
import { courseSubjectClassificationApi } from '@/api/modules/cousre_subject_classification' import { courseSubjectClassificationApi } from '@/api/modules/cousre_subject_classification'
import { commonApi } from '@/api/modules/common' import { commonApi } from '@/api/modules/common'
import { getNotchHeight } from '@/utils/system' import { getNotchHeight } from '@/utils/system'
// import { onPageJump } from '@/utils' // 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' import { useUserStore } from '@/stores/user'
const userStore = useUserStore() const userStore = useUserStore()
const { t } = useI18n()
// 系统信息 // 系统信息
const scrollTop = ref<number>(0) // 滚动位置 const scrollTop = ref<number>(0) // 滚动位置
// 下拉刷新状态
const isRefreshing = ref(false)
/** /**
* 处理搜索点击 * 处理搜索点击
*/ */
@@ -229,41 +247,25 @@ const handleSearch = ({ value }: { value: string }) => {
} }
// 分类相关 // 分类相关
const curseTagList = ref<IMedicalTag[]>([]) // 一级分类标签列表 const curseTagList = ref<ICategory[]>([]) // 一级分类标签列表
const sbuMedicalTagsList = ref<IMedicalTag[]>([]) // 二级分类标签列表 const sbuMedicalTagsList = ref<ICategory[]>([]) // 二级分类标签列表
const currentIndex = ref<number>(0) // 当前选中的一级分类索引 const currentIndex = ref<number>(0) // 当前选中的一级分类索引
const currentItem = ref<IMedicalTag | null>(null) // 当前选中的一级分类项 const currentItem = ref<ICategory | null>(null) // 当前选中的一级分类项
// 学科数据 // 学科数据
const firstLevelCategories = ref<string[]>(['医学', '心理学', '国学']) const firstLevelCategories = ref<string[]>(['医学', '心理学', '国学'])
const selectedFirstLevel = ref<number>('医学') // 当前选中的一级分类索引 const selectedFirstLevel = ref('医学') // 当前选中的一级分类索引
/**
* 学科点击处理
*/
const handleFirstLevelClick = (item: string) => {
getPrompt()
if(!userStore.token) return
selectedFirstLevel.value = item
switch (item) {
case '医学':
getMedicalTags()
break
case '心理学':
getSoulCateList()
break
case '国学':
getSociologyCateList()
break
}
}
/** /**
* 医学 * 医学
* 获取课程分类数据 * 获取课程分类数据
*/ */
const getMedicalTags = async () => { const medicineMenuSkeletonRef = ref()
const getMedicineMenus = () => {
sbuMedicalTagsList.value = [] sbuMedicalTagsList.value = []
const res = await courseSubjectClassificationApi.getCourseMedicalTree() return courseSubjectClassificationApi.getCourseMedicalTree()
}
const getMedicineMenusSuccess = (res: any) => {
if (res && res.code === 0) { if (res && res.code === 0) {
if (res.labels && res.labels.length > 0) { if (res.labels && res.labels.length > 0) {
curseTagList.value = res.labels curseTagList.value = res.labels
@@ -286,7 +288,7 @@ const getMedicalTags = async () => {
* 医学 * 医学
* 一级分类点击处理 * 一级分类点击处理
*/ */
const curseClick = (item: IMedicalTag, index: number) => { const curseClick = (item: ICategory, index: number) => {
currentItem.value = item currentItem.value = item
currentIndex.value = index currentIndex.value = index
@@ -304,35 +306,54 @@ const curseClick = (item: IMedicalTag, index: number) => {
} }
/** /**
* 心理学 * 菜单
* 获取课程分类数据 * 获取课程分类数据
*/ */
const soulCateList = ref<IMedicalTag[]>([]) const menuSkeletonRef = ref()
const getSoulCateList = async () => { const getMenus = () => {
const res = await courseSubjectClassificationApi.getCourseSoulTree() switch (selectedFirstLevel.value) {
if (res.labels&&res.labels.length>0) { // case '医学':
soulCateList.value = res.labels; // return getMedicalTags()
case '心理学':
return courseSubjectClassificationApi.getCourseSoulTree()
case '国学':
return courseSubjectClassificationApi.getCourseSociologyTree()
} }
} }
/** /**
* * 学科点击处理
* 获取课程分类数据
*/ */
const sociologyCateList = ref<IMedicalTag[]>([]) const handleFirstLevelClick = (item: string) => {
const getSociologyCateList = async () => { getPrompt()
const res = await courseSubjectClassificationApi.getCourseSociologyTree() if(!uni.getStorageSync('token')) return
if (res.labels&&res.labels.length>0) { selectedFirstLevel.value = item
sociologyCateList.value = res.labels; 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() getPrompt()
if(!userStore.token) return if(!uni.getStorageSync('token')) return
uni.navigateTo({ uni.navigateTo({
url: `/pages/course/list/category?id=${item.id}&title=${item.title}&pid=${item.pid}&subject=${selectedFirstLevel.value}` 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) => { const onPageJump = (url: string, id?: number, title?: string) => {
getPrompt() getPrompt()
if(!userStore.token) return if(!uni.getStorageSync('token')) return
let targetUrl = url let targetUrl = url
if (id !== undefined) { if (id !== undefined) {
targetUrl += `?id=${id}` targetUrl += `?id=${id}`
@@ -391,7 +412,7 @@ const getNewsList = async () => {
*/ */
const newsClick = (item: INews) => { const newsClick = (item: INews) => {
getPrompt() getPrompt()
if(!userStore.token) return if(!uni.getStorageSync('token')) return
uni.navigateTo({ uni.navigateTo({
url: `/pages/news/details?newsId=${item.id}&url=${item.url}&type=${item.type}` 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 getTryListenList = () => courseApi.getMarketCourseList({ page: 1, limit: 6, id: 1 })
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 getPrompt = () => { const getPrompt = () => {
if(!userStore.token) { console.log(userStore.token);
if(!uni.getStorageSync('token')) {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
content: '请先登录后访问该页面', content: '请先登录后访问该页面',
@@ -441,47 +449,70 @@ const getTryListenList = async () => {
* 统一请求所有数据 * 统一请求所有数据
*/ */
const requestAll = async () => { const requestAll = async () => {
if(userStore.token){ if(uni.getStorageSync('token')){
getLearnCourse() getLearnCourse()
} }
getMedicalTags()
getTryListenList() getTryListenList()
getNewsList() 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(() => { onMounted(() => {
if(!userStore.token) { // 重置分类索引
currentIndex.value = 0
if(!uni.getStorageSync('state') && !uni.getStorageSync('token')) {
uni.navigateTo({ uni.navigateTo({
url: '/pages/login/login' url: '/pages/login/login'
}); });
} }
// 重置分类索引 if(uni.getStorageSync('state')) uni.removeStorageSync('state')
currentIndex.value = 0
// 请求所有数据 // 请求所有数据
requestAll() requestAll()
console.log('进来了2');
}) })
/** /**
* 页面显示 * 页面显示
*/ */
onShow(() => { onShow(() => {
console.log('进来了1');
// 检查是否有固定的分类选择状态 // 检查是否有固定的分类选择状态
const fixed = uni.getStorageSync('fixed') // const fixed = uni.getStorageSync('fixed')
if (fixed && currentItem.value) { // if (fixed && currentItem.value) {
curseClick(currentItem.value, currentIndex.value) // curseClick(currentItem.value, currentIndex.value)
} else { // } else {
currentIndex.value = 0 // currentIndex.value = 0
currentItem.value = null // currentItem.value = null
} // }
// 刷新数据 // 刷新数据
requestAll() // requestAll()
if(uni.getStorageSync('token')){
getLearnCourse()
}
}) })
/** /**
@@ -496,15 +527,13 @@ onHide(() => {
* 下拉刷新 * 下拉刷新
*/ */
onPullDownRefresh(() => { onPullDownRefresh(() => {
requestAll().then(() => { handleRefresh()
uni.stopPullDownRefresh()
})
}) })
/** /**
* 页面滚动 * 页面滚动
*/ */
onPageScroll((e) => { onPageScroll((e: any) => {
scrollTop.value = e.scrollTop scrollTop.value = e.scrollTop
}) })
</script> </script>
@@ -521,7 +550,7 @@ $text-placeholder: #999999;
$border-color: #eeeeee; $border-color: #eeeeee;
.course-home-page { .course-home-page {
min-height: 100vh; height: 100vh;
background-color: $bg-color; background-color: $bg-color;
font-size: 28upx; font-size: 28upx;
} }
@@ -673,42 +702,47 @@ $border-color: #eeeeee;
box-shadow: 0rpx 0rpx 6rpx 0rpx #f9f6ea; box-shadow: 0rpx 0rpx 6rpx 0rpx #f9f6ea;
border-radius: 10rpx; border-radius: 10rpx;
margin: 0 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 { .cate_item_border {
display: flex; width: 60rpx;
align-items: center; height: 60rpx;
justify-content: space-around; 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 { image {
width: 20%; width: 46rpx;
padding: 40rpx 0 30rpx; height: 46rpx;
}
}
.cate_item_name {
margin-top: 15rpx;
font-size: 24rpx;
line-height: 34rpx;
text-align: center; text-align: center;
color: #fff;
font-weight: bold;
}
.cate_item_border { .child-menu {
width: 60rpx; display: none;
height: 60rpx; background-color: #000;
background-size: 100% 100%; box-shadow: 0 4rpx 12rpx rgba(51, 97, 165, 0.08);
background-image: url("@/static/soul/cate_bg.png"); border-radius: 0 0 20rpx 20rpx;
border-radius: 4rpx; padding: 10rpx 0;
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;
}
} }
} }
} }
@@ -742,7 +776,7 @@ $border-color: #eeeeee;
width: 65rpx; width: 65rpx;
height: 78rpx; height: 78rpx;
background-size: 100% 100%; background-size: 100% 100%;
background-image: url("@/static/soul/cate_bg.png"); background-image: url("@/static/icon/cate_bg.png");
border-radius: 4rpx; border-radius: 4rpx;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -890,7 +924,6 @@ $border-color: #eeeeee;
.learn { .learn {
justify-content: space-between; justify-content: space-between;
margin-top: 20rpx;
flex-wrap: wrap; flex-wrap: wrap;
.item { .item {
@@ -917,6 +950,7 @@ $border-color: #eeeeee;
.titleBox { .titleBox {
align-items: center; align-items: center;
margin-bottom: 20rpx;
image { image {
width: 50rpx; width: 50rpx;

View File

@@ -11,7 +11,12 @@
<!-- 子级分类 --> <!-- 子级分类 -->
<view v-if="selectedTab && selectedTab.children && selectedTab.children.length > 0" class="sub-category-list"> <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> </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 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 courseList = ref<ICourse[]>([]) // 课程列表
const selectedCategoryId = computed(() => { 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> <template>
<view class="page"> <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"> <view class="input-box">
@@ -33,7 +35,7 @@
<input <input
class="input-text" class="input-text"
type="password" type="password"
maxlength="20" :maxlength="20"
v-model="password" v-model="password"
:placeholder="$t('forget.passwordPlaceholder')" :placeholder="$t('forget.passwordPlaceholder')"
@input="inputMethod(password)" @input="inputMethod(password)"
@@ -54,8 +56,8 @@
<input <input
class="input-text" class="input-text"
type="password" type="password"
minlength="8" :minlength="8"
maxlength="20" :maxlength="20"
v-model="confirmPassword" v-model="confirmPassword"
:placeholder="$t('forget.passwordAgainPlaceholder')" :placeholder="$t('forget.passwordAgainPlaceholder')"
/> />

View File

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

View File

@@ -4,7 +4,7 @@
<nav-bar :title="$t('order.confirmTitle')" /> <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> <template #goodsList>
<!-- 商品列表内容 --> <!-- 商品列表内容 -->
<view <view
@@ -86,12 +86,15 @@ const isRelearn = ref<boolean>(false)
const orderType = computed(() => { const orderType = computed(() => {
return isRelearn.value ? 'relearn' : 'order' return isRelearn.value ? 'relearn' : 'order'
}) })
// 是否允许积分支付
const allowPointPay = ref<boolean>(false)
/** /**
* 页面加载 * 页面加载
*/ */
onLoad(async (options: any) => { onLoad(async (options: any) => {
try { try {
allowPointPay.value = options.allowPointPay !== '0'
if (options.isRelearn == 1) { if (options.isRelearn == 1) {
uni.$on('selectedGoods', async (data: IOrderGoods) => { uni.$on('selectedGoods', async (data: IOrderGoods) => {
// 获取用户信息 // 获取用户信息

View File

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

View File

@@ -1,7 +1,7 @@
<template> <template>
<view class="recharge-page"> <view class="recharge-page">
<nav-bar :title="$t('user.iHufen')"></nav-bar> <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-group border class="menu-list">
<wd-cell v-for="item in hufenList.list" :key="item.type" :title="item.dict_value" is-link <wd-cell v-for="item in hufenList.list" :key="item.type" :title="item.dict_value" is-link
@click="handleMenuClick(item)"> @click="handleMenuClick(item)">

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="user-page" :style="{ paddingTop: getNotchHeight() + 30 + 'px' }" v-if="userStore.token"> <view class="user-page" :style="{ paddingTop: getNotchHeight() + 30 + 'px' }" v-if="tokenState">
<!-- 设置图标 --> <!-- 设置图标 -->
<view class="settings-icon" :style="{ top: getNotchHeight() + 30 + 'px' }" @click="goSettings"> <view class="settings-icon" :style="{ top: getNotchHeight() + 30 + 'px' }" @click="goSettings">
<wd-icon name="setting1" size="24px" color="#666" /> <wd-icon name="setting1" size="24px" color="#666" />
@@ -63,7 +63,7 @@
</view> </view>
<view class="chong_btn" @click="goRecharge"> </view> <view class="chong_btn" @click="goRecharge"> </view>
<!-- <text class="wallet_title">{{$t('my.coin')}}<uni-icons type="help" size="19" color="#666"></uni-icons></text> <!-- <text class="wallet_title">{{$t('my.coin')}}<uni-icons type="help" size="19" color="#666"></uni-icons></text>
<text class="wallet_count">{{userMes.peanutCoin}}</text> --> <text class="wallet_count">{{userMes.peanutCoin}}</text> -->
</view> </view>
</view> </view>
@@ -72,7 +72,7 @@
<wd-cell-group border class="menu-list"> <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 <wd-cell v-for="item in menuItems" :key="item.id" :title="item.name" :label="item.desc" is-link
@click="handleMenuClick(item)"> @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> style="margin-left: 6rpx;">湖分</text></text>
</wd-cell> </wd-cell>
</wd-cell-group> </wd-cell-group>
@@ -85,12 +85,11 @@
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { useSysStore } from '@/stores/sys' import { useSysStore } from '@/stores/sys'
import { getUserInfo, getVipInfo, getUserContributionData } from '@/api/modules/user' import { getUserInfo, getUserContributionData } from '@/api/modules/user'
import type { IVipInfo } from '@/types/user'
import { getNotchHeight } from '@/utils/system' import { getNotchHeight } from '@/utils/system'
import { parseTime } from '@/utils/index' import { parseTime } from '@/utils/index'
import { t } from '@/utils/i18n' import { t } from '@/utils/i18n'
import { onShow } from '@dcloudio/uni-app' import { onShow, onPullDownRefresh } from '@dcloudio/uni-app'
import visitor from '@/pages/visitor/index.vue'; import visitor from '@/pages/visitor/index.vue';
const userStore = useUserStore() const userStore = useUserStore()
@@ -98,7 +97,6 @@
// 默认头像 // 默认头像
const defaultAvatar = '/static/logo.png' const defaultAvatar = '/static/logo.png'
// 用户信息 // 用户信息
const userInfo = computed(() => userStore.userInfo) const userInfo = computed(() => userStore.userInfo)
@@ -126,6 +124,19 @@
url: '/pages/user/myBook/index', url: '/pages/user/myBook/index',
type: 'pageJump' 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, id: 3,
name: t('user.profile'), name: t('user.profile'),
@@ -144,29 +155,23 @@
url: '/pages/user/feedback/index', url: '/pages/user/feedback/index',
type: 'pageJump' type: 'pageJump'
}, },
// { {
// id: 6, id: 6,
// name: t('user.dataMigrate'), name: t('user.dataMigrate'),
// url: '/pages/user/migrate/index', url: '/pages/user/migrate/index',
// desc: t('user.migrateSubtitle'), desc: t('user.migrateSubtitle'),
// type: 'pageJump' type: 'pageJump'
// } },
// { // {
// id: 7, // id: 7,
// name: t('user.certificate'), // name: t('user.certificate'),
// url: '/pages/user/certificate/index', // url: '/pages/user/certificate/index',
// type: 'pageJump' // 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(() => { onMounted(() => {
console.log(userInfo, 'userInfo'); handleRefresh()
if (userStore.token) { })
getPlatform()
getHufen()
}
onPullDownRefresh(async () => {
handleRefresh().then(() => {
setTimeout(() => {
uni.stopPullDownRefresh()
}, 500)
})
}) })
</script> </script>
@@ -299,6 +317,7 @@
.user-page { .user-page {
min-height: calc(100vh - 50px); min-height: calc(100vh - 50px);
background-color: #f7faf9; background-color: #f7faf9;
position: relative;
} }
.settings-icon { .settings-icon {

View File

@@ -96,7 +96,7 @@ const formData = ref({
const getMigrateInfo = async () => { const getMigrateInfo = async () => {
const res = await getUserMigrateInfo() const res = await getUserMigrateInfo()
migrateInfo.value.alreadyMigration = res.alreadyMigration migrateInfo.value.alreadyMigration = res.alreadyMigration
// migrateInfo.value.notMigration = res.notMigration migrateInfo.value.notMigration = res.notMigration
} }
onMounted(() => { onMounted(() => {
getMigrateInfo() getMigrateInfo()
@@ -151,6 +151,9 @@ const submitMigrate = async () => {
// 清空表单 // 清空表单
formData.value.tel = '' formData.value.tel = ''
formData.value.code = '' formData.value.code = ''
// 刷新用户迁移信息
getMigrateInfo()
} }
</script> </script>

View File

@@ -16,7 +16,7 @@
<!-- 付款信息 --> <!-- 付款信息 -->
<wd-cell-group> <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.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.vipDiscountAmount > 0" title="VIP专享立减" :value="`- ${order.vipDiscountAmount} ${$t('global.coin')}`" />
<wd-cell v-if="order.jfDeduction > 0" title="积分抵扣"> <wd-cell v-if="order.jfDeduction > 0" title="积分抵扣">

View File

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

View File

@@ -3,13 +3,13 @@
<!-- 自定义导航栏 --> <!-- 自定义导航栏 -->
<nav-bar :title="$t('order.recharge')"></nav-bar> <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">{{$t('order.rechargeAmount')}}</view> -->
<view class="text">活动充值金额</view> <view class="text">活动充值金额</view>
<view class="recharge"> <view class="recharge">
<view class="recharge_block" @click="chosPric(item)" <view class="recharge_block" @click="chosPric(item)"
:class="aloneItem.priceTypeId === item.priceTypeId ? 'selected' : ''" :class="aloneItem.priceTypeId === item.priceTypeId ? 'selected' : ''" v-for="item in eventAmountList"
v-for="item in eventAmountList" :key="item.priceTypeId"> :key="item.priceTypeId">
<view class="recharge_money">NZ${{item.realMoney}}</view> <view class="recharge_money">NZ${{item.realMoney}}</view>
<view style="font-size: 26rpx;">{{item.money}}{{ $t('global.coin') }}</view> <view style="font-size: 26rpx;">{{item.money}}{{ $t('global.coin') }}</view>
<span class="activity-label" v-if="item.givejf >0">{{item.description}}</span> <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="text">{{$t('order.rechargeAmount')}}</view>
<view class="recharge"> <view class="recharge">
<view class="recharge_block" @click="chosPric(item)" <view class="recharge_block" @click="chosPric(item)"
:class="aloneItem.priceTypeId === item.priceTypeId ? 'selected' : ''" :class="aloneItem.priceTypeId === item.priceTypeId ? 'selected' : ''" v-for="item in standardAmountList"
v-for="item in standardAmountList" :key="item.priceTypeId"> :key="item.priceTypeId">
<view class="recharge_money">NZ${{item.realMoney}}</view> <view class="recharge_money">NZ${{item.realMoney}}</view>
<view style="font-size: 26rpx;">{{item.money}}{{ $t('global.coin') }}</view> <view style="font-size: 26rpx;">{{item.money}}{{ $t('global.coin') }}</view>
<span class="activity-label" v-if="item.givejf >0">{{item.description}}</span> <span class="activity-label" v-if="item.givejf >0">{{item.description}}</span>
@@ -39,13 +39,11 @@
<view class="cha_fangsh"> <view class="cha_fangsh">
<view class="cf_title">{{$t('user.paymentMethod')}}</view> <view class="cf_title">{{$t('user.paymentMethod')}}</view>
<view class="cf_radio"> <view class="cf_radio">
<radio-group v-for="item in iosPaylist"> <radio-group>
<view> <view>
<view :class="payType == item.id ? 'Tab_xf cf_xuanx' : 'cf_xuanx'"> <view class="cf_xuanx">
<!-- <image class="pay_item_img" :src="item.imgUrl" mode="aspectFil"> <text>{{ isAndroid ? $t('user.googlePay') : $t('user.applePay')}}</text>
</image> --> <radio checked="true;" @click="choseType(item.id)"></radio>
<text>{{ item.title }}</text>
<radio :checked="payType === item.id" @click="choseType(item.id)"></radio>
</view> </view>
</view> </view>
</radio-group> </radio-group>
@@ -80,7 +78,7 @@
import { ref, computed, onMounted, toRefs, reactive } from 'vue' import { ref, computed, onMounted, toRefs, reactive } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useMessage } from '@/uni_modules/wot-design-uni' import { useMessage } from '@/uni_modules/wot-design-uni'
import { getBookBuyConfigList, getAgreement, getActivityDescription, verifyGooglePay, getPlaceOrder } from '@/api/modules/user' import { getBookBuyConfigList, getAgreement, getActivityDescription, verifyGooglePay, getPlaceOrder, getIosPayment } from '@/api/modules/user'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { useThrottle } from '@/hooks/useThrottle'; import { useThrottle } from '@/hooks/useThrottle';
@@ -88,14 +86,6 @@
const userStore = useUserStore() const userStore = useUserStore()
const { t } = useI18n() const { t } = useI18n()
const message = useMessage() const message = useMessage()
const payType = ref('1')
const iosPaylist = ref([
{
title: "Google Pay",
id: '1',
// imgUrl: "/static/icon/currency.png"
}
])
// 充值列表 // 充值列表
const rechargeList = ref([]) const rechargeList = ref([])
// 金额列表单独每项 // 金额列表单独每项
@@ -135,6 +125,8 @@
const eventAmountList = ref([]) const eventAmountList = ref([])
//正常金额数据 //正常金额数据
const standardAmountList = ref([]) const standardAmountList = ref([])
// 声明ios实例
const iapChannel = ref(null)
/** /**
* 获取使用环境 * 获取使用环境
@@ -145,7 +137,8 @@
isAndroid.value = true; isAndroid.value = true;
console.log('运行Android上') console.log('运行Android上')
} else { } else {
qudao.value = 'Google' isAndroid.value = false;
qudao.value = 'IOS'
console.log('运行iOS上') console.log('运行iOS上')
} }
getData() getData()
@@ -189,10 +182,11 @@
* 获取订单编号 * 获取订单编号
*/ */
const getPlaceOrderObj = async () => { const getPlaceOrderObj = async () => {
console.log(isAndroid.value);
const { priceTypeId, realMoney, money } = toRefs(aloneItem.value) const { priceTypeId, realMoney, money } = toRefs(aloneItem.value)
const data = { const data = {
userId: userStore.userInfo.id, // 用户di userId: userStore.userInfo.id, // 用户di
paymentMethod: '5', //支付方式4point 5google paymentMethod: isAndroid.value ? '5' : '3', //支付方式3ios 5google
orderMoney: money.value, //订单金额 orderMoney: money.value, //订单金额
realMoney: realMoney.value, //实际金额 realMoney: realMoney.value, //实际金额
come: '10', //订单来源 2医学吴门医述 10海外读书 come: '10', //订单来源 2医学吴门医述 10海外读书
@@ -200,12 +194,12 @@
productId: priceTypeId.value // 商品id productId: priceTypeId.value // 商品id
} }
try { try {
// uni.hideLoading()
const res = await getPlaceOrder(data) const res = await getPlaceOrder(data)
orderSn.value = res.orderSn orderSn.value = res.orderSn
console.log(orderSn.value, '获取订单号'); console.log(orderSn.value, '获取订单号');
uni.showLoading({ title: t('order.orderCreating') }) uni.showLoading({ title: t('order.orderCreating') })
getGooglePay() isAndroid.value ? getGooglePay() : checkProvider()
// getGooglePay()
} catch (error) { } catch (error) {
console.error('获取订单号失败', 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 = () => { const getGooglePay = () => {
googlePay.init({ googlePay.init({
@@ -375,13 +483,6 @@
} }
} }
/**
* 切换支付方式
*/
const choseType = () => {
// payType.value = val;
}
onMounted(() => { onMounted(() => {
getDevName(); getDevName();
getActivityDescriptionData() getActivityDescriptionData()
@@ -403,7 +504,7 @@
color: #007bff; color: #007bff;
padding: 30rpx 0 20rpx 30rpx; padding: 30rpx 0 20rpx 30rpx;
} }
.recharge { .recharge {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

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

View File

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

View File

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

View File

@@ -19,9 +19,9 @@
import { ref } from 'vue' import { ref } from 'vue'
const menuItems = ref([ const menuItems = ref([
{ // {
name: '分享APP' // name: '分享APP'
}, // },
{ {
name: '关于我们', name: '关于我们',
} }
@@ -55,10 +55,10 @@
provider: "weixin", provider: "weixin",
scene: "WXSceneSession", scene: "WXSceneSession",
type: 0, type: 0,
href: '', href: 'https://a.app.qq.com/o/simple.jsp?pkgname=com.cn.medicine',
title: "吴门医述", title: "吴门医述",
summary: "我正在使用吴门医述提升自己,赶紧跟我一起来体验吧!", summary: "我正在使用吴门医述提升自己,赶紧跟我一起来体验吧!",
imageUrl: "static/icon/home_icon_logo.png", imageUrl: "static/logo.png",
success: function (res) { success: function (res) {
console.log("success:" + JSON.stringify(res)); console.log("success:" + JSON.stringify(res));
}, },
@@ -72,10 +72,10 @@
provider: "weixin", provider: "weixin",
scene: "WXSceneTimeline", scene: "WXSceneTimeline",
type: 0, type: 0,
href: '', href: 'https://a.app.qq.com/o/simple.jsp?pkgname=com.cn.medicine',
title: "吴门医述", title: "吴门医述",
summary: "我正在使用吴门医述提升自己,赶紧跟我一起来体验吧!", summary: "我正在使用吴门医述提升自己,赶紧跟我一起来体验吧!",
imageUrl: "static/icon/home_icon_logo.png", imageUrl: "static/logo.png",
success: function (res) { success: function (res) {
console.log("success:" + JSON.stringify(res)); console.log("success:" + JSON.stringify(res));
}, },
@@ -110,7 +110,7 @@
} }
.visitor-block { .visitor-block {
padding: 40rpx 20rpx; padding: 100rpx 20rpx 40rpx 20rpx;
.visitor_img { .visitor_img {
width: 150rpx; 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; max-width: 96rem;
} }
} }
.mt-2\! {
margin-top: calc(var(--spacing) * 2) !important;
}
.mt-\[20rpx\]\! {
margin-top: 20rpx !important;
}
.mb-2\! { .mb-2\! {
margin-bottom: calc(var(--spacing) * 2) !important; margin-bottom: calc(var(--spacing) * 2) !important;
} }
.mb-2\.5\! {
margin-bottom: calc(var(--spacing) * 2.5) !important;
}
.mb-\[20rpx\]\! { .mb-\[20rpx\]\! {
margin-bottom: 20rpx !important; margin-bottom: 20rpx !important;
} }
@@ -248,6 +257,9 @@
.table { .table {
display: table; display: table;
} }
.w-\[50\%\] {
width: 50%;
}
.w-\[100px\] { .w-\[100px\] {
width: 100px; width: 100px;
} }

1
types/book.d.ts vendored
View File

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

58
types/order.d.ts vendored
View File

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

11
types/video.d.ts vendored
View File

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

12
types/vip.d.ts vendored
View File

@@ -24,4 +24,16 @@ export interface IVipItemProduct {
fee: number | null, // 课程价格 fee: number | null, // 课程价格
lastFee?: number | null, // 未使用字段 lastFee?: number | null, // 未使用字段
[key: string]: any [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() { import { requestUpdatePackage } from '@/api/modules/sys'
// #ifdef APP-PLUS import { getMaxVersion } from './tools'
return new Promise((resolve, reject) => { import { getFileExtension } from '@/utils'
plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) {
let data = { /**
action: 'checkVersion', * 统一检查更新入口
appid: plus.runtime.appid, */
appVersion: plus.runtime.version, export default async function checkUpdate() {
wgtVersion: widgetInfo.version // #ifdef APP-PLUS
} try {
uniCloud.callFunction({ const upgradeData = await getUpgradeCheckData()
name: 'uni-upgrade-center',
data, // 优先调用 uni-upgrade-center3 秒超时)
success: (e) => { const result = await withTimeout(
resolve(e) callUpgradeCenter(upgradeData),
}, 3000
fail: (error) => { )
reject(error)
} console.log('检查版本更新成功:', result)
}) return result
}) } catch (err) {
}) console.warn('uniCloud更新方案失败启用备用方案:', err?.message)
// #endif return await useBackupUpdate()
// #ifndef APP-PLUS }
return new Promise((resolve, reject) => { // #endif
reject({
message: '请在App中使用' // #ifndef APP-PLUS
}) throw { message: '请在 App 中使用' }
}) // #endif
// #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; if (!e.result) return;
const { const {
code, code,
is_backup_update, // 是否备用更新
message, message,
is_silently, // 是否静默更新 is_silently, // 是否静默更新
url, // 安装包下载地址 url, // 安装包下载地址
@@ -17,17 +18,20 @@ export default function() {
type // 安装包类型 type // 安装包类型
} = e.result; } = e.result;
// 此处逻辑仅为实例,可自行编写 const hasUpdate = code > 0 || is_backup_update
if (code > 0) {
// 腾讯云和阿里云下载链接不同,需要处理一下,阿里云会原样返回 // 如果不是备用更新,需要处理下载链接
if (!is_backup_update) {
const { const {
fileList fileList
} = await uniCloud.getTempFileURL({ } = await uniCloud.getTempFileURL({
fileList: [url] fileList: [url]
}); });
if (fileList[0].tempFileURL) e.result.url = fileList[0].tempFileURL;
e.result.url = fileList[0].tempFileURL; }
// 此处逻辑仅为实例,可自行编写
if (hasUpdate) {
resolve(e) resolve(e)
// 静默更新只有wgt有 // 静默更新只有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 extension
}: ChooseFileOption): Promise<ChooseFile[]> { }: ChooseFileOption): Promise<ChooseFile[]> {
return new Promise((resolve, reject) => { 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) { switch (accept) {
case 'image': case 'image':
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN

View File

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

View File

@@ -129,4 +129,27 @@ export function parseTime(time: any, cFormat: string) {
return value.toString().padStart(2, '0') return value.toString().padStart(2, '0')
}) })
return time_str 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() || '';
} }