Files
shanxia2.0/miniprogram/utils/mpUpdate.js
T
2026-09-28 10:47:42 +08:00

325 lines
8.2 KiB
JavaScript

/**
* 微信小程序正式版更新 + 版本/状态可见性。
* 新包就绪后强制重启;「我的」页可展示「已是最新 / 有新版本…」。
*
* @see docs/superpowers/specs/2026-09-26-mp-weixin-force-update-design.md
* @see docs/superpowers/specs/2026-09-26-mp-version-status-design.md
*/
const SHOW_DEBOUNCE_MS = 30 * 1000
const USER_CHECK_FALLBACK_MS = 2800
const ENV_LABEL = {
release: '正式版',
trial: '体验版',
develop: '开发版'
}
let listenersBound = false
let readyModalShown = false
let failedModalShown = false
let lastCheckAt = 0
let cachedUpdateManager = null
let userToastUnsub = null
let userFallbackTimer = null
let snapshot = {
status: 'idle',
version: '',
envVersion: '',
envLabel: '',
isRelease: false,
hasUpdate: false
}
const listeners = new Set()
function notify() {
const copy = { ...snapshot }
listeners.forEach((fn) => {
try {
fn(copy)
} catch (e) {
console.warn('[mpUpdate] listener error', e)
}
})
}
function patchSnapshot(partial) {
snapshot = { ...snapshot, ...partial }
notify()
}
/**
* @returns {{ version: string, envVersion: string, envLabel: string, isRelease: boolean }}
*/
export function getMpAccountInfo() {
// #ifdef MP-WEIXIN
try {
if (typeof uni.getAccountInfoSync === 'function') {
const mini = (uni.getAccountInfoSync() || {}).miniProgram || {}
const envVersion = mini.envVersion || ''
return {
version: mini.version || '',
envVersion,
envLabel: ENV_LABEL[envVersion] || envVersion || '未知',
isRelease: envVersion === 'release'
}
}
} catch (e) {
console.warn('[mpUpdate] getAccountInfoSync failed', e)
}
// #endif
return {
version: '',
envVersion: '',
envLabel: '',
isRelease: false
}
}
export function getMpUpdateSnapshot() {
return { ...snapshot }
}
/**
* @param {(s: typeof snapshot) => void} listener
* @returns {() => void}
*/
export function subscribeMpUpdate(listener) {
listeners.add(listener)
listener({ ...snapshot })
return () => listeners.delete(listener)
}
function refreshAccountIntoSnapshot(statusOverride) {
const account = getMpAccountInfo()
const next = {
version: account.version,
envVersion: account.envVersion,
envLabel: account.envLabel,
isRelease: account.isRelease,
hasUpdate: snapshot.hasUpdate,
status: snapshot.status
}
if (!account.isRelease) {
next.status = account.envVersion ? 'unsupported' : snapshot.status
next.hasUpdate = false
} else if (statusOverride) {
next.status = statusOverride
} else if (snapshot.status === 'unsupported') {
next.status = 'idle'
}
patchSnapshot(next)
}
function showReadyModal(updateManager) {
if (readyModalShown) return
readyModalShown = true
uni.showModal({
title: '更新提示',
content: '新版本已准备好,需要重启以使用最新功能',
showCancel: false,
confirmText: '立即重启',
success: (res) => {
if (res.confirm) {
updateManager.applyUpdate()
}
}
})
}
function clearUserFeedbackHooks() {
if (userToastUnsub) {
userToastUnsub()
userToastUnsub = null
}
if (userFallbackTimer) {
clearTimeout(userFallbackTimer)
userFallbackTimer = null
}
}
function toastForStatus(status) {
if (status === 'latest') {
uni.showToast({ title: '已是最新', icon: 'none' })
} else if (status === 'downloading') {
uni.showToast({ title: '发现新版本,正在下载', icon: 'none' })
} else if (status === 'failed') {
uni.showToast({ title: '更新失败,请稍后重试', icon: 'none' })
}
}
function feedbackFromKnownStatus() {
const { status } = snapshot
if (status === 'ready' && cachedUpdateManager) {
readyModalShown = false
showReadyModal(cachedUpdateManager)
return true
}
if (status === 'latest' || status === 'downloading' || status === 'failed') {
toastForStatus(status)
return true
}
return false
}
function armUserCheckFeedback() {
clearUserFeedbackHooks()
const startedStatus = snapshot.status
userToastUnsub = subscribeMpUpdate((s) => {
if (s.status === startedStatus && s.status === 'checking') return
if (s.status === 'latest' || s.status === 'downloading' || s.status === 'failed') {
toastForStatus(s.status)
clearUserFeedbackHooks()
} else if (s.status === 'ready') {
clearUserFeedbackHooks()
}
})
userFallbackTimer = setTimeout(() => {
userFallbackTimer = null
if (snapshot.status === 'checking') {
// 微信未必再次回调:无 hasUpdate 信号时按「已是最新」收口
patchSnapshot({ status: 'latest', hasUpdate: false })
uni.showToast({ title: '已是最新', icon: 'none' })
}
clearUserFeedbackHooks()
}, USER_CHECK_FALLBACK_MS)
}
function bindUpdateListeners(updateManager) {
if (listenersBound) return
listenersBound = true
updateManager.onCheckForUpdate((res) => {
const hasUpdate = !!(res && res.hasUpdate)
console.log('[mpUpdate] onCheckForUpdate hasUpdate=', hasUpdate)
if (!snapshot.isRelease) {
patchSnapshot({ status: 'unsupported', hasUpdate: false })
return
}
if (hasUpdate) {
if (snapshot.status !== 'ready') {
patchSnapshot({ status: 'downloading', hasUpdate: true })
} else {
patchSnapshot({ hasUpdate: true })
}
} else {
patchSnapshot({ status: 'latest', hasUpdate: false })
}
})
updateManager.onUpdateReady(() => {
console.log('[mpUpdate] onUpdateReady')
patchSnapshot({ status: 'ready', hasUpdate: true })
showReadyModal(updateManager)
})
updateManager.onUpdateFailed(() => {
console.log('[mpUpdate] onUpdateFailed')
patchSnapshot({ status: 'failed', hasUpdate: snapshot.hasUpdate })
if (failedModalShown) return
failedModalShown = true
uni.showModal({
title: '更新失败',
content: '新版本下载失败,请删除小程序后重新搜索打开,或检查网络后重试',
showCancel: false
})
})
}
/**
* @param {{ force?: boolean, fromUser?: boolean }} [options]
*/
export function checkMpUpdate(options = {}) {
// #ifdef MP-WEIXIN
const force = !!options.force
const fromUser = !!options.fromUser
refreshAccountIntoSnapshot()
if (!snapshot.isRelease) {
if (fromUser) {
uni.showToast({
title: '开发/体验版无正式更新流',
icon: 'none'
})
}
return
}
if (fromUser && feedbackFromKnownStatus()) {
return
}
const now = Date.now()
if (!force && lastCheckAt && now - lastCheckAt < SHOW_DEBOUNCE_MS) {
return
}
lastCheckAt = now
if (typeof uni.canIUse !== 'function' || !uni.canIUse('getUpdateManager')) {
if (fromUser) {
uni.showToast({ title: '当前基础库不支持检查更新', icon: 'none' })
}
return
}
if (fromUser || snapshot.status === 'idle') {
patchSnapshot({ status: 'checking' })
}
const updateManager = uni.getUpdateManager()
cachedUpdateManager = updateManager
bindUpdateListeners(updateManager)
if (fromUser) {
armUserCheckFeedback()
}
// #endif
}
/**
* @param {ReturnType<typeof getMpUpdateSnapshot>} [s]
*/
export function formatMpUpdateLine(s) {
const state = s || getMpUpdateSnapshot()
if (state.status === 'unsupported' || (!state.isRelease && state.envVersion)) {
const env = state.envLabel || '非正式版'
return `${env} · 无正式更新流`
}
const ver = state.version ? `版本 ${state.version}` : '版本 —'
const env = state.envLabel || '正式版'
let tip = '点此检查更新'
switch (state.status) {
case 'checking':
tip = '检查中…'
break
case 'latest':
tip = '已是最新'
break
case 'downloading':
tip = '有新版本(下载中)'
break
case 'ready':
tip = '有新版本,点此更新'
break
case 'failed':
tip = '更新失败'
break
default:
tip = '点此检查更新'
}
return `${ver} · ${env} · ${tip}`
}
/** 是否需要用户关注更新(下载中 / 待重启 / 失败) */
export function isMpUpdateUrgent(s) {
const status = (s || getMpUpdateSnapshot()).status
return status === 'downloading' || status === 'ready' || status === 'failed'
}
refreshAccountIntoSnapshot()