/** * 应用版本更新工具类 * 支持版本检查和 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} 返回新版本信息,无新版本返回 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 // 创建下载任务 downloadTask = uni.downloadFile({ url: downloadUrl, success: (res) => { if (res.statusCode === 200) { // 下载成功,安装 APK installApk(res.tempFilePath) } else { uni.showToast({ title: '下载失败', icon: 'none' }) } }, fail: (err) => { 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: `下载中 ${progress}%`, 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) }