Răsfoiți Sursa

Merge branch 'main' into feature/v1.1.0

yuelujie 4 luni în urmă
părinte
comite
59f28871c8
74 a modificat fișierele cu 3596 adăugiri și 532 ștergeri
  1. 1 0
      apps/mobile/package.json
  2. 15 2
      apps/mobile/src/components/ad/Ad.vue
  3. 12 0
      apps/mobile/src/components/ad/Swipe.vue
  4. 15 2
      apps/mobile/src/components/ad/SwipeFloor.vue
  5. 15 2
      apps/mobile/src/components/ad/pop-screen/index.vue
  6. 24 9
      apps/mobile/src/components/footer-tabbar/index.vue
  7. 26 1
      apps/mobile/src/components/home/list.vue
  8. 11 0
      apps/mobile/src/components/message/message-card.vue
  9. 157 12
      apps/mobile/src/components/mine/MineHeader.vue
  10. 18 3
      apps/mobile/src/components/mine/MineList.vue
  11. 29 5
      apps/mobile/src/components/mine/OrderTabs.vue
  12. 42 15
      apps/mobile/src/components/mine/SignIn.vue
  13. 15 1
      apps/mobile/src/components/recommend/index.vue
  14. 79 68
      apps/mobile/src/components/search/bidding/oneKeySubscribeDialog.vue
  15. 11 0
      apps/mobile/src/components/treasure-box/Recommend.vue
  16. 30 2
      apps/mobile/src/components/treasure-box/SunshineGuide.vue
  17. 12 0
      apps/mobile/src/composables/quick-monitor/component/QuickMonitor.vue
  18. 2 1
      apps/mobile/src/main.js
  19. 2 1
      apps/mobile/src/utils/format/modules/ad-formatter.js
  20. 18 10
      apps/mobile/src/views/ai-search/index.vue
  21. 11 0
      apps/mobile/src/views/article/components/ActionCollection.vue
  22. 11 0
      apps/mobile/src/views/article/components/ActionShareToWorkmate.vue
  23. 35 3
      apps/mobile/src/views/article/components/ContentMainText.vue
  24. 11 0
      apps/mobile/src/views/article/components/DataExportBanner.vue
  25. 11 0
      apps/mobile/src/views/article/components/DownProjectReport.vue
  26. 15 1
      apps/mobile/src/views/article/components/OriginLink.vue
  27. 2 2
      apps/mobile/src/views/article/components/TabActions.vue
  28. 81 84
      apps/mobile/src/views/article/content.vue
  29. 11 0
      apps/mobile/src/views/article/ui/MaskCard.vue
  30. 24 0
      apps/mobile/src/views/article/ui/ServiceIntroCard.vue
  31. 35 0
      apps/mobile/src/views/dataSmt/detail.vue
  32. 16 1
      apps/mobile/src/views/dataSmt/index.vue
  33. 15 1
      apps/mobile/src/views/identity/components/CheckVipExpire.vue
  34. 45 2
      apps/mobile/src/views/landing/consultingService.vue
  35. 193 69
      apps/mobile/src/views/search/result/bidding/index.vue
  36. 13 3
      apps/mobile/src/views/search/result/buyer/index.vue
  37. 32 20
      apps/mobile/src/views/search/result/company/index.vue
  38. 13 4
      apps/mobile/src/views/search/result/sun/index.vue
  39. 28 1
      apps/mobile/src/views/subscribe/Guide.vue
  40. 42 4
      apps/mobile/src/views/tabbar/Box.vue
  41. 2 2
      apps/mobile/src/views/tabbar/Mine.vue
  42. 5 0
      plugins/bind-phone-mobile/.browserslistrc
  43. 5 0
      plugins/bind-phone-mobile/.editorconfig
  44. 7 0
      plugins/bind-phone-mobile/.env.development
  45. 7 0
      plugins/bind-phone-mobile/.env.production
  46. 16 0
      plugins/bind-phone-mobile/.eslintignore
  47. 42 0
      plugins/bind-phone-mobile/.gitignore
  48. 5 0
      plugins/bind-phone-mobile/.npmrc
  49. 153 0
      plugins/bind-phone-mobile/README.md
  50. 101 0
      plugins/bind-phone-mobile/index.html
  51. 46 0
      plugins/bind-phone-mobile/package.json
  52. 30 0
      plugins/bind-phone-mobile/postcss.config.js
  53. 45 0
      plugins/bind-phone-mobile/src/App.vue
  54. 29 0
      plugins/bind-phone-mobile/src/api/api.js
  55. 4 0
      plugins/bind-phone-mobile/src/api/index.js
  56. 51 0
      plugins/bind-phone-mobile/src/api/interceptors.js
  57. 5 0
      plugins/bind-phone-mobile/src/api/service.js
  58. 120 0
      plugins/bind-phone-mobile/src/assets/style.css
  59. 417 0
      plugins/bind-phone-mobile/src/components/BindPhoneDialog.vue
  60. 19 0
      plugins/bind-phone-mobile/src/entry.js
  61. 20 0
      plugins/bind-phone-mobile/src/index.js
  62. 32 0
      plugins/bind-phone-mobile/src/main.js
  63. 25 0
      plugins/bind-phone-mobile/src/router/index.js
  64. 318 0
      plugins/bind-phone-mobile/src/utils/appFn.js
  65. 98 0
      plugins/bind-phone-mobile/src/utils/directives/bind-phone.js
  66. 52 0
      plugins/bind-phone-mobile/src/utils/plugins/directive-bind-phone.js
  67. 97 0
      plugins/bind-phone-mobile/src/utils/plugins/prototype-bind-phone.js
  68. 5 0
      plugins/bind-phone-mobile/src/utils/prototype/env.js
  69. 106 0
      plugins/bind-phone-mobile/src/utils/prototype/platform.js
  70. 64 0
      plugins/bind-phone-mobile/src/utils/utils.js
  71. 100 0
      plugins/bind-phone-mobile/src/views/test.vue
  72. 103 0
      plugins/bind-phone-mobile/vite.config.js
  73. 32 17
      plugins/gift-friends/src/components/GiftSubmitDialog.vue
  74. 257 184
      pnpm-lock.yaml

+ 1 - 0
apps/mobile/package.json

@@ -15,6 +15,7 @@
     "@jy/data-models": "workspace:^",
     "@jy/util": "workspace:^",
     "@jy/vue-anti": "workspace:^",
+    "@jy/plugin-bind-phone": "workspace:^",
     "@sentry/vue": "^7.64.0",
     "@tinymce/tinymce-vue": "^3.2.8",
     "aliyun_numberauthsdk_web": "^2.1.9",

+ 15 - 2
apps/mobile/src/components/ad/Ad.vue

@@ -13,7 +13,7 @@
       :src="getConfig.pic"
       :alt="getConfig.name"
       :style="getStyle"
-      @click.stop="openAD"
+      @click.stop="bindPhoneOpenAd"
       @load="loadSuccess"
     >
       <div v-if="showTag" class="tag-text">广告</div>
@@ -72,7 +72,8 @@ export default {
             width: '',
             height: '',
             type: ''
-          }
+          },
+          script: ''
         }
       }
     },
@@ -230,6 +231,18 @@ export default {
       } else {
         console.warn(msg)
       }
+    },
+    bindPhoneOpenAd() {
+      const { script, name } = this.getConfig || {}
+      this.$testBindPhone({
+        props: {
+          name: '广告位-' + name
+        },
+        pass: !script?.tip ? this.openAD : null,
+        next: () => {
+          this.openAD()
+        }
+      })
     }
   }
 }

+ 12 - 0
apps/mobile/src/components/ad/Swipe.vue

@@ -14,6 +14,7 @@
           :key="index"
           :style="{ width: getItemWidth }"
           class="van-clickable"
+          v-bound-phone="bindPhoneOpen(item)"
           @click="open(item)"
         >
           <van-image
@@ -125,6 +126,17 @@ export default {
         }
       }
       return openLinkOfAd(ad)
+    },
+    bindPhoneOpen(item) {
+      return {
+        props: {
+          name: '广告位-' + item.title || item.name
+        },
+        pass: !item?.script ? this.open : null,
+        next: () => {
+          this.open(item)
+        }
+      }
     }
   }
 }

+ 15 - 2
apps/mobile/src/components/ad/SwipeFloor.vue

@@ -15,6 +15,8 @@
         v-for="(floor, fIndex) in getImages"
         :key="fIndex"
         :id="'ad-swipe-floor-item-' + (floor.name || fIndex)"
+        :data-script="floor.script.tip"
+        v-bound-phone="bindPhoneOpenFloor(floor)"
       >
         <van-image fit="contain" :src="floor.pic" :alt="floor.name"></van-image>
         <span>{{ floor.name }}</span>
@@ -76,7 +78,7 @@ export default {
           type: 'progressbar'
         },
         on: {
-          click: this.open
+          // click: this.open
         }
       },
       list: []
@@ -108,7 +110,6 @@ export default {
         codes
       })
         .then((res) => {
-          console.log(res)
           if (res?.error_code === 0 && typeof res?.data === 'object') {
             this.list = Object.values(res.data)
               .filter((v) => Array.isArray(v))
@@ -178,6 +179,18 @@ export default {
         }
       }
       console.warn('获取广告位点击信息异常')
+    },
+    bindPhoneOpenFloor(floor) {
+      const { name, script } = floor || {}
+      return {
+        props: {
+          name: `首页-金刚区${name}`
+        },
+        pass: !script?.tip ? this.open : null,
+        next: () => {
+          this.open()
+        }
+      }
     }
   }
 }

+ 15 - 2
apps/mobile/src/components/ad/pop-screen/index.vue

@@ -17,7 +17,7 @@
         width: getStyle.width,
         height: getStyle.height
       }"
-      @click.stop="openAD"
+      @click.stop="bindPhoneOpenAd"
     >
       <AdSvga
         v-if="isSvgaType"
@@ -79,7 +79,8 @@ export default {
             width: '',
             height: '',
             type: ''
-          }
+          },
+          script: ''
         }
       }
     }
@@ -160,6 +161,18 @@ export default {
         title: this.config.name,
         type: this.config.extend.type
       })
+    },
+    bindPhoneOpenAd() {
+      const { script, name } = this.config
+      this.$testBindPhone({
+        props: {
+          name: '广告位-' + name
+        },
+        pass: !script?.tip ? this.openAD : null,
+        next: () => {
+          this.openAD()
+        }
+      })
     }
   }
 }

+ 24 - 9
apps/mobile/src/components/footer-tabbar/index.vue

@@ -172,18 +172,33 @@ export default {
       const item = this.navs[navIndex]
       this.setActiveIcon(key)
       this.$emit('change', item)
-      if(!this.isLogin) {
-        if (item.key === 'subscribe') {
-          // TODO 跳转到订阅引导页
-          location.replace('/jyapp/freesubscribe/')
-        } else if (item.key === 'box') {
-          // TODO 跳转到工作台引导页
-          location.replace('/jyapp/workbench/')
-        } else {
+      if (!this.isLogin) {
+        if (this.$env.platform === 'app') {
           callChangeTab(item.key, this.$router)
+        } else {
+          if (item.key === 'subscribe') {
+            // TODO 跳转到订阅引导页
+            location.replace('/jyapp/freesubscribe/')
+          } else if (item.key === 'box') {
+            // TODO 跳转到工作台引导页
+            location.replace('/jyapp/workbench/')
+          } else {
+            callChangeTab(item.key, this.$router)
+          }
         }
       } else {
-        callChangeTab(item.key, this.$router)
+        if (item.key === 'message') {
+          this.$testBindPhone({
+            props: {
+              name: '底部导航-消息'
+            },
+            next: () => {
+              callChangeTab(item.key, this.$router)
+            }
+          })
+        } else {
+          callChangeTab(item.key, this.$router)
+        }
         this.getCount()
       }
     }

+ 26 - 1
apps/mobile/src/components/home/list.vue

@@ -25,6 +25,7 @@
         <span
           class="flex flex-items-center header-set"
           @click="gotoKeySettingPage"
+          v-bound-phone="bindPhoneGoSetting()"
           >去设置 <van-icon name="arrow" color="#2ABED1"></van-icon
         ></span>
       </div>
@@ -67,7 +68,11 @@
           </div>
         </template>
         <template slot="icon">
-          <div class="right-event" @click.stop="onKeep(item, index)">
+          <div
+            class="right-event"
+            @click.stop="onKeep(item, index)"
+            v-bound-phone="bindPhoneOnKeep(item, index)"
+          >
             <AppIcon
               style="margin-right: 4px"
               svg
@@ -610,6 +615,26 @@ export default {
             this.loadList()
           }
         })
+    },
+    bindPhoneOnKeep(item, index) {
+      return {
+        props: {
+          name: '首页-商机推荐标讯收藏'
+        },
+        next: () => {
+          this.onKeep(item, index)
+        }
+      }
+    },
+    bindPhoneGoSetting() {
+      return {
+        props: {
+          name: '首页-商机推荐去设置'
+        },
+        next: () => {
+          this.gotoKeySettingPage()
+        }
+      }
     }
   }
 }

+ 11 - 0
apps/mobile/src/components/message/message-card.vue

@@ -25,6 +25,7 @@
             :class="{ visited: item.visited }"
             ref="listItem"
             @click="goMessage(item)"
+            v-bound-phone="bindPhoneGoMessage(item)"
           >
             <!-- <span class="dot"></span> -->
             <van-cell class="message-item-cell" is-link>
@@ -244,6 +245,16 @@ export default {
       // window.open在安卓正常,但是ios无法跳转。改为location.href跳转
       if (!url) return
       location.href = url
+    },
+    bindPhoneGoMessage(item) {
+      return {
+        props: {
+          name: '首页-消息列表'
+        },
+        next: () => {
+          this.goMessage(item)
+        }
+      }
     }
   }
 }

+ 157 - 12
apps/mobile/src/components/mine/MineHeader.vue

@@ -24,7 +24,11 @@
             'single-identity-container': userIdentityList.length === 1
           }"
         >
-          <span class="change-identity" @click="changeIdentityHandle">
+          <span
+            class="change-identity"
+            @click="changeIdentityHandle"
+            v-bound-phone="bindPhoneChangeIdentityHandle()"
+          >
             <span class="identity-name ellipsis">
               {{ userCurrentIdentity ? userCurrentIdentity.name : '' }}
             </span>
@@ -35,13 +39,21 @@
               color="#171826"
             />
           </span>
-          <span class="identity-keep" @click.stop="jumpIdentityKeep">
+          <span
+            class="identity-keep"
+            @click.stop="jumpIdentityKeep"
+            v-bound-phone="bindPhoneJumpIdentityKeep()"
+          >
             <span>身份信息维护</span>
             <AppIcon name="youbian" size="10" color="#fff" />
           </span>
         </div>
       </div>
-      <div class="app-header-set clickable" @click="jumpSetting">
+      <div
+        class="app-header-set clickable"
+        @click="jumpSetting"
+        v-bound-phone="bindPhoneJumpSetting()"
+      >
         <AppIcon name="Setting" size="24" color="#fff" />
       </div>
     </div>
@@ -53,8 +65,10 @@
         <div
           class="account-info-name"
           :class="{ 'active-type': temp.selected && accountList.length > 1 }"
-          v-for="temp in accountList"
+          v-for="(temp, index) in accountList"
+          :key="index"
           @click="changeAccountType(temp)"
