import request from '@/utils/request.js'
|
|
let hasCheckedVersionThisLaunch = false
|
const IOS_APP_STORE_URL = 'https://apps.apple.com/cn/app/%E6%B5%B7%E8%AF%8A%E5%BA%B7%E9%81%93/id6762694057'
|
|
/**
|
* 版本检查接口要求的平台字段:iOS 传 ios,Android 传 Android。
|
*/
|
export function resolveVersionCheckPlatform() {
|
const platform = String(uni.getSystemInfoSync().platform || '').toLowerCase()
|
if (platform === 'android') {
|
return 'Android'
|
}
|
return 'ios'
|
}
|
|
/**
|
* 去掉展示文案里的 V 前缀,统一版本号格式。
|
*/
|
export function normalizeVersionText(value) {
|
if (typeof value !== 'string') {
|
return ''
|
}
|
const trimmedValue = value.trim()
|
if (!trimmedValue) {
|
return ''
|
}
|
return trimmedValue.replace(/^[vV]/, '')
|
}
|
|
/**
|
* 检查 APP 最新版本信息。
|
*/
|
export function checkAppVersion(showLoading = true) {
|
return request.get('/api/app/version/check', { platform: resolveVersionCheckPlatform() }, false, showLoading)
|
}
|
|
/**
|
* 按真实接口结构归一化版本检查数据。
|
*/
|
export function normalizeVersionCheckData(data) {
|
const source = data && typeof data === 'object' ? data : {}
|
const rawDownloadUrl = typeof source.downloadUrl === 'string' ? source.downloadUrl.trim() : ''
|
return {
|
hasUpdate: Boolean(source.hasUpdate),
|
latestVersionNo: normalizeVersionText(source.latestVersionNo),
|
latestVersionCode: Number.isFinite(Number(source.latestVersionCode)) ? Number(source.latestVersionCode) : null,
|
forceUpdate: Boolean(source.forceUpdate),
|
updateNotice: typeof source.updateNotice === 'string' ? source.updateNotice.trim() : '',
|
downloadUrl: rawDownloadUrl.replace(/^`|`$/g, ''),
|
}
|
}
|
|
/**
|
* 比较当前版本与最新版本是否真的存在升级。
|
* 即使后端误返回 hasUpdate=true,只要版本号相同,前端也不再弹更新提示。
|
*/
|
export function shouldShowVersionUpdate(currentVersion, latestVersion) {
|
const safeCurrentVersion = normalizeVersionText(currentVersion)
|
const safeLatestVersion = normalizeVersionText(latestVersion)
|
if (!safeCurrentVersion || !safeLatestVersion) {
|
return false
|
}
|
if (safeCurrentVersion === safeLatestVersion) {
|
return false
|
}
|
return compareVersionText(safeCurrentVersion, safeLatestVersion) < 0
|
}
|
|
/**
|
* 优先读取运行时版本号,取不到时回退 manifest 配置版本。
|
*/
|
export function resolveRuntimeVersion(fallbackVersion = '') {
|
// #ifdef APP-PLUS
|
try {
|
const runtimeVersion = typeof plus !== 'undefined' && plus.runtime ? normalizeVersionText(plus.runtime.version) : ''
|
if (runtimeVersion) {
|
return runtimeVersion
|
}
|
} catch (e) {
|
console.warn('[app-version] 读取运行时版本失败:', e)
|
}
|
// #endif
|
return normalizeVersionText(fallbackVersion)
|
}
|
|
/**
|
* 当前 App 运行周期内是否还需要自动检查版本。
|
* 返回 true 时会顺便占用这次检查机会,确保只触发一次。
|
*/
|
export function shouldAutoCheckVersionOnLaunch() {
|
if (hasCheckedVersionThisLaunch) {
|
return false
|
}
|
hasCheckedVersionThisLaunch = true
|
return true
|
}
|
|
/**
|
* 统一处理版本更新动作。
|
* - Android:下载 APK 后调用系统安装
|
* - iOS:优先跳转 App Store
|
*/
|
export function startVersionUpdate(downloadUrl, onProgress) {
|
const safeDownloadUrl = typeof downloadUrl === 'string' ? downloadUrl.trim() : ''
|
if (!safeDownloadUrl) {
|
uni.showToast({
|
title: '下载地址缺失',
|
icon: 'none',
|
})
|
return Promise.reject(new Error('downloadUrl is empty'))
|
}
|
|
// #ifdef APP-PLUS
|
const platform = String(uni.getSystemInfoSync().platform || '').toLowerCase()
|
if (platform !== 'android') {
|
notifyVersionUpdateProgress(onProgress, {
|
status: 'redirecting',
|
percent: 0,
|
text: '前往 App Store',
|
})
|
plus.runtime.openURL(resolveIosUpdateUrl(safeDownloadUrl))
|
return Promise.resolve()
|
}
|
return downloadAndInstallAndroidApk(safeDownloadUrl, onProgress)
|
// #endif
|
|
// #ifndef APP-PLUS
|
uni.showToast({
|
title: '请在APP内执行更新',
|
icon: 'none',
|
})
|
return Promise.resolve()
|
// #endif
|
}
|
|
/**
|
* iOS 更新地址统一转成 App Store 可识别的链接。
|
* 优先使用 itms-apps:// 协议直接拉起 App Store。
|
*/
|
function resolveIosUpdateUrl(downloadUrl) {
|
const candidateUrl = IOS_APP_STORE_URL || downloadUrl
|
if (typeof candidateUrl !== 'string') {
|
return ''
|
}
|
const safeDownloadUrl = candidateUrl.trim()
|
if (!safeDownloadUrl) {
|
return ''
|
}
|
if (safeDownloadUrl.startsWith('itms-apps://')) {
|
return safeDownloadUrl
|
}
|
if (safeDownloadUrl.includes('apps.apple.com') || safeDownloadUrl.includes('itunes.apple.com')) {
|
return safeDownloadUrl.replace(/^https?:\/\//i, 'itms-apps://')
|
}
|
return safeDownloadUrl
|
}
|
|
/**
|
* Android 端下载 APK 后拉起系统安装。
|
*/
|
function downloadAndInstallAndroidApk(downloadUrl, onProgress) {
|
return new Promise((resolve, reject) => {
|
try {
|
const downloadTarget = `_doc/update/app-release-${Date.now()}.apk`
|
notifyVersionUpdateProgress(onProgress, {
|
status: 'downloading',
|
percent: 0,
|
text: '正在下载 0%',
|
})
|
const downloadTask = plus.downloader.createDownload(
|
downloadUrl,
|
{
|
filename: downloadTarget,
|
},
|
(download, status) => {
|
if (status !== 200 || !download || !download.filename) {
|
uni.showToast({
|
title: '下载更新失败',
|
icon: 'none',
|
})
|
notifyVersionUpdateProgress(onProgress, {
|
status: 'failed',
|
percent: 0,
|
text: '立即更新',
|
})
|
reject(new Error(`download failed: ${status}`))
|
return
|
}
|
notifyVersionUpdateProgress(onProgress, {
|
status: 'downloaded',
|
percent: 100,
|
text: '安装中...',
|
})
|
installDownloadedApk(download.filename, resolve, reject, onProgress)
|
}
|
)
|
// 监听下载进度,把百分比回传给页面,直接展示到弹窗按钮文案。
|
downloadTask.addEventListener('statechanged', (task) => {
|
const totalSize = Number(task && task.totalSize) || 0
|
const downloadedSize = Number(task && task.downloadedSize) || 0
|
if (totalSize <= 0 || downloadedSize < 0) {
|
return
|
}
|
const percent = Math.min(99, Math.max(0, Math.floor((downloadedSize / totalSize) * 100)))
|
notifyVersionUpdateProgress(onProgress, {
|
status: 'downloading',
|
percent,
|
text: `正在下载 ${percent}%`,
|
})
|
})
|
downloadTask.start()
|
} catch (e) {
|
uni.showToast({
|
title: '启动下载失败',
|
icon: 'none',
|
})
|
notifyVersionUpdateProgress(onProgress, {
|
status: 'failed',
|
percent: 0,
|
text: '立即更新',
|
})
|
reject(e)
|
}
|
})
|
}
|
|
/**
|
* APK 下载完成后,交给系统安装器处理。
|
*/
|
function installDownloadedApk(filePath, resolve, reject, onProgress) {
|
try {
|
notifyVersionUpdateProgress(onProgress, {
|
status: 'installing',
|
percent: 100,
|
text: '安装中...',
|
})
|
// 这里直接交给系统安装器处理,用户确认后即可安装新版本。
|
plus.runtime.install(
|
filePath,
|
{},
|
() => {
|
notifyVersionUpdateProgress(onProgress, {
|
status: 'completed',
|
percent: 100,
|
text: '立即更新',
|
})
|
resolve(filePath)
|
},
|
(error) => {
|
console.error('[app-version] 安装 APK 失败:', error)
|
uni.showToast({
|
title: '拉起安装失败',
|
icon: 'none',
|
})
|
notifyVersionUpdateProgress(onProgress, {
|
status: 'failed',
|
percent: 100,
|
text: '立即更新',
|
})
|
reject(error)
|
}
|
)
|
} catch (e) {
|
uni.showToast({
|
title: '拉起安装失败',
|
icon: 'none',
|
})
|
notifyVersionUpdateProgress(onProgress, {
|
status: 'failed',
|
percent: 100,
|
text: '立即更新',
|
})
|
reject(e)
|
}
|
}
|
|
/**
|
* 通知页面当前更新进度,页面可把文案直接展示到弹窗按钮。
|
*/
|
function notifyVersionUpdateProgress(onProgress, progressInfo) {
|
if (typeof onProgress !== 'function') {
|
return
|
}
|
try {
|
onProgress(progressInfo)
|
} catch (e) {
|
console.warn('[app-version] 更新进度回调失败:', e)
|
}
|
}
|
|
/**
|
* 逐段比较版本号,返回值:
|
* -1:current < latest
|
* 0:current = latest
|
* 1:current > latest
|
*/
|
function compareVersionText(currentVersion, latestVersion) {
|
const currentParts = splitVersionText(currentVersion)
|
const latestParts = splitVersionText(latestVersion)
|
const maxLength = Math.max(currentParts.length, latestParts.length)
|
for (let index = 0; index < maxLength; index += 1) {
|
const currentPart = currentParts[index] || 0
|
const latestPart = latestParts[index] || 0
|
if (currentPart < latestPart) {
|
return -1
|
}
|
if (currentPart > latestPart) {
|
return 1
|
}
|
}
|
return 0
|
}
|
|
/**
|
* 把版本号拆成数字数组,便于逐段比较。
|
*/
|
function splitVersionText(versionText) {
|
return String(versionText || '')
|
.split('.')
|
.map((item) => Number.parseInt(item, 10))
|
.filter((item) => Number.isFinite(item))
|
}
|