release.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import appSettings from '@/core/config/app-settings'
  2. import { buildRestPath } from '@/core/config/api-paths'
  3. import { get } from '@/core/http/http-client'
  4. import { STORAGE } from '@/core/constants/storage-keys'
  5. /**
  6. * 解析更新包 / App Store 等跳转地址。
  7. * 绝对 URI(https://、itms-apps:// 等)不再拼 CDN;相对路径才拼 cdnurl。
  8. */
  9. function resolveUpdateAssetUrl(path) {
  10. let p = String(path || '').trim().replace(/^\uFEFF/, '')
  11. if (!p) return ''
  12. p = p.replace(/[\u200B-\u200D\uFEFF\u2060]/g, '')
  13. p = p.replace(/(https?)\uFF1A(\/\/)/gi, '$1:$2')
  14. p = p.replace(/(https?)(\/\/)/gi, (m, scheme, slashes, offset, str) => {
  15. const prev = offset > 0 ? str[offset - 1] : ''
  16. return prev === ':' ? m : `${scheme}:${slashes}`
  17. })
  18. p = p.replace(/^(https?):\/([^/])/i, '$1://$2')
  19. const absIdx = p.search(/https?:\/\//i)
  20. if (absIdx > 0) {
  21. p = p.slice(absIdx)
  22. }
  23. if (/^\/\//.test(p)) {
  24. p = `https:${p}`
  25. }
  26. if (/^[a-z][a-z0-9+.-]*:/i.test(p)) {
  27. return p
  28. }
  29. const base = String(appSettings.cdnurl || '').replace(/\/+$/, '')
  30. const rel = p.replace(/^\/+/, '')
  31. return base ? `${base}/${rel}` : p
  32. }
  33. function normalizeUpdateContent(data) {
  34. const copy = { ...data }
  35. copy.content = String(copy.content || '').replace(/(\r\n)|(\n)/g, '<br>')
  36. return copy
  37. }
  38. function buildDownloadLink(data) {
  39. const os = plus.os.name.toLowerCase()
  40. const linkKey = os === 'ios' ? 'iosLink' : 'androidLink'
  41. const raw = data[linkKey]
  42. if (!raw) return null
  43. const url = resolveUpdateAssetUrl(raw)
  44. if (!url) return null
  45. return {
  46. url,
  47. type: /\.wgt/i.test(url) ? 'wgt' : 'url'
  48. }
  49. }
  50. async function fetchRemoteUpdate() {
  51. const { data } = await get(buildRestPath('common/update'))
  52. if (!data || typeof data !== 'object') return null
  53. const remoteCode = Number(data.versionCode)
  54. const localCode = Number(appSettings.versionCode)
  55. if (!Number.isFinite(remoteCode) || remoteCode <= localCode) return null
  56. return data
  57. }
  58. function presentUpdate({ commit, dispatch }, data) {
  59. const meta = normalizeUpdateContent(data)
  60. commit('assign', { key: 'meta', value: meta })
  61. // #ifdef APP-PLUS
  62. const link = buildDownloadLink(meta)
  63. if (!link) {
  64. commit('assign', { key: 'visible', value: false })
  65. return { hasUpdate: true, hasLink: false, data: meta }
  66. }
  67. commit('assign', { key: 'link', value: link })
  68. if (Number(meta.enforce) === 1) {
  69. dispatch('startDownload')
  70. return { hasUpdate: true, hasLink: true, data: meta, enforced: true }
  71. }
  72. commit('assign', { key: 'visible', value: true })
  73. return { hasUpdate: true, hasLink: true, data: meta }
  74. // #endif
  75. // #ifndef APP-PLUS
  76. commit('assign', { key: 'visible', value: true })
  77. return { hasUpdate: true, hasLink: true, data: meta }
  78. // #endif
  79. }
  80. export default {
  81. namespaced: true,
  82. state: {
  83. visible: false,
  84. meta: {},
  85. link: {},
  86. download: {
  87. path: null,
  88. start: false,
  89. install: false,
  90. progress: 0,
  91. totalBytesWritten: 0,
  92. totalBytesExpectedToWrite: 0
  93. },
  94. task: null
  95. },
  96. mutations: {
  97. assign(state, { key, value }) {
  98. state[key] = value
  99. }
  100. },
  101. actions: {
  102. check({ dispatch }) {
  103. // #ifdef MP
  104. const mgr = uni.getUpdateManager()
  105. mgr.onUpdateReady(() => {
  106. uni.showModal({
  107. title: '更新提示',
  108. content: '新版本已就绪,是否重启应用?',
  109. success: (res) => res.confirm && mgr.applyUpdate()
  110. })
  111. })
  112. // #endif
  113. // #ifdef APP-PLUS
  114. const cache = uni.getStorageSync(STORAGE.update)
  115. if (cache?.ignore) return Promise.resolve({ hasUpdate: false, ignored: true })
  116. return dispatch('checkManual', { silent: true })
  117. // #endif
  118. // #ifndef APP-PLUS
  119. return Promise.resolve({ hasUpdate: false })
  120. // #endif
  121. },
  122. /**
  123. * 手动检查更新(关于我们页):忽略「不再提醒」,无新版本时由调用方 toast。
  124. */
  125. async checkManual({ commit, dispatch }, { silent = false } = {}) {
  126. // #ifdef MP
  127. try {
  128. const mgr = uni.getUpdateManager()
  129. mgr.onCheckForUpdate((res) => {
  130. if (!res.hasUpdate && !silent) {
  131. uni.showToast({ title: '已是最新版本', icon: 'none' })
  132. }
  133. })
  134. mgr.onUpdateReady(() => {
  135. uni.showModal({
  136. title: '更新提示',
  137. content: '新版本已就绪,是否重启应用?',
  138. success: (r) => r.confirm && mgr.applyUpdate()
  139. })
  140. })
  141. } catch (_) {}
  142. return { hasUpdate: false }
  143. // #endif
  144. // #ifdef APP-PLUS
  145. try {
  146. const data = await fetchRemoteUpdate()
  147. if (!data) {
  148. return { hasUpdate: false }
  149. }
  150. return presentUpdate({ commit, dispatch }, data)
  151. } catch (e) {
  152. if (!silent) throw e
  153. return { hasUpdate: false, error: e }
  154. }
  155. // #endif
  156. // #ifndef APP-PLUS
  157. return { hasUpdate: false }
  158. // #endif
  159. },
  160. startDownload({ state, commit, dispatch }) {
  161. // #ifdef APP-PLUS
  162. const link = state.link
  163. if (!link || !link.url) return
  164. if (link.type === 'url') {
  165. plus.runtime.openURL(link.url)
  166. commit('assign', { key: 'visible', value: false })
  167. return
  168. }
  169. commit('assign', { key: 'visible', value: true })
  170. state.download.start = true
  171. state.task = uni.downloadFile({
  172. url: link.url,
  173. success: (res) => {
  174. if (res.statusCode !== 200) return
  175. uni.saveFile({
  176. tempFilePath: res.tempFilePath,
  177. success: (file) => {
  178. uni.setStorageSync(STORAGE.update, { path: file.savedFilePath, ignore: false })
  179. state.download.path = file.savedFilePath
  180. state.download.start = false
  181. state.task?.abort?.()
  182. state.task = null
  183. dispatch('installPackage')
  184. }
  185. })
  186. },
  187. fail: () => {
  188. state.download.start = false
  189. uni.showToast({ title: '下载失败', icon: 'none' })
  190. }
  191. })
  192. state.task.onProgressUpdate((p) => {
  193. state.download.progress = p.progress
  194. state.download.totalBytesWritten = p.totalBytesWritten
  195. state.download.totalBytesExpectedToWrite = p.totalBytesExpectedToWrite
  196. })
  197. // #endif
  198. },
  199. installPackage({ state, commit }) {
  200. // #ifdef APP-PLUS
  201. state.download.install = true
  202. plus.runtime.install(state.download.path, state.visible ? {} : { force: true }, () => {
  203. commit('assign', { key: 'visible', value: false })
  204. state.download.install = false
  205. uni.showModal({
  206. title: '更新完成',
  207. content: '应用将重启以完成更新',
  208. showCancel: false,
  209. complete: () => plus.runtime.restart()
  210. })
  211. }, (e) => {
  212. state.download.install = false
  213. uni.showToast({ title: `更新失败:${e.message}`, icon: 'none' })
  214. })
  215. // #endif
  216. },
  217. ignore({ commit }) {
  218. uni.setStorageSync(STORAGE.update, { path: null, ignore: true })
  219. commit('assign', { key: 'visible', value: false })
  220. }
  221. }
  222. }