/** * 应用版本:与 manifest 发版时尽量保持 versionName/versionCode 一致(非 App 端会回退到默认值)。 * * 本地待更新信息:写入 uni.setStorageSync(APP_PENDING_UPDATE_STORAGE_KEY, 对象或 JSON 字符串) * 形状:{ versionName, versionCode?, changelog?, downloadUrl?, packageSize? } * packageSize 示例:「128 MB」「7.13 GB」,展示在标题下灰色副标题 * 与 getCurrentAppVersionInfo() 比较,若本地版本更新则展示「可更新」界面。 */ export const APP_PENDING_UPDATE_STORAGE_KEY = 'APP_PENDING_UPDATE' export function readPendingUpdateFromStorage() { try { const raw = uni.getStorageSync(APP_PENDING_UPDATE_STORAGE_KEY) if (raw == null || raw === '') return null if (typeof raw === 'object' && raw !== null && !Array.isArray(raw)) return raw if (typeof raw === 'string') { const o = JSON.parse(raw) return o && typeof o === 'object' && !Array.isArray(o) ? o : null } } catch (e) {} return null } function parseVersionNameToComparable(name) { if (!name || typeof name !== 'string') return 0 const m = name.trim().match(/^(\d+)\.(\d+)\.(\d+)/) if (!m) return 0 const a = parseInt(m[1], 10) || 0 const b = parseInt(m[2], 10) || 0 const c = parseInt(m[3], 10) || 0 return a * 10000 + b * 100 + c } export function effectiveVersionComparable(info) { if (!info) return 0 const n = Number(info.versionCode) if (!Number.isNaN(n) && n > 0) return n const fromName = parseVersionNameToComparable(info.versionName) return fromName > 0 ? fromName : 0 } /** 版本号字符串规范化后比较(与接口 `version`、本地存储一致) */ export function normalizeVersionString(s) { if (s == null) return '' return String(s).trim() } function stripHtmlToPlain(html) { if (!html || typeof html !== 'string') return '' return html .replace(/<\/p>/gi, '\n') .replace(//gi, '\n') .replace(/<[^>]+>/g, '') .replace(/\r\n/g, '\n') .replace(/\n{3,}/g, '\n\n') .trim() } /** 当前运行包版本(App 读 runtime,小程序读账号信息,其余与 manifest 默认一致) */ export function getCurrentAppVersionInfo() { let versionName = '1.0.2' let versionCode = 102 // #ifdef APP-PLUS try { if (typeof plus !== 'undefined' && plus.runtime) { versionName = plus.runtime.version || versionName const vc = plus.runtime.versionCode if (vc != null && vc !== '') { const parsed = parseInt(String(vc), 10) if (!Number.isNaN(parsed)) versionCode = parsed } } } catch (e) {} // #endif // #ifdef MP-WEIXIN try { const acc = uni.getAccountInfoSync && uni.getAccountInfoSync() if (acc && acc.miniProgram && acc.miniProgram.version) { versionName = acc.miniProgram.version const parsed = parseVersionNameToComparable(versionName) if (parsed > 0) versionCode = parsed } } catch (e) {} // #endif return { versionName: String(versionName), versionCode: Number(versionCode) || 0 } } /** 与当前包比较:pending 是否为更高版本(本地存储或任意来源的待更新描述对象) */ export function remoteHasNewerVersion(currentInfo, pending) { if (!pending || typeof pending !== 'object') return false const c = effectiveVersionComparable(currentInfo) const r = effectiveVersionComparable({ versionName: pending.versionName, versionCode: pending.versionCode }) if (r <= 0) return false return r > c } function isTrivialEmptyHtml(html) { if (!html || typeof html !== 'string') return true const t = html.replace(/\s/g, '').toLowerCase() return ( t === '' || t === '

' || t === '


' || t === '


' || t === '
' || t === '
' ) } /** 富文本更新说明(接口 `updateContent`,原样供 rich-text 解析) */ export function pickUpdateContentHtml(remote) { if (!remote || typeof remote !== 'object') return '' const raw = remote.updateContent if (typeof raw !== 'string') return '' const trimmed = raw.trim() if (!trimmed || isTrivialEmptyHtml(trimmed)) return '' return trimmed } /** 纯文本更新说明(不含 updateContent,避免与富文本重复) */ export function pickRemoteChangelog(remote) { if (!remote || typeof remote !== 'object') return '' const raw = remote.changelog || remote.description || remote.releaseNotes || remote.intro || '' if (typeof raw !== 'string') return '' return stripHtmlToPlain(raw) } export function pickDownloadUrl(remote) { if (!remote || typeof remote !== 'object') return '' const u = remote.downloadUrl || remote.apkUrl || remote.apkDownloadUrl || remote.fileUrl || remote.url || remote.androidUrl || remote.iosUrl || '' return typeof u === 'string' ? u.trim() : '' } /** 从接口返回的单条记录里取版本号展示名 */ export function pickVersionNameFromRecord(row) { if (!row || typeof row !== 'object') return '' const v = row.versionName ?? row.version ?? row.apkVersion ?? row.appVersion ?? row.newVersion ?? '' if (typeof v === 'string') return v.trim() if (v != null && v !== '') return String(v).trim() return '' } function pickVersionCodeFromRecord(row) { if (!row || typeof row !== 'object') return 0 const raw = row.versionCode ?? row.build ?? row.versionNum const n = Number(raw) return Number.isNaN(n) ? 0 : n } /** * 解析 GET /app/apkVersion/latest 等接口的响应体,得到与本地 pending 相同形状的待更新对象。 * 兼容:data 为对象 / 数组首项 / rows 首项。 */ export function extractLatestApkRecord(httpResponse) { if (!httpResponse || typeof httpResponse !== 'object') return null let row = httpResponse.data if (Array.isArray(row)) row = row[0] else if (row && typeof row === 'object' && Array.isArray(row.rows)) row = row.rows[0] if (!row || typeof row !== 'object' || Array.isArray(row)) return null const versionName = pickVersionNameFromRecord(row) const versionCode = pickVersionCodeFromRecord(row) if (!versionName && versionCode <= 0) return null return { versionName, versionCode, changelog: pickRemoteChangelog(row), updateContentHtml: pickUpdateContentHtml(row), downloadUrl: pickDownloadUrl(row), packageSize: pickPackageSizeLabel(row) } } /** 可选:展示用安装包大小,如「128 MB」「7.13 GB」 */ export function pickPackageSizeLabel(remote) { if (!remote || typeof remote !== 'object') return '' const s = remote.packageSize || remote.size || remote.fileSize || '' return typeof s === 'string' ? s.trim() : '' }