diff --git a/certificate/book.keystore b/certificate/book.keystore new file mode 100644 index 0000000..ad6e4b6 Binary files /dev/null and b/certificate/book.keystore differ diff --git a/certificate/证书信息.txt b/certificate/证书信息.txt new file mode 100644 index 0000000..3a245d2 --- /dev/null +++ b/certificate/证书信息.txt @@ -0,0 +1,9 @@ +证书指纹: +MD5: 9A:AD:4D:78:17:EE:17:D3:62:B2:6E:B2:56:0B:18:C3 +SHA1: B1:1E:CE:4B:70:C7:88:B3:6E:8B:B9:05:98:4B:FD:67:81:C1:3A:8F +SHA256: A9:78:BD:BF:BB:DE:35:14:AA:5D:45:E9:6A:D0:E5:17:6D:BE:58:86:7A:1D:66:A2:F3:62:2F:E7:9C:9E:59:F6 + + +别名: __uni__9f097f0 +证书密码为:uZ6Stufj +包名:com.tianranqi.app \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 1a3631c..2759e92 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,13 +2,15 @@ import { onLaunch, onShow, onHide, onLoad, onReady } from '@dcloudio/uni-app' import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only' import { beforEach } from '@/router/index' - import { jurisdictionApi } from '@/api/system/login'; - import { useAppStore } from '@/store' + import { jurisdictionApi } from '@/api/system'; + import { useAppStore, useUpdateApp } from '@/store' export default { onLaunch: function (options) { console.log('App Launch') console.log('应用启动路径:', options.path) + // 检查更新 + useUpdateApp().checkAppUpdate() }, onShow: function (options) { console.log('App Show') diff --git a/src/api/system/login.ts b/src/api/system/index.ts similarity index 86% rename from src/api/system/login.ts rename to src/api/system/index.ts index 1ddf5db..e839c1a 100644 --- a/src/api/system/login.ts +++ b/src/api/system/index.ts @@ -21,6 +21,18 @@ export function loginApi(config : LoginParams) { }); } +/** + * 热更新 + * @param + * @returns + */ +export function upDateAppApi() { + return http({ + url: '/sys/common/upDateApp', + method: 'GET' + }) +} + /** * 获取是否灰化 * @param id 登录参数 @@ -33,5 +45,6 @@ export function jurisdictionApi(id : string) { // 是否灰化 data: { id } - }) -} \ No newline at end of file + }); +} + diff --git a/src/api/user/index.ts b/src/api/user/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/login/login.vue b/src/pages/login/login.vue index c237c7f..ca080f2 100644 --- a/src/pages/login/login.vue +++ b/src/pages/login/login.vue @@ -69,7 +69,7 @@ import { useRouter } from '@/plugin/uni-mini-router' // import { useParamsStore } from '@/store/page-params' import Base64 from 'base-64'; - import { loginApi } from '@/api/system/login'; + import { loginApi } from '@/api/system'; defineOptions({ name: 'login', diff --git a/src/store/index.ts b/src/store/index.ts index 7ed5de1..bb81b1e 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,6 +1,7 @@ import { createPinia, defineStore } from 'pinia' import { createPersistedState } from 'pinia-plugin-persistedstate' // 数据持久化 - +import { upDateAppApi } from '@/api/system'; +import { hasNewVersion, onClickUpdate } from '@/utils/index'; const store = createPinia() store.use( createPersistedState({ @@ -22,6 +23,66 @@ export const useAppStore = defineStore('app', { persist: true, // 启用持久化 }) +export const useUpdateApp = defineStore('updateApp', () => { + const updateOptions = reactive({ + force: false, + hasNew: false, + content: '', + url: '', + apkUrl: '', + wgtUrl: '' + }) + const systemInfo = uni.getSystemInfoSync() + + /** + * 当前处于APP_PLUS时 检查是否有新版本 + * + * @param to 是否跳转新页面显示更新 默认值false + */ + function checkAppUpdate(to = false) { + try { + upDateAppApi().then(async (res : any) => { + let { + result + } = res + result.apkUrl = import.meta.env.VITE_SERVER_BASEURL + '/sys/common/static/' + result.apkUrl; + result.wgtUrl = import.meta.env.VITE_SERVER_BASEURL + '/sys/common/static/' + result.wgtUrl + updateOptions.wgtUrl = result.wgtUrl + if (systemInfo.osName === 'android') { + // Android + updateOptions.apkUrl = result.apkUrl + // #ifdef APP_PLUS + updateOptions.hasNew = await hasNewVersion(result.versionCode, result.update == 'wgt') as boolean + // #endif + } else { + // IOS + updateOptions.url = `itms-apps://itunes.apple.com/cn/app/id${123456}?mt=8` + } + updateOptions.hasNew && + uni.showModal({ + title: '更新', + content: '发现新版本,请更新', + success(res) { + if (res.confirm) { + onClickUpdate(result.update, result) + } else { + plus.runtime.quit() + } + } + }) + }) + } catch (error) { + updateOptions.hasNew = false + } + } + + return { + checkAppUpdate, + ...toRefs(updateOptions), + systemInfo + } +}) + export default store // 模块统一导出 diff --git a/src/utils/index.ts b/src/utils/index.ts index 2a4217a..7b561a3 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,25 +2,25 @@ import { pages, subPackages, tabBar } from '@/pages.json' import { isMpWeixin } from './platform' const getLastPage = () => { - // getCurrentPages() 至少有1个元素,所以不再额外判断 - // const lastPage = getCurrentPages().at(-1) - // 上面那个在低版本安卓中打包回报错,所以改用下面这个【虽然我加了src/interceptions/prototype.ts,但依然报错】 - const pages = getCurrentPages() - return pages[pages.length - 1] + // getCurrentPages() 至少有1个元素,所以不再额外判断 + // const lastPage = getCurrentPages().at(-1) + // 上面那个在低版本安卓中打包回报错,所以改用下面这个【虽然我加了src/interceptions/prototype.ts,但依然报错】 + const pages = getCurrentPages() + return pages[pages.length - 1] } /** 判断当前页面是否是tabbar页 */ export const getIsTabbar = () => { - if (!tabBar) { - return false - } - if (!tabBar.list.length) { - // 通常有tabBar的话,list不能有空,且至少有2个元素,这里其实不用处理 - return false - } - const lastPage = getLastPage() - const currPath = lastPage.route - return !!tabBar.list.find((e) => e.pagePath === currPath) + if (!tabBar) { + return false + } + if (!tabBar.list.length) { + // 通常有tabBar的话,list不能有空,且至少有2个元素,这里其实不用处理 + return false + } + const lastPage = getLastPage() + const currPath = lastPage.route + return !!tabBar.list.find((e) => e.pagePath === currPath) } /** @@ -29,48 +29,48 @@ export const getIsTabbar = () => { * redirectPath 如 ‘/pages/demo/base/route-interceptor’ */ export const currRoute = () => { - const lastPage = getLastPage() - const currRoute = (lastPage as any).$page - // console.log('lastPage.$page:', currRoute) - // console.log('lastPage.$page.fullpath:', currRoute.fullPath) - // console.log('lastPage.$page.options:', currRoute.options) - // console.log('lastPage.options:', (lastPage as any).options) - // 经过多端测试,只有 fullPath 靠谱,其他都不靠谱 - const { fullPath } = currRoute as { fullPath: string } - // console.log(fullPath) - // eg: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor (小程序) - // eg: /pages/login/index?redirect=%2Fpages%2Froute-interceptor%2Findex%3Fname%3Dfeige%26age%3D30(h5) - return getUrlObj(fullPath) + const lastPage = getLastPage() + const currRoute = (lastPage as any).$page + // console.log('lastPage.$page:', currRoute) + // console.log('lastPage.$page.fullpath:', currRoute.fullPath) + // console.log('lastPage.$page.options:', currRoute.options) + // console.log('lastPage.options:', (lastPage as any).options) + // 经过多端测试,只有 fullPath 靠谱,其他都不靠谱 + const { fullPath } = currRoute as { fullPath : string } + // console.log(fullPath) + // eg: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor (小程序) + // eg: /pages/login/index?redirect=%2Fpages%2Froute-interceptor%2Findex%3Fname%3Dfeige%26age%3D30(h5) + return getUrlObj(fullPath) } -const ensureDecodeURIComponent = (url: string) => { - if (url.startsWith('%')) { - return ensureDecodeURIComponent(decodeURIComponent(url)) - } - return url +const ensureDecodeURIComponent = (url : string) => { + if (url.startsWith('%')) { + return ensureDecodeURIComponent(decodeURIComponent(url)) + } + return url } /** * 解析 url 得到 path 和 query * 比如输入url: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor * 输出: {path: /pages/login/index, query: {redirect: /pages/demo/base/route-interceptor}} */ -export const getUrlObj = (url: string) => { - const [path, queryStr] = url.split('?') - // console.log(path, queryStr) +export const getUrlObj = (url : string) => { + const [path, queryStr] = url.split('?') + // console.log(path, queryStr) - if (!queryStr) { - return { - path, - query: {}, - } - } - const query: Record = {} - queryStr.split('&').forEach((item) => { - const [key, value] = item.split('=') - // console.log(key, value) - query[key] = ensureDecodeURIComponent(value) // 这里需要统一 decodeURIComponent 一下,可以兼容h5和微信y - }) - return { path, query } + if (!queryStr) { + return { + path, + query: {}, + } + } + const query : Record = {} + queryStr.split('&').forEach((item) => { + const [key, value] = item.split('=') + // console.log(key, value) + query[key] = ensureDecodeURIComponent(value) // 这里需要统一 decodeURIComponent 一下,可以兼容h5和微信y + }) + return { path, query } } /** * 得到所有的需要登录的pages,包括主包和分包的 @@ -78,103 +78,103 @@ export const getUrlObj = (url: string) => { * 如果没有传 key,则表示所有的pages,如果传递了 key, 则表示通过 key 过滤 */ export const getAllPages = (key = 'needLogin') => { - // 这里处理主包 - const mainPages = [ - ...pages - .filter((page) => !key || page[key]) - .map((page) => ({ - ...page, - path: `/${page.path}`, - })), - ] - // 这里处理分包 - const subPages: any[] = [] - subPackages.forEach((subPageObj) => { - // console.log(subPageObj) - const { root } = subPageObj + // 这里处理主包 + const mainPages = [ + ...pages + .filter((page) => !key || page[key]) + .map((page) => ({ + ...page, + path: `/${page.path}`, + })), + ] + // 这里处理分包 + const subPages : any[] = [] + subPackages.forEach((subPageObj) => { + // console.log(subPageObj) + const { root } = subPageObj - subPageObj.pages - .filter((page) => !key || page[key]) - .forEach((page: { path: string } & Record) => { - subPages.push({ - ...page, - path: `/${root}/${page.path}`, - }) - }) - }) - const result = [...mainPages, ...subPages] - // console.log(`getAllPages by ${key} result: `, result) - return result + subPageObj.pages + .filter((page) => !key || page[key]) + .forEach((page : { path : string } & Record) => { + subPages.push({ + ...page, + path: `/${root}/${page.path}`, + }) + }) + }) + const result = [...mainPages, ...subPages] + // console.log(`getAllPages by ${key} result: `, result) + return result } /** * 得到所有的需要登录的pages,包括主包和分包的 * 只得到 path 数组 */ -export const getNeedLoginPages = (): string[] => getAllPages('needLogin').map((page) => page.path) +export const getNeedLoginPages = () : string[] => getAllPages('needLogin').map((page) => page.path) /** * 得到所有的需要登录的pages,包括主包和分包的 * 只得到 path 数组 */ -export const needLoginPages: string[] = getAllPages('needLogin').map((page) => page.path) +export const needLoginPages : string[] = getAllPages('needLogin').map((page) => page.path) /** * 根据微信小程序当前环境,判断应该获取的BaseUrl */ export const getEnvBaseUrl = () => { - // 请求基准地址 - let baseUrl = import.meta.env.VITE_SERVER_BASEURL + // 请求基准地址 + let baseUrl = import.meta.env.VITE_SERVER_BASEURL - // 微信小程序端环境区分 - if (isMpWeixin) { - const { - miniProgram: { envVersion }, - } = uni.getAccountInfoSync() + // 微信小程序端环境区分 + if (isMpWeixin) { + const { + miniProgram: { envVersion }, + } = uni.getAccountInfoSync() - switch (envVersion) { - case 'develop': - baseUrl = import.meta.env.VITE_SERVER_BASEURL__WEIXIN_DEVELOP || baseUrl - break - case 'trial': - baseUrl = import.meta.env.VITE_SERVER_BASEURL__WEIXIN_TRIAL || baseUrl - break - case 'release': - baseUrl = import.meta.env.VITE_SERVER_BASEURL__WEIXIN_RELEASE || baseUrl - break - } - } + switch (envVersion) { + case 'develop': + baseUrl = import.meta.env.VITE_SERVER_BASEURL__WEIXIN_DEVELOP || baseUrl + break + case 'trial': + baseUrl = import.meta.env.VITE_SERVER_BASEURL__WEIXIN_TRIAL || baseUrl + break + case 'release': + baseUrl = import.meta.env.VITE_SERVER_BASEURL__WEIXIN_RELEASE || baseUrl + break + } + } - return baseUrl + return baseUrl } /** * 根据微信小程序当前环境,判断应该获取的UPLOAD_BASEURL */ export const getEnvBaseUploadUrl = () => { - // 请求基准地址 - let baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL + // 请求基准地址 + let baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL - // 微信小程序端环境区分 - if (isMpWeixin) { - const { - miniProgram: { envVersion }, - } = uni.getAccountInfoSync() + // 微信小程序端环境区分 + if (isMpWeixin) { + const { + miniProgram: { envVersion }, + } = uni.getAccountInfoSync() - switch (envVersion) { - case 'develop': - baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP || baseUploadUrl - break - case 'trial': - baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL__WEIXIN_TRIAL || baseUploadUrl - break - case 'release': - baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL__WEIXIN_RELEASE || baseUploadUrl - break - } - } + switch (envVersion) { + case 'develop': + baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP || baseUploadUrl + break + case 'trial': + baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL__WEIXIN_TRIAL || baseUploadUrl + break + case 'release': + baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL__WEIXIN_RELEASE || baseUploadUrl + break + } + } - return baseUploadUrl + return baseUploadUrl } /** * 时间格式化 @@ -183,35 +183,138 @@ export const getEnvBaseUploadUrl = () => { * @returns {*} */ export function formatDate(value, fmt) { - var regPos = /^\d+(\.\d+)?$/; - if(regPos.test(value)){ - //如果是数字 - let getDate = new Date(value); - let o = { - 'M+': getDate.getMonth() + 1, - 'd+': getDate.getDate(), - 'h+': getDate.getHours(), - 'H+': getDate.getHours(), - 'm+': getDate.getMinutes(), - 's+': getDate.getSeconds(), - 'q+': Math.floor((getDate.getMonth() + 3) / 3), - 'S': getDate.getMilliseconds() - }; - if (/(y+)/.test(fmt)) { - fmt = fmt.replace(RegExp.$1, (getDate.getFullYear() + '').substr(4 - RegExp.$1.length)) - } - for (let k in o) { - if (new RegExp('(' + k + ')').test(fmt)) { - fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length))) - } - } - return fmt; - }else{ - //TODO - if(value && value.length>0){ - value = value.trim(); - return value.substr(0,fmt.length); - } - return value - } + var regPos = /^\d+(\.\d+)?$/; + if (regPos.test(value)) { + //如果是数字 + let getDate = new Date(value); + let o = { + 'M+': getDate.getMonth() + 1, + 'd+': getDate.getDate(), + 'h+': getDate.getHours(), + 'H+': getDate.getHours(), + 'm+': getDate.getMinutes(), + 's+': getDate.getSeconds(), + 'q+': Math.floor((getDate.getMonth() + 3) / 3), + 'S': getDate.getMilliseconds() + }; + if (/(y+)/.test(fmt)) { + fmt = fmt.replace(RegExp.$1, (getDate.getFullYear() + '').substr(4 - RegExp.$1.length)) + } + for (let k in o) { + if (new RegExp('(' + k + ')').test(fmt)) { + fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length))) + } + } + return fmt; + } else { + //TODO + if (value && value.length > 0) { + value = value.trim(); + return value.substr(0, fmt.length); + } + return value + } } + +/** + * 判断是否有新版本 + * @param version 接口返回的版本号 + * @param isWgt 是否是热资源更新 默认不是 + */ +export const hasNewVersion = (version, isWgt = false) => { + // #ifdef APP_PLUS + return new Promise((resolve) => { + const transfer = (str) => str.replace(/\./g, '') + if (isWgt) { + plus.runtime.getProperty(plus.runtime.appid, (widgetInfo) => { + const currentVersion = widgetInfo.version + resolve(+transfer(version) > +transfer(currentVersion)) + }) + } else { + const currentVersion = plus.runtime.version + resolve(+transfer(version) > +transfer(currentVersion)) + } + }) + // #endif +} + +export function onClickUpdate(updateType : string, url) { + // #ifdef APP-PLUS + if (updateType != 'wgt') plus.runtime.openURL(url.apkUrl) + else downloadApp(url.wgtUrl) + // #endif +} + +function downloadApp(url : string) { + console.log('url', url); + var dtask = plus.downloader.createDownload(url, { + filename: `_downloads/wgt-${Date.now()}.wgt` //利用保存路径,实现下载文件的重命名 + }, function (d, status) { + //d为下载的文件对象 + if (status == 200) { + //下载成功,d.filename是文件在保存在本地的相对路径,使用下面的API可转为平台绝对路径 + var fileSaveUrl = plus.io.convertLocalFileSystemURL(d.filename); + console.log('fileSaveUrl', fileSaveUrl); + installApp(fileSaveUrl) + } else { + //下载失败 + plus.downloader.clear(); //清除下载任务 + uni.showToast({ + title: 'App下载失败!', + icon: 'error' + }) + } + }) + let prg = 0 + let showLoading = plus.nativeUI.showWaiting('正在下載') + dtask.start() + dtask.addEventListener('statechanged', (task) => { + // 给下载任务设置一个监听 并根据状态 做操作 + switch (task.state) { + case 1: + showLoading.setTitle("正在下载") + break + case 2: + showLoading.setTitle("已连接到服务器") + break + case 3: { + const downloaded = +task.downloadedSize; // 转为数字 + const total = +task.totalSize; // 转为数字 + prg = total > 0 ? Math.floor((downloaded / total) * 100) : 0; + showLoading.setTitle(`正在下载 ${prg}%`); + break; + } + case 4: + plus.nativeUI.closeWaiting() + //下载完成 + break + } + }) +} + +function installApp(tempFilePath : string) { + // #ifdef APP-PLUS + plus.runtime.install( + tempFilePath, { + force: true + }, + () => { + uni.showModal({ + title: '更新', + content: '更新成功,请点击确认后重启', + showCancel: false, + success(res) { + if (res.confirm) { + plus.runtime.restart() + } + } + }) + }, + () => + uni.showToast({ + title: '安装失败!', + icon: 'error' + }) + ) + // #endif +} \ No newline at end of file diff --git a/unpackage/res/icons/1024x1024.png b/unpackage/res/icons/1024x1024.png deleted file mode 100644 index 9eaa596..0000000 Binary files a/unpackage/res/icons/1024x1024.png and /dev/null differ diff --git a/unpackage/res/icons/120x120.png b/unpackage/res/icons/120x120.png deleted file mode 100644 index 22690e3..0000000 Binary files a/unpackage/res/icons/120x120.png and /dev/null differ diff --git a/unpackage/res/icons/144x144.png b/unpackage/res/icons/144x144.png deleted file mode 100644 index 6060e5e..0000000 Binary files a/unpackage/res/icons/144x144.png and /dev/null differ diff --git a/unpackage/res/icons/152x152.png b/unpackage/res/icons/152x152.png deleted file mode 100644 index 1d8ad19..0000000 Binary files a/unpackage/res/icons/152x152.png and /dev/null differ diff --git a/unpackage/res/icons/167x167.png b/unpackage/res/icons/167x167.png deleted file mode 100644 index 76a30cb..0000000 Binary files a/unpackage/res/icons/167x167.png and /dev/null differ diff --git a/unpackage/res/icons/180x180.png b/unpackage/res/icons/180x180.png deleted file mode 100644 index 93e9508..0000000 Binary files a/unpackage/res/icons/180x180.png and /dev/null differ diff --git a/unpackage/res/icons/192x192.png b/unpackage/res/icons/192x192.png deleted file mode 100644 index 5e6a2f3..0000000 Binary files a/unpackage/res/icons/192x192.png and /dev/null differ diff --git a/unpackage/res/icons/20x20.png b/unpackage/res/icons/20x20.png deleted file mode 100644 index 8e224bf..0000000 Binary files a/unpackage/res/icons/20x20.png and /dev/null differ diff --git a/unpackage/res/icons/29x29.png b/unpackage/res/icons/29x29.png deleted file mode 100644 index 4b09ac2..0000000 Binary files a/unpackage/res/icons/29x29.png and /dev/null differ diff --git a/unpackage/res/icons/40x40.png b/unpackage/res/icons/40x40.png deleted file mode 100644 index aa196b8..0000000 Binary files a/unpackage/res/icons/40x40.png and /dev/null differ diff --git a/unpackage/res/icons/58x58.png b/unpackage/res/icons/58x58.png deleted file mode 100644 index 68eda33..0000000 Binary files a/unpackage/res/icons/58x58.png and /dev/null differ diff --git a/unpackage/res/icons/60x60.png b/unpackage/res/icons/60x60.png deleted file mode 100644 index 03ed9be..0000000 Binary files a/unpackage/res/icons/60x60.png and /dev/null differ diff --git a/unpackage/res/icons/72x72.png b/unpackage/res/icons/72x72.png deleted file mode 100644 index b502d3e..0000000 Binary files a/unpackage/res/icons/72x72.png and /dev/null differ diff --git a/unpackage/res/icons/76x76.png b/unpackage/res/icons/76x76.png deleted file mode 100644 index 05975bd..0000000 Binary files a/unpackage/res/icons/76x76.png and /dev/null differ diff --git a/unpackage/res/icons/80x80.png b/unpackage/res/icons/80x80.png deleted file mode 100644 index e80c306..0000000 Binary files a/unpackage/res/icons/80x80.png and /dev/null differ diff --git a/unpackage/res/icons/87x87.png b/unpackage/res/icons/87x87.png deleted file mode 100644 index afe3bce..0000000 Binary files a/unpackage/res/icons/87x87.png and /dev/null differ diff --git a/unpackage/res/icons/96x96.png b/unpackage/res/icons/96x96.png deleted file mode 100644 index 7d0d1f9..0000000 Binary files a/unpackage/res/icons/96x96.png and /dev/null differ