我在我的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 }
本文用于记录,方便自己后期查看