app版本更新(vue3+unibest+静默更新+强制更新)

我在我的app首页进行检查更新(index.vue)-通过后端的接口

c 复制代码
<script lang="ts" setup>
import { checkAppUpdate } from '@/utils/update'
import UpdateProgressDialog from '@/components/global/UpdateProgressDialog.vue'
// 标记是否首次加载完成
let isFirstLoad = true
onShow(async () => {
// ✅ 首次加载:加载所有数据
  if (isFirstLoad) {
    isFirstLoad = false
     // 登录成功后检查APP更新
    // #ifdef APP-PLUS
    setTimeout(() => {
      checkAppUpdate({ silent: true })
    }, 500)
    // #endif
  } else {
    // ✅ 后续显示:只刷新列表
    if (paging.value) {
      paging.value.reload()
    }
  }
})
onUnload(() => {
  // 防止内存泄漏
  uni.$off('pushClick')
})
//在页面引用自定义进度弹框
</template>
  <UpdateProgressDialog />

</template>

自定义弹框样式代码

c 复制代码
<script lang="ts" setup>
import { ref, computed, onBeforeUnmount, onMounted } from 'vue'
import type { AppUpdateProgressPayload } from '@/utils/update'
import { onAppUpdateProgress } from '@/utils/update'

onMounted(() => {
})

const visible = ref(false)
const percent = ref(0)
const downloadedBytes = ref(0)
const totalBytes = ref(0)
const title = ref('版本更新中')
const subtitle = ref('')
const forceFlag = ref(false)
const errorMsg = ref('')
const showError = ref(false)
const firstPaintDone = ref(false)

const formatBytes = (bytes: number): string => {
  if (bytes <= 0) return '0KB'
  if (bytes < 1024) return bytes + 'B'
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB'
  if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + 'MB'
  return (bytes / 1024 / 1024 / 1024).toFixed(2) + 'GB'
}

const subtitleComputed = computed<string>(() => {
  if (subtitle.value) return subtitle.value
  if (totalBytes.value > 0) {
    return `${formatBytes(downloadedBytes.value)} / ${formatBytes(totalBytes.value)}`
  }
  return `已下载 ${formatBytes(downloadedBytes.value)}`
})

const percentText = computed<string>(() => {
  if (totalBytes.value > 0) {
    return `${percent.value}%`
  }
  return ''
})

const fillWidthStyle = computed<string>(() => {
  const total = totalBytes.value
  const pct = percent.value
  if (total > 0) {
    return pct <= 0 ? '0%' : `${pct}%`
  }
  return pct <= 0 ? '0%' : `${Math.min(95, pct)}%`
})

const fillTransitionClass = computed<string>(() => {
  return firstPaintDone.value && percent.value > 0
    ? 'update-progress-bar-fill animating'
    : 'update-progress-bar-fill'
})

const handleUpdateProgress = (payload: AppUpdateProgressPayload) => {
  if (payload.visible) {
    visible.value = true
    downloadedBytes.value = payload.downloadedBytes ?? 0
    totalBytes.value = payload.totalBytes ?? 0
    title.value = payload.title ?? '版本更新中'
    subtitle.value = payload.subtitle ?? ''
    forceFlag.value = !!payload.force
    percent.value = Math.max(0, Math.min(99, payload.percent ?? 0))
    if (payload.error) {
      errorMsg.value = payload.error
      showError.value = true
      setTimeout(() => {
        showError.value = false
        errorMsg.value = ''
      }, 3000)
    }
    if (!firstPaintDone.value) {
      setTimeout(() => {
        firstPaintDone.value = true
      }, 50)
    }
  } else {
    visible.value = false
    percent.value = 0
    downloadedBytes.value = 0
    totalBytes.value = 0
    forceFlag.value = false
    firstPaintDone.value = false
  }
}

const offHandler = onAppUpdateProgress(handleUpdateProgress)

onBeforeUnmount(() => {
  offHandler && offHandler()
})
</script>

