浏览代码

feat:新增移动端绑定手机号插件

yangfeng 5 月之前
父节点
当前提交
98594f6aed
共有 34 个文件被更改,包括 2079 次插入113 次删除
  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. 93 0
      plugins/bind-phone-mobile/README.md
  10. 101 0
      plugins/bind-phone-mobile/index.html
  11. 51 0
      plugins/bind-phone-mobile/package.json
  12. 30 0
      plugins/bind-phone-mobile/postcss.config.js
  13. 100 0
      plugins/bind-phone-mobile/public/decrypt-js.html
  14. 二进制
      plugins/bind-phone-mobile/public/favicon.ico
  15. 45 0
      plugins/bind-phone-mobile/src/App.vue
  16. 29 0
      plugins/bind-phone-mobile/src/api/api.js
  17. 4 0
      plugins/bind-phone-mobile/src/api/index.js
  18. 51 0
      plugins/bind-phone-mobile/src/api/interceptors.js
  19. 5 0
      plugins/bind-phone-mobile/src/api/service.js
  20. 106 0
      plugins/bind-phone-mobile/src/assets/style.css
  21. 373 0
      plugins/bind-phone-mobile/src/components/BindPhoneDialog.vue
  22. 43 0
      plugins/bind-phone-mobile/src/entry.js
  23. 6 0
      plugins/bind-phone-mobile/src/index.js
  24. 27 0
      plugins/bind-phone-mobile/src/main.js
  25. 25 0
      plugins/bind-phone-mobile/src/router/index.js
  26. 318 0
      plugins/bind-phone-mobile/src/utils/appFn.js
  27. 65 0
      plugins/bind-phone-mobile/src/utils/directives/bind-phone.js
  28. 36 0
      plugins/bind-phone-mobile/src/utils/plugins/index.js
  29. 5 0
      plugins/bind-phone-mobile/src/utils/prototype/env.js
  30. 106 0
      plugins/bind-phone-mobile/src/utils/prototype/platform.js
  31. 64 0
      plugins/bind-phone-mobile/src/utils/utils.js
  32. 56 0
      plugins/bind-phone-mobile/src/views/test.vue
  33. 103 0
      plugins/bind-phone-mobile/vite.config.js
  34. 149 113
      pnpm-lock.yaml

+ 1 - 0
apps/mobile/package.json

@@ -15,6 +15,7 @@
     "@jy/data-models": "workspace:^",
     "@jy/data-models": "workspace:^",
     "@jy/util": "workspace:^",
     "@jy/util": "workspace:^",
     "@jy/vue-anti": "workspace:^",
     "@jy/vue-anti": "workspace:^",
+    "@jy/bind-phone": "workspace:^",
     "@sentry/vue": "^7.64.0",
     "@sentry/vue": "^7.64.0",
     "@tinymce/tinymce-vue": "^3.2.8",
     "@tinymce/tinymce-vue": "^3.2.8",
     "aliyun_numberauthsdk_web": "^2.1.9",
     "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/

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

