Browse Source

Merge branch 'dev/v1.0.94_yf' of jianyu/web into feature/v1.0.94

yangfeng 5 tháng trước cách đây
mục cha
commit
33e98e68a4
32 tập tin đã thay đổi với 2331 bổ sung311 xóa
  1. 1 0
      apps/mobile/package.json
  2. 5 0
      plugins/bind-phone-mobile/.browserslistrc
  3. 5 0
      plugins/bind-phone-mobile/.editorconfig
  4. 7 0
      plugins/bind-phone-mobile/.env.development
  5. 7 0
      plugins/bind-phone-mobile/.env.production
  6. 16 0
      plugins/bind-phone-mobile/.eslintignore
  7. 42 0
      plugins/bind-phone-mobile/.gitignore
  8. 5 0
      plugins/bind-phone-mobile/.npmrc
  9. 157 0
      plugins/bind-phone-mobile/README.md
  10. 101 0
      plugins/bind-phone-mobile/index.html
  11. 46 0
      plugins/bind-phone-mobile/package.json
  12. 30 0
      plugins/bind-phone-mobile/postcss.config.js
  13. 45 0
      plugins/bind-phone-mobile/src/App.vue
  14. 29 0
      plugins/bind-phone-mobile/src/api/api.js
  15. 4 0
      plugins/bind-phone-mobile/src/api/index.js
  16. 51 0
      plugins/bind-phone-mobile/src/api/interceptors.js
  17. 5 0
      plugins/bind-phone-mobile/src/api/service.js
  18. 119 0
      plugins/bind-phone-mobile/src/assets/style.css
  19. 382 0
      plugins/bind-phone-mobile/src/components/BindPhoneDialog.vue
  20. 52 0
      plugins/bind-phone-mobile/src/entry.js
  21. 15 0
      plugins/bind-phone-mobile/src/index.js
  22. 27 0
      plugins/bind-phone-mobile/src/main.js
  23. 25 0
      plugins/bind-phone-mobile/src/router/index.js
  24. 318 0
      plugins/bind-phone-mobile/src/utils/appFn.js
  25. 72 0
      plugins/bind-phone-mobile/src/utils/directives/bind-phone.js
  26. 43 0
      plugins/bind-phone-mobile/src/utils/plugins/index.js
  27. 5 0
      plugins/bind-phone-mobile/src/utils/prototype/env.js
  28. 106 0
      plugins/bind-phone-mobile/src/utils/prototype/platform.js
  29. 64 0
      plugins/bind-phone-mobile/src/utils/utils.js
  30. 60 0
      plugins/bind-phone-mobile/src/views/test.vue
  31. 103 0
      plugins/bind-phone-mobile/vite.config.js
  32. 384 311
      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/bind-phone": "workspace:^",
     "@sentry/vue": "^7.64.0",
     "@tinymce/tinymce-vue": "^3.2.8",
     "aliyun_numberauthsdk_web": "^2.1.9",

+ 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/

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

@@ -0,0 +1,157 @@
+# @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 { BindPhoneDirective, BindPhoneDialogPlugin } from '@jy/plugin-bind-phone'
+Vue.use(BindPhoneDialogPlugin)
+Vue.directive('bound-phone', BindPhoneDirective)
+```
+
+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 BindPhone from '@jy/plugin-bind-phone'
+Vue.use(BindPhone)
+```
+
+### 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.$bindPhoneDialog({
+        props: {
+          name: '触发位置名称',
+          visible: true // 显示弹框
+        },
+        next: () => {
+          <!-- 绑定成功/已绑定 下一步操作 -->
+        },
+        close: () => {
+          <!-- 关闭弹框 -->
+        }
+      })
+    }
+  }
+}
+</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.4",
+  "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
+})

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

@@ -0,0 +1,119 @@
+.bind-phone-dialog {
+  width: 343px !important;
+  border-radius: 16px !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;
+}

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

@@ -0,0 +1,382 @@
+<template>
+  <van-dialog
+    class="bind-phone-dialog"
+    v-model="show"
+    :show-confirm-button="false"
+  >
+    <div class="bind-header">
+      <van-image
+        :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 Vue from 'vue'
+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: ''
+          }
+        }
+      }
+    }
+  },
+  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])
+    }
+  },
+  mounted() {
+    try {
+      setTimeout(() => {
+        this.$refs.phoneRef.focus()
+      }, 500)
+    } catch (error) {}
+  },
+  watch: {
+    visible(val) {
+      console.log(val, 'visible')
+    }
+  },
+  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 {
+        error_code: code,
+        error_msg: msg,
+        data = {}
+      } = await ajaxGetAD({ codes })
+      if (code === 0 && data) {
+        const info = adConfigFormatter(Object.values(data).flat()[0])
+        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) {
+        // 绑定成功
+        this.$emit('bound')
+        this.show = false
+      } 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>

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

