From 7062e675f6eec7eacc4558edb65b482ec0085466 Mon Sep 17 00:00:00 2001 From: chenghuan Date: Thu, 27 Nov 2025 14:19:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=EF=BC=9A=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E8=AF=BE=E7=A8=8B=E5=92=8C=E5=9B=BE=E4=B9=A6VIP=E8=B4=AD?= =?UTF-8?q?=E4=B9=B0=E5=8F=8A=E2=80=9C=E6=88=91=E7=9A=84=E2=80=9D=E4=B8=BB?= =?UTF-8?q?=E9=A1=B5vip=E8=BA=AB=E4=BB=BD=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/modules/order.ts | 4 +- api/modules/vip.ts | 27 ++ components/course/ChapterList.vue | 4 +- components/order/Confirm.vue | 33 ++- components/order/GoodsSelector.vue | 12 + components/order/ProductInfo.vue | 1 + components/video-player/index.vue | 6 +- locale/en.json | 10 +- locale/zh-Hans.json | 10 +- pages.json | 18 ++ pages/course/details/course.vue | 6 +- pages/order/vipConfirm.vue | 152 +++++++++++ pages/user/index.vue | 103 +++++--- pages/vip/book.vue | 400 +++++++++++++++++++++++++++++ pages/vip/course.vue | 251 ++++++++++++++++++ stores/sys.ts | 10 + style/tailwind.css | 34 +++ types/order.d.ts | 20 +- types/vip.d.ts | 27 ++ utils/index.ts | 55 ++++ 20 files changed, 1117 insertions(+), 66 deletions(-) create mode 100644 api/modules/vip.ts create mode 100644 pages/order/vipConfirm.vue create mode 100644 pages/vip/book.vue create mode 100644 types/vip.d.ts diff --git a/api/modules/order.ts b/api/modules/order.ts index b933e21..e8f0bad 100644 --- a/api/modules/order.ts +++ b/api/modules/order.ts @@ -145,10 +145,10 @@ export const orderApi = { }, /** - * 创建课程订单 + * 创建订单 * @param data 订单数据 */ - async placeCourseOrder(data: ICourseOrderCreateParams) { + async placeOrder(data: ICourseOrderCreateParams) { const res = await mainClient.request>({ url: 'book/buyOrder/placeOrder', method: 'POST', diff --git a/api/modules/vip.ts b/api/modules/vip.ts new file mode 100644 index 0000000..9bae87a --- /dev/null +++ b/api/modules/vip.ts @@ -0,0 +1,27 @@ +import { mainClient } from '@/api/clients/main' +import type { IApiResponse } from '@/api/types' +import type { IVipItem } from '@/types/vip' + +export const vipApi = { + /** + * 获取VIP购买配置列表 + */ + getVipBuyConfigList: async () => { + const res = await mainClient.request>({ + url: 'common/userVip/getVipBuyConfigList', + method: 'POST' + }) + return res + }, + + /** + * 获取图书VIP列表 + */ + getBookVipList: async () => { + const res = await mainClient.request>({ + url: 'bookAbroad/ebookvip/getEbookvipBuyConfigList', + method: 'POST' + }) + return res + } +} \ No newline at end of file diff --git a/components/course/ChapterList.vue b/components/course/ChapterList.vue index 8a3eaf0..e13d742 100644 --- a/components/course/ChapterList.vue +++ b/components/course/ChapterList.vue @@ -259,9 +259,7 @@ const handleChapterClick = (chapter: IChapter) => { border-bottom-left-radius: 40rpx; .vip-badge { - position: absolute; - left: 0; - top: 0; + display: inline-block; font-size: 24rpx; background: linear-gradient(90deg, #6429db 0%, #0075ed 100%); color: #fff; diff --git a/components/order/Confirm.vue b/components/order/Confirm.vue index 46af04e..498a0ce 100644 --- a/components/order/Confirm.vue +++ b/components/order/Confirm.vue @@ -124,22 +124,28 @@ + + \ No newline at end of file diff --git a/pages/user/index.vue b/pages/user/index.vue index 17ed04a..a2a5487 100644 --- a/pages/user/index.vue +++ b/pages/user/index.vue @@ -23,25 +23,28 @@ - 用户VIP功能重写中。。。 - + {{ $t('user.vip') }} + + + {{ vipTypeDict[vip.type] }}(有效期到 {{ parseTime(vip.endTime, '{y}-{m}-{d}') }}) + 办理课程VIP,畅享更多权益 + + {{ $t('vip.renewal') }} + {{ $t('vip.openVip') }} + + + + 电子书VIP{{ vipTypeDict[vip.type] }}(有效期到 {{ parseTime(vip.endTime, '{y}-{m}-{d}') }}) + 办理电子书VIP,畅享更多权益 + + {{ $t('vip.openVip') }} + - - + + {{ t('global.coin') }} @@ -77,13 +80,15 @@ + + \ No newline at end of file diff --git a/pages/vip/course.vue b/pages/vip/course.vue index e69de29..ca81208 100644 --- a/pages/vip/course.vue +++ b/pages/vip/course.vue @@ -0,0 +1,251 @@ + + + + + \ No newline at end of file diff --git a/stores/sys.ts b/stores/sys.ts index 711269c..b935162 100644 --- a/stores/sys.ts +++ b/stores/sys.ts @@ -6,6 +6,16 @@ import type { IUserInfo } from '@/types/user' export const useSysStore = defineStore('sys', { state: (): IUserInfo => ({ language: uni.getStorageSync('appLanguage') || 'zh-CN', + vipTypeDict: { + 1: '医学超V', + 2: '国学与心理学超V', + 4: '中医学VIP', + 5: '针灸学VIP', + 6: '肿瘤学VIP', + 7: '国学VIP', + 8: '心理学VIP', + 9: '中西汇通学VIP', + } }), getters: { diff --git a/style/tailwind.css b/style/tailwind.css index 7e51e61..5d949ea 100644 --- a/style/tailwind.css +++ b/style/tailwind.css @@ -9,6 +9,7 @@ "Courier New", monospace; --color-red-500: oklch(63.7% 0.237 25.331); --spacing: 0.25rem; + --font-weight-bold: 700; --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); --default-transition-duration: 150ms; --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); @@ -204,9 +205,18 @@ max-width: 96rem; } } + .mr-1 { + margin-right: calc(var(--spacing) * 1); + } + .ml-1 { + margin-left: calc(var(--spacing) * 1); + } .ml-1\! { margin-left: calc(var(--spacing) * 1) !important; } + .ml-2 { + margin-left: calc(var(--spacing) * 2); + } .ml-2\.5\! { margin-left: calc(var(--spacing) * 2.5) !important; } @@ -243,6 +253,9 @@ .flex-shrink { flex-shrink: 1; } + .border-collapse { + border-collapse: collapse; + } .transform { transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); } @@ -265,9 +278,18 @@ .bg-\[transparent\] { background-color: transparent; } + .pt-1 { + padding-top: calc(var(--spacing) * 1); + } + .pt-10 { + padding-top: calc(var(--spacing) * 10); + } .pt-\[40px\] { padding-top: 40px; } + .pb-0 { + padding-bottom: calc(var(--spacing) * 0); + } .pb-0\! { padding-bottom: calc(var(--spacing) * 0) !important; } @@ -277,6 +299,10 @@ .text-right { text-align: right; } + .font-bold { + --tw-font-weight: var(--font-weight-bold); + font-weight: var(--font-weight-bold); + } .text-\[\#000\] { color: #000; } @@ -299,6 +325,9 @@ --tw-ordinal: ordinal; font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,); } + .underline { + text-decoration-line: underline; + } .ring { --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); @@ -368,6 +397,10 @@ inherits: false; initial-value: solid; } +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} @property --tw-ordinal { syntax: "*"; inherits: false; @@ -560,6 +593,7 @@ --tw-skew-x: initial; --tw-skew-y: initial; --tw-border-style: solid; + --tw-font-weight: initial; --tw-ordinal: initial; --tw-slashed-zero: initial; --tw-numeric-figure: initial; diff --git a/types/order.d.ts b/types/order.d.ts index c9ca508..51866ef 100644 --- a/types/order.d.ts +++ b/types/order.d.ts @@ -4,12 +4,12 @@ export interface IGoods { productId: number productName: string - productImages: string + productImages?: string price: number - vipPrice: number | null - activityPrice: number | null - isVipPrice: number // 是否有VIP优惠 0-否 1-是 - productAmount: number // 购买数量 + vipPrice?: number | null + activityPrice?: number | null + isVipPrice?: number // 是否有VIP优惠 0-否 1-是 + productAmount?: number // 购买数量 delFlag?: number // 删除标记 -1-已下架 } @@ -39,12 +39,12 @@ export interface IOrder { export interface IOrderGoods { productId: number productName: string - productImages: string + productImages?: string price: number - vipPrice: number | null - activityPrice: number | null - isVipPrice: number // 是否有VIP优惠 0-否 1-是 - productAmount: number // 购买数量 + vipPrice?: number | null + activityPrice?: number | null + isVipPrice?: number // 是否有VIP优惠 0-否 1-是 + productAmount?: number // 购买数量 goodsType: string // 商品类型 "05" for course } diff --git a/types/vip.d.ts b/types/vip.d.ts new file mode 100644 index 0000000..b1153ae --- /dev/null +++ b/types/vip.d.ts @@ -0,0 +1,27 @@ +/** + * vip套餐项 + */ +export interface IVipItem { + courseCount: number | null // 课程数量 + state: number | null // 状态 0:在有效期, 1:已过期, null: 未购买 + originalPrice: number | null // 原价 + title: string // 套餐名称 + type: number + vcbList?: IVipItemProduct[] // 未办理过使用的商品列表 + yanqiList?: IVipItemProduct[] // 延期使用商品列表 + [key: string]: any +} + +/** + * vip套餐项商品 + */ +export interface IVipItemProduct { + id: number + type: number + title: string + year: number // vip年数 + rebateFee: number | null, // 折扣后价格 + fee: number | null, // 课程价格 + lastFee?: number | null, // 未使用字段 + [key: string]: any +} \ No newline at end of file diff --git a/utils/index.ts b/utils/index.ts index 8d31e23..5c30fb8 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -1,6 +1,7 @@ import { t } from '@/utils/i18n' /** * 页面跳转 + * @param {string} path - 要跳转的页面路径 */ export const onPageJump = (path: string) => { uni.navigateTo({ @@ -17,6 +18,8 @@ export const onPageBack = () => { /** * 拨打电话 + * @param {string} phoneNumber - 要拨打的电话号码 + * @param {string} title - 拨打电话提示的标题,默认值为空字符串 */ export const makePhoneCall = (phoneNumber: string, title: string = '') => { uni.showModal({ @@ -42,6 +45,8 @@ export const makePhoneCall = (phoneNumber: string, title: string = '') => { /** * 复制到剪贴板 + * @param {string} content - 要复制的内容 + * @param {string} title - 复制成功提示的标题,默认值为空字符串 */ export const copyToClipboard = (content: string, title: string = '') => { uni.setClipboardData({ @@ -60,6 +65,8 @@ export const copyToClipboard = (content: string, title: string = '') => { /** * 计算最低价格 + * @param {object} priceData - 价格数据对象,键为价格类型,值为价格 + * @returns {object} - 包含最低价格的键值对,{ key: 价格类型, value: 最低价格 } */ export const calculateLowestPrice = (priceData: object): { key: string, value: number } => { const validEntries = Object.entries(priceData) @@ -74,4 +81,52 @@ export const calculateLowestPrice = (priceData: object): { key: string, value: n ); return { key: minKey, value: minValue }; +} + +/** + * Parse the time to string + * @param {(Object|string|number)} time + * @param {string} cFormat + * @returns {string | null} + */ +export function parseTime(time: any, cFormat: string) { + if (arguments.length === 0 || !time) { + return null + } + const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' + let date + if (typeof time === 'object') { + date = time + } else { + if ((typeof time === 'string')) { + if ((/^[0-9]+$/.test(time))) { + // support "1548221490638" + time = parseInt(time) + } else { + // support safari + time = time.replace(new RegExp(/-/gm), '/') + } + } + + if ((typeof time === 'number') && (time.toString().length === 10)) { + time = time * 1000 + } + date = new Date(time) + } + const formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay() + } + const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { + const value = formatObj[key as keyof typeof formatObj] + // Note: getDay() returns 0 on Sunday + if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value as number] } + return value.toString().padStart(2, '0') + }) + return time_str } \ No newline at end of file