325 lines
8.2 KiB
JavaScript
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()
|