<template>
  <view v-if="visible" class="update-progress-mask">
    <view class="update-progress-dialog">
      <view class="update-title">{{ title }}</view>
      <view v-if="showError && errorMsg" class="update-error">{{ errorMsg }}</view>
      <view class="update-progress-wrap">
        <view class="update-progress-bar-bg">
          <view
            :class="fillTransitionClass"
            :style="{ width: fillWidthStyle }"
          />
        </view>
        <text v-if="percentText" class="update-percent">{{ percentText }}</text>
      </view>
      <view class="update-subtitle">{{ subtitleComputed }}</view>
    </view>
  </view>
</template>

<style lang="scss" scoped>
.update-progress-mask {
  position: fixed;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 999999;
  display: flex;
  align-items: center;
  justify-content: center;
}

.update-progress-dialog {
  width: 600rpx;
  background: #ffffff;
  border-radius: 24rpx;
  padding: 48rpx 40rpx 40rpx 40rpx;
  box-shadow: 0 12rpx 48rpx rgba(0, 0, 0, 0.15);
}

.update-title {
  font-size: 34rpx;
  font-weight: 600;
  color: #1a1a1a;
  line-height: 1.4;
  margin-bottom: 12rpx;
}

.update-error {
  font-size: 26rpx;
  color: #f56c6c;
  line-height: 1.5;
  background: #fef0f0;
  border-radius: 8rpx;
  padding: 12rpx 16rpx;
  margin-bottom: 16rpx;
}

.update-progress-wrap {
  display: flex;
  align-items: center;
  margin-top: 28rpx;
  gap: 24rpx;
}

.update-progress-bar-bg {
  flex: 1;
  height: 28rpx;
  background: #e9ecef;
  border-radius: 999rpx;
  overflow: hidden;
}