+          v-bound-phone="bindPhoneChangeAccountType(temp)"
         >
           <span class="type-name"
             >{{ accountList.length === 1 ? '账号类型:' : ''
@@ -85,7 +99,10 @@
           </p>
           <p>
             <span>立享27+项专属权益</span>
-            <span class="self-handle-btn" @click="toBuyVip('buy')"
+            <span
+              class="self-handle-btn"
+              @click="toBuyVip('buy')"
+              v-bound-phone="bindPhoneToBuyVip('buy')"
               >立即开通</span
             >
           </p>
@@ -98,7 +115,11 @@
         >
           <div class="account-handle-item bg-line-gold">
             <p class="bold">到期时间:{{ activeAccountType.vipEntTime }}</p>
-            <p class="self-handle-btn" @click="renewVip">
+            <p
+              class="self-handle-btn"
+              @click="renewVip"
+              v-bound-phone="bindPhoneRenewVip()"
+            >
               去续费 <AppIcon name="youbian" size="16" color="#171826" />
             </p>
           </div>
@@ -108,6 +129,7 @@
             <p
               class="self-handle-btn"
               @click="updateVip(activeAccountType.attr)"
+              v-bound-phone="bindPhoneUpdateVip(activeAccountType.attr)"
             >
               <span>{{
                 activeAccountType.attr.upgrade ? '去升级' : '升级咨询'
@@ -124,7 +146,11 @@
         >
           <div class="account-handle-item bg-line-blue">
             <p class="bold">到期时间:{{ activeAccountType.vipEntTime }}</p>
-            <p class="self-handle-btn" @click="areaVip('renew')">
+            <p
+              class="self-handle-btn"
+              @click="areaVip('renew')"
+              v-bound-phone="bindPhoneAreaVip('renew')"
+            >
               去续费 <AppIcon name="youbian" size="16" color="#171826" />
             </p>
           </div>
@@ -139,6 +165,7 @@
             <p
               class="self-handle-btn"
               @click="areaVipUpdate(activeAccountType.attr)"
+              v-bound-phone="bindPhoneAreaVipUpdate(activeAccountType.attr)"
             >
               <span
                 >{{
@@ -155,7 +182,11 @@
             {{ accountList.length > 1 ? '到期时间:' : '您的会员到期时间:'
             }}{{ activeAccountType.vipEntTime }}
           </p>
-          <p class="self-handle-btn" @click="renewConsult">
+          <p
+            class="self-handle-btn"
+            @click="renewConsult"
+            v-bound-phone="bindPhoneRenewConsult()"
+          >
             续费咨询 <AppIcon name="youbian" size="16" color="#171826" />
           </p>
         </div>
@@ -166,7 +197,12 @@
         <span class="gift-guide-text"
           >支持送好友超级订阅,快快送给好友吧!</span
         >
-        <span class="gift-guide-btn" @click="sendToFriend">送给朋友</span>
+        <span
+          class="gift-guide-btn"
+          @click="sendToFriend"
+          v-bound-phone="bindPhoneSendToFriend()"
+          >送给朋友</span
+        >
       </div>
     </div>
     <!--    订单tab-->
@@ -321,7 +357,8 @@ export default {
         const { error_code: code, data = {} } = await getAccountInfo()
         if (code === 0) {
           Object.assign(this.pageUserInfo, data)
-          this.checkUserHasBindPhone()
+          // P698 绑定手机号ABtest需求,未绑定手机号可以访问我的页面,点击页面内的元素通过通过绑定手机号弹框引导用户绑定手机号
+          // this.checkUserHasBindPhone()
         }
         return data
       } catch (error) {
@@ -490,8 +527,6 @@ export default {
     },
     // 送好友
     sendToFriend() {
-      const nowTime = Date.now()
-      const expireTime = this.activeAccountType.vipEntTime
       if (!this.accountInfo.hasVip) {
         // 非超级订阅
         return this.$dialog
@@ -527,6 +562,116 @@ export default {
       if (data) {
         this.giftMonth = data?.gifted
       }
+    },
+    bindPhoneJumpSetting() {
+      return {
+        props: {
+          name: '我的-设置'
+        },
+        next: () => {
+          this.jumpSetting()
+        }
+      }
+    },
+    bindPhoneChangeIdentityHandle() {
+      return {
+        props: {
+          name: '我的-身份切换'
+        },
+        next: () => {
+          this.changeIdentityHandle()
+        }
+      }
+    },
+    bindPhoneJumpIdentityKeep() {
+      return {
+        props: {
+          name: '我的-身份信息维护'
+        },
+        next: () => {
+          this.jumpIdentityKeep()
+        }
+      }
+    },
+    bindPhoneChangeAccountType(temp) {
+      return {
+        props: {
+          name: '我的-切换账号类型'
+        },
+        next: () => {
+          this.changeAccountType(temp)
+        }
+      }
+    },
+    bindPhoneToBuyVip(type) {
+      return {
+        props: {
+          name: '我的-开通超级订阅'
+        },
+        next: () => {
+          this.toBuyVip(type)
+        }
+      }
+    },
+    bindPhoneRenewVip() {
+      return {
+        props: {
+          name: '我的-超级订阅续费'
+        },
+        next: () => {
+          this.renewVip()
+        }
+      }
+    },
+    bindPhoneUpdateVip(item) {
+      return {
+        props: {
+          name: '我的-超级订阅升级'
+        },
+        next: () => {
+          this.updateVip(item)
+        }
+      }
+    },
+    bindPhoneAreaVip(type) {
+      return {
+        props: {
+          name: '我的-省份订阅包续费'
+        },
+        next: () => {
+          this.areaVip(type)
+        }
+      }
+    },
+    bindPhoneAreaVipUpdate(item) {
+      return {
+        props: {
+          name: '我的-省份订阅包升级'
+        },
+        next: () => {
+          this.areaVipUpdate(item)
+        }
+      }
+    },
+    bindPhoneRenewConsult() {
+      return {
+        props: {
+          name: '我的-大会员续费咨询'
+        },
+        next: () => {
+          this.renewConsult()
+        }
+      }
+    },
+    bindPhoneSendToFriend() {
+      return {
+        props: {
+          name: '我的-送好友'
+        },
+        next: () => {
+          this.sendToFriend()
+        }
+      }
     }
   }
 }

+ 18 - 3
apps/mobile/src/components/mine/MineList.vue

@@ -6,7 +6,11 @@
       :border="false"
       v-if="mergeInfo.show"
     >
-      <van-cell :title="mergeInfo.title" @click="goLink(mergeInfo)">
+      <van-cell
+        :title="mergeInfo.title"
+        @click="goLink(mergeInfo)"
+        v-bound-phone="bindPhoneGoLink(mergeInfo)"
+      >
         <template #extra>
           <span class="to-merge">去合并</span>
         </template>
@@ -24,6 +28,7 @@
         :title="item.name"
         is-link
         @click="goLink(item)"
+        v-bound-phone="bindPhoneGoLink(item)"
         :value="item.name ? item.name.split('--')[1] : null"
       >
         <template #title>
@@ -155,6 +160,16 @@ export default {
       } else {
         openLinkOfOther(item.url)
       }
+    },
+    bindPhoneGoLink(item) {
+      return {
+        props: {
+          name: '我的-' + item.name
+        },
+        next: () => {
+          this.goLink(item)
+        }
+      }
     }
   }
 }
@@ -271,8 +286,8 @@ export default {
     position: relative;
     display: flex;
     align-items: center;
-    .left-icon{
-      font-size:22px !important;
+    .left-icon {
+      font-size: 22px !important;
     }
   }
 }

+ 29 - 5
apps/mobile/src/components/mine/OrderTabs.vue

@@ -1,18 +1,34 @@
 <template>
   <div class="order-tabs">
-    <div class="order-tab-item clickable" @click="jumpOrderPage(1)">
+    <div
+      class="order-tab-item clickable"
+      @click="jumpOrderPage(1)"
+      v-bound-phone="bindPhoneJumpOrderPage(1)"
+    >
       <AppIcon name="dingdan_daifukuan" size="36" color="33373B" />
       <div class="tab-item-text">待付款</div>
     </div>
-    <div class="order-tab-item clickable" @click="jumpOrderPage(2)">
+    <div
+      class="order-tab-item clickable"
+      @click="jumpOrderPage(2)"
+      v-bound-phone="bindPhoneJumpOrderPage(2)"
+    >
       <AppIcon name="dingdan_yiwancheng" size="36" color="33373B" />
       <div class="tab-item-text">已完成</div>
     </div>
-    <div class="order-tab-item clickable" @click="jumpOrderPage(3)">
+    <div
+      class="order-tab-item clickable"
+      @click="jumpOrderPage(3)"
+      v-bound-phone="bindPhoneJumpOrderPage(3)"
+    >
       <AppIcon name="dingdan_yiquxiao" size="36" color="33373B" />
       <div class="tab-item-text">已取消</div>
     </div>
-    <div class="order-tab-item clickable" @click="jumpOrderPage(0)">
+    <div
+      class="order-tab-item clickable"
+      @click="jumpOrderPage(0)"
+      v-bound-phone="bindPhoneJumpOrderPage(0)"
+    >
       <AppIcon name="dingdan_quanbudingdan" size="36" color="33373B" />
       <div class="tab-item-text">全部订单</div>
     </div>
@@ -33,6 +49,14 @@ export default {
       openAppOrWxPage(LINKS.我的订单页面, {
         query: { active: type }
       })
+    },
+    bindPhoneJumpOrderPage(type) {
+      return {
+        props: { name: '我的-全部订单' },
+        next: () => {
+          this.jumpOrderPage(type)
+        }
+      }
     }
   }
 }
@@ -41,7 +65,7 @@ export default {
 <style lang="scss" scoped>
 .order-tabs {
   background: #fff;
-  width:100%;
+  width: 100%;
   display: flex;
   justify-content: space-around;
   .order-tab-item {

+ 42 - 15
apps/mobile/src/components/mine/SignIn.vue

@@ -2,8 +2,15 @@
   <div class="mine-sign-in">
     <div class="continuous">
       <div class="days-row">
-        <div class="days"> 已连续签到 <em>{{ signInfo.days }} </em> 天</div>
-        <div class="points-record" @click="viewMoreRecord" v-if="showMoreRecord">
+        <div class="days">
+          已连续签到 <em>{{ signInfo.days }} </em> 天
+        </div>
+        <div
+          class="points-record"
+          @click="viewMoreRecord"
+          v-if="showMoreRecord"
+          v-bound-phone="bindPhoneViewRecord()"
+        >
           <span>查看记录</span>
           <AppIcon name="youbian" size="14" color="#C26F33" />
         </div>
@@ -12,6 +19,7 @@
         class="btn"
         :class="signInfo.state ? 'normal-btn' : 'sign-btn'"
         @click="signHandle"
+        v-bound-phone="bindPhoneSignHandle()"
         >{{ signInfo.state ? '已签到' : '立即签到' }}</span
       >
     </div>
@@ -177,6 +185,26 @@ export default {
     // 关闭签到成功弹窗
     closeDialog() {
       this.visibleDialog = false
+    },
+    bindPhoneSignHandle() {
+      return {
+        props: {
+          name: '我的-立即签到'
+        },
+        next: () => {
+          this.signHandle()
+        }
+      }
+    },
+    bindPhoneViewRecord() {
+      return {
+        props: {
+          name: '我的-查看记录'
+        },
+        next: () => {
+          this.viewMoreRecord()
+        }
+      }
     }
   }
 }
@@ -193,44 +221,44 @@ export default {
   background-size: cover;
   box-shadow: 0px 2px 8px 0px rgb(54 147 179 / 5%);
   z-index: 3;
-  border: 1px solid #F1D090;
+  border: 1px solid #f1d090;
 }
 .continuous {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  .days-row{
+  .days-row {
     display: flex;
     align-items: center;
   }
   .days {
     font-size: 12px;
-    line-height:16px;
+    line-height: 16px;
     color: #171826;
-    padding-right:8px;
+    padding-right: 8px;
     border-right: 1px solid rgba(0, 0, 0, 0.1);
     em {
-      color: #C26F33;
+      color: #c26f33;
       font-size: 14px;
     }
   }
   .points-record {
     padding-left: 8px;
     font-size: 12px;
-    line-height:16px;
+    line-height: 16px;
   }
   .btn {
     padding: 1px 6px;
     border-radius: 4px;
     font-size: 12px;
-    line-height:18px;
+    line-height: 18px;
   }
   .sign-btn {
-    background: #33323A;
-    color: #F1D090;
+    background: #33323a;
+    color: #f1d090;
   }
   .normal-btn {
-    color: #C26F33;
+    color: #c26f33;
   }
 }
 .sign-data {
@@ -254,11 +282,10 @@ export default {
     right: 0;
     bottom: 0;
     width: 45px;
-    height:45px;
+    height: 45px;
     &.animate {
       animation: turn 6s linear infinite;
-      background: url(@/assets/image/mine/dateGetClick@2x.png) center
-        no-repeat;
+      background: url(@/assets/image/mine/dateGetClick@2x.png) center no-repeat;
       background-size: cover;
     }
   }

+ 15 - 1
apps/mobile/src/components/recommend/index.vue

@@ -59,7 +59,11 @@
                 ></span>
               </div>
               <div class="j-button-group height40">
-                <button class="j-button-confirm" @click="goToSubmitSource">
+                <button
+                  class="j-button-confirm"
+                  @click="goToSubmitSource"
+                  v-bound-phone="bindPhoneGoToSubmitSource()"
+                >
                   感兴趣点我
                 </button>
               </div>
@@ -226,6 +230,16 @@ export default {
       if (index === 1) {
         this.showChartComponentKey = Date.now()
       }
+    },
+    bindPhoneGoToSubmitSource() {
+      return {
+        props: {
+          name: '超前项目推荐-感兴趣点我'
+        },
+        next: () => {
+          this.goToSubmitSource()
+        }
+      }
     }
   }
 }

+ 79 - 68
apps/mobile/src/components/search/bidding/oneKeySubscribeDialog.vue

@@ -70,7 +70,8 @@
         </div>
       </div>
       <div class="subscribe-tip">
-        前往<span @click="toSubSetting">推送设置</span>,设置订阅信息推送渠道和推送时间
+        前往<span @click="toSubSetting">推送设置</span
+        >,设置订阅信息推送渠道和推送时间
       </div>
     </div>
     <div class="j-button-group height40">
@@ -78,6 +79,7 @@
         class="j-button-confirm"
         :disabled="disabledConfirm"
         @click="subConfirmEvent"
+        v-bound-phone="bindPhoneSubConfirm()"
       >
         完成订阅,立即免费查看信息
       </van-button>
@@ -104,11 +106,22 @@
           <div class="count-row">
             <span>可选:</span>
             <strong v-if="subInfo.productType === 'member'">全国</strong>
-            <strong v-else-if="subInfo.productType === 'vip' && areaInfo.buyAreaCount === -1">全国</strong>
-            <span v-else><strong>{{ areaInfo.buyAreaCount }}</strong>个省</span>,
+            <strong
+              v-else-if="
+                subInfo.productType === 'vip' && areaInfo.buyAreaCount === -1
+              "
+              >全国</strong
+            >
+            <span v-else
+              ><strong>{{ areaInfo.buyAreaCount }}</strong
+              >个省</span
+            >,
             <span>已选:</span>
             <strong v-if="selectedCount === -1">全国</strong>
-            <span v-else><strong>{{ selectedCount }}</strong>个省</span>
+            <span v-else
+              ><strong>{{ selectedCount }}</strong
+              >个省</span
+            >
           </div>
         </div>
         <AreaCitySidebar
@@ -122,8 +135,22 @@
           @change="onAreaChanged"
           @onDisabledCityClick="onNoPower"
         />
-        <van-dialog v-model="showVipLimit" overlay-class="vip-limit-dialog-overlay" class="vip-limit-dialog" confirm-button-color="#2ABED1" confirm-button-text="去升级" title="超出可选省份数量" show-cancel-button @confirm="goUpgrade" @cancel="showVipLimit = false">
-          <div>可选:<span style="color: #2ABED1;">{{ areaInfo.buyAreaCount }}个省</span>,如需增加省份数量,您可前往升级超级订阅。</div>
+        <van-dialog
+          v-model="showVipLimit"
+          overlay-class="vip-limit-dialog-overlay"
+          class="vip-limit-dialog"
+          confirm-button-color="#2ABED1"
+          confirm-button-text="去升级"
+          title="超出可选省份数量"
+          show-cancel-button
+          @confirm="goUpgrade"
+          @cancel="showVipLimit = false"
+        >
+          <div>
+            可选:<span style="color: #2abed1"
+              >{{ areaInfo.buyAreaCount }}个省</span
+            >,如需增加省份数量,您可前往升级超级订阅。
+          </div>
         </van-dialog>
       </DropdownLayout>
     </van-popup>
@@ -150,9 +177,7 @@
             <button class="j-button-cancel" @click="areaPickerShow = false">
               取消
             </button>
-            <button class="j-button-confirm" @click="popupConfirm">
-              确定
-            </button>
+            <button class="j-button-confirm" @click="popupConfirm">确定</button>
           </div>
         </div>
       </PopupLayout>
@@ -247,14 +272,13 @@ export default {
     noLoginOrFree() {
       if (this.isLogin) {
         return this.isFree
-      }
-      else {
+      } else {
         return true
       }
     },
     selectedCount() {
       return Object.keys(this.cacheMoreFilters.area).length || -1
-    },
+    }
   },
   watch: {
     value: {
@@ -273,8 +297,7 @@ export default {
               this.areaSelected = Object.keys(freeSelectArea)[0]
               console.log('freeSelectArea', freeSelectArea, this.areaSelected)
             }
-          }
-          else {
+          } else {
             this.getVipUserInfo()
           }
           setTimeout(() => {
@@ -283,7 +306,7 @@ export default {
         }
       },
       immediate: true
-    },
+    }
   },
   created() {
     this.initArea(provinceListMapExp)
@@ -298,8 +321,7 @@ export default {
       for (const key in map) {
         if (key === '#') {
           continue
-        }
-        else {
+        } else {
           if (Array.isArray(map[key])) {
             list = list.concat(map[key])
           }
@@ -315,8 +337,7 @@ export default {
         this.subUserInfo = data
         if (productType === 'vip') {
           this.areaInfo.buyAreaCount = data?.buyset?.areacount || -1
-        }
-        else {
+        } else {
           this.areaInfo.buyAreaCount = -1
         }
       }
@@ -339,35 +360,31 @@ export default {
       // 全国(无限制)
       if (buyAreaCount < 0) {
         return true
-      }
-      else {
+      } else {
         // 非全国(省份有限制)
         if (parent.parentName && parent.parentName === '全国') {
           this.showVipLimit = true
           return false
-        }
-        else {
+        } else {
           if (this.selectedCount >= buyAreaCount) {
             // 已选择过的省份列表
             const selectedProvinceList = []
-            const sourceFirstCount
-              = this.$refs.areaSelector.sourceFirstCount || []
+            const sourceFirstCount =
+              this.$refs.areaSelector.sourceFirstCount || []
             for (const key in sourceFirstCount) {
               selectedProvinceList.push(key)
             }
             // 已经选中过的可勾选掉
             if (
-              parent.parentName
-              && selectedProvinceList.includes(parent.parentName)
+              parent.parentName &&
+              selectedProvinceList.includes(parent.parentName)
             ) {
               return true
-            }
-            else {
+            } else {
               this.showVipLimit = true
               return false
             }
-          }
-          else {
+          } else {
             return true
           }
         }
@@ -385,8 +402,7 @@ export default {
           }
           this.cacheMoreFilters.area = freeArea
         }
-      }
-      else {
+      } else {
         if (area && Object.keys(area).length) {
           this.areaSelected = this.getLastLevelNames(area)
           this.$refs.areaSelector.setState(area)
@@ -429,8 +445,8 @@ export default {
         for (const city in areaObj[province]) {
           // 如果城市有区域信息,则存储
           if (
-            Array.isArray(areaObj[province][city])
-            && areaObj[province][city].length > 0
+            Array.isArray(areaObj[province][city]) &&
+            areaObj[province][city].length > 0
           ) {
             cityDistricts[city] = areaObj[province][city]
           }
@@ -458,8 +474,7 @@ export default {
           items,
           iSwitch: 1
         }
-      }
-      else {
+      } else {
         params = {
           area: this.cacheMoreFilters.area,
           items,
@@ -470,7 +485,7 @@ export default {
     },
     // 设置关键词格式
     formatKeywords(arr) {
-      const aKeyItems = arr.map(item => ({
+      const aKeyItems = arr.map((item) => ({
         appendkey: [],
         key: [item],
         matchway: 1,
@@ -500,8 +515,7 @@ export default {
           // sessionStorage.setItem('JY-MOBILE-bidding-subscribe', JSON.stringify(this.$data))
           // location.href = link
           this.areaPickerShow = true
-        }
-        else {
+        } else {
           this.areaCascaderPickerShow = true
           setTimeout(() => {
             this.$refs.areaSelector.setState(this.cacheMoreFilters.area)
@@ -510,8 +524,7 @@ export default {
       }
     },
     onAddKey() {
-      if (!this.productSelected)
-        return
+      if (!this.productSelected) return
       this.setKeyListFun(this.productSelected)
     },
     setKeyListFun(value) {
@@ -535,13 +548,11 @@ export default {
 
           // 添加新关键词
           this.keyList = [...this.keyList, ...keysToAdd]
-        }
-        else {
+        } else {
           this.keyList.push(value)
         }
         this.productSelected = ''
-      }
-      else {
+      } else {
         if (this.keyList.length > 300) {
           this.$toast('关键词数量不能超过300个')
           this.productSelected = ''
@@ -568,16 +579,14 @@ export default {
         // 如果省下面直接是空对象,说明只有省
         if (!Object.keys(provinceInfo).length) {
           names.push(province)
-        }
-        else {
+        } else {
           // 如果省下面有市
           for (const city in provinceInfo) {
             const cityInfo = provinceInfo[city]
             // 如果市下面直接是空数组或空对象,说明只有市
-            if ((Array.isArray(cityInfo) && cityInfo.length === 0)) {
+            if (Array.isArray(cityInfo) && cityInfo.length === 0) {
               names.push(city)
-            }
-            else if (Array.isArray(cityInfo)) {
+            } else if (Array.isArray(cityInfo)) {
               // 如果市下面有区
               cityInfo.forEach((district) => {
                 if (typeof district === 'string') {
@@ -604,13 +613,11 @@ export default {
               const districtArr = data[item][city]
               if (districtArr.length) {
                 areaData = areaData.concat(districtArr)
-              }
-              else {
+              } else {
                 areaData.push(city)
               }
             })
-          }
-          else {
+          } else {
             areaData.push(item)
           }
         })
@@ -638,14 +645,12 @@ export default {
       if (action === 'confirm') {
         if (this.disabledConfirm) {
           return done(false)
-        }
-        else {
+        } else {
           // 一键订阅
           await this.oneKeySub(true)
           done()
         }
-      }
-      else {
+      } else {
         // 暂不订阅
         await this.oneKeySub(false)
         done()
@@ -655,16 +660,14 @@ export default {
     async oneKeySub(type = false) {
       const params = {}
       if (type) {
-        if (!this.keywords)
-          return this.$toast('订阅关键词不能为空')
+        if (!this.keywords) return this.$toast('订阅关键词不能为空')
         Object.assign(params, {
           isNoSubscribe: 'Y', // 一键订阅
           subsequentPrompt: this.remindChecked ? 'Y' : 'N',
           key: this.keywords.replace(/\s+/g, ''),
           area: this.areaSelected
         })
-      }
-      else {
+      } else {
         Object.assign(params, {
           isNoSubscribe: 'N', // 暂不订阅
           subsequentPrompt: this.remindChecked ? 'Y' : 'N'
@@ -683,18 +686,26 @@ export default {
             this.onClose(false)
             this.$toast('订阅关键词成功')
           }
-        }
-        else {
+        } else {
           // 一键订阅失败
           this.onClose(false)
         }
-      }
-      else {
+      } else {
         if (msg) {
           this.$toast(msg)
         }
       }
       return data.status
+    },
+    bindPhoneSubConfirm() {
+      return {
+        props: {
+          name: '完成订阅,立即免费查看信息'
+        },
+        next: () => {
+          this.subConfirmEvent()
+        }
+      }
     }
   }
 }
@@ -866,7 +877,7 @@ export default {
     }
     .van-dialog__content {
       padding: 16px;
-      color: #5F5E64;
+      color: #5f5e64;
       line-height: 22px;
       font-size: 15px;
     }

+ 11 - 0
apps/mobile/src/components/treasure-box/Recommend.vue

@@ -11,6 +11,7 @@
           :key="fIndex"
           class="box-swipe-item clickable"
           @click="open(floor)"
+          v-bound-phone="bindPhoneOpenFloor(floor)"
         >
           <van-image fit="contain" :src="floor.pic" />
           <div class="floor-text">
@@ -163,6 +164,16 @@ export default {
         // 没有被重定向。则正常走跳转流程
         return openLinkOfAd(ad)
       }
+    },
+    bindPhoneOpenFloor(floor) {
+      return {
+        props: {
+          name: '工作台-推荐-' + floor.title
+        },
+        next: () => {
+          this.open(floor)
+        }
+      }
     }
   }
 }

+ 30 - 2
apps/mobile/src/components/treasure-box/SunshineGuide.vue

@@ -28,10 +28,18 @@
               海量企业直发采购需求,供应商可直接对接采购部门
             </div>
             <div class="guide-dialog-footer">
-              <button class="dialog-button-default" @click="onKnow">
+              <button
+                class="dialog-button-default"
+                @click="onKnow"
+                v-bound-phone="bindPhoneOnKnow()"
+              >
                 我知道了
               </button>
-              <button class="dialog-button-confirm" @click="onMore">
+              <button
+                class="dialog-button-confirm"
+                @click="onMore"
+                v-bound-phone="bindPhoneOnMore()"
+              >
                 了解更多
               </button>
             </div>
@@ -68,6 +76,26 @@ export default {
     },
     onMore() {
       this.$emit('more')
+    },
+    bindPhoneOnKnow() {
+      return {
+        props: {
+          name: '工作台-阳光直采弹窗-我知道了'
+        },
+        next: () => {
+          this.onKnow()
+        }
+      }
+    },
+    bindPhoneOnMore() {
+      return {
+        props: {
+          name: '工作台-阳光直采弹窗-了解更多'
+        },
+        next: () => {
+          this.onMore()
+        }
+      }
     }
   }
 }

+ 12 - 0
apps/mobile/src/composables/quick-monitor/component/QuickMonitor.vue

@@ -20,6 +20,7 @@
         :direction="direction"
         class="action-monitor"
         @click.native.stop="changePopoverState"
+        v-bound-phone="bindPhoneMonitorClick()"
       >
         <AppIcon
           slot="icon"
@@ -283,6 +284,7 @@ export default {
       this.showPopover = f
     },
     async changePopoverState() {
+      console.log(this.model.follow, 'change')
       if (this.model.follow) {
         // 已监控
         if (this.popover) {
@@ -426,6 +428,16 @@ export default {
     },
     syncValue(v) {
       this.$emit('input', v)
+    },
+    bindPhoneMonitorClick() {
+      return {
+        props: {
+          name: '标讯详情页-底部tab监控项目'
+        },
+        next: async () => {
+          await this.changePopoverState()
+        }
+      }
     }
   }
 }

+ 2 - 1
apps/mobile/src/main.js

@@ -3,6 +3,7 @@ import Vue from 'vue'
 import dayjs from 'dayjs'
 import MetaInfo from 'vue-meta-info'
 import { Dialog, Lazyload, Toast } from 'vant'
+import TestBindPhone from '@jy/plugin-bind-phone'
 import App from './App.vue'
 import router from './router'
 import store from './store'
@@ -20,7 +21,7 @@ import { initSentry } from './sentry'
 
 dayjs.locale('zh-cn')
 
-Vue.use(Toast).use(Lazyload).use(Dialog).use(MetaInfo)
+Vue.use(Toast).use(Lazyload).use(Dialog).use(MetaInfo).use(TestBindPhone)
 
 // 设置默认 loading 配置项
 Toast.setDefaultOptions('loading', {

+ 2 - 1
apps/mobile/src/utils/format/modules/ad-formatter.js

@@ -23,6 +23,7 @@ export function adConfigFormatter(config = {}) {
       type: config?.o_extend?.linktype,
       power: oExtend?.power,
       tab: oExtend?.tab
-    }
+    },
+    script: config?.s_script ? JSON.parse(config.s_script) : ''
   }
 }

+ 18 - 10
apps/mobile/src/views/ai-search/index.vue

@@ -35,7 +35,7 @@ const rightAction = ref({
 })
 
 const canShowDoBackHome = ref(true)
-function doBackHome () {
+function doBackHome() {
   trickClick(`退出页面-首页`)
   return openAppOrWxPage(LINKS.首页, {
     query: {
@@ -47,7 +47,7 @@ function doBackHome () {
 
 function doBack() {
   trickClick(`退出页面`)
-  if (history.length === 1) {
+  if (history.length === 1 && that.$env.platform === 'wx') {
     if (needBindPhone.value) {
       checkBindPhone(true)
     } else {
@@ -597,7 +597,7 @@ function doSelectQuestionType(item) {
   })
 }
 
-function fixSwipeResize () {
+function fixSwipeResize() {
   that.$nextTick(() => {
     if (promptEle.value) {
       promptEle.value.resize()
@@ -891,7 +891,7 @@ function echoPageState() {
   }
 }
 
-function triggerFocus () {
+function triggerFocus() {
   questionInputEl.value.focus()
   that.$nextTick(() => {
     questionInputEl.value.focus()
@@ -947,7 +947,12 @@ init()
         </div>
       </div>
       <div>
-        <span v-if='canShowDoBackHome' class="back-icon" style='margin-right: 2px;padding-right: 6px;' @click='doBackHome'>
+        <span
+          v-if="canShowDoBackHome"
+          class="back-icon"
+          style="margin-right: 2px; padding-right: 6px"
+          @click="doBackHome"
+        >
           <i class="iconfont icon-nav_un_home" />
         </span>
         <Popover
@@ -959,14 +964,17 @@ init()
           @select="onSelectRightAction"
         >
           <template #reference>
-          <span class="right-icon">
-            <i class="iconfont icon-gengduo-shuxiang" />
-          </span>
+            <span class="right-icon">
+              <i class="iconfont icon-gengduo-shuxiang" />
+            </span>
           </template>
         </Popover>
-        <div class='bind-phone-popup' v-if="needBindPhone" @click="checkBindPhone"></div>
+        <div
+          class="bind-phone-popup"
+          v-if="needBindPhone"
+          @click="checkBindPhone"
+        ></div>
       </div>
-
     </div>
 
     <div class="ai-search--content j-main" ref="contentEl">

+ 11 - 0
apps/mobile/src/views/article/components/ActionCollection.vue

@@ -4,6 +4,7 @@
     :direction="direction"
     class="action-collection"
     @click.native.stop="doCollection"
+    v-bound-phone="bindPhoneCollection()"
   >
     <AppIcon
       slot="icon"
@@ -156,6 +157,16 @@ export default {
     syncValue(v) {
       this.value = v
       // this.$emit('input', v)
+    },
+    bindPhoneCollection() {
+      return {
+        props: {
+          name: '标讯详情页-收藏'
+        },
+        next: () => {
+          this.doCollection()
+        }
+      }
     }
   }
 }

+ 11 - 0
apps/mobile/src/views/article/components/ActionShareToWorkmate.vue

@@ -4,6 +4,7 @@
     :direction="direction"
     class="action-share-to-workmate"
     @click.native.stop="shareToWorkmate"
+    v-bound-phone="bindPhoneShareToWorkmate()"
   >
     <AppIcon slot="icon" name="fenxiang" color="#2ABED1" size="20"></AppIcon>
     <template #text>
@@ -97,6 +98,16 @@ export default {
       this.$nextTick(() => {
         this.showIframe = true
       })
+    },
+    bindPhoneShareToWorkmate() {
+      return {
+        props: {
+          name: '标讯详情页-转给同事'
+        },
+        next: () => {
+          this.shareToWorkmate()
+        }
+      }
     }
   }
 }

+ 35 - 3
apps/mobile/src/views/article/components/ContentMainText.vue

@@ -10,8 +10,16 @@
           src="@/assets/image/mask/bg/article-content-mask-card@2x.png"
           alt=""
         />
-        <div class="click-to-get" @click="toUnlockContent" />
-        <div class="mask-feedback" @click="feedback" />
+        <div
+          class="click-to-get"
+          @click="toUnlockContent"
+          v-bound-phone="bindPhoneToUnlockContent()"
+        />
+        <div
+          class="mask-feedback"
+          @click="feedback"
+          v-bound-phone="bindPhoneFeedback()"
+        />
       </div>
     </template>
     <template v-else>
@@ -30,7 +38,11 @@
         <div class="origin-link-container">
           <OriginLink v-if="content.originalShow" :id="content.id" />
         </div>
-        <button class="feedback-button clickable" @click="feedback">
+        <button
+          class="feedback-button clickable"
+          @click="feedback"
+          v-bound-phone="bindPhoneFeedback()"
+        >
           意见反馈
         </button>
       </section>
@@ -147,6 +159,26 @@ export default {
           fkid: this.content.id
         }
       })
+    },
+    bindPhoneFeedback() {
+      return {
+        props: {
+          name: '标讯详情页-意见反馈'
+        },
+        next: () => {
+          this.feedback()
+        }
+      }
+    },
+    bindPhoneToUnlockContent() {
+      return {
+        props: {
+          name: '标讯详情页-获取正文及联系方式'
+        },
+        next: () => {
+          this.toUnlockContent()
+        }
+      }
     }
   }
 }

+ 11 - 0
apps/mobile/src/views/article/components/DataExportBanner.vue

@@ -2,6 +2,7 @@
   <section
     class="data-export-banner-container clickable"
     @click="toDataExport"
+    v-bound-phone="bindPhoneDataExport()"
   >
     <div class="banner-left">
       <span class="j-icon icon-data-download"></span>
@@ -40,6 +41,16 @@ export default {
           adv_from: `${platform}_articel_win_sj_1`
         }
       })
+    },
+    bindPhoneDataExport() {
+      return {
+        props: {
+          name: '标讯详情页-数据导出'
+        },
+        next: () => {
+          this.toDataExport()
+        }
+      }
     }
   }
 }

+ 11 - 0
apps/mobile/src/views/article/components/DownProjectReport.vue

@@ -4,6 +4,7 @@
     :direction="direction"
     class="download-project-doc"
     @click.native.stop="doAction"
+    v-bound-phone="bindPhoneDownload()"
   >
     <AppIcon slot="icon" name="xiazaixiangmubaogao" size="20" />
     <template #text>
@@ -176,6 +177,16 @@ export default {
         await this.beforeLeavePage()
       }
       this.$router.push('/common/order/create/svip')
+    },
+    bindPhoneDownload() {
+      return {
+        props: {
+          name: '标讯详情页-下载项目报告'
+        },
+        next: () => {
+          this.doAction()
+        }
+      }
     }
   }
 }

+ 15 - 1
apps/mobile/src/views/article/components/OriginLink.vue

@@ -1,6 +1,10 @@
 <template>
   <span class="origin-link-container">
-    <span class="highlight-text origin-link clickable" @click="viewOriginLink">
+    <span
+      class="highlight-text origin-link clickable"
+      @click="viewOriginLink"
+      v-bound-phone="bindPhoneViewOrigin()"
+    >
       查看原文链接
     </span>
     <van-dialog
@@ -260,6 +264,16 @@ export default {
       const { platform } = this.$env
       const source = `${platform}_${key}`
       this.leaveSource(source, query)
+    },
+    bindPhoneViewOrigin() {
+      return {
+        props: {
+          name: '标讯详情页-查看原文链接'
+        },
+        next: () => {
+          this.viewOriginLink()
+        }
+      }
     }
   }
 }

+ 2 - 2
apps/mobile/src/views/article/components/TabActions.vue

@@ -145,8 +145,8 @@ export default {
       }
     },
     beforeShareToWorkMateAction() {
-      const { phone } = this.userSimpleInfo
-      if (phone) {
+      const { phone, expBinding } = this.userSimpleInfo
+      if (phone || expBinding === 1) {
         return true
       } else {
         this.toBindPhonePage()

+ 81 - 84
apps/mobile/src/views/article/content.vue

@@ -14,7 +14,7 @@ small-tab
           class="j-main article-content-main"
           :class="{
             'show-underline': otherModel.hasProject,
-            'no-scroll': showBindPhone,
+            'no-scroll': showBindPhone
           }"
           @click="onScrollWrapperClick"
           @scroll.passive="onScroll"
@@ -325,13 +325,13 @@ export default {
   },
   computed: {
     ...mapState({
-      preAgentInfo: state => state.article.preAgentInfo,
-      content: state => state.article.mainModel.content,
-      summary: state => state.article.mainModel.summary,
-      mainModel: state => state.article.mainModel,
-      expandModel: state => state.article.expandModel,
-      otherModel: state => state.article.otherModel,
-      bindPhone: state => state.article.bindPhone
+      preAgentInfo: (state) => state.article.preAgentInfo,
+      content: (state) => state.article.mainModel.content,
+      summary: (state) => state.article.mainModel.summary,
+      mainModel: (state) => state.article.mainModel,
+      expandModel: (state) => state.article.expandModel,
+      otherModel: (state) => state.article.otherModel,
+      bindPhone: (state) => state.article.bindPhone
     }),
     IsCustomTopNet() {
       return this.content.IsCustomTopNet || false
@@ -340,7 +340,9 @@ export default {
       return this.content.yyszbContent || location.pathname.includes('/yyszb')
     },
     toBCustom() {
-      return this.IsCustomTopNet || this.yyszbContent
+      return (
+        this.IsCustomTopNet || this.yyszbContent || this.IsSunPublishContent
+      )
     },
     showMonitor() {
       return !this.yyszbContent
@@ -373,8 +375,7 @@ export default {
           show: this.content.isCanRead, // 是否展示全部内容
           showContentModule: this.hasPowerToReadSunPublishContent
         }
-      }
-      else {
+      } else {
         return {
           show: this.content.isCanRead,
           showContentModule: true
@@ -400,10 +401,10 @@ export default {
       return this.mainModel.moduleShow
     },
     advancedModuleShow() {
-      const { tbService, customerRecommend, timeline, zbRecommend }
-        = this.expandModel.moduleShow
-      const toBCustom
-        = this.IsCustomTopNet || this.IsSunPublishContent || this.yyszbContent
+      const { tbService, customerRecommend, timeline, zbRecommend } =
+        this.expandModel.moduleShow
+      const toBCustom =
+        this.IsCustomTopNet || this.IsSunPublishContent || this.yyszbContent
       return Object.assign({}, this.expandModel.moduleShow, {
         tbService: toBCustom ? false : tbService,
         customerRecommend: toBCustom ? false : customerRecommend,
@@ -422,8 +423,7 @@ export default {
       // 如果是有权限用户(权限7),则有数据展示,无数据不展示。无权限7用户则一直展示,展示广告引导
       if (this.hasCustomerModulePower) {
         return this.advancedModuleShow.customerRecommend
-      }
-      else {
+      } else {
         return true
       }
     },
@@ -491,31 +491,26 @@ export default {
           }
           this.checkNpsView()
         })
-      }
-      else {
+      } else {
         try {
           await this.getPreAgentInfo()
           const { data, msg } = await this.getBaseInfo()
           if (data) {
             this.finishLoading()
-          }
-          else {
+          } else {
             this.finishLoading()
             this.isError = true
             // 无效参数 = d解析错误
             // 未查到当前招标信息 = 文章不存在
             if (msg.includes('未查到当前招标信息')) {
               this.errorText = '由于相关部门要求,该信息已下架,敬请原谅'
-            }
-            else {
+            } else {
               this.errorText = '该页面信息不存在'
             }
           }
-        }
-        catch (error) {
+        } catch (error) {
           console.error(error)
-        }
-        finally {
+        } finally {
           // console.log('基础接口请求完成')
         }
         if (this.canReadConf.show) {
@@ -539,8 +534,7 @@ export default {
       let subType = ''
       try {
         subType = this.content?._ob?.subType
-      }
-      catch (error) {
+      } catch (error) {
         console.log(error)
       }
       let prefix = ''
@@ -550,8 +544,7 @@ export default {
       const newTitle = prefix + title
       if (otherTitle) {
         this.postMessageToMiniProgram(otherTitle)
-      }
-      else {
+      } else {
         this.postMessageToMiniProgram(newTitle)
       }
     },
@@ -565,8 +558,7 @@ export default {
             title
           }
         })
-      }
-      catch (e) {
+      } catch (e) {
         console.log(e)
       }
     },
@@ -578,8 +570,7 @@ export default {
     },
     appHeaderActions() {
       const { $envs } = this
-      if (!$envs.inApp)
-        return
+      if (!$envs.inApp) return
       if (this.yyszbContent) {
         return
       }
@@ -607,11 +598,9 @@ export default {
       // project-name事件委托
       if (checkUnderline.status) {
         this.clickKeywordUnderline(e)
-      }
-      else if (e.target.classList.contains('free-view')) {
+      } else if (e.target.classList.contains('free-view')) {
         this.clickFreeView(e)
-      }
-      else if (e.target.className.includes('third-party-popover')) {
+      } else if (e.target.className.includes('third-party-popover')) {
         this.popup.thirdPartyVerify = true
       }
     },
@@ -620,9 +609,16 @@ export default {
       const checkProjectName = checkAncestorClass(e, 'project-name', 3)
       const checkWinnerName = checkAncestorClass(e, 'winner-name', 3)
       if (checkProjectName.status) {
-        this.goMemberFollowPage()
-      }
-      else if (checkWinnerName.status) {
+        // 标题跳转添加ABTest手机号绑定弹框
+        this.$testBindPhone({
+          props: {
+            name: '标讯详情页-标题'
+          },
+          next: () => {
+            this.goMemberFollowPage()
+          }
+        })
+      } else if (checkWinnerName.status) {
         this.goToEntPortraitPage(checkWinnerName.target)
       }
     },
@@ -642,8 +638,7 @@ export default {
             industry: _ob ? _ob.buyerClass : undefined
           }
         })
-      }
-      else {
+      } else {
         openAppOrWxPage(LINKS.项目详情页)
       }
     },
@@ -654,12 +649,11 @@ export default {
         ?.replace(/\?/g, '%3F')
       if (!id) {
         return this.$toast('暂无数据')
-      }
-      else {
-        const noPower
-          = !this.bigMemberPower.includes(4)
-            && !this.bigMemberPower.includes(12)
-            && !this.bigMemberPower.includes(13)
+      } else {
+        const noPower =
+          !this.bigMemberPower.includes(4) &&
+          !this.bigMemberPower.includes(12) &&
+          !this.bigMemberPower.includes(13)
         if (this.isMember && noPower) {
           if (eName) {
             this.beforeLeavePage()
@@ -668,12 +662,10 @@ export default {
               app: LINKS.旧版企业信息前缀.app + eName,
               h5: LINKS.旧版企业信息前缀.h5 + eName
             })
-          }
-          else {
+          } else {
             return this.$toast('暂无数据')
           }
-        }
-        else {
+        } else {
           this.beforeLeavePage()
           sessionStorage.removeItem('$data-ent_portrait')
           openAppOrWxPage(LINKS.企业画像页面, {
@@ -685,15 +677,22 @@ export default {
       }
     },
     clickFreeView() {
-      this.beforeLeavePage()
-      let source = 'peugeot_view_infor'
-      if (this.toBCustom) {
-        source = `${this.$env.platform}_sunlight_viewdetails`
-      }
-      openAppOrWxPage(LINKS.留资, {
-        query: {
-          source,
-          infoid: this.content.id
+      this.$testBindPhone({
+        props: {
+          name: '标讯详情页-采购联系人/联系电话'
+        },
+        next: () => {
+          this.beforeLeavePage()
+          let source = 'peugeot_view_infor'
+          if (this.toBCustom) {
+            source = `${this.$env.platform}_sunlight_viewdetails`
+          }
+          openAppOrWxPage(LINKS.留资, {
+            query: {
+              source,
+              infoid: this.content.id
+            }
+          })
         }
       })
     },
@@ -703,7 +702,7 @@ export default {
       return true
     },
     clickRight() {
-      this.doShare()
+      this.checkTestBindPhone('标讯详情页-分享有礼', this.doShare)
     },
     doShare() {
       this.shareShow = true
@@ -712,14 +711,12 @@ export default {
       if (this.$envs.inWX) {
         try {
           await this.getShareInfoReq()
-        }
-        catch (error) {
+        } catch (error) {
           console.log(error)
         }
         await this.calcWxShareInfo()
         this.initShareMixin()
-      }
-      else {
+      } else {
         this.getShareInfoReq()
       }
     },
@@ -756,8 +753,7 @@ export default {
       if (window.location.href.includes('open_infocontent')) {
         const query = window.location.search.slice(1)
         link += `${query}`
-      }
-      else {
+      } else {
         link += '&source=wx_infocontentshare'
       }
 
@@ -774,22 +770,19 @@ export default {
             subhref = `${add1}/content/${encryptid}${add2}`
             this.wxShareCache.subhref = subhref
           }
-        }
-        catch (error) {
+        } catch (error) {
           console.warn(error)
         }
       }
 
       if (encryptid) {
         link = subhref
-      }
-      else {
+      } else {
         link = window.location.href
       }
       if (!link.includes('?')) {
         link += '?'
-      }
-      else {
+      } else {
         link += '&'
       }
       if (!encryptid) {
@@ -834,14 +827,12 @@ export default {
           link += `${location.search}&source=app_infocontentshare&from=${
             this.shareInfoRes.userId || ''
           }`
-        }
-        else {
+        } else {
           link += `?source=app_infocontentshare&from=${
             this.shareInfoRes.userId || ''
           }`
         }
-      }
-      else if (shareType === 3) {
+      } else if (shareType === 3) {
         // 分享到朋友圈
         link += '&qrcodeType=app_infocontent_timeline_z'
         shareTitle = this.getRandomShareText()
@@ -853,8 +844,7 @@ export default {
       // this.shareConf.link = link
       if (this.$envs.inApp && shareToWx) {
         this.refreshShareLink(this.domainConf.wx)
-      }
-      else {
+      } else {
         this.refreshShareLink()
       }
     },
@@ -877,14 +867,21 @@ export default {
     },
     checkNpsView() {
       const targetVm = this.$refs.npsCard
-      if (!targetVm)
-        return
+      if (!targetVm) return
       const target = targetVm.$el
       const scrollWrapper = this.$refs.scrollWrapper
       const visible = isElementInScrollArea(target, scrollWrapper)
       if (visible) {
         targetVm.getIsView()
       }
+    },
+    checkTestBindPhone(name, next) {
+      this.$testBindPhone({
+        props: {
+          name: name
+        },
+        next: next
+      })
     }
   }
 }

+ 11 - 0
apps/mobile/src/views/article/ui/MaskCard.vue

@@ -33,6 +33,7 @@
               class="mask-footer-btn"
               size="small"
               @click="onFooterBtnClick"
+              v-bound-phone="bindPhoneFooterBtnClick()"
             >
               {{ footerButtonText }}
             </van-button>
@@ -94,6 +95,16 @@ export default {
     },
     onFooterBtnClick() {
       this.$emit('footerBtnClick')
+    },
+    bindPhoneFooterBtnClick() {
+      return {
+        props: {
+          name: '标讯详情页-客户推荐-免费体验'
+        },
+        next: () => {
+          this.onFooterBtnClick()
+        }
+      }
     }
   }
 }

+ 24 - 0
apps/mobile/src/views/article/ui/ServiceIntroCard.vue

@@ -28,6 +28,7 @@
             type="primary"
             plain
             @click="plainButtonClick"
+            v-bound-phone="bindPhonePlainButtonClick(plainButtonText)"
             v-if="plainButtonShow"
           >
             {{ plainButtonText }}
@@ -36,6 +37,7 @@
             class="action-button"
             type="primary"
             @click="confirmButtonClick"
+            v-bound-phone="bindPhoneConfirmButtonClick(confirmButtonText)"
             v-if="confirmButtonShow"
           >
             {{ confirmButtonText }}
@@ -97,6 +99,28 @@ export default {
     },
     confirmButtonClick() {
       this.$emit('confirmButtonClick')
+    },
+    bindPhonePlainButtonClick(text) {
+      const pass = text === '了解详情' ? this.plainButtonClick : null
+      return {
+        props: {
+          name: '标讯详情页-投标服务-' + text
+        },
+        pass: pass,
+        next: () => {
+          this.plainButtonClick()
+        }
+      }
+    },
+    bindPhoneConfirmButtonClick(text) {
+      return {
+        props: {
+          name: '标讯详情页-投标服务-' + text
+        },
+        next: () => {
+          this.confirmButtonClick()
+        }
+      }
     }
   }
 }

+ 35 - 0
apps/mobile/src/views/dataSmt/detail.vue

@@ -5,6 +5,7 @@
         <cornerMark :text="info.data_type" :color-number="cornerBg" />
         <p
           class="label-info"
+          v-if="info.name"
           v-html="replaceKeyword(info.name, searchKeyword)"
         ></p>
         <div class="row-tags">
@@ -68,6 +69,7 @@
           以下数据字段仅为部分字段,如需更多行业字段,如资金来源、报价方式、企业资质、人员资质、业绩资格、企业信用、建设规模等字段,您可<span
             class="click_link"
             @click="reservedHandle('tips', info.name)"
+            v-bound-phone="bindPhoneReserved('tips', info.name)"
             >申请数据定制></span
           >
         </div>
@@ -94,6 +96,7 @@
           class="data_app"
           v-if="info.field_illustrate && info.field_illustrate.application_app"
           @click="reservedHandle('datauser', info.name)"
+          v-bound-phone="bindPhoneReserved('datauser', info.name)"
         >
           <img :src="info.field_illustrate.application_app" alt="" />
         </div>
@@ -110,12 +113,14 @@
       <div
         class="confirm-btn default-btn clickable"
         @click="reservedHandle('buy', info.name)"
+        v-bound-phone="bindPhoneReserved('buy', info.name)"
       >
         购买咨询
       </div>
       <div
         class="confirm-btn clickable"
         @click="reservedHandle('data', info.name)"
+        v-bound-phone="bindPhoneReserved('data', info.name)"
       >
         申请数据定制
       </div>
@@ -276,6 +281,36 @@ export default {
         }
         openLinkOfOther(openLink)
       }
+    },
+    bindPhoneReserved(type, interest) {
+      const pass = !this.isLogin
+        ? function () {
+            openLinkOfOther(LINKS.APP登录页.app, {
+              query: {
+                to: 'back'
+              }
+            })
+          }
+        : null
+      let name = ''
+      if (type === 'tips') {
+        name = '数据超市详情页-正文申请数据定制'
+      } else if (type === 'datauser') {
+        name = '数据超市详情页-数据应用'
+      } else if (type === 'buy') {
+        name = '数据超市详情页-购买咨询'
+      } else if (type === 'data') {
+        name = '数据超市详情页-申请数据定制'
+      }
+      return {
+        props: {
+          name: name
+        },
+        pass: pass,
+        next: () => {
+          this.reservedHandle(type, interest)
+        }
+      }
     }
   }
 }

+ 16 - 1
apps/mobile/src/views/dataSmt/index.vue

@@ -115,7 +115,11 @@
       </div>
     </div>
     <div class="j-footer">
-      <div class="confirm-btn clickable" @click="reservedHandle">
+      <div
+        class="confirm-btn clickable"
+        @click="reservedHandle"
+        v-bound-phone="bindPhoneReserved()"
+      >
         申请数据定制
       </div>
     </div>
@@ -169,6 +173,7 @@
           以下数据字段仅为部分字段,如需更多行业字段,如资金来源、报价方式、企业资质、人员资质、业绩资格、企业信用、建设规模等字段,您可<span
             class="click_link"
             @click="reservedHandle('tips')"
+            v-bound-phone="bindPhoneReserved('tips')"
             >申请数据定制></span
           >
         </div>
@@ -416,6 +421,16 @@ export default {
           break
       }
       return bg
+    },
+    bindPhoneReserved(type) {
+      return {
+        props: {
+          name: '数据超市-申请数据定制'
+        },
+        next: () => {
+          this.reservedHandle(type)
+        }
+      }
     }
   }
 }

+ 15 - 1
apps/mobile/src/views/identity/components/CheckVipExpire.vue

@@ -18,7 +18,11 @@
           <p>为避免遗漏重大项目,请及时续费</p>
           <p class="activity-tip" v-if="activityText">{{ activityText }}</p>
         </div>
-        <button class="confirm-button clickable" @click="rechargeNow">
+        <button
+          class="confirm-button clickable"
+          @click="rechargeNow"
+          v-bound-phone="bindPhoneRechargeNow()"
+        >
           立即续费
         </button>
       </div>
@@ -184,6 +188,16 @@ export default {
           type: 'renew'
         }
       })
+    },
+    bindPhoneRechargeNow() {
+      return {
+        props: {
+          name: '我的-立即续费'
+        },
+        next: () => {
+          this.rechargeNow()
+        }
+      }
     }
   }
 }

+ 45 - 2
apps/mobile/src/views/landing/consultingService.vue

@@ -42,6 +42,7 @@
                   <div
                     class="child-box-btn"
                     @click.stop="goSource('free', e.name)"
+                    v-bound-phone="bindPhoneGoSource('free', e.name)"
                   >
                     <!-- 获取报告样例 -->
                   </div>
@@ -209,8 +210,20 @@
       </div>
     </div>
     <div class="cs-footer">
-      <div class="left-btn" @click="call_Phone">咨询客服</div>
-      <div class="right-btn" @click="goSource('more')">获取更多专属服务</div>
+      <div
+        class="left-btn"
+        @click="call_Phone"
+        v-bound-phone="bindPhoneConsult()"
+      >
+        咨询客服
+      </div>
+      <div
+        class="right-btn"
+        @click="goSource('more')"
+        v-bound-phone="bindPhoneGetMore()"
+      >
+        获取更多专属服务
+      </div>
     </div>
   </div>
 </template>
@@ -460,6 +473,36 @@ export default {
       }
 
       return result
+    },
+    bindPhoneConsult() {
+      return {
+        props: {
+          source: '咨询服务-咨询客服'
+        },
+        next: () => {
+          this.call_Phone()
+        }
+      }
+    },
+    bindPhoneGetMore() {
+      return {
+        props: {
+          name: '咨询服务-获取更多专属服务'
+        },
+        next: () => {
+          this.goSource('more')
+        }
+      }
+    },
+    bindPhoneGoSource(free, name) {
+      return {
+        props: {
+          name: '咨询服务-获取报告样例'
+        },
+        next: () => {
+          this.goSource(free, name)
+        }
+      }
     }
   }
 }

+ 193 - 69
apps/mobile/src/views/search/result/bidding/index.vue

@@ -80,7 +80,12 @@
           <span v-if="!isFree" class="clear-action" @click="onClearFilter"
             >清空</span
           >
-          <span class="save-action" @click="saveFilterToHistory">保存</span>
+          <span
+            class="save-action"
+            @click.stop="saveFilterToHistory"
+            v-bound-phone="bindPhoneSaveFilter()"
+            >保存</span
+          >
         </div>
       </div>
       <div v-show="interceptTipText" class="intercept-tip-container">
@@ -104,11 +109,13 @@
                 >条信息
               </template>
             </p>
+            <!-- @click="dataExport" -->
             <div
               v-show="!inInjectBI"
               v-if="listState.total > 0"
               class="data-export clickable"
               @click="dataExport"
+              v-bound-phone="bindPhoneDataExport()"
             >
               <AppIcon name="shujudaochu_xiao1" />
               <span class="text">导出</span>
@@ -184,9 +191,16 @@
         />
       </div>
       <!-- P640项目名称/标的物引流广告模块-->
-      <div  class="project-bdw-ad-container" :style="{opacity: pageState.bdwCardLoaded ? 'unset' : '0'}" v-show="pageState.projectBdwInfo.sCount > 0">
+      <div
+        class="project-bdw-ad-container"
+        :style="{ opacity: pageState.bdwCardLoaded ? 'unset' : '0' }"
+        v-show="pageState.projectBdwInfo.sCount > 0"
+      >
         <div class="project-bdw-ad">
-          <div class="top-tip">信息不够精准?根据“项目名称/标的物”,为您精准匹配到 <strong>{{pageState.projectBdwInfo.sCount}}条</strong> 信息</div>
+          <div class="top-tip">
+            信息不够精准?根据“项目名称/标的物”,为您精准匹配到
+            <strong>{{ pageState.projectBdwInfo.sCount }}条</strong> 信息
+          </div>
           <ProjectCell
             v-visited:content="pageState.projectBdwInfo.sInfo.id"
             class="list-item"
@@ -195,47 +209,61 @@
             :time-fmt="getTimeFmt"
             :detail-list="pageState.projectBdwInfo.sInfo.detailList"
             :title="pageState.projectBdwInfo.sInfo.title"
-            :detail="filters.scope.includes('content') ? pageState.projectBdwInfo.sInfo.detail : null"
+            :detail="
+              filters.scope.includes('content')
+                ? pageState.projectBdwInfo.sInfo.detail
+                : null
+            "
             :filetext_search="pageState.projectBdwInfo.sInfo.filetext_search"
             :fs_keys="pageState.projectBdwInfo.sInfo.fs_word"
             :time="pageState.projectBdwInfo.sInfo.dateTime"
             :is-file="pageState.projectBdwInfo.sInfo.isFile"
             :keys="pageState.splitKeys"
-            :left-top-badge-text="pageState.projectBdwInfo.sInfo.leftTopBadgeText"
+            :left-top-badge-text="
+              pageState.projectBdwInfo.sInfo.leftTopBadgeText
+            "
             :tags="pageState.projectBdwInfo.sInfo.tagList"
             @click="goToDetail(pageState.projectBdwInfo.sInfo)"
           >
             <template #buyerText="{ item }">
-                <span
-                  class="buyer-item link-clickable"
-                  @click.stop="toToBuyerProfile(item.text)"
+              <span
+                class="buyer-item link-clickable"
+                @click.stop="toToBuyerProfile(item.text)"
                 >{{ item.text }}</span
-                >
+              >
             </template>
             <template #winnerText="{ item }">
-                <span
-                  v-for="(winner, index) in item.children"
-                  :key="index"
-                  class="winner-item highlight-text link-clickable j-splitter"
-                  data-j-splitter="、"
-                  @click.stop="toToEntProfile(winner.id)"
+              <span
+                v-for="(winner, index) in item.children"
+                :key="index"
+                class="winner-item highlight-text link-clickable j-splitter"
+                data-j-splitter="、"
+                @click.stop="toToEntProfile(winner.id)"
                 >{{ winner.text }}</span
-                >
+              >
             </template>
             <template slot="icon">
-              <div  @click.stop="doBdwCollection(pageState.projectBdwInfo.sInfo, )">
-                  <span
-                    class="j-icon"
-                    :class="{
-                      'icon-star-fill': pageState.projectBdwInfo.sInfo.star,
-                      'icon-star-streak': !pageState.projectBdwInfo.sInfo.star
-                    }"
-                  />
-                <span>&nbsp;{{ pageState.projectBdwInfo.sInfo.star ? '已收藏' : '收藏' }}</span>
+              <div
+                @click.stop="doBdwCollection(pageState.projectBdwInfo.sInfo)"
+              >
+                <span
+                  class="j-icon"
+                  :class="{
+                    'icon-star-fill': pageState.projectBdwInfo.sInfo.star,
+                    'icon-star-streak': !pageState.projectBdwInfo.sInfo.star
+                  }"
+                />
+                <span
+                  >&nbsp;{{
+                    pageState.projectBdwInfo.sInfo.star ? '已收藏' : '收藏'
+                  }}</span
+                >
               </div>
             </template>
           </ProjectCell>
-          <div class="follow-text" @click="openSvip">开通超级订阅,立享更多搜索权限,寻找商机更精准 &gt;</div>
+          <div class="follow-text" @click="openSvip">
+            开通超级订阅,立享更多搜索权限,寻找商机更精准 &gt;
+          </div>
         </div>
       </div>
       <van-list
@@ -329,7 +357,11 @@
                 >
                   {{ computedHasAddInfoStatus(item) ? '已添加' : '添加' }}
                 </button>
-                <div v-else @click.stop="doCollection(item, index)">
+                <div
+                  v-else
+                  @click.stop="doCollection(item, index)"
+                  v-bound-phone="bindPhoneDoCollection(item, index)"
+                >
                   <span
                     class="j-icon"
                     :class="{
@@ -367,6 +399,7 @@
               <span
                 >点击<i
                   class="highlight-text"
+                  v-bound-phone="bindPhoneToLeaveInfoPage('jylab_see500_plus')"
                   @click="toLeaveInfoPage('jylab_see500_plus')"
                   >免费查看更多信息</i
                 ></span
@@ -513,7 +546,11 @@
           @click="pageState.toSubThisKeyNoticeBarShow = false"
         />
         <div class="center text">如果您对以上结果满意可直接订阅</div>
-        <div class="right action clickable" @click="toSubThisKey">
+        <div
+          class="right action clickable"
+          @click.stop="toSubThisKey"
+          v-bound-phone="bindPhoneToSubThisKey()"
+        >
           去订阅
           <AppIcon name="youbian" color="#2ABED1" />
         </div>
@@ -1101,14 +1138,15 @@ export default {
       this.moveRecommendCard(n)
       // 项目名称/标的物引流广告插入到第五条公告后边
       console.log(666666666)
-      const insertNum = this.listState.list.length > 4 ? 4 : this.listState.list.length
+      const insertNum =
+        this.listState.list.length > 4 ? 4 : this.listState.list.length
       this.insertProjectBdwAd(insertNum)
     },
     searchList(list) {
       if (list && list.length && this.pageState.listTabActive === 'list') {
         // 市场分析报告插到第一条数据后面位置
         this.moveRecommendCard('list')
-       // 项目名称/标的物引流广告插入到第五条公告后边
+        // 项目名称/标的物引流广告插入到第五条公告后边
         console.log(777777)
         const insertNum = list.length > 4 ? 4 : list.length
         this.insertProjectBdwAd(insertNum)
@@ -1399,7 +1437,15 @@ export default {
             }
           })
         }
-        this.$router.push('/common/order/create/svip')
+        // this.$router.push('/common/order/create/svip')
+        this.$testBindPhone({
+          props: {
+            name: '标讯搜索结果页-筛选条件-去开通超级订阅'
+          },
+          next: () => {
+            this.$router.push('/common/order/create/svip')
+          }
+        })
       } catch (error) {}
     },
     async getBidColPower() {
@@ -1695,7 +1741,7 @@ export default {
       if (t.pageNum === 1) {
         this.saveSearchGroupToLocal()
       }
-      this.pageState.projectBdwInfo.sCount =  0
+      this.pageState.projectBdwInfo.sCount = 0
       this.pageState.projectBdwInfo.sInfo = {}
       this.pageState.projectBdwInfo.bdwCardLoaded = false
       try {
@@ -1706,7 +1752,8 @@ export default {
         } = await getBiddingSearchList(params, this.restfulApiUserTypeDefault)
         // P640处理标的物广告位信息
         this.pageState.projectBdwInfo.sCount = data.sCount || 0
-        this.pageState.projectBdwInfo.sInfo = data.sList && data.sCount > 0 ? data.sList[0]: {}
+        this.pageState.projectBdwInfo.sInfo =
+          data.sList && data.sCount > 0 ? data.sList[0] : {}
         // 计算项目名称标的物广告信息数据
         this.calcProjectBdwTag()
 
@@ -1733,8 +1780,11 @@ export default {
           this.afterSearch(params, data)
           // 列表赋值
           // 如果筛选范围同时包含标题和正文,那么按照正文包括高亮在前,标题在后
-          const resDataList = data.list && data.list.length > 0 ? JSON.parse(JSON.stringify(data.list)) : []
-          const list =this.sortListTitleDetail(resDataList)
+          const resDataList =
+            data.list && data.list.length > 0
+              ? JSON.parse(JSON.stringify(data.list))
+              : []
+          const list = this.sortListTitleDetail(resDataList)
           const count = data.count
           const total = data.total
           if (Array.isArray(list)) {
@@ -1858,19 +1908,24 @@ export default {
     },
     // p640需求,如果筛选类型同时包含标题和正文,先展示标题包含关键词,再展示内容包含关键词的数据
     // 如果不同时包含标题和正文,则不处理
-    sortListTitleDetail (baseArr) {
+    sortListTitleDetail(baseArr) {
       const _this = this
-      if( this.filters.scope.includes('title') &&  this.filters.scope.includes('content')) {
+      if (
+        this.filters.scope.includes('title') &&
+        this.filters.scope.includes('content')
+      ) {
         // 辅助函数:检查字符串是否包含关键词数组中的任何一个关键词
         function containsKeyword(str, keywords) {
-          return keywords.some(keyword => str ? str.includes(keyword) : '');
+          return keywords.some((keyword) => (str ? str.includes(keyword) : ''))
         }
         // 先按照发布时间倒序分组排序
         let arr = []
         const baseTimeMap = {}
         const baseTimeKeys = []
-        baseArr.forEach(v => {
-          const ymd = dayjs(v.publishTime * 1000).startOf('day').unix()
+        baseArr.forEach((v) => {
+          const ymd = dayjs(v.publishTime * 1000)
+            .startOf('day')
+            .unix()
           if (!Array.isArray(baseTimeMap[ymd])) {
             baseTimeMap[ymd] = []
           }
@@ -1880,13 +1935,23 @@ export default {
           }
         })
 
-        const sortBaseTimeKeys = baseTimeKeys.sort((a,b) => b - a)
+        const sortBaseTimeKeys = baseTimeKeys.sort((a, b) => b - a)
         // 再按照标题正文排序
-        function sortItem (arr) {
+        function sortItem(arr) {
           const matchKeys = _this.pageState.splitKeys
-          const titleMatches = arr.filter(item => containsKeyword(item.title, matchKeys))
-          const contentMatches = arr.filter(item => _this.setDetailText(item.detail, matchKeys) && !titleMatches.some(tm => tm.title === item.title))
-          const noMatches = arr.filter(item => !containsKeyword(item.title, matchKeys) && !_this.setDetailText(item.detail, matchKeys));
+          const titleMatches = arr.filter((item) =>
+            containsKeyword(item.title, matchKeys)
+          )
+          const contentMatches = arr.filter(
+            (item) =>
+              _this.setDetailText(item.detail, matchKeys) &&
+              !titleMatches.some((tm) => tm.title === item.title)
+          )
+          const noMatches = arr.filter(
+            (item) =>
+              !containsKeyword(item.title, matchKeys) &&
+              !_this.setDetailText(item.detail, matchKeys)
+          )
           // 合并结果
           const resultArr = [...titleMatches, ...contentMatches, ...noMatches]
           return resultArr
@@ -2287,8 +2352,7 @@ export default {
       this.$keep.action({
         status: item.star,
         id: item.id,
-        beforeRedirect: () => {
-        },
+        beforeRedirect: () => {},
         complete: ({ type, message }) => {
           if (type) {
             item.star = !item.star
@@ -2721,7 +2785,7 @@ export default {
           if (this.topSearch.input) {
             return v && v.text
           } else {
-            return  v.text
+            return v.text
           }
         })
         .map((item) => item.text)
@@ -3109,7 +3173,9 @@ export default {
         // 市场分析报告插到第一条数据后面位置
         this.$nextTick(() => {
           const recommend = document.querySelector('.recommend-card-container')
-          const cellList = document.querySelectorAll('.more-list .list-wrapper>.project-cell')
+          const cellList = document.querySelectorAll(
+            '.more-list .list-wrapper>.project-cell'
+          )
           const recommendChild = document.querySelector('.recommend-bg')
           if (recommend && cellList[0]) {
             const parentNode = cellList[0].parentNode
@@ -3139,7 +3205,7 @@ export default {
       this.preSortItem(this.pageState.projectBdwInfo.sInfo)
     },
     // 开通超级订阅
-    openSvip () {
+    openSvip() {
       if (!this.isLogin) {
         return openLinkOfOther(LINKS.APP登录页.app, {
           query: {
@@ -3147,7 +3213,65 @@ export default {
           }
         })
       }
-      this.$router.push('/common/order/create/svip')
+      this.$testBindPhone({
+        props: {
+          name: '标讯搜索结果页-开通超级订阅'
+        },
+        next: () => {
+          this.$router.push('/common/order/create/svip')
+        }
+      })
+      // this.$router.push('/common/order/create/svip')
+    },
+    bindPhoneDataExport() {
+      return {
+        props: {
+          name: '标讯搜索结果页-导出'
+        },
+        next: () => {
+          this.dataExport()
+        }
+      }
+    },
+    bindPhoneSaveFilter() {
+      return {
+        props: {
+          name: '标讯搜索结果页-保存筛选'
+        },
+        next: () => {
+          this.saveFilterToHistory()
+        }
+      }
+    },
+    bindPhoneToSubThisKey() {
+      return {
+        props: {
+          name: '标讯搜索结果页-去订阅'
+        },
+        next: () => {
+          this.toSubThisKey()
+        }
+      }
+    },
+    bindPhoneToLeaveInfoPage(source) {
+      return {
+        props: {
+          name: '标讯搜索结果页-免费查看更多'
+        },
+        next: () => {
+          this.toLeaveInfoPage(source)
+        }
+      }
+    },
+    bindPhoneDoCollection(item, index) {
+      return {
+        props: {
+          name: '标讯搜索结果页-收藏'
+        },
+        next: () => {
+          this.doCollection(item, index)
+        }
+      }
     }
   }
 }
@@ -3623,37 +3747,37 @@ export default {
 .customer-corner {
   z-index: 10;
 }
-.project-bdw-ad-container{
+.project-bdw-ad-container {
   border-radius: 12px;
   background: linear-gradient(180deg, #fff4e8 0%, #fff 100%);
-  padding:12px;
-  margin:8px;
+  padding: 12px;
+  margin: 8px;
   .top-tip {
-    font-weight:bold;
-    font-size:13px;
-    line-height:20px;
+    font-weight: bold;
+    font-size: 13px;
+    line-height: 20px;
     strong {
-      color:#FF9F40;
-      margin:0 2px;
+      color: #ff9f40;
+      margin: 0 2px;
     }
   }
-  .follow-text{
-    background: #FF9F40;
+  .follow-text {
+    background: #ff9f40;
     color: #fff;
     border-radius: 4px;
     padding: 2px 11px;
-    font-size:12px;
-    line-height:20px;
-    margin-top:10px;
+    font-size: 12px;
+    line-height: 20px;
+    margin-top: 10px;
     cursor: pointer;
   }
-  ::v-deep{
-    .project-cell{
-      padding:0;
-      margin:0;
+  ::v-deep {
+    .project-cell {
+      padding: 0;
+      margin: 0;
       background: unset;
     }
-    .jy-hairline--bottom::after{
+    .jy-hairline--bottom::after {
       border: none;
     }
   }

+ 13 - 3
apps/mobile/src/views/search/result/buyer/index.vue

@@ -91,7 +91,7 @@
           checkedFollowText="已监控"
           followText="监控"
           @cellClick="onClickList(item)"
-          @action-left="onClickFollow(item, index)"
+          @action-left="BindPhoneOnClickFollow(item, index)"
           @action-right="onClickClaim(item, index)"
         >
           <template v-slot:custom-icon="option">
@@ -526,6 +526,16 @@ export default {
       }
       return temp
     },
+    BindPhoneOnClickFollow(item, index) {
+      this.$testBindPhone({
+        props: {
+          name: '采购单位搜索-监控'
+        },
+        next: () => {
+          this.onClickFollow(item, index)
+        }
+      })
+    },
     // 监控
     onClickFollow(item, index) {
       console.log(item)
@@ -726,11 +736,11 @@ export default {
       }
     },
     onClickList(item) {
-      if(this.isLogin) {
+      if (this.isLogin) {
         // 历史记录新增
         saveViewHistoryQuery({
           type: 'buyer',
-          name: item?.title,
+          name: item?.title
         }).then((res) => {
           console.log(res, 'res')
         })

+ 32 - 20
apps/mobile/src/views/search/result/company/index.vue

@@ -116,6 +116,7 @@
         v-if="hasData && listInfo.finished && showUpdate && isLogin"
         class="update-btn"
         @click="goBuyVip"
+        v-bound-phone="bindPhoneGoBuyVip()"
       >
         升级会员,查看更多搜索结果
       </div>
@@ -246,13 +247,12 @@ export default {
     getScopeTags() {
       const list = this.scopeTags
       if (!this.isLogin) {
-        return list.filter(item => !item.disabled)
+        return list.filter((item) => !item.disabled)
       }
       list.forEach((v) => {
         if (this.freeUser && v.vip) {
           v.disabled = true
-        }
-        else {
+        } else {
           v.disabled = false
         }
       })
@@ -277,7 +277,7 @@ export default {
           address: v.company_address,
           zhiMa: v.zhima_labels
             ? v.zhima_labels
-                .map(v => v.zhima_value || v.zhima_name)
+                .map((v) => v.zhima_value || v.zhima_name)
                 .join(',')
             : ''
         }
@@ -400,8 +400,7 @@ export default {
         this.listInfo.loading = true
         this.listInfo.finished = false
         this.onLoad()
-      }
-      catch (error) {
+      } catch (error) {
         console.error(error)
       }
     },
@@ -414,8 +413,7 @@ export default {
         this.$refs[name]?.toggle(false)
         this.setFilterData(name, false)
         // 查询接口
-      }
-      catch (error) {
+      } catch (error) {
         console.error(error)
       }
     },
@@ -437,8 +435,17 @@ export default {
           confirmButtonText: '去开通'
         })
         .then(() => {
-          this.doSave()
-          this.$router.push('/common/order/create/svip?type=buy')
+          // this.doSave()
+          // this.$router.push('/common/order/create/svip?type=buy')
+          this.$testBindPhone({
+            props: {
+              name: '企业搜索结果页- 开通超级订阅'
+            },
+            next: () => {
+              this.doSave()
+              this.$router.push('/common/order/create/svip?type=buy')
+            }
+          })
         })
         .catch(() => {})
     },
@@ -453,11 +460,9 @@ export default {
           let str = this.conditionStrMap[k].toString()
           if (str === 'all' || str === 'A,B,C,D') {
             str = ''
-          }
-          else if (k === 'zhiMa') {
+          } else if (k === 'zhiMa') {
             str = JSON.stringify(this.conditionStrMap[k])
-          }
-          else {
+          } else {
             str = this.conditionStrMap[k].toString()
           }
           params[k] = str
@@ -475,8 +480,7 @@ export default {
             this.listInfo.finished = true
             this.listInfo.entList = []
             this.hasData = false
-          }
-          else {
+          } else {
             this.hasData = true
             this.listInfo.entList.push(...data.list)
           }
@@ -484,13 +488,11 @@ export default {
           if (this.listInfo.entList.length >= this.listInfo.total) {
             this.listInfo.finished = true
           }
-        }
-        else {
+        } else {
           this.listInfo.finished = true
           this.hasData = false
         }
-      }
-      else {
+      } else {
         this.listInfo.finished = true
         this.hasData = false
       }
@@ -519,6 +521,16 @@ export default {
     goBuyVip() {
       this.doSave()
       this.$router.push('/common/order/create/svip?type=buy')
+    },
+    bindPhoneGoBuyVip() {
+      return {
+        props: {
+          name: '企业搜索结果页- 升级会员,查看更多搜索结果 '
+        },
+        next: () => {
+          this.goBuyVip()
+        }
+      }
     }
   }
 }

+ 13 - 4
apps/mobile/src/views/search/result/sun/index.vue

@@ -142,6 +142,7 @@
                   v-if="item.text === '点击查看'"
                   class="buyer-item link-clickable"
                   @click.stop="toToBuyerProfile(item.text)"
+                  v-bound-phone="bindPhoneToBuyerProfile(item.text)"
                   >{{ item.text }}</span
                 >
                 <span class="buyer-item link-clickable" v-else>{{
@@ -2253,6 +2254,16 @@ export default {
         keyStr,
         `<span class="highlight-text-orange-bd">${keyStr}</span>`
       )
+    },
+    bindPhoneToBuyerProfile(id) {
+      return {
+        props: {
+          name: '阳光直采详情页-采购单位点击查看'
+        },
+        next: () => {
+          this.toToBuyerProfile(id)
+        }
+      }
     }
   }
 }
@@ -2410,8 +2421,7 @@ export default {
   border-radius: 8px;
   border: 1px solid transparent;
   background-clip: padding-box, border-box;
-  background-image:
-    linear-gradient(#e8ffff 40%, #ffffff 100%),
+  background-image: linear-gradient(#e8ffff 40%, #ffffff 100%),
     linear-gradient(to right, #2abed1, #4de4f84c, #2abed1);
   background-origin: padding-box, border-box;
   .tab-right {
@@ -2607,8 +2617,7 @@ export default {
   color: #171826;
   border: 0.5px solid transparent;
   background-clip: padding-box, border-box;
-  background-image:
-    linear-gradient(#e8ffff 40%, #ffffff 100%),
+  background-image: linear-gradient(#e8ffff 40%, #ffffff 100%),
     linear-gradient(to right, #2abed1 10%, #4de4f84c 30%, #2abed1);
   background-origin: padding-box, border-box;
   border-radius: 8px;

+ 28 - 1
apps/mobile/src/views/subscribe/Guide.vue

@@ -95,9 +95,10 @@
       <div class="j-button-group" :class="{ height40: active === 1 }">
         <button
           v-show="active === 0"
-          @click="nextHandle"
           class="j-button-confirm"
           :class="{ opacity50: !nextDisabled }"
+          @click="nextHandle"
+          v-bound-phone="bindPhoneNextHandle()"
         >
           下一步
         </button>
@@ -172,6 +173,7 @@ import AreaCitySidebar from '@/components/selector/area-three-sidebar/index'
 import RecommendedWords from '@/components/subscribe/RecommendedWords'
 import { mixinHeader } from '@/utils/mixins/header'
 import { pushHistoryState } from '@/utils/mixins/pushState'
+import Cookies from 'js-cookie'
 import {
   getUserSubscribeSomeInfo,
   saveSubscribeGuide,
@@ -288,6 +290,19 @@ export default {
     this.getAppSwitchStatus()
     await this.isCanEditArea()
     this.pushHistoryState()
+    // 需求:进入页面时,如果没有绑定手机号,自动弹出绑定手机号弹窗,完成绑定返回订阅列表
+    try {
+      this.$testBindPhone({
+        props: {
+          name: '订阅向导-初始化页面'
+        },
+        bound: () => {
+          this.$router.replace('/tabbar/subscribe')
+        }
+      })
+    } catch (error) {
+      console.log(error)
+    }
   },
   methods: {
     ...mapActions('user', ['userVipSwitchState']),
@@ -628,6 +643,18 @@ export default {
       } else {
         return area
       }
+    },
+    // 绑定手机号指令(下一步按钮)
+    bindPhoneNextHandle() {
+      return {
+        props: {
+          name: '订阅向导-下一步'
+        },
+        bound: () => {
+          this.$router.replace('/tabbar/subscribe')
+        },
+        next: this.nextHandle
+      }
     }
   }
 }

+ 42 - 4
apps/mobile/src/views/tabbar/Box.vue

@@ -18,25 +18,33 @@
       <div class="work-common-box">
         <p class="mr-lr12 title-box">
           <span class="content-title">我的常用</span>
-          <span class="title-handle-span" @click="settingCommonFeature">
+          <span
+            class="title-handle-span"
+            @click="settingCommonFeature"
+            v-bound-phone="bindPhoneSettingCommonFeature()"
+          >
             <AppIcon name="Setting" />
             <span class="setting-text">设置</span>
           </span>
         </p>
-        <common-use :more-grid="true" @openLink="openLink" />
+        <common-use :more-grid="true" @openLink="bindPhoneOpenLink" />
       </div>
       <div class="section work-common-box">
         <p class="title-box mr-lr12">
           <span class="content-title">{{
             featureType === 'all' ? '全部功能' : '可用功能'
           }}</span>
-          <span class="title-handle-span" @click="changeFeatureType">
+          <span
+            class="title-handle-span"
+            @click="changeFeatureType"
+            v-bound-phone="bindPhoneChangeFeatureType()"
+          >
             <span>{{
               featureType === 'all' ? '查看可用功能' : '查看全部功能'
             }}</span>
           </span>
         </p>
-        <all-use :all-need-badge="false" @openLink="openLink" />
+        <all-use :all-need-badge="false" @openLink="bindPhoneOpenLink" />
       </div>
     </div>
     <CheckUserDialog />
@@ -220,6 +228,36 @@ export default {
           callback && callback()
         }
       }, 500)
+    },
+    bindPhoneSettingCommonFeature() {
+      return {
+        props: {
+          name: '工作台-常用设置'
+        },
+        next: () => {
+          this.settingCommonFeature()
+        }
+      }
+    },
+    bindPhoneOpenLink(item) {
+      this.$testBindPhone({
+        props: {
+          name: `工作台-${item.name}`
+        },
+        next: () => {
+          this.openLink(item)
+        }
+      })
+    },
+    bindPhoneChangeFeatureType() {
+      return {
+        props: {
+          name: '工作台-查看全部功能'
+        },
+        next: () => {
+          this.changeFeatureType()
+        }
+      }
     }
   }
 }

+ 2 - 2
apps/mobile/src/views/tabbar/Mine.vue

@@ -54,7 +54,7 @@ export default {
 
 <style lang="scss" scoped>
 .mine-page {
-  background: #F5F6F7;
+  background: #f5f6f7;
 }
 .mine-page-list {
   padding-bottom: 12px;
@@ -63,7 +63,7 @@ export default {
   padding-left: 8px;
   padding-right: 8px;
 }
-.mine-sign-in{
+.mine-sign-in {
   margin-top: 0;
 }
 </style>

+ 5 - 0
plugins/bind-phone-mobile/.browserslistrc

@@ -0,0 +1,5 @@
+> 1% in CN and last 2 versions
+Android >= 4.0
+iOS >= 7
+not ie > 0
+not ie_mob > 0

+ 5 - 0
plugins/bind-phone-mobile/.editorconfig

@@ -0,0 +1,5 @@
+[*.{js,jsx,ts,tsx,vue}]
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+insert_final_newline = true

+ 7 - 0
plugins/bind-phone-mobile/.env.development

@@ -0,0 +1,7 @@
+VITE_APP_BASE_API='/api'
+VITE_APP_BASE_URL='/'
+VITE_APP_BASE_PUBLIC=''
+VITE_APP_IMAGE_BASE='https://web2-qmxtest.jydev.jianyu360.com'
+VITE_APP_APP_PROJECT_BASE='https://app2-jytest.jydev.jianyu360.com'
+VITE_APP_WX_PROJECT_BASE='https://jybx2-webtest.jydev.jianyu360.com'
+VITE_APP_GIT_BRANCH='v0.0.1'

+ 7 - 0
plugins/bind-phone-mobile/.env.production

@@ -0,0 +1,7 @@
+VITE_APP_BASE_API=''
+VITE_APP_BASE_URL='/'
+VITE_APP_BASE_PUBLIC='https://cdn-common.jianyu360.cn/jy_mobile/'
+VITE_APP_IMAGE_BASE=''
+VITE_APP_APP_PROJECT_BASE=''
+VITE_APP_WX_PROJECT_BASE=''
+VITE_APP_GIT_BRANCH='v1.0.74'

+ 16 - 0
plugins/bind-phone-mobile/.eslintignore

@@ -0,0 +1,16 @@
+/src/assets/fonts
+src/utils/callFn/checkUpdate.js
+
+/node_modules
+/scripts
+/config
+/pnpm-lock.yaml
+/pnpm-workspace.yaml
+.DS_Store
+
+/package.json
+/tsconfig.json
+**/*.md
+build
+
+.eslintrc.js

+ 42 - 0
plugins/bind-phone-mobile/.gitignore

@@ -0,0 +1,42 @@
+.DS_Store
+node_modules
+/mobile_web
+/jy_mobile
+/dist
+storybook-static
+/docs
+
+
+# local env files
+.env.local
+.env.*.local
+
+# npm
+package-lock.json
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# compressed files
+*.rar
+*.zip
+*.7z
+*.tar
+
+# dev files
+/stats.html

+ 5 - 0
plugins/bind-phone-mobile/.npmrc

@@ -0,0 +1,5 @@
+always-auth=true
+registry=https://registry.npmmirror.com/
+@jianyu:registry=http://172.20.100.235:14873/
+@jy:registry=http://172.20.100.235:14873/
+element-ui:registry=http://172.20.100.235:14873/

+ 153 - 0
plugins/bind-phone-mobile/README.md

@@ -0,0 +1,153 @@
+# @jy/plugin-bind-phone
+
+> 移动端绑定手机号弹框插件,可通过指令、vue实例调用
+
+## 目录结构
+
+```
+├── README.md
+├── package.json
+├── public
+│   ├── favicon.ico
+│   └── index.html
+├── src
+│   ├── api
+│   ├── assets
+│   ├── components  // 项目业务组件
+│   ├── router
+│   ├── utils
+│   └── views
+├── vite.config.js
+└── yarn.lock
+```
+
+## 引入方式
+
+1. web项目内通过package.json工作空间引入
+
+```
+"@jy/plugin-bind-phone": "workspace:*"
+
+// 注册
+import TestBindPhone from '@jy/plugin-bind-phone'
+Vue.use(TestBindPhone)
+```
+
+2. jy项目通过build后放置/common-module/plugins/目录下引入
+
+```
+<script src="/common-module/plugins/js/jy-bind-phone.umd.js"></script>
+// 引入后注册(必须在new Vue()前注册)
+Vue.use(BindPhone)
+```
+
+3.其它项目通过install私有包引入
+
+```
+pnpm add @jy/plugin-bind-phone@1.0.0
+
+import TestBindPhone from '@jy/plugin-bind-phone'
+Vue.use(TestBindPhone)
+```
+
+### Example
+
+```
+<template>
+  <div>
+    <button v-bound-phone="bindPhone()">指令触发</button>
+    <button @click="handle">手动实例触发</button>
+  </div>
+</template>
+<script>
+export default {
+  methods: {
+    bindPhone() {
+      return {
+        props: {
+          name: '触发位置名称(统计需要)'
+        },
+        next: () => {
+          <!-- 绑定成功/已绑定 下一步操作 -->
+        },
+        bound: () => {
+          // 绑定成功 下一步操作
+          // 当绑定完手机号操作与next不一致时需要传入,一致时只需传入next即可
+        },
+        close: () => {
+          <!-- 关闭弹框 -->
+        }
+      }
+    },
+    handle() {
+      this.$testBindPhone({
+        props: {
+          visible: true,
+          name: '测试弹框-全局方法触发'
+        },
+        next: () => {
+          this.$toast('next')
+        }
+      })
+    }
+  }
+}
+</script>
+```
+
+## Project setup
+
+```
+yarn install
+```
+
+### Compiles and hot-reloads for development
+
+```
+yarn serve
+```
+
+### Compiles and minifies for production
+
+```
+yarn build
+```
+
+### 配置package.json
+
+```
+指定打包路径
+"main": "./dist/jy-bind-phone.umd.js",
+"module": "./dist/jy-bind-phone.mjs",
+```
+
+### 配置私有库地址
+
+```
+pnpm set registry http://172.20.100.235:14873/
+```
+
+### 注册私有库
+
+```
+pnpm adduser --registry http://172.20.100.235:14873/
+```
+
+### 登录私有库
+
+```
+pnpm login --registry http://172.20.100.235:14873/
+```
+
+### 修改版本号
+
+```
+手动修改package.json版本号
+"version": "1.0.3",
+```
+
+### 发布私有库
+
+```
+pnpm publish --no-git-checks
+```

+ 101 - 0
plugins/bind-phone-mobile/index.html

@@ -0,0 +1,101 @@
+<!doctype html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover"
+    />
+    <meta name="browsermode" content="application" />
+    <meta name="x5-orientation" content="portrait" />
+    <meta name="screen-orientation" content="portrait" />
+    <meta name="x5-page-mode" content="app" />
+    <meta name="apple-mobile-web-app-capable" content="yes" />
+    <meta name="apple-mobile-web-app-status-bar-style" content="black" />
+    <meta name="format-detection" content="telephone=no" />
+    <link rel="icon" href="/favicon.ico" />
+    <link rel="preconnect" href="cdn-common.jianyu360.com" />
+    <link rel="dns-prefetch" href="cdn-common.jianyu360.com" />
+    <title>剑鱼标讯</title>
+    <!-- <script src="//cdn.bootcdn.net/ajax/libs/vConsole/3.15.0/vconsole.min.js"></script>
+    <script>
+      new window.VConsole()
+    </script> -->
+    <!-- 预加载,提升优先级  -->
+    <% if (!isDev) { %>
+    <link
+      rel="preload"
+      as="style"
+      href="//cdn-common.jianyu360.com/cdn/assets/iconfont/mobile/24.2.28/iconfont.css"
+    />
+    <link
+      rel="preload"
+      as="script"
+      href="//cdn-common.jianyu360.com/cdn/lib/vue/2.7.16/vue.min.js"
+    />
+    <link
+      rel="preload"
+      as="script"
+      href="//cdn-common.jianyu360.com/cdn/lib/vue-router/3.6.5/vue-router.min.js"
+    />
+    <link
+      rel="preload"
+      as="script"
+      href="//cdn-common.jianyu360.com/cdn/lib/vuex/3.6.2/vuex.min.js"
+    />
+    <link
+      rel="preload"
+      as="script"
+      href="//cdn-common.jianyu360.com/cdn/lib/axios/1.6.7/axios.min.js"
+    />
+
+    <link
+      rel="preload"
+      as="script"
+      href="//cdn-common.jianyu360.com/cdn/lib/vant/2.12.24/lib/vant.min.js"
+    />
+    <link
+      rel="preload"
+      as="script"
+      href="//cdn-common.jianyu360.com/cdn/lib/lodash/4.17.21/lodash.min.js"
+    />
+    <link
+      rel="preload"
+      as="script"
+      href="//cdn-common.jianyu360.com/cdn/lib/js-cookie/2.2.1/js.cookie.min.js"
+    />
+    <% } %>
+
+    <!-- 按优先级加载  -->
+    <link
+      rel="stylesheet"
+      href="//cdn-common.jianyu360.com/cdn/assets/iconfont/mobile/24.7.16/iconfont.css"
+    />
+
+    <% if (!isDev) { %>
+
+    <script src="//cdn-common.jianyu360.com/cdn/lib/vue/2.7.16/vue.min.js"></script>
+    <script src="//cdn-common.jianyu360.com/cdn/lib/vue-router/3.6.5/vue-router.min.js"></script>
+    <script src="//cdn-common.jianyu360.com/cdn/lib/vuex/3.6.2/vuex.min.js"></script>
+    <script src="//cdn-common.jianyu360.com/cdn/lib/axios/1.6.7/axios.min.js"></script>
+    <script src="//cdn-common.jianyu360.com/cdn/lib/vant/2.12.24/lib/vant.min.js"></script>
+    <script src="//cdn-common.jianyu360.com/cdn/lib/lodash/4.17.21/lodash.min.js"></script>
+    <script src="//cdn-common.jianyu360.com/cdn/lib/js-cookie/2.2.1/js.cookie.min.js"></script>
+
+    <% } %>
+
+    <script
+      defer
+      src="//cdn-common.jianyu360.com/cdn/assets/iconfont/mobile/24.7.16/iconfont.js"
+    ></script>
+  </head>
+
+  <body>
+    <noscript>
+      <strong>JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 46 - 0
plugins/bind-phone-mobile/package.json

@@ -0,0 +1,46 @@
+{
+  "name": "@jy/plugin-bind-phone",
+  "version": "1.0.7",
+  "private": false,
+  "description": "移动端绑定手机弹框插件",
+  "files": [
+    "dist"
+  ],
+  "main": "./dist/jy-bind-phone.umd.js",
+  "module": "./dist/jy-bind-phone.mjs",
+  "exports": "src/index.js",
+  "scripts": {
+    "dev": "vite",
+    "build": "pnpm run update && pnpm run build:vite",
+    "build:vite": "vite build",
+    "preview": "vite preview --port 4173",
+    "lint": "eslint . --fix",
+    "format": "prettier --write \"./**/*.{,vue,ts,js,json,md}\""
+  },
+  "dependencies": {
+    "js-cookie": "^3.0.1",
+    "qs": "^6.11.2",
+    "vant": "2.12.44"
+  },
+  "devDependencies": {
+    "@jonny1994/postcss-px-to-viewport": "^1.1.0",
+    "@nabla/vite-plugin-eslint": "^2.0.2",
+    "@rushstack/eslint-patch": "^1.1.0",
+    "@vitejs/plugin-vue2": "^2.2.0",
+    "@vue/eslint-config-prettier": "^7.0.0",
+    "autoprefixer": "^10.4.14",
+    "eslint": "^8.57.0",
+    "eslint-plugin-vue": "^9.22.0",
+    "less": "^4.1.3",
+    "prettier": "^2.5.1",
+    "rollup-plugin-visualizer": "^5.9.2",
+    "sass": "^1.63.2",
+    "terser": "^5.14.2",
+    "unplugin-vue-components": "^0.25.1",
+    "vite": "^4.5.3",
+    "vite-plugin-compression": "^0.5.1",
+    "vite-plugin-ejs": "1.6.4",
+    "vite-plugin-externals": "^0.6.2",
+    "vite-plugin-css-injected-by-js": "^3.1.0"
+  }
+}

+ 30 - 0
plugins/bind-phone-mobile/postcss.config.js

@@ -0,0 +1,30 @@
+// 使用 @vue/cli autoprefixer 依赖,如使用严格包管理模式需要兼容处理
+const autoprefixer = require('autoprefixer')
+const pxtoviewport = require('@jonny1994/postcss-px-to-viewport')
+// const envBook = process.argv.includes('config/storybook')
+const envBook = false
+let plugins = []
+if (!envBook) {
+  plugins = [
+    autoprefixer,
+    pxtoviewport({
+      unitToConvert: 'px',
+      viewportWidth: 375,
+      unitPrecision: 3,
+      propList: ['*'],
+      viewportUnit: 'vw',
+      fontViewportUnit: 'vw',
+      selectorBlackList: [],
+      // 小于或等于 1px 的像素值不进行转换
+      minPixelValue: 1,
+      mediaQuery: false,
+      // 兼容 vant 需要去掉此处
+      // exclude: [/node_modules/],
+      replace: true
+    })
+  ]
+}
+
+module.exports = {
+  plugins
+}

+ 45 - 0
plugins/bind-phone-mobile/src/App.vue

@@ -0,0 +1,45 @@
+<template>
+  <div id="app">
+    <router-view class="router j-container" />
+  </div>
+</template>
+<script>
+export default {
+  name: 'App',
+  components: {},
+  data() {
+    return {}
+  },
+  computed: {},
+  created() {},
+  mounted() {},
+  methods: {}
+}
+</script>
+<style lang="scss">
+#app {
+  font-family:
+    -apple-system,
+    BlinkMacSystemFont,
+    Helvetica Neue,
+    PingFang SC,
+    Microsoft YaHei,
+    Source Han Sans SC,
+    Noto Sans CJK SC,
+    WenQuanYi Micro Hei,
+    sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  color: #171826;
+  width: 100%;
+  height: 100vh;
+  position: relative;
+}
+.router {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+</style>

+ 29 - 0
plugins/bind-phone-mobile/src/api/api.js

@@ -0,0 +1,29 @@
+import qs from 'qs'
+import request from './index'
+
+// 获取图形验证码
+export function getPhoneCaptcha() {
+  return request({
+    url: `/jypay/user/phone/imgCaptcha?t=${Date.now()}`,
+    method: 'GET'
+  })
+}
+
+// 发送短信验证码/手机号绑定
+export function setPhoneBind(data, type) {
+  data = qs.stringify(data)
+  return request({
+    url: `/jypay/user/phone/${type}`,
+    method: 'POST',
+    data
+  })
+}
+
+// 广告获取
+export function ajaxGetAD(data) {
+  return request({
+    url: '/publicapply/free/getJyAdList',
+    method: 'post',
+    data
+  })
+}

+ 4 - 0
plugins/bind-phone-mobile/src/api/index.js

@@ -0,0 +1,4 @@
+import service from './service'
+import './interceptors'
+
+export default service

+ 51 - 0
plugins/bind-phone-mobile/src/api/interceptors.js

@@ -0,0 +1,51 @@
+import service from './service'
+import { Toast } from 'vant'
+
+service.interceptors.request.use(
+  (config) => {
+    if (config?.formData) {
+      config.headers.content = 'multipart/form-data'
+    }
+    if (config?.data?.noToast) {
+      delete config?.data.noToast
+      config.noToast = true
+    }
+
+    return config
+  },
+  (error) => {
+    if (debug) {
+      console.log('[debug]请求错误:', error)
+    }
+    return Promise.reject(error)
+  }
+)
+
+service.interceptors.response.use(
+  (response) => {
+    const res = response.data
+    if (response.status === 200) {
+      // 发送请求时配置 noToast 则不弹出 toast 提示
+      if (res && !response.config.noToast) {
+        // 判断是否需要登录
+        if (res.error_msg === '需要登录' || response.data.error_code === 1001) {
+          Toast('需要登录')
+        } else if (res.error_msg) {
+          Toast(res.error_msg)
+        }
+      }
+    } else {
+      console.warn(res)
+      return Promise.reject(new Error('Error'))
+    }
+    return res
+  },
+  (error) => {
+    if (debug) {
+      console.log('[debug]返回数据错误:', error)
+    }
+    return Promise.reject(error)
+  }
+)
+
+export default service

+ 5 - 0
plugins/bind-phone-mobile/src/api/service.js

@@ -0,0 +1,5 @@
+import axios from 'axios'
+
+export default axios.create({
+  baseURL: import.meta.env.VITE_APP_BASE_API
+})

+ 120 - 0
plugins/bind-phone-mobile/src/assets/style.css

@@ -0,0 +1,120 @@
+.bind-phone-dialog {
+  width: 343px !important;
+  border-radius: 16px !important;
+  z-index: 3010 !important;
+}
+.bind-phone-dialog .bind-header {
+  position: relative;
+  height: 80px;
+}
+.bind-phone-dialog .banner-ad {
+  width: 100%;
+  height: 100%;
+  object-fit: fill;
+}
+.bind-phone-dialog .dialog-close {
+  position: absolute;
+  top: 10px;
+  right: 10px;
+  font-size: 16px;
+  color: rgba(0, 0, 0, 0.3);
+}
+.bind-phone-dialog .bind-form .send-code {
+  padding: 0;
+  height: unset;
+  color: #c0c4cc;
+  font-size: 14px;
+  line-height: 20px;
+  border: none;
+}
+.bind-phone-dialog .bind-form .send-code.active {
+  color: #2abed1;
+}
+.bind-phone-dialog .bind-form .van-cell {
+  align-items: center;
+  padding: 16px;
+}
+.bind-phone-dialog .bind-form .van-field__label {
+  width: 75px;
+  margin-right: 8px;
+  font-size: 15px;
+  line-height: 22px;
+  color: #5f5e64;
+}
+.bind-phone-dialog .bind-form .van-field__control {
+  font-size: 16px;
+}
+.bind-phone-dialog .bind-form .van-field__button {
+  display: flex;
+}
+.bind-phone-dialog .j-button-group {
+  padding: 12px 20px !important;
+  height: auto !important;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background-color: #fff;
+}
+.bind-phone-dialog .j-button-confirm,
+.bind-phone-dialog .j-button-cancel {
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex: 1;
+  width: 100%;
+  height: 44px;
+  font-size: 18px;
+  line-height: inherit;
+  text-align: center;
+  border-radius: 8px !important;
+  border: 0;
+  outline: 0;
+}
+.bind-phone-dialog .j-button-confirm {
+  background: #2abed1;
+  color: #fff;
+}
+.bind-phone-dialog .j-button-cancel {
+  margin-right: 13px;
+  color: #5f5e64;
+  background-color: #edeff2;
+}
+.bind-phone-dialog .j-button-confirm[disabled],
+.bind-phone-dialog .j-button-cancel[disabled] {
+  opacity: 0.5;
+}
+.bind-phone-dialog .van-dialog__content {
+  padding: 0 !important;
+}
+
+.bind-phone-dialog .reveal-box {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+.bind-phone-dialog .reveal-box .van-image,
+.bind-phone-dialog .reveal-box .van-icon__image {
+  width: 100%;
+  height: 100%;
+}
+.bind-phone-dialog .reveal-box .tag-text {
+  position: absolute;
+  color: rgba(255, 255, 255, 0.8);
+  background: rgba(0, 0, 0, 0.16);
+  bottom: 0;
+  right: 0;
+  font-size: 9px;
+  line-height: 10px;
+  padding: 3px 6px;
+  border-radius: 8px 0px 8px 0px;
+}
+.bind-phone-dialog input {
+  caret-color: #2abed1;
+}
+.bind-phone-dialog input::placeholder {
+  color: #c0c4cc;
+}
+.bind-phone-dialog input {
+  color: #171826;
+}

+ 417 - 0
plugins/bind-phone-mobile/src/components/BindPhoneDialog.vue

@@ -0,0 +1,417 @@
+<template>
+  <van-dialog
+    class="bind-phone-dialog"
+    v-model="show"
+    :show-confirm-button="false"
+  >
+    <div class="bind-header">
+      <van-image
+        v-if="getConfig.pic"
+        :id="getConfig.id"
+        class="reveal-box"
+        :src="getConfig.pic"
+        :alt="getConfig.name"
+        :style="getStyle"
+        error-icon="https://cdn-ali2.jianyu360.cn/qmxupload/2024/05/06/202405061855550056F9KON1T.png"
+        @click.stop="openAD(getConfig.link)"
+      >
+        <div class="tag-text">广告</div>
+        <van-icon class="dialog-close" name="cross" @click.stop="onClose" />
+      </van-image>
+    </div>
+    <div class="bind-form">
+      <van-field
+        ref="phoneRef"
+        v-model.trim="info.phone"
+        label="手机号"
+        type="tel"
+        maxlength="11"
+        placeholder="请输入手机号码"
+        :error-message="errorMessage.phone"
+        @blur="checkPhoneRegPass"
+      ></van-field>
+      <van-field
+        v-show="picCode.show"
+        v-model.trim="info.picCode"
+        label=""
+        maxlength="6"
+        placeholder="图形验证码"
+        :error-message="errorMessage.picCode"
+      >
+        <template #button>
+          <div class="pic-code" @click="refreshCaptcha">
+            <img :src="imgBase64Complete" v-if="picCode.imgBase64" />
+            <van-loading size="24" v-else></van-loading>
+          </div>
+        </template>
+      </van-field>
+      <van-field
+        v-model.trim="info.code"
+        label="验证码"
+        maxlength="6"
+        placeholder="请输入验证码"
+        :error-message="errorMessage.code"
+      >
+        <template #button>
+          <van-button
+            class="send-code"
+            :class="{ active: info.phone }"
+            size="small"
+            :disabled="sendCodeButtonDisabled"
+            @click="sendVerifyCode"
+            >{{ sendCodeButtonText }}</van-button
+          >
+        </template>
+      </van-field>
+    </div>
+    <div class="bind-footer">
+      <div class="j-button-group">
+        <button
+          class="j-button-confirm clickable"
+          @click="onConfirm"
+          :disabled="confirmButtonDisabled"
+        >
+          绑定
+        </button>
+      </div>
+    </div>
+  </van-dialog>
+</template>
+
+<script>
+import { Dialog, Button, Loading, Field, Icon, Image } from 'vant'
+import { getPhoneCaptcha, setPhoneBind, ajaxGetAD } from '../api/api'
+import { adConfigFormatter, px2viewport } from '../utils/utils'
+
+export default {
+  name: 'BindPhoneDialog',
+  components: {
+    [Dialog.name]: Dialog,
+    [Button.name]: Button,
+    [Loading.name]: Loading,
+    [Field.name]: Field,
+    [Icon.name]: Icon,
+    [Image.name]: Image
+  },
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    adCode: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      show: this.visible,
+      info: {
+        phone: '',
+        picCode: '',
+        code: ''
+      },
+      errorMessage: {
+        phone: '',
+        picCode: '',
+        code: ''
+      },
+      sendCodeButton: {
+        timerId: 0,
+        timeStartDefault: 60,
+        defaultValue: '发送验证码',
+        count: 0
+      },
+      picCode: {
+        show: false,
+        imgBase64: '',
+        cacheShow: false,
+        cacheImgBase64: ''
+      },
+      conf: {
+        phoneReg: /^1[3-9]\d{9}$/
+      },
+      info: {}
+    }
+  },
+  computed: {
+    confirmButtonDisabled() {
+      let hasEmpty = false
+      if (this.picCode.show) {
+        hasEmpty = !this.info.phone || !this.info.code || !this.info.picCode
+      } else {
+        hasEmpty = !this.info.phone || !this.info.code
+      }
+      const pass = this.conf.phoneReg.test(this.info.phone)
+      return hasEmpty || !pass
+    },
+    sendCodeButtonText() {
+      const dText = this.sendCodeButton.defaultValue
+      return this.sendCodeButton.count <= 0
+        ? dText
+        : `重新发送(${this.sendCodeButton.count}s)`
+    },
+    sendCodeButtonDisabled() {
+      return this.sendCodeButton.count > 0
+    },
+    imgBase64Complete() {
+      return 'data:image/png;base64,' + this.picCode.imgBase64
+    },
+    getStyle() {
+      return {
+        width: this.getConfig?.extend?.width
+          ? px2viewport(this.getConfig?.extend?.width)
+          : '',
+        height: this.getConfig?.extend?.height
+          ? px2viewport(this.getConfig?.extend?.height)
+          : ''
+      }
+    },
+    getConfig() {
+      if (this.info?.pic) {
+        return this.info
+      } else {
+        return {
+          pic: '',
+          link: '',
+          name: '内容区域广告',
+          extend: {
+            width: '',
+            height: '',
+            type: ''
+          }
+        }
+      }
+    }
+  },
+  watch: {
+    visible(val) {
+      console.log(val, 'visible')
+    }
+  },
+  created() {
+    if (!this.adCode) {
+      const code = this.$envs.inWX
+        ? 'wx-bind-phone-dialog'
+        : 'app-bind-phone-dialog'
+      this.getAd([code])
+    } else {
+      this.getAd([this.adCode])
+    }
+    this.getImgCaptcha()
+  },
+  mounted() {
+    try {
+      setTimeout(() => {
+        this.$refs.phoneRef.focus()
+      }, 500)
+    } catch (error) {}
+  },
+  methods: {
+    showToast(message) {
+      this.$toast({
+        duration: 1500,
+        forbidClick: true,
+        message: message
+      })
+    },
+    showLoading() {
+      return this.$toast.loading({
+        duration: 0,
+        forbidClick: true,
+        message: 'loading...'
+      })
+    },
+    async getAdInfoFromRequest(codes) {
+      // 生成缓存键名
+      const cacheKey = `AD_CACHE_${codes.join('_')}-login-clear`
+      try {
+        // 尝试读取缓存
+        const cacheData = localStorage.getItem(cacheKey)
+        if (cacheData) {
+          const { data, timestamp } = JSON.parse(cacheData)
+          // 检查缓存是否在10分钟内(600000毫秒)
+          if (Date.now() - timestamp < 600000) {
+            return { info: data }
+          }
+        }
+      } catch (e) {
+        console.warn('广告缓存读取失败', e)
+      }
+      const {
+        error_code: code,
+        error_msg: msg,
+        data = {}
+      } = await ajaxGetAD({ codes })
+      if (code === 0 && data) {
+        const info = adConfigFormatter(Object.values(data).flat()[0])
+        // 写入缓存
+        try {
+          localStorage.setItem(
+            cacheKey,
+            JSON.stringify({
+              data: info,
+              timestamp: Date.now()
+            })
+          )
+        } catch (e) {
+          console.warn('广告缓存写入失败', e)
+        }
+        return { info }
+      } else {
+        console.warn(msg)
+      }
+    },
+    afterGetConfig() {
+      this.$nextTick(() => {
+        // 计算高度过小,给个占位类名
+        const image = this.$refs.image
+        if (image) {
+          const $el = image?.$el
+          if ($el) {
+            const height = $el.clientHeight
+            if (height < 10) {
+              this.mgb = true
+            }
+          }
+        }
+      })
+    },
+    async getAd(codes) {
+      try {
+        const { info = {} } = await this.getAdInfoFromRequest(codes)
+        this.info = info || {}
+      } catch (e) {
+        console.warn('获取广告位信息异常:', e)
+      } finally {
+        this.afterGetConfig()
+      }
+    },
+    async getImgCaptcha(needCache) {
+      if (this.picCode.cacheImgBase64) {
+        this.picCode.show = this.picCode.cacheShow
+        this.picCode.imgBase64 = this.picCode.cacheImgBase64
+        this.picCode.cacheImgBase64 = ''
+        return
+      }
+      const { error_code: code, error_msg: msg, data } = await getPhoneCaptcha()
+      if (code === 0 && data) {
+        // 是否缓存图片
+        if (needCache === 'cache') {
+          // 将下一张图片的状态缓存
+          this.picCode.cacheShow = data.needVerify
+          this.picCode.cacheImgBase64 = data.imageData
+        } else {
+          this.picCode.show = data.needVerify
+          this.picCode.imgBase64 = data.imageData
+        }
+      } else {
+        this.showToast(msg)
+      }
+    },
+    refreshCaptcha() {
+      this.picCode.imgBase64 = ''
+      this.getImgCaptcha()
+    },
+    checkPhoneRegPass() {
+      let pass = this.conf.phoneReg.test(this.info.phone)
+      if (this.info.phone) {
+        if (pass) {
+          this.errorMessage.phone = ''
+        } else {
+          this.errorMessage.phone = '手机号格式不正确'
+        }
+      } else {
+        this.errorMessage.phone = ''
+      }
+      return pass
+    },
+    startSendCodeTimer(t) {
+      this.sendCodeButton.count = t || this.sendCodeButton.timeStartDefault
+      this.sendCodeButton.timerId = setInterval(() => {
+        this.sendCodeButton.count--
+        if (this.sendCodeButton.count <= 0) {
+          // 倒计时结束
+          clearInterval(this.sendCodeButton.timerId)
+          // 倒计时结束,刷新验证码
+          if (this.picCode.cacheImgBase64) {
+            this.picCode.show = this.picCode.cacheShow
+            this.picCode.imgBase64 = this.picCode.cacheImgBase64
+            this.picCode.cacheImgBase64 = ''
+          } else {
+            this.refreshCaptcha()
+          }
+        }
+      }, 1000)
+    },
+    // 发送验证码
+    async sendVerifyCode() {
+      const pass = this.checkPhoneRegPass()
+      if (!pass) return
+      const loading = this.showLoading()
+      const params = {
+        phone: this.info.phone,
+        code: this.info.picCode,
+        step: 1
+      }
+      const {
+        error_code: code,
+        error_msg: msg,
+        data
+      } = await setPhoneBind(params, 'bind')
+      loading.clear()
+      if (code === 0 && data?.state === 1) {
+        this.startSendCodeTimer()
+        this.showToast('验证码发送成功')
+        this.getImgCaptcha('cache')
+      } else {
+        this.showToast(msg || '验证码发送失败')
+        this.refreshCaptcha()
+      }
+    },
+    async onConfirm() {
+      const pass = this.checkPhoneRegPass()
+      if (!pass) return
+      const loading = this.showLoading()
+      const params = {
+        phone: this.info.phone,
+        code: this.info.code,
+        step: 2
+      }
+      const {
+        error_code: code,
+        error_msg: msg,
+        data
+      } = await setPhoneBind(params, 'bind')
+      loading.clear()
+      if (code === 0 && data) {
+        const { isMerge } = data
+        // 绑定成功
+        this.show = false
+        if (isMerge === 1) {
+          // 是否有合并账号动作
+          window.location.reload()
+        } else {
+          this.$emit('bound')
+        }
+      } else {
+        this.$toast(msg || '请求失败')
+      }
+    },
+    onClose() {
+      this.$emit('close')
+      this.show = false
+    },
+    close() {
+      this.show = false
+    },
+    open() {
+      this.show = true
+    },
+    openAD(link) {
+      if (!link) return
+      location.href = link
+    }
+  }
+}
+</script>

+ 19 - 0
plugins/bind-phone-mobile/src/entry.js

@@ -0,0 +1,19 @@
+/**
+ * description: 打包入口文件,可输出js插件供外部html调用
+ */
+
+import BindPhoneDialog from './components/BindPhoneDialog.vue'
+// import BindPhoneDirective from './utils/directives/bind-phone.js'
+import DirectiveBindPhonePlugin from './utils/plugins/directive-bind-phone.js'
+import PrototypeBindPhonePlugin from './utils/plugins/prototype-bind-phone.js'
+import './utils/prototype/env.js'
+import './assets/style.css'
+
+const install = (Vue) => {
+  Vue.use(DirectiveBindPhonePlugin)
+  Vue.use(PrototypeBindPhonePlugin)
+}
+export default {
+  install,
+  BindPhoneDialog
+}

+ 20 - 0
plugins/bind-phone-mobile/src/index.js

@@ -0,0 +1,20 @@
+import DirectiveBindPhonePlugin from './utils/plugins/directive-bind-phone.js'
+import PrototypeBindPhonePlugin from './utils/plugins/prototype-bind-phone.js'
+import BindPhoneDirective from './utils/directives/bind-phone'
+import BindPhoneDialog from './components/BindPhoneDialog.vue'
+import './assets/style.css'
+import './utils/prototype/env.js'
+
+export {
+  DirectiveBindPhonePlugin,
+  BindPhoneDirective,
+  BindPhoneDialog,
+  PrototypeBindPhonePlugin
+}
+
+const TestBindPhone = (Vue) => {
+  Vue.use(DirectiveBindPhonePlugin)
+  Vue.use(PrototypeBindPhonePlugin)
+}
+
+export default TestBindPhone

+ 32 - 0
plugins/bind-phone-mobile/src/main.js

@@ -0,0 +1,32 @@
+import Vue from 'vue'
+import { Dialog, Lazyload, Toast } from 'vant'
+import App from './App.vue'
+import 'vant/lib/index.less'
+import router from './router'
+import DirectiveBindPhonePlugin from './utils/plugins/directive-bind-phone.js'
+import PrototypeBindPhonePlugin from './utils/plugins/prototype-bind-phone.js'
+import './utils/prototype/env.js'
+
+Vue.use(Toast)
+  .use(Lazyload)
+  .use(Dialog)
+  .use(DirectiveBindPhonePlugin)
+  .use(PrototypeBindPhonePlugin)
+
+// 设置默认 loading 配置项
+Toast.setDefaultOptions('loading', {
+  forbidClick: true
+})
+
+Vue.filter('stripHTML', (value) => {
+  const div = document.createElement('div')
+  div.innerHTML = value
+  const text = div.textContent || div.textContent || ''
+  return text
+})
+Vue.config.productionTip = false
+
+new Vue({
+  router,
+  render: (h) => h(App)
+}).$mount('#app')

+ 25 - 0
plugins/bind-phone-mobile/src/router/index.js

@@ -0,0 +1,25 @@
+import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+const routes = [
+  {
+    path: '/',
+    name: 'test',
+    component: () => import('@/views/test.vue')
+  }
+]
+
+if (import.meta.env.DEV) {
+  Vue.use(VueRouter)
+}
+
+const createRouter = () =>
+  new VueRouter({
+    mode: 'history',
+    base: import.meta.env.VITE_APP_BASE_URL,
+    scrollBehavior: () => ({ x: 0, y: 0 }),
+    routes
+  })
+
+const router = createRouter()
+export default router

+ 318 - 0
plugins/bind-phone-mobile/src/utils/appFn.js

@@ -0,0 +1,318 @@
+import { copyText } from '@/utils'
+
+/**
+ * 客户端拨打电话
+ * @param phone
+ */
+export function appCallPhone(phone) {
+  try {
+    JyObj.callPhone(phone)
+  } catch (e) {
+    console.warn('error: app call phone', e)
+  }
+}
+
+export function appCallCopyText(text) {
+  try {
+    JyObj.wirteRight(text)
+  } catch (e) {
+    console.warn('error: app call copy text', e)
+    copyText(text)
+  }
+}
+
+/**
+ * 客户端打开新窗口
+ * @param link 目标链接
+ * @param title 窗口标题
+ */
+export function appCallOpenWindow(link, title = '剑鱼标讯') {
+  try {
+    JyObj.openExternalLink(link, title)
+  } catch (e) {
+    console.warn('error: app call openExternalLink', e)
+  }
+}
+
+/**
+ * 切换底部导航
+ * @param name 需要切换到的TabName, 对应关系 search:搜索 subscribe:订阅 box:百宝箱 me:我的 message:消息
+ */
+export function appCallChangeTab(name) {
+  try {
+    JyObj.skipAppointTab(name)
+  } catch (e) {
+    console.warn('error: app call skipAppointTab', e)
+  }
+}
+
+/**
+ * 刷新对应导航页面
+ * @param name 需要切换到的TabName, 对应关系 search:搜索 subscribe:订阅 box:百宝箱 me:我的 message:消息
+ */
+export function appCallReloadTab(name, status = 1) {
+  try {
+    JyObj.refreshAppointTab(name, status)
+  } catch (e) {
+    console.warn('error: app call refreshAppointTab', e)
+  }
+}
+
+/**
+ * 返回对应导航页面
+ * @param name 需要切换返回到的TabName
+ *              3.0.5以及以前版本:H 首页, '' 当前 webview 顶级
+ *              3.0.6以及以后版本:H 首页, '' 无操作
+ */
+export function appCallBackTab(name) {
+  try {
+    JyObj.backUrl(name)
+  } catch (e) {
+    console.warn('error: app call backUrl', e)
+  }
+}
+
+/**
+ * 隐藏显示底部菜单栏,3.0.6版本移除
+ * @param type 0:隐藏;1:显示
+ */
+export function appCallHideTab(type) {
+  try {
+    JyObj.hiddenBottom(type)
+  } catch (e) {
+    console.warn('error: app call hiddenBottom', e)
+  }
+}
+
+/**
+ * app获取用户token方法
+ */
+export function appCallGetToken() {
+  let result
+  try {
+    result = JyObj.getUserToken()
+  } catch (e) {
+    console.warn('error: app call getUserToken', e)
+  }
+  return result
+}
+
+/**
+ * 保存图片到本地相册
+ */
+
+export function savePic(
+  imgbase64,
+  tip = '剑鱼标讯需要您的存储权限、电话权限,将用于帮助您下载、保存图片到本地,将内容成功分享到社交平台。'
+) {
+  try {
+    window.__compatibleAppFn(JyObj.savePic, imgbase64, tip)
+  } catch (e) {
+    console.warn('error: app call savePic', e)
+  }
+}
+
+// 查看开关状态 是否接受消息
+export function checkNoticePermission() {
+  let status
+  try {
+    status = JyObj.IosCall('checkNoticePermission')
+  } catch (e) {
+    console.warn('error: app call checkNoticePermission', e)
+  }
+  return status
+}
+
+// 打开接受消息开关
+export function openSystemNotification() {
+  try {
+    JyObj.openSystemNotification()
+  } catch (e) {
+    console.warn('error: app call openSystemNotification', e)
+  }
+}
+
+/**
+ * 客户端版本号获取
+ */
+export function getAppVersion() {
+  let version = ''
+  try {
+    version = JyObj.getVersion()
+  } catch (e) {
+    console.warn('error: get app version failed', e)
+  }
+  return version || ''
+}
+
+/**
+ * APP独有 ios附件下载 、Android客户端暂无!
+ * doc 、docx、excel 、xls 、 xlsxppt 、 pptx、 pdf、 txt、png 、PNG、jpg 、JPG 暂定这些为常见类型~支持在线预览+下载+转存
+ * 其他类型仅支持下载+转存
+ * @param filename 文件名称不带后缀
+ * @param filetype 文件类型:doc word excel 等等
+ * @param fileurl 文件链接
+ * @param filesize 文件大小
+ */
+export function appDownLoadFile(filename, filetype, fileurl, filesize) {
+  try {
+    JyObj.downLoadFile(filename, filetype, fileurl, filesize)
+  } catch (e) {
+    console.warn('error: app ios download file failed', e)
+  }
+}
+
+/**
+ * 隐藏小红点,3.0.6版本移除
+ * @param {string} menu [搜索:search 订阅:subscribe 百宝箱:box 我的:me 消息:message]
+ */
+export function appHideRedSpotOnMenu(menu) {
+  if (menu === 'me') {
+    menu = 'my'
+  }
+  try {
+    JyObj.hideRedSpotOnMenu(menu)
+  } catch (e) {
+    console.warn('error: app hideRedSpotOnMenu failed', e)
+  }
+}
+
+/**
+ * 底部栏消息角标数量,3.0.6版本移除
+ * @param {string} num
+ */
+export function appSendMsgCount(num) {
+  try {
+    JyObj.sendMsgCount(num)
+  } catch (e) {
+    console.warn('error: app sendMsgCount failed', e)
+  }
+}
+
+/**
+ * 直接打开微信里的扫码功能,3.0.6版本新增
+ */
+export function appOpenWeChartScan() {
+  try {
+    JyObj.openWeChartScan()
+  } catch (e) {
+    console.warn('error: app openWeChartScan failed', e)
+  }
+}
+
+/**
+ * 用来清除webview浏览历史记录,3.0.6版本新增
+ * 此方法在ios的单页面程序中使用会清除当前页面的历史,造成前进后无法后退。
+ * 所以ios谨慎使用此方法。可使用以下方案替代:(sideslipClose+不提供返回按钮)
+ */
+export function appClearHistory() {
+  try {
+    JyObj.clearHistory()
+  } catch (e) {
+    console.warn('error: app clearHistory failed', e)
+  }
+}
+
+/**
+ * ios开启侧滑(ios专用,安卓调用无效),3.0.6版本新增
+ */
+export function appSideslipOpen() {
+  try {
+    JyObj.sideslipOpen()
+  } catch (e) {
+    console.warn('error: app sideslipOpen failed', e)
+  }
+}
+/**
+ * ios关闭侧滑(ios专用,安卓调用无效),3.0.6版本新增
+ */
+export function appSideslipClose() {
+  try {
+    JyObj.sideslipClose()
+  } catch (e) {
+    console.warn('error: app sideslipClose failed', e)
+  }
+}
+
+/**
+ * app分享
+ * shareType: 1:微信 2:QQ 3:朋友圈
+ * title: 分享标题
+ * content: 分享内容
+ * link: 分享链接
+ * authTip: 授权提示文案
+ */
+export function appShare(
+  shareType,
+  title,
+  content,
+  link,
+  authTip = '剑鱼标讯需申请存储权限、电话权限,以便您能顺利参与分享有礼活动,将招标信息、文库内容成功分享到社交平台。'
+) {
+  try {
+    if (
+      window.__checkAppVersionCanRunTips &&
+      window.__checkAppVersionCanRunTips()
+    ) {
+      JyObj.share(shareType, title, content, link, authTip)
+    } else {
+      JyObj.share(shareType, title, content, link)
+    }
+  } catch (e) {
+    console.warn('error: app call share', e)
+  }
+}
+
+// 一键绑定、登录
+export function appGetPhoneBind() {
+  try {
+    JyObj.getPhoneBind()
+  } catch (e) {
+    console.warn('error: app call getPhoneBind', e)
+  }
+}
+
+// 获取极光推送id
+export function appGetPushRid() {
+  try {
+    JyObj.getPushRid()
+  } catch (e) {
+    console.warn('error: app call getPushRid', e)
+  }
+}
+
+// 获取推送id
+export function appGetOtherPushId() {
+  try {
+    JyObj.getOtherPushId()
+  } catch (e) {
+    console.warn('error: app call getOtherPushId', e)
+  }
+}
+
+// 获取手机型号
+export function appGetPhoneType() {
+  try {
+    JyObj.getPhoneType()
+  } catch (e) {
+    console.warn('error: app call getPhoneType', e)
+  }
+}
+
+// 渠道
+export function appGetChannel() {
+  try {
+    JyObj.getChannel()
+  } catch (e) {
+    console.warn('error: app call getChannel', e)
+  }
+}
+
+// 设备id
+export function appGetDeviceId() {
+  try {
+    JyObj.getDeviceId()
+  } catch (e) {
+    console.warn('error: app call getDeviceId', e)
+  }
+}

+ 98 - 0
plugins/bind-phone-mobile/src/utils/directives/bind-phone.js

@@ -0,0 +1,98 @@
+import Cookies from 'js-cookie'
+
+const BindPhoneDirective = {
+  inserted(el, binding, vNode) {
+    const { value } = binding
+    const vm = vNode.context
+
+    const clickHandler = async (event) => {
+      // event.isProgrammaticClick = false
+      event.stopPropagation()
+      event.preventDefault()
+      const { props = {}, pass, bound, close, next } = value
+      /**
+       * props: 接受的传参
+       * pass: 无需校验是否绑定即可进行下一步,场景:金刚区(部分产品需要绑定手机号、部分产品不需要绑定手机号)
+       * next: 绑定过手机号的下一步操作(必传)
+       * close: 关闭弹框操作
+       * bound:弹框绑定完手机号回调操作(大多数场景与next方法逻辑一致,部分场景有额外需求的会不一致,当绑定完成回调与next不一致时,需要传入bound方法)
+       */
+      console.log(
+        `pass: ${pass}, bound: ${bound},next: ${next}, props: ${JSON.stringify(
+          props
+        )}`
+      )
+
+      // if (event.isProgrammaticClick) {
+      //   event.isProgrammaticClick = false // 重置标志
+      //   return // 阻止递归
+      // }
+
+      // 无需校验/无需绑定手机号
+      if (typeof pass === 'function') {
+        pass()
+        return
+      }
+
+      // 从cookie中获取是否绑定过手机号
+      const needPhoneBound = Cookies.get('EXPERIENCESIGN') === 'experiencing'
+      // 绑定成功cookie里塞的值
+      const alreadyBind = Cookies.get('EXPERIENCE')
+      // 已绑定过手机号
+      if (!needPhoneBound) {
+        if (typeof next === 'function') {
+          next()
+        } else {
+          // event.stopImmediatePropagation()
+          // event.target.dispatchEvent(new Event('click', { bubbles: true }))
+          // event.isProgrammaticClick = true // 设置标志
+          // event.target.click() // 触发不了
+        }
+      } else {
+        if (alreadyBind) {
+          return next?.()
+        }
+        props.visible = true
+        // 未绑定过手机号
+        vm.$bindPhoneDialog({
+          props: props,
+          slots: {},
+          on: {
+            bound: () => {
+              // 绑定完清除cookie
+              Cookies.remove('EXPERIENCESIGN')
+              // 绑定成功埋点
+              try {
+                window.__EasyJTrack.addTrack(props.name, {
+                  break_data: 'abtest',
+                  source: props.name + '-绑定成功'
+                })
+              } catch (error) {}
+
+              if (bound && typeof bound === 'function') {
+                bound()
+              } else {
+                next?.()
+              }
+            },
+            close: () => {
+              if (typeof close === 'function') {
+                close()
+              } else {
+                vm.$toast('请先绑定手机号')
+              }
+            }
+          }
+        })
+      }
+    }
+    // 绑定主点击事件
+    el.addEventListener('click', clickHandler, true)
+    el._clickHandler = clickHandler
+  },
+  unbind(el) {
+    el.removeEventListener('click', el._clickHandler)
+  }
+}
+
+export default BindPhoneDirective

+ 52 - 0
plugins/bind-phone-mobile/src/utils/plugins/directive-bind-phone.js

@@ -0,0 +1,52 @@
+import BindPhoneDialog from '../../components/BindPhoneDialog.vue'
+import BindPhoneDirective from '../../utils/directives/bind-phone.js'
+import '../../assets/style.css'
+
+const DirectiveBindPhonePlugin = {
+  install(Vue) {
+    // 注册全局组件
+    Vue.component('bind-phone-dialog', BindPhoneDialog)
+    // 注册全局指令
+    Vue.directive('bound-phone', BindPhoneDirective)
+
+    const DialogConstructor = Vue.extend(BindPhoneDialog)
+    Vue.prototype.$bindPhoneDialog = function (options) {
+      options.props.visible = true
+      const instance = new DialogConstructor({
+        propsData: options.props
+      })
+      if (options.on) {
+        Object.keys(options.on).forEach((event) => {
+          instance.$on(event, options.on[event])
+        })
+      }
+      // 支持插槽内容
+      if (options.slots) {
+        Object.keys(options.slots).forEach((slotName) => {
+          instance.$slots[slotName] = options.slots[slotName]
+        })
+      }
+      instance.$mount()
+      document.body.appendChild(instance.$el)
+      instance.visible = true
+      // 添加销毁钩子
+      instance.$on('close', () => {
+        // 关闭动画结束后再销毁
+        setTimeout(() => {
+          instance.$el.parentNode.removeChild(instance.$el)
+          instance.$destroy()
+        }, 300)
+      })
+      // 弹框弹起时埋点abtest
+      try {
+        window.__EasyJTrack.addTrack(options.props.name, {
+          break_data: 'abtest',
+          source: options.props.name
+        })
+      } catch (error) {}
+      return instance
+    }
+  }
+}
+
+export default DirectiveBindPhonePlugin

+ 97 - 0
plugins/bind-phone-mobile/src/utils/plugins/prototype-bind-phone.js

@@ -0,0 +1,97 @@
+import BindPhoneDialog from '../../components/BindPhoneDialog.vue'
+import '../../assets/style.css'
+import Cookies from 'js-cookie'
+
+const PrototypeBindPhonePlugin = {
+  install(Vue) {
+    // 注册全局组件
+    Vue.component('bind-phone-dialog', BindPhoneDialog)
+
+    // 创建绑定手机号弹框实例的函数封装
+    function createBindPhoneDialog(instance, options) {
+      if (options.on) {
+        Object.keys(options.on).forEach((event) => {
+          instance.$on(event, options.on[event])
+        })
+      }
+
+      instance.$mount()
+      document.body.appendChild(instance.$el)
+      instance.visible = true
+      return instance
+    }
+
+    Vue.prototype.$testBindPhone = function (options) {
+      const { props = {}, slots, pass, bound, next, close } = options
+      props.visible = true
+      const needPhoneBound = Cookies.get('EXPERIENCESIGN') === 'experiencing'
+      // 绑定成功cookie里塞的值
+      const alreadyBind = Cookies.get('EXPERIENCE')
+      if (typeof pass === 'function') {
+        pass()
+        return
+      }
+      if (needPhoneBound) {
+        if (alreadyBind) {
+          return next?.()
+        }
+        const DialogConstructor = Vue.extend(BindPhoneDialog)
+        const instance = new DialogConstructor({ propsData: props })
+
+        // 封装插槽处理逻辑
+        if (slots) {
+          Object.entries(slots).forEach(([slotName, slotContent]) => {
+            instance.$slots[slotName] = slotContent
+          })
+        }
+        options.on = {
+          bound: () => {
+            if (bound && typeof bound === 'function') {
+              bound()
+            } else {
+              typeof next === 'function' && next()
+            }
+            // 绑定完清除cookie
+            Cookies.remove('EXPERIENCESIGN')
+            // 绑定成功埋点
+            try {
+              window.__EasyJTrack.addTrack(props.name, {
+                break_data: 'abtest',
+                source: props.name + '-绑定成功'
+              })
+            } catch (error) {}
+          },
+          close: () => {
+            if (typeof close === 'function') {
+              close()
+            } else {
+              Vue.prototype.$toast('请先绑定手机号')
+            }
+          }
+        }
+        // 创建对话框实例
+        const dialog = createBindPhoneDialog(instance, options)
+        // 埋点
+        if (window.__EasyJTrack) {
+          window.__EasyJTrack.addTrack(props.name, {
+            break_data: 'abtest',
+            source: props.name
+          })
+        }
+        // 添加销毁钩子
+        dialog.$on('close', () => {
+          // 关闭动画结束后再销毁
+          setTimeout(() => {
+            dialog.$el.parentNode.removeChild(dialog.$el)
+            dialog.$destroy()
+          }, 300)
+        })
+        return dialog
+      } else {
+        next?.()
+      }
+    }
+  }
+}
+
+export default PrototypeBindPhonePlugin

+ 5 - 0
plugins/bind-phone-mobile/src/utils/prototype/env.js

@@ -0,0 +1,5 @@
+import Vue from 'vue'
+import { env, envs } from './platform'
+
+Vue.prototype.$env = env
+Vue.prototype.$envs = envs

+ 106 - 0
plugins/bind-phone-mobile/src/utils/prototype/platform.js

@@ -0,0 +1,106 @@
+const ua = navigator.userAgent
+const hostname = location.hostname.toLowerCase()
+
+// 在安卓或者ios中
+export const androidOrIOS = function () {
+  const u = ua.toLowerCase()
+  let agent = ''
+  if (/iphone|ipod|ipad|ios/.test(u)) {
+    agent = 'ios'
+  } else {
+    agent = 'android'
+  }
+  return agent
+}
+
+/**
+ * 判断是否存在新版APP浏览器UA(webview合一版本)
+ * @returns {boolean}
+ */
+export const getIsInTheUnifyAppContainer = function () {
+  const u = ua.toLowerCase()
+  let inNewApp = false
+
+  if (u.includes('jianyuapp')) {
+    inNewApp = true
+  }
+  return inNewApp
+}
+
+/**
+ * 用于判断是否在APP容器内
+ * @returns {boolean}
+ */
+export const getIsInTheAppContainer = function () {
+  // 判断是否存在新版APP浏览器UA
+  let inApp = getIsInTheUnifyAppContainer()
+
+  if (inApp) {
+    return true
+  }
+
+  if (window.JyObj && window.JyObj.mock) {
+    return inApp
+  }
+  try {
+    if (androidOrIOS() === 'ios') {
+      const iniOSApp =
+        typeof window.webkit.messageHandlers.skipAppointTab.postMessage ===
+        'function'
+      inApp = iniOSApp
+    } else {
+      const inAndroidApp = typeof window.JyObj !== 'undefined'
+      inApp = inAndroidApp
+    }
+  } catch (e) {
+    console.warn(e)
+    inApp = false
+  }
+  return inApp
+}
+
+export function getIsH5HostName() {
+  return hostname.includes('h5')
+}
+
+// 判断是否是微信浏览器
+export const inWeiXinBrowser = ua.toLowerCase().includes('micromessenger')
+export const inWeiXinMiniApp = ua.toLowerCase().includes('miniprogram')
+const platformOS = androidOrIOS()
+const inApp = getIsInTheAppContainer()
+
+export function getPlatform() {
+  const h5host = getIsH5HostName()
+  if (inApp) {
+    return 'app'
+  } else if (h5host) {
+    return 'h5'
+  } else if (inWeiXinBrowser) {
+    return 'wx'
+  } else {
+    return 'h5'
+  }
+}
+
+// 存放基本变量的集合
+const _env = {
+  ua,
+  platformOS,
+  platform: getPlatform()
+}
+
+// 对基本变量扩展计算的集合
+const inH5 = _env.platform === 'h5'
+const _envs = {
+  inWX: _env.platform === 'wx',
+  inH5,
+  inWxMini: inWeiXinMiniApp,
+  inApp,
+  inUnifyApp: getIsInTheUnifyAppContainer(),
+  inAppOrH5: inApp || inH5,
+  inAndroid: _env.platformOS === 'android',
+  inIOS: _env.platformOS === 'ios'
+}
+
+export const env = _env
+export const envs = _envs

+ 64 - 0
plugins/bind-phone-mobile/src/utils/utils.js

@@ -0,0 +1,64 @@
+// URL路径是否包含前缀校验
+export const NotURLPrefixRegExp = /^\//
+export function getPic(link) {
+  if (NotURLPrefixRegExp.test(link)) {
+    return import.meta.env.VITE_APP_IMAGE_BASE + link
+  }
+  return link
+}
+
+/**
+ * 广告位响应值格式转换函数
+ * @param config 广告位响应值
+ * @returns {*}
+ */
+export function adConfigFormatter(config = {}) {
+  config = config || {}
+  const oExtend = config.o_extend || {}
+  return {
+    pic: getPic(config?.s_pic),
+    link: config?.s_link,
+    name: config?.s_picalt || config?.s_remark,
+    type: config?.o_extend?.linktype,
+    title: config?.s_remark,
+    iosHref: config?.o_extend?.iosHref
+      ? `https://${config?.o_extend?.iosHref}`
+      : '',
+    extend: {
+      width: config?.o_extend?.width,
+      height: config?.o_extend?.height,
+      type: config?.o_extend?.linktype,
+      power: oExtend?.power,
+      tab: oExtend?.tab
+    },
+    script: config?.s_script ? JSON.parse(config.s_script) : ''
+  }
+}
+
+/**
+ * 该函数用于将Px换算为Vw
+ * @param {string | number} px 设计图中元素尺寸
+ * @param {string} viewportUnit 转换后单位,默认vw
+ * @param {object} config  px2viewport配置项
+ * @param {number} config.viewportWidth 设计图尺寸
+ * @param {number} config.unitPrecision 转换后保留位数
+ * @returns {string} 转换后结果
+ */
+export function px2viewport(
+  px,
+  viewportUnit = 'vw',
+  config = {
+    viewportWidth: 375,
+    unitPrecision: 3
+  }
+) {
+  try {
+    return (
+      ((String(px).replace('px', '') / config.viewportWidth) * 100).toFixed(
+        config.unitPrecision
+      ) + viewportUnit
+    )
+  } catch (e) {
+    return ''
+  }
+}

+ 100 - 0
plugins/bind-phone-mobile/src/views/test.vue

@@ -0,0 +1,100 @@
+<template>
+  <div class="btn-group">
+    <div class="flex">
+      <button class="btn" @click="setCookie">生成cookie</button>
+      <button class="btn" @click="removeCookie">清除cookie</button>
+    </div>
+    <button class="btn" v-bound-phone="bindPhone()">
+      指令触发绑定弹框(校验cookie)
+    </button>
+    <button class="btn" @click="handle">手动触发绑定弹框(未校验cookie)</button>
+    <button class="btn" @click="globalFn">
+      全局方法触发绑定弹框(校验cookie)
+    </button>
+  </div>
+</template>
+
+<script>
+import Cookies from 'js-cookie'
+export default {
+  name: 'TestPage',
+  data() {
+    return {
+      visible: false
+    }
+  },
+  mounted() {
+    this.$testBindPhone({
+      props: {
+        name: '测试弹框-mounted触发'
+      },
+      next: () => {
+        this.$toast('next')
+      }
+    })
+  },
+  methods: {
+    setCookie() {
+      Cookies.set('EXPERIENCESIGN', 'experiencing', { expires: 7, path: '' })
+    },
+    removeCookie() {
+      Cookies.remove('EXPERIENCESIGN')
+    },
+    bindPhone() {
+      return {
+        props: {
+          name: '测试弹框-指令触发'
+        },
+        next: () => {
+          console.log('指令触发-下一步')
+        }
+      }
+    },
+    // 手动触发绑定弹框
+    handle() {
+      this.$bindPhoneDialog({
+        props: {
+          name: '测试弹框-手动触发'
+        },
+        on: {
+          bound: () => {
+            this.$toast('bound')
+          },
+          next: () => {
+            this.$toast('next')
+          },
+          close: () => {
+            this.$toast('close')
+          }
+        }
+      })
+    },
+    globalFn() {
+      this.$testBindPhone({
+        props: {
+          visible: true,
+          name: '测试弹框-全局方法触发'
+        },
+        next: () => {
+          this.$toast('next')
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.btn-group {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  .btn {
+    margin: 16px;
+  }
+  .flex {
+    display: flex;
+    align-items: center;
+  }
+}
+</style>

+ 103 - 0
plugins/bind-phone-mobile/vite.config.js

@@ -0,0 +1,103 @@
+import { resolve } from 'node:path'
+
+import viteCompression from 'vite-plugin-compression'
+import { defineConfig, loadEnv, splitVendorChunkPlugin } from 'vite'
+import vue2 from '@vitejs/plugin-vue2'
+import { ViteEjsPlugin } from 'vite-plugin-ejs'
+import { viteExternalsPlugin } from 'vite-plugin-externals'
+import { visualizer } from 'rollup-plugin-visualizer'
+import eslintPlugin from '@nabla/vite-plugin-eslint'
+import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
+
+function getExternals(isDev) {
+  if (isDev) {
+    // serve
+    return {}
+  }
+  // build
+  return {
+    // 'vue': 'Vue',
+    // 'vue-router': 'VueRouter',
+    // 'vuex': 'Vuex',
+    axios: 'axios',
+    // 'vant': 'vant',
+    'js-cookie': 'Cookies'
+  }
+}
+
+export default defineConfig(({ mode, command }) => {
+  const env = loadEnv(mode, process.cwd())
+  return {
+    base: env.VITE_APP_BASE_PUBLIC,
+    build: {
+      sourcemap: true,
+      emptyOutDir: true,
+      lib: {
+        entry: './src/entry.js', // 入口文件
+        name: 'TestBindPhone', // 库的全局变量名
+        fileName: 'jy-bind-phone' // 输出的文件名
+      },
+      rollupOptions: {
+        // 确保外部化处理 Vue,避免将 Vue等 打包进库
+        external: ['vue', 'vant', 'vuex', 'vue-router'],
+        output: {
+          globals: {
+            vue: 'Vue'
+          }
+        }
+      },
+      target: 'es2015' // 指定目标语法版本
+    },
+    plugins: [
+      splitVendorChunkPlugin(),
+      vue2(),
+      ViteEjsPlugin({
+        assets: {
+          version: Date.now()
+        }
+      }),
+      eslintPlugin(),
+      // 不打包的库(外部需要通过cdn引入)
+      viteExternalsPlugin(getExternals(command)),
+      viteCompression({
+        threshold: 1024
+      }),
+      visualizer(),
+      cssInjectedByJsPlugin()
+    ],
+    resolve: {
+      alias: [
+        {
+          find: /^~/,
+          replacement: ''
+        },
+        {
+          find: '@',
+          replacement: resolve(__dirname, 'src')
+        }
+      ],
+      extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
+    },
+    server: {
+      host: '0.0.0.0',
+      port: 8080,
+      proxy: {
+        // 接口解密iframe
+        '^/page_decrypt': {
+          target: 'https://jybx-webtest.jydev.jianyu360.com',
+          changeOrigin: true
+        },
+        '/jyapi': {
+          target: 'https://app2-jytest.jydev.jianyu360.com',
+          changeOrigin: true,
+          rewrite: (path) => path.replace(/^\/jyapi/, '')
+        },
+        '/api': {
+          target: 'https://app2-jytest.jydev.jianyu360.com',
+          changeOrigin: true,
+          rewrite: (path) => path.replace(/^\/api/, '')
+        }
+      }
+    }
+  }
+})

+ 32 - 17
plugins/gift-friends/src/components/GiftSubmitDialog.vue

@@ -339,25 +339,40 @@ export default {
       })
     },
     addPerson() {
-      this.personList.map((item, index) => {
-        return this.$refs[`form${index}`][0].validate((valid) => {
-          if (!valid) {
-            this.$toast('请先完成当前未完善的手机号和时长信息')
-            this.addFormStyle(index, !valid)
-          }
-          else {
-            this.personList.push({
-              phone: '',
-              monthnum: '',
-              status: '',
-              error: '',
-              phoneValid: false,
-              monthnumValid: false
-            })
-            this.updateFormValidity()
-          }
+      const validationPromises = this.personList.map((item, index) => {
+        return new Promise((resolve) => {
+          this.$refs[`form${index}`][0].validate((valid) => {
+            // 将 index 和 valid 封装成对象传递
+            resolve({ index, valid })
+          })
         })
       })
+      Promise.all(validationPromises).then((results) => {
+        // 判断是否有未完成的信息
+        const hasIncomplete = results.some(({ valid }) => !valid)
+
+        if (hasIncomplete) {
+          // 处理未完成信息的逻辑
+          results.forEach(({ index, valid }) => {
+            if (!valid) {
+              // 对未完成信息的表单添加样式
+              this.addFormStyle(index, !valid)
+            }
+          })
+          return this.$toast('请先完成当前未完善的手机号和时长信息')
+        }
+        else {
+          this.personList.push({
+            phone: '',
+            monthnum: '',
+            status: '',
+            error: '',
+            phoneValid: false,
+            monthnumValid: false
+          })
+          this.updateFormValidity()
+        }
+      })
     },
     /**
      * 重置人员列表

Fișier diff suprimat deoarece este prea mare
+ 257 - 184
pnpm-lock.yaml


Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff