Browse Source

wgt更新

Zimo 2 tuần trước cách đây
mục cha
commit
fc7fd0fc05
6 tập tin đã thay đổi với 1018 bổ sung316 xóa
  1. 20 0
      App.vue
  2. 28 10
      components/upgrade.vue
  3. 2 2
      manifest.json
  4. 310 287
      pages/user/login.vue
  5. 637 0
      utils/hot-update.js
  6. 21 17
      utils/upgrade.js

+ 20 - 0
App.vue

@@ -1,5 +1,6 @@
 <script>
 import { initAppDatabase } from '@/utils/appDb';
+import { runWgtHotUpdate } from '@/utils/hot-update';
 
 export default {
 	onLaunch: (options) => {
@@ -46,6 +47,25 @@ export default {
 			}
 		});
 		// #endif
+
+		// #ifdef APP-PLUS
+		/**
+		 * 这里是本次新增的 .wgt 热更新入口。
+		 *
+		 * 为什么放在 App.vue 的 onLaunch:
+		 * 1. 这是整个项目真正的应用启动入口,热更新要尽量早执行,就应该接在这里。
+		 * 2. 现有项目的旧升级逻辑写在页面组件里,只会在登录页 / 首页 mounted 后才触发,
+		 *    对“启动即检查热更新”这个目标来说太晚了。
+		 * 3. 放在这里还能保证热更新和数据库初始化、scheme 监听这类“应用级逻辑”处于同一层级,
+		 *    后续维护时更容易看清启动链路。
+		 *
+		 * 为什么这里不 await:
+		 * 1. 原有 onLaunch 里的数据库初始化本身就是非阻塞调用,项目没有把启动流程串行化。
+		 * 2. 热更新检查失败时我们希望“静默降级,不阻断原有启动”。
+		 * 3. 因此这里直接触发即可,让原有启动逻辑保持原样继续往下走。
+		 */
+		runWgtHotUpdate();
+		// #endif
 	},
 	onExit: () => {
 		// #ifdef APP

+ 28 - 10
components/upgrade.vue

@@ -5,14 +5,12 @@
     type="center"
     :animation="false"
     :mask-click="false"
-    style="z-index: 999"
-  >
+    style="z-index: 999">
     <view class="upgrade-popup">
       <image
         class="header-bg"
         src="../static/common/upgrade_bg.png"
-        mode="widthFix"
-      ></image>
+        mode="widthFix"></image>
       <view class="main">
         <view class="version"
           >{{ t("version.newVersion") }}{{ versionName }}</view
@@ -52,6 +50,7 @@
 </template>
 
 <script>
+import { isHotUpdateRunning } from "@/utils/hot-update.js";
 import { getAppVersion } from "@/api/app.js";
 import { checkVersion, downloadApp, installApp } from "@/utils/upgrade.js";
 // 引用全局变量$t
@@ -99,7 +98,29 @@ export default {
   mounted() {
     //检测版本
     // #ifdef APP
-    this.appCheckVersion();
+    /**
+     * 这里保留原来的页面级整包更新检查,但增加一个非常轻量的互斥判断。
+     *
+     * 为什么要加这层判断:
+     * 1. 当前项目原本就在登录页和首页自动挂载这个组件。
+     * 2. 本次新增的 .wgt 热更新改到了 App 启动阶段,如果这里还无条件发起旧检查,
+     *    启动时就可能同时出现“热更新弹窗”和“整包升级弹窗”。
+     * 3. 这不是为了废掉原有逻辑,而是为了让原有逻辑在“启动热更新正在执行”时先让路,
+     *    避免两个升级流程互相打架。
+     *
+     * 不加这段代码会怎样:
+     * - App.vue onLaunch 触发热更新检查
+     * - 页面 mounted 又触发旧的整包升级检查
+     * - 用户可能连续看到两个升级提示,甚至两个流程同时下载,体验会非常差
+     */
+
+    if (!isHotUpdateRunning()) {
+      this.appCheckVersion();
+    } else {
+      console.log(
+        "skip page upgrade check because startup hot update is running"
+      );
+    }
     // #endif
     const { appContext } = getCurrentInstance();
     const t = appContext.config.globalProperties.$t;
@@ -113,18 +134,15 @@ export default {
   },
   methods: {
     async appCheckVersion() {
-      console.log("🚀 ~ appCheckVersion ~ appCheckVersion:");
-
       const { data: remote } = await getAppVersion();
-      console.log("checkVersion-remote", remote);
-      const up = checkVersion({
+      const up = await checkVersion({
         name: remote.appVersion, //最新版本名称
         code: remote.appVersion, //最新版本号
         content: "", //更新内容
         url: remote.url, //下载链接
         forceUpdate: true, //是否强制升级
       });
-      console.log("🚀 ~ appCheckVersion ~ up:", up);
+
       if (up) {
         this.open();
       }

+ 2 - 2
manifest.json

@@ -2,8 +2,8 @@
     "name" : "DeepOil",
     "appid" : "__UNI__6E4BC49",
     "description" : "",
-    "versionName" : "1.3.2",
-    "versionCode" : 10302,
+    "versionName" : "1.3.4",
+    "versionCode" : 10304,
     "transformPx" : false,
     /* 5+App特有相关 */
     "app-plus" : {

+ 310 - 287
pages/user/login.vue

@@ -1,40 +1,54 @@
 <template>
-	<view class="login">
-		<view class="login-top">
-			<image class="back-img" src="../../static/login/login-back.png"></image>
-			<view class="login-text">
-				<view class="text">
-					{{ $t('login.welcome') }}
-				</view>
-				<view class="text">
-					{{ $t('app.appName') }}
-				</view>
-			</view>
-		</view>
-		<view class="login-form-wrap">
-			<uni-forms class="login-form" ref="formRef" :modelValue="loginData" :rules="loginRules">
-				<uni-forms-item name="username" class="margin-bt">
-					<!-- type="number" -->
-					<uni-easyinput class="login-input" v-model="loginData.username" :placeholder="$t('login.enterUsername')" :placeholderStyle="placeholderStyle" :styles="inputStyles" />
-				</uni-forms-item>
-				<uni-forms-item name="password" class="margin-bt">
-					<uni-easyinput type="password" v-model="loginData.password" :placeholder="$t('login.enterPassword')" :placeholderStyle="placeholderStyle" :styles="inputStyles" />
-				</uni-forms-item>
-			</uni-forms>
-			<button type="primary" @click="formSubmit(formRef)">
-				{{ $t('login.login') }}
-			</button>
-			<view class="flex-row align-center justify-between">
-				<view class="btn-text" @click="loginWithDingTalk">
-					{{ $t('login.loginWithDingTalk') }}
-				</view>
-				<view class="btn-text" @click="openLanguagePopup">
-					{{ $t('login.languageChange') }}
-				</view>
-			</view>
-		</view>
-
-		<!-- <view class="uni-padding-wrap">
+  <view class="login">
+    <view class="login-top">
+      <image class="back-img" src="../../static/login/login-back.png"></image>
+      <view class="login-text">
+        <view class="text">
+          {{ $t("login.welcome") }}
+        </view>
+        <view class="text">
+          {{ $t("app.appName") }}
+        </view>
+      </view>
+    </view>
+    <view class="login-form-wrap">
+      <uni-forms
+        class="login-form"
+        ref="formRef"
+        :modelValue="loginData"
+        :rules="loginRules">
+        <uni-forms-item name="username" class="margin-bt">
+          <!-- type="number" -->
+          <uni-easyinput
+            class="login-input"
+            v-model="loginData.username"
+            :placeholder="$t('login.enterUsername')"
+            :placeholderStyle="placeholderStyle"
+            :styles="inputStyles" />
+        </uni-forms-item>
+        <uni-forms-item name="password" class="margin-bt">
+          <uni-easyinput
+            type="password"
+            v-model="loginData.password"
+            :placeholder="$t('login.enterPassword')"
+            :placeholderStyle="placeholderStyle"
+            :styles="inputStyles" />
+        </uni-forms-item>
+      </uni-forms>
+      <button type="primary" @click="formSubmit(formRef)">
+        {{ $t("login.login") }}
+      </button>
+      <view class="flex-row align-center justify-between">
+        <view class="btn-text" @click="loginWithDingTalk">
+          {{ $t("login.loginWithDingTalk") }}
+        </view>
+        <view class="btn-text" @click="openLanguagePopup">
+          {{ $t("login.languageChange") }}
+        </view>
+      </view>
+    </view>
+
+    <!-- <view class="uni-padding-wrap">
 			<view>
 				<checkbox-group @change="handleChange">
 					<label>
@@ -68,33 +82,37 @@
 			</uni-popup>
 		</view> -->
 
-		<!-- 引用语言选择组件 -->
-		<language-popup ref="languagePopupRef" />
-		<upgrade ref="upgradeRef" />
-	</view>
+    <!-- 引用语言选择组件 -->
+    <language-popup ref="languagePopupRef" />
+    <upgrade ref="upgradeRef" />
+  </view>
 </template>
 
 <script setup>
-import { reactive, ref, onMounted, nextTick, getCurrentInstance } from 'vue';
-import { onLoad } from '@dcloudio/uni-app';
+import { reactive, ref, onMounted, nextTick, getCurrentInstance } from "vue";
+import { onLoad } from "@dcloudio/uni-app";
 // 引入接口api
-import { appLogin, dingTalkLogin, dingTalkLoginH5, getInfo, getTokenByUserId } from '@/api/login.js';
+import {
+  appLogin,
+  dingTalkLogin,
+  dingTalkLoginH5,
+  getInfo,
+  getTokenByUserId,
+} from "@/api/login.js";
 // 引入配置文件
-import config from '@/utils/config';
+import config from "@/utils/config";
 // 引入数据库操作
-import { saveUser } from '@/utils/appDb';
+import { saveUser } from "@/utils/appDb";
 // 引入本地存储操作
-import { setUserId, setToken, setDeptId, setUserInfo } from '@/utils/auth.js';
+import { setUserId, setToken, setDeptId, setUserInfo } from "@/utils/auth.js";
 // 引入组件
-import Upgrade from '@/components/upgrade.vue';
-import LanguagePopup from '@/components/language-popup.vue';
-import Privacy from './privacy.vue';
-import Agg from './agreement.vue';
+import Upgrade from "@/components/upgrade.vue";
+import LanguagePopup from "@/components/language-popup.vue";
 
 // 引入钉钉JSAPI -- 仅在H5环境下使用
 let dd = null;
 // #ifdef H5
-import * as dingTalkJsApi from 'dingtalk-jsapi';
+import * as dingTalkJsApi from "dingtalk-jsapi";
 dd = dingTalkJsApi;
 // #endif
 
@@ -103,14 +121,14 @@ const t = appContext.config.globalProperties.$t;
 const languagePopupRef = ref(null);
 
 const openLanguagePopup = () => {
-	languagePopupRef.value.open();
+  languagePopupRef.value.open();
 };
 
 let isChecked = ref(false);
 let my_value = ref(false);
 
 const handleChange = (val) => {
-	my_value.value = val.detail.value[0];
+  my_value.value = val.detail.value[0];
 };
 
 // let privacyRef = ref(null);
@@ -126,303 +144,308 @@ const handleChange = (val) => {
 
 // 判断当前环境是否在钉钉环境
 const isDingTalk = () => {
-	const ua = window.navigator.userAgent.toLowerCase();
-	console.log('🚀 ~ 当前环境 ~ ua:', ua);
-	return ua.includes('dingtalk') || ua.includes('dingtalkwork');
+  const ua = window.navigator.userAgent.toLowerCase();
+  console.log("🚀 ~ 当前环境 ~ ua:", ua);
+  return ua.includes("dingtalk") || ua.includes("dingtalkwork");
 };
 
 const dingTalkAutoLogin = async () => {
-	// 判断是否在钉钉环境
-	if (!isDingTalk()) {
-		console.log('当前环境不是钉钉环境,无法自动登录');
-		return;
-	}
-	// 执行钉钉微应用免登逻辑
-	loginWithDingTalkH5();
+  // 判断是否在钉钉环境
+  if (!isDingTalk()) {
+    console.log("当前环境不是钉钉环境,无法自动登录");
+    return;
+  }
+  // 执行钉钉微应用免登逻辑
+  loginWithDingTalkH5();
 };
 
 // 钉钉登录
 const loginWithDingTalk = async () => {
-	// #ifdef APP
-	const plugin = uni.requireNativePlugin('DingTalk');
-	// 钉钉登录,这里无法使用async,否则java端会报参数错误
-	plugin.login((res) => {
-		console.log(res);
-		if (res.success === 1) {
-			dingTalkLogin({
-				type: 20,
-				code: res.code,
-				state: res.state
-			}).then((res) => {
-				console.log(res);
-				handleLoginSuccess(res);
-			});
-		} else if (res.success === 2) {
-			uni.showToast({ title: t('login.dingTalkError'), icon: 'none' });
-			console.error('APP端钉钉登录失败:', res);
-		}
-	});
-	// #endif
-
-	// #ifdef H5
-	if (isDingTalk()) {
-		if (!dd) {
-			uni.showToast({ title: t('login.dingTalkJsapiMissing'), icon: 'none' });
-			return;
-		}
-		loginWithDingTalkH5();
-	} else {
-		console.log('当前是普通 H5 环境,无法使用钉钉登录');
-		uni.showToast({ title: t('login.h5DingTalk'), icon: 'none' });
-	}
-	// #endif
+  // #ifdef APP
+  const plugin = uni.requireNativePlugin("DingTalk");
+  // 钉钉登录,这里无法使用async,否则java端会报参数错误
+  plugin.login((res) => {
+    console.log(res);
+    if (res.success === 1) {
+      dingTalkLogin({
+        type: 20,
+        code: res.code,
+        state: res.state,
+      }).then((res) => {
+        console.log(res);
+        handleLoginSuccess(res);
+      });
+    } else if (res.success === 2) {
+      uni.showToast({ title: t("login.dingTalkError"), icon: "none" });
+      console.error("APP端钉钉登录失败:", res);
+    }
+  });
+  // #endif
+
+  // #ifdef H5
+  if (isDingTalk()) {
+    if (!dd) {
+      uni.showToast({ title: t("login.dingTalkJsapiMissing"), icon: "none" });
+      return;
+    }
+    loginWithDingTalkH5();
+  } else {
+    console.log("当前是普通 H5 环境,无法使用钉钉登录");
+    uni.showToast({ title: t("login.h5DingTalk"), icon: "none" });
+  }
+  // #endif
 };
 
 const loginWithDingTalkH5 = async () => {
-	const corpId = config.default.corpId;
-	console.log('🚀 ~ loginWithDingTalkH5 ~ corpId:', corpId);
-	const clientId = config.default.clientId;
-	console.log('🚀 ~ loginWithDingTalkH5 ~ clientId:', clientId);
-
-	if (!corpId || !clientId) {
-		console.error('缺少必要参数');
-		return;
-	}
-	dd.requestAuthCode({
-		corpId,
-		clientId,
-		success: async (result) => {
-			console.log('🚀 ~ loginWithDingTalkH5 ~ result:', result);
-			const { code } = result;
-			dingTalkLoginH5({
-				type: 10,
-				state: new Date().getTime(),
-				code: code
-			})
-				.then((res) => {
-					console.log('🚀 ~ loginWithDingTalkH5 ~ res:', res);
-					handleLoginSuccess(res);
-				})
-				.catch((err) => {
-					console.log('🚀 ~ loginWithDingTalkH5 ~ err:', err);
-				});
-		},
-		fail: (err) => {
-			console.log('🚀 ~ loginWithDingTalkH5 ~ err:', err);
-			uni.showToast({
-				title: '获取code失败:' + JSON.stringify(err),
-				icon: 'none'
-			});
-		}
-	});
+  const corpId = config.default.corpId;
+  console.log("🚀 ~ loginWithDingTalkH5 ~ corpId:", corpId);
+  const clientId = config.default.clientId;
+  console.log("🚀 ~ loginWithDingTalkH5 ~ clientId:", clientId);
+
+  if (!corpId || !clientId) {
+    console.error("缺少必要参数");
+    return;
+  }
+  dd.requestAuthCode({
+    corpId,
+    clientId,
+    success: async (result) => {
+      console.log("🚀 ~ loginWithDingTalkH5 ~ result:", result);
+      const { code } = result;
+      dingTalkLoginH5({
+        type: 10,
+        state: new Date().getTime(),
+        code: code,
+      })
+        .then((res) => {
+          console.log("🚀 ~ loginWithDingTalkH5 ~ res:", res);
+          handleLoginSuccess(res);
+        })
+        .catch((err) => {
+          console.log("🚀 ~ loginWithDingTalkH5 ~ err:", err);
+        });
+    },
+    fail: (err) => {
+      console.log("🚀 ~ loginWithDingTalkH5 ~ err:", err);
+      uni.showToast({
+        title: "获取code失败:" + JSON.stringify(err),
+        icon: "none",
+      });
+    },
+  });
 };
 
 onLoad(async (options) => {
-	console.log('onLoad Login', uni.getLocale(), 11, uni.getStorageSync('language'));
-
-	console.log(options);
-
-	// 保存钉钉消息传递的参数
-	if (options.userId) {
-		uni.setStorageSync('dingTalkJson', JSON.stringify(options));
-		const isLoggedIn = uni.getStorageSync('userId');
-		if (!isLoggedIn) {
-			const result = await getTokenByUserId(options.userId);
-			await handleLoginSuccess(result);
-		}
-	}
-	// #ifdef H5
-	// 当前环境为H5时,判断是否是通过钉钉微应用打开的链接
-	// 获取当前Url地址
-	const url = window.location.href;
-	console.log('当前环境为H5时 当前Url地址:', url);
-	// 判断是否是通过钉钉微应用打开的链接
-	if (url.includes('/deepoil')) {
-		dingTalkAutoLogin();
-	}
-	// #endif
+  console.log(
+    "onLoad Login",
+    uni.getLocale(),
+    11,
+    uni.getStorageSync("language")
+  );
+
+  console.log(options);
+
+  // 保存钉钉消息传递的参数
+  if (options.userId) {
+    uni.setStorageSync("dingTalkJson", JSON.stringify(options));
+    const isLoggedIn = uni.getStorageSync("userId");
+    if (!isLoggedIn) {
+      const result = await getTokenByUserId(options.userId);
+      await handleLoginSuccess(result);
+    }
+  }
+  // #ifdef H5
+  // 当前环境为H5时,判断是否是通过钉钉微应用打开的链接
+  // 获取当前Url地址
+  const url = window.location.href;
+  console.log("当前环境为H5时 当前Url地址:", url);
+  // 判断是否是通过钉钉微应用打开的链接
+  if (url.includes("/deepoil")) {
+    dingTalkAutoLogin();
+  }
+  // #endif
 });
 
 onMounted(() => {
-	// console.log("onMounted");
-	// 检查是否需要显示语言选择弹窗
-	if (!uni.getStorageSync('language')) {
-		nextTick(() => {
-			openLanguagePopup();
-		});
-	}
-	// 检查是否已登录
-	const isLoggedIn = uni.getStorageSync('userId');
-	// console.log("isLoggedIn", isLoggedIn);
-	if (isLoggedIn) {
-		uni.switchTab({
-			url: '/pages/home/index'
-		});
-	}
+  // console.log("onMounted");
+  // 检查是否需要显示语言选择弹窗
+  if (!uni.getStorageSync("language")) {
+    nextTick(() => {
+      openLanguagePopup();
+    });
+  }
+  // 检查是否已登录
+  const isLoggedIn = uni.getStorageSync("userId");
+  // console.log("isLoggedIn", isLoggedIn);
+  if (isLoggedIn) {
+    uni.switchTab({
+      url: "/pages/home/index",
+    });
+  }
 });
 
-const placeholderStyle = ref('color:#797979;font-weight:500;font-size:16px');
+const placeholderStyle = ref("color:#797979;font-weight:500;font-size:16px");
 const inputStyles = reactive({
-	backgroundColor: '#F0F3FB',
-	color: '#797979'
+  backgroundColor: "#F0F3FB",
+  color: "#797979",
 });
 const loginData = reactive({
-	username: '',
-	password: ''
+  username: "",
+  password: "",
 });
 const loginRules = ref({
-	username: {
-		rules: [
-			{
-				required: true,
-				errorMessage: t('login.enterUsername')
-			}
-		]
-	},
-	password: {
-		rules: [
-			{
-				required: true,
-				errorMessage: t('login.enterPassword')
-			}
-		]
-	}
+  username: {
+    rules: [
+      {
+        required: true,
+        errorMessage: t("login.enterUsername"),
+      },
+    ],
+  },
+  password: {
+    rules: [
+      {
+        required: true,
+        errorMessage: t("login.enterPassword"),
+      },
+    ],
+  },
 });
 
 const formRef = ref();
 const formSubmit = async (formEl) => {
-	// if (!my_value.value) {
-	// 	uni.showToast({
-	// 		title: '请阅读并同意隐私政策和用户服务协议',
-	// 		icon: 'none', // 可选 success/error/loading/none
-	// 		duration: 2000, // 持续时间,单位ms
-	// 		mask: true // 是否显示透明蒙层,防止触摸穿透
-	// 	});
-
-	// 	return;
-	// }
-	if (!formEl) return;
-	await formEl
-		.validate()
-		.then((res) => {
-			appLogin({
-				...loginData
-				// rememberMe: ,
-				// tenantName: ""
-			})
-				.then(async (result) => {
-					console.log('result,', result.data);
-					if (result) {
-						await saveUser({
-							name: loginData.username,
-							pwd: loginData.password
-						});
-						await handleLoginSuccess(result);
-					}
-				})
-				.finally(() => {});
-		})
-		.catch((err) => {
-			console.log('err', err);
-		});
+  // if (!my_value.value) {
+  // 	uni.showToast({
+  // 		title: '请阅读并同意隐私政策和用户服务协议',
+  // 		icon: 'none', // 可选 success/error/loading/none
+  // 		duration: 2000, // 持续时间,单位ms
+  // 		mask: true // 是否显示透明蒙层,防止触摸穿透
+  // 	});
+
+  // 	return;
+  // }
+  if (!formEl) return;
+  await formEl
+    .validate()
+    .then((res) => {
+      appLogin({
+        ...loginData,
+        // rememberMe: ,
+        // tenantName: ""
+      })
+        .then(async (result) => {
+          console.log("result,", result.data);
+          if (result) {
+            await saveUser({
+              name: loginData.username,
+              pwd: loginData.password,
+            });
+            await handleLoginSuccess(result);
+          }
+        })
+        .finally(() => {});
+    })
+    .catch((err) => {
+      console.log("err", err);
+    });
 };
 
 const handleLoginSuccess = async (result) => {
-	if (result) {
-		await setUserId(result.data.userId);
-		await setToken(result.data);
-		await getInfo().then(async (res) => {
-			// console.log('useres', res)
-			const data = JSON.stringify({
-				user: res.data.user,
-				roles: res.data.roles
-			});
-			// console.log('data', data)
-			await setUserInfo(data);
-			await setDeptId(res.data.user.deptId);
-
-			await uni.switchTab({
-				url: '/pages/home/index'
-			});
-		});
-	}
+  if (result) {
+    await setUserId(result.data.userId);
+    await setToken(result.data);
+    await getInfo().then(async (res) => {
+      // console.log('useres', res)
+      const data = JSON.stringify({
+        user: res.data.user,
+        roles: res.data.roles,
+      });
+      // console.log('data', data)
+      await setUserInfo(data);
+      await setDeptId(res.data.user.deptId);
+
+      await uni.switchTab({
+        url: "/pages/home/index",
+      });
+    });
+  }
 };
 </script>
 
 <style lang="scss" scoped>
 .privacy {
-	height: 60vh;
-	width: 85vw;
-	overflow: hidden;
+  height: 60vh;
+  width: 85vw;
+  overflow: hidden;
 }
 
 .uni-padding-wrap {
-	display: flex;
-	z-index: 999;
-	justify-content: center;
-	align-items: center;
-	padding: 0 10rpx;
+  display: flex;
+  z-index: 999;
+  justify-content: center;
+  align-items: center;
+  padding: 0 10rpx;
 }
 .uni-title {
-	font-size: 12px;
+  font-size: 12px;
 }
 .login-top {
-	position: relative;
-	width: 100%;
-	height: 422rpx;
+  position: relative;
+  width: 100%;
+  height: 422rpx;
 }
 
 .back-img {
-	width: 100%;
-	height: 100%;
+  width: 100%;
+  height: 100%;
 }
 
 .login-text {
-	width: 100%;
-	height: 100%;
-	box-sizing: border-box;
-	position: absolute;
-	top: 0;
-	left: 0;
-	color: #ffffff;
-	font-size: 40rpx;
-	font-family: 'Negreta,PingFang SC';
-	font-weight: 600;
-	padding: 0 56rpx;
-	display: flex;
-	justify-content: center;
-	flex-direction: column;
-
-	.text {
-		width: 100%;
-		margin-bottom: 6rpx;
-	}
+  width: 100%;
+  height: 100%;
+  box-sizing: border-box;
+  position: absolute;
+  top: 0;
+  left: 0;
+  color: #ffffff;
+  font-size: 40rpx;
+  font-family: "Negreta,PingFang SC";
+  font-weight: 600;
+  padding: 0 56rpx;
+  display: flex;
+  justify-content: center;
+  flex-direction: column;
+
+  .text {
+    width: 100%;
+    margin-bottom: 6rpx;
+  }
 }
 
 .margin-bt {
-	margin-bottom: 25px;
+  margin-bottom: 25px;
 }
 
 .login-form-wrap {
-	padding: 60rpx;
+  padding: 60rpx;
 }
 
 :deep(.uni-easyinput__content-input) {
-	height: 45px;
+  height: 45px;
 }
 
 :deep(.uni-input-input) {
-	color: #999999 !important;
+  color: #999999 !important;
 }
 
-uni-button[type='primary'] {
-	background: #004098;
+uni-button[type="primary"] {
+  background: #004098;
 }
 
 .btn-text {
-	color: #004098;
-	margin-top: 20px;
-	font-size: 14px;
-	font-weight: 500;
+  color: #004098;
+  margin-top: 20px;
+  font-size: 14px;
+  font-weight: 500;
 }
-</style>
+</style>

+ 637 - 0
utils/hot-update.js

@@ -0,0 +1,637 @@
+import config from "@/utils/config";
+
+/**
+ * 这里用本地存储做一个非常轻量的“启动期热更新锁”。
+ *
+ * 为什么不用纯内存变量:
+ * 1. App.vue 的 onLaunch 和页面组件 mounted 不在同一个文件里,直接共享内存状态不直观。
+ * 2. 现有项目已经在登录页、首页挂了旧的整包升级弹窗组件,我们需要一个跨文件、最小改动的互斥标记。
+ * 3. 用 storage 的好处是组件侧只要读一个 key 就能知道“启动阶段是否已经有热更新流程在跑”。
+ *
+ * 注意:
+ * 这个锁只是为了解决“启动期并发弹窗”的问题,不是分布式锁,也不是强一致锁。
+ * 对当前这个 uni-app 项目来说,这样的复杂度已经足够,而且侵入性最低。
+ */
+export const HOT_UPDATE_LOCK_KEY = "__APP_WGT_HOT_UPDATE_RUNNING__";
+
+/**
+ * 当用户点击“稍后再说”时,我们把当前版本记下来。
+ *
+ * 这样做的原因:
+ * 1. 热更新检查放在 onLaunch,意味着每次冷启动都会检查一次。
+ * 2. 如果用户已经明确拒绝了某个版本,下一次冷启动继续弹同一个版本,体验会比较差。
+ * 3. 只记录“被跳过的目标版本”,新版本来了仍然会重新提示。
+ */
+const HOT_UPDATE_SKIP_VERSION_KEY = "__APP_WGT_HOT_UPDATE_SKIP_VERSION__";
+
+/**
+ * 这里单独声明接口地址,而不是直接写死在 uni.request 里。
+ *
+ * 这样做的原因:
+ * 1. 现有项目已经有 config 环境配置,说明项目本身就是按环境切换接口域名的。
+ * 2. 热更新检查应该沿用现有配置习惯,避免你上线后再去改很多地方。
+ * 3. 这个路径是本次方案“约定”的服务端接口,你可以直接让后端按这个接口实现,
+ *    也可以只改这里一处,把路径替换成你们实际的地址。
+ */
+const HOT_UPDATE_API_URL =
+  config.default.apiUrl + config.default.apiUrlSuffix + "/rq/iot-app/newWgt";
+
+/**
+ * 读取启动期热更新锁。
+ *
+ * 对页面组件的意义:
+ * 旧的整包升级逻辑挂在页面里,页面 mounted 时如果发现启动热更新还在跑,
+ * 就应该先让路,避免同时弹两个升级框。
+ */
+export const isHotUpdateRunning = () => {
+  return uni.getStorageSync(HOT_UPDATE_LOCK_KEY) === "1";
+};
+
+/**
+ * 设置/清理热更新锁。
+ *
+ * 输入:
+ * - running: Boolean,true 表示流程开始,false 表示流程结束。
+ *
+ * 输出:
+ * - 无返回值,副作用是修改 storage 中的互斥标记。
+ */
+const setHotUpdateRunning = (running) => {
+  if (running) {
+    uni.setStorageSync(HOT_UPDATE_LOCK_KEY, "1");
+  } else {
+    uni.removeStorageSync(HOT_UPDATE_LOCK_KEY);
+  }
+};
+
+/**
+ * 等待 plus 对象可用。
+ *
+ * 为什么即便在 APP-PLUS 里也还要做这一步:
+ * 1. 你要求热更新接在启动阶段,启动阶段最容易踩到“plus 尚未完全就绪”的时序问题。
+ * 2. 现有 App.vue 的 onLaunch 里已经直接用了 plus.globalEvent,这说明项目默认运行在 App-Plus 环境。
+ * 3. 但热更新比普通日志更敏感,因为它要马上调用 plus.runtime.getProperty / install / restart,
+ *    所以这里显式兜底一次,会比“直接假设 plus 一定存在”更稳。
+ *
+ * 成功分支:
+ * - plus 已就绪,resolve,继续后续热更新流程。
+ *
+ * 失败分支:
+ * - 理论上这里没有主动 reject,超时场景也继续等待;
+ *   如果 plus 始终不可用,后续逻辑不会开始,也就不会影响原有启动逻辑。
+ */
+const waitForPlusReady = () => {
+  return new Promise((resolve) => {
+    if (typeof plus !== "undefined") {
+      resolve();
+      return;
+    }
+
+    document.addEventListener(
+      "plusready",
+      () => {
+        resolve();
+      },
+      { once: true }
+    );
+  });
+};
+
+/**
+ * 使用 plus.runtime.getProperty 读取当前资源包信息。
+ *
+ * 这是这次方案里最关键的一步,也是你特别强调不能替换掉的地方。
+ *
+ * 为什么必须用它:
+ * 1. 你要做的是 .wgt 资源热更新,比较的是“当前资源包版本”和“服务端资源包版本”。
+ * 2. plus.runtime.getProperty(plus.runtime.appid) 返回的是当前应用资源信息,
+ *    其中 wgtinfo.version 才是最贴近“资源版本”的值。
+ * 3. plus.runtime.version 更偏运行时/客户端信息,uni.getSystemInfo 也不是这个场景最合适的来源,
+ *    所以这里严格按你的要求使用 getProperty。
+ *
+ * 输出:
+ * - resolve(wgtInfo)
+ *   其中最常用的字段包括:
+ *   - appid: 当前应用的 DCloud appid
+ *   - version: 当前资源版本
+ *   - name: 当前应用名称
+ */
+export const getCurrentWgtInfo = () => {
+  return new Promise((resolve, reject) => {
+    try {
+      plus.runtime.getProperty(plus.runtime.appid, (wgtInfo) => {
+        if (!wgtInfo || !wgtInfo.version) {
+          reject(new Error("未能获取当前应用资源版本信息"));
+          return;
+        }
+
+        resolve(wgtInfo);
+      });
+    } catch (error) {
+      reject(error);
+    }
+  });
+};
+
+/**
+ * 一个简单的版本比较函数。
+ *
+ * 为什么这里仍然保留本地比较,而不是完全信任服务端的 update 字段:
+ * 1. 服务端当然应该负责判断版本,但客户端再做一次兜底,可以避免后端配置错误导致重复更新。
+ * 2. 这对“启动期自动更新”尤其重要,因为一旦后端误配,用户会在每次启动都被弹框打扰。
+ *
+ * 比较规则:
+ * - a > b 返回 1
+ * - a = b 返回 0
+ * - a < b 返回 -1
+ */
+const compareVersion = (a = "", b = "") => {
+  const aList = String(a)
+    .split(".")
+    .map((item) => Number(item || 0));
+  const bList = String(b)
+    .split(".")
+    .map((item) => Number(item || 0));
+  const length = Math.max(aList.length, bList.length);
+
+  for (let i = 0; i < length; i += 1) {
+    const aNum = aList[i] || 0;
+    const bNum = bList[i] || 0;
+
+    if (aNum > bNum) return 1;
+    if (aNum < bNum) return -1;
+  }
+
+  return 0;
+};
+
+/**
+ * 请求服务端检查是否存在新的 .wgt 资源包。
+ *
+ * 为什么这里直接使用 uni.request,而不是复用现有 utils/request.js:
+ * 1. 现有 request 封装默认会带 token、tenant-id、loading、401 刷新令牌等业务逻辑。
+ * 2. 热更新检查发生在 App 启动最早阶段,不应该依赖登录态,也不应该因为 token 过期影响更新检查。
+ * 3. 启动期代码越“去业务化”,越不容易被后续鉴权改造牵连。
+ *
+ * 请求参数说明:
+ * - appid: 用来确保服务端下发的是当前应用对应的更新包。
+ * - version: 当前资源版本,服务端据此判断是否需要更新。
+ * - platform: 当前平台,便于服务端做 Android / iOS 区分。
+ * - name: 可选,便于后端日志排查。
+ *
+ * 服务端返回格式约定:
+ * {
+ *   code: 0,
+ *   data: {
+ *     update: true,
+ *     packageType: "wgt",
+ *     version: "1.3.6",
+ *     wgtUrl: "https://example.com/app/1.3.6/update.wgt",
+ *     note: "1. 修复巡检表单保存异常\\n2. 优化首页加载速度",
+ *     force: false
+ *   },
+ *   message: "success"
+ * }
+ *
+ * 字段用途:
+ * - code: 业务状态码,0 表示接口业务成功。
+ * - data.update: 服务端是否认为当前客户端需要更新。
+ * - data.packageType: 更新类型。这里约定 wgt 表示资源热更新,native 表示整包更新。
+ * - data.version: 服务端最新资源版本。
+ * - data.wgtUrl: .wgt 下载地址。
+ * - data.note: 更新说明,弹窗里给用户看。
+ * - data.force: 是否强制更新。true 时不展示取消按钮。
+ * - message: 服务端通用提示。
+ */
+const requestHotUpdateInfo = () => {
+  console.log("11", 11);
+  return new Promise((resolve, reject) => {
+    uni.request({
+      url: HOT_UPDATE_API_URL,
+      method: "GET",
+      header: {
+        "Content-Type": "application/json",
+        "tenant-id": "1",
+      },
+      timeout: 10000,
+      success: (response) => {
+        if (response.statusCode !== 200) {
+          reject(
+            new Error(
+              `热更新检查接口请求失败,HTTP 状态码:${response.statusCode}`
+            )
+          );
+          return;
+        }
+
+        resolve(response.data || {});
+      },
+      fail: (error) => {
+        reject(
+          new Error(
+            error?.errMsg || "热更新检查接口请求失败,请检查网络或服务端状态"
+          )
+        );
+      },
+      complete: () => {
+        /**
+         * 这里故意不做 UI 处理,只保留 complete 生命周期位置。
+         *
+         * 原因:
+         * 启动期的检查更新是“后台检查”,不是用户手动点按钮触发的请求。
+         * 如果这里也弹 loading,会和现有登录页/首页首屏体验打架。
+         */
+      },
+    });
+  });
+};
+
+/**
+ * 规范化服务端返回,统一后续分支判断。
+ *
+ * 为什么要单独做这一层:
+ * 1. 启动流程最怕到处写 if/else,后面维护时很难看出到底在哪个条件提前返回。
+ * 2. 统一成“shouldUpdate + reason + payload”的结构,后面 orchestration 会清晰很多。
+ *
+ * 成功输出示例:
+ * {
+ *   shouldUpdate: true,
+ *   reason: "has-update",
+ *   payload: { ...服务端 data... }
+ * }
+ */
+const normalizeUpdateResult = (response, currentVersion) => {
+  console.log("response", response);
+
+  const code = response?.code;
+  const message = response?.message || response?.msg || "";
+  const data = response?.data || {};
+
+  if (code !== 0) {
+    return {
+      shouldUpdate: false,
+      reason: "server-error",
+      message: message || "热更新接口返回了业务错误",
+    };
+  }
+
+  // if (!data.update) {
+  //   return {
+  //     shouldUpdate: false,
+  //     reason: "no-update",
+  //     message: "服务端确认当前版本无需更新",
+  //   };
+  // }
+
+  // if (data.packageType && data.packageType !== "wgt") {
+  //   return {
+  //     shouldUpdate: false,
+  //     reason: "not-wgt",
+  //     message: `服务端返回的是 ${data.packageType} 更新,不属于本次 .wgt 热更新流程`,
+  //     payload: data,
+  //   };
+  // }
+
+  if (!data.version || !data.wgtUrl) {
+    return {
+      shouldUpdate: false,
+      reason: "invalid-data",
+      message: "服务端返回缺少 version 或 wgtUrl,无法继续热更新",
+    };
+  }
+
+  if (compareVersion(data.version, currentVersion) <= 0) {
+    return {
+      shouldUpdate: false,
+      reason: "not-newer",
+      message: "服务端返回的版本不高于当前版本,跳过本次热更新",
+      payload: data,
+    };
+  }
+
+  const skippedVersion = uni.getStorageSync(HOT_UPDATE_SKIP_VERSION_KEY);
+  if (!data.force && skippedVersion === data.version) {
+    return {
+      shouldUpdate: false,
+      reason: "skipped-same-version",
+      message: "用户之前已经跳过该版本,本次启动不再重复提示",
+      payload: data,
+    };
+  }
+
+  return {
+    shouldUpdate: true,
+    reason: "has-update",
+    payload: data,
+  };
+};
+
+/**
+ * 组装展示给用户的更新文案。
+ *
+ * 这里没有引入 i18n,也没有复用页面组件里的 UI。
+ *
+ * 原因:
+ * 1. 热更新发生在 App 启动阶段,应该尽量减少对页面层、全局实例、组件状态的依赖。
+ * 2. 现有项目的旧升级逻辑是在组件里拿 $t,这条链路不适合直接照搬到启动期。
+ * 3. 启动期用系统级的 uni.showModal 更稳,也更符合“最小侵入式改造”。
+ */
+const buildUpdateContent = (payload) => {
+  const note = payload.note
+    ? String(payload.note)
+    : "修复已知问题,优化使用体验";
+  const content = [`发现新的资源版本:${payload.version}`, "", note];
+
+  if (payload.force) {
+    content.push("", "该更新为强制更新,请完成安装后继续使用。");
+  }
+
+  return content.join("\n");
+};
+
+/**
+ * 提示用户是否开始热更新。
+ *
+ * 成功分支:
+ * - 用户点击确认,resolve(true),继续下载。
+ *
+ * 失败/取消分支:
+ * - 非强更时,用户点击取消,记录本次跳过的版本,resolve(false)。
+ * - 强更时,没有取消按钮,只能确认。
+ */
+const confirmHotUpdate = (payload) => {
+  return new Promise((resolve) => {
+    uni.showModal({
+      title: payload.force ? "发现重要更新" : "发现新版本",
+      content: buildUpdateContent(payload),
+      showCancel: !payload.force,
+      confirmText: "立即更新",
+      cancelText: "稍后再说",
+      success: (result) => {
+        if (result.confirm) {
+          uni.removeStorageSync(HOT_UPDATE_SKIP_VERSION_KEY);
+          resolve(true);
+          return;
+        }
+
+        if (!payload.force && result.cancel) {
+          uni.setStorageSync(HOT_UPDATE_SKIP_VERSION_KEY, payload.version);
+        }
+        resolve(false);
+      },
+      fail: () => {
+        resolve(false);
+      },
+    });
+  });
+};
+
+/**
+ * 下载 .wgt 文件。
+ *
+ * 为什么这里要单独拆出来:
+ * 1. 下载是一个明显独立的异步阶段,最需要看清楚成功和失败分支。
+ * 2. 下载是启动期流程里耗时较长的一步,单独封装后更方便控制提示方式。
+ *
+ * 用户可见表现:
+ * - 下载中:顶部展示固定 loading 提示,不展示实时百分比。
+ * - 下载成功:loading 自动关闭,进入安装阶段。
+ * - 下载失败:loading 关闭,并在上层统一提示“热更新失败”。
+ *
+ * 输出:
+ * - resolve(tempFilePath) 返回下载到本地后的临时文件路径。
+ */
+const downloadWgtPackage = (wgtUrl) => {
+  return new Promise((resolve, reject) => {
+    uni.showLoading({
+      title: "下载更新中",
+      mask: true,
+    });
+
+    uni.downloadFile({
+      url: wgtUrl,
+      timeout: 600000,
+      success: (response) => {
+        if (response.statusCode === 200 && response.tempFilePath) {
+          resolve(response.tempFilePath);
+          return;
+        }
+
+        reject(
+          new Error(
+            `热更新包下载失败,HTTP 状态码:${response.statusCode || "unknown"}`
+          )
+        );
+      },
+      fail: (error) => {
+        reject(new Error(error?.errMsg || "热更新包下载失败"));
+      },
+      complete: () => {
+        uni.hideLoading();
+      },
+    });
+  });
+};
+
+/**
+ * 安装下载完成后的 .wgt 包。
+ *
+ * 为什么安装时要带 appid:
+ * 1. 官方文档说明可以传入 appid 做校验。
+ * 2. 这可以避免服务端配置错误时,把不属于当前应用的资源包安装进来。
+ *
+ * 为什么 force 设为 false:
+ * 1. force=true 会跳过版本校验,风险更高。
+ * 2. 现在我们已经在服务端和客户端各做了一层版本判断,没有必要强制覆盖旧包。
+ */
+const installWgtPackage = (filePath, appid) => {
+  return new Promise((resolve, reject) => {
+    try {
+      plus.runtime.install(
+        filePath,
+        {
+          appid,
+          force: false,
+        },
+        () => {
+          resolve();
+        },
+        (error) => {
+          reject(
+            new Error(
+              error?.message || `热更新安装失败,错误码:${error?.code || ""}`
+            )
+          );
+        }
+      );
+    } catch (error) {
+      reject(error);
+    }
+  });
+};
+
+/**
+ * 安装完成后提示用户并重启应用。
+ *
+ * 为什么这里显式调用 plus.runtime.restart:
+ * 1. .wgt 安装完成只是把新资源放到了运行环境,想让用户真正跑到新代码,还需要重启应用。
+ * 2. 这一步和旧的页面级整包升级不同,旧逻辑 Android 是直接安装 apk。
+ * 3. 热更新的目标是“尽快让新前端资源生效”,因此安装完成后立即重启是最直接的方案。
+ *
+ * 注意这里在 restart 前先清掉锁:
+ * - 因为 storage 会跨重启保留。
+ * - 如果不先清,应用重启后旧组件会误以为“热更新一直在运行中”,造成后续检查失效。
+ */
+const restartAppAfterInstall = (targetVersion) => {
+  return new Promise((resolve) => {
+    uni.showModal({
+      title: "更新完成",
+      content: `资源版本 ${targetVersion} 已安装完成,点击确定后将立即重启应用使更新生效。`,
+      showCancel: false,
+      success: () => {
+        setHotUpdateRunning(false);
+        plus.runtime.restart();
+        resolve();
+      },
+      fail: () => {
+        /**
+         * 即便弹窗失败,我们也仍然执行重启。
+         *
+         * 原因:
+         * - 到这里说明安装已经完成。
+         * - 如果不重启,用户还会继续运行旧资源,和“安装成功”的状态不一致。
+         */
+        setHotUpdateRunning(false);
+        plus.runtime.restart();
+        resolve();
+      },
+    });
+  });
+};
+
+/**
+ * 统一展示“热更新失败”提示。
+ *
+ * 为什么不把所有失败都提示给用户:
+ * 1. 启动阶段的“检查接口失败 / 无网络”很常见,如果每次启动都弹错,会严重影响体验。
+ * 2. 所以我们只对“用户已经明确点击开始更新之后”的失败给出弹窗提示。
+ * 3. 纯检查阶段失败,记录日志后继续进入原有流程即可。
+ */
+const showHotUpdateError = (error, title = "更新失败") => {
+  uni.showModal({
+    title,
+    content: error?.message || "热更新执行失败,请稍后重试",
+    showCancel: false,
+  });
+};
+
+/**
+ * 对外暴露的启动期热更新入口。
+ *
+ * 这是你后续在 App.vue onLaunch 里真正调用的函数。
+ *
+ * 整体执行顺序:
+ * 1. 等待 plus 就绪
+ * 2. 获取当前资源版本
+ * 3. 请求服务端检查更新
+ * 4. 判断是否存在新的 .wgt 包
+ * 5. 提示用户确认
+ * 6. 下载 .wgt
+ * 7. 安装 .wgt
+ * 8. 重启应用
+ *
+ * 返回值:
+ * - Promise<{ status: string, ... }>
+ *   主要用于日志或后续扩展,目前 App.vue 不需要依赖它的返回值。
+ */
+export const runWgtHotUpdate = async () => {
+  // #ifdef APP-PLUS
+  if (isHotUpdateRunning()) {
+    return {
+      status: "locked",
+      message: "已有热更新流程在执行,跳过重复检查",
+    };
+  }
+
+  let currentStage = "prepare";
+  setHotUpdateRunning(true);
+
+  try {
+    currentStage = "plus-ready";
+    await waitForPlusReady();
+
+    currentStage = "read-local-version";
+    const wgtInfo = await getCurrentWgtInfo();
+
+    currentStage = "request-server";
+    const response = await requestHotUpdateInfo();
+    const updateResult = normalizeUpdateResult(response, wgtInfo.version);
+
+    if (!updateResult.shouldUpdate) {
+      return {
+        status: updateResult.reason,
+        message: updateResult.message,
+        data: updateResult.payload || null,
+      };
+    }
+
+    currentStage = "confirm";
+    const confirmed = await confirmHotUpdate(updateResult.payload);
+    if (!confirmed) {
+      return {
+        status: "cancelled",
+        message: "用户取消本次热更新",
+      };
+    }
+
+    currentStage = "download";
+    const filePath = await downloadWgtPackage(updateResult.payload.wgtUrl);
+
+    currentStage = "install";
+    await installWgtPackage(filePath, wgtInfo.appid);
+
+    currentStage = "restart";
+    await restartAppAfterInstall(updateResult.payload.version);
+
+    return {
+      status: "restarted",
+      message: "热更新安装完成,应用已触发重启",
+    };
+  } catch (error) {
+    console.error("[hot-update] 执行失败:", currentStage, error);
+
+    if (currentStage === "download") {
+      showHotUpdateError(error, "下载失败");
+    } else if (currentStage === "install" || currentStage === "restart") {
+      showHotUpdateError(error, "安装失败");
+    } else {
+      /**
+       * 检查阶段失败只打日志,不主动打扰用户。
+       *
+       * 为什么这么处理:
+       * - 当前项目 onLaunch 里还有数据库初始化、url scheme 参数接收等逻辑。
+       * - 热更新检查只是“加分项”,不应该因为检查失败阻断原有启动流程。
+       * - 这样最符合“最小侵入式改造”的目标。
+       */
+    }
+
+    return {
+      status: "error",
+      stage: currentStage,
+      error,
+    };
+  } finally {
+    /**
+     * 正常情况下:
+     * - 无更新 / 用户取消 / 下载失败 / 安装失败,都会走到这里清锁。
+     * - 安装成功后会在 restart 之前先清锁,所以这里再次清理也没有副作用。
+     */
+    setHotUpdateRunning(false);
+  }
+  // #endif
+
+  return {
+    status: "skip-non-app-plus",
+    message: "当前不是 APP-PLUS 环境,跳过 .wgt 热更新检查",
+  };
+};

+ 21 - 17
utils/upgrade.js

@@ -1,3 +1,5 @@
+import { getCurrentWgtInfo } from "./hot-update";
+
 /**
  * @description 检查app版本是否需要升级
  * @param name:最新版本名称
@@ -6,23 +8,18 @@
  * @param url:下载链接
  * @param forceUpdate:是否强制升级
  */
-export const checkVersion = ({
+export const checkVersion = async ({
   name, //最新版本名称
   code, //最新版本号
   content, //更新内容
   url, //下载链接
   forceUpdate, //是否强制升级
 }) => {
-  const selfVersionCode = uni.getAppBaseInfo().appVersion; // uni.getSystemInfoSync().appVersion; //当前App版本号
-  // console.log('selfVersionCode-getAppBaseInfo', uni.getAppBaseInfo())
-  console.log("selfVersionCode1", selfVersionCode);
-  //线上版本号高于当前,进行在线升级
-  if (code > selfVersionCode) {
+  // const selfVersionCode = uni.getAppBaseInfo().appVersion;
+  const info = await getCurrentWgtInfo();
+  if (code > info.version) {
     let platform = uni.getSystemInfoSync().platform; //手机平台
-    console.log("platform", platform);
-    //安卓手机弹窗升级
     if (platform === "android") {
-      //当前页面不是升级页面跳转防止多次打开
       uni.$emit("upgrade-app", {
         name,
         content,
@@ -30,9 +27,7 @@ export const checkVersion = ({
         forceUpdate,
       });
       return true;
-    }
-    //IOS无法在线升级提示到商店下载
-    else {
+    } else {
       uni.showModal({
         title: "发现新版本 V" + name,
         content: "请到App store进行升级",
@@ -40,6 +35,8 @@ export const checkVersion = ({
       });
     }
   }
+
+  return false;
 };
 
 //获取当前页面url
@@ -64,7 +61,7 @@ export const downloadApp = (downloadUrl, progressCallBack = () => {}) => {
   // 进度更新节流控制变量
   let lastProgressTime = 0;
   const PROGRESS_INTERVAL = 300; // 限制300ms内最多更新一次进度
-  
+
   return new Promise((resolve, reject) => {
     // 创建下载任务
     const downloadTask = plus.downloader.createDownload(
@@ -100,15 +97,22 @@ export const downloadApp = (downloadUrl, progressCallBack = () => {}) => {
           const hasProgress = task.totalSize && task.totalSize > 0;
           if (hasProgress) {
             mockProgress = 0;
-            const current = parseInt((100 * task.downloadedSize) / task.totalSize);
+            const current = parseInt(
+              (100 * task.downloadedSize) / task.totalSize
+            );
             progressCallBack(current, task.downloadedSize, task.totalSize);
           } else {
             // 模拟进度:添加节流控制和平滑增长
-            if (mockProgress < 95 && (now - lastProgressTime > PROGRESS_INTERVAL)) {
+            if (
+              mockProgress < 95 &&
+              now - lastProgressTime > PROGRESS_INTERVAL
+            ) {
               // 根据时间间隔动态调整增长速度,避免停滞感
               const baseIncrement = 1;
-              const maxIncrement = mockProgress < 30 ? 3 : mockProgress < 60 ? 2 : 1;
-              const increment = Math.floor(Math.random() * maxIncrement) + baseIncrement;
+              const maxIncrement =
+                mockProgress < 30 ? 3 : mockProgress < 60 ? 2 : 1;
+              const increment =
+                Math.floor(Math.random() * maxIncrement) + baseIncrement;
               mockProgress = Math.min(mockProgress + increment, 95);
               lastProgressTime = now; // 更新最后更新时间
               progressCallBack(mockProgress, task.downloadedSize, 0);