.update-progress-bar-fill {
  height: 100%;
  background: linear-gradient(90deg, #3370ff 0%, #5a8cff 100%);
  border-radius: 999rpx;
  width: 0;
}

.update-progress-bar-fill.animating {
  transition: width 0.3s ease-out;
}

.update-percent {
  min-width: 110rpx;
  text-align: right;
  font-size: 34rpx;
  font-weight: 600;
  color: #1a1a1a;
  line-height: 1;
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}

.update-subtitle {
  margin-top: 20rpx;
  font-size: 24rpx;
  color: #86909c;
  line-height: 1.4;
  font-variant-numeric: tabular-nums;
}
</style>

这是untils/updates.ts文件代码,app版本更新的主力代码

c 复制代码
//import { httpGet } from '@/http/http' //@/api/version文件代码
//export interface AppVersionInfo {
//  id?: number
 // version: string
 // type: 1 | 2           // 1=wgt热更新, 2=apk整包更新
 // downUrl: string       // 下载地址
 // content?: string      // 更新内容
 // isUpdate: 1 | 2       // 1=强制更新, 2=非强制更新
 // createTime?: string
//}
//export function checkAppVersion() {
 // return httpGet<any>('/screen/getAppVersion')
//}*
import { checkAppVersion, type AppVersionInfo } from '@/api/version'

export interface AppUpdateProgressPayload {
  visible: boolean
  percent: number
  downloadedBytes: number
  totalBytes: number
  title: string
  subtitle?: string
  force?: boolean
  error?: string
}

let progressListenerCount = 0
const EVENT_NAME = 'app-update-progress'

export function onAppUpdateProgress(cb: (payload: AppUpdateProgressPayload) => void) {
  uni.$on(EVENT_NAME, cb)
  progressListenerCount++
  return () => {
    uni.$off(EVENT_NAME, cb)
    progressListenerCount = Math.max(0, progressListenerCount - 1)
  }
}

function emitProgress(payload: Partial<AppUpdateProgressPayload> & { visible: boolean }) {
  uni.$emit(EVENT_NAME, payload)
}

function hasProgressComponent(): boolean {
  return progressListenerCount > 0
}

function getCurrentVersion() : Promise<{ versionName : string, versionCode : number }> {
	return new Promise((resolve) => {
		let versionName = '1.0.0'
		let versionCode = 100
		// #ifdef APP-PLUS
		try {
			plus.runtime.getProperty(plus.runtime.appid, (widgetInfo) => {
				if (widgetInfo && widgetInfo.version) {
					versionName = widgetInfo.version
				}
				resolve({ versionName, versionCode })
			})
		}
		catch (e) {
			console.error('获取版本号失败', e)
			resolve({ versionName, versionCode })
		}
		// #endif
		// #ifndef APP-PLUS
		resolve({ versionName, versionCode })
		// #endif
	})
}

function normalizeVersion(version : string) : string {
	return version.replace(/^v/i, '').replace(/^V/, '')
}

function compareVersion(v1 : string, v2 : string) : number {
	const arr1 = normalizeVersion(v1).split('.').map(Number)
	const arr2 = normalizeVersion(v2).split('.').map(Number)
	for (let i = 0; i < Math.max(arr1.length, arr2.length); i++) {
		const n1 = arr1[i] || 0
		const n2 = arr2[i] || 0
		if (n1 > n2) return 1
		if (n1 < n2) return -1
	}
	return 0
}

async function fetchVersionInfo() : Promise<AppVersionInfo | null> {
	try {
		const res = await checkAppVersion()
		// console.log('=== 原始版本接口返回 ===', res)
		//{ "id": 1,
   // "version": "v1.0.1",
   // "type": 2, //1静默更新  2强制更新
  //  "downUrl": "http://221.179.31.22:8990/profile/upload/2026/09/09/zg.apk",
  //  "content": "初始版",
   // "isUpdate": 1,//1不强制更新    2强制更新 }

		let versionInfo : any = null

		if (res?.data && typeof res.data === 'object' && res.data.version) {
			versionInfo = res.data
		} else if (res?.version) {
			versionInfo = res
		}

		return versionInfo as AppVersionInfo | null
	} catch (e : any) {
		console.error('获取版本信息失败', e)
		return null
	}
}

/** WGT 热更新:下载 + 安装 + 重启 */
function downloadAndInstallWgt(url : string, silent = false) : Promise<void> {
	return new Promise((resolve, reject) => {
		// #ifdef APP-PLUS
		let showing = false
		let finished = false
		let timeoutTimer : any = null

		const safeHide = () => {
			if (showing) {
				uni.hideLoading()
				showing = false
			}
			if (timeoutTimer) {
				clearTimeout(timeoutTimer)
				timeoutTimer = null
			}
		}

		if (!silent) {
			uni.showLoading({ title: '更新中...', mask: true })
			showing = true
		}

		// 超时兜底:防止下载回调不触发导致 loading 一直转
		timeoutTimer = setTimeout(() => {
			if (finished) return
			finished = true
			safeHide()
			reject(new Error('WGT下载超时'))
		}, 60000)

		const dtask = plus.downloader.createDownload(url, {}, (d, status) => {
			if (finished) return
			safeHide()
			if (status === 200 && d.filename) {
				plus.runtime.install(
					d.filename,
					{ force: false },
					() => {
						finished = true
						uni.setStorageSync('updateJustCompleted', true)
						uni.showToast({ title: '更新完成,正在重启...', icon: 'none' })
						setTimeout(() => {
							plus.runtime.restart()
						}, 1500)
						resolve()
					},
					(e : any) => {
						finished = true
						console.error('安装WGT失败', e)
						reject(e)
					},
				)
			} else {
				finished = true
				reject(new Error(`下载失败: ${status}`))
			}
		})
		dtask.start()
		// #endif
		// #ifndef APP-PLUS
		reject(new Error('仅支持APP端更新'))
		// #endif
	})
}

/**
 * APK 整包更新:下载 + 安装
 * @param url 下载地址
 * @param force 是否强制更新(强制更新时失败可重试)
 * @param retryCount 内部使用,当前已重试次数
 */
function downloadAndInstallApk(url: string, force = false, retryCount = 0): Promise<void> {
  const MAX_RETRY = 3
  return new Promise((resolve, reject) => {
    // #ifdef APP-PLUS
    const cleanUrl = String(url || '').trim().replace(/^[`'"\s]+|[`'"\s]+$/g, '')
    if (!cleanUrl) {
      uni.showModal({ title: '错误', content: '下载地址无效', showCancel: false })
      reject(new Error('下载地址无效'))
      return
    }

    const useComponent = hasProgressComponent()

    const waiting: { close: () => void; setTitle?: (t: string) => void } | null =
      useComponent ? null : plus.nativeUI.showWaiting('版本更新中 0%', { modal: true })

    let finished = false
    let lastPercentProgress = -1
    let lastBytesProgress = -1
    let maxDownloadedSize = 0
    let knownTotal = 0
    let timeoutTimer: any = null
    let pollTimer: any = null
    let forceRefreshCounter = 0
    let lastSetTitleText = ''
    let zwToggle = false
    let lastTaskState = -1

    const formatBytes = (bytes: number): string => {
      if (bytes <= 0) return '0KB'
      if (bytes < 1024) return bytes + 'B'
      if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB'
      if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + 'MB'
      return (bytes / 1024 / 1024 / 1024).toFixed(2) + 'GB'
    }

    const setWaitingTitle = (text: string, forceRender = false) => {
      if (useComponent || !waiting || !waiting.setTitle) return
      if (forceRender) {
        zwToggle = !zwToggle
        const zw = zwToggle ? '\u200B' : ''
        waiting.setTitle(text + zw)
        lastSetTitleText = text
      } else if (text !== lastSetTitleText) {
        waiting.setTitle(text)
        lastSetTitleText = text
      }
    }

    const emitUI = (percent: number, done: number, total: number, forceRender = false) => {
      const subtitle = total > 0
        ? `${formatBytes(done)} / ${formatBytes(total)}`
        : `已下载 ${formatBytes(done)}`
      if (useComponent) {
        emitProgress({
          visible: true,
          percent,
          downloadedBytes: done,
          totalBytes: total,
          title: '版本更新中',
          subtitle,
          force,
        })
      } else {
        const text = total > 0 ? `版本更新中 ${percent}%` : `版本更新中 ${formatBytes(done)}`
        setWaitingTitle(text, forceRender)
      }
    }

    const safeClose = () => {
      if (!finished) {
        finished = true
        if (waiting && waiting.close) waiting.close()
        if (useComponent) emitProgress({ visible: false, percent: 0, downloadedBytes: 0, totalBytes: 0, title: '' })
      }
      if (timeoutTimer) { clearTimeout(timeoutTimer); timeoutTimer = null }
      if (pollTimer) { clearInterval(pollTimer); pollTimer = null }
    }

    const handleFail = (errCode: number, msg: string) => {
      if (finished) return
      const errorText = `错误码:${errCode}`
      if (useComponent) {
        emitProgress({
          visible: true,
          percent: lastPercentProgress,
          downloadedBytes: maxDownloadedSize,
          totalBytes: knownTotal,
          title: '版本更新中',
          force,
          error: errorText,
        })
      }
      safeClose()
      console.error('APK下载失败:', { errCode, url: cleanUrl, msg })

      const canRetry = force && retryCount < MAX_RETRY
      uni.showModal({
        title: '下载失败',
        content: canRetry
          ? `${errorText},请检查网络后重试(${retryCount + 1}/${MAX_RETRY})`
          : `${errorText},请检查网络或下载地址`,
        showCancel: false,
        confirmText: canRetry ? '重试' : '我知道了',
        success: (res) => {
          if (res.confirm && canRetry) {
            downloadAndInstallApk(cleanUrl, force, retryCount + 1)
          }
        },
      })
      reject(new Error(`下载失败: ${errCode}`))
    }

    const resetTimeout = () => {
      if (timeoutTimer) clearTimeout(timeoutTimer)
      timeoutTimer = setTimeout(() => {
        handleFail(-1, '下载超时:长时间无数据响应,请检查网络')
      }, 120000)
    }
    resetTimeout()
    emitUI(0, 0, 0)

    const applyProgressData = (rawDone: number, rawTotal: number) => {
      const done = Number(rawDone) || 0
      const total = Number(rawTotal) || 0

      if (total > 0 && total > knownTotal) knownTotal = total
      if (done > maxDownloadedSize) {
        maxDownloadedSize = done
        resetTimeout()
      }

      forceRefreshCounter++
      const shouldForceRefresh = forceRefreshCounter % 10 === 0
      const effectiveDone = Math.max(done, maxDownloadedSize)

      if (knownTotal > 0) {
        let progress = Math.floor((effectiveDone / knownTotal) * 100)
        if (!Number.isFinite(progress)) progress = 0
        progress = Math.max(0, Math.min(99, progress))
        if (progress < lastPercentProgress) progress = lastPercentProgress
        if (progress !== lastPercentProgress || shouldForceRefresh) {
          lastPercentProgress = progress
          emitUI(progress, effectiveDone, knownTotal, shouldForceRefresh)
        }
      } else {
        if (effectiveDone !== lastBytesProgress || shouldForceRefresh) {
          lastBytesProgress = effectiveDone
          emitUI(0, effectiveDone, 0, shouldForceRefresh)
        }
      }
    }

    const dtask = plus.downloader.createDownload(cleanUrl, {
      retry: 2,
      timeout: 120,
    }, (d, status) => {
      if (finished) return
      if (status === 200 && d.filename) {
        safeClose()
        plus.runtime.install(
          d.filename,
          { force: false },
          () => {
            uni.setStorageSync('updateJustCompleted', true)
            resolve()
          },
          (e: any) => {
            uni.showModal({
              title: '安装失败',
              content: '请手动安装更新包',
              showCancel: false,
            })
            reject(e)
          },
        )
      } else {
        const errCode = status || d.statusCode || -1
        handleFail(errCode, '下载失败')
      }
    })

    // 事件主通道
    dtask.addEventListener('statechanged', (task: any) => {
      if (finished) return
      const taskState = Number(task.state) || 0
      if (taskState !== lastTaskState) {
        lastTaskState = taskState
      }
      if (taskState !== 5) {
        const eventTotal = Number(task.totalSize) || 0
        const eventDone = Number(task.downloadedSize) || 0
        if (eventDone > 0 || eventTotal > 0) applyProgressData(eventDone, eventTotal)
      }
    })

    // 轮询副通道兜底
    const startPoll = () => {
      if (pollTimer) return
      pollTimer = setInterval(() => {
        if (finished) { if (pollTimer) { clearInterval(pollTimer); pollTimer = null } return }
        const pollTotal = Number(dtask.totalSize) || 0
        const pollDone = Number(dtask.downloadedSize) || 0
        const pollState = Number(dtask.state) || 0
        if (pollState !== lastTaskState) {
          lastTaskState = pollState
        }
        applyProgressData(pollDone, pollTotal)
        if (pollState === 5) {
          const errCode = Number(dtask.errorCode) || dtask.statusCode || -2
          handleFail(errCode, '下载器状态异常')
        }
      }, 500)
    }
    startPoll()

    dtask.start()
    // #endif
    // #ifndef APP-PLUS
    reject(new Error('仅支持APP端更新'))
    // #endif
  })
}
export async function checkAppUpdate(options : { silent ?: boolean, ignoreForce ?: boolean } = {}) {
	const { silent: optSilent = false, ignoreForce: optIgnoreForce = false } = options

	try {
		const { versionName } = await getCurrentVersion()
		const versionInfo = await fetchVersionInfo()

		if (!versionInfo) {
			return
		}

		const remoteVersion = versionInfo.version
		const hasNewVersion = compareVersion(versionName, remoteVersion) < 0

		if (!hasNewVersion) {
			// #ifdef APP-PLUS
			uni.hideLoading()
			// #endif
			if (!optSilent) {
				uni.showToast({ title: '当前已是最新版本', icon: 'none' })
			}
			return
		}

		// 有新版本
		// #ifdef APP-PLUS
		uni.hideLoading()
		// #endif

		const updateType = versionInfo.type // 1=wgt, 2=apk
		const downloadUrl = versionInfo.downUrl

		if (!downloadUrl) {
			uni.showToast({ title: '下载地址无效', icon: 'none' })
			return
		}

		// WGT 热更新:静默下载安装,不弹框
		if (updateType === 1) {
			downloadAndInstallWgt(downloadUrl, false)
			return
		}

		// APK 整包更新:弹框让用户确认
		const isForceUpdate = versionInfo.isUpdate === 1 && !optIgnoreForce

		const showUpdateDialog = () => {
			uni.showModal({
				title: isForceUpdate ? '发现新版本(强制更新)' : '发现新版本',
				content: versionInfo.content || `新版本 v${normalizeVersion(remoteVersion)} 已发布`,
				confirmText: '立即更新',
				cancelText: isForceUpdate ? '退出应用' : '稍后再说',
				showCancel: true,
				success: (res) => {
					if (res.confirm) {
						downloadAndInstallApk(downloadUrl, isForceUpdate)
					} else {
						if (isForceUpdate) {
							// #ifdef APP-PLUS
							try {
								// 先尝试调用系统 Activity 的 finishAffinity,确保彻底退出
								const main = plus.android.runtimeMainActivity()
								if (main) {
									main.finishAffinity()
								}
							} catch (e) {
								console.error('finishAffinity 失败', e)
							}
							// 兜底:plus.runtime.exit
							setTimeout(() => {
								try { plus.runtime.exit() } catch (e) {}
								// #ifdef ANDROID
								try { plus.android.importClass('android.os.Process'); android.os.Process.killProcess(android.os.Process.myPid()) } catch (e) {}
								// #endif
							}, 300)
							// #endif
						}
					}
				},
			})
		}

		showUpdateDialog()
	} catch (e) {
		// #ifdef APP-PLUS
		uni.hideLoading()
		// #endif
		console.error('检查更新出错:', e)
		if (!optSilent) {
			uni.showToast({ title: '检查更新失败', icon: 'none' })
		}
	}
}

export { getCurrentVersion, fetchVersionInfo, compareVersion }

本文用于记录,方便自己后期查看

相关推荐
零依赖极客44 分钟前
《30 天手搓 ARM 架构零依赖纯 C 推理引擎》课程介绍与大纲
c语言·开发语言·arm开发·人工智能·嵌入式硬件·ai编程
qq_452396231 小时前
第三篇:《变量、类型与函数:Rust 的“基本盘”》
开发语言·后端·rust
chushiyunen1 小时前
mockito笔记
java·开发语言·笔记
悟天特斯1 小时前
AI驱动的楼宇节能:从粗放管控到精准降碳的实践路径
开发语言·人工智能·python·物联网
名字还没想好☜1 小时前
Python 用 tempfile 安全创建临时文件:NamedTemporaryFile、TemporaryDirectory 与别自己拼 /tmp 的坑
开发语言·后端·python·安全·编程语言
lvshuocool1 小时前
golang 多版本管理工具 -- g
开发语言·后端·golang
matlab代码1 小时前
基于matlab数字图像处理的指纹识别系统 点线特征(GUI界面)【源码69期】
开发语言·matlab
linx2951 小时前
单元三 · 那 C 的底层知识怎么办
c语言·开发语言·数据结构·c++·嵌入式硬件