首次完整推送,
V:1.20240808.006
This commit is contained in:
98
uni_modules/uni-upgrade-center-app/changelog.md
Normal file
98
uni_modules/uni-upgrade-center-app/changelog.md
Normal file
@ -0,0 +1,98 @@
|
||||
## 0.8.1(2024-04-28)
|
||||
- 修复 在 HX 4.0.3+ uni-app x 项目运行到 Android 调不起安装的Bug
|
||||
## 0.8.0(2024-04-15)
|
||||
- 修复 更新弹窗 data 中新增初始化字段
|
||||
## 0.7.9(2024-03-15)
|
||||
- 移除无用代码
|
||||
- 调整 is_silently 类型为可为 null
|
||||
## 0.7.8(2024-01-04)
|
||||
- 新增 移除无用代码
|
||||
## 0.7.7(2024-01-04)
|
||||
- 新增 uni-app x 项目中新增 @show 回调
|
||||
## 0.7.6(2023-12-21)
|
||||
- 修复 iOS使用升级中心云打包时报错(使用新版的 [uts-progressNotification](https://ext.dcloud.net.cn/plugin?name=uts-progressNotification) 插件,如果之前下载过请删除 `uts-progressNotification\utssdk\app-ios` 文件夹)
|
||||
## 0.7.5(2023-12-12)
|
||||
- 新增 通知栏进度条使用 uts-progressNotification 插件
|
||||
- 新增 依赖 uni-installApk、uts-progressNotification。使用前要安装插件三方依赖
|
||||
## 0.7.4(2023-11-29)
|
||||
- 修复 uni-app-x 项目中由上版引发的无法升级的Bug
|
||||
## 0.7.3(2023-11-27)
|
||||
- 修复 在 uni-app x 中无更新时报错的Bug
|
||||
## 0.7.2(2023-11-20)
|
||||
- 新增 插件根目录 utils 文件夹中新增 check-update-nvue.js 文件(vue2 的 nvue 页面请引用该文件)
|
||||
## 0.7.1(2023-11-17)
|
||||
- 修复 运行至浏览器 ts 语法报错
|
||||
## 0.7.0(2023-11-10)
|
||||
- 新增 兼容 uni-app x 项目 [详情](https://uniapp.dcloud.net.cn/uniCloud/upgrade-center.html)
|
||||
## 0.6.5(2023-10-27)
|
||||
- 修复 安装 wgt 报错 manifest.json 文件不存在的Bug
|
||||
## 0.6.4(2023-09-01)
|
||||
chore: 优化代码结构
|
||||
## 0.6.3(2023-08-30)
|
||||
- 修复 下载 wgt 时如果后缀名不正确,重命名后安装
|
||||
## 0.6.2(2022-11-21)
|
||||
- 处理 cloudfunctions 目录
|
||||
## 0.6.1(2022-08-17)
|
||||
- 修复 后台添加应用市场,但都没有启用的情况下报错的Bug (需要 uni-admin 1.9.3+)
|
||||
## 0.6.0(2022-07-19)
|
||||
- 新增 支持多应用商店配置(需要 uni-admin 1.9.3+)
|
||||
## 0.4.1(2022-05-27)
|
||||
- 修复 上版引出的报错问题
|
||||
## 0.4.0(2022-05-27)
|
||||
- 新增 Android 支持跳转手机自带商店,填写升级包地址时请填写跳转商店链接
|
||||
- 新增 改为云对象调用方式,使用更直观
|
||||
## 0.3.3(2022-04-14)
|
||||
- 修复 调用 check-update,当 code 为 0 时没有回调
|
||||
## 0.3.2(2022-01-12)
|
||||
- 优化显示逻辑
|
||||
## 0.3.1(2021-11-24)
|
||||
- 修复 vue3 上图片不显示的Bug
|
||||
## 0.3.0(2021-11-18)
|
||||
- 移除 wgt 安装成功后提示,防止重启过快弹框不消失
|
||||
## 0.2.2(2021-08-25)
|
||||
- 兼容vue3.0
|
||||
## 0.2.1(2021-07-26)
|
||||
- 修复 使用腾讯云并手动填写地址时,导致下载链接失效的bug
|
||||
## 0.2.0(2021-07-13)
|
||||
- 更新文档 关于报错local_storage_key 为空,请不要将页面路径设置为pages.json中第一项
|
||||
## 0.1.9(2021-06-28)
|
||||
- 更新文档
|
||||
- 修复 wgt安装失败时,按钮状态不对
|
||||
## 0.1.8(2021-06-16)
|
||||
- 修复 跳转安装时,导致上次下载的apk还没安装就被删掉的bug
|
||||
## 0.1.7(2021-06-03)
|
||||
- 修改 移除static中的图片
|
||||
## 0.1.6(2021-06-03)
|
||||
- 修改 下载更新按钮使用CSS渐变色
|
||||
## 0.1.5(2021-04-22)
|
||||
- 更新check-update函数。现在返回一个Promise,有更新时成功回调,其他情况错误回调
|
||||
## 0.1.4(2021-04-13)
|
||||
- 更新文档。明确云函数调用结果
|
||||
## 0.1.3(2021-04-13)
|
||||
- 解耦云函数与弹框处理。utils中新增 call-check-version.js,可用于单独检测是否有更新
|
||||
## 0.1.2(2021-04-07)
|
||||
- 更新版本对比函数 compare
|
||||
## 0.1.1(2021-04-07)
|
||||
- 修复 腾讯云空间下载链接不能下载问题
|
||||
## 0.1.0(2021-04-07)
|
||||
- 新增使用uni.showModal提示升级示例
|
||||
- 修改iOS升级提示方式
|
||||
## 0.0.7(2021-04-02)
|
||||
- 修复在iOS上打开弹框报错
|
||||
## 0.0.6(2021-04-01)
|
||||
- 兼容旧版本安卓
|
||||
## 0.0.5(2021-04-01)
|
||||
- 修复低版本安卓上进度条错位
|
||||
## 0.0.4(2021-04-01)
|
||||
- 更新readme
|
||||
- 修复check-update语法错误
|
||||
## 0.0.3(2021-04-01)
|
||||
- 新增前台更新弹框,详见readme
|
||||
- 更新前台检查更新方法
|
||||
|
||||
## 0.0.2(2021-03-29)
|
||||
- 更新文档
|
||||
- 移除 dependencies
|
||||
|
||||
## 0.0.1(2021-03-25)
|
||||
- 升级中心前台检查更新
|
@ -0,0 +1,485 @@
|
||||
<template>
|
||||
<view v-show="shown" class="mask flex-center">
|
||||
<view class="content">
|
||||
|
||||
<view class="content-top">
|
||||
<text class="content-top-text">{{title}}</text>
|
||||
<image class="content-top-image" mode="widthFix"
|
||||
src="/uni_modules/uni-upgrade-center-app/static/bg_top.png"></image>
|
||||
</view>
|
||||
|
||||
<view class="content-body">
|
||||
<view class="content-body-title">
|
||||
<text class="text title">{{subTitle}}</text>
|
||||
<text class="text version">v{{version}}</text>
|
||||
</view>
|
||||
<view class="body">
|
||||
<scroll-view class="box-des-scroll" scroll-y="true">
|
||||
<text class="text box-des">
|
||||
{{contents}}
|
||||
</text>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="footer flex-center">
|
||||
<template v-if="isiOS">
|
||||
<button class="content-button" style="border: none;color: #fff;" plain @click="jumpToAppStore">
|
||||
{{downLoadBtnTextiOS}}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="!downloadSuccess">
|
||||
<view class="progress-box flex-column" v-if="downloading">
|
||||
<progress class="progress" :percent="downLoadPercent" activeColor="#3DA7FF" :show-info="true" :stroke-width="10" />
|
||||
<view style="width:100%;display: flex;justify-content: space-around;flex-direction: row;">
|
||||
<text class="text" style="font-size: 14px;">{{downLoadingText}}</text>
|
||||
<text class="text" style="font-size: 14px;">({{downloadedSize}}/{{packageFileSize}}M)</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button v-else class="content-button" @click="updateApp">
|
||||
{{downLoadBtnText}}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<button v-else-if="downloadSuccess && !installed" class="content-button" :loading="installing" :disabled="installing" @click="installPackage">
|
||||
{{installing ? '正在安装……' : '下载完成,立即安装'}}
|
||||
</button>
|
||||
|
||||
<button v-else-if="installed" class="content-button" @click="installPackage">
|
||||
安装未完成,点击安装
|
||||
</button>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="content-bottom">
|
||||
<image v-if="!is_mandatory" class="close-img" mode="widthFix" src="/uni_modules/uni-upgrade-center-app/static/app_update_close.png" @click="closeUpdate">
|
||||
</image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { openSchema as utsOpenSchema } from '@/uni_modules/uts-openSchema'
|
||||
import { createNotificationProgress, cancelNotificationProgress, finishNotificationProgress } from '@/uni_modules/uts-progressNotification'
|
||||
import { type CreateNotificationProgressOptions, type FinishNotificationProgressOptions } from '@/uni_modules/uts-progressNotification/utssdk/interface.uts'
|
||||
import { UniUpgradeCenterResult, StoreListItem } from '../../utils/call-check-version'
|
||||
|
||||
const platform_iOS = 'iOS';
|
||||
const platform_Android = 'Android';
|
||||
const requiredKey = ['version', 'url', 'type']
|
||||
let downloadTask : DownloadTask | null = null;
|
||||
let openSchemePromise: Promise<boolean> | null = null;
|
||||
|
||||
const openSchema = (url: string): Promise<boolean> => new Promise<boolean>((resolve,reject) =>{
|
||||
try{
|
||||
utsOpenSchema(url)
|
||||
resolve(true)
|
||||
}catch(e){
|
||||
reject(false)
|
||||
}
|
||||
})
|
||||
|
||||
export default {
|
||||
emits: ['close', 'show'],
|
||||
data() {
|
||||
return {
|
||||
shown: false,
|
||||
// 从之前下载安装
|
||||
installForBeforeFilePath: '',
|
||||
|
||||
// 安装
|
||||
installed: false,
|
||||
installing: false,
|
||||
|
||||
// 下载
|
||||
downloadSuccess: false,
|
||||
downloading: false,
|
||||
|
||||
downLoadPercent: 0,
|
||||
downloadedSize: 0,
|
||||
packageFileSize: 0,
|
||||
|
||||
tempFilePath: '', // 要安装的本地包地址
|
||||
|
||||
// 默认安装包信息
|
||||
title: '更新日志',
|
||||
contents: '',
|
||||
version: '',
|
||||
is_mandatory: false,
|
||||
url: "",
|
||||
platform: [] as string[],
|
||||
store_list: null as StoreListItem[] | null,
|
||||
|
||||
// 可自定义属性
|
||||
subTitle: '发现新版本',
|
||||
downLoadBtnTextiOS: '立即跳转更新',
|
||||
downLoadBtnText: '立即下载更新',
|
||||
downLoadingText: '安装包下载中,请稍后'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isiOS() : boolean {
|
||||
return this.platform.includes(platform_iOS);
|
||||
},
|
||||
isAndroid() : boolean {
|
||||
return this.platform.includes(platform_Android);
|
||||
},
|
||||
needNotificationProgress(): boolean {
|
||||
return this.isAndroid && !this.is_mandatory
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.needNotificationProgress) {
|
||||
cancelNotificationProgress()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
jumpToAppStore() {
|
||||
openSchema(this.url)
|
||||
},
|
||||
show(shown : boolean, localPackageInfo : UniUpgradeCenterResult | null) {
|
||||
if (localPackageInfo === null) return;
|
||||
|
||||
for (let key in localPackageInfo) {
|
||||
if (requiredKey.indexOf(key) != -1 && localPackageInfo[key] === null) {
|
||||
console.error(`参数 ${key} 必填,请检查后重试`)
|
||||
uni.navigateBack()
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.title = localPackageInfo.title
|
||||
this.url = localPackageInfo.url
|
||||
this.contents = localPackageInfo.contents
|
||||
this.is_mandatory = localPackageInfo.is_mandatory
|
||||
this.platform = localPackageInfo.platform
|
||||
this.version = localPackageInfo.version
|
||||
this.store_list = localPackageInfo.store_list
|
||||
this.shown = shown
|
||||
|
||||
this.$emit('show')
|
||||
},
|
||||
askAbortDownload() {
|
||||
uni.showModal({
|
||||
title: '是否取消下载?',
|
||||
cancelText: '否',
|
||||
confirmText: '是',
|
||||
success: res => {
|
||||
if (res.confirm) {
|
||||
if (downloadTask !== null) downloadTask!.abort()
|
||||
this.closePopup()
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
closeUpdate() {
|
||||
if (this.downloading) {
|
||||
if (this.is_mandatory) {
|
||||
return uni.showToast({
|
||||
title: '下载中,请稍后……',
|
||||
icon: 'none',
|
||||
duration: 500
|
||||
})
|
||||
}
|
||||
if (!this.needNotificationProgress) {
|
||||
this.askAbortDownload()
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.closePopup()
|
||||
},
|
||||
closePopup() {
|
||||
this.shown = false
|
||||
this.downloadSuccess = false
|
||||
this.downloading = false
|
||||
this.downLoadPercent = 0
|
||||
this.downloadedSize = 0
|
||||
this.packageFileSize = 0
|
||||
this.tempFilePath = ''
|
||||
|
||||
this.installing = false
|
||||
this.installed = false
|
||||
|
||||
if (this.needNotificationProgress) cancelNotificationProgress()
|
||||
this.$emit('close')
|
||||
},
|
||||
updateApp() {
|
||||
const checkStoreScheme = this.checkStoreScheme()
|
||||
if (checkStoreScheme !== null) {
|
||||
checkStoreScheme
|
||||
.then(_ => { })
|
||||
.catch(() => { this.downloadPackage() })
|
||||
.finally(() => {
|
||||
openSchemePromise = null
|
||||
})
|
||||
} else { this.downloadPackage() }
|
||||
},
|
||||
// 跳转应用商店
|
||||
checkStoreScheme(): Promise<boolean> | null {
|
||||
if (this.store_list !== null) {
|
||||
const storeList: StoreListItem[] = this.store_list!.filter((item: StoreListItem): boolean => item.enable)
|
||||
if (storeList.length > 0) {
|
||||
if (openSchemePromise === null) {
|
||||
openSchemePromise = Promise.reject() as Promise<boolean>
|
||||
}
|
||||
storeList
|
||||
.sort((cur: StoreListItem, next: StoreListItem): number => next.priority - cur.priority)
|
||||
.map((item: StoreListItem): string => item.scheme)
|
||||
.reduce((promise: Promise<boolean>, cur: string): Promise<boolean> => {
|
||||
openSchemePromise = promise.catch((): Promise<boolean> => openSchema(cur))
|
||||
return openSchemePromise!
|
||||
}, openSchemePromise!)
|
||||
return openSchemePromise!
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
downloadPackage() {
|
||||
//下载包
|
||||
downloadTask = uni.downloadFile({
|
||||
url: this.url,
|
||||
success: res => {
|
||||
if (res.statusCode == 200) {
|
||||
this.tempFilePath = res.tempFilePath
|
||||
this.downLoadComplete()
|
||||
}
|
||||
},
|
||||
fail: err => {
|
||||
console.log('downloadFile err: ', err);
|
||||
}
|
||||
});
|
||||
if (downloadTask !== null) {
|
||||
this.downloading = true;
|
||||
if (this.needNotificationProgress) {
|
||||
this.closePopup()
|
||||
}
|
||||
downloadTask!.onProgressUpdate(res => {
|
||||
this.downLoadPercent = parseFloat(res.progress.toFixed(0));
|
||||
this.downloadedSize = parseFloat((res.totalBytesWritten / Math.pow(1024, 2)).toFixed(2));
|
||||
this.packageFileSize = parseFloat((res.totalBytesExpectedToWrite / Math.pow(1024, 2)).toFixed(2));
|
||||
|
||||
if (this.needNotificationProgress) {
|
||||
createNotificationProgress({
|
||||
title: "升级中心正在下载安装包……",
|
||||
content: `${this.downLoadPercent}%`,
|
||||
progress: this.downLoadPercent,
|
||||
onClick: () => {
|
||||
if (!this.downloadSuccess) {
|
||||
this.askAbortDownload()
|
||||
}
|
||||
}
|
||||
} as CreateNotificationProgressOptions)
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
downLoadComplete() {
|
||||
this.downloadSuccess = true;
|
||||
this.downloading = false;
|
||||
|
||||
this.downLoadPercent = 0
|
||||
this.downloadedSize = 0
|
||||
this.packageFileSize = 0
|
||||
|
||||
downloadTask = null;
|
||||
|
||||
if (this.needNotificationProgress) {
|
||||
finishNotificationProgress({
|
||||
title: "安装升级包",
|
||||
content: "下载完成",
|
||||
onClick(){}
|
||||
} as FinishNotificationProgressOptions)
|
||||
|
||||
this.installPackage();
|
||||
return
|
||||
}
|
||||
|
||||
// 强制更新,直接安装
|
||||
if (this.is_mandatory) {
|
||||
this.installPackage();
|
||||
}
|
||||
},
|
||||
installPackage() {
|
||||
this.installing = true;
|
||||
// #ifdef APP
|
||||
uni.installApk({
|
||||
filePath: this.tempFilePath,
|
||||
success: _ => {
|
||||
console.log('installApk success');
|
||||
this.installing = false;
|
||||
this.installed = true;
|
||||
},
|
||||
fail: err => {
|
||||
console.error('installApk fail', err);
|
||||
// 安装失败需要重新下载安装包
|
||||
this.installing = false;
|
||||
this.installed = false;
|
||||
|
||||
uni.showModal({
|
||||
title: '更新失败,请重新下载',
|
||||
content: `uni.installApk 错误码 ${err.errCode}`,
|
||||
showCancel: false
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 安装跳出覆盖安装,此处直接返回上一页
|
||||
if (!this.is_mandatory) {
|
||||
uni.navigateBack()
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.flex-center {
|
||||
/* #ifndef APP-NVUE | UNI-APP-X */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, .65);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
top: 0;
|
||||
width: 600rpx;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-family: Source Han Sans CN;
|
||||
}
|
||||
|
||||
.content-top {
|
||||
width: 100%;
|
||||
border-bottom-color: #fff;
|
||||
border-bottom-width: 15px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
.content-top-image {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
bottom: -18px;
|
||||
}
|
||||
|
||||
.content-top-text {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #F8F8FA;
|
||||
position: absolute;
|
||||
width: 65%;
|
||||
top: 77.5px;
|
||||
left: 25px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.content-body {
|
||||
box-sizing: border-box;
|
||||
padding: 0 25px;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border-bottom-left-radius: 15px;
|
||||
border-bottom-right-radius: 15px;
|
||||
}
|
||||
|
||||
.content-body-title {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content-body-title .version {
|
||||
padding-left: 10px;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
margin-left: 5px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 10px;
|
||||
background: #50aefd;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #3DA7FF;
|
||||
line-height: 38px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 75px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.box-des-scroll {
|
||||
box-sizing: border-box;
|
||||
padding: 0 15px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.box-des {
|
||||
font-size: 13px;
|
||||
color: #000000;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.progress-box {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress {
|
||||
width: 90%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.content-bottom {
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
.close-img {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
z-index: 1000;
|
||||
position: relative;
|
||||
bottom: -25px;
|
||||
left: 132px;
|
||||
}
|
||||
|
||||
.content-button {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
color: #fff;
|
||||
|
||||
text-align: center;
|
||||
|
||||
background-color: #1785ff;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
BIN
uni_modules/uni-upgrade-center-app/images/app_update_close.png
Normal file
BIN
uni_modules/uni-upgrade-center-app/images/app_update_close.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.5 KiB |
BIN
uni_modules/uni-upgrade-center-app/images/bg_top.png
Normal file
BIN
uni_modules/uni-upgrade-center-app/images/bg_top.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
86
uni_modules/uni-upgrade-center-app/package.json
Normal file
86
uni_modules/uni-upgrade-center-app/package.json
Normal file
@ -0,0 +1,86 @@
|
||||
{
|
||||
"id": "uni-upgrade-center-app",
|
||||
"displayName": "升级中心 uni-upgrade-center - App",
|
||||
"version": "0.8.1",
|
||||
"description": "uni升级中心 - 客户端检查更新",
|
||||
"keywords": [
|
||||
"uniCloud",
|
||||
"update",
|
||||
"升级",
|
||||
"wgt"
|
||||
],
|
||||
"repository": "https://gitee.com/dcloud/uni-upgrade-center/tree/master/uni_modules/uni-upgrade-center-app",
|
||||
"engines": {
|
||||
"HBuilderX": "^4.03"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "插件不采集任何数据",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "",
|
||||
"type": "unicloud-template-page"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
"uni-installApk",
|
||||
"uts-progressNotification",
|
||||
"uts-openSchema"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "y",
|
||||
"aliyun": "y",
|
||||
"alipay": "y"
|
||||
},
|
||||
"client": {
|
||||
"App": {
|
||||
"app-vue": "y",
|
||||
"app-nvue": "u"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "y",
|
||||
"Android Browser": "y",
|
||||
"微信浏览器(Android)": "y",
|
||||
"QQ浏览器(Android)": "y"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "y",
|
||||
"IE": "y",
|
||||
"Edge": "y",
|
||||
"Firefox": "y",
|
||||
"Safari": "y"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "u",
|
||||
"阿里": "u",
|
||||
"百度": "u",
|
||||
"字节跳动": "u",
|
||||
"QQ": "u",
|
||||
"京东": "u"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "u",
|
||||
"联盟": "u"
|
||||
},
|
||||
"Vue": {
|
||||
"vue2": "y",
|
||||
"vue3": "y"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
611
uni_modules/uni-upgrade-center-app/pages/upgrade-popup.vue
Normal file
611
uni_modules/uni-upgrade-center-app/pages/upgrade-popup.vue
Normal file
@ -0,0 +1,611 @@
|
||||
<template>
|
||||
<view class="mask flex-center">
|
||||
<view class="content botton-radius">
|
||||
<view class="content-top">
|
||||
<text class="content-top-text">{{title}}</text>
|
||||
<image class="content-top" style="top: 0;" width="100%" height="100%" src="/uni_modules/uni-upgrade-center-app/static/bg_top.png">
|
||||
</image>
|
||||
</view>
|
||||
<view class="content-header"></view>
|
||||
<view class="content-body">
|
||||
<view class="title">
|
||||
<text>{{subTitle}}</text>
|
||||
<text class="content-body-version">{{version}}</text>
|
||||
</view>
|
||||
<view class="body">
|
||||
<scroll-view class="box-des-scroll" scroll-y="true">
|
||||
<text class="box-des">
|
||||
{{contents}}
|
||||
</text>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="footer flex-center">
|
||||
<template v-if="isAppStore">
|
||||
<button class="content-button" style="border: none;color: #fff;" plain @click="jumpToAppStore">
|
||||
{{downLoadBtnTextiOS}}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="!downloadSuccess">
|
||||
<view class="progress-box flex-column" v-if="downloading">
|
||||
<progress class="progress" :percent="downLoadPercent" activeColor="#3DA7FF" show-info
|
||||
stroke-width="10"/>
|
||||
<view style="width:100%;font-size: 28rpx;display: flex;justify-content: space-around;">
|
||||
<text>{{downLoadingText}}</text>
|
||||
<text>({{downloadedSize}}/{{packageFileSize}}M)</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button v-else class="content-button" style="border: none;color: #fff;" plain @click="updateApp">
|
||||
{{downLoadBtnText}}
|
||||
</button>
|
||||
</template>
|
||||
<button v-else-if="downloadSuccess && !installed" class="content-button" style="border: none;color: #fff;"
|
||||
plain :loading="installing" :disabled="installing" @click="installPackage">
|
||||
{{installing ? '正在安装……' : '下载完成,立即安装'}}
|
||||
</button>
|
||||
<button v-else-if="installed && !isWGT" class="content-button" style="border: none;color: #fff;"
|
||||
plain :loading="installing" :disabled="installing" @click="installPackage">
|
||||
安装未完成,点击安装
|
||||
</button>
|
||||
|
||||
<button v-else-if="installed && isWGT" class="content-button" style="border: none;color: #fff;" plain
|
||||
@click="restart">
|
||||
安装完毕,点击重启
|
||||
</button>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<image v-if="!is_mandatory" class="close-img" src="/uni_modules/uni-upgrade-center-app/static/app_update_close.png" @click.stop="closeUpdate">
|
||||
</image>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// #ifdef APP
|
||||
import { createNotificationProgress, cancelNotificationProgress, finishNotificationProgress } from '@/uni_modules/uts-progressNotification'
|
||||
// #endif
|
||||
const localFilePathKey = 'UNI_ADMIN_UPGRADE_CENTER_LOCAL_FILE_PATH'
|
||||
const platform_iOS = 'iOS';
|
||||
const platform_Android = 'Android';
|
||||
let downloadTask = null;
|
||||
let openSchemePromise
|
||||
|
||||
/**
|
||||
* 对比版本号,如需要,请自行修改判断规则
|
||||
* 支持比对 ("3.0.0.0.0.1.0.1", "3.0.0.0.0.1") ("3.0.0.1", "3.0") ("3.1.1", "3.1.1.1") 之类的
|
||||
* @param {Object} v1
|
||||
* @param {Object} v2
|
||||
* v1 > v2 return 1
|
||||
* v1 < v2 return -1
|
||||
* v1 == v2 return 0
|
||||
*/
|
||||
function compare(v1 = '0', v2 = '0') {
|
||||
v1 = String(v1).split('.')
|
||||
v2 = String(v2).split('.')
|
||||
const minVersionLens = Math.min(v1.length, v2.length);
|
||||
|
||||
let result = 0;
|
||||
for (let i = 0; i < minVersionLens; i++) {
|
||||
const curV1 = Number(v1[i])
|
||||
const curV2 = Number(v2[i])
|
||||
|
||||
if (curV1 > curV2) {
|
||||
result = 1
|
||||
break;
|
||||
} else if (curV1 < curV2) {
|
||||
result = -1
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (result === 0 && (v1.length !== v2.length)) {
|
||||
const v1BiggerThenv2 = v1.length > v2.length;
|
||||
const maxLensVersion = v1BiggerThenv2 ? v1 : v2;
|
||||
for (let i = minVersionLens; i < maxLensVersion.length; i++) {
|
||||
const curVersion = Number(maxLensVersion[i])
|
||||
if (curVersion > 0) {
|
||||
v1BiggerThenv2 ? result = 1 : result = -1
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
// 从之前下载安装
|
||||
installForBeforeFilePath: '',
|
||||
|
||||
// 安装
|
||||
installed: false,
|
||||
installing: false,
|
||||
|
||||
// 下载
|
||||
downloadSuccess: false,
|
||||
downloading: false,
|
||||
|
||||
downLoadPercent: 0,
|
||||
downloadedSize: 0,
|
||||
packageFileSize: 0,
|
||||
|
||||
tempFilePath: '', // 要安装的本地包地址
|
||||
|
||||
// 默认安装包信息
|
||||
title: '更新日志',
|
||||
contents: '',
|
||||
version: '',
|
||||
is_mandatory: false,
|
||||
url: '',
|
||||
platform: [],
|
||||
store_list: null,
|
||||
|
||||
// 可自定义属性
|
||||
subTitle: '发现新版本',
|
||||
downLoadBtnTextiOS: '立即跳转更新',
|
||||
downLoadBtnText: '立即下载更新',
|
||||
downLoadingText: '安装包下载中,请稍后'
|
||||
}
|
||||
},
|
||||
onLoad({
|
||||
local_storage_key
|
||||
}) {
|
||||
if (!local_storage_key) {
|
||||
console.error('local_storage_key为空,请检查后重试')
|
||||
uni.navigateBack()
|
||||
return;
|
||||
};
|
||||
|
||||
const localPackageInfo = uni.getStorageSync(local_storage_key);
|
||||
if (!localPackageInfo) {
|
||||
console.error('安装包信息为空,请检查后重试')
|
||||
uni.navigateBack()
|
||||
return;
|
||||
};
|
||||
|
||||
const requiredKey = ['version', 'url', 'type']
|
||||
for (let key in localPackageInfo) {
|
||||
if (requiredKey.indexOf(key) !== -1 && !localPackageInfo[key]) {
|
||||
console.error(`参数 ${key} 必填,请检查后重试`)
|
||||
uni.navigateBack()
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(this, localPackageInfo)
|
||||
this.checkLocalStoragePackage()
|
||||
},
|
||||
onBackPress() {
|
||||
// 强制更新不允许返回
|
||||
if (this.is_mandatory) return true
|
||||
if (!this.needNotificationProgress) downloadTask && downloadTask.abort()
|
||||
},
|
||||
onHide() {
|
||||
openSchemePromise = null
|
||||
},
|
||||
computed: {
|
||||
isWGT() {
|
||||
return this.type === 'wgt'
|
||||
},
|
||||
isiOS() {
|
||||
return !this.isWGT ? this.platform.indexOf(platform_iOS) !== -1 : false;
|
||||
},
|
||||
isAndroid() {
|
||||
return this.platform.indexOf(platform_Android) !== -1
|
||||
},
|
||||
isAppStore() {
|
||||
return this.isiOS || (!this.isiOS && !this.isWGT && this.url.indexOf('.apk') === -1)
|
||||
},
|
||||
needNotificationProgress() {
|
||||
return this.platform.indexOf(platform_iOS) === -1 && !this.is_mandatory
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkLocalStoragePackage() {
|
||||
// 如果已经有下载好的包,则直接提示安装
|
||||
const localFilePathRecord = uni.getStorageSync(localFilePathKey)
|
||||
if (localFilePathRecord) {
|
||||
const {
|
||||
version,
|
||||
savedFilePath,
|
||||
installed
|
||||
} = localFilePathRecord
|
||||
|
||||
// 比对版本
|
||||
if (!installed && compare(version, this.version) === 0) {
|
||||
this.downloadSuccess = true;
|
||||
this.installForBeforeFilePath = savedFilePath;
|
||||
this.tempFilePath = savedFilePath
|
||||
} else {
|
||||
// 如果保存的包版本小 或 已安装过,则直接删除
|
||||
this.deleteSavedFile(savedFilePath)
|
||||
}
|
||||
}
|
||||
},
|
||||
askAbortDownload() {
|
||||
uni.showModal({
|
||||
title: '是否取消下载?',
|
||||
cancelText: '否',
|
||||
confirmText: '是',
|
||||
success: res => {
|
||||
if (res.confirm) {
|
||||
downloadTask && downloadTask.abort()
|
||||
cancelNotificationProgress()
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
async closeUpdate() {
|
||||
if (this.downloading) {
|
||||
if (this.is_mandatory) {
|
||||
return uni.showToast({
|
||||
title: '下载中,请稍后……',
|
||||
icon: 'none',
|
||||
duration: 500
|
||||
})
|
||||
}
|
||||
if (!this.needNotificationProgress) {
|
||||
this.askAbortDownload()
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.needNotificationProgress && this.downloadSuccess && this.tempFilePath) {
|
||||
// 包已经下载完毕,稍后安装,将包保存在本地
|
||||
await this.saveFile(this.tempFilePath, this.version)
|
||||
}
|
||||
|
||||
uni.navigateBack()
|
||||
},
|
||||
updateApp() {
|
||||
this.checkStoreScheme()
|
||||
.catch(() => {
|
||||
this.downloadPackage()
|
||||
})
|
||||
.finally(() => {
|
||||
openSchemePromise = null
|
||||
})
|
||||
},
|
||||
// 跳转应用商店
|
||||
checkStoreScheme() {
|
||||
const storeList = (this.store_list || []).filter(item => item.enable)
|
||||
if (storeList && storeList.length) {
|
||||
storeList
|
||||
.sort((cur, next) => next.priority - cur.priority)
|
||||
.map(item => item.scheme)
|
||||
.reduce((promise, cur, curIndex) => {
|
||||
openSchemePromise = (promise || (promise = Promise.reject())).catch(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
plus.runtime.openURL(cur, (err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
})
|
||||
return openSchemePromise
|
||||
}, openSchemePromise)
|
||||
return openSchemePromise
|
||||
}
|
||||
|
||||
return Promise.reject()
|
||||
},
|
||||
downloadPackage() {
|
||||
this.downloading = true;
|
||||
|
||||
//下载包
|
||||
downloadTask = uni.downloadFile({
|
||||
url: this.url,
|
||||
success: res => {
|
||||
if (res.statusCode == 200) {
|
||||
// fix: wgt 文件下载完成后后缀不是 wgt
|
||||
if (this.isWGT && res.tempFilePath.split('.').slice(-1)[0] !== 'wgt') {
|
||||
const failCallback = (e) => {
|
||||
console.log('[FILE RENAME FAIL]:', JSON.stringify(e));
|
||||
}
|
||||
plus.io.resolveLocalFileSystemURL(res.tempFilePath, (entry) => {
|
||||
entry.getParent((parent) => {
|
||||
const newName = `new_wgt_${Date.now()}.wgt`
|
||||
entry.copyTo(parent, newName, (res) => {
|
||||
this.tempFilePath = res.fullPath
|
||||
this.downLoadComplete()
|
||||
}, failCallback)
|
||||
}, failCallback)
|
||||
}, failCallback);
|
||||
} else {
|
||||
this.tempFilePath = res.tempFilePath
|
||||
this.downLoadComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
downloadTask.onProgressUpdate(res => {
|
||||
this.downLoadPercent = res.progress;
|
||||
this.downloadedSize = (res.totalBytesWritten / Math.pow(1024, 2)).toFixed(2);
|
||||
this.packageFileSize = (res.totalBytesExpectedToWrite / Math.pow(1024, 2)).toFixed(2);
|
||||
|
||||
if (this.needNotificationProgress && !this.downloadSuccess) {
|
||||
createNotificationProgress({
|
||||
title: "升级中心正在下载安装包……",
|
||||
content: `${this.downLoadPercent}%`,
|
||||
progress: this.downLoadPercent,
|
||||
onClick: () => {
|
||||
this.askAbortDownload()
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
if (this.needNotificationProgress) {
|
||||
uni.navigateBack()
|
||||
}
|
||||
},
|
||||
downLoadComplete() {
|
||||
this.downloadSuccess = true;
|
||||
this.downloading = false;
|
||||
|
||||
this.downLoadPercent = 0
|
||||
this.downloadedSize = 0
|
||||
this.packageFileSize = 0
|
||||
|
||||
downloadTask = null;
|
||||
|
||||
if (this.needNotificationProgress) {
|
||||
finishNotificationProgress({
|
||||
title: "安装升级包",
|
||||
content: "下载完成"
|
||||
})
|
||||
|
||||
this.installPackage();
|
||||
return
|
||||
}
|
||||
|
||||
// 强制更新,直接安装
|
||||
if (this.is_mandatory) {
|
||||
this.installPackage();
|
||||
}
|
||||
},
|
||||
installPackage() {
|
||||
// #ifdef APP-PLUS
|
||||
// wgt资源包安装
|
||||
if (this.isWGT) {
|
||||
this.installing = true;
|
||||
}
|
||||
plus.runtime.install(this.tempFilePath, {
|
||||
force: false
|
||||
}, async res => {
|
||||
this.installing = false;
|
||||
this.installed = true;
|
||||
|
||||
// wgt包,安装后会提示 安装成功,是否重启
|
||||
if (this.isWGT) {
|
||||
// 强制更新安装完成重启
|
||||
if (this.is_mandatory) {
|
||||
uni.showLoading({
|
||||
icon: 'none',
|
||||
title: '安装成功,正在重启……'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
this.restart();
|
||||
}, 1000)
|
||||
}
|
||||
} else {
|
||||
const localFilePathRecord = uni.getStorageSync(localFilePathKey)
|
||||
uni.setStorageSync(localFilePathKey, {
|
||||
...localFilePathRecord,
|
||||
installed: true
|
||||
})
|
||||
}
|
||||
}, async err => {
|
||||
// 如果是安装之前的包,安装失败后删除之前的包
|
||||
if (this.installForBeforeFilePath) {
|
||||
await this.deleteSavedFile(this.installForBeforeFilePath)
|
||||
this.installForBeforeFilePath = '';
|
||||
}
|
||||
|
||||
// 安装失败需要重新下载安装包
|
||||
this.installing = false;
|
||||
this.installed = false;
|
||||
|
||||
uni.showModal({
|
||||
title: '更新失败,请重新下载',
|
||||
content: err.message,
|
||||
showCancel: false
|
||||
});
|
||||
});
|
||||
|
||||
// 非wgt包,安装跳出覆盖安装,此处直接返回上一页
|
||||
if (!this.isWGT && !this.is_mandatory) {
|
||||
uni.navigateBack()
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
restart() {
|
||||
this.installed = false;
|
||||
// #ifdef APP-PLUS
|
||||
//更新完重启app
|
||||
plus.runtime.restart();
|
||||
// #endif
|
||||
},
|
||||
saveFile(tempFilePath, version) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.saveFile({
|
||||
tempFilePath,
|
||||
success({
|
||||
savedFilePath
|
||||
}) {
|
||||
uni.setStorageSync(localFilePathKey, {
|
||||
version,
|
||||
savedFilePath
|
||||
})
|
||||
},
|
||||
complete() {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteSavedFile(filePath) {
|
||||
uni.removeStorageSync(localFilePathKey)
|
||||
return uni.removeSavedFile({
|
||||
filePath
|
||||
})
|
||||
},
|
||||
jumpToAppStore() {
|
||||
plus.runtime.openURL(this.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
page {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, .65);
|
||||
}
|
||||
|
||||
.botton-radius {
|
||||
border-bottom-left-radius: 30rpx;
|
||||
border-bottom-right-radius: 30rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
top: 0;
|
||||
width: 600rpx;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
padding: 0 50rpx;
|
||||
font-family: Source Han Sans CN;
|
||||
}
|
||||
|
||||
.text {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: block;
|
||||
/* #endif */
|
||||
line-height: 200px;
|
||||
text-align: center;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.content-top {
|
||||
position: absolute;
|
||||
top: -195rpx;
|
||||
left: 0;
|
||||
width: 600rpx;
|
||||
height: 270rpx;
|
||||
}
|
||||
|
||||
.content-top-text {
|
||||
font-size: 45rpx;
|
||||
font-weight: bold;
|
||||
color: #F8F8FA;
|
||||
position: absolute;
|
||||
top: 120rpx;
|
||||
left: 50rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
height: 70rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 33rpx;
|
||||
font-weight: bold;
|
||||
color: #3DA7FF;
|
||||
line-height: 38px;
|
||||
}
|
||||
|
||||
.content-body-version {
|
||||
padding-left: 10px;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
margin-left: 5px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 10px;
|
||||
background: #50aefd;
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 150rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.box-des-scroll {
|
||||
box-sizing: border-box;
|
||||
padding: 0 40rpx;
|
||||
height: 200rpx;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.box-des {
|
||||
font-size: 26rpx;
|
||||
color: #000000;
|
||||
line-height: 50rpx;
|
||||
}
|
||||
|
||||
.progress-box {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress {
|
||||
width: 90%;
|
||||
height: 40rpx;
|
||||
/* border-radius: 35px; */
|
||||
}
|
||||
|
||||
.close-img {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
bottom: -120rpx;
|
||||
left: calc(50% - 70rpx / 2);
|
||||
}
|
||||
|
||||
.content-button {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
font-size: 30rpx;
|
||||
font-weight: 400;
|
||||
color: #FFFFFF;
|
||||
border-radius: 40rpx;
|
||||
margin: 0 18rpx;
|
||||
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
|
||||
background: linear-gradient(to right, #1785ff, #3DA7FF);
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
1
uni_modules/uni-upgrade-center-app/readme.md
Normal file
1
uni_modules/uni-upgrade-center-app/readme.md
Normal file
@ -0,0 +1 @@
|
||||
文档已移至 [uni-upgrade-center](https://uniapp.dcloud.net.cn/uniCloud/upgrade-center.html)
|
BIN
uni_modules/uni-upgrade-center-app/static/app_update_close.png
Normal file
BIN
uni_modules/uni-upgrade-center-app/static/app_update_close.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.5 KiB |
BIN
uni_modules/uni-upgrade-center-app/static/bg_top.png
Normal file
BIN
uni_modules/uni-upgrade-center-app/static/bg_top.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
@ -0,0 +1 @@
|
||||
{}
|
@ -0,0 +1,32 @@
|
||||
export default function() {
|
||||
// #ifdef APP-PLUS
|
||||
return new Promise((resolve, reject) => {
|
||||
plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) {
|
||||
let data = {
|
||||
action: 'checkVersion',
|
||||
appid: plus.runtime.appid,
|
||||
appVersion: plus.runtime.version,
|
||||
wgtVersion: widgetInfo.version
|
||||
}
|
||||
uniCloud.callFunction({
|
||||
name: 'uni-upgrade-center',
|
||||
data,
|
||||
success: (e) => {
|
||||
console.log("e: ", e);
|
||||
resolve(e)
|
||||
},
|
||||
fail: (error) => {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
return new Promise((resolve, reject) => {
|
||||
reject({
|
||||
message: '请在App中使用'
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
}
|
120
uni_modules/uni-upgrade-center-app/utils/call-check-version.ts
Normal file
120
uni_modules/uni-upgrade-center-app/utils/call-check-version.ts
Normal file
@ -0,0 +1,120 @@
|
||||
export type StoreListItem = {
|
||||
enable : boolean
|
||||
id : string
|
||||
name : string
|
||||
scheme : string
|
||||
priority : number // 优先级
|
||||
}
|
||||
|
||||
export type UniUpgradeCenterResult = {
|
||||
_id : string
|
||||
appid : string
|
||||
name : string
|
||||
title : string
|
||||
contents : string
|
||||
url : string // 安装包下载地址
|
||||
platform : Array<string> // Array<'Android' | 'iOS'>
|
||||
version : string // 版本号 1.0.0
|
||||
uni_platform : string // "android" | "ios" // 版本号 1.0.0
|
||||
stable_publish : boolean // 是否是稳定版
|
||||
is_mandatory : boolean // 是否强制更新
|
||||
is_silently : boolean | null // 是否静默更新
|
||||
create_env : string // "upgrade-center"
|
||||
create_date : number
|
||||
message : string
|
||||
code : number
|
||||
|
||||
type : string // "native_app" | "wgt"
|
||||
store_list : StoreListItem[] | null
|
||||
min_uni_version : string | null // 升级 wgt 的最低 uni-app 版本
|
||||
}
|
||||
|
||||
export default function () : Promise<UniUpgradeCenterResult> {
|
||||
// #ifdef APP
|
||||
return new Promise<UniUpgradeCenterResult>((resolve, reject) => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
const appId = systemInfo.appId
|
||||
const appVersion = systemInfo.appVersion //systemInfo.appVersion
|
||||
// #ifndef UNI-APP-X
|
||||
if (typeof appId === 'string' && typeof appVersion === 'string' && appId.length > 0 && appVersion.length > 0) {
|
||||
plus.runtime.getProperty(appId, function (widgetInfo) {
|
||||
if (widgetInfo.version) {
|
||||
let data = {
|
||||
action: 'checkVersion',
|
||||
appid: appId,
|
||||
appVersion: appVersion,
|
||||
wgtVersion: widgetInfo.version
|
||||
}
|
||||
uniCloud.callFunction({
|
||||
name: 'uni-upgrade-center',
|
||||
data,
|
||||
success: (e) => {
|
||||
resolve(e.result as UniUpgradeCenterResult)
|
||||
},
|
||||
fail: (error) => {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
reject('widgetInfo.version is EMPTY')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
reject('plus.runtime.appid is EMPTY')
|
||||
}
|
||||
// #endif
|
||||
// #ifdef UNI-APP-X
|
||||
if (typeof appId === 'string' && typeof appVersion === 'string' && appId.length > 0 && appVersion.length > 0) {
|
||||
let data = {
|
||||
action: 'checkVersion',
|
||||
appid: appId,
|
||||
appVersion: appVersion,
|
||||
is_uniapp_x: true,
|
||||
wgtVersion: '0.0.0.0.0.1'
|
||||
}
|
||||
try {
|
||||
uniCloud.callFunction({
|
||||
name: 'uni-upgrade-center',
|
||||
data: data
|
||||
}).then(res => {
|
||||
const code = res.result['code']
|
||||
const codeIsNumber = ['Int', 'Long', 'number'].includes(typeof code)
|
||||
if (codeIsNumber) {
|
||||
if ((code as number) == 0) {
|
||||
reject({
|
||||
code: res.result['code'],
|
||||
message: res.result['message']
|
||||
})
|
||||
} else if ((code as number) < 0) {
|
||||
reject({
|
||||
code: res.result['code'],
|
||||
message: res.result['message']
|
||||
})
|
||||
} else {
|
||||
const result = JSON.parse<UniUpgradeCenterResult>(JSON.stringify(res.result)) as UniUpgradeCenterResult
|
||||
resolve(result)
|
||||
}
|
||||
}
|
||||
}).catch<void>((err : any | null) => {
|
||||
const error = err as UniCloudError
|
||||
if (error.errMsg == '未匹配到云函数[uni-upgrade-center]')
|
||||
error.errMsg = '【uni-upgrade-center-app】未配置uni-upgrade-center,无法升级。参考: https://uniapp.dcloud.net.cn/uniCloud/upgrade-center.html'
|
||||
reject(error.errMsg)
|
||||
})
|
||||
} catch (e) {
|
||||
reject(e.message)
|
||||
}
|
||||
} else {
|
||||
reject('invalid appid or appVersion')
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
return new Promise((resolve, reject) => {
|
||||
reject({
|
||||
message: '请在App中使用'
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
}
|
184
uni_modules/uni-upgrade-center-app/utils/check-update-nvue.js
Normal file
184
uni_modules/uni-upgrade-center-app/utils/check-update-nvue.js
Normal file
@ -0,0 +1,184 @@
|
||||
function callCheckVersion() {
|
||||
// #ifdef APP-PLUS
|
||||
return new Promise((resolve, reject) => {
|
||||
plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) {
|
||||
let data = {
|
||||
action: 'checkVersion',
|
||||
appid: plus.runtime.appid,
|
||||
appVersion: plus.runtime.version,
|
||||
wgtVersion: widgetInfo.version
|
||||
}
|
||||
uniCloud.callFunction({
|
||||
name: 'uni-upgrade-center',
|
||||
data,
|
||||
success: (e) => {
|
||||
resolve(e)
|
||||
},
|
||||
fail: (error) => {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
return new Promise((resolve, reject) => {})
|
||||
// #endif
|
||||
}
|
||||
|
||||
// 推荐再App.vue中使用
|
||||
const PACKAGE_INFO_KEY = '__package_info__'
|
||||
|
||||
export default function() {
|
||||
// #ifdef APP-PLUS
|
||||
return new Promise((resolve, reject) => {
|
||||
callCheckVersion().then(async (e) => {
|
||||
if (!e.result) return;
|
||||
const {
|
||||
code,
|
||||
message,
|
||||
is_silently, // 是否静默更新
|
||||
url, // 安装包下载地址
|
||||
platform, // 安装包平台
|
||||
type // 安装包类型
|
||||
} = e.result;
|
||||
|
||||
// 此处逻辑仅为实例,可自行编写
|
||||
if (code > 0) {
|
||||
// 腾讯云和阿里云下载链接不同,需要处理一下,阿里云会原样返回
|
||||
const {
|
||||
fileList
|
||||
} = await uniCloud.getTempFileURL({
|
||||
fileList: [url]
|
||||
});
|
||||
if (fileList[0].tempFileURL)
|
||||
e.result.url = fileList[0].tempFileURL;
|
||||
|
||||
resolve(e)
|
||||
|
||||
// 静默更新,只有wgt有
|
||||
if (is_silently) {
|
||||
uni.downloadFile({
|
||||
url: e.result.url,
|
||||
success: res => {
|
||||
if (res.statusCode == 200) {
|
||||
// 下载好直接安装,下次启动生效
|
||||
plus.runtime.install(res.tempFilePath, {
|
||||
force: false
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提示升级一
|
||||
* 使用 uni.showModal
|
||||
*/
|
||||
// return updateUseModal(e.result)
|
||||
|
||||
/**
|
||||
* 提示升级二
|
||||
* 官方适配的升级弹窗,可自行替换资源适配UI风格
|
||||
*/
|
||||
uni.setStorageSync(PACKAGE_INFO_KEY, e.result)
|
||||
uni.navigateTo({
|
||||
url: `/uni_modules/uni-upgrade-center-app/pages/upgrade-popup?local_storage_key=${PACKAGE_INFO_KEY}`,
|
||||
fail: (err) => {
|
||||
console.error('更新弹框跳转失败', err)
|
||||
uni.removeStorageSync(PACKAGE_INFO_KEY)
|
||||
}
|
||||
})
|
||||
|
||||
return
|
||||
} else if (code < 0) {
|
||||
// TODO 云函数报错处理
|
||||
console.error(message)
|
||||
return reject(e)
|
||||
}
|
||||
return resolve(e)
|
||||
}).catch(err => {
|
||||
// TODO 云函数报错处理
|
||||
console.error(err.message)
|
||||
reject(err)
|
||||
})
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 uni.showModal 升级
|
||||
*/
|
||||
function updateUseModal(packageInfo) {
|
||||
const {
|
||||
title, // 标题
|
||||
contents, // 升级内容
|
||||
is_mandatory, // 是否强制更新
|
||||
url, // 安装包下载地址
|
||||
platform, // 安装包平台
|
||||
type // 安装包类型
|
||||
} = packageInfo;
|
||||
|
||||
let isWGT = type === 'wgt'
|
||||
let isiOS = !isWGT ? platform.includes('iOS') : false;
|
||||
let confirmText = isiOS ? '立即跳转更新' : '立即下载更新'
|
||||
|
||||
return uni.showModal({
|
||||
title,
|
||||
content: contents,
|
||||
showCancel: !is_mandatory,
|
||||
confirmText,
|
||||
success: res => {
|
||||
if (res.cancel) return;
|
||||
|
||||
// 安装包下载
|
||||
if (isiOS) {
|
||||
plus.runtime.openURL(url);
|
||||
return;
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '后台下载中……',
|
||||
duration: 1000
|
||||
});
|
||||
|
||||
// wgt 和 安卓下载更新
|
||||
downloadTask = uni.downloadFile({
|
||||
url,
|
||||
success: res => {
|
||||
if (res.statusCode !== 200) {
|
||||
console.error('下载安装包失败', err);
|
||||
return;
|
||||
}
|
||||
// 下载好直接安装,下次启动生效
|
||||
plus.runtime.install(res.tempFilePath, {
|
||||
force: false
|
||||
}, () => {
|
||||
if (is_mandatory) {
|
||||
//更新完重启app
|
||||
plus.runtime.restart();
|
||||
return;
|
||||
}
|
||||
uni.showModal({
|
||||
title: '安装成功是否重启?',
|
||||
success: res => {
|
||||
if (res.confirm) {
|
||||
//更新完重启app
|
||||
plus.runtime.restart();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, err => {
|
||||
uni.showModal({
|
||||
title: '更新失败',
|
||||
content: err
|
||||
.message,
|
||||
showCancel: false
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
158
uni_modules/uni-upgrade-center-app/utils/check-update.js
Normal file
158
uni_modules/uni-upgrade-center-app/utils/check-update.js
Normal file
@ -0,0 +1,158 @@
|
||||
import callCheckVersion from './call-check-version'
|
||||
|
||||
// 推荐再App.vue中使用
|
||||
const PACKAGE_INFO_KEY = '__package_info__'
|
||||
|
||||
export default function() {
|
||||
// #ifdef APP-PLUS
|
||||
return new Promise((resolve, reject) => {
|
||||
callCheckVersion().then(async (e) => {
|
||||
if (!e.result) return;
|
||||
const {
|
||||
code,
|
||||
message,
|
||||
is_silently, // 是否静默更新
|
||||
url, // 安装包下载地址
|
||||
platform, // 安装包平台
|
||||
type // 安装包类型
|
||||
} = e.result;
|
||||
|
||||
// 此处逻辑仅为实例,可自行编写
|
||||
if (code > 0) {
|
||||
// 腾讯云和阿里云下载链接不同,需要处理一下,阿里云会原样返回
|
||||
const {
|
||||
fileList
|
||||
} = await uniCloud.getTempFileURL({
|
||||
fileList: [url]
|
||||
});
|
||||
if (fileList[0].tempFileURL)
|
||||
e.result.url = fileList[0].tempFileURL;
|
||||
|
||||
resolve(e)
|
||||
|
||||
// 静默更新,只有wgt有
|
||||
if (is_silently) {
|
||||
uni.downloadFile({
|
||||
url: e.result.url,
|
||||
success: res => {
|
||||
if (res.statusCode == 200) {
|
||||
// 下载好直接安装,下次启动生效
|
||||
plus.runtime.install(res.tempFilePath, {
|
||||
force: false
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提示升级一
|
||||
* 使用 uni.showModal
|
||||
*/
|
||||
// return updateUseModal(e.result)
|
||||
|
||||
/**
|
||||
* 提示升级二
|
||||
* 官方适配的升级弹窗,可自行替换资源适配UI风格
|
||||
*/
|
||||
uni.setStorageSync(PACKAGE_INFO_KEY, e.result)
|
||||
uni.navigateTo({
|
||||
url: `/uni_modules/uni-upgrade-center-app/pages/upgrade-popup?local_storage_key=${PACKAGE_INFO_KEY}`,
|
||||
fail: (err) => {
|
||||
console.error('更新弹框跳转失败', err)
|
||||
uni.removeStorageSync(PACKAGE_INFO_KEY)
|
||||
}
|
||||
})
|
||||
|
||||
return
|
||||
} else if (code < 0) {
|
||||
// TODO 云函数报错处理
|
||||
console.error(message)
|
||||
return reject(e)
|
||||
}
|
||||
return resolve(e)
|
||||
}).catch(err => {
|
||||
// TODO 云函数报错处理
|
||||
console.error(err.message)
|
||||
reject(err)
|
||||
})
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 uni.showModal 升级
|
||||
*/
|
||||
function updateUseModal(packageInfo) {
|
||||
const {
|
||||
title, // 标题
|
||||
contents, // 升级内容
|
||||
is_mandatory, // 是否强制更新
|
||||
url, // 安装包下载地址
|
||||
platform, // 安装包平台
|
||||
type // 安装包类型
|
||||
} = packageInfo;
|
||||
|
||||
let isWGT = type === 'wgt'
|
||||
let isiOS = !isWGT ? platform.includes('iOS') : false;
|
||||
let confirmText = isiOS ? '立即跳转更新' : '立即下载更新'
|
||||
|
||||
return uni.showModal({
|
||||
title,
|
||||
content: contents,
|
||||
showCancel: !is_mandatory,
|
||||
confirmText,
|
||||
success: res => {
|
||||
if (res.cancel) return;
|
||||
|
||||
// 安装包下载
|
||||
if (isiOS) {
|
||||
plus.runtime.openURL(url);
|
||||
return;
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '后台下载中……',
|
||||
duration: 1000
|
||||
});
|
||||
|
||||
// wgt 和 安卓下载更新
|
||||
downloadTask = uni.downloadFile({
|
||||
url,
|
||||
success: res => {
|
||||
if (res.statusCode !== 200) {
|
||||
console.error('下载安装包失败', err);
|
||||
return;
|
||||
}
|
||||
// 下载好直接安装,下次启动生效
|
||||
plus.runtime.install(res.tempFilePath, {
|
||||
force: false
|
||||
}, () => {
|
||||
if (is_mandatory) {
|
||||
//更新完重启app
|
||||
plus.runtime.restart();
|
||||
return;
|
||||
}
|
||||
uni.showModal({
|
||||
title: '安装成功是否重启?',
|
||||
success: res => {
|
||||
if (res.confirm) {
|
||||
//更新完重启app
|
||||
plus.runtime.restart();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, err => {
|
||||
uni.showModal({
|
||||
title: '更新失败',
|
||||
content: err
|
||||
.message,
|
||||
showCancel: false
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
191
uni_modules/uni-upgrade-center-app/utils/check-update.ts
Normal file
191
uni_modules/uni-upgrade-center-app/utils/check-update.ts
Normal file
@ -0,0 +1,191 @@
|
||||
import callCheckVersion, { UniUpgradeCenterResult } from "./call-check-version"
|
||||
// #ifdef UNI-APP-X
|
||||
import { openSchema } from '@/uni_modules/uts-openSchema'
|
||||
// #endif
|
||||
|
||||
// 推荐再App.vue中使用
|
||||
const PACKAGE_INFO_KEY = '__package_info__'
|
||||
|
||||
// uni-app 项目无法从 vue 中导出 ComponentPublicInstance 类型,故使用条件编译
|
||||
// #ifdef UNI-APP-X
|
||||
export default function (component : ComponentPublicInstance | null = null) : Promise<UniUpgradeCenterResult> {
|
||||
// #endif
|
||||
// #ifndef UNI-APP-X
|
||||
export default function () : Promise<UniUpgradeCenterResult> {
|
||||
// #endif
|
||||
return new Promise<UniUpgradeCenterResult>((resolve, reject) => {
|
||||
callCheckVersion().then(async (uniUpgradeCenterResult) => {
|
||||
// NOTE uni-app x 3.96 解构有问题
|
||||
const code = uniUpgradeCenterResult.code
|
||||
const message = uniUpgradeCenterResult.message
|
||||
const url = uniUpgradeCenterResult.url // 安装包下载地址
|
||||
|
||||
// 此处逻辑仅为示例,可自行编写
|
||||
if (code > 0) {
|
||||
// 腾讯云和阿里云下载链接不同,需要处理一下,阿里云会原样返回
|
||||
const tcbRes = await uniCloud.getTempFileURL({ fileList: [url] });
|
||||
if (typeof tcbRes.fileList[0].tempFileURL !== 'undefined') uniUpgradeCenterResult.url = tcbRes.fileList[0].tempFileURL;
|
||||
|
||||
/**
|
||||
* 提示升级一
|
||||
* 使用 uni.showModal
|
||||
*/
|
||||
// return updateUseModal(uniUpgradeCenterResult)
|
||||
|
||||
// #ifndef UNI-APP-X
|
||||
// 静默更新,只有wgt有
|
||||
if (uniUpgradeCenterResult.is_silently) {
|
||||
uni.downloadFile({
|
||||
url,
|
||||
success: res => {
|
||||
if (res.statusCode == 200) {
|
||||
// 下载好直接安装,下次启动生效
|
||||
plus.runtime.install(res.tempFilePath, {
|
||||
force: false
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* 提示升级二
|
||||
* 官方适配的升级弹窗,可自行替换资源适配UI风格
|
||||
*/
|
||||
// #ifndef UNI-APP-X
|
||||
uni.setStorageSync(PACKAGE_INFO_KEY, uniUpgradeCenterResult)
|
||||
uni.navigateTo({
|
||||
url: `/uni_modules/uni-upgrade-center-app/pages/upgrade-popup?local_storage_key=${PACKAGE_INFO_KEY}`,
|
||||
fail: (err) => {
|
||||
console.error('更新弹框跳转失败', err)
|
||||
uni.removeStorageSync(PACKAGE_INFO_KEY)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
// #ifdef UNI-APP-X
|
||||
component?.$callMethod('show', true, uniUpgradeCenterResult)
|
||||
// #endif
|
||||
|
||||
return resolve(uniUpgradeCenterResult)
|
||||
} else if (code < 0) {
|
||||
console.error(message)
|
||||
return reject(uniUpgradeCenterResult)
|
||||
}
|
||||
return resolve(uniUpgradeCenterResult)
|
||||
}).catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 uni.showModal 升级
|
||||
*/
|
||||
function updateUseModal(packageInfo : UniUpgradeCenterResult) : void {
|
||||
// #ifdef APP
|
||||
const {
|
||||
title, // 标题
|
||||
contents, // 升级内容
|
||||
is_mandatory, // 是否强制更新
|
||||
url, // 安装包下载地址
|
||||
type,
|
||||
platform
|
||||
} = packageInfo;
|
||||
|
||||
let isWGT = type === 'wgt'
|
||||
let isiOS = !isWGT ? platform.includes('iOS') : false;
|
||||
|
||||
// #ifndef UNI-APP-X
|
||||
let confirmText = isiOS ? '立即跳转更新' : '立即下载更新'
|
||||
// #endif
|
||||
// #ifdef UNI-APP-X
|
||||
let confirmText = '立即下载更新'
|
||||
// #endif
|
||||
|
||||
return uni.showModal({
|
||||
title,
|
||||
content: contents,
|
||||
showCancel: !is_mandatory,
|
||||
confirmText,
|
||||
success: res => {
|
||||
if (res.cancel) return;
|
||||
|
||||
if (isiOS) {
|
||||
// iOS 平台跳转 AppStore
|
||||
// #ifndef UNI-APP-X
|
||||
plus.runtime.openURL(url);
|
||||
// #endif
|
||||
// #ifdef UNI-APP-X
|
||||
openSchema(url)
|
||||
// #endif
|
||||
return;
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '后台下载中……',
|
||||
duration: 1000
|
||||
});
|
||||
|
||||
// wgt 和 安卓下载更新
|
||||
uni.downloadFile({
|
||||
url,
|
||||
success: res => {
|
||||
if (res.statusCode !== 200) {
|
||||
console.error('下载安装包失败');
|
||||
return;
|
||||
}
|
||||
// 下载好直接安装,下次启动生效
|
||||
// uni-app x 项目没有 plus5+ 故使用条件编译
|
||||
// #ifndef UNI-APP-X
|
||||
plus.runtime.install(res.tempFilePath, {
|
||||
force: false
|
||||
}, () => {
|
||||
if (is_mandatory) {
|
||||
//更新完重启app
|
||||
plus.runtime.restart();
|
||||
return;
|
||||
}
|
||||
uni.showModal({
|
||||
title: '安装成功是否重启?',
|
||||
success: res => {
|
||||
if (res.confirm) {
|
||||
//更新完重启app
|
||||
plus.runtime.restart();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, err => {
|
||||
uni.showModal({
|
||||
title: '更新失败',
|
||||
content: err
|
||||
.message,
|
||||
showCancel: false
|
||||
});
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifdef UNI-APP-X
|
||||
uni.installApk({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
uni.showModal({
|
||||
title: '安装成功请手动重启'
|
||||
});
|
||||
},
|
||||
fail: err => {
|
||||
uni.showModal({
|
||||
title: '更新失败',
|
||||
content: err.message,
|
||||
showCancel: false
|
||||
});
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
}
|
Reference in New Issue
Block a user