import appSettings from '@/core/config/app-settings' import { buildRestPath } from '@/core/config/api-paths' import { get } from '@/core/http/http-client' import { STORAGE } from '@/core/constants/storage-keys' /** * 解析更新包 / App Store 等跳转地址。 * 绝对 URI(https://、itms-apps:// 等)不再拼 CDN;相对路径才拼 cdnurl。 */ function resolveUpdateAssetUrl(path) { let p = String(path || '').trim().replace(/^\uFEFF/, '') if (!p) return '' p = p.replace(/[\u200B-\u200D\uFEFF\u2060]/g, '') p = p.replace(/(https?)\uFF1A(\/\/)/gi, '$1:$2') p = p.replace(/(https?)(\/\/)/gi, (m, scheme, slashes, offset, str) => { const prev = offset > 0 ? str[offset - 1] : '' return prev === ':' ? m : `${scheme}:${slashes}` }) p = p.replace(/^(https?):\/([^/])/i, '$1://$2') const absIdx = p.search(/https?:\/\//i) if (absIdx > 0) { p = p.slice(absIdx) } if (/^\/\//.test(p)) { p = `https:${p}` } if (/^[a-z][a-z0-9+.-]*:/i.test(p)) { return p } const base = String(appSettings.cdnurl || '').replace(/\/+$/, '') const rel = p.replace(/^\/+/, '') return base ? `${base}/${rel}` : p } function normalizeUpdateContent(data) { const copy = { ...data } copy.content = String(copy.content || '').replace(/(\r\n)|(\n)/g, '
') return copy } function buildDownloadLink(data) { const os = plus.os.name.toLowerCase() const linkKey = os === 'ios' ? 'iosLink' : 'androidLink' const raw = data[linkKey] if (!raw) return null const url = resolveUpdateAssetUrl(raw) if (!url) return null return { url, type: /\.wgt/i.test(url) ? 'wgt' : 'url' } } async function fetchRemoteUpdate() { const { data } = await get(buildRestPath('common/update')) if (!data || typeof data !== 'object') return null const remoteCode = Number(data.versionCode) const localCode = Number(appSettings.versionCode) if (!Number.isFinite(remoteCode) || remoteCode <= localCode) return null return data } function presentUpdate({ commit, dispatch }, data) { const meta = normalizeUpdateContent(data) commit('assign', { key: 'meta', value: meta }) // #ifdef APP-PLUS const link = buildDownloadLink(meta) if (!link) { commit('assign', { key: 'visible', value: false }) return { hasUpdate: true, hasLink: false, data: meta } } commit('assign', { key: 'link', value: link }) if (Number(meta.enforce) === 1) { dispatch('startDownload') return { hasUpdate: true, hasLink: true, data: meta, enforced: true } } commit('assign', { key: 'visible', value: true }) return { hasUpdate: true, hasLink: true, data: meta } // #endif // #ifndef APP-PLUS commit('assign', { key: 'visible', value: true }) return { hasUpdate: true, hasLink: true, data: meta } // #endif } export default { namespaced: true, state: { visible: false, meta: {}, link: {}, download: { path: null, start: false, install: false, progress: 0, totalBytesWritten: 0, totalBytesExpectedToWrite: 0 }, task: null }, mutations: { assign(state, { key, value }) { state[key] = value } }, actions: { check({ dispatch }) { // #ifdef MP const mgr = uni.getUpdateManager() mgr.onUpdateReady(() => { uni.showModal({ title: '更新提示', content: '新版本已就绪,是否重启应用?', success: (res) => res.confirm && mgr.applyUpdate() }) }) // #endif // #ifdef APP-PLUS const cache = uni.getStorageSync(STORAGE.update) if (cache?.ignore) return Promise.resolve({ hasUpdate: false, ignored: true }) return dispatch('checkManual', { silent: true }) // #endif // #ifndef APP-PLUS return Promise.resolve({ hasUpdate: false }) // #endif }, /** * 手动检查更新(关于我们页):忽略「不再提醒」,无新版本时由调用方 toast。 */ async checkManual({ commit, dispatch }, { silent = false } = {}) { // #ifdef MP try { const mgr = uni.getUpdateManager() mgr.onCheckForUpdate((res) => { if (!res.hasUpdate && !silent) { uni.showToast({ title: '已是最新版本', icon: 'none' }) } }) mgr.onUpdateReady(() => { uni.showModal({ title: '更新提示', content: '新版本已就绪,是否重启应用?', success: (r) => r.confirm && mgr.applyUpdate() }) }) } catch (_) {} return { hasUpdate: false } // #endif // #ifdef APP-PLUS try { const data = await fetchRemoteUpdate() if (!data) { return { hasUpdate: false } } return presentUpdate({ commit, dispatch }, data) } catch (e) { if (!silent) throw e return { hasUpdate: false, error: e } } // #endif // #ifndef APP-PLUS return { hasUpdate: false } // #endif }, startDownload({ state, commit, dispatch }) { // #ifdef APP-PLUS const link = state.link if (!link || !link.url) return if (link.type === 'url') { plus.runtime.openURL(link.url) commit('assign', { key: 'visible', value: false }) return } commit('assign', { key: 'visible', value: true }) state.download.start = true state.task = uni.downloadFile({ url: link.url, success: (res) => { if (res.statusCode !== 200) return uni.saveFile({ tempFilePath: res.tempFilePath, success: (file) => { uni.setStorageSync(STORAGE.update, { path: file.savedFilePath, ignore: false }) state.download.path = file.savedFilePath state.download.start = false state.task?.abort?.() state.task = null dispatch('installPackage') } }) }, fail: () => { state.download.start = false uni.showToast({ title: '下载失败', icon: 'none' }) } }) state.task.onProgressUpdate((p) => { state.download.progress = p.progress state.download.totalBytesWritten = p.totalBytesWritten state.download.totalBytesExpectedToWrite = p.totalBytesExpectedToWrite }) // #endif }, installPackage({ state, commit }) { // #ifdef APP-PLUS state.download.install = true plus.runtime.install(state.download.path, state.visible ? {} : { force: true }, () => { commit('assign', { key: 'visible', value: false }) state.download.install = false uni.showModal({ title: '更新完成', content: '应用将重启以完成更新', showCancel: false, complete: () => plus.runtime.restart() }) }, (e) => { state.download.install = false uni.showToast({ title: `更新失败:${e.message}`, icon: 'none' }) }) // #endif }, ignore({ commit }) { uni.setStorageSync(STORAGE.update, { path: null, ignore: true }) commit('assign', { key: 'visible', value: false }) } } }