OfficeSystem/utils/update.js
2025-11-25 17:33:44 +08:00

388 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 应用版本更新工具类
* 支持版本检查和 APK 下载下载
*/
import { checkAppVersion } from '@/api/update'
import manifest from '@/manifest.json'
/**
* 获取当前应用版本信息
* @returns {Object} {versionName}
*/
export const getCurrentVersion = () => {
// 统一从 manifest.json 读取版本号
let versionName = manifest?.versionName || ''
// #ifdef APP-PLUS
// APP 环境下,如果 manifest 读取失败,使用运行时版本作为后备
if (!versionName) {
versionName = plus.runtime.version || ''
}
// #endif
// 如果都读取不到,使用默认值
if (!versionName) {
versionName = '1.0.1'
}
return {
versionName: versionName
}
}
/**
* 比较版本号
* @param {string} version1 - 版本号1如 "1.0.1"
* @param {string} version2 - 版本号2如 "1.0.2"
* @returns {number} 1: version1 > version2, -1: version1 < version2, 0: 相等
*/
export const compareVersion = (version1, version2) => {
const v1parts = version1.split('.').map(Number)
const v2parts = version2.split('.').map(Number)
const maxLength = Math.max(v1parts.length, v2parts.length)
for (let i = 0; i < maxLength; i++) {
const v1part = v1parts[i] || 0
const v2part = v2parts[i] || 0
if (v1part > v2part) return 1
if (v1part < v2part) return -1
}
return 0
}
/**
* 检查是否有新版本
* @param {boolean} silent - 是否静默检查(不显示提示)
* @returns {Promise<Object|null>} 返回新版本信息,无新版本返回 null
*/
export const checkForUpdate = async (silent = false) => {
try {
const currentVersion = getCurrentVersion()
const platform = uni.getSystemInfoSync().platform
// 只在 Android 平台检查更新
// #ifdef APP-PLUS
if (platform !== 'android') {
if (!silent) {
console.log('当前平台不支持自动更新:', platform)
}
return null
}
// #endif
// #ifndef APP-PLUS
// 非 APP 环境,跳过检查
return null
// #endif
const params = {
versionName: currentVersion.versionName,
platform: 'android'
}
const response = await checkAppVersion(params)
console.log('版本检查 API 返回:', response)
// API 返回的数据在 data 字段中,或者直接就是数据对象
const result = response.data || response
if (!result || !result.hasUpdate) {
if (!silent) {
uni.showToast({
title: '已是最新版本',
icon: 'success',
duration: 2000
})
}
return null
}
// 映射 API 返回的字段到内部使用的字段
// API 返回: packageUrl, versionNumber, log
// 内部使用: downloadUrl, versionName, updateLog
const updateInfo = {
hasUpdate: result.hasUpdate,
versionName: result.versionNumber || result.versionName || result.version || '',
downloadUrl: result.packageUrl || result.downloadUrl || result.download_url || '',
updateLog: result.log || result.updateLog || result.update_log || result.description || '',
forceUpdate: result.forceUpdate || result.force_update || false,
fileSize: result.fileSize || result.file_size || 0,
versionCode: result.versionCode || 0
}
console.log('处理后的更新信息:', updateInfo)
return updateInfo
} catch (error) {
console.error('检查版本更新失败:', error)
if (!silent) {
uni.showToast({
title: '检查更新失败',
icon: 'none',
duration: 2000
})
}
return null
}
}
/**
* 显示版本更新提示
* @param {Object} updateInfo - 更新信息
* @param {string} updateInfo.versionName - 新版本号
* @param {string} updateInfo.downloadUrl - 下载地址
* @param {string} updateInfo.updateLog - 更新日志
* @param {boolean} updateInfo.forceUpdate - 是否强制更新
* @param {number} updateInfo.fileSize - 文件大小(字节)
*/
export const showUpdateDialog = (updateInfo) => {
const fileSizeMB = updateInfo.fileSize
? (updateInfo.fileSize / 1024 / 1024).toFixed(2) + 'MB'
: '未知大小'
const content = updateInfo.updateLog
? `版本:${updateInfo.versionName}\n大小:${fileSizeMB}\n\n${updateInfo.updateLog}`
: `发现新版本 ${updateInfo.versionName}\n大小:${fileSizeMB}`
uni.showModal({
title: '发现新版本',
content: content,
showCancel: !updateInfo.forceUpdate,
cancelText: '稍后更新',
confirmText: '立即更新',
success: (res) => {
if (res.confirm) {
downloadAndInstall(updateInfo.downloadUrl, updateInfo.versionName)
} else if (updateInfo.forceUpdate) {
// 强制更新时,取消也退出应用
// #ifdef APP-PLUS
plus.runtime.quit()
// #endif
}
}
})
}
/**
* 下载并安装 APK
* @param {string} downloadUrl - APK 下载地址
* @param {string} versionName - 版本名称(用于文件命名)
*/
export const downloadAndInstall = (downloadUrl, versionName = '') => {
// #ifdef APP-PLUS
if (!downloadUrl) {
uni.showToast({
title: '下载地址无效',
icon: 'none'
})
return
}
// 显示下载进度
let downloadTask = null
let progressModal = null
uni.showLoading({
title: '下载中',
mask: true
})
// 创建下载任务
downloadTask = uni.downloadFile({
url: downloadUrl,
//
// if (progress === 100) {
// uni.hideLoading()
// }
success: (res) => {
if (res.statusCode === 200) {
// 下载成功,安装 APK
uni.hideLoading()
installApk(res.tempFilePath)
} else {
uni.hideLoading()
uni.showToast({
title: '下载失败',
icon: 'none'
})
}
},
fail: (err) => {
uni.hideLoading()
console.error('下载失败:', err)
uni.showToast({
title: '下载失败:' + (err.errMsg || '未知错误'),
icon: 'none',
duration: 3000
})
}
})
// // 监听下载进度
// downloadTask.onProgressUpdate((res) => {
// const progress = res.progress
// const received = (res.totalBytesWritten / 1024 / 1024).toFixed(2)
// const total = (res.totalBytesExpectedToWrite / 1024 / 1024).toFixed(2)
//
// // 显示下载进度(使用 uni.showLoading
// uni.showLoading({
// title: '下载中',
// mask: true
// })
//
// if (progress === 100) {
// uni.hideLoading()
// }
// })
// #endif
// #ifndef APP-PLUS
// 非 APP 环境,提示用户手动下载
uni.showModal({
title: '提示',
content: `请访问以下地址下载新版本:\n${downloadUrl}`,
showCancel: false,
confirmText: '复制链接',
success: (res) => {
if (res.confirm) {
// #ifdef H5
// H5 环境可以尝试复制到剪贴板
if (navigator.clipboard) {
navigator.clipboard.writeText(downloadUrl).then(() => {
uni.showToast({
title: '链接已复制',
icon: 'success'
})
})
}
// #endif
}
}
})
// #endif
}
/**
* 安装 APK 文件
* @param {string} filePath - APK 文件路径
*/
const installApk = (filePath) => {
// #ifdef APP-PLUS
try {
// Android 8.0+ 需要请求安装权限
const systemInfo = uni.getSystemInfoSync()
const androidVersion = systemInfo.system.split(' ')[1] || '0'
const majorVersion = parseInt(androidVersion.split('.')[0]) || 0
// Android 8.0 (API 26) 及以上需要请求安装权限
if (majorVersion >= 8) {
// 检查是否有安装权限
const main = plus.android.runtimeMainActivity()
const pkName = main.getPackageName()
const Intent = plus.android.importClass('android.content.Intent')
const Settings = plus.android.importClass('android.provider.Settings')
const Uri = plus.android.importClass('android.net.Uri')
const Build = plus.android.importClass('android.os.Build')
// Android 8.0+ 使用 REQUEST_INSTALL_PACKAGES 权限
if (Build.VERSION.SDK_INT >= 26) {
const PackageManager = plus.android.importClass('android.content.pm.PackageManager')
const pm = main.getPackageManager()
const hasPermission = pm.canRequestPackageInstalls()
if (!hasPermission) {
// 请求安装权限
uni.showModal({
title: '需要安装权限',
content: '应用需要安装权限才能更新,是否前往设置开启?',
confirmText: '前往设置',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
try {
const intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
const uri = Uri.parse('package:' + pkName)
intent.setData(uri)
main.startActivity(intent)
// 提示用户开启权限后重新安装
uni.showModal({
title: '提示',
content: '请在设置中开启"允许安装未知应用"权限,然后返回应用重新更新',
showCancel: false,
confirmText: '知道了'
})
} catch (e) {
console.error('打开设置失败:', e)
uni.showToast({
title: '无法打开设置',
icon: 'none'
})
}
}
}
})
return
}
}
}
// 使用 plus.runtime.install 安装 APK
plus.runtime.install(
filePath,
{
force: false // 不强制安装
},
() => {
uni.showToast({
title: '下载成功',
icon: 'success'
})
// 安装成功后,延迟退出应用以便用户重启
setTimeout(() => {
plus.runtime.quit()
}, 1500)
},
(error) => {
console.error('下载失败:', error)
uni.showModal({
title: '下载失败',
content: '请检查是否允许下载未知来源应用,或手动下载下载的 APK 文件',
showCancel: false,
confirmText: '知道了'
})
}
)
} catch (error) {
console.error('下载 APK 异常:', error)
uni.showToast({
title: '下载失败',
icon: 'none'
})
}
// #endif
}
/**
* 初始化版本检查(在 App 启动时调用)
* @param {Object} options - 配置选项
* @param {boolean} options.silent - 是否静默检查(默认 false
* @param {number} options.delay - 延迟检查时间(毫秒,默认 2000
*/
export const initVersionCheck = (options = {}) => {
const { silent = false, delay = 2000 } = options
setTimeout(() => {
checkForUpdate(silent).then(updateInfo => {
if (updateInfo) {
showUpdateDialog(updateInfo)
}
})
}, delay)
}