@@ -0,0 +1,93 @@
+# jy-mobile
+
+## 待办
+
+1. 使用 swiper 替换 vue-awesome-swiper
+2. 提取 lodash
+3.
+
+## 移动端剑鱼项目 GitLab
+
+http://192.168.3.207:8929/efe/jy-mobile
+
+!原 Gogs 项目已启用分支保护,不接受代码提交。
+
+## 开发规范流程
+
+1. 注册 GitLab
+2. 访问 jy-mobile 项目 dev1.1
+3. Fork 项目或者新建 feature/xxxx 功能分支
+4. 完成需求后通过 GitLab 申请合并需求
+5. 等待 CI Lint、代码评审
+6. 通过后合入 dev1.1 分支
+
+因加入 git-hook,需删除 .git/hook、node_modules 重新安装依赖。
+
+### 代码约束
+
+http://192.168.3.11:10081/doc-serve/page_site/standard/
+
+### 组件预览
+
+http://192.168.3.11:10081/doc-serve/page_docs/
+
+## 目录结构
+
+```
+.
+├── config          // UI预览工具配置
+│   └── storybook
+├── package.json
+├── public
+│   ├── favicon.ico
+│   └── index.html
+├── src
+│   ├── api
+│   ├── assets
+│   ├── components  // 项目业务组件
+│   ├── router
+│   ├── store
+│   ├── stories     // UI 预览示例
+│   ├── ui          // 可提取公共UI组件
+│   ├── utils
+│   └── views
+├── vue.config.js
+└── yarn.lock
+```
+
+## UI 预览结构规范
+
+```
+├── UI/我的组件
+│   ├── index.vue
+│   ├── index.stories.js   // 基础 js or jsx 组件示例
+│   └── index.stories.mdx  // 用于更自由的 markdown jsx 组件示例
+```
+
+## Project setup
+
+```
+yarn install
+```
+
+### Compiles and hot-reloads for development
+
+```
+yarn serve
+```
+
+### Compiles and minifies for production
+
+```
+yarn build
+```
+
+### Lints and fixes files
+
+```
+yarn lint
+```
+
+### Customize configuration
+
+See [Configuration Reference](https://cli.vuejs.org/config/).

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

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

@@ -0,0 +1,51 @@
+{
+  "name": "@jy/bind-phone",
+  "version": "0.1.1",
+  "private": true,
+  "main": "src/index.js",
+  "files": [
+    "src"
+  ],
+  "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": {
+    "@jy/data-models": "workspace:^",
+    "@jy/util": "workspace:^",
+    "@jy/vue-anti": "workspace:^",
+    "js-cookie": "^3.0.1",
+    "lodash": "^4.17.21",
+    "qs": "^6.11.2",
+    "vant": "2.12.44",
+    "vite-plugin-css-injected-by-js": "^3.1.0",
+    "vuex": "^3.6.2"
+  },
+  "devDependencies": {
+    "@jonny1994/postcss-px-to-viewport": "^1.1.0",
+    "@nabla/vite-plugin-eslint": "^2.0.2",
+    "@rushstack/eslint-patch": "^1.1.0",
+    "@sentry/vite-plugin": "^2.21.1",
+    "@unocss/transformer-variant-group": "^0.58.5",
+    "@vitejs/plugin-legacy": "^4.0.4",
+    "@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"
+  }
+}

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

+ 100 - 0
plugins/bind-phone-mobile/public/decrypt-js.html

@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title></title>
+</head>
+<body>
+    <script src='https://cdn-common.jianyu360.com/cdn/lib/jsencrypt/3.3.2/jsencrypt.min.js'></script>
+    <script>
+        const config = {
+            plainKey: '', // rsa解密后的key
+            privateKey: `-----BEGIN PRIVATE KEY-----
+                MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOhM0pNOfGeiBr+t
+                nunphCHReY3RiS4Fuc2nD3cbjKNdLezeViGmsZwHsb2SVUb6rpPHyX0+3xjXYn//
+                n39/Q8uPjWRA332TtN8MDEkSR2HMbn8ufRRt2TnlfsFDFTgBywSP7cwd0CiEdvBX
+                5w8Jifc9VbedwbeplBWyDeLLqjRjAgMBAAECgYB4es+EAuLWxNwHMb8Hxkr3VzNZ
+                8GDbc7DIDmsg9TLdz4fwH+hAD7pyGDOBBJIh/AXrM2U3BhKjSaIWjLdmYtT/kzg8
+                BxQDr9YoO7u2jvTcEE+/6p2YugYX/ngpinawFJqyM+N7Or8yRABaw6Aq8VuKtv6p
+                980Y2BBVVYn+/KorYQJBAP+9lu8iolzKRzJrFt/rosdWkOpNg5ujcSCwbxhYnYC0
+                UY85sPLsMvnLgegkpO8jocSAt586BmcsA+Q9o97qVCkCQQDoiSVegtOvG3U0mNlN
+                rCVpPEL22s9Kkwps3ZCdTl3VtUtNiyfhE8rbw/qOGti3VxMCRhpKi9hTIgeq13UG
+                67WrAkEA/WQ1c5XGd9f4eU1AKffInmf4SB8rgn+L7I7EVMQgstB3a0kHOXqs+3IX
+                shL01PliJFhBF+QfSgSDipdEke9uGQJBAOcw46xxmhDw1bizdulYi+Fy/oj7xzi3
+                tJfEObGMZpLBKtsvzThkOz4APS3n1yuBMO8Dz8PqAeu1W7YpfLqiwv0CQF68N244
+                dFebDSoZLl1hbCExpbtC7SDBpYxlIVNVqwN7ymr+Z0rIcAMVv5Ldp/bJEWaXJs9C
+                0sPCBpjDnyK9Z04=
+                -----END PRIVATE KEY-----`
+        }
+    </script>
+
+    <script>
+        var decryptTools = {
+            // rsa解密
+            rsaDecrypt: function (cipherText, privateKey) {
+                // 解密
+                var decrypt = new JSEncrypt()//创建解密对象实例
+                decrypt.setPrivateKey(privateKey)//设置秘钥
+                var uncrypted = decrypt.decrypt(cipherText)//解密之前拿公钥加密的内容
+                return uncrypted
+            },
+            // AES解密
+            async AESDecrypt(content, base64Key) {
+                const key = new TextEncoder().encode(base64Key)
+                const encryptedBase64 = content;
+                const encryptedData = Uint8Array.from(window.atob(encryptedBase64), c => c.charCodeAt(0));
+
+                const decryptData = async () => {
+                    const iv = encryptedData.slice(0, 16);
+                    const ciphertext = encryptedData.slice(16);
+
+                    const aesKey = await crypto.subtle.importKey("raw", key, {name: "AES-CTR"}, false, ["encrypt", "decrypt"]);
+
+                    const decryptedData = await crypto.subtle.decrypt({name: "AES-CTR", counter: iv, length: 128}, aesKey, ciphertext);
+                    const decryptedText = new TextDecoder().decode(decryptedData);
+
+                    return {
+                        value: decryptedText
+                    }
+                };
+
+                const result = await decryptData()
+                return result
+            }
+        }
+
+        window.addEventListener('message', async (e) => {
+            if (e.data.type !== 'decrypt') {
+                return
+            }
+            if (window === e.source) {
+                return
+            }
+
+            var base64Key = e.data.base64Key
+            var cipherText = e.data.cipherText
+            // 1. 先解密base64Key
+            var plainKey = decryptTools.rsaDecrypt(base64Key, config.privateKey)
+            config.plainKey = plainKey
+            // 2. 再用key解密cipherText
+            var plainText = await decryptTools.AESDecrypt(cipherText, plainKey)
+
+            const result = {
+                ...e.data,
+                plainKey: plainKey,
+                plainText: plainText.value,
+            }
+            sendPostMessage(e, result)
+        })
+        function sendPostMessage(e, result) {
+            const win = e.source
+            var payload = {
+                ...result,
+                type: 'after-decrypt'
+            }
+            window.parent.postMessage(payload, payload.fromOrigin)
+        }
+    </script>
+</body>
+</html>

二进制
plugins/bind-phone-mobile/public/favicon.ico


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

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

@@ -0,0 +1,106 @@
+.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;
+  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;
+  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;
+}
+
+.reveal-box {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+.reveal-box .van-image,
+.reveal-box .van-icon__image {
+  width: 100%;
+  height: 100%;
+}
+.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;
+}

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

