Compare commits

7 Commits

25 changed files with 1233 additions and 217 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

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

@@ -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))
}
/**

View File

@@ -226,8 +226,8 @@
"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."
"instruction4": "If you encounter any issues, please contact customer service for assistance.",
"closeWindow": "Close the payment pop-up window"
},
"book": {
"title": "My Books",

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 {
'zh-Hans': zhHans,
en
'zh-Hans': zhHans
}

View File

@@ -227,7 +227,8 @@
"instruction1": "请在吴门医述、心灵空间、众妙之门、疯子读书任意APP中获取迁移验证码获取方式【我的】-【数据迁移】-【获取迁移验证码】。",
"instruction2": "数据迁移完成后旧账号数据将被清空已购买的天医币、积分、课程、电子书、VIP、证书、湖分将转移到当前账号。",
"instruction3": "迁移过程可能需要几分钟时间,请耐心等待。",
"instruction4": "如遇到问题,请联系客服获取帮助。"
"instruction4": "如遇到问题,请联系客服获取帮助。",
"closeWindow": "关闭支付弹窗"
},
"book": {
"title": "我的书单",

View File

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

View File

@@ -3,11 +3,6 @@
<!-- 导航栏 -->
<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" />
@@ -25,12 +20,13 @@
</wd-button>
</view>
<view class="divider-line" />
<!-- 章节列表 -->
<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>
</view>
<!-- 购买弹窗 -->
<GoodsSelector
@@ -62,7 +58,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { t } from '@/utils/i18n'
import { bookApi } from '@/api/modules/book'
import { useUserStore } from '@/stores/user'
@@ -91,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) => {
@@ -102,11 +97,12 @@ onLoad((options: any) => {
fromIndex.value = Number(options.index)
activeIndex.value = fromIndex.value
}
loadGoodsInfo()
})
initScrollHeight()
onShow(() => {
loadBookInfo()
loadChapterList()
loadGoodsInfo()
})
// 购买弹窗状态
@@ -131,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)
@@ -212,9 +193,9 @@ function goToPurchase() {
.listen-page {
background: #f7faf9;
min-height: 100vh;
}
.listen-scroll {
.book-info {
.book-info {
margin: 20rpx;
padding: 30rpx;
background: #fff;
@@ -251,14 +232,9 @@ function goToPurchase() {
color: #666;
}
}
}
}
.divider-line {
height: 20rpx;
background: #f7faf9;
}
.chapter-section {
.chapter-section {
background: #fff;
padding: 30rpx;
min-height: 400rpx;
@@ -311,7 +287,5 @@ function goToPurchase() {
font-size: 28rpx;
color: #999;
}
}
}
}
</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

@@ -86,7 +86,7 @@
const sysStore = useSysStore()
// 默认头像
const defaultAvatar = '/static/home_icon.png'
const defaultAvatar = '/static/logo.png'
// 用户信息
const userInfo = computed(() => userStore.userInfo)

View File

@@ -2,16 +2,31 @@
<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">
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>
<!-- 红框位置的618活动标签 -->
<!-- <view class="activity-tag">618活动</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>
@@ -116,7 +131,10 @@
const purchaseToken = ref()
// 订单编号
const orderSn = ref('')
// 活动充值数据
const eventAmountList = ref([])
//正常金额数据
const standardAmountList = ref([])
/**
* 获取使用环境
@@ -140,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)
}
@@ -279,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');
// 支付失败
}

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

@@ -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,18 +245,29 @@ 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) => {
const tempFiles = res.filePaths.map((item: any) => ({
path: item
}))
resolve(formatImage({ tempFiles }))
},
fail: reject
})
// #endif
break
case 'video':
// #ifdef MP-WEIXIN
@@ -269,18 +281,29 @@ 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
@@ -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,
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
})