Compare commits

21 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
44 changed files with 1513 additions and 448 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

@@ -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

@@ -237,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',

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

@@ -313,10 +313,10 @@ 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
@@ -331,7 +331,7 @@ const calculateFinalPrice = () => {
0,
totalAmount.value - couponAmount - pointsDiscounted.value - promotionDiscounted.value - vipDiscounted.value
)
finalAmount.value = result
finalAmount.value = parseFloat(result.toPrecision(12))
}
/**
@@ -419,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

@@ -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) // 注释掉 不再保存到本地
}
}
}

View File

@@ -224,10 +224,10 @@
"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, VIP and other data will be transferred to the current account",
"instruction2": "After data migration is complete, the chinese account data will be cleared, and all purchased Tianyi Coins, points, courses, 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."
"instruction4": "If you encounter any issues, please contact customer service for assistance.",
"closeWindow": "Close the payment pop-up window"
},
"book": {
"title": "My Books",
@@ -237,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",
@@ -498,7 +498,8 @@
"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 on the device"
"unusable": "The billing service cannot be used",
"give": "give"
},
"vip": {
"courseVip": "Course VIP",

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

@@ -224,10 +224,11 @@
"migrateCodePlaceholder": "国内版账号获取的迁移验证码",
"migrateWarning": "迁移后不可恢复,请谨慎操作!",
"migrateInstructions": "迁移说明",
"instruction1": "请在吴门医述APP中获取迁移验证码获取方式【我的】-【数据迁移】-【获取迁移验证码】。",
"instruction2": "数据迁移完成后,旧账号数据将被清空,已购买的天医币、积分、课程、VIP等数据将转移到当前账号。",
"instruction1": "请在吴门医述、心灵空间、众妙之门、疯子读书任意APP中获取迁移验证码获取方式【我的】-【数据迁移】-【获取迁移验证码】。",
"instruction2": "数据迁移完成后,旧账号数据将被清空,已购买的天医币、积分、课程、电子书、VIP、证书、湖分将转移到当前账号。",
"instruction3": "迁移过程可能需要几分钟时间,请耐心等待。",
"instruction4": "如遇到问题,请联系客服获取帮助。"
"instruction4": "如遇到问题,请联系客服获取帮助。",
"closeWindow": "关闭支付弹窗"
},
"book": {
"title": "我的书单",
@@ -237,7 +238,7 @@
"comment": "书评",
"choose": "去选书",
"nullText": "暂无书籍,快去选购吧~",
"afterPurchase": "购买后即可使用此功能",
"afterPurchase": "购买后即可阅读此章节",
"contents": "目录",
"zjContents": "章节目录",
"set": "设置",
@@ -498,7 +499,8 @@
"readAgreeServices": "请先阅读并同意增值服务",
"orderDetails": "订单详情",
"pointsRecord": "积分消费记录",
"unusable": "设备上无法使用计费服务"
"unusable": "无法用计费服务",
"give": "赠"
},
"vip": {
"courseVip": "课程VIP",

View File

@@ -2,8 +2,8 @@
"name" : "吴门国际",
"appid" : "__UNI__1250B39",
"description" : "吴门国际",
"versionName" : "0.1.1",
"versionCode" : 11,
"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

@@ -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,

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)
/**
* 获取我的书单
@@ -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="purchaseVisible = true"
>
{{ $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"
@@ -47,8 +43,8 @@
</view>
<text v-else class="empty-text">{{ nullText }}</text>
</view>
</scroll-view>
</scroll-view>
</view>
<!-- 购买弹窗 -->
<GoodsSelector
@@ -62,19 +58,23 @@
<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 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,
@@ -87,7 +87,6 @@ const bookInfo = ref<IBookDetail>({
const chapterList = ref<IChapter[]>([])
const activeIndex = ref(-1)
const nullText = ref('')
const scrollHeight = ref(0)
// 生命周期
onLoad((options: any) => {
@@ -98,11 +97,12 @@ onLoad((options: any) => {
fromIndex.value = Number(options.index)
activeIndex.value = fromIndex.value
}
initScrollHeight()
loadGoodsInfo()
})
onShow(() => {
loadBookInfo()
loadChapterList()
loadGoodsInfo()
})
// 购买弹窗状态
@@ -127,21 +127,6 @@ async function loadGoodsInfo() {
goodsList.value = res.productList || []
}
// 初始化滚动区域高度
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
}
// 加载书籍信息
async function loadBookInfo() {
const res = await bookApi.getBookInfo(bookId.value)
@@ -163,7 +148,7 @@ async function loadChapterList() {
// 判断章节是否锁定
function isLocked(index: number): boolean {
return !bookInfo.value.isBuy && index + 1 > bookInfo.value.freeChapterCount
return !bookInfo.value.isBuy && index + 1 > bookInfo.value.freeChapterCount && !hasVip.value
}
// 播放章节
@@ -208,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-on" 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,6 +201,10 @@ 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)
@@ -430,7 +437,7 @@ async function switchChapter(chapter: IChapter, index: number) {
// 判断章节是否锁定
function isLocked(index: number): boolean {
return !isBuy.value && 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

@@ -122,14 +122,6 @@ const loadChapterDetail = async () => {
}
}
/**
* 选择视频
*/
const selectVideo = async (index: number) => {
if (index === currentVideoIndex.value) return
currentVideoIndex.value = index
}
/**
* 预览图片
*/

View File

@@ -122,7 +122,7 @@
</view>
<!-- 锁定图标 -->
<view v-if="!canAccess(chapter)" class="lock-icon">
<view v-if="!canAccess(chapter) && currentCatalogue.type != 0" class="lock-icon">
<wd-icon name="lock-on" size="24px" color="#258feb" />
</view>
</view>
@@ -252,7 +252,7 @@ const canAccess = (chapter: IChapter): boolean => {
if (chapter.isAudition === 1) return true
// 免费课程可以访问
if (currentCatalogue.value.type === 0) return true
// if (currentCatalogue.value.type === 0) return true
return false
}
@@ -261,18 +261,18 @@ const canAccess = (chapter: IChapter): boolean => {
* 点击章节
*/
const handleChapterClick = (chapter: IChapter, catalogue: ICatalogue) => {
if (!isPurchased.value && currentCatalogue.value.type === 0) {
uni.showToast({
title: '请先领取课程',
icon: 'none'
})
return
}
if (!canAccess(chapter)) {
uni.showToast({
title: '请先购买课程',
icon: 'none'
})
if (currentCatalogue.value.type === 0) {
uni.showToast({
title: '请先领取课程',
icon: 'none'
})
} else {
uni.showToast({
title: '请先购买课程',
icon: 'none'
})
}
return
}

View File

@@ -3,7 +3,7 @@
<!-- 自定义导航栏 -->
<nav-bar :title="$t('news.newsDetail')" />
<web-view v-if="urlVisible" :webview-styles="{ progress: { color: '#55aaff' } }" :src="surl"></web-view>
<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
@@ -32,28 +32,27 @@ onLoad((e: any) => {
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()
}
// 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
})
// 新闻详情
@@ -102,6 +101,9 @@ const formattedContent = computed(() => formatRichText(news.value.content))
</script>
<style lang="scss" scoped>
.web-view-container {
padding-top: 60px;
}
.box {
background-color: #fff;
padding: 10px;

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

@@ -86,7 +86,7 @@
const sysStore = useSysStore()
// 默认头像
const defaultAvatar = '/static/home_icon.png'
const defaultAvatar = '/static/logo.png'
// 用户信息
const userInfo = computed(() => userStore.userInfo)
@@ -133,13 +133,13 @@
url: '/pages/user/feedback/index',
type: 'pageJump'
},
{
id: 6,
name: t('user.dataMigrate'),
url: '/pages/user/migrate/index',
desc: t('user.migrateSubtitle'),
type: 'pageJump'
}
// {
// id: 6,
// name: t('user.dataMigrate'),
// url: '/pages/user/migrate/index',
// desc: t('user.migrateSubtitle'),
// type: 'pageJump'
// }
])
/**

View File

@@ -54,6 +54,8 @@
</view>
</view>
</view>
<wd-message-box />
</view>
</template>
@@ -61,6 +63,9 @@
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()
@@ -85,14 +90,20 @@ const rules = ref({
const handleSubmit = async () => {
migrateForm.value.validate().then(({ valid, errors }: any) => {
if (valid) {
uni.showModal({
message.confirm({
title: t('global.tips'),
content: t('user.migrateWarning'),
success: (res: any) => {
if (res.confirm) {
submitMigrate()
}
}
msg: t('user.instruction2'),
}).then(() => {
message.confirm({
title: t('global.tips'),
msg: t('user.migrateWarning'),
}).then(() => {
submitMigrate()
}).catch(() => {
// 取消数据迁移
})
}).catch(() => {
// 取消数据迁移
})
}
})

View File

@@ -23,7 +23,7 @@
<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> {{ $t('global.coin') }}
<text class="text-red-500 text-lg font-bold">{{ `${order.realMoney}` }}</text> {{ order.orderType === 'point' ? 'NZ$' : $t('global.coin') }}
</wd-cell>
</wd-cell-group>
@@ -36,7 +36,7 @@
<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" />
<wd-cell title="付款时间" :value="order.paymentDate" v-if="order.paymentDate"/>
</wd-cell-group>
</view>

View File

@@ -27,7 +27,7 @@
<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>

View File

@@ -11,8 +11,8 @@
<view><wd-icon name="arrow-right" size="16px" color="#fff" /></view>
</view>
<view class="title">{{$t('order.pointsRecord')}}</view>
<view class="recharge-record-block" v-for="(item, index) in bookList" :key="index">
<view class="recharge-record-block-row">{{item.remark.slice(0, (item.remark.indexOf('')))}}<text
<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>
@@ -64,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>

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> -->
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">{{item.givejf}}</text>
<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>
@@ -100,7 +116,7 @@
const popup = ref(null)
const agreemenState = ref(false)
// 协议id
const id = ref(101)
const id = ref(116)
const richTextContent = ref('');
// 协议数据
@@ -115,7 +131,10 @@
const purchaseToken = ref()
// 订单编号
const orderSn = ref('')
// 活动充值数据
const eventAmountList = ref([])
//正常金额数据
const standardAmountList = ref([])
/**
* 获取使用环境
@@ -139,8 +158,11 @@
try {
rechargeList.value = await getBookBuyConfigList(type.value, qudao.value)
console.log(rechargeList.value.bookBuyConfigList, '充值列表');
const data = rechargeList.value.bookBuyConfigList
// 默认选择第一个金额
aloneItem.value = rechargeList.value.bookBuyConfigList[0]
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)
}
@@ -223,10 +245,10 @@
getQuerySku()
// 初始化成功
} else {
console.log('init失败了');
console.log('init失败了/谷歌商店没有登录');
uni.showToast({
title: '设备上无法使用计费服务',
icon: 'fail'
title: t('order.unusable'),
icon: 'error'
})
// 初始化失败
isConnected.value = false;
@@ -251,7 +273,7 @@
getPayAll()
uni.hideLoading()
} else {
console.log('查询失败', e);
console.log('查询失败\网络连接失败', e);
uni.showToast({
title: t('global.networkConnectionError'),
icon: 'error'
@@ -278,7 +300,7 @@
console.log(e, 'payAll方法成功返参');
getConsume()
} else {
uni.showToast({ title: t('user.paymentFailed'), icon: 'error' })
uni.showToast({ title: t('user.closeWindow'), icon: 'error' })
console.log(e, 'e');
// 支付失败
}
@@ -347,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)
}
@@ -377,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;
@@ -399,13 +421,12 @@
height: 160rpx;
.recharge_money {
font-size: 30rpx;
margin-bottom: 10rpx;
font-size: 50rpx;
font-weight: bold;
}
.recharge_give {
font-size: 20rpx;
font-size: 22rpx;
color: #FF0033;
}
}
@@ -421,7 +442,7 @@
bottom: 0;
left: 0;
right: 0;
padding: 30rpx;
padding: 20rpx;
background-color: #ffffff;
border-top: 1px solid #f0f0f0;
z-index: 999;
@@ -429,12 +450,12 @@
.recharge-button {
width: 60%;
height: 50rpx;
line-height: 50rpx;
height: 100rpx;
line-height: 100rpx;
background-color: #007bff;
color: #ffffff;
font-size: 30rpx;
border-radius: 30rpx;
font-size: 40rpx;
border-radius: 50rpx;
border: none;
}
@@ -443,7 +464,7 @@
}
.activity-container {
margin: 0 20rpx 20rpx 20rpx;
margin: 0 30rpx 20rpx 30rpx;
padding: 15rpx;
background-color: #e6f4ff;
border-radius: 8rpx;
@@ -454,7 +475,7 @@
position: absolute;
top: -10rpx;
right: -6rpx;
font-size: 16rpx;
font-size: 20rpx;
font-weight: 600;
color: #ffffff;
background-color: #007bff;
@@ -464,10 +485,11 @@
}
.cha_fangsh {
padding: 20rpx;
padding: 30rpx;
.cf_title {
font-size: 30rpx;
font-size: 45rpx;
font-weight: bold;
color: #007bff;
}
@@ -475,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;
@@ -508,9 +530,9 @@
.agree_wo {
display: flex;
padding: 20rpx 20rpx 160rpx 20rpx;
padding: 20rpx 20rpx 180rpx 20rpx;
color: #aaa;
font-size: 26rpx;
font-size: 30rpx;
align-items: center;
.highlight {
@@ -541,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') }
])
// 获取当前语言名称
@@ -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

@@ -14,7 +14,7 @@
<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" 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>
@@ -70,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>

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">

View File

@@ -211,27 +211,15 @@
max-width: 96rem;
}
}
.mb-2 {
margin-bottom: calc(var(--spacing) * 2);
}
.mb-2\! {
margin-bottom: calc(var(--spacing) * 2) !important;
}
.mb-\[20rpx\] {
margin-bottom: 20rpx;
}
.mb-\[20rpx\]\! {
margin-bottom: 20rpx !important;
}
.ml-1 {
margin-left: calc(var(--spacing) * 1);
}
.ml-1\! {
margin-left: calc(var(--spacing) * 1) !important;
}
.ml-2 {
margin-left: calc(var(--spacing) * 2);
}
.ml-2\.5\! {
margin-left: calc(var(--spacing) * 2.5) !important;
}
@@ -268,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,);
}
@@ -284,9 +269,6 @@
border-style: var(--tw-border-style);
border-width: 1px;
}
.p-0 {
padding: calc(var(--spacing) * 0);
}
.p-0\! {
padding: calc(var(--spacing) * 0) !important;
}
@@ -305,12 +287,6 @@
.pt-\[10rpx\] {
padding-top: 10rpx;
}
.pt-\[20rpx\] {
padding-top: 20rpx;
}
.pb-0 {
padding-bottom: calc(var(--spacing) * 0);
}
.pb-0\! {
padding-bottom: calc(var(--spacing) * 0) !important;
}
@@ -342,9 +318,6 @@
.text-\[\#7dc1f0\] {
color: #7dc1f0;
}
.text-\[14rpx\] {
color: 14rpx;
}
.text-\[cadetblue\] {
color: cadetblue;
}
@@ -364,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

@@ -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;
}

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
})