@@ -0,0 +1,373 @@
+<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
+        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: ''
+          }
+        }
+      }
+    }
+  },
+  mounted() {
+    if (!this.adCode) {
+      const code = this.$envs.inWX
+        ? 'wx-bind-phone-dialog'
+        : 'app-bind-phone-dialog'
+      this.getAd([code])
+    } else {
+      this.getAd([this.adCode])
+    }
+  },
+  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')
+      } 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>

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

@@ -0,0 +1,43 @@
+/**
+ * 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
+    return instance
+  }
+}
+
+export default {
+  install
+}

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

@@ -0,0 +1,6 @@
+import BindPhoneDialogPlugin from './utils/plugins/index.js'
+import BindPhoneDirective from './utils/directives/bind-phone'
+import BindPhoneDialog from './components/BindPhoneDialog.vue'
+import './assets/style.css'
+
+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)
+  }
+}

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

@@ -0,0 +1,65 @@
+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 isPhoneBound = Cookies.get('EXPERIENCESIGN')
+      // 无需绑定手机号
+      if (typeof pass === 'function') {
+        pass()
+        return
+      }
+      // 已绑定过手机号
+      if (isPhoneBound) {
+        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')
+            },
+            close: () => {
+              if (typeof close === 'function') {
+                close()
+              } else {
+                vm.$toast('请先绑定手机号')
+              }
+            }
+          }
+        })
+      }
+    })
+  }
+}
+
+export default BindPhoneDirective

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

@@ -0,0 +1,36 @@
+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
+      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 ''
+  }
+}

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

@@ -0,0 +1,56 @@
+<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: {},
+        next: () => {
+          console.log('1111')
+        }
+      }
+    },
+    handle() {
+      this.$bindPhoneDialog({
+        props: {
+          visible: true
+        },
+        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', // 输出的文件名
+        formats: ['umd'] // 打包格式
+      },
+      rollupOptions: {
+        // 确保外部化处理 Vue,避免将 Vue等 打包进库
+        external: ['vue', 'vant', 'vuex', 'vue-router'],
+        output: {
+          globals: {
+            vue: 'Vue'
+          }
+        }
+      }
+    },
+    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/, '')
+        }
+      }
+    }
+  }
+})

文件差异内容过多而无法显示
+ 149 - 113
pnpm-lock.yaml


部分文件因为文件数量过多而无法显示