@@ -0,0 +1,52 @@
+/**
+ * description: 打包入口文件,可输出js插件供外部html调用
+ */
+
+import BindPhoneDialog from './components/BindPhoneDialog.vue'
+import BoundPhoneDirective from './utils/directives/bind-phone.js'
+import './utils/prototype/env.js'
+import './assets/style.css'
+
+const install = (Vue) => {
+  // 注册全局组件
+  Vue.component('bind-phone-dialog', BindPhoneDialog)
+
+  // 注册全局指令
+  Vue.directive('bound-phone', BoundPhoneDirective)
+
+  // 创建弹窗实例
+  const ModalConstructor = Vue.extend(BindPhoneDialog)
+  Vue.prototype.$bindPhoneDialog = function (options) {
+    const instance = new ModalConstructor({
+      propsData: options.props
+    })
+    if (options.on) {
+      Object.keys(options.on).forEach((event) => {
+        instance.$on(event, options.on[event], (instance.visible = false))
+      })
+    }
+    // 支持插槽内容
+    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
+    // 弹框弹起时埋点abtest
+    try {
+      window.__EasyJTrack.addTrack(options.props.name, {
+        break_data: 'abtest',
+        source: options.props.name
+      })
+    } catch (error) {}
+    return instance
+  }
+}
+
+export default {
+  install,
+  BindPhoneDialog,
+  BoundPhoneDirective
+}

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

@@ -0,0 +1,15 @@
+import BindPhoneDialogPlugin from './utils/plugins/index.js'
+import BindPhoneDirective from './utils/directives/bind-phone'
+import BindPhoneDialog from './components/BindPhoneDialog.vue'
+import './assets/style.css'
+// import install from './entry.js'
+
+// const BindPhonePlugin = {
+//   install,
+//   BindPhoneDialogPlugin,
+//   BindPhoneDirective,
+//   BindPhoneDialog
+// }
+// export default install
+
+export { BindPhoneDialogPlugin, BindPhoneDirective, BindPhoneDialog }

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

@@ -0,0 +1,27 @@
+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 BindPhoneDialogPlugin from './utils/plugins/index.js'
+import './utils/prototype/env.js'
+
+Vue.use(Toast).use(Lazyload).use(Dialog).use(BindPhoneDialogPlugin)
+
+// 设置默认 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)
+  }
+}

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

@@ -0,0 +1,72 @@
+import Cookies from 'js-cookie'
+
+const BindPhoneDirective = {
+  inserted(el, binding, vNode) {
+    const { value } = binding
+    const vm = vNode.context
+    /**
+     * props: 接受的传参
+     * pass: 无需校验是否绑定即可进行下一步,场景:金刚区(部分产品需要绑定手机号、部分产品不需要绑定手机号)
+     * next: 绑定过手机号的下一步操作(必传)
+     * close: 关闭弹框操作
+     * bound:弹框绑定完手机号回调操作(大多数场景与next方法逻辑一致,部分场景有额外需求的会不一致,当绑定完成回调与next不一致时,需要传入bound方法)
+     */
+
+    el.addEventListener('click', async (event) => {
+      const { props = {}, pass, bound, close, next } = value
+      console.log(
+        `pass: ${pass}, bound: ${bound},next: ${next}, props: ${JSON.stringify(
+          props
+        )}`
+      )
+
+      event.stopPropagation()
+      event.preventDefault()
+      // 从cookie中获取是否绑定过手机号
+      const needPhoneBound = Cookies.get('EXPERIENCESIGN')
+      // 无需绑定手机号
+      if (typeof pass === 'function') {
+        pass()
+        return
+      }
+      // 已绑定过手机号
+      if (!needPhoneBound) {
+        typeof next === 'function' && next()
+      } else {
+        props.visible = true
+        // 未绑定过手机号
+        vm.$bindPhoneDialog({
+          props: props,
+          slots: {},
+          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 {
+                vm.$toast('请先绑定手机号')
+              }
+            }
+          }
+        })
+      }
+    })
+  }
+}
+
+export default BindPhoneDirective

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

@@ -0,0 +1,43 @@
+import BindPhoneDialog from '../../components/BindPhoneDialog.vue'
+import BindPhoneDirective from '../../utils/directives/bind-phone.js'
+import '../../assets/style.css'
+
+const BindPhoneDialogPlugin = {
+  install(Vue) {
+    // 注册全局组件
+    Vue.component('bind-phone-dialog', BindPhoneDialog)
+    // 注册全局指令
+    Vue.directive('bound-phone', BindPhoneDirective)
+
+    const DialogConstructor = Vue.extend(BindPhoneDialog)
+    Vue.prototype.$bindPhoneDialog = function (options) {
+      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
+      // 弹框弹起时埋点abtest
+      try {
+        window.__EasyJTrack.addTrack(options.props.name, {
+          break_data: 'abtest',
+          source: options.props.name
+        })
+      } catch (error) {}
+      return instance
+    }
+  }
+}
+
+export default BindPhoneDialogPlugin

+ 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 ''
+  }
+}

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

@@ -0,0 +1,60 @@
+<template>
+  <div class="btn-group">
+    <button class="btn" v-bound-phone="bindPhone()">指令触发绑定弹框</button>
+    <button class="btn" @click="handle">手动触发绑定弹框</button>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'TestPage',
+  data() {
+    return {
+      visible: false
+    }
+  },
+  methods: {
+    bindPhone() {
+      return {
+        props: {
+          name: '测试弹框-指令触发'
+        },
+        next: () => {
+          console.log('指令触发-下一步')
+        }
+      }
+    },
+    // 手动触发绑定弹框
+    handle() {
+      this.$bindPhoneDialog({
+        props: {
+          visible: true,
+          name: '测试弹框-手动触发'
+        },
+        on: {
+          bound: () => {
+            this.$toast('bound')
+          },
+          next: () => {
+            this.$toast('next')
+          },
+          close: () => {
+            this.$toast('close')
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.btn-group {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  .btn {
+    margin: 16px;
+  }
+}
+</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: 'BindPhone', // 库的全局变量名
+        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/, '')
+        }
+      }
+    }
+  }
+})

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 384 - 311
pnpm-lock.yaml


Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác