| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722 |
- import { ref, onUnmounted } from 'vue'
- import { useI18n } from 'vue-i18n'
- import { useProgress } from './useProgress'
- import useUserStore from '@/stores/use-user-store'
- import { ucardApi } from '@/api/ucard'
- import { userToken } from '@/composables/config'
- import config from '@/config'
- import { trim } from 'lodash'
- // ================== 类型声明 ==================
- declare const plus: any
- declare const uni: any
- interface PlusDownloaderDownload {
- filename: string
- pause?: () => void
- resume?: () => void
- start: () => void
- abort: () => void
- addEventListener: (event: string, callback: (data: any) => void) => void
- removeEventListener?: (event: string, callback: (data: any) => void) => void
- }
- // ================== 常量定义 ==================
- /** 存储键名 */
- const STORAGE_KEYS = {
- LAST_CHECK: 'last_update_check_time',
- SKIP_VERSION: 'skip_update_version',
- DOWNLOAD_CACHE: 'app_download_cache',
- } as const
- /** 下载状态码 */
- enum DownloadState {
- DOWNLOADING = 3, // 下载中
- COMPLETED = 4 // 下载完成
- }
- /** HTTP 成功状态码 */
- enum HttpStatus {
- OK = 200, // 成功
- PARTIAL_CONTENT = 206 // 部分内容(断点续传)
- }
- /** 下载配置 */
- const DOWNLOAD_CONFIG = {
- FILE_PATH: '_downloads/app_update.wgt',
- MAX_RETRY: 3, // 最大重试次数
- RETRY_DELAY: 2000, // 重试延迟(毫秒)
- TOAST_DURATION: 2000, // Toast 显示时长
- } as const
- // ================== 类型定义 ==================
- /** 更新信息 */
- interface UpdateInfo {
- version: string
- forceUpdate: boolean
- wgtUrl?: string
- iosStoreUrl?: string
- }
- /** 下载缓存 */
- interface DownloadCache {
- url: string
- progress: number
- time: number
- }
- /** API 响应 */
- interface ApiResponse<T = any> {
- code: number
- data?: T
- message?: string
- }
- /** 下载状态变化事件 */
- interface DownloadStateEvent {
- state: number
- downloadedSize: number
- totalSize: number
- }
- /** 平台类型 */
- type PlatformType = 'ios' | 'android'
- /** 设备类型 */
- type EquipmentType = 'ios' | 'Android'
- // ================== 工具函数 ==================
- /**
- * 获取当前应用版本
- */
- function getCurrentVersion(): Promise<string> {
- return new Promise((resolve, reject) => {
- // #ifdef APP-PLUS
- try {
- plus.runtime.getProperty(plus.runtime.appid, (info) => {
- resolve(info.version)
- }, (error) => {
- reject(error)
- })
- } catch (error) {
- reject(error)
- }
- // #endif
- // #ifndef APP-PLUS
- reject(new Error('Not in APP-PLUS environment'))
- // #endif
- })
- }
- /**
- * 比较版本号
- * @returns 1: v1 > v2, -1: v1 < v2, 0: v1 === v2
- */
- function compareVersion(v1: string, v2: string): number {
- const normalizeVersion = (v: string): number[] => {
- return v.split('.').map(segment => parseInt(segment || '0', 10))
- }
- const parts1 = normalizeVersion(v1)
- const parts2 = normalizeVersion(v2)
- const maxLength = Math.max(parts1.length, parts2.length)
- for (let i = 0; i < maxLength; i++) {
- const num1 = parts1[i] || 0
- const num2 = parts2[i] || 0
- if (num1 > num2) return 1
- if (num1 < num2) return -1
- }
- return 0
- }
- /**
- * 获取平台类型
- */
- function getPlatform(): PlatformType {
- // #ifdef APP-PLUS
- try {
- const platform = uni.getSystemInfoSync().platform
- return platform === 'ios' ? 'ios' : 'android'
- } catch (error) {
- // console.error('获取平台类型失败:', error)
- return 'android'
- }
- // #endif
- // #ifndef APP-PLUS
- return 'android'
- // #endif
- }
- /**
- * 获取设备类型(API 参数格式)
- */
- function getEquipmentType(): EquipmentType {
- return getPlatform() === 'ios' ? 'ios' : 'Android'
- }
- /**
- * 显示 Toast 提示
- */
- function showToast(message: string, duration = DOWNLOAD_CONFIG.TOAST_DURATION): void {
- uni.showToast({
- title: message,
- icon: 'none',
- duration,
- })
- }
- /**
- * 安全地获取存储值
- */
- function getStorageSync<T = any>(key: string, defaultValue: T | null = null): T | null {
- try {
- return uni.getStorageSync(key) || defaultValue
- } catch (error) {
- // console.warn(`获取存储失败: ${key}`, error)
- return defaultValue
- }
- }
- /**
- * 安全地设置存储值
- */
- function setStorageSync(key: string, value: any): boolean {
- try {
- uni.setStorageSync(key, value)
- return true
- } catch (error) {
- // console.warn(`设置存储失败: ${key}`, error)
- return false
- }
- }
- /**
- * 安全地移除存储值
- */
- function removeStorageSync(key: string): boolean {
- try {
- uni.removeStorageSync(key)
- return true
- } catch (error) {
- // console.warn(`移除存储失败: ${key}`, error)
- return false
- }
- }
- // ================== 主函数 ==================
- export function useAppUpdate() {
- const { t } = useI18n()
- const { Host80 } = config
- // 测试环境地址
- // const Host80 = 'https://secure.44a5c8109e4.com/'
- const userStore = useUserStore()
- const progress = useProgress()
- const checking = ref(false)
- const updating = ref(false)
- const lastVersion = ref('')
- let downloadTask: PlusDownloaderDownload | null = null
- let networkListener: ((res: any) => void) | null = null
- let downloadUrl = ''
- let retryCount = 0
- let stateChangeHandler: ((d: any) => void) | null = null
- // ================== 对外入口 ==================
- /**
- * 检查应用更新
- */
- async function checkUpdate(): Promise<void> {
- // #ifdef APP-PLUS
- if (checking.value) {
- // console.warn('更新检查已在进行中')
- return
- }
- checking.value = true
- try {
- const res = await uni.request({
- url: `${Host80}/wgt/list.json?_t=${Date.now()}`,
- method: 'GET',
- timeout: 5000,
- })
- console.log('up:filedata', res)
- const currentVersion = await getCurrentVersion()
- console.log(currentVersion, 'currentVersion')
- const files = res.data?.files || []
- if (!files.length) return
- const latestFile = files[files.length - 1]
- const latestVersion = latestFile
- console.log('last', latestFile, latestVersion)
- if (!latestFile) return
- lastVersion.value = latestVersion
- const lastInstalled = uni.getStorageSync('lastWgtVersion')
- console.log(lastInstalled, 'lastInstalled')
- if (latestVersion === lastInstalled) return
- console.log('版本对比', compareVersion(latestVersion, currentVersion))
- const needUpdate = compareVersion(latestVersion, currentVersion) > 0
- // 保存版本信息
- userStore.saveAppVersion({
- currentVersion,
- version: latestVersion,
- isUpdate: !needUpdate,
- })
- if (!needUpdate) {
- // console.log('当前已是最新版本')
- return
- }
- // 检查是否已跳过此版本
- // const skipVersion = getStorageSync<string>(STORAGE_KEYS.SKIP_VERSION)
- // if (!update.forceUpdate && skipVersion === update.version) {
- // // console.log('用户已跳过此版本更新')
- // return
- // }
- // 显示更新提示
- const url = `${Host80}/wgt/CwgApp_${latestVersion}.wgt`
- console.log('url',url)
- showForceUpdate({ version: latestVersion, forceUpdate: true, wgtUrl: url })
- } catch (error) {
- // console.error('检查更新失败:', error)
- const errorMsg = error instanceof Error ? error.msg : String(error)
- showToast(t('mine.p28') || `检查更新失败: ${errorMsg}`)
- } finally {
- checking.value = false
- }
- // #endif
- }
- // ================== 更新流程 ==================
- /**
- * 显示强制更新弹窗
- */
- function showForceUpdate(update: UpdateInfo): void {
- uni.showModal({
- title: t('mine.p22'),
- content: t('mine.p23'),
- showCancel: false,
- confirmText: t('mine.p35'),
- success: () => doUpdate(update),
- })
- }
- /**
- * 显示可选更新弹窗
- */
- function showOptionalUpdate(update: UpdateInfo): void {
- uni.showModal({
- title: t('mine.p24'),
- content: t('mine.p25', { version: `v${update.version}` }),
- confirmText: t('mine.p35'),
- cancelText: t('mine.p36'),
- success: (res) => {
- if (res.confirm) {
- doUpdate(update)
- } else {
- // 记录跳过的版本
- setStorageSync(STORAGE_KEYS.SKIP_VERSION, update.version)
- }
- },
- })
- }
- /**
- * 执行更新
- */
- function doUpdate(update: UpdateInfo): void {
- console.log(update)
- const platform = getPlatform()
- // ios不走这里面
- if (platform === 'ios') {
- // handleIosUpdate(update)
- } else {
- handleAndroidUpdate(update)
- }
- }
- /**
- * 处理 iOS 更新
- */
- function handleIosUpdate(update: UpdateInfo): void {
- if (!update.iosStoreUrl) {
- showToast(t('mine.p28') || 'iOS 更新链接不存在')
- return
- }
- try {
- plus.runtime.openURL(update.iosStoreUrl)
- } catch (error) {
- // console.error('打开 App Store 失败:', error)
- showToast(t('mine.p28') || '打开 App Store 失败')
- }
- }
- /**
- * 处理 Android 更新
- */
- function handleAndroidUpdate(update: UpdateInfo): void {
- console.log('up2',update)
- if (!update.wgtUrl) {
- showToast(t('mine.p28') || '更新包链接不存在')
- return
- }
- downloadWgt(update.wgtUrl)
- }
- // ================== 下载核心 ==================
- /**
- * 下载 wgt 更新包
- */
- function downloadWgt(url: string): void {
- // #ifdef APP-PLUS
- if (downloadTask) {
- console.log('下载任务已存在')
- return
- }
- if (!url || !isValidUrl(url)) {
- handleDownloadFail(t('mine.p28') || '下载链接无效')
- return
- }
- downloadUrl = url
- retryCount = 0
- updating.value = true
- progress.show(t('mine.p29'))
- // 初始化网络监听
- initNetworkType()
- startNetworkMonitor()
- createDownloadTask(url)
- // #endif
- }
- /**
- * 创建下载任务
- */
- function createDownloadTask(url: string): void {
- try {
- downloadTask = plus.downloader.createDownload(
- url,
- { filename: DOWNLOAD_CONFIG.FILE_PATH },
- handleDownloadComplete,
- )
- if (!downloadTask) {
- throw new Error('创建下载任务失败')
- }
- // 创建状态变化处理器
- stateChangeHandler = handleDownloadStateChanged
- downloadTask.addEventListener('statechanged', stateChangeHandler)
- downloadTask.start()
- } catch (error) {
- // console.error('创建下载任务失败:', error)
- downloadTask = null
- handleDownloadFail(t('mine.p28') || '创建下载任务失败')
- }
- }
- /**
- * 验证 URL 格式
- */
- function isValidUrl(url: string): boolean {
- try {
- return /^https?:\/\/.+/.test(url)
- } catch {
- return false
- }
- }
- /**
- * 处理下载完成回调
- */
- function handleDownloadComplete(download: PlusDownloaderDownload, status: number): void {
- const isSuccess = status === HttpStatus.OK || status === HttpStatus.PARTIAL_CONTENT
- if (isSuccess) {
- // console.log('下载成功,准备安装')
- clearCache()
- stopNetworkMonitor()
- installWgt(download.filename)
- } else {
- // console.error('下载失败,状态码:', status)
- retryDownload()
- }
- }
- /**
- * 处理下载状态变化
- */
- function handleDownloadStateChanged(event: DownloadStateEvent): void {
- if (event.state === DownloadState.DOWNLOADING && event.totalSize > 0) {
- const percent = Math.min(100, Math.floor((event.downloadedSize / event.totalSize) * 100))
- const progressText = `${t('mine.p29')} ${percent}%`
- progress.update(percent, progressText)
- saveCache(percent)
- }
- }
- /**
- * 重试下载
- */
- function retryDownload(): void {
- if (retryCount >= DOWNLOAD_CONFIG.MAX_RETRY) {
- // console.error(`下载失败,已重试 ${retryCount} 次`)
- handleDownloadFail(t('mine.p28') || '下载失败')
- return
- }
- retryCount++
- // console.log(`下载失败,${DOWNLOAD_CONFIG.RETRY_DELAY / 1000}秒后重试 (${retryCount}/${DOWNLOAD_CONFIG.MAX_RETRY})`)
- cleanupDownloadTask()
- setTimeout(() => {
- if (downloadUrl) {
- createDownloadTask(downloadUrl)
- }
- }, DOWNLOAD_CONFIG.RETRY_DELAY)
- }
- /**
- * 初始化网络类型
- */
- function initNetworkType(): void {
- uni.getNetworkType({
- success: (res) => {
- // console.log('当前网络类型:', res.networkType)
- },
- fail: (error) => {
- // console.warn('获取网络类型失败:', error)
- },
- })
- }
- // ================== 网络处理 ==================
- /**
- * 启动网络监听
- */
- function startNetworkMonitor(): void {
- // #ifdef APP-PLUS
- if (networkListener) {
- // console.warn('网络监听已存在')
- return
- }
- networkListener = (res: any) => {
- if (!downloadTask || !updating.value) {
- return
- }
- if (!res.isConnected) {
- // 网络断开,暂停下载
- pauseDownload()
- return
- }
- // 网络恢复,继续下载
- resumeDownload()
- }
- uni.onNetworkStatusChange(networkListener)
- // #endif
- }
- /**
- * 暂停下载
- */
- function pauseDownload(): void {
- if (!downloadTask) return
- try {
- if (typeof downloadTask.pause === 'function') {
- downloadTask.pause()
- }
- } catch (error) {
- // console.error('暂停下载失败:', error)
- }
- }
- /**
- * 恢复下载
- */
- function resumeDownload(): void {
- if (!downloadTask) return
- try {
- if (typeof downloadTask.resume === 'function') {
- downloadTask.resume()
- } else if (typeof downloadTask.start === 'function') {
- downloadTask.start()
- }
- } catch (error) {
- // console.error('恢复下载失败,尝试重新开始:', error)
- // 失败时尝试重新开始
- try {
- if (downloadTask.start) {
- downloadTask.start()
- }
- } catch (e2) {
- // console.error('重新开始下载也失败:', e2)
- }
- }
- }
- /**
- * 停止网络监听
- */
- function stopNetworkMonitor(): void {
- if (networkListener) {
- uni.offNetworkStatusChange(networkListener)
- networkListener = null
- }
- }
- // ================== 安装 ==================
- /**
- * 安装 wgt 更新包
- */
- function installWgt(path: string): void {
- // #ifdef APP-PLUS
- try {
- plus.runtime.install(
- path,
- { force: true },
- () => {
- // 安装成功
- progress.hide()
- updating.value = false
- uni.showModal({
- title: t('mine.p37'),
- content: t('mine.p38'),
- showCancel: false,
- success: () => {
- uni.setStorageSync('lastWgtVersion', lastVersion.value)
- console.log('[wgt] install success:', lastVersion.value)
- uni.setStorageSync('wgtNeedRestart', true)
- plus.runtime.restart()
- },
- })
- },
- (error) => {
- // 安装失败
- // console.error('安装失败:', error)
- handleDownloadFail(t('mine.p33'))
- },
- )
- } catch (error) {
- // console.error('安装异常:', error)
- handleDownloadFail(t('mine.p33'))
- }
- // #endif
- }
- // ================== 缓存管理 ==================
- /**
- * 保存下载缓存
- */
- function saveCache(progressPercent: number): void {
- const cache: DownloadCache = {
- url: downloadUrl,
- progress: progressPercent,
- time: Date.now(),
- }
- setStorageSync(STORAGE_KEYS.DOWNLOAD_CACHE, cache)
- }
- /**
- * 清除下载缓存
- */
- function clearCache(): void {
- removeStorageSync(STORAGE_KEYS.DOWNLOAD_CACHE)
- }
- // ================== 错误处理 ==================
- /**
- * 处理下载失败
- */
- function handleDownloadFail(msg: string): void {
- progress.hide()
- updating.value = false
- retryCount = 0
- stopNetworkMonitor()
- cleanupDownloadTask()
- showToast(msg)
- }
- /**
- * 清理下载任务
- */
- function cleanupDownloadTask(): void {
- if (!downloadTask) return
- try {
- // 移除事件监听
- if (stateChangeHandler && downloadTask.removeEventListener) {
- downloadTask.removeEventListener('statechanged', stateChangeHandler)
- }
- // 中止下载
- downloadTask.abort()
- } catch (error) {
- // console.warn('清理下载任务失败:', error)
- } finally {
- downloadTask = null
- stateChangeHandler = null
- }
- }
- /**
- * 取消更新
- */
- function cancelUpdate(): void {
- if (!updating.value) {
- // console.warn('当前没有正在进行的更新')
- return
- }
- // console.log('用户取消更新')
- handleDownloadFail(t('mine.p39') || '已取消更新')
- }
- // ================== 生命周期 ==================
- onUnmounted(() => {
- // console.log('useAppUpdate 组件卸载,清理资源')
- stopNetworkMonitor()
- cleanupDownloadTask()
- retryCount = 0
- })
- return {
- checkUpdate,
- cancelUpdate,
- checking,
- updating,
- }
- }
|