| New file |
| | |
| | | { |
| | | "version" : "1", |
| | | "prompt" : "template", |
| | | "title" : "服务协议和隐私政策", |
| | | "message" : "欢迎使用海诊康道!海诊康道非常重视你的隐私和个人信息保护,为保障你的权益,请在使用前认真阅读<a href=\"https://haizhentong.com/agreement/?type=user\">《用户服务协议》</a>和<a href=\"https://haizhentong.com/agreement/?type=privacy\">《隐私政策》</a>,点击【同意并继续】表示已充分阅读并接受已上协议,政策的内容", |
| | | "buttonAccept" : "同意并接受", |
| | | "buttonRefuse" : "暂不同意", |
| | | "hrefLoader" : "system", |
| | | "backToExit" : "false", |
| | | "second" : { |
| | | "title" : "确认提示", |
| | | "message" : "进入应用前,你需先同意<a href=\"https://haizhentong.com/agreement/?type=user\">《用户服务协议》</a>和<a href=\"https://haizhentong.com/agreement/?type=privacy\">《隐私政策》</a>,否则将退出应用。", |
| | | "buttonAccept" : "同意并继续", |
| | | "buttonRefuse" : "退出应用" |
| | | }, |
| | | "disagreeMode" : { |
| | | "support" : false, |
| | | "loadNativePlugins" : false, |
| | | "visitorEntry" : false, |
| | | "showAlways" : false |
| | | }, |
| | | "styles" : { |
| | | "backgroundColor" : "#ffffff", |
| | | "borderRadius" : "5px", |
| | | "title" : { |
| | | "color" : "#000000" |
| | | }, |
| | | "buttonAccept" : { |
| | | "color" : "#41A2EB" |
| | | }, |
| | | "buttonRefuse" : { |
| | | "color" : "#000000" |
| | | }, |
| | | "buttonVisitor" : { |
| | | "color" : "#000000" |
| | | } |
| | | } |
| | | } |
| | |
| | | { |
| | | "sassImplementationName" : "node-sass", |
| | | "name" : "海诊康道", |
| | | "appid" : "__UNI__1A90541", |
| | | "description" : "", |
| | | "versionName" : "1.0.1", |
| | | "versionCode" : 101, |
| | | "transformPx" : false, |
| | | /* 5+App特有相关 */ |
| | | "app-plus" : { |
| | | "usingComponents" : true, |
| | | "nvueStyleCompiler" : "uni-app", |
| | | "compilerVersion" : 3, |
| | | "splashscreen" : { |
| | | "alwaysShowBeforeRender" : true, |
| | | "waiting" : true, |
| | | "autoclose" : true, |
| | | "delay" : 0 |
| | | }, |
| | | /* 模块配置 */ |
| | | "modules" : { |
| | | "Barcode" : {}, |
| | | "VideoPlayer" : {}, |
| | | "Share" : {}, |
| | | "OAuth" : {} |
| | | }, |
| | | /* 应用发布信息 */ |
| | | "distribute" : { |
| | | /* android打包配置 */ |
| | | "android" : { |
| | | "permissions" : [ |
| | | "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>", |
| | | "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>", |
| | | "<uses-permission android:name=\"android.permission.VIBRATE\"/>", |
| | | "<uses-permission android:name=\"android.permission.READ_LOGS\"/>", |
| | | "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>", |
| | | "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>", |
| | | "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>", |
| | | "<uses-permission android:name=\"android.permission.CAMERA\"/>", |
| | | "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>", |
| | | "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>", |
| | | "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>", |
| | | "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>", |
| | | "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>", |
| | | "<uses-feature android:name=\"android.hardware.camera\"/>", |
| | | "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>" |
| | | ], |
| | | "minSdkVersion" : 30, |
| | | "targetSdkVersion" : 29 |
| | | }, |
| | | /* ios打包配置 */ |
| | | "ios" : { |
| | | "dSYMs" : false, |
| | | "capabilities" : { |
| | | "entitlements" : { |
| | | "com.apple.developer.associated-domains" : [ |
| | | "applinks:static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com" |
| | | ] |
| | | } |
| | | } |
| | | }, |
| | | /* SDK配置 */ |
| | | "sdkConfigs" : { |
| | | "push" : {}, |
| | | "share" : { |
| | | "weixin" : { |
| | | "appid" : "wx074c1f0cf5a0ecf4", |
| | | "UniversalLinks" : "https://static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com/uni-universallinks/__UNI__1A90541/" |
| | | } |
| | | }, |
| | | "oauth" : { |
| | | "weixin" : { |
| | | "appid" : "wx074c1f0cf5a0ecf4", |
| | | "UniversalLinks" : "https://static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com/uni-universallinks/__UNI__1A90541/" |
| | | } |
| | | }, |
| | | "payment" : {} |
| | | }, |
| | | "icons" : { |
| | | "android" : { |
| | | "hdpi" : "unpackage/res/icons/72x72.png", |
| | | "xhdpi" : "unpackage/res/icons/96x96.png", |
| | | "xxhdpi" : "unpackage/res/icons/144x144.png", |
| | | "xxxhdpi" : "unpackage/res/icons/192x192.png" |
| | | }, |
| | | "ios" : { |
| | | "appstore" : "unpackage/res/icons/1024x1024.png", |
| | | "ipad" : { |
| | | "app" : "unpackage/res/icons/76x76.png", |
| | | "app@2x" : "unpackage/res/icons/152x152.png", |
| | | "notification" : "unpackage/res/icons/20x20.png", |
| | | "notification@2x" : "unpackage/res/icons/40x40.png", |
| | | "proapp@2x" : "unpackage/res/icons/167x167.png", |
| | | "settings" : "unpackage/res/icons/29x29.png", |
| | | "settings@2x" : "unpackage/res/icons/58x58.png", |
| | | "spotlight" : "unpackage/res/icons/40x40.png", |
| | | "spotlight@2x" : "unpackage/res/icons/80x80.png" |
| | | }, |
| | | "iphone" : { |
| | | "app@2x" : "unpackage/res/icons/120x120.png", |
| | | "app@3x" : "unpackage/res/icons/180x180.png", |
| | | "notification@2x" : "unpackage/res/icons/40x40.png", |
| | | "notification@3x" : "unpackage/res/icons/60x60.png", |
| | | "settings@2x" : "unpackage/res/icons/58x58.png", |
| | | "settings@3x" : "unpackage/res/icons/87x87.png", |
| | | "spotlight@2x" : "unpackage/res/icons/80x80.png", |
| | | "spotlight@3x" : "unpackage/res/icons/120x120.png" |
| | | } |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | /* 快应用特有相关 */ |
| | | "quickapp" : {}, |
| | | /* 小程序特有相关 */ |
| | | "mp-weixin" : { |
| | | "appid" : "wxa17e8d1331e50934", |
| | | "setting" : { |
| | | "urlCheck" : false, |
| | | "minified" : true |
| | | }, |
| | | "usingComponents" : true |
| | | }, |
| | | "mp-alipay" : { |
| | | "usingComponents" : true |
| | | }, |
| | | "mp-baidu" : { |
| | | "usingComponents" : true |
| | | }, |
| | | "mp-toutiao" : { |
| | | "usingComponents" : true |
| | | }, |
| | | "uniStatistics" : { |
| | | "enable" : false |
| | | }, |
| | | "vueVersion" : "3", |
| | | "permission" : { |
| | | "scope.userLocation" : { |
| | | "desc" : "下载文件" |
| | | } |
| | | }, |
| | | "h5" : { |
| | | "unipush" : { |
| | | "enable" : false |
| | | } |
| | | }, |
| | | "_spaceID" : "mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8" |
| | | } |
| | | "sassImplementationName": "node-sass", |
| | | "name": "海诊康道", |
| | | "appid": "__UNI__FB03FDD", |
| | | "description": "", |
| | | "versionName": "1.0.1", |
| | | "versionCode": 101, |
| | | "transformPx": false, |
| | | /* 5+App特有相关 */ |
| | | "app-plus": { |
| | | "usingComponents": true, |
| | | "nvueStyleCompiler": "uni-app", |
| | | "compilerVersion": 3, |
| | | "splashscreen": { |
| | | "alwaysShowBeforeRender": true, |
| | | "waiting": true, |
| | | "autoclose": true, |
| | | "delay": 0 |
| | | }, |
| | | /* 模块配置 */ |
| | | "modules": { |
| | | "Barcode": {}, |
| | | "VideoPlayer": {}, |
| | | "Share": {}, |
| | | "OAuth": {}, |
| | | "Payment": {} |
| | | }, |
| | | /* 应用发布信息 */ |
| | | "distribute": { |
| | | /* android打包配置 */ |
| | | "android": { |
| | | "permissions": [ |
| | | "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>", |
| | | "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>", |
| | | "<uses-permission android:name=\"android.permission.VIBRATE\"/>", |
| | | "<uses-permission android:name=\"android.permission.READ_LOGS\"/>", |
| | | "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>", |
| | | "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>", |
| | | "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>", |
| | | "<uses-permission android:name=\"android.permission.CAMERA\"/>", |
| | | "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>", |
| | | "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>", |
| | | "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>", |
| | | "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>", |
| | | "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>", |
| | | "<uses-feature android:name=\"android.hardware.camera\"/>", |
| | | "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>" |
| | | ], |
| | | "minSdkVersion": 28, |
| | | "targetSdkVersion": 30 |
| | | }, |
| | | /* ios打包配置 */ |
| | | "ios": { |
| | | "dSYMs": false, |
| | | "capabilities": { |
| | | "entitlements": { |
| | | "com.apple.developer.associated-domains": [ |
| | | "applinks:static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com" |
| | | ] |
| | | } |
| | | } |
| | | }, |
| | | /* SDK配置 */ |
| | | "sdkConfigs": { |
| | | "push": {}, |
| | | "share": { |
| | | "weixin": { |
| | | "appid": "wx074c1f0cf5a0ecf4", |
| | | "UniversalLinks": "https://static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com/uni-universallinks/__UNI__1A90541/" |
| | | } |
| | | }, |
| | | "oauth": { |
| | | "weixin": { |
| | | "appid": "wx074c1f0cf5a0ecf4", |
| | | "UniversalLinks": "https://static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com/uni-universallinks/__UNI__1A90541/" |
| | | } |
| | | }, |
| | | "payment": { |
| | | "appleiap": {}, |
| | | "weixin": { |
| | | "__platform__": ["ios", "android"], |
| | | "appid": "wx074c1f0cf5a0ecf4", |
| | | "UniversalLinks": "https://static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com/uni-universallinks/__UNI__1A90541/" |
| | | } |
| | | } |
| | | }, |
| | | "icons": { |
| | | "android": { |
| | | "hdpi": "unpackage/res/icons/72x72.png", |
| | | "xhdpi": "unpackage/res/icons/96x96.png", |
| | | "xxhdpi": "unpackage/res/icons/144x144.png", |
| | | "xxxhdpi": "unpackage/res/icons/192x192.png" |
| | | }, |
| | | "ios": { |
| | | "appstore": "unpackage/res/icons/1024x1024.png", |
| | | "ipad": { |
| | | "app": "unpackage/res/icons/76x76.png", |
| | | "app@2x": "unpackage/res/icons/152x152.png", |
| | | "notification": "unpackage/res/icons/20x20.png", |
| | | "notification@2x": "unpackage/res/icons/40x40.png", |
| | | "proapp@2x": "unpackage/res/icons/167x167.png", |
| | | "settings": "unpackage/res/icons/29x29.png", |
| | | "settings@2x": "unpackage/res/icons/58x58.png", |
| | | "spotlight": "unpackage/res/icons/40x40.png", |
| | | "spotlight@2x": "unpackage/res/icons/80x80.png" |
| | | }, |
| | | "iphone": { |
| | | "app@2x": "unpackage/res/icons/120x120.png", |
| | | "app@3x": "unpackage/res/icons/180x180.png", |
| | | "notification@2x": "unpackage/res/icons/40x40.png", |
| | | "notification@3x": "unpackage/res/icons/60x60.png", |
| | | "settings@2x": "unpackage/res/icons/58x58.png", |
| | | "settings@3x": "unpackage/res/icons/87x87.png", |
| | | "spotlight@2x": "unpackage/res/icons/80x80.png", |
| | | "spotlight@3x": "unpackage/res/icons/120x120.png" |
| | | } |
| | | } |
| | | }, |
| | | "splashscreen": { |
| | | "androidStyle": "common", |
| | | "useOriginalMsgbox": true |
| | | } |
| | | } |
| | | }, |
| | | /* 快应用特有相关 */ |
| | | "quickapp": {}, |
| | | /* 小程序特有相关 */ |
| | | "mp-weixin": { |
| | | "appid": "wxa17e8d1331e50934", |
| | | "setting": { |
| | | "urlCheck": false, |
| | | "minified": true |
| | | }, |
| | | "usingComponents": true |
| | | }, |
| | | "mp-alipay": { |
| | | "usingComponents": true |
| | | }, |
| | | "mp-baidu": { |
| | | "usingComponents": true |
| | | }, |
| | | "mp-toutiao": { |
| | | "usingComponents": true |
| | | }, |
| | | "uniStatistics": { |
| | | "enable": false |
| | | }, |
| | | "vueVersion": "3", |
| | | "permission": { |
| | | "scope.userLocation": { |
| | | "desc": "下载文件" |
| | | } |
| | | }, |
| | | "h5": { |
| | | "unipush": { |
| | | "enable": false |
| | | } |
| | | }, |
| | | "_spaceID": "mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8" |
| | | } |
| | |
| | | </view> |
| | | |
| | | <!-- 页面滚动内容区域 --> |
| | | <scroll-view scroll-y class="scroll-content"> |
| | | <scroll-view scroll-y class="scroll-content" :style="scrollContentStyle"> |
| | | <template v-if="currentTab === 'course'"> |
| | | <!-- 基本信息模块 --> |
| | | <view class="info-section"> |
| | |
| | | <view class="item-right-box"> |
| | | <text class="item-time">{{ item.durationText }}</text> |
| | | <image |
| | | v-if="shouldShowVideoDownload(item)" |
| | | v-if="!isReadonlyDisplayMode && shouldShowVideoDownload(item)" |
| | | class="download-icon" |
| | | src="/static/image/course/redDownload@2x.png" |
| | | mode="aspectFit" |
| | | @tap.stop="handleVideoDownload(item)" |
| | | ></image> |
| | | <view |
| | | v-else |
| | | v-else-if="!isReadonlyDisplayMode" |
| | | class="cost-btn" |
| | | :class="{ 'cost-btn--purchased': item.purchased || courseDetail.courseOwned }" |
| | | @tap.stop="handlePaidVideoTap(item)" |
| | |
| | | <view class="item-right-box"> |
| | | <text class="item-time">{{ item.durationText }}</text> |
| | | <image |
| | | v-if="shouldShowVideoDownload(item)" |
| | | v-if="!isReadonlyDisplayMode && shouldShowVideoDownload(item)" |
| | | class="download-icon" |
| | | src="/static/image/course/redDownload@2x.png" |
| | | mode="aspectFit" |
| | | @tap.stop="handleVideoDownload(item)" |
| | | ></image> |
| | | <view |
| | | v-else |
| | | v-else-if="!isReadonlyDisplayMode" |
| | | class="cost-btn" |
| | | :class="{ 'cost-btn--purchased': item.purchased || courseDetail.courseOwned }" |
| | | @tap.stop="handlePaidVideoTap(item)" |
| | |
| | | </template> |
| | | |
| | | <view v-if="loading" class="loading-text">加载中...</view> |
| | | <view v-if="isReadonlyDisplayMode" class="open-app-banner" @tap="openApp"> |
| | | <view class="app-logo-placeholder"></view> |
| | | <text class="banner-text">打开APP,了解更多</text> |
| | | <view class="open-btn">立即打开</view> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <!-- 底部悬浮按钮 --> |
| | | <view class="bottom-bar"> |
| | | <view v-if="!isReadonlyDisplayMode" class="bottom-bar"> |
| | | <view class="action-icons"> |
| | | <view class="action-icon-item" @tap="handleConsult"> |
| | | <image class="icon-img icon-listen" src="/static/image/healthCourses/listen.png" mode="aspectFit"></image> |
| | |
| | | <view class="payment-methods"> |
| | | <text class="method-title">选择支付方式</text> |
| | | |
| | | <view class="method-item" @tap="selectPaymentMethod('wechat')"> |
| | | <view v-if="isIosApp" class="method-item" @tap="selectPaymentMethod('appleiap')"> |
| | | <view class="method-left"> |
| | | <image class="pay-icon" src="/static/image/vip/weixinPay.png" mode="aspectFit"></image> |
| | | <text class="method-name">微信支付</text> |
| | | <view class="pay-icon pay-icon--apple">A</view> |
| | | <text class="method-name">App Store 支付</text> |
| | | </view> |
| | | <image class="radio-img" :src="selectedPaymentMethod === 'wechat' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image> |
| | | <image class="radio-img" :src="selectedPaymentMethod === 'appleiap' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image> |
| | | </view> |
| | | |
| | | <view class="method-item" @tap="selectPaymentMethod('alipay')"> |
| | | <view class="method-left"> |
| | | <image class="pay-icon" src="/static/image/vip/aliPay.png" mode="aspectFit"></image> |
| | | <text class="method-name">支付宝支付</text> |
| | | <template v-else> |
| | | <view class="method-item" @tap="selectPaymentMethod('wechat')"> |
| | | <view class="method-left"> |
| | | <image class="pay-icon" src="/static/image/vip/weixinPay.png" mode="aspectFit"></image> |
| | | <text class="method-name">微信支付</text> |
| | | </view> |
| | | <image class="radio-img" :src="selectedPaymentMethod === 'wechat' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image> |
| | | </view> |
| | | <image class="radio-img" :src="selectedPaymentMethod === 'alipay' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image> |
| | | </view> |
| | | |
| | | <view class="method-item" @tap="selectPaymentMethod('alipay')"> |
| | | <view class="method-left"> |
| | | <image class="pay-icon" src="/static/image/vip/aliPay.png" mode="aspectFit"></image> |
| | | <text class="method-name">支付宝支付</text> |
| | | </view> |
| | | <image class="radio-img" :src="selectedPaymentMethod === 'alipay' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image> |
| | | </view> |
| | | </template> |
| | | </view> |
| | | |
| | | <view class="popup-footer"> |
| | |
| | | </view> |
| | | </view> |
| | | </u-popup> |
| | | |
| | | </view> |
| | | </template> |
| | | |
| | |
| | | unfavoriteHealthCourse, |
| | | } from './service' |
| | | import { startCourseVideoDownload, hasCourseDownloadTask } from '@/utils/course-download.js' |
| | | import { requestAppleIapPayment } from '@/utils/appleIap.js' |
| | | import { resolveCurrentAppUserId } from '@/utils/app-user.js' |
| | | import { |
| | | bindVideoCaptureListener, |
| | |
| | | purchased: boolean |
| | | locked: boolean |
| | | videoUrl?: string |
| | | appleProductId?: string |
| | | productId?: string |
| | | iosProductId?: string |
| | | } |
| | | |
| | | interface CourseDetail { |
| | |
| | | memberCourseJoined: boolean |
| | | courseOwned: boolean |
| | | coursePurchased: boolean |
| | | appleProductId?: string |
| | | productId?: string |
| | | iosProductId?: string |
| | | } |
| | | |
| | | interface SwiperChangeEvent { |
| | |
| | | } |
| | | } |
| | | |
| | | type PaymentMethod = 'wechat' | 'alipay' |
| | | type PaymentMethod = 'wechat' | 'alipay' | 'appleiap' |
| | | type PurchaseTarget = 'COURSE' | 'VIDEO' |
| | | |
| | | const VIDEO_PLAYER_ID = 'course-detail-video-player' |
| | |
| | | const purchaseSubmitting = ref(false) |
| | | const memberJoinSubmitting = ref(false) |
| | | const showPaymentPopup = ref(false) |
| | | const selectedPaymentMethod = ref<PaymentMethod>('wechat') |
| | | const selectedPaymentMethod = ref<PaymentMethod>(isIosApp ? 'appleiap' : 'wechat') |
| | | const activePurchaseTarget = ref<PurchaseTarget>('COURSE') |
| | | const activePurchaseVideoId = ref<number | null>(null) |
| | | const courseId = ref('') |
| | |
| | | }) |
| | | const currentVideoUrl = computed(() => currentVideo.value?.videoUrl || '') |
| | | const currentVideoPoster = computed(() => courseDetail.value.bannerImages[0] || '/static/image/home/class.png') |
| | | const isReadonlyDisplayMode = isH5 |
| | | const scrollContentStyle = computed(() => { |
| | | if (isReadonlyDisplayMode) { |
| | | return { |
| | | paddingBottom: 'calc(140rpx + env(safe-area-inset-bottom))', |
| | | } |
| | | } |
| | | if (!isAppPlus) { |
| | | return { |
| | | paddingBottom: 'calc(260rpx + env(safe-area-inset-bottom))', |
| | | } |
| | | } |
| | | return {} |
| | | }) |
| | | const videoWatermarkText = computed(() => { |
| | | const currentUserId = resolveCurrentAppUserId() |
| | | if (!currentUserId) { |
| | |
| | | * 统一处理章节点击,可播放时切换顶部播放器,否则保留原有购买弹窗。 |
| | | */ |
| | | function handleVideoSelect(item: CourseVideo) { |
| | | if (isReadonlyDisplayMode) { |
| | | return |
| | | } |
| | | if (canPlayVideo(item)) { |
| | | playSelectedVideo(item) |
| | | return |
| | |
| | | * 对当前播放器执行播放重试,避免首次切换章节时无感失败。 |
| | | */ |
| | | function retryPlayback() { |
| | | if (isReadonlyDisplayMode) { |
| | | return |
| | | } |
| | | if (!videoContext || !currentVideoUrl.value) { |
| | | return |
| | | } |
| | |
| | | * 跳转专家介绍页,并携带专家主键加载真实内容。 |
| | | */ |
| | | function goToExpertDetail() { |
| | | if (isReadonlyDisplayMode) { |
| | | return |
| | | } |
| | | if (!courseDetail.value.expert?.id) { |
| | | return |
| | | } |
| | |
| | | * 拨打后台系统配置中的客服电话,未配置时给出兜底提示。 |
| | | */ |
| | | function handleConsult() { |
| | | if (isReadonlyDisplayMode) { |
| | | return |
| | | } |
| | | const servicePhone = courseDetail.value.servicePhone.trim() |
| | | if (!servicePhone) { |
| | | uni.showToast({ |
| | |
| | | uni.makePhoneCall({ |
| | | phoneNumber: servicePhone, |
| | | }) |
| | | } |
| | | |
| | | /** |
| | | * 拉起或跳转下载 APP |
| | | */ |
| | | function openApp() { |
| | | if (isReadonlyDisplayMode) { |
| | | return |
| | | } |
| | | uni.showToast({ |
| | | title: '即将打开APP', |
| | | icon: 'none', |
| | | }) |
| | | // 可在此处补充跳转下载引导页的具体逻辑 |
| | | } |
| | | |
| | | /** |
| | |
| | | * 通过微信好友聊天场景分享当前课程。 |
| | | */ |
| | | function shareToWechatSession() { |
| | | if (isReadonlyDisplayMode) { |
| | | return |
| | | } |
| | | // #ifdef APP-PLUS |
| | | plus.share.getServices((services) => { |
| | | const weixinService = services.find((service) => service.id === 'weixin') |
| | |
| | | * 打开整课购买支付弹窗。 |
| | | */ |
| | | function handleCoursePurchase() { |
| | | if (isReadonlyDisplayMode) { |
| | | return |
| | | } |
| | | if (!courseId.value || courseDetail.value.courseOwned || memberJoinSubmitting.value) { |
| | | return |
| | | } |
| | | if (courseDetail.value.isMember) { |
| | | submitMemberJoin() |
| | | return |
| | | } |
| | | if (isIosApp) { |
| | | selectedPaymentMethod.value = 'appleiap' |
| | | } |
| | | activePurchaseTarget.value = 'COURSE' |
| | | activePurchaseVideoId.value = null |
| | |
| | | * 处理付费小节点击,未购买时进入对应支付弹窗。 |
| | | */ |
| | | function handlePaidVideoTap(item: CourseVideo) { |
| | | if (isReadonlyDisplayMode) { |
| | | return |
| | | } |
| | | if (!courseId.value || courseDetail.value.courseOwned || item.purchased) { |
| | | return |
| | | } |
| | | if (isIosApp) { |
| | | selectedPaymentMethod.value = 'appleiap' |
| | | } |
| | | activePurchaseTarget.value = 'VIDEO' |
| | | activePurchaseVideoId.value = item.id |
| | |
| | | * 下载已购买章节视频,H5 直接打开资源地址,其它端走文件下载。 |
| | | */ |
| | | function handleVideoDownload(item: CourseVideo) { |
| | | if (isReadonlyDisplayMode) { |
| | | return |
| | | } |
| | | if (!shouldShowVideoDownload(item)) { |
| | | return |
| | | } |
| | |
| | | * 提交当前购买目标的直付订单。 |
| | | */ |
| | | async function submitPurchase() { |
| | | if (isReadonlyDisplayMode) { |
| | | return |
| | | } |
| | | if (!courseId.value || purchaseSubmitting.value) { |
| | | return |
| | | } |
| | |
| | | } |
| | | purchaseSubmitting.value = true |
| | | try { |
| | | const payload = { |
| | | paymentMethod: selectedPaymentMethod.value === 'wechat' ? 'WECHAT' : 'ALIPAY', |
| | | const payload = selectedPaymentMethod.value === 'appleiap' |
| | | ? await buildAppleIapPurchasePayload() |
| | | : { |
| | | paymentMethod: selectedPaymentMethod.value === 'wechat' ? 'WECHAT' : 'ALIPAY', |
| | | } |
| | | if (!payload) { |
| | | return |
| | | } |
| | | const response = activePurchaseTarget.value === 'VIDEO' && activePurchaseVideoId.value !== null |
| | | ? await purchaseHealthCourseVideo(courseId.value, activePurchaseVideoId.value, payload) |
| | |
| | | }) |
| | | } finally { |
| | | purchaseSubmitting.value = false |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 拉起苹果内购并组装课程/章节购买所需的验单参数。 |
| | | */ |
| | | async function buildAppleIapPurchasePayload() { |
| | | const appleProductId = resolveActiveAppleProductId() |
| | | if (!appleProductId) { |
| | | uni.showToast({ |
| | | title: '当前商品未配置苹果内购商品', |
| | | icon: 'none', |
| | | }) |
| | | return null |
| | | } |
| | | const iapResult = await requestAppleIapPayment({ |
| | | productId: appleProductId, |
| | | username: resolveAppleIapUsername(), |
| | | }) |
| | | if (!normalizeOptionalText(iapResult?.transactionReceipt)) { |
| | | uni.showToast({ |
| | | title: '苹果内购凭证缺失', |
| | | icon: 'none', |
| | | }) |
| | | return null |
| | | } |
| | | return { |
| | | paymentMethod: 'APPLE_IAP', |
| | | appleProductId, |
| | | transactionReceipt: iapResult.transactionReceipt, |
| | | transactionIdentifier: iapResult.transactionIdentifier, |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | function resolveActiveAppleProductId() { |
| | | if (activePurchaseTarget.value === 'VIDEO' && activePurchaseVideoId.value !== null) { |
| | | const currentVideo = courseDetail.value.videos.find((item) => Number(item.id) === Number(activePurchaseVideoId.value)) |
| | | return normalizeOptionalText(currentVideo?.appleProductId || currentVideo?.productId || currentVideo?.iosProductId) |
| | | } |
| | | return normalizeOptionalText(courseDetail.value.appleProductId || courseDetail.value.productId || courseDetail.value.iosProductId) |
| | | } |
| | | |
| | | function resolveAppleIapUsername() { |
| | | const userId = resolveCurrentAppUserId() |
| | | if (userId > 0) { |
| | | return String(userId) |
| | | } |
| | | return 'guest' |
| | | } |
| | | |
| | | /** |
| | | * 提交会员加课请求,并将整课直接回写为已拥有。 |
| | | */ |
| | | async function submitMemberJoin() { |
| | | if (isReadonlyDisplayMode) { |
| | | return |
| | | } |
| | | if (!courseId.value || memberJoinSubmitting.value || courseDetail.value.courseOwned || !courseDetail.value.isMember) { |
| | | return |
| | | } |
| | |
| | | * 切换课程收藏状态,并直接回写详情页当前展示数据。 |
| | | */ |
| | | async function toggleFavorite() { |
| | | if (isReadonlyDisplayMode) { |
| | | return |
| | | } |
| | | if (!courseId.value || favoriteSubmitting.value) { |
| | | return |
| | | } |
| | |
| | | color: #999; |
| | | } |
| | | |
| | | .open-app-banner { |
| | | position: fixed; |
| | | left: 32rpx; |
| | | right: 32rpx; |
| | | bottom: calc(32rpx + env(safe-area-inset-bottom)); |
| | | height: 96rpx; |
| | | background-color: #FFF1F0; |
| | | border-radius: 16rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 0 24rpx; |
| | | box-sizing: border-box; |
| | | z-index: 90; |
| | | |
| | | .app-logo-placeholder { |
| | | width: 48rpx; |
| | | height: 48rpx; |
| | | background-color: #FFFFFF; |
| | | border-radius: 8rpx; |
| | | margin-right: 16rpx; |
| | | } |
| | | |
| | | .banner-text { |
| | | flex: 1; |
| | | font-size: 26rpx; |
| | | color: #C63520; |
| | | } |
| | | |
| | | .open-btn { |
| | | width: 140rpx; |
| | | height: 56rpx; |
| | | background-color: #C63520; |
| | | border-radius: 8rpx; |
| | | color: #FFFFFF; |
| | | font-size: 24rpx; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | } |
| | | } |
| | | |
| | | .bottom-bar { |
| | | position: fixed; |
| | | bottom: 0; |
| | |
| | | margin-right: 16rpx; |
| | | } |
| | | |
| | | .pay-icon--apple { |
| | | border-radius: 12rpx; |
| | | background: #111111; |
| | | color: #ffffff; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 28rpx; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .method-name { |
| | | font-size: 28rpx; |
| | | color: #1A1A1A; |
| | |
| | | }) |
| | | return |
| | | } |
| | | weixinService.authorize((event : any) => { |
| | | const code = normalizeOptionalText(event?.code) |
| | | if (!code) { |
| | | reject({ |
| | | code: -99, |
| | | errMsg: '微信授权未返回有效code' |
| | | }) |
| | | return |
| | | } |
| | | resolve({ |
| | | code |
| | | }) |
| | | }, (error : any) => { |
| | | reject({ |
| | | code: error?.code, |
| | | errMsg: error?.message || '微信授权失败' |
| | | }) |
| | | }, { |
| | | scope: 'snsapi_userinfo', |
| | | state: 'login' |
| | | }) |
| | | requestWechatAuthorizeByService(weixinService, resolve, reject) |
| | | }, (error : any) => { |
| | | reject({ |
| | | code: error?.code, |
| | |
| | | } |
| | | |
| | | /** |
| | | * 通过微信原生服务执行授权;iOS 端先清理残留授权态,再重新拉起微信。 |
| | | */ |
| | | const requestWechatAuthorizeByService = ( |
| | | weixinService : any, |
| | | resolve : (value : WechatLoginResult) => void, |
| | | reject : (reason ?: any) => void |
| | | ) => { |
| | | const executeAuthorize = () => { |
| | | weixinService.authorize((event : any) => { |
| | | const code = normalizeOptionalText(event?.code) |
| | | const authResult = weixinService.authResult || {} |
| | | const accessToken = normalizeOptionalText(authResult.access_token || authResult.accessToken) |
| | | const openid = normalizeOptionalText(authResult.openid || authResult.openId) |
| | | if (!code && !(accessToken && openid)) { |
| | | reject({ |
| | | code: -99, |
| | | errMsg: '微信授权未返回有效凭证' |
| | | }) |
| | | return |
| | | } |
| | | resolve({ |
| | | code, |
| | | authResult: { |
| | | access_token: authResult.access_token, |
| | | accessToken: authResult.accessToken, |
| | | openid: authResult.openid, |
| | | openId: authResult.openId, |
| | | unionid: authResult.unionid, |
| | | unionId: authResult.unionId |
| | | } |
| | | }) |
| | | }, (error : any) => { |
| | | reject({ |
| | | code: error?.code, |
| | | errMsg: error?.message || '微信授权失败' |
| | | }) |
| | | }, { |
| | | scope: 'snsapi_userinfo', |
| | | state: 'login' |
| | | }) |
| | | } |
| | | |
| | | const authResult = weixinService?.authResult || {} |
| | | const hasAuthorized = !!normalizeOptionalText(authResult.access_token || authResult.accessToken || authResult.openid || authResult.openId) |
| | | if (!hasAuthorized || typeof weixinService.logout !== 'function') { |
| | | executeAuthorize() |
| | | return |
| | | } |
| | | weixinService.logout(() => { |
| | | executeAuthorize() |
| | | }, () => { |
| | | executeAuthorize() |
| | | }) |
| | | } |
| | | |
| | | /** |
| | | * 从 uni.login 返回结果中提取微信登录接口需要的参数。 |
| | | */ |
| | | const buildWechatLoginPayload = (loginRes : WechatLoginResult) => { |
| | |
| | | <view class="payment-methods"> |
| | | <text class="method-title">选择支付方式</text> |
| | | |
| | | <view class="method-item" @tap="selectPayment('wechat')"> |
| | | <view v-if="isIosApp" class="method-item" @tap="selectPayment('appleiap')"> |
| | | <view class="method-left"> |
| | | <image class="pay-icon" src="/static/image/vip/weixinPay.png" mode="aspectFit"></image> |
| | | <text class="method-name">微信支付</text> |
| | | <view class="pay-icon pay-icon--apple">A</view> |
| | | <text class="method-name">App Store 支付</text> |
| | | </view> |
| | | <view class="method-radio"> |
| | | <image class="radio-img" :src="selectedPaymentMethod === 'wechat' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image> |
| | | <image class="radio-img" :src="selectedPaymentMethod === 'appleiap' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="method-item" @tap="selectPayment('alipay')"> |
| | | <view class="method-left"> |
| | | <image class="pay-icon" src="/static/image/vip/aliPay.png" mode="aspectFit"></image> |
| | | <text class="method-name">支付宝支付</text> |
| | | <template v-else> |
| | | <view class="method-item" @tap="selectPayment('wechat')"> |
| | | <view class="method-left"> |
| | | <image class="pay-icon" src="/static/image/vip/weixinPay.png" mode="aspectFit"></image> |
| | | <text class="method-name">微信支付</text> |
| | | </view> |
| | | <view class="method-radio"> |
| | | <image class="radio-img" :src="selectedPaymentMethod === 'wechat' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image> |
| | | </view> |
| | | </view> |
| | | <view class="method-radio"> |
| | | <image class="radio-img" :src="selectedPaymentMethod === 'alipay' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image> |
| | | |
| | | <view class="method-item" @tap="selectPayment('alipay')"> |
| | | <view class="method-left"> |
| | | <image class="pay-icon" src="/static/image/vip/aliPay.png" mode="aspectFit"></image> |
| | | <text class="method-name">支付宝支付</text> |
| | | </view> |
| | | <view class="method-radio"> |
| | | <image class="radio-img" :src="selectedPaymentMethod === 'alipay' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | </view> |
| | | |
| | | <view class="popup-footer"> |
| | |
| | | import { computed, ref } from 'vue' |
| | | import { onLoad, onShow } from '@dcloudio/uni-app' |
| | | import { getMemberOpenPage, purchaseMember } from './service' |
| | | import { requestAppleIapPayment, isIosAppRuntime } from '@/utils/appleIap.js' |
| | | import { resolveCurrentAppUserId } from '@/utils/app-user.js' |
| | | |
| | | type PaymentMethod = 'wechat' | 'alipay' |
| | | type PaymentMethod = 'wechat' | 'alipay' | 'appleiap' |
| | | |
| | | type PurchaseMemberType = 'MONTH_CARD' | 'QUARTER_CARD' |
| | | |
| | |
| | | title: string |
| | | price: number | string | null |
| | | days: number | null |
| | | appleProductId?: string |
| | | productId?: string |
| | | iosProductId?: string |
| | | } |
| | | |
| | | interface MemberBenefitItem { |
| | |
| | | } |
| | | |
| | | interface AppUserInfo { |
| | | id?: number | null |
| | | nickname?: string |
| | | phone?: string |
| | | avatar?: string |
| | |
| | | const statusBarHeight = ref(20) |
| | | const currentPlan = ref('') |
| | | const showPaymentPopup = ref(false) |
| | | const selectedPaymentMethod = ref<PaymentMethod>('alipay') |
| | | const isIosApp = isIosAppRuntime() |
| | | const selectedPaymentMethod = ref<PaymentMethod>(isIosApp ? 'appleiap' : 'alipay') |
| | | const purchaseSubmitting = ref(false) |
| | | const memberOpenPage = ref<MemberOpenPageData>(createDefaultMemberOpenPage()) |
| | | const appUserInfo = ref<AppUserInfo>(createDefaultAppUserInfo()) |
| | |
| | | }) |
| | | return |
| | | } |
| | | if (isIosApp) { |
| | | selectedPaymentMethod.value = 'appleiap' |
| | | } |
| | | showPaymentPopup.value = true |
| | | } |
| | | |
| | |
| | | } |
| | | purchaseSubmitting.value = true |
| | | try { |
| | | if (selectedPaymentMethod.value === 'appleiap') { |
| | | const response = await submitAppleIapMemberPurchase(memberType) |
| | | if (response?.code !== 0 || !response.data) { |
| | | return |
| | | } |
| | | applyPurchaseResult(response.data) |
| | | showPaymentPopup.value = false |
| | | uni.showToast({ |
| | | title: '开通成功', |
| | | icon: 'none', |
| | | }) |
| | | return |
| | | } |
| | | const response = await purchaseMember({ |
| | | memberType, |
| | | paymentMethod: selectedPaymentMethod.value === 'wechat' ? 'WECHAT' : 'ALIPAY', |
| | |
| | | } finally { |
| | | purchaseSubmitting.value = false |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 拉起苹果内购后,将交易凭证回传会员购买接口完成验单。 |
| | | */ |
| | | async function submitAppleIapMemberPurchase(memberType: PurchaseMemberType) { |
| | | const appleProductId = resolveMemberAppleProductId(selectedPlan.value) |
| | | if (!appleProductId) { |
| | | uni.showToast({ |
| | | title: '当前套餐未配置苹果内购商品', |
| | | icon: 'none', |
| | | }) |
| | | return null |
| | | } |
| | | const iapResult = await requestAppleIapPayment({ |
| | | productId: appleProductId, |
| | | username: resolveAppleIapUsername(), |
| | | }) |
| | | if (!normalizeOptionalText(iapResult?.transactionReceipt)) { |
| | | uni.showToast({ |
| | | title: '苹果内购凭证缺失', |
| | | icon: 'none', |
| | | }) |
| | | return null |
| | | } |
| | | return purchaseMember({ |
| | | memberType, |
| | | paymentMethod: 'APPLE_IAP', |
| | | appleProductId, |
| | | transactionReceipt: iapResult.transactionReceipt, |
| | | transactionIdentifier: iapResult.transactionIdentifier, |
| | | }) |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | uni.setStorageSync('appUserInfo', nextUserInfo) |
| | | syncCachedUserInfo() |
| | | } |
| | | |
| | | function resolveMemberAppleProductId(plan?: MemberOpenPackage | null) { |
| | | if (!plan) { |
| | | return '' |
| | | } |
| | | return normalizeOptionalText(plan.appleProductId || plan.productId || plan.iosProductId) |
| | | } |
| | | |
| | | function resolveAppleIapUsername() { |
| | | const userId = resolveCurrentAppUserId() |
| | | if (userId > 0) { |
| | | return String(userId) |
| | | } |
| | | return normalizeOptionalText(appUserInfo.value.phone) || 'guest' |
| | | } |
| | | |
| | | /** |
| | |
| | | width: 100rpx; |
| | | height: 68.57rpx; |
| | | } |
| | | |
| | | .pay-icon--apple { |
| | | width: 68rpx; |
| | | height: 68rpx; |
| | | border-radius: 16rpx; |
| | | background: #111111; |
| | | color: #ffffff; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 34rpx; |
| | | font-weight: 600; |
| | | } |
| | | } |
| | | |
| | | .method-name { |
| New file |
| | |
| | | /** |
| | | * 判断当前是否为 iOS App 运行环境。 |
| | | */ |
| | | export function isIosAppRuntime() { |
| | | // #ifndef APP-PLUS |
| | | return false |
| | | // #endif |
| | | // #ifdef APP-PLUS |
| | | try { |
| | | return plus.os?.name === 'iOS' |
| | | } catch { |
| | | return false |
| | | } |
| | | // #endif |
| | | } |
| | | |
| | | /** |
| | | * 读取苹果内购支付通道。 |
| | | */ |
| | | export function getAppleIapChannel() { |
| | | return new Promise((resolve, reject) => { |
| | | // #ifndef APP-PLUS |
| | | reject({ |
| | | code: -3, |
| | | message: '当前环境不支持苹果内购', |
| | | }) |
| | | return |
| | | // #endif |
| | | // #ifdef APP-PLUS |
| | | plus.payment.getChannels((channels) => { |
| | | const appleChannel = Array.isArray(channels) ? channels.find((item) => item.id === 'appleiap') : null |
| | | if (!appleChannel) { |
| | | reject({ |
| | | code: -3, |
| | | message: '当前应用未启用苹果内购', |
| | | }) |
| | | return |
| | | } |
| | | resolve(appleChannel) |
| | | }, (error) => { |
| | | reject(error || { |
| | | code: -99, |
| | | message: '获取苹果内购通道失败', |
| | | }) |
| | | }) |
| | | // #endif |
| | | }) |
| | | } |
| | | |
| | | /** |
| | | * 拉起苹果内购支付并返回原始交易结果。 |
| | | */ |
| | | export async function requestAppleIapPayment({ productId, username } = {}) { |
| | | const normalizedProductId = typeof productId === 'string' ? productId.trim() : '' |
| | | if (!normalizedProductId) { |
| | | throw { |
| | | code: -1, |
| | | message: '缺少苹果内购商品ID', |
| | | } |
| | | } |
| | | const channel = await getAppleIapChannel() |
| | | return new Promise((resolve, reject) => { |
| | | const orderInfo = { |
| | | productid: normalizedProductId, |
| | | } |
| | | const normalizedUsername = typeof username === 'string' ? username.trim() : '' |
| | | if (normalizedUsername) { |
| | | orderInfo.username = normalizedUsername |
| | | } |
| | | plus.payment.request(channel, orderInfo, (result) => { |
| | | resolve(normalizeAppleIapResult(result, normalizedProductId)) |
| | | }, (error) => { |
| | | reject(error || { |
| | | code: -99, |
| | | message: '苹果内购支付失败', |
| | | }) |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | /** |
| | | * 归一化苹果内购结果,方便业务页直接拿 receipt 回传后端。 |
| | | */ |
| | | function normalizeAppleIapResult(result, productId) { |
| | | const receipt = normalizeOptionalText(result?.transactionReceipt || result?.receipt) |
| | | return { |
| | | productId, |
| | | transactionDate: normalizeOptionalText(result?.transactionDate), |
| | | transactionIdentifier: normalizeOptionalText(result?.transactionIdentifier || result?.transactionId), |
| | | transactionReceipt: receipt, |
| | | receiptData: receipt, |
| | | raw: result || null, |
| | | } |
| | | } |
| | | |
| | | function normalizeOptionalText(value) { |
| | | return typeof value === 'string' ? value.trim() : '' |
| | | } |