diff --git a/App.vue b/App.vue index 6195dc8..8521a82 100644 --- a/App.vue +++ b/App.vue @@ -2,6 +2,9 @@ // #ifdef VUE3 import { initDictData } from '@/utils/dict' // #endif + // #ifdef APP-PLUS + import { initVersionCheck } from '@/utils/update' + // #endif export default { onLaunch: function() { @@ -32,6 +35,14 @@ uni.$uv.toast('网络连接不可用,请检查网络设置') } }) + + // #ifdef APP-PLUS + // 检查应用版本更新(延迟 2 秒,避免影响启动速度) + initVersionCheck({ + silent: false, // 显示提示 + delay: 2000 // 延迟 2 秒检查 + }) + // #endif }, onShow: function() { console.log('App Show') diff --git a/api/index.js b/api/index.js index 74d9675..184a189 100644 --- a/api/index.js +++ b/api/index.js @@ -29,4 +29,7 @@ export * from './common'; export * from './verify'; // 导入项目相关 API -export * from './project'; \ No newline at end of file +export * from './project'; + +// 导入版本更新相关 API +export * from './update'; \ No newline at end of file diff --git a/api/update.js b/api/update.js new file mode 100644 index 0000000..e167177 --- /dev/null +++ b/api/update.js @@ -0,0 +1,19 @@ +/** + * 版本更新相关 API + */ + +/** + * 检查应用版本 + * @param {Object} params - 版本检查参数 + * @param {string} params.versionName - 当前版本名称(如 1.0.1) + * @param {string} params.platform - 平台类型(android/ios) + * @returns {Promise} 返回版本信息 + */ +export const checkAppVersion = (params) => { + return uni.$uv.http.post('/app/version/check', params, { + custom: { + auth: false, // 版本检查可能不需要登录 + toast: false // 不自动提示错误 + } + }); +}; diff --git a/manifest.json b/manifest.json index 52949ec..4f87464 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "name" : "OfficeSystem", "appid" : "__UNI__53A0BE0", "description" : "", - "versionName" : "1.0.0", + "versionName" : "1.0.1", "versionCode" : "100", "transformPx" : false, /* 5+App特有相关 */ @@ -39,7 +39,11 @@ "", "", "", - "" + "", + "", + "", + "", + "" ] }, /* ios打包配置 */ diff --git a/utils/update.js b/utils/update.js new file mode 100644 index 0000000..4481db2 --- /dev/null +++ b/utils/update.js @@ -0,0 +1,349 @@ +/** + * 应用版本更新工具类 + * 支持版本检查和 APK 下载安装 + */ + +import { checkAppVersion } from '@/api/update' + +/** + * 获取当前应用版本信息 + * @returns {Object} {versionName} + */ +export const getCurrentVersion = () => { + // #ifdef APP-PLUS + const versionName = plus.runtime.version || '' + return { + versionName: versionName + } + // #endif + + // #ifndef APP-PLUS + // 非 APP 环境,从 manifest.json 读取(开发环境) + return { + versionName: '1.0.1' + } + // #endif +} + +/** + * 比较版本号 + * @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 result = await checkAppVersion(params) + + if (!result || !result.hasUpdate) { + if (!silent) { + uni.showToast({ + title: '已是最新版本', + icon: 'success', + duration: 2000 + }) + } + return null + } + + return result + } 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) +} +