useAppUpdate.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  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. // ================== 类型声明 ==================
  7. declare const plus: any
  8. declare const uni: any
  9. interface PlusDownloaderDownload {
  10. filename: string
  11. pause?: () => void
  12. resume?: () => void
  13. start: () => void
  14. abort: () => void
  15. addEventListener: (event: string, callback: (data: any) => void) => void
  16. removeEventListener?: (event: string, callback: (data: any) => void) => void
  17. }
  18. // ================== 常量定义 ==================
  19. /** 存储键名 */
  20. const STORAGE_KEYS = {
  21. LAST_CHECK: 'last_update_check_time',
  22. SKIP_VERSION: 'skip_update_version',
  23. DOWNLOAD_CACHE: 'app_download_cache'
  24. } as const
  25. /** 下载状态码 */
  26. enum DownloadState {
  27. DOWNLOADING = 3, // 下载中
  28. COMPLETED = 4 // 下载完成
  29. }
  30. /** HTTP 成功状态码 */
  31. enum HttpStatus {
  32. OK = 200, // 成功
  33. PARTIAL_CONTENT = 206 // 部分内容(断点续传)
  34. }
  35. /** 下载配置 */
  36. const DOWNLOAD_CONFIG = {
  37. FILE_PATH: '_downloads/app_update.wgt',
  38. MAX_RETRY: 3, // 最大重试次数
  39. RETRY_DELAY: 2000, // 重试延迟(毫秒)
  40. TOAST_DURATION: 2000 // Toast 显示时长
  41. } as const
  42. // ================== 类型定义 ==================
  43. /** 更新信息 */
  44. interface UpdateInfo {
  45. version: string
  46. forceUpdate: boolean
  47. wgtUrl?: string
  48. iosStoreUrl?: string
  49. }
  50. /** 下载缓存 */
  51. interface DownloadCache {
  52. url: string
  53. progress: number
  54. time: number
  55. }
  56. /** API 响应 */
  57. interface ApiResponse<T = any> {
  58. code: number
  59. data?: T
  60. message?: string
  61. }
  62. /** 下载状态变化事件 */
  63. interface DownloadStateEvent {
  64. state: number
  65. downloadedSize: number
  66. totalSize: number
  67. }
  68. /** 平台类型 */
  69. type PlatformType = 'ios' | 'android'
  70. /** 设备类型 */
  71. type EquipmentType = 'ios' | 'Android'
  72. // ================== 工具函数 ==================
  73. /**
  74. * 获取当前应用版本
  75. */
  76. function getCurrentVersion(): Promise<string> {
  77. return new Promise((resolve, reject) => {
  78. // #ifdef APP-PLUS
  79. try {
  80. plus.runtime.getProperty(plus.runtime.appid, (info) => {
  81. resolve(info.version)
  82. }, (error) => {
  83. reject(error)
  84. })
  85. } catch (error) {
  86. reject(error)
  87. }
  88. // #endif
  89. // #ifndef APP-PLUS
  90. reject(new Error('Not in APP-PLUS environment'))
  91. // #endif
  92. })
  93. }
  94. /**
  95. * 比较版本号
  96. * @returns 1: v1 > v2, -1: v1 < v2, 0: v1 === v2
  97. */
  98. function compareVersion(v1: string, v2: string): number {
  99. const normalizeVersion = (v: string): number[] => {
  100. return v.split('.').map(segment => parseInt(segment || '0', 10))
  101. }
  102. const parts1 = normalizeVersion(v1)
  103. const parts2 = normalizeVersion(v2)
  104. const maxLength = Math.max(parts1.length, parts2.length)
  105. for (let i = 0; i < maxLength; i++) {
  106. const num1 = parts1[i] || 0
  107. const num2 = parts2[i] || 0
  108. if (num1 > num2) return 1
  109. if (num1 < num2) return -1
  110. }
  111. return 0
  112. }
  113. /**
  114. * 获取平台类型
  115. */
  116. function getPlatform(): PlatformType {
  117. // #ifdef APP-PLUS
  118. try {
  119. const platform = uni.getSystemInfoSync().platform
  120. return platform === 'ios' ? 'ios' : 'android'
  121. } catch (error) {
  122. console.error('获取平台类型失败:', error)
  123. return 'android'
  124. }
  125. // #endif
  126. // #ifndef APP-PLUS
  127. return 'android'
  128. // #endif
  129. }
  130. /**
  131. * 获取设备类型(API 参数格式)
  132. */
  133. function getEquipmentType(): EquipmentType {
  134. return getPlatform() === 'ios' ? 'ios' : 'Android'
  135. }
  136. /**
  137. * 显示 Toast 提示
  138. */
  139. function showToast(message: string, duration = DOWNLOAD_CONFIG.TOAST_DURATION): void {
  140. uni.showToast({
  141. title: message,
  142. icon: 'none',
  143. duration
  144. })
  145. }
  146. /**
  147. * 安全地获取存储值
  148. */
  149. function getStorageSync<T = any>(key: string, defaultValue: T | null = null): T | null {
  150. try {
  151. return uni.getStorageSync(key) || defaultValue
  152. } catch (error) {
  153. console.warn(`获取存储失败: ${key}`, error)
  154. return defaultValue
  155. }
  156. }
  157. /**
  158. * 安全地设置存储值
  159. */
  160. function setStorageSync(key: string, value: any): boolean {
  161. try {
  162. uni.setStorageSync(key, value)
  163. return true
  164. } catch (error) {
  165. console.warn(`设置存储失败: ${key}`, error)
  166. return false
  167. }
  168. }
  169. /**
  170. * 安全地移除存储值
  171. */
  172. function removeStorageSync(key: string): boolean {
  173. try {
  174. uni.removeStorageSync(key)
  175. return true
  176. } catch (error) {
  177. console.warn(`移除存储失败: ${key}`, error)
  178. return false
  179. }
  180. }
  181. // ================== 主函数 ==================
  182. export function useAppUpdate() {
  183. const { t } = useI18n()
  184. const userStore = useUserStore()
  185. const progress = useProgress()
  186. const checking = ref(false)
  187. const updating = ref(false)
  188. let downloadTask: PlusDownloaderDownload | null = null
  189. let networkListener: ((res: any) => void) | null = null
  190. let downloadUrl = ''
  191. let retryCount = 0
  192. let stateChangeHandler: ((d: any) => void) | null = null
  193. // ================== 对外入口 ==================
  194. /**
  195. * 检查应用更新
  196. */
  197. async function checkUpdate(): Promise<void> {
  198. // #ifdef APP-PLUS
  199. if (checking.value) {
  200. console.warn('更新检查已在进行中')
  201. return
  202. }
  203. checking.value = true
  204. try {
  205. const equipmentType = getEquipmentType()
  206. const res: ApiResponse<UpdateInfo> = await ucardApi.getAppVersionDetail({ equipmentType })
  207. if (res.code !== 200 || !res.data) {
  208. const errorMsg = res.message || '获取版本信息失败'
  209. console.warn(errorMsg)
  210. return
  211. }
  212. const update = res.data
  213. const currentVersion = await getCurrentVersion()
  214. const needUpdate = compareVersion(update.version, currentVersion) > 0
  215. // 保存版本信息
  216. userStore.saveAppVersion({
  217. currentVersion,
  218. version: update.version,
  219. isUpdate: !needUpdate
  220. })
  221. if (!needUpdate) {
  222. console.log('当前已是最新版本')
  223. return
  224. }
  225. // 检查是否已跳过此版本
  226. // const skipVersion = getStorageSync<string>(STORAGE_KEYS.SKIP_VERSION)
  227. // if (!update.forceUpdate && skipVersion === update.version) {
  228. // console.log('用户已跳过此版本更新')
  229. // return
  230. // }
  231. // 显示更新提示
  232. if (update.forceUpdate) {
  233. showForceUpdate(update)
  234. } else {
  235. showOptionalUpdate(update)
  236. }
  237. } catch (error) {
  238. console.error('检查更新失败:', error)
  239. const errorMsg = error instanceof Error ? error.message : String(error)
  240. showToast(t('mine.p28') || `检查更新失败: ${errorMsg}`)
  241. } finally {
  242. checking.value = false
  243. }
  244. // #endif
  245. }
  246. // ================== 更新流程 ==================
  247. /**
  248. * 显示强制更新弹窗
  249. */
  250. function showForceUpdate(update: UpdateInfo): void {
  251. uni.showModal({
  252. title: t('mine.p22'),
  253. content: t('mine.p23'),
  254. showCancel: false,
  255. confirmText: t('mine.p35'),
  256. success: () => doUpdate(update)
  257. })
  258. }
  259. /**
  260. * 显示可选更新弹窗
  261. */
  262. function showOptionalUpdate(update: UpdateInfo): void {
  263. uni.showModal({
  264. title: t('mine.p24'),
  265. content: t('mine.p25', { version: `v${update.version}` }),
  266. confirmText: t('mine.p35'),
  267. cancelText: t('mine.p36'),
  268. success: (res) => {
  269. if (res.confirm) {
  270. doUpdate(update)
  271. } else {
  272. // 记录跳过的版本
  273. setStorageSync(STORAGE_KEYS.SKIP_VERSION, update.version)
  274. }
  275. }
  276. })
  277. }
  278. /**
  279. * 执行更新
  280. */
  281. function doUpdate(update: UpdateInfo): void {
  282. const platform = getPlatform()
  283. if (platform === 'ios') {
  284. handleIosUpdate(update)
  285. } else {
  286. handleAndroidUpdate(update)
  287. }
  288. }
  289. /**
  290. * 处理 iOS 更新
  291. */
  292. function handleIosUpdate(update: UpdateInfo): void {
  293. if (!update.iosStoreUrl) {
  294. showToast(t('mine.p28') || 'iOS 更新链接不存在')
  295. return
  296. }
  297. try {
  298. plus.runtime.openURL(update.iosStoreUrl)
  299. } catch (error) {
  300. console.error('打开 App Store 失败:', error)
  301. showToast(t('mine.p28') || '打开 App Store 失败')
  302. }
  303. }
  304. /**
  305. * 处理 Android 更新
  306. */
  307. function handleAndroidUpdate(update: UpdateInfo): void {
  308. if (!update.wgtUrl) {
  309. showToast(t('mine.p28') || '更新包链接不存在')
  310. return
  311. }
  312. downloadWgt(update.wgtUrl)
  313. }
  314. // ================== 下载核心 ==================
  315. /**
  316. * 下载 wgt 更新包
  317. */
  318. function downloadWgt(url: string): void {
  319. // #ifdef APP-PLUS
  320. if (downloadTask) {
  321. console.warn('下载任务已存在')
  322. return
  323. }
  324. if (!url || !isValidUrl(url)) {
  325. handleDownloadFail(t('mine.p28') || '下载链接无效')
  326. return
  327. }
  328. downloadUrl = url
  329. retryCount = 0
  330. updating.value = true
  331. progress.show(t('mine.p29'))
  332. // 初始化网络监听
  333. initNetworkType()
  334. startNetworkMonitor()
  335. createDownloadTask(url)
  336. // #endif
  337. }
  338. /**
  339. * 创建下载任务
  340. */
  341. function createDownloadTask(url: string): void {
  342. try {
  343. downloadTask = plus.downloader.createDownload(
  344. url,
  345. { filename: DOWNLOAD_CONFIG.FILE_PATH },
  346. handleDownloadComplete
  347. )
  348. if (!downloadTask) {
  349. throw new Error('创建下载任务失败')
  350. }
  351. // 创建状态变化处理器
  352. stateChangeHandler = handleDownloadStateChanged
  353. downloadTask.addEventListener('statechanged', stateChangeHandler)
  354. downloadTask.start()
  355. } catch (error) {
  356. console.error('创建下载任务失败:', error)
  357. downloadTask = null
  358. handleDownloadFail(t('mine.p28') || '创建下载任务失败')
  359. }
  360. }
  361. /**
  362. * 验证 URL 格式
  363. */
  364. function isValidUrl(url: string): boolean {
  365. try {
  366. return /^https?:\/\/.+/.test(url)
  367. } catch {
  368. return false
  369. }
  370. }
  371. /**
  372. * 处理下载完成回调
  373. */
  374. function handleDownloadComplete(download: PlusDownloaderDownload, status: number): void {
  375. const isSuccess = status === HttpStatus.OK || status === HttpStatus.PARTIAL_CONTENT
  376. if (isSuccess) {
  377. console.log('下载成功,准备安装')
  378. clearCache()
  379. stopNetworkMonitor()
  380. installWgt(download.filename)
  381. } else {
  382. console.error('下载失败,状态码:', status)
  383. retryDownload()
  384. }
  385. }
  386. /**
  387. * 处理下载状态变化
  388. */
  389. function handleDownloadStateChanged(event: DownloadStateEvent): void {
  390. if (event.state === DownloadState.DOWNLOADING && event.totalSize > 0) {
  391. const percent = Math.min(100, Math.floor((event.downloadedSize / event.totalSize) * 100))
  392. const progressText = `${t('mine.p29')} ${percent}%`
  393. progress.update(percent, progressText)
  394. saveCache(percent)
  395. }
  396. }
  397. /**
  398. * 重试下载
  399. */
  400. function retryDownload(): void {
  401. if (retryCount >= DOWNLOAD_CONFIG.MAX_RETRY) {
  402. console.error(`下载失败,已重试 ${retryCount} 次`)
  403. handleDownloadFail(t('mine.p28') || '下载失败')
  404. return
  405. }
  406. retryCount++
  407. console.log(`下载失败,${DOWNLOAD_CONFIG.RETRY_DELAY / 1000}秒后重试 (${retryCount}/${DOWNLOAD_CONFIG.MAX_RETRY})`)
  408. cleanupDownloadTask()
  409. setTimeout(() => {
  410. if (downloadUrl) {
  411. createDownloadTask(downloadUrl)
  412. }
  413. }, DOWNLOAD_CONFIG.RETRY_DELAY)
  414. }
  415. /**
  416. * 初始化网络类型
  417. */
  418. function initNetworkType(): void {
  419. uni.getNetworkType({
  420. success: (res) => {
  421. console.log('当前网络类型:', res.networkType)
  422. },
  423. fail: (error) => {
  424. console.warn('获取网络类型失败:', error)
  425. }
  426. })
  427. }
  428. // ================== 网络处理 ==================
  429. /**
  430. * 启动网络监听
  431. */
  432. function startNetworkMonitor(): void {
  433. // #ifdef APP-PLUS
  434. if (networkListener) {
  435. console.warn('网络监听已存在')
  436. return
  437. }
  438. networkListener = (res: any) => {
  439. if (!downloadTask || !updating.value) {
  440. return
  441. }
  442. if (!res.isConnected) {
  443. // 网络断开,暂停下载
  444. pauseDownload()
  445. return
  446. }
  447. // 网络恢复,继续下载
  448. resumeDownload()
  449. }
  450. uni.onNetworkStatusChange(networkListener)
  451. // #endif
  452. }
  453. /**
  454. * 暂停下载
  455. */
  456. function pauseDownload(): void {
  457. if (!downloadTask) return
  458. try {
  459. if (typeof downloadTask.pause === 'function') {
  460. downloadTask.pause()
  461. }
  462. } catch (error) {
  463. console.error('暂停下载失败:', error)
  464. }
  465. }
  466. /**
  467. * 恢复下载
  468. */
  469. function resumeDownload(): void {
  470. if (!downloadTask) return
  471. try {
  472. if (typeof downloadTask.resume === 'function') {
  473. downloadTask.resume()
  474. } else if (typeof downloadTask.start === 'function') {
  475. downloadTask.start()
  476. }
  477. } catch (error) {
  478. console.error('恢复下载失败,尝试重新开始:', error)
  479. // 失败时尝试重新开始
  480. try {
  481. if (downloadTask.start) {
  482. downloadTask.start()
  483. }
  484. } catch (e2) {
  485. console.error('重新开始下载也失败:', e2)
  486. }
  487. }
  488. }
  489. /**
  490. * 停止网络监听
  491. */
  492. function stopNetworkMonitor(): void {
  493. if (networkListener) {
  494. uni.offNetworkStatusChange(networkListener)
  495. networkListener = null
  496. }
  497. }
  498. // ================== 安装 ==================
  499. /**
  500. * 安装 wgt 更新包
  501. */
  502. function installWgt(path: string): void {
  503. // #ifdef APP-PLUS
  504. try {
  505. plus.runtime.install(
  506. path,
  507. { force: false },
  508. () => {
  509. // 安装成功
  510. progress.hide()
  511. updating.value = false
  512. uni.showModal({
  513. title: t('mine.p37'),
  514. content: t('mine.p38'),
  515. showCancel: false,
  516. success: () => {
  517. plus.runtime.restart()
  518. }
  519. })
  520. },
  521. (error) => {
  522. // 安装失败
  523. console.error('安装失败:', error)
  524. handleDownloadFail(t('mine.p33'))
  525. }
  526. )
  527. } catch (error) {
  528. console.error('安装异常:', error)
  529. handleDownloadFail(t('mine.p33'))
  530. }
  531. // #endif
  532. }
  533. // ================== 缓存管理 ==================
  534. /**
  535. * 保存下载缓存
  536. */
  537. function saveCache(progressPercent: number): void {
  538. const cache: DownloadCache = {
  539. url: downloadUrl,
  540. progress: progressPercent,
  541. time: Date.now()
  542. }
  543. setStorageSync(STORAGE_KEYS.DOWNLOAD_CACHE, cache)
  544. }
  545. /**
  546. * 清除下载缓存
  547. */
  548. function clearCache(): void {
  549. removeStorageSync(STORAGE_KEYS.DOWNLOAD_CACHE)
  550. }
  551. // ================== 错误处理 ==================
  552. /**
  553. * 处理下载失败
  554. */
  555. function handleDownloadFail(msg: string): void {
  556. progress.hide()
  557. updating.value = false
  558. retryCount = 0
  559. stopNetworkMonitor()
  560. cleanupDownloadTask()
  561. showToast(msg)
  562. }
  563. /**
  564. * 清理下载任务
  565. */
  566. function cleanupDownloadTask(): void {
  567. if (!downloadTask) return
  568. try {
  569. // 移除事件监听
  570. if (stateChangeHandler && downloadTask.removeEventListener) {
  571. downloadTask.removeEventListener('statechanged', stateChangeHandler)
  572. }
  573. // 中止下载
  574. downloadTask.abort()
  575. } catch (error) {
  576. console.warn('清理下载任务失败:', error)
  577. } finally {
  578. downloadTask = null
  579. stateChangeHandler = null
  580. }
  581. }
  582. /**
  583. * 取消更新
  584. */
  585. function cancelUpdate(): void {
  586. if (!updating.value) {
  587. console.warn('当前没有正在进行的更新')
  588. return
  589. }
  590. console.log('用户取消更新')
  591. handleDownloadFail(t('mine.p39') || '已取消更新')
  592. }
  593. // ================== 生命周期 ==================
  594. onUnmounted(() => {
  595. console.log('useAppUpdate 组件卸载,清理资源')
  596. stopNetworkMonitor()
  597. cleanupDownloadTask()
  598. retryCount = 0
  599. })
  600. return {
  601. checkUpdate,
  602. cancelUpdate,
  603. checking,
  604. updating
  605. }
  606. }