Compare commits

43 Commits

Author SHA1 Message Date
79aa8e3fe6 修复:版本修改 2025-12-05 16:02:14 +08:00
abb7a81b98 修复:解决google play的图片和视频权限要求 2025-12-05 13:41:01 +08:00
f34bae22e4 修复:修改支付失败提示音 2025-12-05 09:51:05 +08:00
339dfeeddc Merge branch 'main' of https://git.nuttyreading.com/zm/taimed-international-app 2025-12-05 08:59:05 +08:00
063dac39e4 更新:增加活动充值金额模块 2025-12-05 08:58:56 +08:00
8502e2d337 Merge branch 'main' of https://git.nuttyreading.com/zm/taimed-international-app 2025-12-04 15:16:33 +08:00
a7360e6459 修复:解决隐私政策查看问题;解决积分支付bug; 2025-12-04 15:16:30 +08:00
50743184ff 修复:修改分割符号 2025-12-04 14:45:51 +08:00
d4e3c5c7a5 修复:去掉多余样式 2025-12-04 14:41:34 +08:00
871eecb889 更新:天医币、积分列表增加跳转订单详情页。修改活动说明字体样式 2025-12-04 14:08:57 +08:00
48a4fb20a6 版本修改 2025-12-03 18:03:44 +08:00
5d7c0042b3 修复:修改提示语类型 2025-12-03 17:08:27 +08:00
44096df651 Merge branch 'main' of https://git.nuttyreading.com/zm/taimed-international-app 2025-12-03 17:03:22 +08:00
c2221bae4c 修复:修复提示音 2025-12-03 17:03:07 +08:00
aa6639ad6c Merge branch 'main' of https://git.nuttyreading.com/zm/taimed-international-app 2025-12-03 15:56:53 +08:00
fa21c7bb74 修复:版本检测及默认语言设置修改 2025-12-03 15:56:51 +08:00
008bc08f81 修复:修改字体大小 2025-12-03 14:56:02 +08:00
ca581eeca2 Merge branch 'main' of https://git.nuttyreading.com/zm/taimed-international-app 2025-12-03 14:14:45 +08:00
03afe41793 修复:更新字体大小 2025-12-03 14:14:33 +08:00
385c28428e Merge branch 'main' of https://git.nuttyreading.com/zm/taimed-international-app 2025-12-03 14:10:30 +08:00
677fe7436e 修复:内测问题修改 2025-12-03 14:10:27 +08:00
a53f482728 修改:字体样式调整 2025-12-03 13:34:04 +08:00
e61ceaf5f3 修复:修改init初始化失败提示 2025-12-03 11:28:18 +08:00
98738a494f 修复:修复样式、及字体大小 2025-12-02 16:22:50 +08:00
35e27753b8 更新:增加数据迁移功能 2025-12-02 11:46:19 +08:00
d653575d27 Merge branch 'main' of https://git.nuttyreading.com/zm/taimed-international-app 2025-12-01 18:03:05 +08:00
fecbb79508 更新:修改金额列表和积分列表显示细节 2025-12-01 18:02:55 +08:00
0ab55b538b 修复:修改书籍购买跳转和章节锁定问题 2025-12-01 17:58:25 +08:00
b4585c93a6 修复:默认头像改为吴门国际logo;修改密码输入提示修改 2025-12-01 16:52:00 +08:00
1049030a46 修复:内测问题修改:免费课程及课程详情代码优化、积分支付默认值、书籍价格显示 2025-12-01 16:39:58 +08:00
cd54ee48de Merge branch 'main' of https://git.nuttyreading.com/zm/taimed-international-app 2025-12-01 16:36:03 +08:00
4b706bb811 更新:增加新闻详情页面 2025-12-01 16:35:44 +08:00
23de3944dc 更新:积分列表字段修改 2025-12-01 15:45:57 +08:00
299989c75a 修复:修复显示文字 2025-12-01 09:01:18 +08:00
1d7d00b6b6 Merge branch 'main' of https://git.nuttyreading.com/zm/taimed-international-app 2025-11-28 18:03:53 +08:00
bdf1b41098 更新:增加订单详情功能 2025-11-28 18:03:13 +08:00
ef2c63784f 更新:配置国际语言 2025-11-28 17:42:47 +08:00
3d20683d76 更新:loading加载时机问题 2025-11-28 16:16:22 +08:00
0e7952ac4e Merge branch 'main' of https://git.nuttyreading.com/zm/taimed-international-app 2025-11-28 14:31:09 +08:00
8f890a6802 修复:内测问题修改 v0.1.1 2025-11-28 14:31:02 +08:00
8edf719431 Merge branch 'main' of https://git.nuttyreading.com/zm/taimed-international-app 2025-11-28 11:50:07 +08:00
3587e15e7a 更新:加按钮节流 2025-11-28 11:48:39 +08:00
79c6fb247c 修复:开发测试问题修改 2025-11-28 11:05:07 +08:00
72 changed files with 3088 additions and 1463 deletions

22
AndroidManifest.xml Normal file
View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 移除 Android 13+ 的图片/视频读取权限,改用系统照片选择器 -->
<uses-permission
android:name="android.permission.READ_MEDIA_IMAGES"
tools:node="remove" />
<uses-permission
android:name="android.permission.READ_MEDIA_VIDEO"
tools:node="remove" />
<!-- 兼容旧版本:移除外部存储读写权限,避免被提升为 READ_MEDIA_* -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:node="remove" />
<application>
<!-- 此文件用于权限移除,不声明组件 -->
</application>
</manifest>

View File

