useAppUpdate.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import { ref, onUnmounted } from 'vue'
  2. import { useI18n } from 'vue-i18n'
  3. import { useProgress } from './useProgress'
  4. import useUserStore from '@/stores/use-user-store'
  5. import { ucardApi } from '@/api/ucard'
  6. const LAST_CHECK_KEY = 'last_update_check_time'
  7. const SKIP_VERSION_KEY = 'skip_update_version'
  8. const DOWNLOAD_CACHE_KEY = 'app_download_cache'
  9. export function useAppUpdate() {
  10. const { t } = useI18n()
  11. const userStore = useUserStore()
  12. const progress = useProgress()
  13. const checking = ref(false)
  14. const updating = ref(false)
  15. let downloadTask: any = null
  16. let networkListener: any = null
  17. let downloadUrl = ''
  18. let savedFilePath = '_downloads/app_update.wgt'
  19. let currentNetworkType = ''
  20. /* ================== 对外入口 ================== */
  21. async function checkUpdate() {
  22. // #ifdef APP-PLUS
  23. if (checking.value) return
  24. checking.value = true
  25. try {
  26. const platform = uni.getSystemInfoSync().platform
  27. const equipmentType = platform === 'ios' ? 'ios' : 'Android'
  28. const res = await ucardApi.getAppVersionDetail({ equipmentType })
  29. if (res.code !== 200 || !res.data) return
  30. const update = res.data
  31. const currentVersion = await getCurrentVersion()
  32. const needUpdate = compareVersion(update.version, currentVersion) > 0
  33. userStore.saveAppVersion({
  34. currentVersion,
  35. version: update.version,
  36. isUpdate: !needUpdate
  37. })
  38. if (!needUpdate) return
  39. const skip = uni.getStorageSync(SKIP_VERSION_KEY)
  40. if (!update.forceUpdate && skip === update.version) return
  41. if (update.forceUpdate) {
  42. showForceUpdate(update)
  43. } else {
  44. showOptionalUpdate(update)
  45. }
  46. } finally {
  47. checking.value = false
  48. }
  49. // #endif
  50. }
  51. /* ================== 更新流程 ================== */
  52. function showForceUpdate(update: any) {
  53. uni.showModal({
  54. title: t('mine.p22'),
  55. content: t('mine.p23'),
  56. showCancel: false,
  57. confirmText: t('mine.p35'),
  58. success: () => doUpdate(update)
  59. })
  60. }
  61. function showOptionalUpdate(update: any) {
  62. uni.showModal({
  63. title: t('mine.p24'),
  64. content: t('mine.p25', { version: 'v' + update.version }),
  65. confirmText: t('mine.p35'),
  66. cancelText: t('mine.p36'),
  67. success: res => {
  68. if (res.confirm) doUpdate(update)
  69. else uni.setStorageSync(SKIP_VERSION_KEY, update.version)
  70. }
  71. })
  72. }
  73. function doUpdate(update: any) {
  74. const platform = uni.getSystemInfoSync().platform
  75. if (platform === 'ios') {
  76. plus.runtime.openURL(update.iosStoreUrl)
  77. return
  78. }
  79. downloadWgt(update.wgtUrl)
  80. }
  81. /* ================== 下载核心 ================== */
  82. function downloadWgt(url: string) {
  83. // #ifdef APP-PLUS
  84. if (downloadTask) return
  85. downloadUrl = url
  86. updating.value = true
  87. progress.show(t('mine.p29'))
  88. // 获取当前网络类型
  89. uni.getNetworkType({
  90. success: (res) => {
  91. currentNetworkType = res.networkType
  92. }
  93. })
  94. startNetworkMonitor()
  95. downloadTask = plus.downloader.createDownload(
  96. url,
  97. { filename: savedFilePath },
  98. (download, status) => {
  99. if (status === 200 || status === 206) {
  100. clearCache()
  101. stopNetworkMonitor()
  102. installWgt(download.filename)
  103. } else {
  104. fail(t('mine.p28'))
  105. }
  106. }
  107. )
  108. downloadTask.addEventListener('statechanged', (d: any) => {
  109. const state = d.state
  110. if (state === 3 && d.totalSize) { // 下载中
  111. const p = Math.floor((d.downloadedSize / d.totalSize) * 100)
  112. progress.update(p, `${t('mine.p29')} ${p}%`)
  113. saveCache(p)
  114. }
  115. })
  116. downloadTask.start()
  117. // #endif
  118. }
  119. /* ================== 网络处理 ================== */
  120. function startNetworkMonitor() {
  121. // #ifdef APP-PLUS
  122. networkListener = uni.onNetworkStatusChange(res => {
  123. if (!downloadTask || !updating.value) return
  124. // 只要有网络变化,直接尝试恢复下载
  125. if (!res.isConnected) {
  126. currentNetworkType = 'none'
  127. try {
  128. downloadTask.pause && downloadTask.pause()
  129. } catch (e) {
  130. console.error('暂停下载失败:', e)
  131. }
  132. return
  133. }
  134. // 网络恢复或切换,直接 resume,不依赖 state
  135. currentNetworkType = res.networkType
  136. try {
  137. if (downloadTask.resume) {
  138. downloadTask.resume()
  139. } else {
  140. downloadTask.start()
  141. }
  142. } catch (e) {
  143. // 失败时尝试用 start 重试
  144. try {
  145. downloadTask.start()
  146. } catch (e2) {
  147. console.error('start() 也失败:', e2)
  148. }
  149. }
  150. })
  151. // #endif
  152. }
  153. function stopNetworkMonitor() {
  154. if (networkListener) {
  155. uni.offNetworkStatusChange(networkListener)
  156. networkListener = null
  157. }
  158. currentNetworkType = ''
  159. }
  160. /* ================== 安装 ================== */
  161. function installWgt(path: string) {
  162. plus.runtime.install(
  163. path,
  164. { force: false },
  165. () => {
  166. progress.hide()
  167. updating.value = false
  168. uni.showModal({
  169. title: t('mine.p37'),
  170. content: t('mine.p38'),
  171. showCancel: false,
  172. success: () => plus.runtime.restart()
  173. })
  174. },
  175. () => fail(t('mine.p33'))
  176. )
  177. }
  178. /* ================== 缓存(仅 UI) ================== */
  179. function saveCache(progress: number) {
  180. uni.setStorageSync(DOWNLOAD_CACHE_KEY, {
  181. url: downloadUrl,
  182. progress,
  183. time: Date.now()
  184. })
  185. }
  186. function clearCache() {
  187. uni.removeStorageSync(DOWNLOAD_CACHE_KEY)
  188. }
  189. /* ================== 兜底失败 ================== */
  190. function fail(msg: string) {
  191. progress.hide()
  192. updating.value = false
  193. stopNetworkMonitor()
  194. if (downloadTask) {
  195. try { downloadTask.abort() } catch { }
  196. downloadTask = null
  197. }
  198. uni.showToast({ title: msg, icon: 'none' })
  199. }
  200. onUnmounted(() => {
  201. stopNetworkMonitor()
  202. })
  203. return { checkUpdate }
  204. }
  205. /* ================== 工具 ================== */
  206. function getCurrentVersion(): Promise<string> {
  207. return new Promise(resolve => {
  208. plus.runtime.getProperty(plus.runtime.appid, info => {
  209. resolve(info.version)
  210. })
  211. })
  212. }
  213. function compareVersion(v1: string, v2: string) {
  214. const a1 = v1.split('.')
  215. const a2 = v2.split('.')
  216. const len = Math.max(a1.length, a2.length)
  217. for (let i = 0; i < len; i++) {
  218. const n1 = parseInt(a1[i] || '0')
  219. const n2 = parseInt(a2[i] || '0')
  220. if (n1 > n2) return 1
  221. if (n1 < n2) return -1
  222. }
  223. return 0
  224. }