Переглянути джерело

feat: 增加全局拦截判断和首次启动的协议弹窗

leo 2 днів тому
батько
коміт
a3dffa2e60
10 змінених файлів з 3483 додано та 2068 видалено
  1. 13 3
      App.vue
  2. 4 0
      main.js
  3. 51 0
      mixins/privacyMixin.js
  4. 1870 1750
      pages.json
  5. 150 47
      pages/login/agreement.vue
  6. BIN
      pages/login/assets/logo.png
  7. 528 268
      pages/login/login.vue
  8. 535 0
      pages/login/loginOld.vue
  9. 256 0
      pages/privacy/privacy.vue
  10. 76 0
      util/privacyConfig.js

+ 13 - 3
App.vue

@@ -38,9 +38,10 @@ import { fetchPermissionList } from './util/QueryPermission.js';
 // App.vue
 export default {
   onLaunch() {
-    // 已登录时提前拉取权限列表,保证“未经过首页”等特殊入口也能拿到权限数据(非阻塞)
-    if (uni.getStorageSync('session_key')) {
-      fetchPermissionList();
+    // 已同意隐私协议 → 执行业务初始化(拉取权限列表等)
+    // 未同意 → 等待页面层(如 login.vue)弹出自定义隐私弹窗,同意后再初始化
+    if (uni.getStorageSync('privacy_agreed')) {
+      this.doAfterPrivacy();
     }
 
     // 监听分享到朋友圈
@@ -64,6 +65,15 @@ export default {
       imageUrl: '',
     };
   },
+  methods: {
+    // 隐私协议同意后才执行的业务初始化(可被页面层调用)
+    doAfterPrivacy() {
+      // 已登录时提前拉取权限列表,保证"未经过首页"等特殊入口也能拿到权限数据(非阻塞)
+      if (uni.getStorageSync('session_key')) {
+        fetchPermissionList();
+      }
+    },
+  },
 };
 </script>
 <style lang="scss">

+ 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.$imageHost = config.imageHost; // 线上图片服务器路径常量 58003
 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
+  },
+};

Різницю між файлами не показано, бо вона завелика
+ 1870 - 1750
pages.json


+ 150 - 47
pages/login/agreement.vue

@@ -1,21 +1,42 @@
 <template>
   <view class="agreement-page">
     <view class="agreement-title">用户服务与隐私协议</view>
-    <view class="agreement-date">更新日期:2026-09-01</view>
+    <view class="agreement-meta">
+      <view class="meta-version">版本:V2.0</view>
+      <view class="meta-date">更新日期:2026-09-14</view>
+      <view class="meta-effective">生效日期:2026-09-14</view>
+    </view>
     <view class="agreement-intro">
-      欢迎使用本小程序!您在使用前需同意本协议。我们致力于为您提供优质服务,同时保障您的合法权益。您应妥善保管账号信息,并对账号下的一切行为负责。
+      欢迎使用本小程序!您在使用前需同意本协议。我们致力于为您提供优质服务,同时依法保障您的个人信息权益。您应妥善保管账号信息,并对账号下的一切行为负责。
     </view>
 
     <view class="agreement-summary">
+      <view class="summary-title">一图读懂:信息收集概览</view>
       <view class="summary-content">
-        我们深知个人信息对您的重要性,并承诺依法保护您的隐私。我们仅收集实现服务所必要的信息,包括但不限于
+        我们深知个人信息对您的重要性,并承诺依法保护您的隐私。我们遵循"最小必要"原则收集信息,以下为核心信息一览表
       </view>
-      <view class="summary-item" v-for="(item, i) in summary" :key="i">
-        <text class="item-name">{{ item.name }}</text>
-        <text class="item-desc">—— {{ item.desc }}</text>
+      <view class="summary-table">
+        <view class="table-row table-header">
+          <view class="col-type">信息类型</view>
+          <view class="col-purpose">使用目的</view>
+          <view class="col-auth">授权类型</view>
+        </view>
+        <view class="table-row" v-for="(item, i) in summary" :key="i">
+          <view class="col-type">{{ item.name }}</view>
+          <view class="col-purpose">{{ item.desc }}</view>
+          <view class="col-auth">
+            <view
+              class="auth-tag"
+              :class="
+                item.authType === '必要' ? 'auth-required' : 'auth-optional'
+              "
+              >{{ item.authType }}</view
+            >
+          </view>
+        </view>
       </view>
       <view class="summary-footer">
-        我们不会主动向第三方共享您的个人信息,除非获得您的明确同意或法律法规要求。
+        我们不会主动向第三方共享您的个人信息,除非获得您的明确同意或法律法规要求。"可选"授权拒绝后不影响其他功能的正常使用。
       </view>
     </view>
 
@@ -47,19 +68,23 @@ export default {
       summary: [
         {
           name: '微信昵称、头像',
-          desc: '用于展示您的身份',
+          desc: '展示您的身份',
+          authType: '必要',
         },
         {
           name: '手机号',
-          desc: '用于售后联系与账号安全',
+          desc: '售后联系与账号安全',
+          authType: '必要',
         },
         {
           name: '位置信息',
-          desc: '用于推荐附近服务(需您授权)',
+          desc: '推荐附近服务',
+          authType: '可选',
         },
         {
           name: '设备信息',
-          desc: '用于保障服务稳定运行',
+          desc: '保障服务稳定运行',
+          authType: '必要',
         },
       ],
       sections: [
@@ -71,49 +96,59 @@ export default {
         {
           title: '二、我们收集的信息',
           content: [
-            '我们遵循"最小必要"原则,仅收集实现服务所必需的信息:',
-            '1. 微信昵称、头像:在您授权后获取,用于在小程序内展示您的身份;',
-            '2. 手机号:在您绑定或授权后获取,用于售后服务联系与账号安全验证;',
-            '3. 位置信息:在您授权后获取,用于为您推荐附近的服务拒绝授权仅会影响该功能,不影响其他功能的正常使用;',
-            '4. 设备信息:如设备型号、操作系统版本、网络状态等,用于保障服务的稳定运行和问题排查',
-            '除上述信息外,我们不会收集与服务无关的个人信息。',
+            '我们遵循"最小必要"与"目的明确"原则,仅收集实现服务所必需的信息。以下为各类信息的收集目的、授权类型及拒绝授权的影响:',
+            '1. 微信昵称、头像【必要授权】:在您授权后获取,用于在小程序内展示您的身份;拒绝授权将无法展示您的个性化身份信息,但不影响其他功能使用;',
+            '2. 手机号【必要授权】:在您绑定或授权后获取,用于售后服务联系与账号安全验证;拒绝授权将影响售后联系及账号安全相关功能;',
+            '3. 位置信息【可选授权】:在您授权后获取,用于为您推荐附近的服务拒绝授权仅会影响该功能,不影响其他功能的正常使用;',
+            '4. 设备信息【必要授权】:如设备型号、操作系统版本、网络状态等,用于保障服务的稳定运行和问题排查;该信息为服务运行自动采集,无法单独拒绝;',
+            '除上述信息外,我们不会收集与服务无关的个人信息,不会收集您的微信聊天内容、通讯录、相册等敏感信息。',
           ],
         },
         {
           title: '三、我们如何使用信息',
           content: [
-            '我们收集的信息将用于:',
-            '1. 验证您的身份,保障账号与数据安全;',
-            '2. 为您提供设备管理、监测数据展示、预警通知等核心服务;',
-            '3. 与您取得联系,处理售后、报修及咨询等事项;',
-            '4. 分析与改进服务性能,优化您的使用体验。',
-            '我们不会将您的个人信息用于上述目的之外的任何用途。',
+            '我们收集的信息将严格限于以下使用场景:',
+            '1. 身份验证与账号安全:验证您的身份,保障账号与数据安全,识别异常登录或风险行为;',
+            '2. 核心服务提供:为您提供设备管理、监测数据展示、预警通知等核心服务功能;',
+            '3. 售后服务:与您取得联系,处理售后、报修及咨询等事项,提升服务质量;',
+            '4. 服务改进:分析服务运行情况,优化性能与使用体验(仅使用去标识化或匿名化后的数据);',
+            '5. 合规义务:履行法律法规规定的义务,配合监管或司法机关的合法要求。',
+            '我们不会将您的个人信息用于上述目的之外的任何用途。如需变更使用目的,我们将再次征得您的同意。',
           ],
         },
         {
           title: '四、信息的共享与披露',
           content: [
-            '我们不会主动向第三方共享、转让或公开披露您的个人信息,但以下情形除外:',
-            '1. 事先获得您的明确同意;',
-            '2. 根据法律法规、司法机关或行政机关的强制性要求;',
-            '3. 为提供核心服务所必需,且仅共享实现服务所必需的最少信息(例如委托技术合作方进行数据存储或消息推送),我们将要求其遵守保密义务并采取足够的安全措施。',
+            '我们不会主动向第三方共享、转让或公开披露您的个人信息。以下情形除外:',
+            '1. 事先获得您的明确同意:经您授权后,我们才会共享相应的个人信息;',
+            '2. 法定强制要求:根据法律法规、司法机关或行政机关的强制性要求;',
+            '3. 委托处理:为提供核心服务所必需,我们可能委托技术合作方进行数据存储或消息推送,仅共享实现服务所必需的最少信息,并要求其签署保密协议、采取足够的安全措施,不得用于其他用途;',
+            '4. 合并重组:如我们涉及合并、收购或资产转让,我们将确保接收方继续履行本协议,并在变更用途前再次征得您的同意。',
+            '我们承诺:不向任何第三方出售您的个人信息;不与关联方进行未经授权的数据共享。',
           ],
         },
         {
           title: '五、信息的存储与保护',
-          content:
-            '您的个人信息存储于中华人民共和国境内。我们采用加密传输、访问控制、安全审计等技术手段和管理措施,防止您的信息被未经授权地访问、篡改、泄露或丢失。我们仅在实现本协议所述目的所必需的期限内保留您的个人信息,超出期限后将对您的个人信息进行删除或匿名化处理。',
+          content: [
+            '1. 存储地点:您的个人信息存储于中华人民共和国境内,不会跨境传输;',
+            '2. 存储期限:我们仅在实现本协议所述目的所必需的最短期限内保留您的个人信息:',
+            '   - 账号信息:账号存续期间;账号注销后30日内删除或匿名化处理;',
+            '   - 交易/服务记录:自服务终止之日起保留3年(法律法规另有要求的除外);',
+            '   - 设备日志信息:保留期限不超过6个月;',
+            '3. 安全措施:我们采用加密传输、访问控制、安全审计等技术手段和管理措施,防止您的信息被未经授权地访问、篡改、泄露或丢失;',
+            '4. 安全事件响应:如发生个人信息安全事件,我们将及时启动应急预案,并在15个工作日内通过公告或站内信等方式告知您事件情况、处置措施及应对建议。',
+          ],
         },
         {
           title: '六、您的权利',
           content: [
-            '按照相关法律法规,您有权:',
-            '1. 随时查阅您的个人信息;',
-            '2. 更正不准确或不完整的信息;',
-            '3. 删除您的部分或全部个人信息;',
-            '4. 撤回此前作出的授权同意,撤回授权不影响此前基于授权已进行的信息处理的合法性;',
-            '5. 注销账号。',
-            '您可以通过小程序内的相关功能或联系我们行使上述权利,我们将在核实您的身份后及时处理。',
+            '按照《个人信息保护法》等相关法律法规,您对个人信息享以下利。您可通过联系客服行使:',
+            '1. 知情权与查阅权:您有权随时查阅我们收集的您的个人信息类型与内容;',
+            '2. 更正权:您有权要求更正不准确或不完整的信息,我们将在核实身份后15个工作日内处理;',
+            '3. 删除权:您有权要求删除您的部分或全部个人信息(法律法规要求保留的除外),我们将在15个工作日内处理;',
+            '4. 账号注销权:您有权随时注销账号,注销后我们将停止使用并删除您的相关信息。您可联系客服发起注销申请;',
+            '5. 投诉举报权:如您认为我们处理个人信息的行为损害了您的合法权益,您有权向网信、公安等监管部门投诉举报。',
+            '我们将在收到您的请求后核实身份并及时处理,原则上不超过15个工作日。如遇特殊情况,处理时限最长不超过30个工作日,并会向您说明原因。',
           ],
         },
         {
@@ -123,8 +158,15 @@ export default {
         },
         {
           title: '八、协议的更新',
-          content:
-            '我们可能根据法律法规变化或服务调整适时修订本协议,修订后的协议将在本页面发布。当协议发生重大变更时,我们会以显著方式提示您,请您定期查阅本协议以了解最新内容。若您在协议更新后继续使用本小程序服务,即视为您已接受修订后的协议。',
+          content: [
+            '1. 版本公示:本协议当前版本号为V2.0,最新更新日期为2026-09-14,生效日期为2026-09-14;',
+            '2. 修订情形:我们可能根据法律法规变化、监管要求或服务调整适时修订本协议;',
+            '3. 告知机制:',
+            '   - 一般变更:修订后的协议将在本页面发布,并通过站内通知告知您;',
+            '   - 重大变更(如信息收集范围扩大、使用目的变更、新增第三方共享等):我们将通过弹窗显著提示,并要求您重新确认是否同意;',
+            '4. 查阅建议:请您定期查阅本协议以了解最新内容。如您不同意修订后的协议,您可停止使用本小程序并申请注销账号;',
+            '5. 历史版本:如需查阅历史版本,请联系客服获取。',
+          ],
         },
       ],
     };
@@ -148,11 +190,24 @@ export default {
   margin-bottom: 16rpx;
 }
 
-.agreement-date {
+.agreement-meta {
+  display: flex;
+  justify-content: center;
+  flex-wrap: wrap;
   font-size: 24rpx;
   color: #999;
-  text-align: center;
   margin-bottom: 40rpx;
+
+  .meta-version,
+  .meta-date,
+  .meta-effective {
+    margin: 0 12rpx;
+  }
+
+  .meta-version {
+    color: #3a8ee6;
+    font-weight: bold;
+  }
 }
 
 .agreement-intro {
@@ -164,8 +219,11 @@ export default {
 }
 
 .agreement-summary {
-  padding: 24rpx 24rpx;
+  padding: 24rpx;
   margin-bottom: 40rpx;
+  background-color: #f7f9fc;
+  border-radius: 12rpx;
+  border-left: 6rpx solid #3a8ee6;
 
   .summary-title {
     font-size: 30rpx;
@@ -182,14 +240,59 @@ export default {
     margin-bottom: 16rpx;
   }
 
-  .summary-item {
-    font-size: 26rpx;
-    color: #666;
-    line-height: 1.9;
+  .summary-table {
+    margin-bottom: 16rpx;
+    border: 1rpx solid #e4e7ed;
+    border-radius: 8rpx;
+    overflow: hidden;
+  }
 
-    .item-name {
-      color: #333;
+  .table-row {
+    display: flex;
+    align-items: center;
+    padding: 16rpx 12rpx;
+    border-bottom: 1rpx solid #e4e7ed;
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    &.table-header {
+      background-color: #ecf5ff;
       font-weight: bold;
+      color: #333;
+    }
+
+    .col-type {
+      flex: 0 0 30%;
+      font-size: 26rpx;
+    }
+
+    .col-purpose {
+      flex: 1;
+      font-size: 26rpx;
+    }
+
+    .col-auth {
+      flex: 0 0 20%;
+      text-align: center;
+    }
+  }
+
+  .auth-tag {
+    display: inline-block;
+    padding: 2rpx 12rpx;
+    border-radius: 20rpx;
+    font-size: 22rpx;
+
+    &.auth-required {
+      background-color: #fef0f0;
+      color: #f56c6c;
+    }
+
+    &.auth-optional {
+      background-color: #f0f9eb;
+      color: #67c23a;
     }
   }
 

BIN
pages/login/assets/logo.png


Різницю між файлами не показано, бо вона завелика
+ 528 - 268
pages/login/login.vue


+ 535 - 0
pages/login/loginOld.vue

@@ -0,0 +1,535 @@
+<template>
+  <view>
+    <view class="bg">
+      <image
+        :src="$imageURL + '/bigdata_app/log/bg.png'"
+        mode=""
+        class="bgimg"
+      ></image>
+    </view>
+    <!-- <view class="status_bar"></view> -->
+    <view class="login-center">
+      <view class="apptitle"> 云飞智控 </view>
+      <view class="set" @click="set" v-if="setTF">
+        <u-icon name="setting-fill" size="50" color="#fff"></u-icon>
+      </view>
+      <view class="formbox">
+        <form @submit="formSubmit">
+        <view class="uni-form-item uni-column">
+          <view class="username">
+            <u-icon
+              name="account"
+              size="36"
+              style="margin-right: 30rpx; color: #fff"
+            ></u-icon>
+            <u-input
+              class="uni-input"
+              name="username"
+              v-model="formdata.username"
+              placeholder-class="icon iconfont icon-bianji1"
+              placeholder="请输入用户名"
+              placeholderStyle="color:#ffffff;"
+              color="#FFFFFF"
+              @blur="blur"
+            />
+          </view>
+          <view class="passwold">
+            <u-icon
+              name="lock"
+              size="36"
+              style="margin-right: 30rpx; color: #fff"
+            ></u-icon>
+            <u-input
+              v-model="formdata.passwold"
+              type="password"
+              :password-icon="true"
+              :clearable="false"
+              placeholder="请输入密码"
+              @confirm="formSubmit"
+              @input="passwoldddata"
+              placeholderStyle="color:#fff;"
+              suffixIconStyle="color:#fff;"
+              color="#fff"
+              class="uni-input"
+            />
+          </view>
+          <view class="aboutpass">
+            <u-checkbox-group>
+              <u-checkbox
+                v-model="checked"
+                :label-disabled="false"
+                size="28"
+                @change="rempass"
+                >记住密码
+              </u-checkbox>
+            </u-checkbox-group>
+          </view>
+          <view class="uni-btn-v">
+            <button form-type="submit" @click="denglu">登 录</button>
+          </view>
+          <!--用户隐私协议-->
+          <view class="agreement">
+            <u-checkbox-group>
+              <u-checkbox
+                v-model="agree"
+                shape="circle"
+                size="28"
+                active-color="#2979ff"
+              ></u-checkbox>
+            </u-checkbox-group>
+            <text class="agree-text" @click="agree = !agree"
+              >我已阅读并同意</text
+            >
+            <text class="agree-link" @click="goAgreement"
+              >《用户隐私协议》</text
+            >
+          </view>
+        </view>
+      </form>
+    </view>
+    </view>
+    <u-modal
+      title="升级中请勿随意操作"
+      :show-confirm-button="false"
+      v-model="showA"
+      :content="contentA"
+    >
+      <view class="upgradeBox">
+        <u-line-progress
+          v-show="isShow"
+          active-color="#19be6b"
+          :striped="true"
+          :percent="percentNum"
+          :striped-active="true"
+        ></u-line-progress>
+      </view>
+    </u-modal>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      checked: false,
+      formdata: {
+        username: '',
+        passwold: '',
+      },
+      setbgtf: false,
+      setTF: false,
+      value: 'http://8.136.98.49:8002',
+      httparr: ['http://8.136.98.49:8002', 'http://dev.hnyfwlw.com'],
+      arrowtf: false,
+      showA: false, //
+      contentA: '',
+      isShow: false, //进度条
+      percentNum: 0, //在线下载进度
+      passvalue: false,
+      agree: false, //是否同意隐私协议
+    };
+  },
+  onLoad() {
+    uni.getStorage({
+      key: 'user_pass',
+      success: (res) => {
+        if (res.data) {
+          this.formdata.passwold = res.data;
+          this.checked = true;
+        } else {
+          this.checked = false;
+        }
+      },
+    });
+    uni.getStorage({
+      key: 'user_name',
+      success: (res) => {
+        this.formdata.username = res.data;
+      },
+    });
+    uni.getStorage({
+      key: 'http',
+      success: (res) => {
+        this.value = res.data;
+      },
+    });
+    this.getEquipList();
+  },
+  onShow() {},
+  methods: {
+    async getEquipList() {
+      const res = await this.$myRequest({
+        url: '/api/api_gateway?method=home.homes.app_version_record',
+        data: {
+          ret: 'first',
+        },
+      });
+      console.log(res);
+      this.appName = res[0].app_name;
+      // uni.getStorage({
+      // 	key: 'session_key',
+      // 	success: (res) => {
+      // 		console.log(res)
+      // 		if (res.data != "") {
+      // 			uni.switchTab({
+      // 				url: "../index/index"
+      // 			})
+      // 		}
+      // 	},
+      // })
+      // }
+    },
+    async formSubmit() {
+      if (!this.agree) {
+        uni.showToast({
+          title: '请先阅读并同意用户隐私协议',
+          icon: 'none',
+        });
+        return;
+      }
+      const res = await this.$myRequest({
+        url: '/api/api_gateway?method=user.login.login_user',
+        data: {
+          username: this.formdata.username,
+          password: this.formdata.passwold,
+        },
+      });
+      console.log(res.session_key);
+      let session_key = res.session_key;
+      uni.setStorage({
+        key: 'session_key',
+
+        data: session_key,
+        success: () => {
+          // 登录前清除可能残留的权限缓存,再用新账号拉取
+          this.$resetPermissionList();
+          this.$fetchPermissionList();
+          uni.switchTab({
+            url: '../index/index',
+          });
+        },
+      });
+    },
+    passwoldddata() {
+      this.formdata.passwold = this.formdata.passwold.replace(
+        /[\u4E00-\u9FA5]/g,
+        '',
+      );
+    },
+    rempass(val) {
+      this.passvalue = val.value;
+      if (val.value) {
+        uni.setStorage({
+          key: 'user_pass',
+          data: this.formdata.passwold,
+          success: function () {
+            console.log('success');
+          },
+        });
+      }
+    },
+    blur(val) {
+      uni.setStorage({
+        key: 'user_name',
+        data: val,
+        success: function () {
+          console.log('success');
+        },
+      });
+    },
+    logoTime() {
+      this.setTF = true;
+    },
+    set() {
+      this.setbgtf = true;
+    },
+    sethttp() {
+      uni.setStorage({
+        key: 'http',
+        data: this.value,
+        success: () => {
+          // console.log(this.value);
+          this.setbgtf = false;
+          uni.showToast({
+            title: '修改成功',
+            icon: 'none',
+          });
+          this.getEquipList();
+        },
+      });
+    },
+    arrow() {
+      this.arrowtf = !this.arrowtf;
+    },
+    denglu() {
+      if (this.passvalue) {
+        uni.setStorage({
+          key: 'user_pass',
+          data: this.formdata.passwold,
+          success: function () {
+            console.log('success');
+          },
+        });
+      } else {
+        uni.removeStorage({
+          key: 'user_pass',
+          success: function () {
+            console.log('success');
+          },
+        });
+      }
+    },
+    goAgreement() {
+      uni.navigateTo({
+        url: './agreement',
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+.bg {
+  width: 100%;
+  height: 100vh;
+  position: absolute;
+  top: 0;
+  left: 0;
+  .bgimg {
+    width: 100%;
+    height: 100%;
+  }
+}
+.login-center {
+  position: relative;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+.apptitle {
+  font-size: 52rpx;
+  color: #fff;
+  width: 80%;
+  margin: 0 auto 60rpx;
+}
+.logo {
+  width: 100%;
+  height: 340rpx;
+  text-align: center;
+  display: flex;
+  align-items: center;
+  padding-top: 240rpx;
+
+  image {
+    width: 280rpx;
+    margin: 0 auto;
+    height: 120rpx;
+  }
+}
+
+.set {
+  position: absolute;
+  right: 50rpx;
+  top: 150rpx;
+}
+
+.bg {
+  width: 100%;
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  z-index: -1;
+
+  image {
+    width: 100%;
+  }
+}
+
+/deep/.u-input__right-icon {
+  line-height: 35px !important;
+}
+.uni-form-item {
+  width: 100%;
+
+  .username {
+    width: 80%;
+    margin: 0 auto;
+    display: flex;
+    margin-bottom: 40rpx;
+    padding-bottom: 10rpx;
+    border-bottom: 2rpx solid rgba(249, 249, 249, 0.4);
+
+    .u-icon__icon {
+      margin-top: 17rpx;
+    }
+
+    .uni-input {
+      width: 100%;
+      color: #fff;
+    }
+    .u-input__input {
+      color: #fff !important;
+    }
+  }
+
+  .passwold {
+    width: 80%;
+    margin: 0 auto;
+    display: flex;
+    margin-bottom: 40rpx;
+    padding-bottom: 10rpx;
+    border-bottom: 2rpx solid rgba(249, 249, 249, 0.4);
+
+    .u-icon__icon {
+      margin-top: 17rpx;
+    }
+    /deep/.u-input__input {
+      color: #fff;
+    }
+    .uni-input {
+      width: 100%;
+      color: #fff;
+    }
+    /deep/.uicon-eye {
+      color: #fff !important;
+    }
+  }
+
+  .aboutpass {
+    width: 80%;
+    margin: 0 auto;
+    display: flex;
+    justify-content: flex-end;
+
+    p {
+      color: #fff;
+      font-size: 28rpx;
+    }
+    /deep/.uicon-checkbox-mark {
+      border-color: #ff0000;
+      // color: #f00 !important;
+    }
+    /deep/.u-checkbox__label {
+      font-size: 28rpx;
+      color: #fff;
+      margin-right: 0;
+    }
+  }
+
+  .uni-btn-v {
+    width: 80%;
+    margin: 112rpx auto 0;
+    position: relative;
+    z-index: 100;
+
+    button {
+      width: 100%;
+      height: 90rpx;
+      line-height: 90rpx;
+      color: #ffffff;
+      font-size: 36rpx;
+      background-image: linear-gradient(
+        to bottom,
+        rgba(249, 249, 249, 0.6),
+        rgba(249, 249, 249, 0.1)
+      );
+      color: #5dc18b;
+    }
+  }
+}
+
+.setbg {
+  width: 100%;
+  height: 100vh;
+  position: absolute;
+  top: 0;
+  z-index: 99999;
+
+  .mengban {
+    width: 100%;
+    height: 100vh;
+    position: absolute;
+    top: 0;
+    background-color: rgba($color: #000000, $alpha: 0.5);
+  }
+
+  .set_http {
+    position: absolute;
+    width: 90%;
+    left: 5%;
+    top: 30%;
+
+    .set_http_top {
+      display: flex;
+      justify-content: space-around;
+      background-color: #5dc18b;
+      height: 60px;
+      line-height: 60px;
+      color: #ffffff;
+      border-top-right-radius: 20px;
+      border-top-left-radius: 20px;
+      font-size: 32rpx;
+    }
+
+    .set_http_bot {
+      height: 150px;
+      background-color: #ffffff;
+      border-bottom-right-radius: 20px;
+      border-bottom-left-radius: 20px;
+      padding: 30px;
+
+      .set_http_bot_input {
+        margin-top: 20rpx;
+        border: 2rpx solid #bdb6a6;
+        border-radius: 20rpx;
+        padding: 10rpx 40rpx 0 20rpx;
+        font-size: 32rpx;
+        height: 30px;
+        display: flex;
+        justify-content: space-between;
+
+        input {
+          width: 90%;
+        }
+      }
+    }
+
+    .scroll-Y {
+      border: 2rpx solid #d0d0d0;
+      border-radius: 20rpx;
+
+      .scroll-view-item {
+        padding-left: 20rpx;
+        height: 70rpx;
+        font-size: 28rpx;
+        line-height: 70rpx;
+        border-bottom: 2rpx solid #d0d0d0;
+      }
+    }
+  }
+}
+
+.upgradeBox {
+  padding: 15rpx;
+}
+
+.agreement {
+  width: 80%;
+  margin: 40rpx auto 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  .agree-text {
+    font-size: 24rpx;
+    color: #fff;
+    margin-left: 10rpx;
+  }
+  .agree-link {
+    font-size: 24rpx;
+    color: #fff;
+    text-decoration: underline;
+  }
+}
+</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>

+ 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);
+}