@@ -1,13 +1,13 @@
<script>
// #ifdef APP-PLUS
import updata from "@/uni_modules/uni-upgrade-center-app/utils/check-update";
import update from "@/uni_modules/uni-upgrade-center-app/utils/check-update";
// #endif
export default {
onLaunch: function() {
console.log('App Launch')
// 检测自动更新
// #ifdef APP-PLUS
updata();
update();
// #endif
},
onShow: function() {

View File

@@ -6,6 +6,8 @@ import { createRequestClient } from '../request';
import { SERVICE_MAP } from '../config';
export const paymentClient = createRequestClient({
baseURL: SERVICE_MAP.PAYMENT,
baseURL: SERVICE_MAP.MAIN,
// baseURL: SERVICE_MAP.PAYMENT,
loading: false
});

View File

@@ -7,7 +7,7 @@ export const ENV = process.env.NODE_ENV || 'development';
*/
const BASE_URL_MAP = {
development: {
// MAIN: 'http://192.168.110.100:9300/pb/', // 张川川
// MAIN: 'http://192.168.110.100:9300/pb/', // 张川川
MAIN: 'https://global.nuttyreading.com/', // 线上
// PAYMENT: 'https://dev-pay.example.com', // 暂时用不到
// CDN: 'https://cdn-dev.example.com', // 暂时用不到

View File

@@ -114,7 +114,7 @@ export const courseApi = {
},
/**
* 开始学习免费课程
* 领取免费课程
* @param catalogueId 目录ID
*/
startStudyForMF(catalogueId: number) {

27
api/modules/news.ts Normal file
View File

@@ -0,0 +1,27 @@
import { mainClient } from '@/api/clients/main'
import type { IApiResponse } from '@/api/types'
export const newsApi = {
/**
* 获取新闻详情
*/
getNewsDetail: async (newsId: string | number) => {
const res = await mainClient.request<IApiResponse<any>>({
url: `common/message/getMessageById?id=${newsId}`,
method: 'POST'
})
return res
},
/**
* 获取太湖之光文章详情
*/
getTaihuWelfareArticleDetail: async (newsId: string | number) => {
const res = await mainClient.request<IApiResponse<any>>({
url: 'common/taihuWelfare/getTaihuWelfareArticleDetail',
method: 'POST',
data: { id: newsId }
})
return res
}
}

View File

@@ -9,7 +9,8 @@ import type {
ICoupon,
ICourseOrderCreateParams,
IOrderInitData,
IGoodsDiscountParams
IGoodsDiscountParams,
IOrderDetail
} from '@/types/order'
import type { IUserInfo } from '@/types/user'
@@ -30,19 +31,6 @@ export const orderApi = {
return res
},
/**
* 验证 Google Pay 支付
* @param params 支付验证参数
*/
async verifyGooglePay(params: IGooglePayVerifyParams) {
const res = await mainClient.request<IApiResponse>({
url: 'pay/googlepay/googleVerify',
method: 'POST',
data: params
})
return res
},
/**
* 获取用户信息(包含虚拟币余额)
*/
@@ -106,7 +94,7 @@ export const orderApi = {
},
/**
* 获取地区优惠金额
* 获取活动优惠金额
* @param productList 商品列表
*/
async getDistrictAmount(productList: IGoodsDiscountParams[]) {
@@ -155,5 +143,18 @@ export const orderApi = {
data
})
return res
}
},
/**
* 获取订单详情
* @param orderId 订单ID
*/
async getOrderDetail(orderId: string) {
const res = await mainClient.request<IApiResponse<{ buyOrder: IOrderDetail, productInfo: IOrderGoods[] }>>({
url: 'common/buyOrder/commonOrderDetail',
method: 'POST',
data: { orderId }
})
return res
},
}

View File

@@ -1,5 +1,6 @@
// api/modules/user.ts
import { mainClient } from '@/api/clients/main'
import { paymentClient } from '@/api/clients/payment'
import type { IApiResponse } from '@/api/types'
import type {
IUserInfo,
@@ -195,7 +196,8 @@ export async function submitFeedback(data: IFeedbackForm) {
* @param productId 产品ID
*/
export async function verifyGooglePay(productId: number, purchaseToken: string, orderSn: string) {
const res = await mainClient.request<IApiResponse>({
console.log(productId, purchaseToken, orderSn);
const res = await paymentClient.request<IApiResponse>({
url: 'pay/googlepay/googleVerify',
method: 'POST',
data: { productId, purchaseToken, orderSn }
@@ -235,7 +237,6 @@ export async function getBookBuyConfigList(type: string, qudao: string) {
* @param id 101众妙之门隐私政策
*/
export async function getAgreement(id: string) {
console.log(id, 'id');
const res = await mainClient.request<IApiResponse>({
url: '/sys/agreement/getAgreement',
method: 'POST',
@@ -277,7 +278,7 @@ export async function getTransactionDetailsList(current : number, limit : number
*/
export async function getPlaceOrder(data: object) {
const res = await mainClient.request<IApiResponse>({
const res = await paymentClient.request<IApiResponse>({
url: '/book/buyOrder/placeOrder',
method: 'POST',
data: data
@@ -297,4 +298,19 @@ export async function getPointsData(current : number, limit : number, userId : s
data: { current, limit, userId, }
})
return res
}
/**
* 迁移用户数据
* @param tel 旧账号
* @param code 迁移验证码
* @return
*/
export async function migrateUserData(data: { tel: string, code: string }) {
const res = await mainClient.request<IApiResponse>({
url: 'common/user/migrationWumenData',
method: 'POST',
data
})
return res
}

View File

@@ -23,7 +23,7 @@ export function createRequestClient(cfg: ICreateClientConfig) {
const intercepted = requestInterceptor(final as IRequestOptions);
// 全局处理请求 loading
const loading = !cfg.loading ? true : cfg.loading // 接口请求参数不传loading默认显示loading
const loading = cfg.loading ?? true // 接口请求参数不传loading默认显示loading
if (loading) {
uni.showLoading({ mask: true })
reqCount++

View File

@@ -26,7 +26,7 @@
<text>{{ $t('book.listen') }}</text>
</view>
<view
<!-- <view
v-if="!isIOS"
class="action-btn review"
@click.stop="handleReview"
@@ -35,7 +35,7 @@
<image src="@/static/icon/icon_pl.png" mode="aspectFit" />
</view>
<text>{{ $t('book.comment') }}</text>
</view>
</view> -->
</view>
</view>
</view>

View File

@@ -3,7 +3,7 @@
<view v-if="data.isBuy" class="book-flag">已购买</view>
<view v-else-if="data.isVip == '0'" class="book-flag">免费</view>
<view v-else-if="userHasVip && data.isVip == '1'" class="book-price">VIP免费</view>
<view v-else class="book-price">{{ item.minPrice }} {{ $t('global.coin') }}</view>
<view v-else class="book-price">{{ data.minPrice }} {{ $t('global.coin') }}</view>
<view>
<text v-if="data.readCount" class="book-flag">{{ `${data.readCount}${$t('bookHome.readingCount')}` }}</text>
<text v-else-if="data.buyCount" class="book-flag">{{ `${data.buyCount}${$t('bookHome.purchased')}` }}</text>

View File

@@ -184,24 +184,24 @@ const handleEmojiSelect = (emoji: any) => {
/**
* 选择图片
*/
const chooseImage = () => {
if (uploadedImages.value.length >= 3) {
uni.showToast({
title: '最多只能上传3张图片',
icon: 'none'
})
return
}
// const chooseImage = () => {
// if (uploadedImages.value.length >= 3) {
// uni.showToast({
// title: '最多只能上传3张图片',
// icon: 'none'
// })
// return
// }
uni.chooseImage({
count: 3 - uploadedImages.value.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
uploadImages(res.tempFilePaths)
}
})
}
// uni.chooseImage({
// count: 3 - uploadedImages.value.length,
// sizeType: ['compressed'],
// sourceType: ['album', 'camera'],
// success: (res) => {
// uploadImages(res.tempFilePaths)
// }
// })
// }
/**
* 上传图片

View File

@@ -1,96 +0,0 @@
<template>
<view
v-if="catalogues.length > 1"
:class="['catalogue-list', userVip ? 'vip-style' : '']"
>
<view
v-for="(catalogue, index) in catalogues"
:key="catalogue.id"
:class="['catalogue-item', currentIndex === index ? 'active' : '']"
@click="handleSelect(index)"
>
<text class="catalogue-title">{{ catalogue.title }}</text>
</view>
</view>
</template>
<script setup lang="ts">
import type { ICatalogue, IVipInfo } from '@/types/course'
interface Props {
catalogues: ICatalogue[]
currentIndex: number
userVip: IVipInfo | null
}
const props = defineProps<Props>()
const emit = defineEmits<{
change: [index: number]
}>()
/**
* 选择目录
*/
const handleSelect = (index: number) => {
if (index === props.currentIndex) return
emit('change', index)
}
</script>
<style lang="scss" scoped>
.catalogue-list {
display: flex;
align-items: flex-end;
padding: 20rpx;
padding-bottom: 0;
border-radius: 20rpx 20rpx 0 0;
margin-top: 20rpx;
&.vip-style {
background: linear-gradient(90deg, #6429db 0%, #0075ed 100%);
.catalogue-item {
background-color: rgba(0, 0, 0, 0.4);
color: #fff;
border-color: #fff;
&.active {
background-color: #258feb;
color: #fff;
}
}
}
.catalogue-item {
flex: 1;
text-align: center;
padding: 16rpx 0;
margin-right: 10rpx;
border-radius: 20rpx 20rpx 0 0;
border: 1px solid #fff;
border-bottom: none;
background-color: rgba(0, 0, 0, 0.4);
color: #fff;
transition: all 0.3s;
&:last-child {
margin-right: 0;
}
&.active {
background-color: #258feb;
padding: 20rpx 0;
.catalogue-title {
font-size: 36rpx;
font-weight: bold;
}
}
.catalogue-title {
font-size: 30rpx;
}
}
}
</style>

View File

@@ -1,320 +0,0 @@
<template>
<view class="chapter-list">
<!-- 目录状态信息 -->
<view v-if="catalogue" class="catalogue-status">
<view v-if="catalogue.isBuy === 1 || userVip" class="purchased-info">
<view class="info-row">
<text v-if="userVip">
VIP畅学权益有效期截止到{{ userVip.endTime }}
</text>
<template v-else>
<text v-if="!catalogue.startTime">
当前目录还未开始学习
</text>
<text v-else>
课程有效期截止到{{ catalogue.endTime }}
</text>
<!-- <wd-button
v-if="catalogue.startTime"
size="small"
@click="handleRenew"
>
续费
</wd-button> -->
</template>
</view>
</view>
<!-- 未购买状态 -->
<view v-else-if="catalogue.type === 0" class="free-course">
<wd-button type="success" @click="handleGetFreeCourse">
{{ $t('courseDetails.free') }}
</wd-button>
</view>
<view v-else class="unpurchased-info">
<text class="tip-text">
{{ $t('courseDetails.unpurchasedTip') }}
</text>
<view class="action-btns">
<wd-button size="small" type="warning" @click="handlePurchase">
{{ $t('courseDetails.purchase') }}
</wd-button>
<wd-button
v-if="showRenewBtn"
size="small"
type="success"
@click="handleRenew"
>
{{ $t('courseDetails.relearn') }}
</wd-button>
<wd-button size="small" type="primary" @click="goToVip">
{{ $t('courseDetails.openVip') }}
</wd-button>
</view>
</view>
</view>
<view v-if="chapters.length > 0" class="chapter-content">
<!-- VIP标识 -->
<view v-if="userVip" class="vip-badge">
<text>VIP畅学权益生效中</text>
</view>
<!-- 章节列表 -->
<view
v-for="(chapter, index) in chapters"
:key="chapter.id"
class="chapter-item"
@click="handleChapterClick(chapter)"
>
<view class="chapter-content-wrapper">
<view :class="['chapter-info', !canAccess(chapter) ? 'locked' : '']">
<text class="chapter-title">{{ chapter.title }}</text>
<!-- 试听标签 -->
<wd-tag
v-if="chapter.isAudition === 1 && !isPurchased && !userVip"
type="success"
plain
size="small"
custom-class="chapter-tag"
>
试听
</wd-tag>
<!-- 学习状态标签 -->
<template v-if="isPurchased || userVip">
<wd-tag
v-if="chapter.isLearned === 0"
type="primary"
plain
size="small"
custom-class="chapter-tag"
>
未学
</wd-tag>
<wd-tag
v-else
type="success"
plain
size="small"
custom-class="chapter-tag"
>
已学
</wd-tag>
</template>
</view>
<!-- 锁定图标 -->
<view v-if="!canAccess(chapter)" class="lock-icon">
<wd-icon name="lock-on" size="24px" color="#258feb" />
</view>
</view>
</view>
</view>
<!-- 暂无章节 -->
<view v-else class="no-chapters">
<text>暂无章节内容</text>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { IChapter, ICatalogue, IVipInfo } from '@/types/course'
interface Props {
chapters: IChapter[]
catalogue: ICatalogue
userVip: IVipInfo | null
showRenewBtn?: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
click: [chapter: IChapter],
purchase: [catalogue: ICatalogue],
renew: [catalogue: ICatalogue],
toVip: [catalogue: ICatalogue],
}>()
/**
* 判断目录是否已购买
*/
const isPurchased = computed(() => {
return props.catalogue.isBuy === 1
})
// 购买
const handlePurchase = () => {
emit('purchase', props.catalogue)
}
// 去开通vip
const goToVip = () => {
emit('toVip', props.catalogue)
}
// 续费/复读
const handleRenew = () => {
emit('renew', props.catalogue)
}
// 领取免费课程
const handleGetFreeCourse = () => {
emit('purchase', props.catalogue)
}
/**
* 判断章节是否可以访问
*/
const canAccess = (chapter: IChapter): boolean => {
// VIP用户可以访问所有章节
if (props.userVip) return true
// 已购买目录可以访问所有章节
if (isPurchased.value) return true
// 试听章节可以访问
if (chapter.isAudition === 1) return true
// 免费课程可以访问
if (props.catalogue.type === 0) return true
return false
}
/**
* 点击章节
*/
const handleChapterClick = (chapter: IChapter) => {
if (!canAccess(chapter)) {
if (props.catalogue.type === 0) {
uni.showToast({
title: '请先领取课程',
icon: 'none'
})
} else {
uni.showToast({
title: '请先购买课程',
icon: 'none'
})
}
return
}
emit('click', chapter)
}
</script>
<style lang="scss" scoped>
.chapter-list {
padding: 20rpx;
}
.catalogue-status {
padding: 20rpx;
margin-bottom: 20rpx;
background-color: #fff;
.purchased-info {
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 26rpx;
line-height: 50rpx;
}
}
.free-course {
text-align: center;
}
.unpurchased-info {
.tip-text {
display: block;
font-size: 26rpx;
color: #666;
margin-bottom: 20rpx;
line-height: 1.6;
}
.action-btns {
display: flex;
gap: 20rpx;
justify-content: center;
}
}
}
.chapter-content {
position: relative;
padding: 20rpx;
border: 4rpx solid #fffffc;
background: linear-gradient(52deg, #e8f6ff 0%, #e3f2fe 50%);
box-shadow: 0px 0px 10px 0px #89c8e9;
border-top-right-radius: 40rpx;
border-bottom-left-radius: 40rpx;
.vip-badge {
display: inline-block;
font-size: 24rpx;
background: linear-gradient(90deg, #6429db 0%, #0075ed 100%);
color: #fff;
padding: 10rpx 20rpx;
border-radius: 0 50rpx 50rpx 0;
z-index: 1;
}
.chapter-item {
padding: 20rpx 0;
border-bottom: 1px solid #fff;
&:last-child {
border-bottom: none;
}
.chapter-content-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
.chapter-info {
flex: 1;
display: flex;
align-items: center;
gap: 10rpx;
&.locked {
opacity: 0.6;
}
.chapter-title {
flex: 1;
font-size: 28rpx;
color: #1e2f3e;
line-height: 1.5;
}
.chapter-tag {
flex-shrink: 0;
}
}
.lock-icon {
margin-left: 20rpx;
flex-shrink: 0;
}
}
}
}
.no-chapters {
text-align: center;
padding: 80rpx 0;
color: #999;
font-size: 28rpx;
}
</style>

View File

@@ -290,7 +290,11 @@ const handlePointsInput = (value: any) => {
}
// 重新计算实付款
calculateFinalPrice()
const result = Math.max(
0,
totalAmount.value - pointsDiscounted.value - promotionDiscounted.value - vipDiscounted.value
)
finalAmount.value = result
}
/**
@@ -309,14 +313,16 @@ const calculateFinalPrice = () => {
const couponAmount = 0
// 计算最大可用积分
const orderAmountAfterDiscount = totalAmount.value - promotionDiscounted.value - vipDiscounted.value
const orderAmountAfterDiscount = totalAmount.value - promotionDiscounted.value - vipDiscounted.value - couponAmount
pointsUsableMax.value = Math.min(
props?.userInfo?.jf || 0,
Math.floor(orderAmountAfterDiscount - couponAmount)
Math.floor(props.allowPointPay ? orderAmountAfterDiscount : 0)
)
pointsDiscounted.value = pointsUsableMax.value
// 限制当前积分不超过最大值
if (pointsDiscounted.value > pointsUsableMax.value) {
if (pointsDiscounted.value >= pointsUsableMax.value) {
pointsDiscounted.value = pointsUsableMax.value
}
@@ -325,7 +331,7 @@ const calculateFinalPrice = () => {
0,
totalAmount.value - couponAmount - pointsDiscounted.value - promotionDiscounted.value - vipDiscounted.value
)
finalAmount.value = result
finalAmount.value = parseFloat(result.toPrecision(12))
}
/**
@@ -373,9 +379,11 @@ const handleSubmit = async () => {
})
// 返回上一页
uni.navigateBack({
delta: props.backStep
})
setTimeout(() => {
uni.navigateBack({
delta: props.backStep
})
}, 500)
}
/**
@@ -411,7 +419,7 @@ const createOrder = async (): Promise<string | null> => {
<style lang="scss" scoped>
.confirm-order-page {
min-height: 100vh;
min-height: calc(100vh - 60px - 40rpx);
background-color: #f5f5f5;
padding: 20rpx;
padding-bottom: 60px;

View File

@@ -52,7 +52,7 @@ const props = defineProps({
*/
const goToRecharge = () => {
uni.navigateTo({
url: '/pages/user/wallet/recharge/index?source=order'
url: '/pages/user/recharge/index'
})
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<view class="order-item-content">
<view class="order-item-content" :class="size === 'large' ? 'size-large' : ''">
<view class="order-item-product-info">
<image
:src="productImg"
@@ -18,17 +18,19 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { t } from '@/utils/i18n'
import type { IGoods } from '@/types/order'
interface Props {
data: any
data: IGoods
type: string
size?: 'small' | 'large'
}
const props = defineProps<Props>()
const productImg = computed(() => {
console.log(props.data)
switch (props.type) {
case 'order':
return props.data[0]?.product?.productImages || ''
return props.data?.productImages || ''
case 'abroadBook':
return props.data?.images || ''
case 'vip':
@@ -44,9 +46,9 @@ const productImg = computed(() => {
const title = computed(() => {
switch (props.type) {
case 'order':
return props.data[0]?.product?.productName || ''
return (props.data?.goodsType == '02' ? '(电子书)' : '') + props.data?.productName || ''
case 'abroadBook':
return props.data?.name || ''
return '(电子书)' + props.data?.name || ''
case 'vip':
return props.data?.title + '<text style="color: #ff4703; font-weight: bold;">(' + props.data?.year + '年)</text>' || ''
case 'abroadVip':
@@ -60,7 +62,7 @@ const title = computed(() => {
const price = computed(() => {
switch (props.type) {
case 'order':
return props.data[0]?.product?.price || 0
return props.data?.price || 0
case 'abroadBook':
return props.data?.abroadPrice || 0
case 'vip':
@@ -107,6 +109,7 @@ const price = computed(() => {
color: #333;
text-align: right;
font-size: 12px;
white-space: nowrap;
.count {
font-weight: normal;
@@ -114,4 +117,17 @@ const price = computed(() => {
}
}
}
.order-item-content.size-large {
.order-item-product-cover {
width: 70px;
height: 70px;
}
.order-item-product-name {
font-size: 16px;
}
.order-item-product-price {
font-size: 14px;
}
}
</style>

View File

@@ -25,7 +25,9 @@ export function useVideoProgress() {
const localPosition = videoStorage.getVideoPosition(videoInfo.id) || 0
// 返回较大的值
let position = Math.max(serverPosition, localPosition)
// let position = Math.max(serverPosition, localPosition)
// 采用服务器记录的播放位置
let position = serverPosition
// 如果播放位置接近视频结尾最后5秒内则从头开始
const videoDuration = videoInfo.duration || 0
@@ -99,12 +101,12 @@ export function useVideoProgress() {
*/
const saveNow = async (videoInfo: IVideoInfo) => {
if (currentTime.value > 0) {
// 保存到本地
videoStorage.saveVideoPosition(
videoInfo.id,
Math.floor(currentTime.value),
videoInfo
)
// 保存到本地 注释掉: 不再保存到本地
// videoStorage.saveVideoPosition(
// videoInfo.id,
// Math.floor(currentTime.value),
// videoInfo
// )
// 保存到服务器
await saveToServer(videoInfo.id, Math.floor(currentTime.value))

View File

@@ -134,7 +134,7 @@ onMounted(async () => {
}
// 开始保存进度
player.videoProgress.startSaving(videoInfo)
// player.videoProgress.startSaving(videoInfo) // 注释掉 不再保存到本地
}
})
@@ -142,12 +142,12 @@ onMounted(async () => {
onHide(() => {
console.log('Page hidden, saving progress...')
if (player.currentVideoData.value && player.videoProgress.currentTime.value > 0) {
// 立即保存到本地
videoStorage.saveVideoPosition(
player.currentVideoData.value.id,
Math.floor(player.videoProgress.currentTime.value),
player.currentVideoData.value
)
// 立即保存到本地 注释掉: 不再保存到本地
// videoStorage.saveVideoPosition(
// player.currentVideoData.value.id,
// Math.floor(player.videoProgress.currentTime.value),
// player.currentVideoData.value
// )
// 保存到服务器
player.videoProgress.saveToServer(
player.currentVideoData.value.id,
@@ -164,12 +164,12 @@ onUnmounted(() => {
// 立即保存当前播放进度(同步保存到本地,异步保存到服务器)
if (player.currentVideoData.value && player.videoProgress.currentTime.value > 0) {
// 立即保存到本地
videoStorage.saveVideoPosition(
player.currentVideoData.value.id,
Math.floor(player.videoProgress.currentTime.value),
player.currentVideoData.value
)
// 立即保存到本地 注释掉: 不再保存到本地
// videoStorage.saveVideoPosition(
// player.currentVideoData.value.id,
// Math.floor(player.videoProgress.currentTime.value),
// player.currentVideoData.value
// )
// 保存到服务器(不等待完成)
player.videoProgress.saveToServer(
player.currentVideoData.value.id,
@@ -214,11 +214,13 @@ const pause = () => {
// 暂停时保存进度
if (player.currentVideoData.value && player.videoProgress.currentTime.value > 0) {
videoStorage.saveVideoPosition(
player.currentVideoData.value.id,
Math.floor(player.videoProgress.currentTime.value),
player.currentVideoData.value
)
// 保存进度到本地 注释掉: 不再保存到本地
// videoStorage.saveVideoPosition(
// player.currentVideoData.value.id,
// Math.floor(player.videoProgress.currentTime.value),
// player.currentVideoData.value
// )
// 保存进度到服务器
player.videoProgress.saveToServer(
player.currentVideoData.value.id,
Math.floor(player.videoProgress.currentTime.value)
@@ -342,7 +344,7 @@ const playNext = async () => {
// 获取新视频的初始播放位置
if (player.currentVideoData.value) {
initialTime.value = player.videoProgress.getInitialPosition(player.currentVideoData.value)
player.videoProgress.startSaving(player.currentVideoData.value)
// player.videoProgress.startSaving(player.currentVideoData.value) // 注释掉 不再保存到本地
}
}
}

39
hooks/useThrottle.ts Normal file
View File

@@ -0,0 +1,39 @@
import { ref } from 'vue';
/**
* 按钮节流Hook
* @param callback 点击后执行的业务逻辑回调
* @param delay 节流时间默认1500ms
* @returns 节流后的点击事件函数
* @template T 回调函数的参数类型(支持多参数元组)
*/
export const useThrottle = <T extends any[]>(
callback: (...args: T) => void | Promise<void>,
delay: number = 1500
) => {
// 标记是否可点击
const isClickable = ref(true);
// 节流后的函数,支持传递任意参数
const throttledFn = async (...args: T) => {
if (!isClickable.value) return;
// 锁定按钮
isClickable.value = false;
try {
// 执行业务回调,支持异步函数
await callback(...args);
} catch (error) {
console.error('节流回调执行失败:', error);
throw error; // 抛出错误,方便组件内捕获
} finally {
// 延迟后解锁按钮(无论成功/失败都解锁)
setTimeout(() => {
isClickable.value = true;
}, delay);
}
};
return throttledFn;
};

View File

@@ -19,7 +19,8 @@
"requestException": "Request exception",
"coin": "Coin",
"days": "Days",
"and": "and"
"and": "and",
"call": "Call"
},
"tabar.course": "COURSE",
"tabar.book": "EBOOK",
@@ -193,10 +194,6 @@
"pleaseInputOrderSn": "Please enter order number",
"uploadImageFailed": "Image upload failed",
"maxImagesCount": "Maximum 4 images",
"permissionDenied": "Permission Denied",
"cameraPermission": "Camera permission required",
"storagePermission": "Storage permission required",
"phonePermission": "Phone permission required",
"sendCodeSuccess": "Verification code sent",
"sendCodeFailed": "Failed to send code",
"countdown": "s to resend",
@@ -211,7 +208,26 @@
"yearCard": "Yearly",
"days": "days",
"selectPackage": "Please select a package",
"consumptionRecord": "Consumption record"
"consumptionRecord": "Consumption record",
"returnMine": "I'm about to return to my page",
"dataMigrate": "Data Migration",
"pointsRecord": "Points consumption record",
"migrateSubtitle": "Migrate data from Chinese account to current account",
"oldAccount": "Chinese Account",
"migrateCode": "Migration Code",
"confirmMigrate": "Confirm Migration",
"migrateSuccess": "Migration Successful",
"migrateFailed": "Migration failed, please check if the information is correct",
"pleaseCompleteInfo": "Please complete all information",
"oldAccountPlaceholder": "The chinese account to migrate",
"migrateCodePlaceholder": "The code obtained from chinese account",
"migrateWarning": "Migration is irreversible, please proceed with caution!",
"migrateInstructions": "Migration Instructions",
"instruction1": "Please obtain the migration code from your chinese account, get it by going to 【我的】-【数据迁移】-【获取迁移验证码】.",
"instruction2": "After data migration is complete, the chinese account data will be cleared, and all purchased Tianyi Coins, points, courses, E-book, VIP, certificate, and User Contribution will be transferred to the current account",
"instruction3": "The migration process may take a few minutes, please be patient.",
"instruction4": "If you encounter any issues, please contact customer service for assistance.",
"closeWindow": "Close the payment pop-up window"
},
"book": {
"title": "My Books",
@@ -221,7 +237,7 @@
"comment": "Review",
"choose": "Browse Books",
"nullText": "No books yet, go shopping~",
"afterPurchase": "Available after purchase",
"afterPurchase": "Purchase to read",
"contents": "Contents",
"zjContents": "Chapter List",
"set": "Settings",
@@ -479,7 +495,11 @@
"rechargeAmount": "Recharge amount",
"valueAddedServices": "Value-added services",
"readAgree": "I have read and agreed",
"readAgreeServices": "Please read and agree to the value-added services first"
"readAgreeServices": "Please read and agree to the value-added services first",
"orderDetails": "Order Details",
"pointsRecord": "Points consumption record",
"unusable": "The billing service cannot be used",
"give": "give"
},
"vip": {
"courseVip": "Course VIP",
@@ -487,5 +507,8 @@
"openVip": "Open Now",
"renewal": "Renewal",
"daily": "Daily"
},
"news": {
"newsDetail": "News Detail"
}
}

View File

@@ -1,8 +1,7 @@
import en from './en.json'
// import en from './en.json'
import zhHans from './zh-Hans.json'
// import zhHant from './zh-Hant.json'
// import ja from './ja.json'
export default {
en,
'zh-Hans': zhHans
}

View File

@@ -19,7 +19,8 @@
"requestException": "请求异常",
"coin": "天医币",
"days": "天",
"and": "和"
"and": "和",
"call": "拨打电话"
},
"tabar.course": "课程",
"tabar.book": "图书",
@@ -194,10 +195,6 @@
"pleaseInputOrderSn": "请输入订单编号",
"uploadImageFailed": "图片上传失败",
"maxImagesCount": "最多上传4张图片",
"permissionDenied": "权限被拒绝",
"cameraPermission": "需要相机权限",
"storagePermission": "需要存储权限",
"phonePermission": "需要电话权限",
"sendCodeSuccess": "验证码发送成功",
"sendCodeFailed": "验证码发送失败",
"countdown": "秒后重新发送",
@@ -212,7 +209,26 @@
"yearCard": "年卡",
"days": "天",
"selectPackage": "请选择套餐",
"consumptionRecord": "消费记录"
"consumptionRecord": "消费记录",
"returnMine": "即将返回我的页面",
"pointsRecord": "积分记录",
"dataMigrate": "数据迁移",
"migrateSubtitle": "迁移国内账号数据到本账号",
"oldAccount": "国内版账号",
"migrateCode": "迁移验证码",
"confirmMigrate": "确定迁移",
"migrateSuccess": "迁移成功",
"migrateFailed": "迁移失败,请检查信息是否正确",
"pleaseCompleteInfo": "请填写完整信息",
"oldAccountPlaceholder": "需要迁移的国内版账号",
"migrateCodePlaceholder": "国内版账号获取的迁移验证码",
"migrateWarning": "迁移后不可恢复,请谨慎操作!",
"migrateInstructions": "迁移说明",
"instruction1": "请在吴门医述、心灵空间、众妙之门、疯子读书任意APP中获取迁移验证码获取方式【我的】-【数据迁移】-【获取迁移验证码】。",
"instruction2": "数据迁移完成后旧账号数据将被清空已购买的天医币、积分、课程、电子书、VIP、证书、湖分将转移到当前账号。",
"instruction3": "迁移过程可能需要几分钟时间,请耐心等待。",
"instruction4": "如遇到问题,请联系客服获取帮助。",
"closeWindow": "关闭支付弹窗"
},
"book": {
"title": "我的书单",
@@ -222,7 +238,7 @@
"comment": "书评",
"choose": "去选书",
"nullText": "暂无书籍,快去选购吧~",
"afterPurchase": "购买后即可使用此功能",
"afterPurchase": "购买后即可阅读此章节",
"contents": "目录",
"zjContents": "章节目录",
"set": "设置",
@@ -480,7 +496,11 @@
"rechargeAmount": "充值金额",
"valueAddedServices": "增值服务",
"readAgree": "我已阅读并同意",
"readAgreeServices": "请先阅读并同意增值服务"
"readAgreeServices": "请先阅读并同意增值服务",
"orderDetails": "订单详情",
"pointsRecord": "积分消费记录",
"unusable": "无法用计费服务",
"give": "赠"
},
"vip": {
"courseVip": "课程VIP",
@@ -488,5 +508,8 @@
"openVip": "立即开通",
"renewal": "续费",
"daily": "日均"
},
"news": {
"newsDetail": "新闻详情"
}
}

View File

@@ -2,8 +2,8 @@
"name" : "吴门国际",
"appid" : "__UNI__1250B39",
"description" : "吴门国际",
"versionName" : "1.0.4",
"versionCode" : 104,
"versionName" : "1.0.9",
"versionCode" : 109,
"transformPx" : false,
/* 5+App */
"app-plus" : {
@@ -19,6 +19,15 @@
"autoclose" : true,
"delay" : 0
},
"privacy" : {
"prompt" : "template",
"template" : {
"title" : "用户协议和隐私政策",
"message" : "请你务必审慎阅读、充分理解“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/>你可阅读<a href='https://www.amazinglimited.com/agreement.html'>《用户协议》</a> 和 <a href='https://www.amazinglimited.com/privacy.html'>《隐私协议》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。",
"buttonAccept" : "同意",
"buttonRefuse" : "暂不同意"
}
},
/* */
"modules" : {
"Camera" : {},
@@ -29,27 +38,23 @@
"distribute" : {
/* android */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.READ_MEDIA_IMAGES\"/>",
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>"
],
"permissions" : [],
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ],
"minSdkVersion" : 23,
"targetSdkVersion" : 35
"targetSdkVersion" : 35,
"excludePermissions" : [
"<uses-permission android:name=\"android.permission.READ_MEDIA_IMAGES\" />",
"<uses-permission android:name=\"android.permission.READ_MEDIA_VIDEO\" />",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />"
]
},
/* ios */
"ios" : {
"dSYMs" : false,
"privacyDescription" : {
"NSPhotoLibraryUsageDescription" : "Ensure the normal use of your avatar modification, appeal feedback, image upload, and message upload functions in this app.",
"NSPhotoLibraryAddUsageDescription" : "Ensure the normal use of the functions of modifying avatars, uploading images for appeals and feedback, and uploading images for comments in this app.",
"NSCameraUsageDescription" : "Ensure the normal use of the functions of modifying avatars, uploading images for appeals and feedback, and uploading images for comments in this app."
"NSPhotoLibraryUsageDescription" : "保障您在此app中的订单售后问题描述上传图片、使用问题反馈上传图片、修改头像功能的正常使用",
"NSPhotoLibraryAddUsageDescription" : "保障您在此app中的订单售后问题描述上传图片、使用问题反馈上传图片、修改头像功能的正常使用",
"NSCameraUsageDescription" : "保障您在此app中的订单售后问题描述上传图片、使用问题反馈上传图片、修改头像功能的正常使用"
},
"idfa" : false
},
@@ -140,5 +145,7 @@
"enable" : false,
"version" : "2"
},
"vueVersion" : "3"
"vueVersion" : "3",
"locale" : "zh-Hans",
"fallbackLocale" : "zh-Hans"
}

View File

@@ -187,6 +187,24 @@
"navigationStyle": "custom",
"navigationBarTitleText": "%order.bookVip%"
}
}, {
"path": "pages/user/order/details",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "%order.orderDetails%"
}
}, {
"path": "pages/news/details",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "新闻详情"
}
}, {
"path": "pages/user/migrate/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "数据迁移"
}
}, {
"path": "uni_modules/uni-upgrade-center-app/pages/upgrade-popup",
"style": {

View File

@@ -40,7 +40,7 @@
</view>
<!-- 书评列表 (非iOS) -->
<view v-if="!isIOS" class="comments-section">
<!-- <view v-if="!isIOS" class="comments-section">
<view class="section-header">
<text class="section-title">{{ $t('bookDetails.message') }}</text>
<view v-if="commentList.length > 0" class="more-link" @click="goToReview">
@@ -55,7 +55,7 @@
/>
<text v-else class="empty-text">{{ nullText }}</text>
</view>
</view>
</view> -->
<!-- 相关推荐 -->
<view class="related-books">
@@ -79,15 +79,15 @@
<!-- 底部操作栏 -->
<view class="action-bar">
<template v-if="bookInfo.isBuy">
<template v-if="bookInfo.isBuy || hasVip">
<view class="action-btn read" @click="goToReader">
<text>{{ $t('bookDetails.startReading') }}</text>
</view>
<view class="action-btn purchased">
<!-- <view class="action-btn purchased">
<wd-button disabled custom-class="purchased-btn">
{{ $t('bookDetails.buttonText2') }}
</wd-button>
</view>
</view> -->
<view class="action-btn listen" @click="goToListen">
<text>{{ $t('bookDetails.startListening') }}</text>
</view>
@@ -120,19 +120,23 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import { t } from '@/utils/i18n'
import { bookApi } from '@/api/modules/book'
import { useUserStore } from '@/stores/user'
import type { IBookDetail, IBook, IComment } from '@/types/book'
import type { IGoods } from '@/types/order'
import GoodsSelector from '@/components/order/GoodsSelector.vue'
import CommentList from '@/components/book/CommentList.vue'
const { t } = useI18n()
const userStore = useUserStore()
// 路由参数
const bookId = ref(0)
const pageFrom = ref('')
// 会员状态
const hasVip = computed(() => userStore.userInfo?.userEbookVip?.length > 0 || false)
// 数据状态
const bookInfo = ref<IBookDetail>({
id: 0,
@@ -264,7 +268,7 @@ function handlePurchase(goods: IGoods) {
// 页面跳转
function goToReader() {
const isBuy = bookInfo.value.isBuy ? 0 : 1
const isBuy = bookInfo.value.isBuy ? 1 : 0
const count = bookInfo.value.freeChapterCount || 0
uni.navigateTo({
url: `/pages/book/reader?isBuy=${isBuy}&bookId=${bookId.value}&count=${count}`

View File

@@ -172,10 +172,11 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { homeApi } from '@/api/modules/book_home'
import { getNotchHeight } from '@/utils/system'
import { useUserStore } from '@/stores/user'
import BookPrice from '@/components/book/BookPrice.vue'
import type {
IBook,
@@ -184,6 +185,8 @@ import type {
IVipInfo
} from '@/types/book'
const userStore = useUserStore()
// 状态定义
const showMyBooks = ref(false)
const showActivity = ref(false)
@@ -208,17 +211,7 @@ const currentLevel1Index = ref(0)
const currentLevel2Index = ref(0)
// VIP信息
const vipInfo = ref<IVipInfo | null>(null)
/**
* 获取VIP信息
*/
const getVipInfo = async () => {
const res = await homeApi.getVipInfo()
if (res.vipInfo) {
vipInfo.value = res.vipInfo
}
}
const vipInfo = computed(() => userStore.userInfo?.userEbookVip?.[0] || null)
/**
* 获取我的书单
@@ -329,7 +322,7 @@ const handleSearch = ({ value }: { value: string }) => {
*/
const handleMyBookClick = (bookId: number) => {
uni.navigateTo({
url: `/pages/book/reader?isBuy=0&bookId=${bookId}`
url: `/pages/book/reader?isBuy=1&bookId=${bookId}`
})
}
@@ -388,7 +381,6 @@ onMounted(() => {
*/
onShow(() => {
// 刷新数据
getVipInfo()
getMyBooks()
getRecommendBooks()
getActivityLabels()

View File

@@ -2,35 +2,31 @@
<view class="listen-page">
<!-- 导航栏 -->
<nav-bar :title="$t('listen.title')"></nav-bar>
<scroll-view
scroll-y
class="listen-scroll"
:style="{ height: scrollHeight + 'px' }"
>
<!-- 书籍信息 -->
<view class="book-info">
<image :src="bookInfo.images" class="cover" mode="aspectFill" />
<view class="info">
<text class="title">{{ bookInfo.name }}</text>
<text class="author">{{ $t('bookDetails.authorName') }}{{ bookInfo.author?.authorName }}</text>
</view>
<wd-button
v-if="!bookInfo.isBuy"
type="primary"
size="small"
@click="goToPurchase"
>
{{ $t('bookDetails.buy') }}
</wd-button>
<!-- 书籍信息 -->
<view class="book-info">
<image :src="bookInfo.images" class="cover" mode="aspectFill" />
<view class="info">
<text class="title">{{ bookInfo.name }}</text>
<text class="author">{{ $t('bookDetails.authorName') }}{{ bookInfo.author?.authorName }}</text>
</view>
<view class="divider-line" />
<!-- 章节列表 -->
<view class="chapter-section">
<text class="section-title">{{ $t('book.zjContents') }}</text>
<wd-button
v-if="!bookInfo.isBuy && !hasVip"
type="primary"
size="small"
@click="purchaseVisible = true"
>
{{ $t('bookDetails.buy') }}
</wd-button>
</view>
<!-- 章节列表 -->
<view class="chapter-section">
<text class="section-title">{{ $t('book.zjContents') }}</text>
<scroll-view
scroll-y
style="height: calc(100vh - 570rpx);"
>
<view v-if="chapterList.length > 0" class="chapter-list">
<view
v-for="(chapter, index) in chapterList"
@@ -42,30 +38,43 @@
<text class="chapter-text" :class="{ locked: isLocked(index) }">
{{ chapter.chapter }}{{ chapter.content ? ' - ' + chapter.content : '' }}
</text>
<wd-icon v-if="isLocked(index)" name="lock" size="20px" />
<wd-icon v-if="isLocked(index)" name="lock-on" size="20px" />
</view>
</view>
<text v-else class="empty-text">{{ nullText }}</text>
</view>
</scroll-view>
</scroll-view>
</view>
<!-- 购买弹窗 -->
<GoodsSelector
:show="purchaseVisible"
:goods="goodsList"
@confirm="handlePurchase"
@close="closePurchasePopup"
/>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { t } from '@/utils/i18n'
import { bookApi } from '@/api/modules/book'
import { useUserStore } from '@/stores/user'
import type { IBookDetail, IChapter } from '@/types/book'
import CustomNavbar from '@/components/book/CustomNavbar.vue'
import type { IGoods } from '@/types/order'
import GoodsSelector from '@/components/order/GoodsSelector.vue'
const { t } = useI18n()
const userStore = useUserStore()
// 路由参数
const bookId = ref(0)
const fromIndex = ref(-1)
// 会员状态
const hasVip = computed(() => userStore.userInfo?.userEbookVip?.length > 0 || false)
// 数据状态
const bookInfo = ref<IBookDetail>({
id: 0,
@@ -78,12 +87,6 @@ const bookInfo = ref<IBookDetail>({
const chapterList = ref<IChapter[]>([])
const activeIndex = ref(-1)
const nullText = ref('')
const scrollHeight = ref(0)
// 计算属性
const isLocked = computed(() => (index: number) => {
return !bookInfo.value.isBuy && index + 1 > bookInfo.value.freeChapterCount
})
// 生命周期
onLoad((options: any) => {
@@ -94,25 +97,34 @@ onLoad((options: any) => {
fromIndex.value = Number(options.index)
activeIndex.value = fromIndex.value
}
initScrollHeight()
loadGoodsInfo()
})
onShow(() => {
loadBookInfo()
loadChapterList()
})
// 初始化滚动区域高度
function initScrollHeight() {
const systemInfo = uni.getSystemInfoSync()
const statusBarHeight = systemInfo.statusBarHeight || 0
let navBarHeight = 44
if (systemInfo.model.includes('iPhone')) {
const modelNumber = parseInt(systemInfo.model.match(/\d+/)?.[0] || '0')
if (modelNumber >= 11) {
navBarHeight = 48
}
}
const totalNavHeight = statusBarHeight + navBarHeight
scrollHeight.value = systemInfo.windowHeight - totalNavHeight
// 购买弹窗状态
const purchaseVisible = ref(false)
const goodsList = ref<IGoods[]>([])
// 关闭购买弹窗
function closePurchasePopup() {
purchaseVisible.value = false
}
// 确认购买
function handlePurchase(goods: IGoods) {
uni.navigateTo({
url: `/pages/order/goodsConfirm?goods=${goods.productId}`
})
}
// 加载购买商品信息
async function loadGoodsInfo() {
const res = await bookApi.getBookGoods(bookId.value)
goodsList.value = res.productList || []
}
// 加载书籍信息
@@ -134,10 +146,15 @@ async function loadChapterList() {
}
}
// 判断章节是否锁定
function isLocked(index: number): boolean {
return !bookInfo.value.isBuy && index + 1 > bookInfo.value.freeChapterCount && !hasVip.value
}
// 播放章节
function playChapter(chapter: IChapter, index: number) {
// 检查是否锁定
if (isLocked.value(index)) {
if (isLocked(index)) {
uni.showToast({
title: t('book.afterPurchase'),
icon: 'none'
@@ -176,106 +193,99 @@ function goToPurchase() {
.listen-page {
background: #f7faf9;
min-height: 100vh;
}
.book-info {
margin: 20rpx;
padding: 30rpx;
background: #fff;
border-radius: 15rpx;
display: flex;
align-items: center;
position: relative;
.listen-scroll {
.book-info {
margin: 20rpx;
padding: 30rpx;
background: #fff;
border-radius: 15rpx;
display: flex;
align-items: center;
position: relative;
.cover {
width: 180rpx;
height: 240rpx;
border-radius: 10rpx;
flex-shrink: 0;
}
.info {
flex: 1;
padding: 0 20rpx;
.title {
display: block;
font-size: 36rpx;
font-weight: bold;
color: #333;
line-height: 44rpx;
max-height: 88rpx;
overflow: hidden;
}
.author {
display: block;
font-size: 30rpx;
padding-top: 15rpx;
color: #666;
}
}
.cover {
width: 180rpx;
height: 240rpx;
border-radius: 10rpx;
flex-shrink: 0;
}
.info {
flex: 1;
padding: 0 20rpx;
.title {
display: block;
font-size: 36rpx;
font-weight: bold;
color: #333;
line-height: 44rpx;
max-height: 88rpx;
overflow: hidden;
}
.divider-line {
height: 20rpx;
background: #f7faf9;
}
.chapter-section {
background: #fff;
padding: 30rpx;
min-height: 400rpx;
.section-title {
display: block;
font-size: 34rpx;
line-height: 50rpx;
margin-bottom: 20rpx;
color: #333;
font-weight: 500;
}
.chapter-list {
.chapter-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18rpx 0;
border-bottom: 1rpx solid #dcdfe6;
&.active .chapter-text {
color: #54a966;
}
.chapter-text {
flex: 1;
font-size: 28rpx;
line-height: 50rpx;
color: #333;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&.locked {
color: #999;
}
}
&:last-child {
border-bottom: none;
}
}
}
.empty-text {
display: block;
text-align: center;
padding: 100rpx 0;
font-size: 28rpx;
color: #999;
}
.author {
display: block;
font-size: 30rpx;
padding-top: 15rpx;
color: #666;
}
}
}
.chapter-section {
background: #fff;
padding: 30rpx;
min-height: 400rpx;
.section-title {
display: block;
font-size: 34rpx;
line-height: 50rpx;
margin-bottom: 20rpx;
color: #333;
font-weight: 500;
}
.chapter-list {
.chapter-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18rpx 0;
border-bottom: 1rpx solid #dcdfe6;
&.active .chapter-text {
color: #54a966;
}
.chapter-text {
flex: 1;
font-size: 28rpx;
line-height: 50rpx;
color: #333;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&.locked {
color: #999;
}
}
&:last-child {
border-bottom: none;
}
}
}
.empty-text {
display: block;
text-align: center;
padding: 100rpx 0;
font-size: 28rpx;
color: #999;
}
}
</style>

View File

@@ -80,7 +80,8 @@
:class="{ 'chapter-item-active': currentChapterIndex === index }"
@click="selectChapter(index)"
>
<text class="chapter-title">{{ chapter.chapter }}</text>
<text class="chapter-title" :class="{ locked: isLocked(index) }">{{ chapter.chapter }}</text>
<wd-icon v-if="isLocked(index)" name="lock-on" size="20px" />
</view>
</scroll-view>
</view>
@@ -92,10 +93,15 @@
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { onLoad, onHide, onUnload } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user'
import { bookApi } from '@/api/modules/book'
import type { IBookDetail, IChapter } from '@/types/book'
const { t } = useI18n()
const userStore = useUserStore()
// 会员状态
const hasVip = computed(() => userStore.userInfo?.userEbookVip?.length > 0 || false)
// 路由参数
const bookId = ref(0)
@@ -284,6 +290,11 @@ async function loadChapterList() {
}
}
// 判断章节是否锁定
function isLocked(index: number): boolean {
return !bookInfo.value.isBuy && index + 1 > bookInfo.value.freeChapterCount && !hasVip.value
}
// 加载章节内容(带音频时间点)
async function loadChapterContent(chapterId: number) {
try {
@@ -375,7 +386,7 @@ function togglePlay() {
// 上一章
async function prevChapter() {
if (currentChapterIndex.value > 0) {
if (currentChapterIndex.value > 0 && !isLocked(currentChapterIndex.value - 1)) {
currentChapterIndex.value--
await loadChapterContent(chapterList.value[currentChapterIndex.value].id)
playChapter(chapterList.value[currentChapterIndex.value])
@@ -390,19 +401,23 @@ async function prevChapter() {
// 下一章
async function nextChapter() {
// 检查是否锁定
if (isBuy.value === '1' && currentChapterIndex.value + 1 >= count.value) {
uni.showModal({
title: t('common.limit_title'),
content: t('book.afterPurchase'),
confirmText: t('common.confirm_text'),
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: `/pages/book/order?id=${bookId.value}`
})
}
}
if (isLocked(currentChapterIndex.value + 1)) {
uni.showToast({
title: t('book.afterPurchase'),
icon: 'none'
})
// uni.showModal({
// title: t('common.limit_title'),
// content: t('book.afterPurchase'),
// confirmText: t('common.confirm_text'),
// success: (res) => {
// if (res.confirm) {
// uni.navigateTo({
// url: `/pages/book/order?id=${bookId.value}`
// })
// }
// }
// })
return
}
@@ -491,7 +506,7 @@ async function selectChapter(index: number) {
}
// 检查是否锁定
if (isBuy.value === '1' && index >= count.value) {
if (isLocked(index)) {
uni.showToast({
title: t('book.afterPurchase'),
icon: 'none'
@@ -698,6 +713,10 @@ function formatTime(seconds: number): string {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.locked {
color: #999;
}
}
.chapter-item-active .chapter-title {

View File

@@ -102,6 +102,7 @@
<text class="popup-title">{{ $t('book.zjContents') }}</text>
<scroll-view scroll-y class="chapter-list">
<view
v-if="chapterList.length > 0"
v-for="(chapter, index) in chapterList"
:key="chapter.id"
class="chapter-item"
@@ -111,8 +112,9 @@
<text class="chapter-text" :class="{ locked: isLocked(index) }">
{{ chapter.chapter }}{{ chapter.content ? ' - ' + chapter.content : '' }}
</text>
<wd-icon v-if="isLocked(index)" name="lock" size="20px" />
<wd-icon v-if="isLocked(index) && !hasVip" name="lock-on" size="20px" />
</view>
<wd-status-tip v-else image="content" :tip="$t('global.dataNull')" />
</scroll-view>
</view>
</wd-popup>
@@ -191,6 +193,7 @@
import { ref, computed, onMounted } from 'vue'
import { onLoad, onShow, onHide, onBackPress } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user'
import { useBookStore } from '@/stores/book'
import { bookApi } from '@/api/modules/book'
import type { IChapter, IChapterContent, IReadProgress } from '@/types/book'
@@ -198,10 +201,14 @@ import { onPageBack } from '@/utils/index'
const { t } = useI18n()
const bookStore = useBookStore()
const userStore = useUserStore()
// 会员状态
const hasVip = computed(() => userStore.userInfo?.userEbookVip?.length > 0 || false)
// 路由参数
const bookId = ref(0)
const isBuy = ref('0')
const isBuy = ref(false)
const count = ref(0)
// 数据状态
@@ -273,7 +280,7 @@ const currentChapterTitle = computed(() => {
onLoad((options: any) => {
if (options.bookId) bookId.value = Number(options.bookId)
if (options.isBuy) isBuy.value = options.isBuy
if (options.isBuy) isBuy.value = options.isBuy == 1 ? true : false
if (options.count) count.value = Number(options.count)
// 获取刘海高度
@@ -430,7 +437,7 @@ async function switchChapter(chapter: IChapter, index: number) {
// 判断章节是否锁定
function isLocked(index: number): boolean {
return isBuy.value === '1' && index + 1 > count.value
return !isBuy.value && index + 1 > count.value && !hasVip.value
}
// 判断是否是图片
@@ -604,10 +611,9 @@ function changeReadMode(mode: 'scroll' | 'page') {
// 语言配置
const bookLanguages = ref([])
const currentLanguage = ref('')
const currentLanguage = ref('中文')
onMounted(() => {
currentLanguage.value = uni.getStorageSync('currentBookLanguage') || '中文'
console.log('currentLanguage', currentLanguage.value)
})
const getBookLanguages = async () => {
const res = await bookApi.getBookLanguages(bookId.value)
@@ -658,7 +664,7 @@ async function saveProgress() {
position: fixed;
left: 0;
right: 0;
z-index: 999;
z-index: 90;
background: inherit;
display: flex;
align-items: center;

View File

@@ -27,12 +27,7 @@
>
<image :src="item.images" />
<text class="book-text">{{ item.name }}</text>
<text v-if="formatPrice(item)" class="book-price">{{
formatPrice(item)
}}</text>
<text v-if="formatStats(item)" class="book-flag">{{
formatStats(item)
}}</text>
<BookPrice :data="item" class="book-price-container" />
</view>
</view>
<view v-else-if="isEmpty" class="empty-wrapper">
@@ -47,6 +42,7 @@ import { ref, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import { homeApi } from '@/api/modules/book_home'
import BookPrice from '@/components/book/BookPrice.vue'
import type { IBookWithStats, IVipInfo } from '@/types/home'
const { t } = useI18n()
@@ -260,21 +256,9 @@ onMounted(async () => {
overflow: hidden;
}
.book-price {
position: absolute;
font-size: 28rpx;
color: #ff4703;
left: 30rpx;
bottom: 20rpx;
}
.book-flag {
display: block;
font-size: 26rpx;
color: #999;
position: absolute;
right: 6%;
bottom: 20rpx;
.book-price-container {
width: 80%;
margin: 15rpx auto 0;
}
}
}

View File

@@ -31,7 +31,7 @@
<view v-if="videoList.length > 0" class="video-list-section">
<view class="section-title">{{ $t('courseDetails.videoTeaching') }}</view>
<wd-radio-group v-model="currentVideoIndex" shape="button" >
<wd-radio v-for="(video, index) in videoList" :key="video.id" :value="index">
<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 }}
</wd-radio>
</wd-radio-group>
@@ -122,14 +122,6 @@ const loadChapterDetail = async () => {
}
}
/**
* 选择视频
*/
const selectVideo = async (index: number) => {
if (index === currentVideoIndex.value) return
currentVideoIndex.value = index
}
/**
* 预览图片
*/

View File

@@ -0,0 +1,450 @@
<template>
<view class="course-content-wrapper">
<view
v-if="catalogues.length > 1"
:class="['catalogue-list', userVip ? 'vip-style' : '']"
>
<view
v-for="(catalogue, index) in catalogues"
:key="catalogue.id"
:class="['catalogue-item', currentCatalogueIndex === index ? 'active' : '']"
@click="handleSelect(index)"
>
<text class="catalogue-title">{{ catalogue.title }}</text>
</view>
</view>
<view class="chapter-list">
<!-- 目录状态信息 -->
<view class="catalogue-status">
<view v-if="currentCatalogue?.isBuy === 1 || userVip" class="purchased-info">
<view class="info-row">
<text v-if="userVip">
VIP畅学权益有效期截止到{{ userVip.endTime }}
</text>
<template v-else>
<text v-if="!currentCatalogue.startTime">
当前目录还未开始学习
</text>
<text v-else>
课程有效期截止到{{ currentCatalogue.endTime }}
</text>
<!-- <wd-button
v-if="currentCatalogue.startTime"
size="small"
@click="handleRenew"
>
续费
</wd-button> -->
</template>
</view>
</view>
<!-- 未购买状态 -->
<view v-else-if="currentCatalogue?.type === 0" class="free-course">
<wd-button type="success" @click="handleGetFreeCourse">
{{ $t('courseDetails.free') }}
</wd-button>
</view>
<view v-else class="unpurchased-info">
<text class="tip-text">
{{ $t('courseDetails.unpurchasedTip') }}
</text>
<view class="action-btns">
<wd-button size="small" type="warning" @click="handlePurchase">
{{ $t('courseDetails.purchase') }}
</wd-button>
<wd-button
v-if="showRenewBtn"
size="small"
type="success"
@click="handleRenew"
>
{{ $t('courseDetails.relearn') }}
</wd-button>
<wd-button size="small" type="primary" @click="goToVip">
{{ $t('courseDetails.openVip') }}
</wd-button>
</view>
</view>
</view>
<view v-if="chapterList.length > 0" class="chapter-content">
<!-- VIP标识 -->
<view v-if="userVip" class="vip-badge">
<text>VIP畅学权益生效中</text>
</view>
<!-- 章节列表 -->
<view
v-for="(chapter, index) in chapterList"
:key="chapter.id"
class="chapter-item"
@click="handleChapterClick(chapter)"
>
<view class="chapter-content-wrapper">
<view :class="['chapter-info', !canAccess(chapter) ? 'locked' : '']">
<text class="chapter-title">{{ chapter.title }}</text>
<!-- 试听标签 -->
<wd-tag
v-if="chapter.isAudition === 1 && !isPurchased && !userVip"
type="success"
plain
size="small"
custom-class="chapter-tag"
>
试听
</wd-tag>
<!-- 学习状态标签 -->
<template v-if="isPurchased || userVip">
<wd-tag
v-if="chapter.isLearned === 0"
type="primary"
plain
size="small"
custom-class="chapter-tag"
>
未学
</wd-tag>
<wd-tag
v-else
type="success"
plain
size="small"
custom-class="chapter-tag"
>
已学
</wd-tag>
</template>
</view>
<!-- 锁定图标 -->
<view v-if="!canAccess(chapter) && currentCatalogue.type != 0" class="lock-icon">
<wd-icon name="lock-on" size="24px" color="#258feb" />
</view>
</view>
</view>
</view>
<!-- 暂无章节 -->
<view v-else class="no-chapters">
<text>暂无章节内容</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { courseApi } from '@/api/modules/course'
import type { IChapter, ICatalogue, IVipInfo } from '@/types/course'
interface Props {
catalogues: ICatalogue[]
userVip: IVipInfo | null
showRenewBtn?: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
click: [chapter: IChapter],
purchase: [catalogue: ICatalogue],
renew: [catalogue: ICatalogue],
toVip: [catalogue: ICatalogue],
change: [index: number]
}>()
// 当前目录索引
const currentCatalogueIndex = ref<number>(0)
// 当前目录
const currentCatalogue = computed(() => {
return props.catalogues[currentCatalogueIndex.value]
})
// 当前目录的章节
const chapterList = ref<IChapter[]>([])
// 显示续费按钮
const showRenewBtn = ref<boolean>(false)
// 判断目录是否已购买
const isPurchased = computed(() => {
return currentCatalogue.value.isBuy === 1
})
/**
* 选择目录
*/
const handleSelect = (index: number) => {
if (index === currentCatalogueIndex.value) return
currentCatalogueIndex.value = index
getChapters() // 获取章节列表
checkRenewPayment() // 检查是否支持复读
}
/**
* 获取当前目录的章节
*/
const getChapters = async () => {
const res = await courseApi.getCatalogueChapterList(currentCatalogue.value.id)
chapterList.value = res.chapterList || []
}
/**
* 检查目录是否支持复读
*/
const checkRenewPayment = async () => {
if (currentCatalogue.value.isBuy === 0 && !props.userVip) {
const renewRes = await courseApi.checkRenewPayment(currentCatalogue.value.id)
showRenewBtn.value = renewRes.canRelearn || false
} else {
showRenewBtn.value = false
}
}
/**
* 监听目录变化
*/
watch(() => props.catalogues, (newVal: ICatalogue[]) => {
if (newVal.length > 0) {
currentCatalogueIndex.value = 0
getChapters()
checkRenewPayment()
}
}, { immediate: true, deep: true })
// 购买
const handlePurchase = () => {
emit('purchase', currentCatalogue.value)
}
// 去开通vip
const goToVip = () => {
emit('toVip', currentCatalogue.value)
}
// 续费/复读
const handleRenew = () => {
emit('renew', currentCatalogue.value)
}
// 领取免费课程
const handleGetFreeCourse = async () => {
emit('getFreeCourse', currentCatalogue.value)
}
/**
* 判断章节是否可以访问
*/
const canAccess = (chapter: IChapter): boolean => {
// VIP用户可以访问所有章节
if (props.userVip) return true
// 已购买目录可以访问所有章节
if (isPurchased.value) return true
// 试听章节可以访问
if (chapter.isAudition === 1) return true
// 免费课程可以访问
// if (currentCatalogue.value.type === 0) return true
return false
}
/**
* 点击章节
*/
const handleChapterClick = (chapter: IChapter, catalogue: ICatalogue) => {
if (!canAccess(chapter)) {
if (currentCatalogue.value.type === 0) {
uni.showToast({
title: '请先领取课程',
icon: 'none'
})
} else {
uni.showToast({
title: '请先购买课程',
icon: 'none'
})
}
return
}
emit('toDetail', chapter, currentCatalogue.value)
}
</script>
<style lang="scss" scoped>
.course-content-wrapper {
background: linear-gradient(108deg, #c3e7ff 0%, #59bafe 100%);
}
.catalogue-list {
display: flex;
align-items: flex-end;
padding: 20rpx;
padding-bottom: 0;
border-radius: 20rpx 20rpx 0 0;
margin-top: 20rpx;
&.vip-style {
background: linear-gradient(90deg, #6429db 0%, #0075ed 100%);
.catalogue-item {
background-color: rgba(0, 0, 0, 0.4);
color: #fff;
border-color: #fff;
&.active {
background-color: #258feb;
color: #fff;
}
}
}
.catalogue-item {
flex: 1;
text-align: center;
padding: 16rpx 0;
margin-right: 10rpx;
border-radius: 20rpx 20rpx 0 0;
border: 1px solid #fff;
border-bottom: none;
background-color: rgba(0, 0, 0, 0.4);
color: #fff;
transition: all 0.3s;
&:last-child {
margin-right: 0;
}
&.active {
background-color: #258feb;
padding: 20rpx 0;
.catalogue-title {
font-size: 36rpx;
font-weight: bold;
}
}
.catalogue-title {
font-size: 30rpx;
}
}
}
.chapter-list {
padding: 20rpx;
}
.catalogue-status {
padding: 20rpx;
margin-bottom: 20rpx;
background-color: #fff;
.purchased-info {
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 26rpx;
line-height: 50rpx;
}
}
.free-course {
text-align: center;
}
.unpurchased-info {
.tip-text {
display: block;
font-size: 26rpx;
color: #666;
margin-bottom: 20rpx;
line-height: 1.6;
}
.action-btns {
display: flex;
gap: 20rpx;
justify-content: center;
}
}
}
.chapter-content {
position: relative;
padding: 20rpx;
border: 4rpx solid #fffffc;
background: linear-gradient(52deg, #e8f6ff 0%, #e3f2fe 50%);
box-shadow: 0px 0px 10px 0px #89c8e9;
border-top-right-radius: 40rpx;
border-bottom-left-radius: 40rpx;
.vip-badge {
display: inline-block;
font-size: 24rpx;
background: linear-gradient(90deg, #6429db 0%, #0075ed 100%);
color: #fff;
padding: 10rpx 20rpx;
border-radius: 0 50rpx 50rpx 0;
z-index: 1;
}
.chapter-item {
padding: 20rpx 0;
border-bottom: 1px solid #fff;
&:last-child {
border-bottom: none;
}
.chapter-content-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
.chapter-info {
flex: 1;
display: flex;
align-items: center;
gap: 10rpx;
&.locked {
opacity: 0.6;
}
.chapter-title {
flex: 1;
font-size: 28rpx;
color: #1e2f3e;
line-height: 1.5;
}
.chapter-tag {
flex-shrink: 0;
}
}
.lock-icon {
margin-left: 20rpx;
flex-shrink: 0;
}
}
}
}
.no-chapters {
text-align: center;
padding: 80rpx 0;
color: #999;
font-size: 28rpx;
}
</style>

View File

@@ -16,40 +16,23 @@
<CourseInfo v-if="courseDetail" :course="courseDetail" :class="{'pt-10': !!vipTip}" />
<!-- 课程内容包装器 -->
<view class="course-content-wrapper">
<!-- 目录列表 -->
<CatalogueList
v-if="catalogueList.length > 0"
:catalogues="catalogueList"
:currentIndex="currentCatalogueIndex"
:userVip="userVip"
@change="handleCatalogueChange"
/>
<!-- 章节列表 -->
<ChapterList
v-if="chapterList.length > 0"
:chapters="chapterList"
:catalogue="currentCatalogue"
:userVip="userVip"
:showRenewBtn="showRenewBtn"
@purchase="handlePurchase"
@toVip="goToVip"
@renew="handleRenew"
@click="handleChapterClick"
/>
</view>
<CatalogueList
v-if="catalogueList.length > 0"
:catalogues="catalogueList"
:userVip="userVip"
@getFreeCourse="handleGetFreeCourse"
@purchase="handlePurchase"
@toVip="goToVip"
@renew="handleRenew"
@toDetail="handleToDetail"
/>
<!-- 学习进度 -->
<view class="learning-progress">
<view class="progress-title">
<text>{{ $t('courseDetails.progress') }}</text>
</view>
<wd-progress
:percentage="learningProgress"
show-text
stroke-width="6"
/>
<wd-progress :percentage="learningProgress" show-text stroke-width="6" />
</view>
<!-- 相关书籍 -->
@@ -134,7 +117,7 @@
<view>
1.手机pad电脑均为可登陆电子设备均有唯一标识码一个用户名仅允许在一个手机或一个ipad或一个电脑登陆请根据您的使用习惯自行选择<br />
2.如若申请变更登陆设备请联系客服<br />
客服电话:13110039505;022-24142321<br />
客服电话:021-08371305<br />
客服微信号:yilujiankangkefu<br />
3.如因违反上述使用规定...概不退款本公司保留追究用户相关法律责任的权利<br />
4.点击同意按钮即表示您同意遵守以上条款
@@ -166,30 +149,24 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { onLoad, onPageScroll, onPullDownRefresh, onReachBottom, onShow } from '@dcloudio/uni-app'
import { useCourseStore } from '@/stores/course'
import { useUserStore } from '@/stores/user'
import { courseApi } from '@/api/modules/course'
import CourseInfo from '@/components/course/CourseInfo.vue'
import CatalogueList from '@/components/course/CatalogueList.vue'
import ChapterList from '@/components/course/ChapterList.vue'
import CourseInfo from './components/CourseInfo.vue'
import CatalogueList from './components/CatalogueList.vue'
import GoodsSelector from '@/components/order/GoodsSelector.vue'
import CommentList from '@/components/comment/CommentList.vue'
import CommentEditor from '@/components/comment/CommentEditor.vue'
import NavBar from '@/components/nav-bar/nav-bar.vue'
import type { ICourseDetail, ICatalogue, IChapter, IVipInfo } from '@/types/course'
import type { IGoods } from '@/types/order'
import type { IComment } from '@/types/comment'
// Stores
const courseStore = useCourseStore()
const userStore = useUserStore()
// 页面数据
const courseId = ref<number>(0)
const courseDetail = ref<ICourseDetail | null>(null)
const catalogueList = ref<ICatalogue[]>([])
const currentCatalogueIndex = ref(0)
const chapterList = ref<IChapter[]>([])
const userVip = ref<IVipInfo | null>(null)
const vipModuleList = ref<string[]>([])
const learningProgress = ref(0)
@@ -202,7 +179,6 @@ const selectedGoods = ref<IGoods | null>(null)
const showProtocol = ref(false)
const isFudu = ref(false)
const fuduCatalogueId = ref<number>(0)
const showRenewBtn = ref(false)
// 评论相关
const commentList = ref<IComment[]>([])
@@ -215,14 +191,6 @@ const replyComment = ref<IComment | undefined>(undefined)
// UI状态
const scrollTop = ref(0)
/**
* 当前目录
*/
const currentCatalogue = computed(() => {
if (catalogueList.value.length === 0) return null
return catalogueList.value[currentCatalogueIndex.value] || null
})
/**
* VIP提示文案
*/
@@ -286,11 +254,6 @@ const loadPageData = async () => {
const totalProgress = catalogueList.value.reduce((sum, cat) => sum + cat.completion, 0)
learningProgress.value = Number((totalProgress / catalogueList.value.length).toFixed(2))
}
// 默认选择第一个目录
if (catalogueList.value.length > 0) {
await switchCatalogue(0)
}
}
// 检查VIP权益
@@ -318,69 +281,50 @@ const checkVipStatus = async () => {
}
}
/**
* 切换目录
*/
const switchCatalogue = async (index: number) => {
currentCatalogueIndex.value = index
const catalogue = catalogueList.value[index]
// 获取章节列表
const res = await courseApi.getCatalogueChapterList(catalogue.id)
if (res.code === 0) {
chapterList.value = res.chapterList || []
}
// 检查是否支持复读
if (catalogue.isBuy === 0 && !userVip.value) {
const renewRes = await courseApi.checkRenewPayment(catalogue.id)
showRenewBtn.value = renewRes.canRelearn || false
} else {
showRenewBtn.value = false
}
}
/**
* 目录切换事件
*/
const handleCatalogueChange = (index: number) => {
switchCatalogue(index)
}
/**
* 点击章节
*/
const handleChapterClick = (chapter: IChapter) => {
const noRecored = chapter.isAudition === 1 && currentCatalogue.value?.isBuy === 0 && !userVip.value
const handleToDetail = (chapter: IChapter, catalogue: ICatalogue) => {
const noRecored = chapter.isAudition === 1 && catalogue.isBuy === 0 && !userVip.value
uni.navigateTo({
url: `/pages/course/details/chapter?id=${chapter.id}&courseId=${courseId.value}&courseTitle=${courseDetail.value?.title}&title=${chapter.title}&noRecored=${noRecored}`
})
}
/**
* 去开通vip
*/
const goToVip = () => {
uni.navigateTo({
url: '/pages/vip/course'
})
}
/**
* 领取免费课程
*/
const handleGetFreeCourse = async () => {
if (!currentCatalogue.value) return
const handleGetFreeCourse = async (catalogue: ICatalogue) => {
if (!catalogue) return
uni.showLoading({ title: '领取中...' })
const res = await courseApi.startStudyForMF(currentCatalogue.value.id)
const res = await courseApi.startStudyForMF(catalogue.id)
if (res.code === 0) {
uni.showToast({ title: '领取成功', icon: 'success' })
// 刷新页面数据
await loadPageData()
loadPageData()
} else {
uni.showToast({ title: res.msg || '领取失败', icon: 'none' })
}
}
/**
* 购买课程
*/
const handlePurchase = async () => {
if (!currentCatalogue.value) return
const handlePurchase = async (catalogue: ICatalogue) => {
if (!catalogue) return
isFudu.value = false
const res = await courseApi.getProductListForCourse(currentCatalogue.value.id)
const res = await courseApi.getProductListForCourse(catalogue.id)
if (res.code === 0 && res.productList.length > 0) {
goodsList.value = res.productList
showGoodsSelector.value = true
@@ -393,17 +337,17 @@ const handlePurchase = async () => {
* 续费/复读
*/
const handleRenew = async () => {
if (!currentCatalogue.value) return
// if (!currentCatalogue.value) return
isFudu.value = true
fuduCatalogueId.value = currentCatalogue.value.id
const res = await courseApi.getRenewProductList(currentCatalogue.value.id)
if (res.code === 0 && res.productList.length > 0) {
goodsList.value = res.productList
showGoodsSelector.value = true
} else {
uni.showToast({ title: '暂无复读方案', icon: 'none' })
}
// isFudu.value = true
// fuduCatalogueId.value = currentCatalogue.value.id
// const res = await courseApi.getRenewProductList(currentCatalogue.value.id)
// if (res.code === 0 && res.productList.length > 0) {
// goodsList.value = res.productList
// showGoodsSelector.value = true
// } else {
// uni.showToast({ title: '暂无复读方案', icon: 'none' })
// }
}
/**
@@ -443,15 +387,6 @@ const confirmPurchase = () => {
})
}
/**
* 跳转到VIP页面
*/
const goToVip = () => {
uni.navigateTo({
url: '/pages/vip/course'
})
}
/**
* 跳转到书籍详情
*/
@@ -698,10 +633,6 @@ onReachBottom(() => {
}
}
.course-content-wrapper {
background: linear-gradient(108deg, #c3e7ff 0%, #59bafe 100%);
}
.learning-progress {
padding: 20rpx;
background-color: #fff;

View File

@@ -326,10 +326,6 @@ const getSociologyCateList = async () => {
* 终极分类点击处理
*/
const curseClickJump = (item: IMedicalTag) => {
// uni.showToast({
// title: '课程分类列表正在开发中,现在还不能跳转',
// icon: 'none'
// })
uni.navigateTo({
url: `/pages/course/list/category?id=${item.id}&title=${item.title}&pid=${item.pid}&subject=${selectedFirstLevel.value}`
})
@@ -385,15 +381,9 @@ const getNewsList = async () => {
* 新闻点击处理
*/
const newsClick = (item: INews) => {
if (item.type === 1 && item.url) {
uni.navigateTo({
url: `/pages/news/newsForwebview?newsId=${item.id}&url=${item.url}&type=${item.type}`
})
} else {
uni.navigateTo({
url: `/pages/news/news?newsId=${item.id}&url=${item.url}&type=${item.type}`
})
}
uni.navigateTo({
url: `/pages/news/details?newsId=${item.id}&url=${item.url}&type=${item.type}`
})
}
// 精彩试听
@@ -846,7 +836,7 @@ $border-color: #eeeeee;
// 观看记录和试听样式
.learnBox {
background-color: #fff;
margin: 10px 5px 20px;
margin: 10px 5px;
border-radius: 20rpx;
padding: 10px;
box-shadow: 0px 0px 10px 0px rgba(167, 187, 228, 0.3);

View File

@@ -23,7 +23,7 @@
:placeholder="$t('login.codePlaceholder')"
/>
<wd-button type="info" :class="['code-btn', { active: !readonly }]" @click="getCode">
{{ t('login.getCode') }}
{{ codeText }}
</wd-button>
</view>
@@ -70,14 +70,12 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { t } from '@/utils/i18n'
import { commonApi } from '@/api/modules/common'
import { resetPassword } from '@/api/modules/auth'
import { validateEmail, checkPasswordStrength } from '@/utils/validator'
import { getNotchHeight } from '@/utils/system'
const { t } = useI18n()
// 表单数据
const email = ref('')
const code = ref('')
@@ -85,7 +83,7 @@ const password = ref('')
const confirmPassword = ref('')
// 验证码相关
const codeText = ref('Get Code')
const codeText = ref(t('login.getCode'))
const readonly = ref(false)
// 密码强度相关

View File

@@ -2,7 +2,7 @@
<view class="login-page">
<!-- Logo 背景区域 -->
<view class="logo-bg">
<text class="welcome-text">Hello! Welcome to<br>太湖国际</text>
<text class="welcome-text">Hello! Welcome to<br>WU'S INTERNATIONAL</text>
<image src="@/static/icon/login_icon.png" mode="aspectFit" class="icon-hua-1"></image>
<image src="@/static/icon/login_icon.png" mode="aspectFit" class="icon-hua-2"></image>
</view>
@@ -37,8 +37,8 @@
maxlength="6"
@confirm="onSubmit"
/>
<wd-button type="info" :class="['code-btn', { active: !readonly }]" @click="onSetCode">
{{ t('login.getCode') }}
<wd-button type="info" :class="['code-btn', { 'active': !readonly }]" @click="onSetCode">
{{ codeText }}
</wd-button>
</view>
</view>
@@ -83,7 +83,7 @@
<!-- 协议同意 -->
<view class="protocol-box">
<view class="select" :class="{ active: agree }" @click="agreeAgreements"></view>
<view class="select" :class="{ 'active': agree }" @click="agreeAgreements"></view>
<view class="protocol-text">
{{ $t('login.agree') }}
<text class="highlight" @click="yhxy">《{{ $t('login.userAgreement') }}》</text>
@@ -94,7 +94,7 @@
<!-- 登录按钮 -->
<view class="btn-box">
<button @click="onSubmit" class="login-btn" :class="{ active: btnShow }">
<button @click="onSubmit" class="login-btn" :class="{ 'active': btnShow }">
{{ $t('login.goLogin') }}
</button>
</view>
@@ -148,8 +148,7 @@ import { commonApi } from '@/api/modules/common'
import { validateEmail } from '@/utils/validator'
import { onPageJump } from '@/utils'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
import { t } from '@/utils/i18n'
const userStore = useUserStore()
@@ -165,7 +164,7 @@ const agree = ref(false)
const isSee = ref(false)
// 验证码相关
const codeText = ref('Get Code')
const codeText = ref(t('login.getCode'))
const readonly = ref(false)
const btnShow = ref(true)

124
pages/news/details.vue Normal file
View File

@@ -0,0 +1,124 @@
<template>
<view>
<!-- 自定义导航栏 -->
<nav-bar :title="$t('news.newsDetail')" />
<view v-if="urlVisible" class="web-view-container"><web-view :webview-styles="{ progress: { color: '#55aaff' } }" :src="surl"></web-view></view>
<view v-else class="box">
<view class="title">{{ news.title }}</view>
<view
class="content"
v-html="formattedContent"
></view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { newsApi } from '@/api/modules/news'
const newsId = ref<string | number | null>(null)
const type = ref<number | null>(null)
const urlVisible = ref(false)
const surl = ref('')
const source = ref('')
onLoad((e: any) => {
newsId.value = e.newsId
type.value = Number(e.type)
surl.value = e.url || ''
source.value = e.source || ''
if(type.value == 1 && surl.value != ''){
urlVisible.value = true
// APP 设置导航栏按钮
// #ifdef APP-PLUS
const pages = getCurrentPages()
const page = pages[pages.length - 1]
const currentWebview = page.$getAppWebview()
currentWebview.setStyle({
titleNView: {
buttons: [{
float: 'right',
type: 'close',
onclick: () => {
uni.navigateBack({ delta: 1 })
}
}]
}
})
// #endif
} else {
getData()
}
})
// 新闻详情
const news = ref({
title: '',
content: ''
})
// 获取新闻详情
const getData = async () => {
let res = null
if (source.value === 'taihuzhiguang') {
res = await newsApi.getTaihuWelfareArticleDetail(newsId.value)
} else {
res = await newsApi.getNewsDetail(newsId.value)
}
news.value.title = res.result?.title || ''
news.value.content = res.result?.content || ''
}
// 格式化富文本内容
const formatRichText = (html: string) => {
if (!html) return ""
let newContent = html.replace(/<img[^>]*>/gi, (match) => {
match = match.replace(/style="[^"]+"/gi, '')
match = match.replace(/width="[^"]+"/gi, '')
match = match.replace(/height="[^"]+"/gi, '')
return match
})
newContent = newContent.replace(/style="[^"]+"/gi, (match) => {
match = match.replace(/width:[^;]+;/gi, 'max-width:100%;')
return match
})
newContent = newContent.replace(/<br[^>]*\/>/gi, '')
newContent = newContent.replace(
/\<img/gi,
'<img style="max-width:100%;height:auto;display:inline-block;margin:10rpx auto;"'
)
return newContent
}
// 格式化后内容
const formattedContent = computed(() => formatRichText(news.value.content))
</script>
<style lang="scss" scoped>
.web-view-container {
padding-top: 60px;
}
.box {
background-color: #fff;
padding: 10px;
min-height: calc(100vh - 270rpx);
}
.title {
font-size: 32rpx;
font-weight: bold;
text-align: center;
}
.content {
font-size: 26rpx;
line-height: 48rpx;
margin-top: 10rpx;
}
</style>

View File

@@ -59,14 +59,16 @@ const orderType = ref<string>('')
onLoad(async () => {
try {
// 获取商品列表
uni.$on('selectedGoods', (data: IOrderGoods) => {
await uni.$on('selectedGoods', async (data: IOrderGoods) => {
// 获取用户信息
await getUserInfo()
// 处理商品数据
console.log('监听到传入的商品数据:', data)
isLengthen.value = data.state !== null
orderType.value = data.orderType || ''
goodsList.value = [ data ]
})
// 获取用户信息
getUserInfo()
} catch (error) {
console.error('解析商品数据失败:', error)
uni.showToast({

View File

@@ -36,7 +36,7 @@
</view>
</view> -->
<wd-cell-group border class="contact-info">
<wd-cell :title="$t('user.hotline')" clickable icon="call" value="022-24142321" @click="handlePhoneCall"></wd-cell>
<wd-cell :title="$t('user.hotline')" clickable icon="call" value="021-08371305" @click="handlePhoneCall"></wd-cell>
<!-- <wd-cell :title="$t('user.wechat')" value="yilujiankangkefu" clickable @click="handlePhoneCall" /> -->
</wd-cell-group>
@@ -83,7 +83,7 @@ const getAppVersion = () => {
* 拨打电话
*/
const handlePhoneCall = () => {
makePhoneCall('022-24142321', t('user.hotline'))
makePhoneCall('021-08371305', t('user.hotline'))
}
/**

View File

@@ -89,6 +89,8 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useUserStore } from '@/stores/user'
import { submitFeedback } from '@/api/modules/user'
import type { IFeedbackForm } from '@/types/user'
@@ -97,6 +99,14 @@ import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const userStore = useUserStore()
onLoad((options: any) => {
const orderSn = options.orderSn
if (orderSn) {
form.value.relation = orderSn
form.value.type = '3'
}
})
// 问题类型选项
const issueTypeOptions = computed(() => [
{ label: t('user.issueTypeAccount'), value: '1' },

View File

@@ -13,9 +13,6 @@
<view class="user-details">
<text class="nickname">{{ userInfo.nickname || $t('user.notSet') }}</text>
<text v-if="userInfo.email" class="email">{{ userInfo.email }}</text>
<text v-if="vipInfo.endTime && platform === 'ios'" class="vip-time">
VIP {{ vipInfo.endTime.split(' ')[0] }} {{ $t('user.vipExpireTime') }}
</text>
</view>
</view>
</view>
@@ -26,22 +23,18 @@
<view class="vip-card-title">{{ $t('user.vip') }}</view>
<view class="vip-card-content">
<view class="vip-item-list">
<view v-if="vipInfo.length > 0" v-for="vip in vipInfo">{{ vipTypeDict[vip.type] }}有效期到
{{ parseTime(vip.endTime, '{y}-{m}-{d}') }}</view>
<view v-if="vipInfo?.length > 0" v-for="vip in vipInfo">{{ vipTypeDict[vip.type] }}{{ parseTime(vip.endTime, '{y}-{m}-{d}') }} 截止</view>
<view v-else>办理课程VIP畅享更多权益</view>
</view>
<wd-button v-if="vipInfo.length > 0" plain type="primary" size="small"
@click="goSubscribe">{{ $t('vip.renewal') }}</wd-button>
<wd-button v-else plain type="primary" size="small" @click="goSubscribe">{{ $t('vip.openVip') }}</wd-button>
<wd-button v-if="vipInfo?.length > 0" plain type="primary" size="small" @click="goCourseVipSub">{{ $t('vip.renewal') }}</wd-button>
<wd-button v-else plain type="primary" size="small" @click="goCourseVipSub">{{ $t('vip.openVip') }}</wd-button>
</view>
<view class="vip-card-content">
<view class="vip-item-list">
<view v-if="vipInfoEbook.length > 0" v-for="vip in vipInfoEbook">电子书VIP{{ vipTypeDict[vip.type] }}有效期到
{{ parseTime(vip.endTime, '{y}-{m}-{d}') }}</view>
<view v-if="vipInfoEbook?.length > 0" v-for="vip in vipInfoEbook">电子书VIP{{ vipTypeDict[vip.type] }}{{ parseTime(vip.endTime, '{y}-{m}-{d}') }} 截止</view>
<view v-else>办理电子书VIP畅享更多权益</view>
</view>
<wd-button v-if="!vipInfoEbook.length" plain type="primary" size="small"
@click="goSubscribe">{{ $t('vip.openVip') }}</wd-button>
<wd-button v-if="!vipInfoEbook?.length" plain type="primary" size="small" @click="goSubscribe">{{ $t('vip.openVip') }}</wd-button>
</view>
</view>
</view>
@@ -71,12 +64,9 @@
<!-- 功能菜单列表 -->
<view class="menu-section">
<view class="menu-list">
<view v-for="item in menuItems" :key="item.id" class="menu-item" @click="handleMenuClick(item)">
<text class="menu-text">{{ item.name }}</text>
<wd-icon name="arrow-right" size="16px" color="#aaa" />
</view>
</view>
<wd-cell-group border class="menu-list">
<wd-cell v-for="item in menuItems" :key="item.id" :title="item.name" :label="item.desc" is-link @click="handleMenuClick(item)" />
</wd-cell-group>
</view>
</view>
</template>
@@ -96,7 +86,7 @@
const sysStore = useSysStore()
// 默认头像
const defaultAvatar = '/static/home_icon.png'
const defaultAvatar = '/static/logo.png'
// 用户信息
const userInfo = computed(() => userStore.userInfo)
@@ -142,7 +132,14 @@
name: t('user.feedback'),
url: '/pages/user/feedback/index',
type: 'pageJump'
}
},
// {
// id: 6,
// name: t('user.dataMigrate'),
// url: '/pages/user/migrate/index',
// desc: t('user.migrateSubtitle'),
// type: 'pageJump'
// }
])
/**
@@ -183,7 +180,7 @@
}
/**
* 跳转到订阅页面
* 跳转到电子书vip订阅页面
*/
const goSubscribe = () => {
uni.navigateTo({
@@ -191,6 +188,15 @@
})
}
/**
* 跳转到课程vip订阅页面
*/
const goCourseVipSub = () => {
uni.navigateTo({
url: '/pages/vip/course'
})
}
/**
* 处理菜单点击
*/
@@ -380,7 +386,7 @@
}
.menu-section {
padding: 20rpx 20rpx 0;
padding: 20rpx 20rpx;
}
.menu-list {
@@ -390,37 +396,13 @@
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1px solid #e0e0e0;
&:last-child {
border-bottom: none;
}
&:active {
background-color: #f5f5f5;
}
.menu-text {
font-size: 30rpx;
color: #333;
line-height: 40rpx;
}
}
.chong_btn {
font-size: 26rpx;
display: block;
border-radius: 50rpx;
color: #fffbf6;
padding: 10rpx 32rpx;
background-image: linear-gradient(90deg, #3ab3ae 0%, #d5ecdd 200%);
background: #007bff;
}
.assets {

View File

@@ -0,0 +1,195 @@
<template>
<view class="page-wrap">
<!-- 自定义导航栏 -->
<nav-bar :title="$t('user.dataMigrate')"></nav-bar>
<view class="text-red-500 text-center mb-[20rpx]! font-bold">{{ $t('user.migrateWarning') }}</view>
<!-- 主要内容区域 -->
<wd-form ref="migrateForm" :model="formData" :rules="rules" :label-width="120" class="migrate-card p-[10rpx]">
<wd-cell-group border>
<wd-input
v-model="formData.tel"
prop="tel"
clearable
:label="$t('user.oldAccount')"
:placeholder="$t('user.oldAccountPlaceholder')"
/>
<wd-input
v-model="formData.code"
prop="code"
clearable
:label="$t('user.migrateCode')"
:placeholder="$t('user.migrateCodePlaceholder')"
/>
<view class="pb-[20rpx] pt-[10rpx]">
<wd-button type="primary" size="medium" @click="handleSubmit" block>{{ $t('user.confirmMigrate') }}</wd-button>
</view>
</wd-cell-group>
</wd-form>
<!-- 迁移说明 -->
<view class="migrate-card p-[30rpx]">
<view class="instructions-title">{{ $t('user.migrateInstructions') }}</view>
<view class="instructions-content">
<view class="instruction-item">
<text class="instruction-number">1.</text>
<text class="instruction-text">{{ $t('user.instruction1') }}</text>
</view>
<view class="instruction-item">
<text class="instruction-number">2.</text>
<text class="instruction-text">{{ $t('user.instruction2') }}</text>
</view>
<view class="instruction-item">
<text class="instruction-number">3.</text>
<text class="instruction-text">{{ $t('user.instruction3') }}</text>
</view>
<view class="instruction-item">
<text class="instruction-number">4.</text>
<text class="instruction-text">{{ $t('user.instruction4') }}</text>
</view>
</view>
</view>
<wd-message-box />
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { t } from '@/utils/i18n'
import { migrateUserData } from '@/api/modules/user'
import { useMessage } from '@/uni_modules/wot-design-uni'
const message = useMessage()
// 表单引用
const migrateForm = ref()
// 表单数据
const formData = ref({
tel: '',
code: ''
})
// 表单验证规则
const rules = ref({
tel: [
{ required: true, message: t('common.pleaseInput') + t('user.oldAccountPlaceholder'), trigger: 'blur' }
],
code: [
{ required: true, message: t('common.pleaseInput') + t('user.migrateCodePlaceholder'), trigger: 'blur' }
]
})
// 提交表单
const handleSubmit = async () => {
migrateForm.value.validate().then(({ valid, errors }: any) => {
if (valid) {
message.confirm({
title: t('global.tips'),
msg: t('user.instruction2'),
}).then(() => {
message.confirm({
title: t('global.tips'),
msg: t('user.migrateWarning'),
}).then(() => {
submitMigrate()
}).catch(() => {
// 取消数据迁移
})
}).catch(() => {
// 取消数据迁移
})
}
})
.catch((error: any) => {
console.log(error)
})
}
// 处理迁移
const submitMigrate = async () => {
await migrateUserData(formData.value)
uni.showToast({
title: t('user.migrateSuccess'),
icon: 'success'
})
// 清空表单
formData.value.tel = ''
formData.value.code = ''
}
</script>
<style lang="scss" scoped>
.page-wrap {
padding: 20rpx;
background-color: #F8F9FA;
min-height: 100vh;
}
.migrate-card {
background: #ffffff;
border-radius: 16rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
}
.migrate-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #007aff 0%, #0051d5 100%);
border-radius: 12rpx;
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
margin-top: 40rpx;
&:not([disabled]):active {
opacity: 0.8;
}
&[disabled] {
background: #cccccc;
color: #ffffff;
}
}
.instructions-title {
font-size: 32rpx;
font-weight: 600;
color: #333333;
margin-bottom: 30rpx;
}
.instructions-content {
.instruction-item {
display: flex;
align-items: flex-start;
margin-bottom: 20rpx;
line-height: 1.6;
&:last-child {
margin-bottom: 0;
}
}
.instruction-number {
font-size: 28rpx;
color: #007aff;
font-weight: 600;
margin-right: 16rpx;
min-width: 40rpx;
}
.instruction-text {
font-size: 28rpx;
color: #666666;
flex: 1;
}
}
</style>

View File

@@ -68,7 +68,7 @@ function goToDetail(bookId: number) {
function goToReader(bookId: number) {
uni.navigateTo({
url: `/pages/book/reader?isBuy=0&bookId=${bookId}`
url: `/pages/book/reader?isBuy=1&bookId=${bookId}`
})
}

View File

@@ -0,0 +1,153 @@
<template>
<view class="page-wrapper">
<!-- 自定义导航栏 -->
<nav-bar :title="$t('order.orderDetails')"></nav-bar>
<!-- 订单信息 -->
<view class="order-info">
<view class="order-status">{{ sysStore.orderStatusMap[order.orderStatus] }}</view>
<!-- 三种订单类型商品信息 -->
<ProductInfo v-if="order.orderType === 'order'" size="large" :data="productList[0]" :type="order.orderType" />
<ProductInfo v-if="order.orderType === 'vip'" size="large" :data="order.vipBuyConfigEntity" :type="order.orderType" />
<ProductInfo v-if="order.orderType === 'abroadVip'" size="large" :data="order.ebookvipBuyConfig" :type="order.orderType" />
<!-- 三种订单类型商品信息 end -->
<wd-divider class="p-0!" />
<!-- 付款信息 -->
<wd-cell-group>
<wd-cell title="商品总价" :value="`${order.orderMoney} ${$t('global.coin')}`" />
<wd-cell v-if="order.districtMoney > 0" title="活动优惠" :value="`- ${order.districtMoney} ${$t('global.coin')}`" />
<wd-cell v-if="order.vipDiscountAmount > 0" title="VIP专享立减" :value="`- ${order.vipDiscountAmount} ${$t('global.coin')}`" />
<wd-cell v-if="order.jfDeduction > 0" title="积分抵扣">
<text class="text-red-500 text-lg font-bold">{{ `- ${order.jfDeduction}` }}</text>
</wd-cell>
<wd-cell title="实付金额">
<text class="text-red-500 text-lg font-bold">{{ `${order.realMoney}` }}</text> {{ order.orderType === 'point' ? 'NZ$' : $t('global.coin') }}
</wd-cell>
</wd-cell-group>
<wd-divider class="p-0!" />
<!-- 下单信息 -->
<wd-cell-group>
<wd-cell title="订单编号" title-width="4em">
<text class="text-xs">{{ order.orderSn }}</text>
<wd-icon name="file-copy" size="14px" color="#65A1FA" class="ml-1!" @click="copyToClipboard(order.orderSn)"></wd-icon>
</wd-cell>
<wd-cell title="创建时间" :value="order.createTime" />
<wd-cell title="付款时间" :value="order.paymentDate" v-if="order.paymentDate"/>
</wd-cell-group>
</view>
<view class="text-center">
<text @click="toWorkOrder" class="text-[cadetblue] text-sm">订单有问题去申诉</text>
</view>
<view class="contact-customer" @click="makePhoneCall(sysStore.customerServicePhone)">
<wd-icon name="service" size="30px"></wd-icon>
<view class="text-sm">联系客服</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useSysStore } from '@/stores/sys'
import { orderApi } from '@/api/modules/order'
import type { IOrderDetail, IOrderGoods } from '@/types/order'
import ProductInfo from '@/components/order/ProductInfo.vue'
import { copyToClipboard, makePhoneCall } from '@/utils/index'
const sysStore = useSysStore()
// 订单详情
const order = ref<IOrderDetail>({
orderMoney: 0,
districtMoney: 0,
vipDiscountAmount: 0,
jfDeduction: 0,
realMoney: 0,
})
const productList = ref<IOrderGoods[]>([])
onLoad(async (options: { orderId: string }) => {
const orderId = options.orderId
if (orderId) {
const res = await orderApi.getOrderDetail(orderId)
const orderDetails = res.data.buyOrder
order.value = orderDetails
switch (orderDetails.orderType) {
case 'order':
productList.value = res.data.productInfo
break
case 'vip':
productList.value = orderDetails.vipBuyConfigEntity
break
case 'abroadVip':
productList.value = orderDetails
break
default:
break
}
}
})
const toWorkOrder = () => {
uni.navigateTo({
url: '/pages/user/feedback/index?orderSn=' + order.value.orderSn
})
}
</script>
<style lang="scss" scoped>
body {
background-color: #F8F9FA;
}
.page-wrapper {
padding: 20rpx;
}
.order-info {
padding: 30rpx;
background-color: #fff;
border-radius: 10px;
margin-bottom: 15px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
.order-status {
margin-bottom: 20rpx;
padding: 10rpx 0;
width: 6em;
background-color: #34D19D;
color: #fff;
border-radius: 0 10px 10px 0;
text-align: center;
font-size: 16px;
font-weight: bold;
}
}
:deep(.wd-cell-group) {
padding: 0 !important;
.wd-cell__wrapper {
padding: 0 !important;
}
}
.contact-customer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #fff;
border-radius: 50%;
width: 80px;
height: 80px;
margin: 40rpx auto 0;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
line-height: 1;
}
</style>

View File

@@ -3,14 +3,14 @@
<template #top>
<!-- 自定义导航栏 -->
<nav-bar :title="$t('user.myOrders')"></nav-bar>
<wd-tabs v-model="orderStatus" @change="handleOrderStatusTabChange">
<!-- <wd-tabs v-model="orderStatus" @change="handleOrderStatusTabChange">
<wd-tab v-for="item in ordersTabs" :key="item.value" :title="item.name" :name="item.value"></wd-tab>
</wd-tabs>
</wd-tabs> -->
</template>
<!-- 订单列表 -->
<view class="order-list">
<wd-card v-for="order in orderList" :key="order.id" type="rectangle" custom-class="order-item">
<wd-card v-for="order in orderList" :key="order.id" type="rectangle" custom-class="order-item" @click="toDetails(order)">
<template #title>
<view class="order-item-title">
<view class="order-item-sn">
@@ -23,12 +23,11 @@
</template>
<!-- 三种订单类型商品信息 -->
<ProductInfo v-if="order.orderType === 'order'" :data="order.productList" :type="order.orderType" />
<ProductInfo v-if="order.orderType === 'abroadBook'" :data="order.bookEntity" :type="order.orderType" />
<ProductInfo v-if="order.orderType === 'order'" :data="order.productList[0].product" :type="order.orderType" />
<ProductInfo v-if="order.orderType === 'vip'" :data="order.vipBuyConfigEntity" :type="order.orderType" />
<ProductInfo v-if="order.orderType === 'abroadVip'" :data="order.ebookvipBuyConfig" :type="order.orderType" />
<!-- 三种订单类型商品信息 end -->
<view class="order-item-total-price">实付款{{ order.orderMoney }} {{ t('global.coin') }}</view>
<view class="order-item-total-price">实付款{{ order.realMoney }} {{ t('global.coin') }}</view>
<template #footer>
<view>
@@ -48,12 +47,16 @@ import type { IOrder } from '@/types/order'
import { useI18n } from 'vue-i18n'
import { copyToClipboard } from '@/utils/index'
import ProductInfo from '@/components/order/ProductInfo.vue'
import { useSysStore } from '@/stores/sys'
const { t } = useI18n()
const paging = ref<any>(null)
// 订单状态映射
const orderStatusMap = useSysStore().orderStatusMap
// 订单状态
const orderStatus = ref<string>('-1')
const orderStatus = ref<string>('3')
const ordersTabs = [
{
name: "全部",
@@ -72,12 +75,6 @@ const ordersTabs = [
},
]
// 订单状态映射
const orderStatusMap = {
'0': '待付款',
'3': '已完成',
}
/**
* 处理订单状态切换
*/
@@ -122,22 +119,17 @@ const getOrderImage = (order: IOrder) => {
}
}
const getOrderTitle = (order: IOrder) => {
switch (order.orderType) {
case 'order':
return order.productList[0]?.product?.productName || ''
case 'abroadBook':
return order.bookEntity?.name || ''
case 'vip':
return order.vipBuyConfigEntity?.title || ''
case 'point':
return ''
default:
return ''
}
/**
* 跳转订单详情
*/
const toDetails = (order: IOrder) => {
uni.navigateTo({
url: '/pages/user/order/details?orderId=' + order.orderId
})
}
</script>
<style lang="scss" scoped>

View File

@@ -1,5 +1,6 @@
<template>
<z-paging ref="paging" v-model="bookList" auto-show-back-to-top class="my-book-page" @query="pointsList" :default-page-size="10">
<z-paging ref="paging" v-model="bookList" auto-show-back-to-top class="my-book-page" @query="pointsList"
:default-page-size="10">
<template #top>
<!-- 自定义导航栏 -->
<nav-bar :title="$t('user.consumptionRecord')"></nav-bar>
@@ -7,12 +8,16 @@
<view class="recharge-record" v-if="(bookList && bookList.length > 0)">
<view class="go-gecharge" @click="goRecharge">
<view>{{$t('order.recharge')}}</view>
<view><wd-icon name="arrow-right" size="16px" color="#fff"/></view>
<view><wd-icon name="arrow-right" size="16px" color="#fff" /></view>
</view>
<view class="title">{{$t('order.rechargeConsumptionList')}}</view>
<view class="recharge-record-block" v-for="(item, index) in bookList" :key="index">
<view class="recharge-record-block-row">{{item.orderType}}<text class="text">{{item.changeAmount}}</text></view>
<view class="title">{{$t('order.pointsRecord')}}</view>
<view class="recharge-record-block" v-for="(item, index) in bookList" :key="index" @click="toDetails(item)">
<view class="recharge-record-block-row">{{item.remark.slice(0, (item.remark.indexOf(',')))}}<text
:class="item.actType === 1 ? 'text1' : 'text2'">{{item.actType === 1 ? '' : '+'}}{{item.changeAmount}}</text>
</view>
<view class="time">{{item.createTime}}</view>
<view style="font-size: 24rpx;">{{item.remark.slice((item.remark.indexOf(','))+1)}}<wd-icon name="file-copy"
size="14px" color="#65A1FA" style="margin-left: 10rpx;" @click="copyToClipboard()"></wd-icon></view>
</view>
</view>
</z-paging>
@@ -23,6 +28,7 @@
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user'
import { getPointsData } from '@/api/modules/user'
import { copyToClipboard } from '@/utils/index'
const { t } = useI18n()
const paging = ref<any>()
@@ -40,7 +46,7 @@
try {
const res = await getPointsData(pageNo, pageSize, userId)
console.log(res, 'res');
paging.value.complete(res.transactionDetailsList.records)
paging.value.complete(res.transactionDetailsList)
} catch (error) {
paging.value.complete(false)
console.error('Failed to load book list:', error)
@@ -49,7 +55,7 @@
loading.value = false
}
}
/**
* 跳转充值页面
*/
@@ -58,6 +64,16 @@
url: '/pages/user/recharge/index'
})
}
/**
* 跳转订单详情
*/
const toDetails = (order: IOrder) => {
console.log(order.relationId, "order");
uni.navigateTo({
url: '/pages/user/order/details?orderId=' + order.relationId
})
}
</script>
<style lang="scss" scoped>
@@ -72,8 +88,8 @@
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
// padding: 20rpx;
margin: 20rpx;
.go-gecharge{
.go-gecharge {
//height: 100rpx;
background: linear-gradient(to right, #007bff, #17a2b8);
font-size: 30rpx;
@@ -81,12 +97,14 @@
color: #fff;
padding: 20rpx;
margin-bottom: 20rpx;
display: flex;justify-content:space-between;align-items:center
display: flex;
justify-content: space-between;
align-items: center
}
.title {
font-size: 30rpx;
padding-left:20rpx;
padding-left: 20rpx;
margin-bottom: 30rpx;
color: #007bff;
font-weight: bold;
@@ -95,41 +113,46 @@
.recharge-record-block {
border-bottom: 1px solid #e0e0e0;
padding: 20rpx;
.time{
font-size: 20rpx;
.time {
font-size: 24rpx;
margin-bottom: 20rpx;
}
.recharge-record-block-row {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
.text{
color: #007bff;
font-weight: 700;
.text1 {
color: #ff0000;
}
.text2 {
color: #228B22;
}
}
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 200rpx;
image {
width: 400rpx;
height: 300rpx;
margin-bottom: 40rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 50rpx;
}
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 200rpx;
image {
width: 400rpx;
height: 300rpx;
margin-bottom: 40rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 50rpx;
}
}
</style>

View File

@@ -7,7 +7,7 @@
<!-- 头像区域 -->
<view class="avatar-section">
<image
:src="userInfo.avatar || defaultAvatar"
:src="userInfo.avatar"
class="avatar"
@click="editAvatar"
/>
@@ -148,7 +148,7 @@ const { t } = useI18n()
const userStore = useUserStore()
// 默认头像
const defaultAvatar = '/static/home_icon.png'
const defaultAvatar = '/static/logo.png'
// 字段列表
const fields = computed(() => [
@@ -200,10 +200,9 @@ const avatarUrl = ref('')
const userInfo = ref<any>({}) // 用户信息
const getData = async () => {
const res = await getUserInfo()
if (res.result) {
userStore.setUserInfo(res.result)
userInfo.value = res.result
}
userStore.setUserInfo(res.result)
userInfo.value = res.result
userInfo.value.avatar = res.result.avatar || defaultAvatar
}
/**

View File

@@ -2,29 +2,45 @@
<view class="recharge-page">
<!-- 自定义导航栏 -->
<nav-bar :title="$t('order.recharge')"></nav-bar>
<!-- 活动充值金额 -->
<view class="block" v-if="eventAmountList.length > 0">
<!-- <view class="text">{{$t('order.rechargeAmount')}}</view> -->
<view class="text">活动充值金额</view>
<view class="recharge">
<view class="recharge_block" @click="chosPric(item)"
:class="aloneItem.priceTypeId === item.priceTypeId ? 'selected' : ''"
v-for="item in eventAmountList" :key="item.priceTypeId">
<view class="recharge_money">NZ${{item.realMoney}}</view>
<view style="font-size: 26rpx;">{{item.money}}{{ $t('global.coin') }}</view>
<span class="activity-label" v-if="item.givejf >0">{{item.description}}</span>
<text class="recharge_give"
v-if="item.givejf >0">{{$t('order.give')}}{{item.givejf}}{{$t('order.points')}}</text>
</view>
</view>
</view>
<!-- 标准充值金额 -->
<view class="block">
<view class="text">{{$t('order.rechargeAmount')}}</view>
<view class="recharge">
<view class="recharge_block" @click="chosPric(item)"
:class="aloneItem.priceTypeId === item.priceTypeId ? 'selected' : ''"
v-for="item in rechargeList.bookBuyConfigList" :key="item.priceTypeId">
<view class="recharge_money">{{item.realMoney}}</view>
<view>{{item.money}}{{ t('global.coin') }}</view>
<!-- 红框位置的618活动标签 -->
<!-- <view class="activity-tag">618活动</view> -->
<span class="activity-label" v-if="item.givejf >0">618充值活动</span>
<text class="recharge_give" v-if="item.givejf >0">{{item.givejf}}</text>
v-for="item in standardAmountList" :key="item.priceTypeId">
<view class="recharge_money">NZ${{item.realMoney}}</view>
<view style="font-size: 26rpx;">{{item.money}}{{ $t('global.coin') }}</view>
<span class="activity-label" v-if="item.givejf >0">{{item.description}}</span>
<text class="recharge_give"
v-if="item.givejf >0">{{$t('order.give')}}{{item.givejf}}{{$t('order.points')}}</text>
</view>
</view>
</view>
<view class="activity-container">
<view class="active_block" v-if="remark?.remark">
<view v-html="remark.remark"></view>
</view>
<view class="cha_fangsh">
<view class="cf_title PM_font">{{$t('user.paymentMethod')}}</view>
<view class="cf_title">{{$t('user.paymentMethod')}}</view>
<view class="cf_radio">
<radio-group v-for="item in iosPaylist">
<view style="width: 100%">
<view>
<view :class="payType == item.id ? 'Tab_xf cf_xuanx' : 'cf_xuanx'">
<!-- <image class="pay_item_img" :src="item.imgUrl" mode="aspectFil">
</image> -->
@@ -35,7 +51,7 @@
</radio-group>
</view>
</view>
<view class="agree_wo flexbox">
<view class="agree_wo">
<radio-group class="agree" v-for="(item, index) in argee" :key="index">
<view>
<radio class="agreeRadio" :value="item.id" :checked="state" color="#007bff" @click="radioCheck"></radio>
@@ -66,6 +82,7 @@
import { useMessage } from '@/uni_modules/wot-design-uni'
import { getBookBuyConfigList, getAgreement, getActivityDescription, verifyGooglePay, getPlaceOrder } from '@/api/modules/user'
import { useUserStore } from '@/stores/user'
import { useThrottle } from '@/hooks/useThrottle';
const googlePay = uni.requireNativePlugin("sn-googlepay5");
const userStore = useUserStore()
@@ -99,7 +116,7 @@
const popup = ref(null)
const agreemenState = ref(false)
// 协议id
const id = ref(101)
const id = ref(116)
const richTextContent = ref('');
// 协议数据
@@ -114,7 +131,59 @@
const purchaseToken = ref()
// 订单编号
const orderSn = ref('')
// 活动充值数据
const eventAmountList = ref([])
//正常金额数据
const standardAmountList = ref([])
/**
* 获取使用环境
*/
const getDevName = () => {
if (uni.getSystemInfoSync().platform === "android") {
qudao.value = 'Google'
isAndroid.value = true;
console.log('运行Android上')
} else {
qudao.value = 'Google'
console.log('运行iOS上')
}
getData()
}
/**
* 获取充值列表数据
*/
const getData = async () => {
try {
rechargeList.value = await getBookBuyConfigList(type.value, qudao.value)
console.log(rechargeList.value.bookBuyConfigList, '充值列表');
const data = rechargeList.value.bookBuyConfigList
// 默认选择第一个金额
aloneItem.value = data[0]
eventAmountList.value = data.filter(item => item.givejf > 0)
standardAmountList.value = data.filter(item => item.givejf <= 0)
} catch (error) {
console.error('获取订单列表失败:', error)
}
}
/**
* 点击支付按钮
*/
const paymentButton = async () => {
if (!state.value) {
uni.showToast({
title: t('order.readAgreeServices'),
icon: 'none'
})
return
}
getPlaceOrderObj()
}
// 节流支付按钮
const handleRecharge = useThrottle(paymentButton);
/**
* 获取订单编号
@@ -131,31 +200,17 @@
productId: priceTypeId.value // 商品id
}
try {
// uni.hideLoading()
const res = await getPlaceOrder(data)
orderSn.value = res.orderSn
console.log(orderSn.value, '获取订单号');
uni.showLoading({ title: t('order.orderCreating') })
getGooglePay()
} catch (error) {
console.error('获取订单号失败', error)
}
}
/**
* 获取使用环境
*/
const getDevName = () => {
if (uni.getSystemInfoSync().platform === "android") {
qudao.value = 'Google'
isAndroid.value = true;
console.log('运行Android上')
} else {
qudao.value = 'Google'
console.log('运行iOS上')
}
getData()
}
// 点击金额
const chosPric = (item : any) => {
console.log(item, '金额每项');
@@ -176,17 +231,119 @@
const showAgreement = () => {
agreemenState.value = true
}
/**
* 获取充值列表数据
* 初始化
*/
const getData = async () => {
const getGooglePay = () => {
googlePay.init({
}, (e : any) => {
console.log('init', e);
if (e.code == 0) {
isConnected.value = true;
console.log('init成功了');
getQuerySku()
// 初始化成功
} else {
console.log('init失败了/谷歌商店没有登录');
uni.showToast({
title: t('order.unusable'),
icon: 'error'
})
// 初始化失败
isConnected.value = false;
}
});
}
/**
* 查询sku
*/
const getQuerySku = () => {
const id = aloneItem.value.priceTypeId
console.log(id, '获取每项');
googlePay.querySku(
{
inapp: [id], // 与subs二选一, 参数为商品ID字符串数组
},
(e : any) => {
if (e.code == 0) {
// 查询成功.
console.log('querySku查询成功', e);
getPayAll()
uni.hideLoading()
} else {
console.log('查询失败\网络连接失败', e);
uni.showToast({
title: t('global.networkConnectionError'),
icon: 'error'
})
}
}
)
}
/**
* 发起支付
*/
const getPayAll = () => {
console.log(aloneItem.value.priceTypeId, orderSn.value, '发起支付传入产品id,订单id');
googlePay.payAll(
{
productId: aloneItem.value.priceTypeId, // 产品id
accountId: orderSn.value // 订单编号
},
(e : any) => {
if (e.code == 0) {
purchaseToken.value = e.data[0].original.purchaseToken
// 支付成功
console.log(e, 'payAll方法成功返参');
getConsume()
} else {
uni.showToast({ title: t('user.closeWindow'), icon: 'error' })
console.log(e, 'e');
// 支付失败
}
},
)
}
/**
* 消耗品 确认交易
*/
const getConsume = () => {
googlePay.consume(
{
purchaseToken: purchaseToken.value, // 来自支付结果的original.purchaseToken (或 original.token)
},
(e : any) => {
if (e.code == 0) {
console.log(e, '确认交易成功');
uni.showToast({ title: t('user.returnMine'), icon: 'none' })
// 确认成功
googleVerify()
} else {
console.log(e, '确认交易失败');
// 确认失败
}
},
);
}
/**
* 校验订单
*/
const googleVerify = async () => {
uni.hideLoading()
console.log(typeof aloneItem.value.priceTypeId, typeof purchaseToken.value, typeof orderSn.value);
try {
rechargeList.value = await getBookBuyConfigList(type.value, qudao.value)
console.log(rechargeList.value.bookBuyConfigList, '充值列表');
// 默认选择第一个金额
aloneItem.value = rechargeList.value.bookBuyConfigList[0]
const obj = await verifyGooglePay(aloneItem.value.priceTypeId, purchaseToken.value, orderSn.value)
uni.switchTab({
url: '/pages/user/index'
})
console.log(obj, '校验订单');
} catch (error) {
console.error('获取订单列表失败:', error)
console.error('校验订单失败:', error)
}
}
@@ -212,8 +369,7 @@
try {
const text = ref(await getActivityDescription())
remark.value = text.value.res[0]
// console.log(remark.value, '活动说明内容');
console.log(remark.value, '活动说明内容');
} catch (error) {
console.error('获取协议数据:', error)
}
@@ -226,125 +382,6 @@
// payType.value = val;
}
const handleRecharge = async () => {
if (!state.value) {
uni.showToast({
title: t('order.readAgreeServices'),
icon: 'none'
})
return
}
getPlaceOrderObj()
uni.showLoading({ title: '生成订单中...' })
}
/**
* 初始化
*/
const getGooglePay = () => {
googlePay.init({
}, (e : any) => {
console.log('init', e);
if (e.code == 0) {
isConnected.value = true;
getQuerySku()
// 初始化成功
} else {
// 初始化失败
isConnected.value = false;
}
});
}
/**
* 查询sku
*/
const getQuerySku = () => {
const id = aloneItem.value.priceTypeId
console.log(id, '获取每项');
googlePay.querySku(
{
inapp: [id], // 与subs二选一, 参数为商品ID字符串数组
},
(e : any) => {
if (e.code == 0) {
// 查询成功.
console.log('querySku查询成功', e);
uni.hideLoading()
getPayAll()
} else {
console.log('查询失败', e);
// 查询失败
}
}
)
}
/**
* 发起支付
*/
const getPayAll = () => {
console.log(aloneItem.value.priceTypeId, orderSn.value, '发起支付传入产品id,订单id');
googlePay.payAll(
{
productId: aloneItem.value.priceTypeId, // 产品id
accountId: orderSn.value // 订单编号
},
(e : any) => {
if (e.code == 0) {
purchaseToken.value = e.data[0].original.purchaseToken
// 支付成功
console.log(e, 'payAll方法成功返参');
getConsume()
} else {
uni.showToast({ title: '支付失败', icon: 'success' })
console.log(e, 'e');
// 支付失败
}
},
)
}
/**
* 消耗品 确认交易
*/
const getConsume = () => {
googlePay.consume(
{
purchaseToken: purchaseToken.value, // 来自支付结果的original.purchaseToken (或 original.token)
},
(e : any) => {
if (e.code == 0) {
console.log(e, '确认交易成功');
// 确认成功
googleVerify()
} else {
console.log(e, '确认交易失败');
// 确认失败
}
},
);
}
/**
* 校验订单
*/
const googleVerify = async () => {
console.log(typeof aloneItem.value.priceTypeId, typeof purchaseToken.value, typeof orderSn.value);
try {
const obj = await verifyGooglePay(aloneItem.value.priceTypeId, purchaseToken.value, orderSn.value)
uni.switchTab({
url: '/pages/user/index'
})
console.log(obj, '校验订单');
} catch (error) {
console.error('校验订单失败:', error)
}
}
onMounted(() => {
getDevName();
getActivityDescriptionData()
@@ -361,11 +398,12 @@
}
.text {
font-size: 30rpx;
font-size: 45rpx;
font-weight: bold;
color: #007bff;
padding: 30rpx 0 20rpx 20rpx;
padding: 30rpx 0 20rpx 30rpx;
}
.recharge {
display: flex;
flex-wrap: wrap;
@@ -383,13 +421,12 @@
height: 160rpx;
.recharge_money {
font-size: 30rpx;
margin-bottom: 10rpx;
font-size: 50rpx;
font-weight: bold;
}
.recharge_give {
font-size: 16rpx;
font-size: 22rpx;
color: #FF0033;
}
}
@@ -405,20 +442,20 @@
bottom: 0;
left: 0;
right: 0;
padding: 30rpx;
padding: 20rpx;
background-color: #ffffff;
border-top: 1px solid #f0f0f0;
z-index: 999;
}
.recharge-button {
width: 100%;
height: 50rpx;
line-height: 50rpx;
width: 60%;
height: 100rpx;
line-height: 100rpx;
background-color: #007bff;
color: #ffffff;
font-size: 18rpx;
border-radius: 25rpx;
font-size: 40rpx;
border-radius: 50rpx;
border: none;
}
@@ -427,7 +464,7 @@
}
.activity-container {
margin: 0 20rpx 20rpx 20rpx;
margin: 0 30rpx 20rpx 30rpx;
padding: 15rpx;
background-color: #e6f4ff;
border-radius: 8rpx;
@@ -438,7 +475,7 @@
position: absolute;
top: -10rpx;
right: -6rpx;
font-size: 16rpx;
font-size: 20rpx;
font-weight: 600;
color: #ffffff;
background-color: #007bff;
@@ -448,10 +485,11 @@
}
.cha_fangsh {
padding: 20rpx 20rpx 60rpx 20rpx;
padding: 30rpx;
.cf_title {
font-size: 30rpx;
font-size: 45rpx;
font-weight: bold;
color: #007bff;
}
@@ -459,7 +497,7 @@
margin-top: 20rpx;
.cf_xuanx {
font-size: 24rpx;
font-size: 36rpx;
padding: 10rpx 0;
margin-bottom: 20rpx;
border-bottom: 1px solid #ededed;
@@ -492,9 +530,9 @@
.agree_wo {
display: flex;
padding: 20rpx 20rpx 160rpx 20rpx;
padding: 20rpx 20rpx 180rpx 20rpx;
color: #aaa;
font-size: 18rpx;
font-size: 30rpx;
align-items: center;
.highlight {
@@ -525,4 +563,35 @@
overflow-y: scroll
}
}
.active_block {
background-color: rgba(37, 143, 235, 0.2);
margin: 0 30rpx 20rpx 30rpx;
border-radius: 10rpx;
padding: 15rpx;
font-size: 28rpx;
color: #333;
}
::v-deep.active_block span {
color: red;
font-size: 32rpx;
font-weight: bold;
padding: 0 5rpx;
}
::v-deep.active_block span:first-child {
padding: 0 5rpx 0 0;
}
::v-deep.active_block p {
padding-top: 10rpx;
color: #666;
font-size: 24rpx;
line-height: 34rpx;
}
::v-deep.active_block p span {
padding: 0 5rpx !important;
}
</style>

View File

@@ -66,12 +66,15 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed } from 'vue'
import { useSysStore } from '@/stores/sys'
import { useUserStore } from '@/stores/user'
import { useI18n } from 'vue-i18n'
import { useMessage } from '@/uni_modules/wot-design-uni'
import { makePhoneCall, copyToClipboard } from '@/utils/index'
// #ifdef APP-PLUS
import update from "@/uni_modules/uni-upgrade-center-app/utils/check-update";
// #endif
const { t, locale } = useI18n()
const sysStore = useSysStore()
@@ -81,7 +84,6 @@ const message = useMessage()
// 导航栏高度
const statusBarHeight = ref(0)
const navbarHeight = ref('44px')
// 弹窗状态
const showQrCode = ref(false)
@@ -89,8 +91,8 @@ const showLanguageSelect = ref(false)
// 可选语言列表
const availableLanguages = computed(() => [
{ code: 'en', name: t('locale.en') },
{ code: 'zh-Hans', name: t('locale.zh-hans') }
{ code: 'zh-Hans', name: t('locale.zh-hans') },
{ code: 'en', name: t('locale.en') }
])
// 获取当前语言名称
@@ -102,22 +104,22 @@ const getCurrentLanguageName = () => {
// 设置项列表
const settingItems = computed(() => [
{
id: 0,
label: t('user.language'),
value: getCurrentLanguageName(),
type: 'language'
},
// {
// id: 0,
// label: t('user.language'),
// value: getCurrentLanguageName(),
// type: 'language'
// },
{
id: 1,
label: t('user.hotline'),
value: '022-24142321',
value: '021-08371305',
type: 'tel'
},
{
id: 2,
label: t('user.customerEmail'),
value: 'appyilujiankang@sina.com',
value: 'AmazingLimited@163.com',
type: 'email'
},
{
@@ -134,22 +136,6 @@ const settingItems = computed(() => [
}
])
/**
* 获取导航栏高度
*/
const getNavbarHeight = () => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
let navBarHeight = 44
if (systemInfo.model.indexOf('iPhone') !== -1 && parseInt(systemInfo.model.slice(-2)) >= 11) {
navBarHeight = 48
}
const totalHeight = statusBarHeight.value + navBarHeight
navbarHeight.value = totalHeight + 'px'
}
/**
* 处理设置项点击
*/
@@ -221,13 +207,16 @@ const selectLanguage = (languageCode: string) => {
/**
* 检查版本更新
*/
const checkVersion = () => {
const checkVersion = async () => {
// #ifdef APP-PLUS
// TODO: 集成 uni-upgrade-center-app 插件
uni.showToast({
title: '当前已是最新版本',
icon: 'none'
})
var info = await update();
console.log('版本检测信息', info)
if(info.result.code == 0){
uni.showToast({
title:info.result.message,
icon:'none'
})
}
// #endif
// #ifndef APP-PLUS
@@ -283,10 +272,6 @@ const performLogout = () => {
url: '/pages/login/login'
})
}
onMounted(() => {
getNavbarHeight()
})
</script>
<style lang="scss" scoped>

View File

@@ -1,17 +1,27 @@
<template>
<z-paging ref="paging" v-model="bookList" auto-show-back-to-top class="my-book-page" @query="rechargeList" :default-page-size="10">
<z-paging ref="paging" v-model="bookList" auto-show-back-to-top class="my-book-page" @query="rechargeList"
:default-page-size="10">
<template #top>
<!-- 自定义导航栏 -->
<nav-bar :title="$t('user.consumptionRecord')"></nav-bar>
</template>
<view style="padding: 20rpx;background-color:#FFF3CD">
<text style="font-size: 26rpx; color: #FFA500;">{{goBuyTitle}}</text>
</view>
<view class="recharge-record" v-if="(bookList && bookList.length > 0)">
<view class="go-gecharge" @click="goRecharge">
<view>{{$t('order.recharge')}}</view>
<view><wd-icon name="arrow-right" size="16px" color="#fff"/></view>
<view><wd-icon name="arrow-right" size="16px" color="#fff" /></view>
</view>
<view class="title">{{$t('order.rechargeConsumptionList')}}</view>
<view class="recharge-record-block" v-for="(item, index) in bookList" :key="index">
<view class="recharge-record-block-row">{{item.orderType}}<text class="text">{{item.changeAmount}}</text></view>
<view class="recharge-record-block" v-for="(item, index) in bookList" :key="index" @click="toDetails(item)">
<view class="recharge-record-block-row">{{item.orderType}}<text
:class="item.orderType !== '充值' ? 'text1' : 'text2'">{{item.orderType !== '充值' ? '' : '+'}}{{item.changeAmount}}</text>
</view>
<view class="recharge-record-block-row_">{{item.productName}}</view>
<view class="recharge-record-block-row_">{{$t('user.orderSn')}}{{item.payNo}}<wd-icon name="file-copy"
size="14px" color="#65A1FA" style="margin-left: 10rpx;" @click="copyToClipboard()"></wd-icon>
</view>
<view class="time">{{item.createTime}}</view>
</view>
</view>
@@ -23,6 +33,7 @@
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user'
import { getTransactionDetailsList } from '@/api/modules/user'
import { copyToClipboard } from '@/utils/index'
const { t } = useI18n()
const paging = ref<any>()
@@ -32,6 +43,7 @@
const bookList = ref([])
const loading = ref(false)
const firstLoad = ref(true)
const goBuyTitle = ref('【天医币】仅为我平台支付使用币种,仅为了方便用户支付使用。【天医币】可以用于在我平台支付书籍或课程使用。【天医币】这个名称是为适应我们平台的定位属性,所起名称。与区块链虚拟货币无任何关系。')
// 充值记录列表
async function rechargeList(pageNo : number, pageSize : number) {
@@ -49,7 +61,7 @@
loading.value = false
}
}
/**
* 跳转充值页面
*/
@@ -58,6 +70,16 @@
url: '/pages/user/recharge/index'
})
}
/**
* 跳转订单详情
*/
const toDetails = (order: IOrder) => {
console.log(order.relationId, "order");
uni.navigateTo({
url: '/pages/user/order/details?orderId=' + order.relationId
})
}
</script>
<style lang="scss" scoped>
@@ -72,8 +94,8 @@
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
// padding: 20rpx;
margin: 20rpx;
.go-gecharge{
.go-gecharge {
//height: 100rpx;
background: linear-gradient(to right, #007bff, #17a2b8);
font-size: 30rpx;
@@ -81,12 +103,14 @@
color: #fff;
padding: 20rpx;
margin-bottom: 20rpx;
display: flex;justify-content:space-between;align-items:center
display: flex;
justify-content: space-between;
align-items: center
}
.title {
font-size: 30rpx;
padding-left:20rpx;
padding-left: 20rpx;
margin-bottom: 30rpx;
color: #007bff;
font-weight: bold;
@@ -95,41 +119,50 @@
.recharge-record-block {
border-bottom: 1px solid #e0e0e0;
padding: 20rpx;
.time{
font-size: 20rpx;
.recharge-record-block-row_ {
font-size: 24rpx;
margin-bottom: 20rpx;
}
.time {
font-size: 22rpx;
}
.recharge-record-block-row {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
.text{
color: #007bff;
font-weight: 700;
.text1 {
color: #ff0000;
}
.text2 {
color: #228B22;
}
}
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 200rpx;
image {
width: 400rpx;
height: 300rpx;
margin-bottom: 40rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 50rpx;
}
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 200rpx;
image {
width: 400rpx;
height: 300rpx;
margin-bottom: 40rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 50rpx;
}
}
</style>

View File

@@ -26,7 +26,6 @@
:class="{ 'package-card--popular': vip.isRecommend }"
v-for="(vip, index) in vipList"
:key="index"
@click="selectPackage(vip)"
>
<view class="package-header">
<view class="package-title-wrapper">

BIN
static/nobg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -1,142 +0,0 @@
// stores/course.ts
import { defineStore } from 'pinia'
import { courseApi } from '@/api/modules/course'
import type { ICourseDetail, ICatalogue, IChapter, IVipInfo } from '@/types/course'
interface CourseState {
currentCourse: ICourseDetail | null
catalogueList: ICatalogue[]
currentCatalogueIndex: number
chapterList: IChapter[]
userVip: IVipInfo | null
learningProgress: number
}
export const useCourseStore = defineStore('course', {
state: (): CourseState => ({
currentCourse: null,
catalogueList: [],
currentCatalogueIndex: 0,
chapterList: [],
userVip: null,
learningProgress: 0,
}),
getters: {
/**
* 获取当前选中的目录
*/
currentCatalogue: (state): ICatalogue | null => {
if (state.catalogueList.length === 0) return null
return state.catalogueList[state.currentCatalogueIndex] || null
},
/**
* 判断当前目录是否已购买
*/
isCurrentCataloguePurchased: (state): boolean => {
const catalogue = state.catalogueList[state.currentCatalogueIndex]
return catalogue ? catalogue.isBuy === 1 : false
},
/**
* 判断用户是否为VIP
*/
isVip: (state): boolean => {
return state.userVip !== null
},
},
actions: {
/**
* 获取课程详情
* @param courseId 课程ID
*/
async fetchCourseDetail(courseId: number) {
try {
const res = await courseApi.getCourseDetail(courseId)
if (res.code === 0 && res.data) {
this.currentCourse = res.data.course
this.catalogueList = res.data.catalogues || []
// 计算学习进度
if (this.catalogueList.length > 0) {
const totalProgress = this.catalogueList.reduce((sum, cat) => sum + cat.completion, 0)
this.learningProgress = Number((totalProgress / this.catalogueList.length).toFixed(2))
} else {
this.learningProgress = 0
}
}
return res
} catch (error) {
console.error('获取课程详情失败:', error)
throw error
}
},
/**
* 切换目录
* @param index 目录索引
*/
async switchCatalogue(index: number) {
if (index < 0 || index >= this.catalogueList.length) {
console.warn('目录索引超出范围')
return
}
this.currentCatalogueIndex = index
const catalogue = this.catalogueList[index]
// 获取该目录的章节列表
await this.fetchChapterList(catalogue.id)
},
/**
* 获取章节列表
* @param catalogueId 目录ID
*/
async fetchChapterList(catalogueId: number) {
try {
const res = await courseApi.getCatalogueChapterList(catalogueId)
if (res.code === 0) {
this.chapterList = res.chapterList || []
}
return res
} catch (error) {
console.error('获取章节列表失败:', error)
this.chapterList = []
throw error
}
},
/**
* 检查用户VIP权益
* @param courseId 课程ID
*/
async checkVipStatus(courseId: number) {
try {
const res = await courseApi.checkCourseVip(courseId)
if (res.code === 0) {
this.userVip = res.userVip || null
}
return res
} catch (error) {
console.error('检查VIP权益失败:', error)
this.userVip = null
throw error
}
},
/**
* 重置课程状态
*/
resetCourseState() {
this.currentCourse = null
this.catalogueList = []
this.currentCatalogueIndex = 0
this.chapterList = []
this.userVip = null
this.learningProgress = 0
},
},
})

View File

@@ -15,6 +15,11 @@ export const useSysStore = defineStore('sys', {
7: '国学VIP',
8: '心理学VIP',
9: '中西汇通学VIP',
},
customerServicePhone: '021-08371305',
orderStatusMap: {
'0': '待付款',
'3': '已完成',
}
}),

View File

@@ -9,6 +9,12 @@
"Courier New", monospace;
--color-red-500: oklch(63.7% 0.237 25.331);
--spacing: 0.25rem;
--text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.75);
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
--text-lg: 1.125rem;
--text-lg--line-height: calc(1.75 / 1.125);
--font-weight-bold: 700;
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--default-transition-duration: 150ms;
@@ -205,18 +211,15 @@
max-width: 96rem;
}
}
.mr-1 {
margin-right: calc(var(--spacing) * 1);
.mb-2\! {
margin-bottom: calc(var(--spacing) * 2) !important;
}
.ml-1 {
margin-left: calc(var(--spacing) * 1);
.mb-\[20rpx\]\! {
margin-bottom: 20rpx !important;
}
.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;
}
@@ -253,9 +256,6 @@
.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,);
}
@@ -269,14 +269,14 @@
border-style: var(--tw-border-style);
border-width: 1px;
}
.bg-\[blue\] {
background-color: blue;
.p-0\! {
padding: calc(var(--spacing) * 0) !important;
}
.bg-\[red\] {
background-color: red;
.p-\[10rpx\] {
padding: 10rpx;
}
.bg-\[transparent\] {
background-color: transparent;
.p-\[30rpx\] {
padding: 30rpx;
}
.pt-1 {
padding-top: calc(var(--spacing) * 1);
@@ -284,34 +284,49 @@
.pt-10 {
padding-top: calc(var(--spacing) * 10);
}
.pb-0 {
padding-bottom: calc(var(--spacing) * 0);
.pt-\[10rpx\] {
padding-top: 10rpx;
}
.pb-0\! {
padding-bottom: calc(var(--spacing) * 0) !important;
}
.pb-\[20rpx\] {
padding-bottom: 20rpx;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.text-lg {
font-size: var(--text-lg);
line-height: var(--tw-leading, var(--text-lg--line-height));
}
.text-sm {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
}
.text-xs {
font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height));
}
.font-bold {
--tw-font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-bold);
}
.text-\[\#000\] {
color: #000;
}
.text-\[\#7dc1f0\] {
color: #7dc1f0;
}
.text-\[\#fff\] {
color: #fff;
.text-\[cadetblue\] {
color: cadetblue;
}
.text-\[red\] {
color: red;
}
.text-red-500 {
color: var(--color-red-500);
}
.lowercase {
text-transform: lowercase;
}
@@ -322,9 +337,6 @@
--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);

View File

@@ -109,10 +109,10 @@ uni-textarea {
// popup
.wd-popup {
z-index: 9999 !important;
z-index: 99 !important;
}
.wd-overlay {
z-index: 9998 !important;
z-index: 98 !important;
}
.wd-popup-wrapper {
.wd-popup {
@@ -144,4 +144,9 @@ uni-textarea {
font-size: var(--wot-fs-tertiary) !important;
border-radius: 4px !important;
padding: 2px 6px !important;
}
}
// 缺省
.wd-status-tip__text {
margin: 0px auto 20px !important;
}

22
types/order.d.ts vendored
View File

@@ -11,6 +11,7 @@ export interface IGoods {
isVipPrice?: number // 是否有VIP优惠 0-否 1-是
productAmount?: number // 购买数量
delFlag?: number // 删除标记 -1-已下架
goodsType?: string // 商品类型 "05" 课程 "02" 电子书
}
/**
@@ -138,3 +139,24 @@ export interface IPaymentOption {
value: '4' | '5'
name: string
}
/**
* 订单详情
*/
export interface IOrderDetail {
id: number
orderSn: string
orderMoney: number
realMoney: number
paymentDate: string
createTime: string
orderType: string
districtMoney?: number
vipDiscountAmount?: number
couponId?: number
couponName?: string
couponAmount?: number
jfDeduction?: number
remark?: string
[key: string]: any
}

View File

@@ -0,0 +1,10 @@
## 1.1.42025-12-02
修复部分设备上可能出现的选择图片之后不触发任何回调的bug
## 1.1.22025-04-18
修复部分情况下选中了图片但是没有返回的问题。
## 1.1.12024-11-28
修复设置count为1时选择图片提示失败的bug。
## 1.1.02024-10-31
新增chooseSystemMedia支持选择图片和视频。
## 1.0.02024-10-23
新增插件

View File

@@ -0,0 +1,114 @@
{
"id": "uni-chooseSystemImage",
"displayName": "uni-chooseSystemMedia",
"version": "1.1.4",
"description": "从手机相册中选择图片或视频解决google play新政策禁止添加媒体权限的问题",
"keywords": [
"google",
"上架",
"图片选择"
],
"repository": "",
"engines": {
"HBuilderX": "^4.29",
"uni-app": "^3.99",
"uni-app-x": "^3.99"
},
"dcloudext": {
"type": "uts",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "本插件不会采集任何隐私信息获取权限仅是为了兼容android12及以下版本的系统。",
"permissions": "<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />"
},
"npmurl": "",
"darkmode": "x",
"i18n": "x",
"widescreen": "x"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "√",
"aliyun": "√",
"alipay": "√"
},
"client": {
"uni-app": {
"vue": {
"vue2": {
"extVersion": "1.0.0",
"minVersion": ""
},
"vue3": {
"extVersion": "1.0.0",
"minVersion": ""
}
},
"web": {
"safari": "x",
"chrome": "x"
},
"app": {
"vue": {
"extVersion": "1.0.0",
"minVersion": ""
},
"nvue": {
"extVersion": "1.1.0",
"minVersion": ""
},
"android": {
"extVersion": "1.0.0",
"minVersion": "19"
},
"ios": "x",
"harmony": "x"
},
"mp": {
"weixin": "x",
"alipay": "x",
"toutiao": "x",
"baidu": "x",
"kuaishou": "x",
"jd": "x",
"harmony": "x",
"qq": "x",
"lark": "x"
},
"quickapp": {
"huawei": "x",
"union": "x"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}

View File

@@ -0,0 +1,89 @@
## chooseSystemMedia
chooseSystemMedia支持通过系统API选择图片解决google play新政策要求[移除照片和视频访问权限权限](https://support.google.com/googleplay/android-developer/answer/14115180)。
### 引入插件
```
import {
chooseSystemMedia
} from "@/uni_modules/uni-chooseSystemImage"
```
### 参数说明
|参数名称 |类型 |描述 |取值 |默认值 |
|:-- |:-- |:-- |:-- |:-- |
|count |number |最多可以选择的文件个数 |最多支持100个 | |
|mediaType |Array<string> |支持的文件类型 |image:只能选择图片<br/>video:只能选择视频<br/>mix可以同时选择图片和视频 |['image'] |
|pageOrientation|string |图片选择的方向 |auto:跟随系统方向<br/>landscape:横向显示<br/>portrait:竖向显示 |portrait |
|success |function |成功回调 | | |
|fail |function |失败回调 | | |
|complete |function |完成回调 | | |
图片选择成功回调:
|参数名称 |类型 |描述 |
|:-- |:-- |:-- |
|filePaths | Array<string> |选择的文件列表 |
图片选择失败回调错误码
|错误码 |描述 |
|:-- |:-- |
|2101001|用户取消 |
|2101002|传入的参数异常 |
|2101005|权限申请失败 |
|2101010|其他异常,如果遇到可以评论反馈 |
### 调用APIK
```javascript
chooseSystemMedia({
count: 2,
mediaType: ['image'],
pageOrientation:"portrait",
success: (e) => {
console.log(e.filePaths)
},
fail: (e) => {
console.log(e)
}
```
## chooseSystemImage
`chooseSystemImage`已废弃,后续不在维护,建议切换成`chooseSystemMedia`
### 引入插件
```
import {
chooseSystemImage
} from "@/uni_modules/uni-chooseSystemImage"
```
### 调用API
```javascript
chooseSystemImage({
count: 3,
success: (e) => {
console.log(e.filePaths)
},
fail: (e) => {
console.log(e)
}
})
```
注意在Android 11及以上的系统中调用的是系统的照片选择器。低于android 11的系统中会调用系统的文件选择器。
目前android系统的图片选择仅支持选择图片数量如果需要针对图片压缩可以使用[uni.compressImage](https://uniapp.dcloud.net.cn/api/media/image.html#compressimage)。
引入当前插件时同时需要将照片和视频权限移除。将下面内容拷贝到项目的manifest.json->Android/iOS权限配置->强制移除的权限。
```xml
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" ></uses-permission>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" ></uses-permission>
```

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application android:requestLegacyExternalStorage="true">
<meta-data android:name="ScopedStorage" android:value="true" />
<activity
android:name=".ChooseSystemImageActivity"
android:configChanges="orientation|screenSize"
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen"/>
<service
android:name="com.google.android.gms.metadata.ModuleDependencies"
android:enabled="false"
android:exported="false"
tools:ignore="MissingClass">
<intent-filter>
<action android:name="com.google.android.gms.metadata.MODULE_DEPENDENCIES" />
</intent-filter>
<meta-data
android:name="photopicker_activity:0:required"
android:value="" />
</service>
</application>
</manifest>

View File

@@ -0,0 +1,143 @@
package uts.sdk.modules.uniChooseSystemImage
import android.app.Activity
import android.content.Intent
import android.content.pm.ActivityInfo
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.view.WindowManager
import android.widget.LinearLayout
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType
import androidx.fragment.app.FragmentActivity
import java.util.Locale
class ChooseSystemImageActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStatusBarTransparent(this)
val layout = LinearLayout(this)
layout.setBackgroundColor(Color.TRANSPARENT)
setContentView(layout)
if (intent.hasExtra("page_orientation")) {
requestedOrientation =
intent.getIntExtra("page_orientation", ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
}
val count = intent.getIntExtra("count", 9)
val type = intent.getIntExtra("type", 1)
val mediaType: VisualMediaType = when (type) {
1 -> {
ActivityResultContracts.PickVisualMedia.ImageOnly
}
2 -> {
ActivityResultContracts.PickVisualMedia.VideoOnly
}
3 -> {
ActivityResultContracts.PickVisualMedia.ImageAndVideo
}
else -> {
ActivityResultContracts.PickVisualMedia.ImageOnly
}
}
val pickMultipleMedia = if (count == 1) {
this.registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
val intent = Intent()
if (uri != null) {
this.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
var path = uri.toString()
val mediaT = this.contentResolver.getType(uri)?.lowercase(Locale.ENGLISH)
val m = Media(
if (mediaT?.startsWith("video/") == true) {
2
} else if (mediaT?.startsWith("image/") == true) {
1
} else {
0
}, path
)
intent.putExtra("paths", arrayOf(m))
this.setResult(RESULT_OK, intent)
this.finish()
} else {
this.setResult(RESULT_OK, intent)
this.finish()
}
}
} else
this.registerForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia(count)
) { result ->
val paths = mutableListOf<Media>()
for (uri in result) {
this.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
var path = uri.toString()
val mediaT = this.contentResolver.getType(uri)?.lowercase(Locale.ENGLISH)
val m = Media(
if (mediaT?.startsWith("video/") == true) {
2
} else if (mediaT?.startsWith("image/") == true) {
1
} else {
0
}, path
)
paths.add(m)
}
val intent = Intent()
intent.putExtra("paths", paths.toTypedArray())
this.setResult(RESULT_OK, intent)
this.finish()
}
pickMultipleMedia.launch(
PickVisualMediaRequest.Builder()
.setMediaType(mediaType)
.build()
)
}
private fun getFilePathFromUri(uri: Uri): String? {
var filePath: String? = null
if (uri.scheme == "file") {
filePath = uri.path
} else if (uri.scheme == "content") {
val contentResolver = contentResolver
val cursor =
contentResolver.query(uri, arrayOf(MediaStore.Images.Media.DATA), null, null, null)
if (cursor != null && cursor.moveToFirst()) {
val columnIndex = cursor.getColumnIndex("_data")
filePath = cursor.getString(columnIndex)
cursor.close()
}
}
return filePath
}
private fun setStatusBarTransparent(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
val window = activity.window
// 设置透明状态栏标志
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.clearFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION
)
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
window.statusBarColor = Color.TRANSPARENT
}
}
}

View File

@@ -0,0 +1,210 @@
package uts.sdk.modules.uniChooseSystemImage
import android.content.ContentResolver
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.webkit.MimeTypeMap
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.net.URLConnection
import java.security.MessageDigest
object FileUtils {
fun getFilePathByUri(context: Context, uri: Uri): String? {
var path: String? = null
// 以 file:// 开头的
if (ContentResolver.SCHEME_FILE == uri.scheme) {
path = uri.path
return path
}
// 以 content:// 开头的,比如 content://media/extenral/images/media/17766
if (ContentResolver.SCHEME_CONTENT == uri.scheme && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
val cursor = context.contentResolver.query(
uri,
arrayOf(MediaStore.Images.Media.DATA),
null,
null,
null
)
if (cursor != null) {
if (cursor.moveToFirst()) {
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
if (columnIndex > -1) {
path = cursor.getString(columnIndex)
}
}
cursor.close()
}
return path
}
// 4.4及之后的 是以 content:// 开头的,比如 content://com.android.providers.media.documents/document/image%3A235700
if (ContentResolver.SCHEME_CONTENT == uri.scheme && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (DocumentsContract.isDocumentUri(context, uri)) {
if (isExternalStorageDocument(uri)) {
// ExternalStorageProvider
val docId = DocumentsContract.getDocumentId(uri)
val split =
docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val type = split[0]
if ("primary".equals(type, ignoreCase = true)) {
path = Environment.getExternalStorageDirectory().toString() + "/" + split[1]
return path
}
} else if (isDownloadsDocument(uri)) {
// DownloadsProvider
val id = DocumentsContract.getDocumentId(uri)
val contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"),
id.toLong()
)
path = getDataColumn(context, contentUri, null, null)
return path
} else if (isMediaDocument(uri)) {
// MediaProvider
val docId = DocumentsContract.getDocumentId(uri)
val split =
docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val type = split[0]
var contentUri: Uri? = null
if ("image" == type) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
} else if ("video" == type) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
} else if ("audio" == type) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
val selection = "_id=?"
val selectionArgs = arrayOf(split[1])
path = getDataColumn(context, contentUri, selection, selectionArgs)
return path
}
}
}
return null
}
// 新增:将 uri 拷贝到传入的父文件夹,文件名为 uri 的 MD5后缀从头信息或原文件名推断。
// 若目标文件已存在则直接返回已存在路径,不重复拷贝。
// 返回目标文件的绝对路径,失败返回 null。
fun copyUriToDir(context: Context, parentDirStr: String, uriString: String): String? {
try {
var uri = Uri.parse(uriString)
var parentDir = File(parentDirStr)
val resolver = context.contentResolver
// 读取全部数据到内存(用于判断 MIME 并写入目标文件)
val inputStream = resolver.openInputStream(uri) ?: return null
val baos = ByteArrayOutputStream()
inputStream.use { ins ->
val buf = ByteArray(8 * 1024)
var len: Int
while (ins.read(buf).also { len = it } != -1) {
baos.write(buf, 0, len)
}
}
val data = baos.toByteArray()
// 通过头信息猜 MIME
var mime: String? = null
try {
mime = URLConnection.guessContentTypeFromStream(ByteArrayInputStream(data))
} catch (_: Exception) {
}
if (mime == null) {
try {
mime = resolver.getType(uri)
} catch (_: Exception) {
}
}
// 若仍为空,尝试从原始路径推断
var originalPath: String? = null
try {
originalPath = getFilePathByUri(context, uri)
} catch (_: Exception) {
}
// 根据 mime 获取扩展名
var ext: String? = null
if (mime != null) {
ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime)
}
// 如果通过 mime 无法得到扩展名,尝试从原始路径取后缀
if (ext.isNullOrEmpty() && !originalPath.isNullOrEmpty()) {
val idx = originalPath.lastIndexOf('.')
if (idx != -1 && idx + 1 < originalPath.length) {
ext = originalPath.substring(idx + 1).lowercase()
}
}
val extSuffix = if (!ext.isNullOrEmpty()) ".${ext}" else ""
// 计算 MD5 作为文件名(基于 uri.toString()
val name = md5(uri.toString()) + extSuffix
// 确保父目录存在
if (!parentDir.exists()) {
parentDir.mkdirs()
}
val destFile = File(parentDir, name)
// 若已存在,直接返回
if (destFile.exists()) {
return destFile.absolutePath
}
// 写入文件
FileOutputStream(destFile).use { fos ->
fos.write(data)
fos.flush()
}
return destFile.absolutePath
} catch (e: Exception) {
// 出错返回 null
return null
}
}
// 辅助:计算字符串的 MD5小写 hex
private fun md5(input: String): String {
val md = MessageDigest.getInstance("MD5")
val bytes = md.digest(input.toByteArray(Charsets.UTF_8))
return bytes.joinToString("") { "%02x".format(it) }
}
private fun getDataColumn(
context: Context,
uri: Uri?,
selection: String?,
selectionArgs: Array<String>?,
): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(column)
try {
cursor =
context.contentResolver.query(uri!!, projection, selection, selectionArgs, null)
if (cursor != null && cursor.moveToFirst()) {
val column_index = cursor.getColumnIndexOrThrow(column)
return cursor.getString(column_index)
}
} finally {
cursor?.close()
}
return null
}
private fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
private fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}
private fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
}

View File

@@ -0,0 +1,42 @@
package uts.sdk.modules.uniChooseSystemImage
import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
class Media : Parcelable {
var type: Int
var path: String?
constructor(type: Int, path: String?) {
this.type = type
this.path = path
}
protected constructor(`in`: Parcel) {
type = `in`.readInt()
path = `in`.readString()
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeInt(type)
dest.writeString(path)
}
companion object {
@JvmField
val CREATOR: Creator<Media> = object : Creator<Media> {
override fun createFromParcel(`in`: Parcel): Media {
return Media(`in`)
}
override fun newArray(size: Int): Array<Media?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -0,0 +1,7 @@
{
"dependencies": [
"androidx.appcompat:appcompat:1.6.1",
"androidx.activity:activity-ktx:1.9.2"
],
"minSdkVersion": "21"
}

View File

@@ -0,0 +1,260 @@
/* 引入 interface.uts 文件中定义的变量 */
import { ChooseSystemImage, ChooseSystemImageOptions, ChooseSystemImageSuccessResult, ChooseSystemMedia, ChooseSystemMediaOptions, ChooseSystemMediaSuccessResult, ChooseSystemVideo, ChooseSystemVideoOptions, ChooseSystemVideoSuccessResult } from '../interface.uts';
import AppCompatActivity from 'androidx.appcompat.app.AppCompatActivity';
import ActivityResultCallback from 'androidx.activity.result.ActivityResultCallback';
import List from 'kotlin.collections.List';
import Uri from 'android.net.Uri';
import ActivityResultContracts from 'androidx.activity.result.contract.ActivityResultContracts';
import ActivityResultLauncher from 'androidx.activity.result.ActivityResultLauncher';
import PickVisualMediaRequest from "androidx.activity.result.PickVisualMediaRequest";
import Builder from "androidx.activity.result.PickVisualMediaRequest.Builder";
import Context from 'com.alibaba.fastjson.parser.deserializer.ASMDeserializerFactory.Context';
import MediaStore from 'android.provider.MediaStore';
import Activity from "android.app.Activity"
import Intent from 'android.content.Intent';
import ChooseSystemImageActivity from "uts.sdk.modules.uniChooseSystemImage.ChooseSystemImageActivity"
/* 引入 unierror.uts 文件中定义的变量 */
import { ImageErrorImpl } from '../unierror';
import ChooseVideoOptions from 'uts.sdk.modules.DCloudUniMedia.ChooseVideoOptions';
import BitmapFactory from 'android.graphics.BitmapFactory';
import File from 'java.io.File';
import FileInputStream from 'java.io.FileInputStream';
import FileOutputStream from 'java.io.FileOutputStream';
import InputStream from 'java.io.InputStream';
import Build from 'android.os.Build';
import Parcelable from 'android.os.Parcelable';
import Media from 'uts.sdk.modules.uniChooseSystemImage.Media';
import FileUtils from "uts.sdk.modules.uniChooseSystemImage.FileUtils"
var resultCallback : ((requestCode : Int, resultCode : Int, data ?: Intent) => void) | null = null
export const chooseSystemImage : ChooseSystemImage = function (option : ChooseSystemImageOptions) {
if (option.count <= 0) {
var error = new ImageErrorImpl(2101002, "uni-chooseSystemImage")
option.fail?.(error)
option.complete?.(error)
return
}
if (Build.VERSION.SDK_INT > 32 || UTSAndroid.getUniActivity()!.applicationInfo.targetSdkVersion >= 33) {
__chooseSystemImage(option)
} else {
UTSAndroid.requestSystemPermission(UTSAndroid.getUniActivity()!, [android.Manifest.permission.READ_EXTERNAL_STORAGE], (a : boolean, b : string[]) => {
__chooseSystemImage(option)
}, (a : boolean, b : string[]) => {
var error = new ImageErrorImpl(2101005, "uni-chooseSystemImage")
option.fail?.(error)
option.complete?.(error)
})
}
}
export const chooseSystemMedia : ChooseSystemMedia = function (option : ChooseSystemMediaOptions) {
if (option.count <= 0) {
var error = new ImageErrorImpl(2101002, "uni-chooseSystemMedia")
option.fail?.(error)
option.complete?.(error)
return
}
if (option.count > 100) {
option.count = 100
}
if (Build.VERSION.SDK_INT > 32 || UTSAndroid.getUniActivity()!.applicationInfo.targetSdkVersion >= 33) {
__chooseSystemMedia(option)
} else {
UTSAndroid.requestSystemPermission(UTSAndroid.getUniActivity()!, [android.Manifest.permission.READ_EXTERNAL_STORAGE], (a : boolean, b : string[]) => {
__chooseSystemMedia(option)
}, (a : boolean, b : string[]) => {
var error = new ImageErrorImpl(2101005, "uni-chooseSystemMedia")
option.fail?.(error)
option.complete?.(error)
})
}
}
function __chooseSystemMedia(option : ChooseSystemMediaOptions) {
try {
resultCallback = (requestCode : Int, resultCode : Int, data : Intent | null) => {
UTSAndroid.offAppActivityResult(resultCallback!)
if (10086 == requestCode && resultCode == -1) {
if (data != null) {
var result = data!.getParcelableArrayExtra("paths")
if (result != null && result!.size > 0) {
var paths : Array<string> = []
result.forEach((p : Parcelable) => {
if (p instanceof Media)
if (UTSAndroid.isUniAppX()) {
paths.push((p.path!))
} else {
paths.push("file://" + copyResource(p.path!))
}
})
var success : ChooseSystemMediaSuccessResult = {
filePaths: paths
}
option.success?.(success)
option.complete?.(success)
} else {
var error = new ImageErrorImpl(2101001, "uni-chooseSystemMedia")
option.fail?.(error)
option.complete?.(error)
}
} else {
var error = new ImageErrorImpl(2101001, "uni-chooseSystemMedia")
option.fail?.(error)
option.complete?.(error)
}
} else {
var error = new ImageErrorImpl(2101001, "uni-chooseSystemMedia")
option.fail?.(error)
option.complete?.(error)
}
}
UTSAndroid.onAppActivityResult(resultCallback!)
var intent = new Intent(UTSAndroid.getUniActivity()!, Class.forName("uts.sdk.modules.uniChooseSystemImage.ChooseSystemImageActivity"))
intent.putExtra("count", option.count)
if (option.mediaType != null) {
if (option.mediaType!.indexOf("mix") >= 0) {
intent.putExtra("type", 3)
} else if (option.mediaType!.indexOf("image") >= 0) {
intent.putExtra("type", 1)
} else if (option.mediaType!.indexOf("video") >= 0) {
intent.putExtra("type", 2)
} else {
intent.putExtra("type", 1)
}
}
switch (option.pageOrientation) {
case "auto": {
intent.putExtra("page_orientation", 2)
break
}
case "portrait": {
intent.putExtra("page_orientation", 1)
break
}
case "landscape": {
intent.putExtra("page_orientation", 0)
break
}
default: {
intent.putExtra("page_orientation", 1)
break
}
}
UTSAndroid.getUniActivity()!.startActivityForResult(intent, 10086)
} catch (e) {
var error = new ImageErrorImpl(2101010, "uni-chooseSystemMedia")
option.fail?.(error)
option.complete?.(error)
}
}
function __chooseSystemImage(option : ChooseSystemImageOptions) {
try {
resultCallback = (requestCode : Int, resultCode : Int, data : Intent | null) => {
UTSAndroid.offAppActivityResult(resultCallback!)
if (10086 == requestCode && resultCode == -1) {
if (data != null) {
var result = data!.getParcelableArrayExtra("paths")
if (result != null && result!.size > 0) {
var paths : Array<string> = []
result.forEach((p : Parcelable) => {
if (p instanceof Media)
if (UTSAndroid.isUniAppX()) {
paths.push((p.path!))
} else {
paths.push("file://" + copyResource(p.path!))
}
})
var success : ChooseSystemImageSuccessResult = {
filePaths: paths
}
option.success?.(success)
option.complete?.(success)
} else {
var error = new ImageErrorImpl(2101001, "uni-chooseSystemImage")
option.fail?.(error)
option.complete?.(error)
}
} else {
var error = new ImageErrorImpl(2101001, "uni-chooseSystemImage")
option.fail?.(error)
option.complete?.(error)
}
} else {
var error = new ImageErrorImpl(2101001, "uni-chooseSystemImage")
option.fail?.(error)
option.complete?.(error)
}
}
UTSAndroid.onAppActivityResult(resultCallback!)
var intent = new Intent(UTSAndroid.getUniActivity()!, Class.forName("uts.sdk.modules.uniChooseSystemImage.ChooseSystemImageActivity"))
intent.putExtra("count", option.count)
intent.putExtra("type", 1)
UTSAndroid.getUniActivity()!.startActivityForResult(intent, 10086)
} catch (e) {
var error = new ImageErrorImpl(2101010, "uni-chooseSystemImage")
option.fail?.(error)
option.complete?.(error)
}
}
var CACHEPATH = UTSAndroid.getAppCachePath()
function copyResource(url : string) : string {
var path : String = CACHEPATH!
if (CACHEPATH?.endsWith("/") == true) {
path = CACHEPATH + "uni-getSystemMedia/"
} else {
path = CACHEPATH + "/uni-getSystemMedia/"
}
console.log(url)
var result = FileUtils.copyUriToDir(UTSAndroid.getAppContext()!,path,url)
// path = path + new File(url).getName()
// copyFile(url, path)
return result!
}
function copyFile(fromFilePath : string, toFilePath : string) : boolean {
var fis : InputStream | null = null
try {
let fromFile = new File(fromFilePath)
if (!fromFile.exists()) {
return false;
}
if (!fromFile.isFile()) {
return false
}
if (!fromFile.canRead()) {
return false;
}
fis = new FileInputStream(fromFile);
if (fis == null) {
return false
}
} catch (e) {
return false;
}
let toFile = new File(toFilePath)
if (!toFile.getParentFile().exists()) {
toFile.getParentFile().mkdirs()
}
if (!toFile.exists()) {
toFile.createNewFile()
}
try {
let fos = new FileOutputStream(toFile)
let byteArrays = ByteArray(1024)
var c = fis!!.read(byteArrays)
while (c > 0) {
fos.write(byteArrays, 0, c)
c = fis!!.read(byteArrays)
}
fis!!.close()
fos.close()
return true
} catch (e) {
return false;
}
}

View File

@@ -0,0 +1,38 @@
export type ChooseSystemImageSuccessResult = {
filePaths : Array<string>
}
export type ImageErrorCode = 2101001 | 2101010 | 2101002 | 2101005
export interface ChooseSystemImageError extends IUniError {
errCode : ImageErrorCode
};
export type ChooseSystemImageSuccessCallback = (result : ChooseSystemImageSuccessResult) => void
export type ChooseSystemImageFailResult = ChooseSystemImageError
export type ChooseSystemImageFailCallback = (result : ChooseSystemImageFailResult) => void
export type ChooseSystemImageCompleteCallback = (callback : any) => void
export type ChooseSystemImageOptions = {
count : number,
success ?: ChooseSystemImageSuccessCallback | null,
fail ?: ChooseSystemImageFailCallback | null,
complete ?: ChooseSystemImageCompleteCallback | null
}
export type ChooseSystemImage = (options : ChooseSystemImageOptions) => void
export type ChooseSystemMediaSuccessResult = {
filePaths : Array<string>
}
export type ChooseSystemMediaSuccessCallback = (result : ChooseSystemMediaSuccessResult) => void
export type ChooseSystemMediaFailResult = ChooseSystemImageError
export type ChooseSystemMediaFailCallback = (result : ChooseSystemMediaFailResult) => void
export type ChooseSystemMediaCompleteCallback = (callback : any) => void
export type ChooseSystemMediaOptions = {
count : number,
mediaType ?: Array<string> | null,
pageOrientation ?: string | null,
success ?: ChooseSystemMediaSuccessCallback | null,
fail ?: ChooseSystemMediaFailCallback | null,
complete ?: ChooseSystemMediaCompleteCallback | null
}
export type ChooseSystemMedia = (options : ChooseSystemMediaOptions) => void

View File

@@ -0,0 +1,25 @@
import { ImageErrorCode, ChooseSystemImageError } from "./interface.uts"
export const ImageUniErrors : Map<number, string> = new Map([
/**
* 用户取消
*/
[2101001, 'user cancel'],
[2101002, 'fail parameter error'],
[2101005, "No Permission"],
/**
* 其他错误
*/
[2101010, "unexpect error:"]
]);
export class ImageErrorImpl extends UniError implements ChooseSystemImageError {
// #ifdef APP-ANDROID
override errCode : ImageErrorCode
// #endif
constructor(errCode : ImageErrorCode, uniErrorSubject : string) {
super()
this.errSubject = uniErrorSubject
this.errCode = errCode
this.errMsg = ImageUniErrors.get(errCode) ?? "";
}
}

View File

@@ -1,5 +1,6 @@
import { isArray, isDef, isFunction } from '../common/util'
import type { ChooseFile, ChooseFileOption, UploadFileItem, UploadMethod, UploadStatusType } from '../wd-upload/types'
import { chooseSystemMedia } from "@/uni_modules/uni-chooseSystemImage"
export const UPLOAD_STATUS: Record<string, UploadStatusType> = {
PENDING: 'pending',
@@ -244,15 +245,26 @@ export function useUpload(): UseUploadReturn {
fail: reject
})
// #endif
// #ifndef MP-WEIXIN
uni.chooseImage({
// #ifdef H5
uni.chooseImage({
count: multiple ? maxCount : 1,
sizeType,
sourceType,
extension,
success: (res) => resolve(formatImage(res)),
fail: reject
})
// #endif
// #ifdef APP-PLUS
chooseSystemMedia({
count: multiple ? maxCount : 1,
sizeType,
sourceType,
// #ifdef H5
extension,
// #endif
success: (res) => resolve(formatImage(res)),
mediaType: ['image'],
success: (res) => {
const tempFiles = res.filePaths.map((item: any) => ({
path: item
}))
resolve(formatImage({ tempFiles }))
},
fail: reject
})
// #endif
@@ -269,19 +281,30 @@ export function useUpload(): UseUploadReturn {
fail: reject
})
// #endif
// #ifndef MP-WEIXIN
// #ifdef H5
uni.chooseVideo({
sourceType,
compressed,
maxDuration,
camera,
// #ifdef H5
extension,
// #endif
success: (res) => resolve(formatVideo(res)),
fail: reject
})
// #endif
// #ifdef APP-PLUS
chooseSystemMedia({
count: multiple ? maxCount : 1,
mediaType: ['video'],
success: (res) => {
const tempFiles = res.filePaths.map((item: any) => ({
path: item
}))
resolve(formatImage({ tempFiles }))
},
fail: reject
})
// #endif
break
// #ifdef MP-WEIXIN
case 'media':
@@ -324,7 +347,6 @@ export function useUpload(): UseUploadReturn {
fail: reject
})
// #endif
break
default:
// #ifdef MP-WEIXIN
@@ -338,14 +360,20 @@ export function useUpload(): UseUploadReturn {
fail: reject
})
// #endif
// #ifndef MP-WEIXIN
// #ifdef H5
uni.chooseImage({
count: multiple ? maxCount : 1,
sizeType,
sourceType,
// #ifdef H5
extension,
// #endif
success: (res) => resolve(formatImage(res)),
fail: reject
})
// #endif
// #ifdef APP-PLUS
chooseSystemMedia({
count: multiple ? maxCount : 1,
mediaType: ['image'],
success: (res) => resolve(formatImage(res)),
fail: reject
})

View File

@@ -21,7 +21,7 @@ export const onPageBack = () => {
* @param {string} phoneNumber - 要拨打的电话号码
* @param {string} title - 拨打电话提示的标题,默认值为空字符串
*/
export const makePhoneCall = (phoneNumber: string, title: string = '') => {
export const makePhoneCall = (phoneNumber: string, title: string = t('global.call')) => {
uni.showModal({
title: title,
content: phoneNumber,