Explorar o código

feat: 登录页改版和协议弹窗逻辑以及全局拦截处理

leo hai 1 día
pai
achega
c90fe1c237
Modificáronse 9 ficheiros con 593 adicións e 159 borrados
  1. 4 55
      App.vue
  2. 4 0
      main.js
  3. 51 0
      mixins/privacyMixin.js
  4. 36 31
      pages.json
  5. 152 68
      pages/login/agreement.vue
  6. 12 3
      pages/login/login.vue
  7. 256 0
      pages/privacy/privacy.vue
  8. 2 2
      util/api.js
  9. 76 0
      util/privacyConfig.js

+ 4 - 55
App.vue

@@ -3,65 +3,14 @@ import { fetchPermissionList } from './util/QueryPermission.js';
 
 export default {
   onLaunch: function () {
-    console.log(' APP onLaunch');
-    // 全局隐私协议检查:首次启动时通过原生 uni.showModal 弹出
-    if (!uni.getStorageSync('privacy_agreed')) {
-      this.showPrivacyModal();
-    } else {
-      // 已同意过,直接执行初始化
+    // 已同意隐私协议 → 执行业务初始化(拉取权限列表等)
+    // 未同意 → 等待页面层(如 login.vue)弹出自定义隐私弹窗,同意后再初始化
+    if (uni.getStorageSync('privacy_agreed')) {
       this.doAfterPrivacy();
     }
   },
   methods: {
-    // 独立的隐私协议弹窗方法(避免递归调用 onLaunch)
-    showPrivacyModal() {
-      uni.showModal({
-        title: '用户服务与隐私协议',
-        content:
-          '欢迎使用本应用!在您首次使用前,请认真阅读《用户服务与隐私协议》。我们将遵循"最小必要"原则,仅收集实现服务所必需的信息(微信昵称、头像、手机号、位置信息、设备信息等),不会主动向第三方共享您的个人信息。',
-        confirmText: '同意并继续',
-        cancelText: '不同意',
-        success: (res) => {
-          if (res.confirm) {
-            // 用户同意,记录状态并执行初始化
-            uni.setStorageSync('privacy_agreed', true);
-            this.doAfterPrivacy();
-          } else {
-            // 用户不同意,二次确认
-            uni.showModal({
-              title: '提示',
-              content: '您需要同意隐私协议才能使用本应用',
-              confirmText: '再次查看完整协议',
-              cancelText: '退出应用',
-              success: (res2) => {
-                if (res2.confirm) {
-                  // 跳转到完整协议页面
-                  uni.navigateTo({
-                    url: '/pages/login/agreement',
-                  });
-                  // 同时重新弹出摘要弹窗让用户做最终决定
-                  setTimeout(() => {
-                    this.showPrivacyModal();
-                  }, 500);
-                } else {
-                  // 退出应用
-                  // #ifdef APP-PLUS
-                  plus.runtime.quit();
-                  // #endif
-                }
-              },
-            });
-          }
-        },
-        fail: () => {
-          // 弹窗失败(如 App 启动早期),延迟重试
-          setTimeout(() => {
-            this.showPrivacyModal();
-          }, 300);
-        },
-      });
-    },
-    // 隐私协议同意后才执行的初始化逻辑
+    // 隐私协议同意后才执行的业务初始化(可被页面层调用)
     doAfterPrivacy() {
       // 已登录时提前拉取权限列表,保证"未经过首页"等特殊入口也能拿到权限数据(非阻塞)
       if (uni.getStorageSync('session_key')) {

+ 4 - 0
main.js

@@ -9,6 +9,10 @@ Vue.config.productionTip = false;
 
 App.mpType = 'app';
 
+// 全局隐私协议检查 mixin(所有页面的 onShow 自动拦截)
+import privacyMixin from './mixins/privacyMixin.js';
+Vue.mixin(privacyMixin);
+
 import config from './util/neutral.js';
 Vue.prototype.$isneutral = config.isneutral;
 Vue.prototype.$imageURL = config.imageURL; // 线上图片服务器路径常量

+ 51 - 0
mixins/privacyMixin.js

@@ -0,0 +1,51 @@
+/**
+ * 全局隐私协议检查 mixin
+ * 注入所有页面的 onShow 生命周期
+ * 通过版本号对比判断是否需要重新弹窗,支持协议更新后自动重新同意
+ * 作用于:App 端 + 小程序端(H5 不拦截)
+ */
+import { needPrivacyPopup } from '@/util/privacyConfig.js';
+
+// 防止多次 reLaunch 冲突导致页面卡死
+let isReLaunching = false;
+
+export default {
+  onShow() {
+    // privacy 页面自身不拦截(避免死循环)
+    // 协议详情页也不拦截(用户从弹窗点"查看完整协议"跳过去时不能被 reLaunch 回来,否则白屏)
+    // 登录页也不拦截(登录本身不涉及隐私数据使用,用户应能自由访问;
+    //   若未同意隐私协议,登录成功进入主页时仍会被拦截到 privacy)
+    // 关于我们页也不拦截(纯静态信息展示,不涉及隐私数据使用)
+    // 注意:不能用 this.$route,vue-router 只在 H5 端存在
+    // 统一用 getCurrentPages() 获取当前页面路径(三端都支持)
+    const pages = getCurrentPages();
+    if (pages && pages.length > 0) {
+      const current = pages[pages.length - 1];
+      const route = '/' + (current.route || '');
+
+      if (
+        route.indexOf('/pages/privacy/') !== -1 ||
+        route.indexOf('/pages/login/agreement') !== -1 ||
+        route.indexOf('/pages/login/login') !== -1 ||
+        route.indexOf('/pages/my/about/about') !== -1
+      ) {
+        return;
+      }
+    }
+
+    // App 端 + 小程序端:都拦截隐私协议(版本号不匹配时)
+    // #ifndef H5
+    if (needPrivacyPopup()) {
+      // 防抖锁:避免多个页面 onShow 同时触发 reLaunch 导致页面栈混乱
+      if (isReLaunching) return;
+      isReLaunching = true;
+      uni.reLaunch({
+        url: '/pages/privacy/privacy',
+        complete: () => {
+          isReLaunching = false;
+        },
+      });
+    }
+    // #endif
+  },
+};

+ 36 - 31
pages.json

@@ -12,6 +12,14 @@
       }
     },
     {
+      "path": "pages/privacy/privacy",
+      "style": {
+        "navigationBarTitleText": "",
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
+      }
+    },
+    {
       "path": "pages/login/agreement",
       "style": {
         "navigationBarTitleText": "用户隐私协议",
@@ -23,17 +31,17 @@
       "path": "pages/index/index",
       "style": {
         "navigationBarTitleText": "首页",
-				"navigationStyle": "custom",
+        "navigationStyle": "custom",
         "navigationBarBackgroundColor": "#00B075"
       }
     },
-		{
-			"path": "pages/index/developing",
-			"style": {
-				"navigationBarTitleText": "智能AI助理",
-				"enablePullDownRefresh": false
-			}
-		},
+    {
+      "path": "pages/index/developing",
+      "style": {
+        "navigationBarTitleText": "智能AI助理",
+        "enablePullDownRefresh": false
+      }
+    },
     {
       "path": "pages/cb/index/index",
       "style": {
@@ -257,7 +265,7 @@
       "style": {
         "navigationBarTitleText": "个人中心",
         "enablePullDownRefresh": false,
-				"navigationStyle": "custom",
+        "navigationStyle": "custom",
         "navigationBarBackgroundColor": "#00B075"
       }
     },
@@ -267,7 +275,7 @@
         "navigationBarTitleText": "设备列表",
         "navigationBarBackgroundColor": "#00B075",
         "enablePullDownRefresh": false,
-				"navigationStyle": "custom"
+        "navigationStyle": "custom"
       }
     },
     {
@@ -275,7 +283,7 @@
       "style": {
         "navigationBarTitleText": "信息修改",
         "enablePullDownRefresh": false,
-				"navigationStyle": "custom"
+        "navigationStyle": "custom"
       }
     },
     {
@@ -1013,22 +1021,22 @@
         "navigationStyle": "custom"
       }
     },
-		{
-			"path": "pages/server/index",
-			"style": {
-				"navigationBarTitleText": "更多服务",
-				"navigationStyle": "custom",
-				"enablePullDownRefresh": false
-			}
-		},
-		{
-			"path": "pages/banner/index",
-			"style": {
-				"navigationBarTitleText": "水肥",
-				"navigationStyle": "custom",
-				"enablePullDownRefresh": false
-			}
-		},
+    {
+      "path": "pages/server/index",
+      "style": {
+        "navigationBarTitleText": "更多服务",
+        "navigationStyle": "custom",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/banner/index",
+      "style": {
+        "navigationBarTitleText": "水肥",
+        "navigationStyle": "custom",
+        "enablePullDownRefresh": false
+      }
+    },
     {
       "path": "pages/cb/zhamenFirst/zhamenzs",
       "style": {
@@ -1323,7 +1331,7 @@
         "navigationStyle": "custom"
       }
     },
-    
+
     {
       "path": "pages/deviceDetails/weatherStation1/index",
       "style": {
@@ -1360,7 +1368,6 @@
       }
     },
 
-    
     {
       "path": "pages/deviceDetails/weatherStation2/index",
       "style": {
@@ -1397,8 +1404,6 @@
       }
     },
 
-
-    
     {
       "path": "pages/deviceDetails/SoilMoisturelist/index",
       "style": {

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 152 - 68
pages/login/agreement.vue


+ 12 - 3
pages/login/login.vue

@@ -160,6 +160,8 @@
 </template>
 
 <script>
+import { needPrivacyPopup } from '@/util/privacyConfig.js';
+
 export default {
   data() {
     return {
@@ -183,8 +185,11 @@ export default {
       appVersion: '',
     };
   },
-  onLoad() {},
   onShow() {
+    // 已同意当前版本的隐私协议,自动勾选表单协议复选框
+    if (!needPrivacyPopup()) {
+      this.agree = true;
+    }
     // 获取当前 app 版本号
     // #ifdef APP-PLUS
     this.appVersion = plus.runtime.version;
@@ -512,7 +517,7 @@ export default {
 .login-container {
   height: 100vh;
   width: 100%;
-  background: linear-gradient(180deg, #e8f7ef 0%, #f5f9f6 45%, #ffffff 100%);
+  background: linear-gradient(180deg, #d1f2e8 6.77%, #fff 28.02%);
   position: relative;
   overflow: hidden;
   box-sizing: border-box;
@@ -525,7 +530,6 @@ export default {
   left: 0;
   width: 100%;
   height: 420rpx;
-  background: linear-gradient(180deg, #c8ecdb 0%, #e8f7ef 100%);
   z-index: 0;
 }
 
@@ -770,4 +774,9 @@ export default {
 .upgradeBox {
   padding: 15rpx;
 }
+
+::v-deep .u-flex {
+  display: flex;
+  align-items: center;
+}
 </style>

+ 256 - 0
pages/privacy/privacy.vue

@@ -0,0 +1,256 @@
+<template>
+  <view class="privacy-container">
+    <!-- 遮罩层 -->
+    <view class="privacy-mask"></view>
+    <!-- 弹窗卡片 -->
+    <view class="privacy-content">
+      <view class="privacy-title">{{ titleText }}</view>
+      <view class="privacy-body">
+        <view class="privacy-intro">
+          {{ introText }}
+          <text class="privacy-highlight">《用户服务与隐私协议》</text>
+          。我们将遵循"最小必要"原则,仅收集实现服务所必需的信息。
+        </view>
+        <view class="privacy-list">
+          <view class="privacy-item">
+            <text class="privacy-dot">•</text>
+            <text>微信昵称、头像 —— 用于展示您的身份</text>
+          </view>
+          <view class="privacy-item">
+            <text class="privacy-dot">•</text>
+            <text>手机号 —— 用于售后联系与账号安全</text>
+          </view>
+          <view class="privacy-item">
+            <text class="privacy-dot">•</text>
+            <text>位置信息 —— 用于推荐附近服务(需授权)</text>
+          </view>
+          <view class="privacy-item">
+            <text class="privacy-dot">•</text>
+            <text>设备信息 —— 用于保障服务稳定运行</text>
+          </view>
+        </view>
+        <view class="privacy-note">
+          我们不会主动向第三方共享您的个人信息,除非获得您的明确同意或法律法规要求。
+        </view>
+        <text class="privacy-link" @click="goAgreement"
+          >查看完整协议详情 ></text
+        >
+      </view>
+      <view class="privacy-footer">
+        <view class="privacy-btn privacy-btn-cancel" @click="onDisagree">
+          不同意
+        </view>
+        <view class="privacy-btn privacy-btn-confirm" @click="onAgree">
+          同意并继续
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import {
+  markPrivacyAgreed,
+  PRIVACY_STORAGE_KEY,
+} from '@/util/privacyConfig.js';
+
+export default {
+  name: 'PrivacyPage',
+  data() {
+    return {
+      // 是否为协议更新(老用户之前同意过旧版本)
+      isUpdate: false,
+    };
+  },
+  computed: {
+    // 标题:首次显示"用户服务与隐私协议",更新时显示"隐私协议已更新"
+    titleText() {
+      return this.isUpdate ? '隐私协议已更新' : '用户服务与隐私协议';
+    },
+    // 介绍文案
+    introText() {
+      return this.isUpdate
+        ? '我们更新了隐私协议,请您认真阅读'
+        : '欢迎使用本应用!在您首次使用前,请认真阅读';
+    },
+  },
+  created() {
+    // 判断是首次还是协议更新:
+    // 有老数据 privacy_agreed: true(旧方案) 或 有旧版本号(新方案但版本不同)→ 视为更新
+    const oldFlag = uni.getStorageSync('privacy_agreed');
+    const oldVersion = uni.getStorageSync(PRIVACY_STORAGE_KEY);
+    this.isUpdate = !!(oldFlag || (oldVersion && oldVersion.length > 0));
+  },
+  methods: {
+    // 同意隐私协议
+    onAgree() {
+      // 存版本号(替换老数据)
+      markPrivacyAgreed();
+      // 清理老的 boolean 标志(如果存在)
+      uni.removeStorageSync('privacy_agreed');
+
+      uni.showToast({
+        title: '已同意隐私协议',
+        icon: 'none',
+        duration: 1200,
+      });
+      setTimeout(() => {
+        // #ifdef APP-PLUS
+        // 通知 App.vue 执行全局初始化
+        var app = getApp();
+        if (app && app.doAfterPrivacy) {
+          app.doAfterPrivacy();
+        }
+        // #endif
+        // 跳回首页
+        uni.reLaunch({
+          url: '/pages/index/index',
+        });
+      }, 800);
+    },
+    // 不同意
+    onDisagree() {
+      uni.showModal({
+        title: '提示',
+        content: '您需要同意隐私协议才能使用本应用',
+        confirmText: '再次查看',
+        cancelText: '退出应用',
+        success: (res) => {
+          if (!res.confirm) {
+            // 退出应用:App 端和小程序端分别处理
+            // #ifdef APP-PLUS
+            plus.runtime.quit();
+            // #endif
+            // #ifdef MP
+            uni.showToast({
+              title: '请同意隐私协议后重试',
+              icon: 'none',
+            });
+            // #endif
+          }
+          // "再次查看"则什么也不做,弹窗仍在
+        },
+      });
+    },
+    // 查看完整协议
+    goAgreement() {
+      uni.navigateTo({
+        url: '/pages/login/agreement',
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.privacy-container {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 9999;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.privacy-mask {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.55);
+}
+
+.privacy-content {
+  position: relative;
+  width: 80%;
+  max-width: 620rpx;
+  background-color: #ffffff;
+  border-radius: 24rpx;
+  overflow: hidden;
+}
+
+.privacy-title {
+  text-align: center;
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333333;
+  padding: 40rpx 40rpx 0;
+}
+
+.privacy-body {
+  padding: 28rpx 40rpx 0;
+}
+
+.privacy-intro {
+  font-size: 26rpx;
+  color: #555555;
+  line-height: 1.7;
+
+  .privacy-highlight {
+    color: #24b35d;
+  }
+}
+
+.privacy-list {
+  margin-top: 20rpx;
+
+  .privacy-item {
+    font-size: 24rpx;
+    color: #666666;
+    line-height: 2;
+
+    .privacy-dot {
+      color: #24b35d;
+      margin-right: 6rpx;
+    }
+  }
+}
+
+.privacy-note {
+  margin-top: 16rpx;
+  font-size: 24rpx;
+  color: #999999;
+  line-height: 1.6;
+}
+
+.privacy-link {
+  display: block;
+  margin-top: 28rpx;
+  font-size: 26rpx;
+  color: #24b35d;
+  text-align: center;
+  padding: 10rpx 0;
+}
+
+.privacy-footer {
+  display: flex;
+  border-top: 1rpx solid #f0f0f0;
+  margin-top: 32rpx;
+
+  .privacy-btn {
+    flex: 1;
+    height: 96rpx;
+    line-height: 96rpx;
+    text-align: center;
+    font-size: 28rpx;
+
+    &.privacy-btn-cancel {
+      color: #999999;
+      border-right: 1rpx solid #f0f0f0;
+    }
+
+    &.privacy-btn-confirm {
+      color: #24b35d;
+      font-weight: 500;
+    }
+
+    &:active {
+      opacity: 0.7;
+    }
+  }
+}
+</style>

+ 2 - 2
util/api.js

@@ -11,8 +11,8 @@ export const myRequest = (options) => {
     // BASE_URL = 'http://218.28.198.186:10508';
     // BASE_URL = 'http://8.136.98.49:8002';
   }
-  // BASE_URL = config.productAPI;
-  BASE_URL = config.developAPI;
+  BASE_URL = config.productAPI;
+  // BASE_URL = config.developAPI;
   var session_key = '';
   session_key = uni.getStorageSync('session_key');
   let url = '';

+ 76 - 0
util/privacyConfig.js

@@ -0,0 +1,76 @@
+/**
+ * 隐私协议版本号 - 自动跟随 App/小程序版本
+ *
+ * 【正常流程】
+ *   每次发版时,开发者只需要改 manifest.json 中的 versionName / versionCode,
+ *   或者发版工具自动改版本号,隐私协议就会自动重新弹窗,无需手动调整此处。
+ *
+ * 【特殊兜底】
+ *   如果协议改了但不想发新版 App,把下方 FORCE_REFRESH_FLAG 改成新值即可
+ *   (比如改成日期 '2026-10-01'),所有用户下次启动会重新弹窗。
+ *   协议没改时保持空字符串 '' 即可。
+ *
+ * 各端获取版本号方式:
+ *   App 端    → plus.runtime.versionCode   (manifest.json 中 versionCode)
+ *   小程序端  → uni.getAccountInfoSync().miniProgram.version
+ */
+
+// ====== 兜底强制刷新标志 ======
+// 协议变了但 App/小程序版本号没变时,改这里触发重新弹窗
+// 保持 '' 不额外触发;改成任意新值(如 '2026-10-01')即可强制所有用户重新同意
+const FORCE_REFRESH_FLAG = '';
+
+// 本地存储 key
+export const PRIVACY_STORAGE_KEY = 'privacy_version';
+
+// 动态获取当前运行端的版本号(含兜底标志)
+function getCurrentVersion() {
+  let base = '';
+
+  // #ifdef APP-PLUS
+  // App 端:用 versionCode(数字,每次发版必变)
+  base = 'app_' + plus.runtime.versionCode;
+  // #endif
+
+  // #ifdef MP
+  // 小程序端:用微信小程序平台的版本号
+  try {
+    const info = uni.getAccountInfoSync();
+    base =
+      'mp_' +
+      (info.miniProgram.version || info.miniProgram.envVersion || 'dev');
+  } catch (e) {
+    base = 'mp_unknown';
+  }
+  // #endif
+
+  // #ifdef H5
+  // H5 暂不强制拦截
+  return 'h5_static';
+  // #endif
+
+  // 拼上兜底标志:有值时即使版本号没变也触发重新弹窗
+  return FORCE_REFRESH_FLAG ? base + '__' + FORCE_REFRESH_FLAG : base;
+}
+
+// 判断是否需要重新弹窗
+// 用户同意的版本号 !== 当前运行端版本号 → 需要弹窗
+export function needPrivacyPopup() {
+  // #ifdef H5
+  // H5 不强制拦截
+  return false;
+  // #endif
+
+  const current = getCurrentVersion();
+  const saved = uni.getStorageSync(PRIVACY_STORAGE_KEY);
+
+  // 没有记录(含老数据 privacy_agreed: true)或版本号不匹配 → 需要弹窗
+  if (!saved) return true;
+  return saved !== current;
+}
+
+// 标记已同意当前版本的隐私协议
+export function markPrivacyAgreed() {
+  const current = getCurrentVersion();
+  uni.setStorageSync(PRIVACY_STORAGE_KEY, current);
+}