소스 검색

feat: 赠送好友插件

Signed-off-by: tangshizhe <48740614+tangshizhe@users.noreply.github.com>
tangshizhe 4 달 전
부모
커밋
7283531ca5
39개의 변경된 파일2950개의 추가작업 그리고 1개의 파일을 삭제
  1. 1 0
      apps/bigmember_pc/package.json
  2. 2 0
      apps/bigmember_pc/src/main.js
  3. 7 1
      apps/bigmember_pc/src/views/workspace/dashboard.vue
  4. 5 0
      plugins/gift-friends/.browserslistrc
  5. 5 0
      plugins/gift-friends/.editorconfig
  6. 7 0
      plugins/gift-friends/.env.development
  7. 7 0
      plugins/gift-friends/.env.production
  8. 16 0
      plugins/gift-friends/.eslintignore
  9. 42 0
      plugins/gift-friends/.gitignore
  10. 5 0
      plugins/gift-friends/.npmrc
  11. 157 0
      plugins/gift-friends/README.md
  12. 101 0
      plugins/gift-friends/index.html
  13. 45 0
      plugins/gift-friends/package.json
  14. 30 0
      plugins/gift-friends/postcss.config.js
  15. 47 0
      plugins/gift-friends/src/App.vue
  16. 56 0
      plugins/gift-friends/src/api/api.js
  17. 4 0
      plugins/gift-friends/src/api/index.js
  18. 52 0
      plugins/gift-friends/src/api/interceptors.js
  19. 5 0
      plugins/gift-friends/src/api/service.js
  20. BIN
      plugins/gift-friends/src/assets/images/icon-delete.png
  21. 32 0
      plugins/gift-friends/src/assets/style/_mixin.scss
  22. 35 0
      plugins/gift-friends/src/assets/style/_variables.scss
  23. 310 0
      plugins/gift-friends/src/assets/style/common.scss
  24. 48 0
      plugins/gift-friends/src/assets/style/dialog.css
  25. 207 0
      plugins/gift-friends/src/assets/style/gift.css
  26. 91 0
      plugins/gift-friends/src/assets/style/pic-icon.scss
  27. 353 0
      plugins/gift-friends/src/assets/style/reset-ele.scss
  28. 171 0
      plugins/gift-friends/src/components/Dialog.vue
  29. 622 0
      plugins/gift-friends/src/components/GiftSubmitDialog.vue
  30. 43 0
      plugins/gift-friends/src/components/toast/Toast.vue
  31. 58 0
      plugins/gift-friends/src/components/toast/index.js
  32. 48 0
      plugins/gift-friends/src/entry.js
  33. 15 0
      plugins/gift-friends/src/index.js
  34. 17 0
      plugins/gift-friends/src/main.js
  35. 25 0
      plugins/gift-friends/src/router/index.js
  36. 41 0
      plugins/gift-friends/src/utils/plugins/index.js
  37. 53 0
      plugins/gift-friends/src/views/test.vue
  38. 103 0
      plugins/gift-friends/vite.config.js
  39. 84 0
      pnpm-lock.yaml

+ 1 - 0
apps/bigmember_pc/package.json

@@ -18,6 +18,7 @@
     "@jianyu/reset.css": "~0.1.1",
     "@jy/data-models": "workspace:^",
     "@jy/pc-ui": "workspace:^",
+    "@jy/plugin-gift-friends": "workspace:^",
     "@jy/util": "workspace:^",
     "@jy/vue-anti": "workspace:^",
     "@sentry/vue": "^7.64.0",

+ 2 - 0
apps/bigmember_pc/src/main.js

@@ -7,6 +7,7 @@ import 'element-ui/lib/theme-chalk/index.css'
 import { easySubAppRegister } from '@jianyu/easy-inject-qiankun'
 import { fixGetComputedStyle } from '@jianyu/easy-fix-sub-app/lib/getComputedStyle.js'
 import VueCookies from 'vue-cookies'
+import { GiftFriendsDialogPlugin } from '@jy/plugin-gift-friends'
 
 import echarts from 'echarts'
 import './utils/jq-help'
@@ -30,6 +31,7 @@ Vue.use(Toast)
 Vue.use(ElementUI)
 Vue.use(MetaInfo).use(JyIcon)
 Vue.use(VueClipboard)
+Vue.use(GiftFriendsDialogPlugin)
 
 // # 修复v-charts 不适配 vue2.7x https://github.com/ElemeFE/v-charts/issues/934
 Vue._watchers = Vue.prototype._watchers = []

+ 7 - 1
apps/bigmember_pc/src/views/workspace/dashboard.vue

@@ -182,7 +182,13 @@ export default {
   methods: {
     showGiftDialog(data) {
       if (data === 'gift') {
-        this.showGiftSubmit = true
+        this.$GiftFriendsDialog({
+          props: {
+            visible: true,
+            name: '送给朋友'
+          }
+        })
+        // this.showGiftSubmit = true
       }
       else {
         const freeText = '您当前不是超级订阅会员,无法赠送给好友。您可以先购买后再赠送给好友。'

+ 5 - 0
plugins/gift-friends/.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/gift-friends/.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/gift-friends/.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/gift-friends/.env.production

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

+ 16 - 0
plugins/gift-friends/.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/gift-friends/.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/gift-friends/.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/gift-friends/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.$GiftSubmitDialog({
+        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/gift-friends/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>

+ 45 - 0
plugins/gift-friends/package.json

@@ -0,0 +1,45 @@
+{
+  "name": "@jy/plugin-gift-friends",
+  "version": "0.0.1",
+  "private": false,
+  "description": "赠送好友超级订阅插件",
+  "exports": "src/index.js",
+  "main": "./dist/jy-gift-friends.umd.js",
+  "module": "./dist/jy-gift-friends.mjs",
+  "files": [
+    "dist"
+  ],
+  "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": {
+    "element-ui": "^2.15.14-rc",
+    "qs": "^6.11.2"
+  },
+  "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-css-injected-by-js": "^3.1.0",
+    "vite-plugin-ejs": "1.6.4",
+    "vite-plugin-externals": "^0.6.2"
+  }
+}

+ 30 - 0
plugins/gift-friends/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
+// }

+ 47 - 0
plugins/gift-friends/src/App.vue

@@ -0,0 +1,47 @@
+<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>

+ 56 - 0
plugins/gift-friends/src/api/api.js

@@ -0,0 +1,56 @@
+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
+  })
+}
+
+// 超级订阅赠送
+export function setTransferSubDuration(data) {
+  return request({
+    url: '/subscribepay/vip/gift/transferSubDuration',
+    method: 'post',
+    data: qs.stringify(data)
+  })
+}
+
+// 根据手机号获取可赠信息
+export function getInfoByPhone(data) {
+  return request({
+    url: '/subscribepay/vip/gift/getInfoByPhone',
+    method: 'post',
+    data: qs.stringify(data)
+  })
+}
+
+// 赠送人超级订阅可赠资源查询
+export function getSubDuration(data) {
+  return request({
+    url: '/subscribepay/vip/gift/getSubDuration',
+    method: 'post',
+    data: qs.stringify(data)
+  })
+}

+ 4 - 0
plugins/gift-friends/src/api/index.js

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

+ 52 - 0
plugins/gift-friends/src/api/interceptors.js

@@ -0,0 +1,52 @@
+import service from './service'
+
+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) {
+          this.$toast('需要登录')
+        }
+        else if (res.error_msg) {
+          this.$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/gift-friends/src/api/service.js

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

BIN
plugins/gift-friends/src/assets/images/icon-delete.png


+ 32 - 0
plugins/gift-friends/src/assets/style/_mixin.scss

@@ -0,0 +1,32 @@
+// @import '@jianyu/easy-fix-sub-app/lib/in-app';
+// 公用函数
+@function addPx($a, $b) {
+  @return $a + $b;
+}
+
+@function addTop($a) {
+  @return $topNavHeight + $a;
+}
+
+@function addFooter($a) {
+  @return $footerHeight + $a;
+}
+
+@mixin diy-icon($name, $width: 24, $height: 24) {
+  ::v-deep .el-icon-jy-#{$name} {
+    background: url('~@/assets/images/icon/#{$name}.png') no-repeat;
+    background-size: cover;
+    display: inline-block;
+    width: #{$width}px;
+    height: #{$height}px;
+  }
+}
+
+@mixin ellipsis($lines: 1) {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: #{$lines};
+  -webkit-box-orient: vertical;
+  text-align: justify;
+}

+ 35 - 0
plugins/gift-friends/src/assets/style/_variables.scss

@@ -0,0 +1,35 @@
+// 导航栏
+$topNavHeight: 77px;
+
+// 底部栏
+$footerHeight: 364px;
+
+$color_main: #2cb7ca;
+
+// Background
+// 透明背景色使用时,需要配合白色背景使用
+$color_main_background: rgb($color_main,.1);
+
+$bg-retrieve: #010c28;
+$bg-button--default: linear-gradient(84deg, #af9552 0%, #efda98 100%);
+$bg-card--default: linear-gradient(#031242 0%, #010e36 100%);
+$bg-button--tran: linear-gradient(#53f1dd 0%, #07907e 100%);
+$bg-color-1: linear-gradient(90deg, #9f1b89 0%, #bb36a5 100%);
+$bg-color-2: linear-gradient(90deg, #46b5d1 0%, #8cd5e4 100%);
+$bg-color-3: linear-gradient(90deg, #f5ab48 0%, #f4ce8f 100%);
+$bg-color-4: linear-gradient(90deg, #f83f4f 0%, #f38797 100%);
+$bg-color-5: linear-gradient(90deg, #41af92 0%, #84ceb7 100%);
+$bg-color-6: linear-gradient(90deg, #7446a0 0%, #a380c4 100%);
+
+$bg-less: #f5feff;
+
+$font-text--title: 17px;
+
+$color-text--default: #1d1d1d;
+$color-text--active: $color_main;
+$color-text--highlight: $color_main;
+$color-text--less: #2abed1;
+
+$color-input--default: #1d1d1d;
+
+$bg-main-color: #fff;

+ 310 - 0
plugins/gift-friends/src/assets/style/common.scss

@@ -0,0 +1,310 @@
+@import './mixin';
+@import './pic-icon.scss';
+@import './variables.scss';
+
+html {
+  height: 100%;
+  // overflow-y: auto;
+}
+
+body {
+  background-color: #f2f2f4;
+}
+
+.highlight-text {
+  color: $color-text--highlight;
+}
+.highlight-text-orange {
+  color: #fa6f33;
+}
+.highlight-text-orange-bd {
+  color: #fa6f33;
+  font-weight: bold;
+}
+
+.pointer {
+  cursor: pointer;
+}
+
+[class*='no-select'] {
+  user-select: none;
+}
+
+.ellipsis {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  text-align: justify;
+}
+
+@for $i from 2 through 5 {
+  .ellipsis-#{$i} {
+    @include ellipsis($i);
+  }
+}
+
+/* 超过2行省略号显示 */
+.ellipsis-2 {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  text-align: justify;
+}
+
+/* 超过3行省略号显示 */
+.ellipsis-3 {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 3;
+  -webkit-box-orient: vertical;
+}
+
+::-webkit-scrollbar {
+  /*滚动条整体样式*/
+  width: 8px;
+}
+::-webkit-scrollbar-thumb {
+  /*滚动条里面小方块*/
+  border-radius: 3px;
+  background-color: #ececec;
+  opacity: 0.15;
+}
+.scrollbar {
+  &::-webkit-scrollbar {
+    /*滚动条整体样式*/
+    width: 8px;
+  }
+  &::-webkit-scrollbar-thumb {
+    /*滚动条里面小方块*/
+    border-radius: 3px;
+    background-color: #ececec;
+    opacity: 0.15;
+  }
+}
+
+*:focus-visible {
+  outline: none;
+}
+
+// 清除 input type=number 默认样式
+input::-webkit-outer-spin-button,
+input::-webkit-inner-spin-button {
+  -webkit-appearance: none;
+}
+input[type='number'] {
+  -moz-appearance: textfield;
+}
+
+.flex-w-100 {
+  width: 100%;
+  flex: 1;
+}
+.flex-r-c {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  &.center {
+    align-items: center;
+    justify-content: center;
+    &.sb {
+      justify-content: space-between;
+    }
+  }
+  &.left {
+    justify-content: flex-start;
+  }
+  &.right {
+    justify-content: flex-start;
+  }
+  .bottom {
+    align-items: flex-end;
+  }
+  &.wrap {
+    flex-wrap: wrap;
+  }
+}
+.flex-c-c {
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  &.center {
+    align-items: center;
+    justify-content: center;
+  }
+  &.right {
+    align-items: flex-end;
+  }
+  &.left {
+    align-items: flex-start;
+  }
+}
+.f-share {
+  padding: 6px 4px !important;
+  box-shadow: 0px 0px 28px 0px #999;
+  border: none !important;
+  .popper__arrow {
+    border: none !important;
+  }
+}
+
+// 复选框类名
+.j-checkbox {
+  flex-shrink: 0;
+  display: inline-block;
+  width: 18px;
+  height: 18px;
+  margin-right: 8px;
+  border-radius: 2px;
+  border: 1px solid #e0e0e0;
+  cursor: pointer;
+
+  &.checked {
+    border: 0;
+    background: url('~@/assets/images/icon/icon-checked.png') no-repeat
+      center center;
+    background-size: 16px;
+
+    &[disabled] {
+      background: url('~@/assets/images/icon/icon-checked.png') no-repeat;
+      background-size: 18px;
+    }
+  }
+}
+
+.show-underline {
+  .keyword.hide-underline {
+    border-bottom-width: 1px;
+  }
+}
+.keyword-underline {
+  border-bottom: 1px solid $color-main;
+  padding-bottom: 1px;
+  &.project-name,
+  &.winner-name {
+    cursor: pointer;
+  }
+}
+.keyword.hide-underline {
+  border-width: 0;
+}
+
+.iconfont {
+  &.icon-jiankong {
+    color: #9b9ca3;
+  }
+  &.icon-yijiankong {
+    color: #ff9f40;
+  }
+}
+
+a{
+  user-select: text!important;
+}
+
+.use-badge {
+  position: relative;
+  &::after {
+    content: attr(data-badge);
+    position: absolute;
+    top: 0;
+    right: -24px;
+    display: inline-block;
+    font-size: 12px;
+    line-height: 12px;
+    color: #fff;
+    padding: 2px 6px;
+    background-color: #FF3A20;
+    border: 1px solid #fff;
+    border-radius: 12px;
+    border-bottom-left-radius: 0;
+  }
+
+  // 扩展
+  &.el-button::after {
+    top: 0;
+    right: -11px;
+    transform: translate3d(0, -50%, 0);
+  }
+}
+
+/* 删除筛选提示框 */
+.filter-delete-messagebox{
+  width: 420px;
+  border-radius: 8px;
+  padding: 32px;
+  .el-message-box__title{
+    color: #1D1D1D;
+  }
+  .el-message-box__header{
+    padding: 0!important;
+  }
+  .el-message-box__content{
+    padding: 20px 27px 32px;
+  }
+  .el-message-box__message p{
+    font-size: 14px;
+    color: #686868;
+  }
+  .el-message-box__btns{
+    display: flex;
+    flex-direction: row-reverse;
+    justify-content: space-between;
+  }
+  .btn-group.confirm-btn{
+    background: #2cb7ca;
+    margin-right: 52px;
+    border: 0;
+    color: #fff;
+  }
+  .btn-group{
+    width: 132px;
+    height: 36px;
+    padding: 0;
+    border-radius: 6px;
+    font-size: 16px;
+  }
+  .btn-group.confirm-btn:focus{
+    color: #fff;
+  }
+  .btn-group.confirm-btn:hover {
+    color: #fff;
+  }
+}
+.download-message-tip{
+  border-radius: 8px !important;
+  width:380px !important;
+  &.el-message-box--center{
+    padding-bottom: 32px;
+  }
+  .el-message-box__header{
+    padding: 32px 32px 20px 32px;
+  }
+  .el-message-box--center .el-message-box__header {
+    padding-top: 32px;
+  }
+  .el-message-box__message{
+    color:#686868;
+  }
+  .el-message-box__content{
+    padding:0 32px 32px;
+  }
+  .el-message-box__btns {
+    padding: 0 32px !important;
+    display: flex !important;
+    justify-content: space-between !important;
+    .el-button{
+      width:132px !important;
+      height:36px !important;
+      border-radius: 6px !important;
+      font-size:16px !important;
+    }
+  }
+  &.btn-reverse {
+    .el-message-box__btns{
+      flex-direction:row-reverse !important;
+    }
+  }
+}

+ 48 - 0
plugins/gift-friends/src/assets/style/dialog.css

@@ -0,0 +1,48 @@
+.custom-dialog .el-dialog {
+	border-radius: 8px;
+}
+.custom-dialog .el-dialog__body {
+	color: #686868;
+	font-size: 14px;
+	line-height: 22px;
+}
+.custom-dialog .el-dialog__body,
+.custom-dialog .el-dialog__footer {
+	padding-left: 32px;
+	padding-right: 32px;
+}
+.custom-dialog .el-dialog__footer {
+	padding-top: 6px;
+}
+.custom-dialog .dialog-footer {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+.custom-dialog .action-button {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	flex: 1;
+	height: 36px;
+	border-radius: 6px;
+}
+.custom-dialog .action-button.cancel {
+	border: 1px solid #e0e0e0;
+	background-color: #fff;
+	color: #686868;
+}
+.custom-dialog .action-button.confirm {
+	border: 1px solid #2ABED1;
+	background-color: #2ABED1;
+	color: #fff;
+}
+.custom-dialog .action-button.confirm:disabled {
+	opacity: 0.5;
+}
+.custom-dialog .action-button:not(:last-of-type) {
+	margin-right: 48px;
+}
+.custom-dialog .text-center .el-dialog__body {
+	text-align: center;
+}

+ 207 - 0
plugins/gift-friends/src/assets/style/gift.css

@@ -0,0 +1,207 @@
+.custom-dialog.gift-submit-dialog .el-dialog__header {
+	text-align: left;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__header .el-dialog__title {
+	padding-left: 10px;
+	line-height: 28px;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__header::after {
+	content: '';
+	position: absolute;
+	left: 20px;
+	top: 26px;
+	display: inline-block;
+	width: 2px;
+	height: 16px;
+	background: #2ABED1;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body {
+	padding: 0 20px 10px;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-header {
+	display: flex;
+	align-items: center;
+	padding-bottom: 10px;
+	border-bottom: 1px solid #2ABED1;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-header__item {
+	margin-right: 24px;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body {
+	padding: 10px 0;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-person-list-button {
+	display: flex;
+	align-items: center;
+	font-size: 16px;
+	line-height: 24px;
+	color: #1D1D1D;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-person-list-button__icon {
+	display: flex;
+	justify-content: center;
+	margin-left: 10px;
+	width: 20px;
+	height: 20px;
+	line-height: 18px;
+	border-radius: 50%;
+	background-color: #2ABED1;
+	color: #fff;
+	font-size: 18px;
+	cursor: pointer;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-person-tip {
+	margin: 10px 0;
+	font-size: 14px;
+	line-height: 22px;
+	color: #2ABED1;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-person-list {
+	max-height: 200px;
+	padding: 10px;
+	border: 1px solid #ECECEC;
+	border-radius: 8px;
+	overflow-y: auto;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-person-list .gift-person-list__item {
+	position: relative;
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	margin-bottom: 10px;
+	padding: 10px 20px;
+	width: 440px;
+	border-radius: 8px;
+	background: linear-gradient(to bottom, #F6F6F6, #fff);
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-person-list .gift-person-list__item .gift-person-info-wrapper {
+	display: flex;
+	align-items: center;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-person-list .gift-person-list__item .gift-person-info-wrapper .el-form-item {
+	margin-bottom: 0;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-person-list .gift-person-list__item .gift-person-info-wrapper .el-form-item__label {
+	font-size: 14px;
+	line-height: 22px;
+	color: #1D1D1D;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-person-list .gift-person-list__item .gift-person-info-wrapper .el-form-item__label::before {
+	content: '';
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-person-list .gift-person-list__item .delete-person {
+	position: absolute;
+	right: -30px;
+	top: 0;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-person-list .gift-person-list__item .delete-person .icon-delete-button {
+	display: inline-block;
+	width: 20px;
+	height: 20px;
+	background: url("../images/icon-delete.png") no-repeat;
+	background-size: 20px 20px;
+	cursor: pointer;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-person-list .gift-person-info {
+	font-size: 14px;
+	color: #1D1D1D;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-person-list .gift-person-info.time {
+	position: relative;
+	margin-left: 24px;
+	width: 120px;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-person-list .gift-person-info.time .unit {
+	position: absolute;
+	top: 30px;
+	right: -40px;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-person-list .custom-long-input {
+	margin-top: 8px;
+	height: 36px;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-person-list .custom-long-input .el-input__inner {
+	height: 36px;
+	line-height: 36px;
+}
+.custom-dialog.gift-submit-dialog .info-error-tip, .custom-dialog.gift-submit-dialog .phone-no-register-tip {
+	margin-top: 10px;
+	font-size: 14px;
+	line-height: 22px;
+	color: #2ABED1;
+}
+.custom-dialog.gift-submit-dialog .info-error-tip.error-highlight, .custom-dialog.gift-submit-dialog .phone-no-register-tip.error-highlight {
+  margin-top: 18px;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-person-list .info-error-tip, .custom-dialog.gift-submit-dialog .el-dialog__footer .info-error-tip {
+	color: #FF3A20;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-total-tip {
+	margin-top: 10px;
+	font-size: 14px;
+	line-height: 22px;
+	color: #686868;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-total-tip span {
+	color: #2ABED1;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-read-agree {
+	margin-top: 10px;
+	font-size: 14px;
+	color: #686868;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-read-agree .el-checkbox.is-checked .el-checkbox__label {
+	color: #888888;
+}
+.custom-dialog.gift-submit-dialog .el-dialog__body .gift-submit-body .gift-read-agree a {
+	color: #2ABED1;
+	text-decoration: none;
+}
+.custom-dialog.gift-submit-dialog .dialog-footer {
+	justify-content: center;
+}
+.custom-dialog.gift-submit-dialog .dialog-footer-wrapper {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+}
+.custom-dialog.gift-submit-dialog .dialog-footer-wrapper button {
+	width: 132px;
+	height: 36px;
+	background: #2ABED1;
+	color: #fff;
+	font-size: 16px;
+	border: none;
+}
+.custom-dialog .action-button:not(:last-of-type) {
+  margin-right: 20px;
+}
+.custom-dialog.gift-submit-dialog .dialog-footer-wrapper button.cancel {
+	background: #fff;
+	color: #1D1D1D;
+	border: 1px solid #E0E0E0;
+}
+
+.gift-submit-confirm-dialog .el-dialog {
+	padding: 32px;
+}
+.gift-submit-confirm-dialog .el-dialog__header {
+	padding: 0;
+}
+.gift-submit-confirm-dialog .el-dialog__body {
+	padding: 20px 0 32px;
+}
+.gift-submit-confirm-dialog .el-dialog__body .confirm-content {
+	text-align: center;
+}
+.gift-submit-confirm-dialog .el-dialog__footer {
+	padding: 0;
+	text-align: center;
+}
+.gift-submit-confirm-dialog .el-dialog__footer .dialog-footer-wrapper {
+	display: flex;
+}
+.gift-submit-confirm-dialog .el-dialog__footer .dialog-footer-wrapper button {
+	width: 132px;
+	height: 36px;
+}

+ 91 - 0
plugins/gift-friends/src/assets/style/pic-icon.scss

@@ -0,0 +1,91 @@
+.j-icon {
+  display: inline-block;
+  width: 20px;
+  height: 20px;
+}
+.wh24 {
+  width: 24px;
+  height: 24px;
+}
+.j-icon-base {
+  background-color: transparent;
+  background-repeat: no-repeat;
+  background-position: center;
+  background-size: contain;
+}
+
+// 选择器前的 checkbox,需要添加 j-icon 基类
+// .checkbox {
+//   border: 1px solid #ddd;
+//   border-radius: 50%;
+//   -webkit-appearance: none;
+//   background: #fff;
+//   &:checked,
+//   &.checked {
+//     border: 0;
+//     background: url(~@/assets/image/icon/checkbox-checked.png) no-repeat center;
+//     background-size: 100% 100%;
+//     &[disabled] {
+//       border: 0;
+//       background: url(~@/assets/image/icon/checkbox-disabled.png) no-repeat center;
+//       background-size: 100% 100%;
+//     }
+//   }
+
+//   &.half {
+//     border: 0;
+//     background: url(~@/assets/image/icon/checkbox-checked-half.png) no-repeat center;
+//     background-size: 100% 100%;
+//     &[disabled] {
+//       background: url(~@/assets/image/icon/checkbox-checked-half-disabled.png) no-repeat center;
+//     }
+//   }
+
+//   &.transparent {
+//     &:checked,
+//     &.checked {
+//       border: 0;
+//       background: url(~@/assets/image/icon/checkbox-transparent-checked.png) no-repeat center;
+//       background-size: 100% 100%;
+//     }
+//   }
+// }
+
+.icon-vip-mark-img {
+  background-image: url(~@/assets/images/icon/vip.png);
+}
+.icon-help-img {
+  background-image: url(~@/assets/images/icon/help.png);
+}
+.icon-img-close {
+  background-image: url(@/assets/images/icon/close-icon2x.png);
+}
+
+// zhima芝麻
+.j-icon.icon-img-zhima-zhimaxinyong-logo {
+  width: 90px;
+  height: 20px;
+  background-image: url(@/assets/images/icon/zhima/zhimaxinyong-logo.png);
+}
+.icon-img-zhima-guzhuchengxin {
+  background-image: url(@/assets/images/icon/zhima/guzhuchengxin.png);
+}
+.icon-img-zhima-lianhepingjia {
+  background-image: url(@/assets/images/icon/zhima/lianhepingjia.png);
+}
+.icon-img-zhima-qiyeshili {
+  background-image: url(@/assets/images/icon/zhima/qiyeshili.png);
+}
+.icon-img-zhima-shehuijiazhi {
+  background-image: url(@/assets/images/icon/zhima/shehuijiazhi.png);
+}
+.icon-img-zhima-shehuiyingxiangli {
+  background-image: url(@/assets/images/icon/zhima/shehuiyingxiangli.png);
+}
+.icon-img-zhima-xinyongpingjia {
+  background-image: url(@/assets/images/icon/zhima/xinyongpingjia.png);
+}
+.icon-img-arrow-down {
+  background-image: url(@/assets/images/icon/arrow-down.png);
+  background-size: contain;
+}

+ 353 - 0
plugins/gift-friends/src/assets/style/reset-ele.scss

@@ -0,0 +1,353 @@
+@import './_variables';
+
+.gift-submit-dialog {
+  // 分页样式重置
+  .el-pagination-container {
+    position: relative;
+    margin-top: 32px;
+    margin-right: 16px;
+    padding-bottom: 60px;
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+    .el-pagination {
+      position: absolute;
+      right: 0;
+    }
+  }
+  .el-pagination.is-background .el-pager {
+    li {
+      background-color: #fff;
+      border: 1px solid rgba($color: #000, $alpha: 0.05);
+    }
+
+    li:not(.disabled).active,
+    li:not(.disabled):hover {
+      color: #fff;
+      background-color: $color-text--highlight;
+    }
+  }
+
+  // 修改输入框默认focus边框颜色
+  .el-input.is-active .el-input__inner,
+  .el-input__inner:focus {
+    border-color: $color-text--highlight;
+  }
+
+  .el-checkbox__inner {
+    width: 16px;
+    height: 16px;
+    border-radius: 3px;
+    &::after {
+      border-width: 2px;
+      left: 5px;
+      top: 1px;
+    }
+  }
+  .el-checkbox__input.is-checked .el-checkbox__inner {
+    background-color: $color-text--highlight;
+    border-color: $color-text--highlight;
+  }
+
+  .el-checkbox__input.is-focus .el-checkbox__inner {
+    border-color: $color-text--highlight;
+  }
+
+
+  .el-button--main,
+  .el-button--confirm {
+    border-color: $color-text--highlight;
+    background: $color-text--highlight;
+    border-radius: 6px;
+    padding: 8px 16px;
+    box-sizing: border-box;
+    font-size: 14px;
+    font-weight: 400;
+    color: #fff;
+    line-height: 24px;
+    &:hover,
+    &:focus {
+      border-color: $color-text--highlight;
+      background: $color-text--highlight;
+      color: #fff;
+    }
+  }
+
+  .el-button--less,
+  .el-button--cancel {
+    border-color: $color-text--less;
+    background: $bg-less;
+    border-radius: 6px;
+    padding: 6px 16px;
+    box-sizing: border-box;
+    font-size: 16px;
+    line-height: 22px;
+    font-weight: 400;
+    color: $color-text--less;
+    &:hover,
+    &:focus {
+      border-color: $color-text--highlight;
+      background: $color-text--highlight;
+      color: #fff;
+    }
+  }
+
+  .el-link {
+    &.el-link--default {
+      &:hover {
+        color: $color-text--highlight;
+      }
+    }
+  }
+
+  .el-loading-mask {
+    transition: opacity 1s;
+  }
+  input[type="number"] {
+    -moz-appearance: textfield; /* Firefox */
+    -webkit-appearance: none; /* Chrome, Safari, Edge */
+    appearance: none; /* Modern browsers */
+  }
+  
+  input[type="number"]::-webkit-inner-spin-button,
+  input[type="number"]::-webkit-outer-spin-button {
+    -webkit-appearance: none;
+    margin: 0;
+  }
+}
+.custom-message-box,
+.custom-alert-box {
+  width: 380px !important;
+  border-radius: 8px!important;
+  .custom-confirm-btn {
+    margin-top: 12px;
+    width: 132px;
+    height: 36px;
+    background: #2cb7ca;
+    border-radius: 6px;
+    border: 0;
+    font-size: 16px;
+    &:hover {
+      background: #2cb7ca;
+    }
+  }
+
+  .custom-cancel-btn,
+  .custom-cancel-btn:hover,
+  .custom-cancel-btn:focus{
+    width: 132px;
+    height: 34px;
+    background-color: #fff;
+    border: 1px solid #DCDFE6;
+    color: #686868;
+    font-size: 16px;
+  }
+  .el-message-box__message,
+  .message-text {
+    font-size: 14px;
+    color: #686868;
+    line-height: 24px;
+  }
+}
+.custom-alert-box{
+  padding-bottom: 32px!important;
+  .el-message-box__header{
+    padding-top: 32px!important;
+  }
+  .el-message-box__title{
+    line-height: 28px;
+  }
+  .el-message-box__btns{
+    display: flex;
+    align-items: center;
+    flex-direction: row-reverse;
+    justify-content: space-between;
+    padding: 17px 32px 0;
+  }
+  .custom-confirm-btn{
+    margin-left: 0!important;
+    margin-top: 0;
+  }
+  .custom-default-btn,
+  .custom-default-btn:hover,
+  .custom-default-btn:focus{
+    width: 132px;
+    height: 36px;
+    border-radius: 6px;
+    background-color: #fff;
+    border: 1px solid #DCDFE6;
+    color: #686868;
+    font-size: 16px;
+  }
+}
+.el-popper {
+  li {
+    float: none;
+  }
+}
+
+.el-pagination__jump {
+  color: #686868 !important;
+}
+.el-pagination.is-background .el-pagination__confirm {
+  width: 52px;
+  text-align: center;
+  color: #1d1d1d;
+}
+.el-pagination{
+  .el-select.el-select--mini{
+    .el-input__suffix{
+      display: block;
+      top: -2px;
+    }
+  }
+}
+
+// 分页组件页码选择select下拉框样式
+.pagination-custom-select {
+  top: -138px !important;
+  left: 4px !important;
+  min-width: 100px !important;
+  margin-top: 0px !important;
+  border-radius: 2px !important;
+  transform-origin: center bottom !important;
+  .el-scrollbar {
+    height: 136px;
+    border-radius: 2px;
+  }
+  .el-scrollbar__wrap {
+    height: 136px !important;
+    overflow: unset !important;
+  }
+  .el-select-dropdown__list {
+    padding: 0;
+    max-width: 100px !important;
+  }
+  .el-select-dropdown__item {
+    width: 100%;
+    text-align: center;
+    color: #1d1d1d;
+    border-bottom: 1px solid #ececec;
+    text-overflow: unset !important;
+  }
+  .el-select-dropdown__item.selected {
+    color: #2cb7ca;
+  }
+  .el-select-dropdown__item.hover,
+  .el-select-dropdown__item:hover {
+    background: #2cb7ca;
+    color: #fff;
+  }
+  .el-scrollbar__bar.is-horizontal {
+    height: 0;
+  }
+  .popper__arrow {
+    display: none !important;
+  }
+  .el-select-dropdown__item.selected {
+    color: #2cb7ca;
+  }
+  .el-select-dropdown__item.hover,
+  .el-select-dropdown__item:hover {
+    background: #2cb7ca;
+    color: #fff;
+  }
+  .el-select-dropdown__wrap {
+    margin-bottom: -18px !important;
+  }
+}
+
+.el-popover.reset-el-popover {
+  padding: 0;
+  border: none;
+  box-shadow: none;
+  &.no-content {
+    display: none;
+  }
+}
+
+.el-radio.jy-radio {
+  margin-right: 16px;
+  .el-radio__input.is-checked .el-radio__inner {
+    background-color: transparent;
+  }
+  .el-radio__inner {
+    width: 18px;
+    height: 18px;
+  }
+  .el-radio__inner::after {
+    width: 9px;
+    height: 9px;
+    background-color: #2abed1;
+  }
+}
+
+// 从selector-cascader-common.scss中提取成全局
+.selector-cascader {
+  $min-width: 204px;
+
+  position: relative;
+  > .el-popper[x-placement^='bottom'] {
+    margin-top: 0;
+  }
+  > .el-popover,
+  > .el-select-dropdown {
+    padding: 0;
+    left: 0 !important;
+    border-color: $color_main;
+
+    .el-cascader-menu__wrap {
+      height: 204px;
+    }
+    .el-cascader-menu {
+      min-width: 160px;
+      color: #1d1d1d;
+    }
+    .el-cascader-node,
+    .el-select-dropdown__item {
+      height: 30px;
+      line-height: 30px;
+    }
+  }
+
+  .popper__arrow {
+    display: none !important;
+  }
+  .el-cascader-panel.is-bordered {
+    border: none;
+  }
+
+  // 此处公共样式不定义。可在组件内自行修改
+  // .el-cascader-menu__list {
+  //   min-width: $min-width - 2px;
+  // }
+
+  .virtual-cascader,
+  .el-cascader {
+    min-width: $min-width;
+    height: 30px;
+    line-height: 30px;
+  }
+  .virtual-input,
+  .el-input {
+    height: 100%;
+    .el-input__inner {
+      height: 30px;
+      line-height: 30px;
+      font-size: 14px;
+      color: #1d1d1d;
+      border-color: #e0e0e0;
+    }
+    .el-input__icon {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+}
+.el-date-table td.in-range div,
+.el-date-table td.in-range div:hover,
+.el-date-table.is-week-mode .el-date-table__row.current div,
+.el-date-table.is-week-mode .el-date-table__row:hover div{
+  background-color:#EAF8FA!important;
+}

+ 171 - 0
plugins/gift-friends/src/components/Dialog.vue

@@ -0,0 +1,171 @@
+<template>
+  <el-dialog
+    class="custom-dialog"
+    :custom-class="customClass"
+    v-bind="$props"
+    :show-close="showClose"
+    :visible="visible"
+    :append-to-body="true"
+    :close-on-click-modal="closeClickModal"
+    :close-on-press-escape="closeOnPressEscape"
+    :destroy-on-close="destroyOnClose"
+    :before-close="beforeClose"
+    @update:visible="update"
+    @open="$emit('open')"
+    @opened="$emit('opened')"
+    @close="$emit('close')"
+    @closed="$emit('closed')"
+  >
+    <slot name="default" />
+    <span v-if="showFooter" slot="footer" class="dialog-footer">
+      <slot name="footer">
+        <button class="action-button cancel" @click="onClickCancel">
+          取消
+        </button>
+        <button
+          class="action-button confirm"
+          :disabled="disabled"
+          @click="onClickConfirm"
+        >
+          确定
+        </button>
+      </slot>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+import { Button, Dialog } from 'element-ui'
+
+export default {
+  name: 'CustomDialog',
+  components: {
+    [Dialog.name]: Dialog,
+    [Button.name]: Button
+  },
+  props: {
+    visible: Boolean,
+    showClose: Boolean,
+    beforeClose: Function,
+    comMount: {
+      type: String,
+      default: ''
+    },
+    showFooter: {
+      type: Boolean,
+      default() {
+        return true
+      }
+    },
+    top: String,
+    title: {
+      type: String,
+      default: ''
+    },
+    width: {
+      type: String,
+      default: '30%'
+    },
+    showClose: {
+      type: Boolean,
+      default: false
+    },
+    center: {
+      type: Boolean,
+      default: true
+    },
+    customClass: {
+      type: String,
+      default: ''
+    },
+    disabled: Boolean,
+    closeClickModal: {
+      type: Boolean,
+      default: false
+    },
+    closeOnPressEscape: {
+      type: Boolean,
+      default: false
+    },
+    destroyOnClose: {
+      type: Boolean,
+      default: false
+    }
+  },
+  watch: {
+    visible(val) {
+      //  console.log(val, 'visible')
+    }
+  },
+  methods: {
+    update(e) {
+      this.$emit('update:visible', e)
+    },
+    onClickCancel() {
+      this.$emit('cancel')
+    },
+    onClickConfirm() {
+      this.$emit('confirm')
+    }
+  }
+}
+</script>
+
+<!-- <style scoped lang="scss">
+.custom-dialog {
+  ::v-deep {
+  .el-dialog {
+    border-radius: 8px;
+  }
+  .el-dialog__body {
+    color: #686868;
+    font-size: 14px;
+    line-height: 22px;
+  }
+  .el-dialog__body,
+  .el-dialog__footer {
+    padding-left: 32px;
+    padding-right: 32px;
+  }
+  .el-dialog__footer {
+    padding-top: 6px;
+  }
+  .dialog-footer {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+  .action-button {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex: 1;
+    height: 36px;
+    border-radius: 6px;
+    &.cancel {
+      border: 1px solid #e0e0e0;
+      background-color: #fff;
+      color: #686868;
+    }
+    &.confirm {
+      border: 1px solid #2ABED1;
+      background-color: #2ABED1;
+      color: #fff;
+      &:disabled {
+        opacity: 0.5;
+      }
+    }
+    &:not(:last-of-type) {
+      margin-right: 48px;
+    }
+  }
+}
+.text-center {
+  ::v-deep {
+    .el-dialog__body {
+      text-align: center;
+    }
+  }
+}
+}
+</style> -->

+ 622 - 0
plugins/gift-friends/src/components/GiftSubmitDialog.vue

@@ -0,0 +1,622 @@
+<template>
+  <CustomDialog
+    width="600px"
+    :title="title"
+    class="gift-submit-dialog"
+    :visible="show"
+  >
+    <div class="gift-submit-header">
+      <div class="gift-submit-header__item">
+        <span>订阅区域:</span>
+        <span>{{ subduration.areacount }}个省</span>
+      </div>
+      <div class="gift-submit-header__item">
+        <span>可赠送时长(取整):</span>
+        <span>{{ subduration.gifted }}个月</span>
+      </div>
+    </div>
+    <div class="gift-submit-body">
+      <div class="gift-person-list-button">
+        <span>人员列表</span>
+        <span class="gift-person-list-button__icon" @click="addPerson">+</span>
+      </div>
+      <div class="gift-person-tip">
+        说明:如手机号尚未注册剑鱼,赠送其超级订阅后,平台会自动帮其按照对应手机号注册。
+      </div>
+      <div class="gift-person-list">
+        <div v-for="(item, index) in personList" :key="index" class="gift-person-list__item">
+          <el-form :ref="`form${index}`" :model="item" :rules="rules" class="gift-person-info-wrapper">
+            <el-form-item label="朋友手机号" prop="phone" class="gift-person-info">
+              <el-input
+                v-model="item.phone"
+                maxlength="11"
+                class="custom-long-input"
+                placeholder="请输入手机号"
+                @blur="validateSingleForm(index, item, 'phone')"
+              />
+            </el-form-item>
+            <el-form-item label="赠予时长" prop="monthnum" class="gift-person-info time">
+              <el-input
+                v-model="item.monthnum"
+                type="number"
+                class="custom-long-input"
+                placeholder="请输入整数"
+                @blur="validateSingleForm(index, item, 'monthnum')"
+              />
+              <span class="unit">个月</span>
+            </el-form-item>
+          </el-form>
+          <div v-show="index !== 0" class="delete-person" @click="deletePerson(index)">
+            <span class="icon-delete-button" />
+          </div>
+          <div v-if="item.status === -1" :ref="`phoneTip${index}`" class="phone-no-register-tip">
+            {{ statusMessages[item.status] }}
+          </div>
+          <div v-else :ref="`infoTip${index}`" class="info-error-tip">
+            {{ statusMessages[item.status] }}
+          </div>
+        </div>
+      </div>
+      <div v-if="monthNumTotal > 0" class="gift-total-tip">
+        共赠送<span> {{ personList.length }} </span>人,赠送时长<span> {{ monthNumTotal }} </span>个月,剩余<span> {{ getGifted }} </span>个月可赠送
+      </div>
+      <div class="gift-read-agree">
+        <el-checkbox v-model="checked">
+          阅读并同意<a href="javascript:;">《“送好友超级订阅”产品须知》</a>
+        </el-checkbox>
+      </div>
+    </div>
+    <span slot="footer">
+      <div class="dialog-footer-wrapper">
+        <button
+          class="action-button confirm"
+          :disabled="!isFormValid"
+          @click="onClickConfirm"
+        >
+          提交
+        </button>
+        <button class="action-button cancel" @click="closeFn">
+          取消
+        </button>
+      </div>
+      <div v-if="monthNumTotal > subduration.gifted" class="info-error-tip">
+        人员时长总和大于可赠送时长,请调整后提交
+      </div>
+    </span>
+    <CustomDialog
+      class="gift-submit-confirm-dialog"
+      width="380px"
+      top="28vh"
+      :visible="showConfirm"
+      title="赠送确认"
+      @close="showConfirm = false"
+    >
+      <div class="confirm-content">
+        确定将剩余时长赠送给好友?
+      </div>
+      <span slot="footer" class="dialog-footer-wrapper">
+        <button
+          class="action-button confirm"
+          @click="onGiftConfirm"
+        >
+          确定
+        </button>
+        <button class="action-button cancel" @click="closeConfirm">
+          取消
+        </button>
+      </span>
+    </CustomDialog>
+  </CustomDialog>
+</template>
+
+<script>
+import { Checkbox, Dialog, Form, FormItem, Input } from 'element-ui'
+import { getInfoByPhone, getSubDuration, setTransferSubDuration } from '../api/api'
+import CustomDialog from './Dialog.vue'
+
+export default {
+  name: 'GiftSubmitDialog',
+  components: {
+    CustomDialog,
+    [Form.name]: Form,
+    [FormItem.name]: FormItem,
+    [Input.name]: Input,
+    [Checkbox.name]: Checkbox,
+    [Dialog.name]: Dialog
+  },
+  props: {
+    visible: Boolean,
+    title: {
+      type: String,
+      default: '送给朋友'
+    },
+  },
+  data() {
+    const validatePhone = (rule, value, callback) => {
+      if (value === '') {
+        callback(new Error('请输入手机号'))
+      }
+      else if (!/^1[3-9]\d{9}$/.test(value)) {
+        callback(new Error('手机号码格式不正确'))
+      }
+      else {
+        callback()
+      }
+    }
+    const validateMonthnum = (rule, value, callback) => {
+      if (value === '') {
+        callback(new Error('请输入赠予时长'))
+      }
+      else if (value <= 0) {
+        callback(new Error('赠予时长应大于0'))
+      }
+      else {
+        callback()
+      }
+    }
+    return {
+      show: this.visible,
+      checked: false,
+      personList: [
+        {
+          phone: '',
+          monthnum: '',
+          status: '',
+          error: '',
+          phoneValid: false,
+          monthnumValid: false
+        }
+      ],
+      rules: {
+        phone: [
+          { validator: validatePhone, trigger: 'blur' }
+        ],
+        monthnum: [
+          { validator: validateMonthnum, trigger: 'blur' },
+        ]
+      },
+      statusMessages: {
+        '1': '',
+        '-1': '提示:手机号尚未注册剑鱼,赠送其超级订阅后,平台会自动帮其按照对应手机号注册。',
+        '-2': '手机号已是超级订阅会员,且购买省份与当前省份不一致,不可赠送。',
+        '-3': '不能将超级订阅赠送给自己,请更换手机号。该提示展示在对应手机号下方'
+      },
+      subduration: {},
+      showConfirm: false
+    }
+  },
+  computed: {
+    isFormValid() {
+      const res = Number(this.subduration.gifted) - this.monthNumTotal
+      if (res < 0) {
+        return false
+      }
+      else {
+        return this.personList.every(item => item.phoneValid && item.monthnumValid)
+      }
+    },
+    monthNumTotal() {
+      return this.personList.reduce((total, item) => {
+        return total + Number(item.monthnum) || 0
+      }, 0)
+    },
+    // 剩余可赠送时长
+    getGifted() {
+      const res = Number(this.subduration.gifted) - this.monthNumTotal
+      if (res >= 0) {
+        return res
+      }
+      else {
+        return 0
+      }
+    }
+  },
+  created() {
+    this.getSubDurationEvent()
+  },
+  methods: {
+    async getSubDurationEvent() {
+      const { error_code: code, data } = await getSubDuration()
+      if (code === 0) {
+        this.subduration = data
+      }
+    },
+    /**
+     * 验证单个表单
+     *
+     * @param {number} index - 表单的索引
+     */
+    validateSingleForm(index, item, fieldname) {
+      if (this.$refs[`form${index}`]) {
+        this.$refs[`form${index}`][0].validateField(fieldname, (error) => {
+          if (fieldname === 'phone') {
+            if (!error) {
+              // 需要只调用一次接口,避免多次请求
+              if (item.status !== 1) {
+                this.getInfoByPhoneEvent(index, item)
+              }
+            }
+            else {
+              item.phoneValid = false
+            }
+          }
+          else if (fieldname === 'monthnum') {
+            if (error) {
+              // 添加错误提示类名
+              if (item.status === -1) {
+                if (this.$refs[`phoneTip${index}`]) {
+                  this.$refs[`phoneTip${index}`][0].classList.add('error-highlight')
+                }
+              }
+              else {
+                if (this.$refs[`infoTip${index}`]) {
+                  this.$refs[`infoTip${index}`][0].classList.add('error-highlight')
+                }
+              }
+            }
+            else {
+              if (item.status === -1) {
+                if (this.$refs[`phoneTip${index}`]) {
+                  this.$refs[`phoneTip${index}`][0].classList.remove('error-highlight')
+                }
+              }
+              else {
+                if (this.$refs[`infoTip${index}`]) {
+                  this.$refs[`infoTip${index}`][0].classList.remove('error-highlight')
+                }
+              }
+            }
+            item.monthnumValid = !error
+          }
+        })
+      }
+    },
+    async getInfoByPhoneEvent(index, item) {
+      // 此处可以调用接口验证手机号是否已经注册剑鱼
+      try {
+        const { error_code: code, data } = await getInfoByPhone({
+          phone: item.phone
+        })
+        if (code === 0) {
+          item.status = data.status
+          item.error = this.statusMessages[data.status]
+          if (data.status === 1 || data.status === -1) {
+            item.phoneValid = true
+          }
+          else {
+            item.phoneValid = false
+          }
+          // 当data.status不等于1时,移除同组的赠予时长输入框校验结果
+          // if (data.status !== '1') {
+          //   this.$refs[`form${index}`][0].clearValidate('monthnum')
+          // }
+          // 当data.status不等于1时,检查personList中的每一项的status,如果有任何一个的status不等于1,提交按钮不能点击
+          // for (let i = 0; i < this.personList.length; i++) {
+          //   if (this.personList[i].status !== 1 && this.personList[i].status !== -1) {
+          //     this.isFormValid = false
+          //     return
+          //   }
+          // }
+        }
+      }
+      catch (error) {
+        console.log(error)
+        item.phoneValid = true
+      }
+    },
+    /**
+     * 更新整体表单的有效性
+     */
+    updateFormValidity() {
+      const validationPromises = this.personList.map((item, index) => {
+        return new Promise((resolve) => {
+          try {
+            this.$refs[`form${index}`][0].validate((valid) => {
+              resolve(valid)
+            })
+          }
+          catch (e) {
+            resolve(false)
+          }
+        })
+      })
+
+      Promise.all(validationPromises).then((results) => {
+        // this.isFormValid = results.every(result => result)
+      })
+    },
+    addPerson() {
+      this.personList.push({
+        phone: '',
+        monthnum: '',
+        status: '',
+        error: '',
+        phoneValid: false,
+        monthnumValid: false
+      })
+      this.updateFormValidity()
+    },
+    /**
+     * 重置人员列表
+     */
+    resetPersonList() {
+      this.personList = [
+        {
+          phone: '',
+          monthnum: '',
+          status: '',
+          error: '',
+          phoneValid: false,
+          monthnumValid: false
+        }
+      ]
+    },
+    /**
+     * 删除指定索引位置的人员
+     *
+     * @method deletePerson
+     */
+    deletePerson(index) {
+      this.personList.splice(index, 1)
+      this.updateFormValidity()
+    },
+    onClickConfirm() {
+      if (!this.checked)
+        return this.$toast('请勾选协议')
+      if (this.isFormValid) {
+        this.showConfirm = true
+      }
+    },
+    onGiftConfirm() {
+      this.confirmGiftData()
+      this.showConfirm = false
+      setTimeout(() => {
+        this.show = false
+      }, 1000)
+    },
+    async confirmGiftData() {
+      // 参数格式:{phones:{18439509554: 1,18439509555: 2}}
+      const data = this.personList.reduce((acc, cur) => {
+        if (cur.phone && cur.monthnum) {
+          acc[cur.phone] = cur.monthnum
+        }
+        return acc
+      }, {})
+      const { error_code: code, error_msg: msg } = await setTransferSubDuration({ phones: JSON.stringify(data) })
+      if (code === 0) {
+        this.$toast('赠送成功')
+      }
+      else {
+        this.$toast(msg)
+      }
+      this.$emit('close')
+      this.resetPersonList()
+    },
+    closeFn() {
+      this.$emit('close')
+      this.show = false
+      this.resetPersonList()
+    },
+    closeConfirm() {
+      this.showConfirm = false
+    }
+  }
+}
+</script>
+
+<!-- <style lang="scss" scoped>
+.gift-submit-confirm-dialog {
+
+  ::v-deep {
+    .el-dialog {
+      padding: 32px;
+    }
+    .el-dialog__header {
+      padding: 0;
+    }
+    .el-dialog__body {
+      padding: 20px 0 32px;
+      .confirm-content {
+        text-align: center;
+      }
+    }
+    .el-dialog__footer {
+      padding: 0;
+      text-align: center;
+      .dialog-footer-wrapper {
+        display: flex;
+        button {
+          width: 132px;
+          height: 36px;
+        }
+      }
+    }
+  }
+}
+</style> -->
+
+<!-- <style lang="scss" scoped>
+  .gift-submit-dialog {
+    ::v-deep {
+      .el-dialog__header {
+        text-align: left;
+        .el-dialog__title {
+          padding-left: 10px;
+          line-height: 28px;
+        }
+        &::after {
+          content: '';
+          position: absolute;
+          left: 20px;
+          top: 26px;
+          display: inline-block;
+          width: 2px;
+          height: 16px;
+          background: #2ABED1;
+        }
+      }
+      .el-dialog__body {
+        padding: 0 20px 10px;
+        .gift-submit-header {
+          display: flex;
+          align-items: center;
+          padding-bottom: 10px;
+          border-bottom: 1px solid #2ABED1;
+          &__item {
+            margin-right: 24px;
+          }
+        }
+        .gift-submit-body {
+          padding: 10px 0;
+          .gift-person-list-button {
+            display: flex;
+            align-items: center;
+            font-size: 16px;
+            line-height: 24px;
+            color: #1D1D1D;
+          }
+          .gift-person-list-button__icon {
+            display: flex;
+            justify-content: center;
+            margin-left: 10px;
+            width: 20px;
+            height: 20px;
+            line-height: 18px;
+            border-radius: 50%;
+            background-color: #2ABED1;
+            color: #fff;
+            font-size: 18px;
+            cursor: pointer;
+          }
+          .gift-person-tip {
+            margin: 10px 0;
+            font-size: 14px;
+            line-height: 22px;
+            color: #2ABED1;
+          }
+          .gift-person-list {
+            max-height: 200px;
+            padding: 10px;
+            border: 1px solid #ECECEC;
+            border-radius: 8px;
+            overflow-y: auto;
+            .gift-person-list__item {
+              position: relative;
+              display: flex;
+              flex-direction: column;
+              justify-content: center;
+              margin-bottom: 10px;
+              padding: 10px 20px;
+              width: 480px;
+              border-radius: 8px;
+              background: linear-gradient(to bottom, #F6F6F6, #fff);
+              .gift-person-info-wrapper {
+                display: flex;
+                align-items: center;
+                .el-form-item {
+                  margin-bottom: 0;
+                }
+                .el-form-item__label {
+                  font-size: 14px;
+                  line-height: 22px;
+                  color: #1D1D1D;
+                  &::before {
+                    content: '';
+                  }
+                }
+              }
+              .delete-person {
+                position: absolute;
+                right: -30px;
+                top: 0;
+                .icon-delete-button {
+                  display: inline-block;
+                  width: 20px;
+                  height: 20px;
+                  background: url('~@/assets/images/icon-delete.png') no-repeat;
+                  background-size:20px 20px ;
+                  cursor: pointer;
+                }
+              }
+            }
+            .gift-person-info {
+              font-size: 14px;
+              color: #1D1D1D;
+              &.time {
+                position: relative;
+                margin-left: 24px;
+                width: 120px;
+                .unit {
+                  position: absolute;
+                  top: 30px;
+                  right: -40px;
+                }
+              }
+            }
+            .custom-long-input {
+              margin-top: 8px;
+              height: 36px;
+              .el-input__inner {
+                height: 36px;
+                line-height: 36px;
+              }
+            }
+            .phone-no-register-tip, .info-error-tip {
+              margin-top: 10px;
+              font-size: 14px;
+              line-height: 22px;
+              color: #2ABED1;
+            }
+            .info-error-tip {
+              color: #FF3A20;
+            }
+          }
+          .gift-total-tip {
+            margin-top: 10px;
+            font-size: 14px;
+            line-height: 22px;
+            color: #686868;
+            span {
+              color: #2ABED1;
+            }
+          }
+          .gift-read-agree {
+            margin-top: 10px;
+            font-size: 14px;
+            color: #686868;
+            .el-checkbox.is-checked {
+              .el-checkbox__label {
+                color: #888888;
+              }
+            }
+            a {
+              color: #2ABED1;
+              text-decoration: none;
+            }
+          }
+        }
+      }
+      .dialog-footer {
+        justify-content: center;
+      }
+    }
+    .dialog-footer-wrapper {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      button {
+        width: 132px;
+        height: 36px;
+        background: #2ABED1;
+        color: #fff;
+        font-size: 16px;
+        border: none;
+        &.cancel {
+          background: #fff;
+          color: #1D1D1D;
+          border: 1px solid #E0E0E0;
+        }
+      }
+    }
+  }
+</style> -->

+ 43 - 0
plugins/gift-friends/src/components/toast/Toast.vue

@@ -0,0 +1,43 @@
+<template>
+  <div class="wrap" v-if="showWrap" :class="showContent ? 'fadein' : 'fadeout'">
+    {{ text }}
+  </div>
+</template>
+
+<style scoped>
+.wrap {
+  position: fixed;
+  left: 50%;
+  top: 50%;
+  background: rgba(0, 0, 0, 0.65);
+  padding: 16px 32px;
+  border-radius: 8px;
+  transform: translate(-50%, -50%);
+  color: #fff;
+  font-size: 16px;
+  z-index: 9999;
+}
+.fadein {
+  animation: animate_in 0.25s;
+}
+.fadeout {
+  animation: animate_out 0.25s;
+  opacity: 0;
+}
+@keyframes animate_in {
+  0% {
+    opacity: 0;
+  }
+  100% {
+    opacity: 1;
+  }
+}
+@keyframes animate_out {
+  0% {
+    opacity: 1;
+  }
+  100% {
+    opacity: 0;
+  }
+}
+</style>

+ 58 - 0
plugins/gift-friends/src/components/toast/index.js

@@ -0,0 +1,58 @@
+import vue from 'vue'
+import toastComponent from './Toast.vue'
+
+const ToastConstructor = vue.extend(toastComponent)
+
+let ToastHistory = {}
+
+// 定义弹出组件的函数 接收2个参数, 要显示的文本 和 显示时间
+export function showToast(text, duration = 2000) {
+  if (ToastHistory.el) {
+    ToastHistory.destory()
+  }
+  // 实例化一个 toast.vue
+  const toastDom = new ToastConstructor({
+    el: document.createElement('div'),
+    data() {
+      return {
+        text,
+        showWrap: true,
+        showContent: true
+      }
+    }
+  })
+  // 把 实例化的 toast.vue 添加到 body 里
+  try {
+    this.$root.$el.appendChild(toastDom.$el)
+  }
+  catch (error) {
+    document.body.appendChild(toastDom.$el)
+  }
+  return new Promise((resolve, reject) => {
+    // 提前 250ms 执行淡出动画(因为我们再css里面设置的隐藏动画持续是250ms)
+    const tFn1 = setTimeout(() => {
+      toastDom.showContent = false
+    }, duration - 250)
+    // 过了 duration 时间后隐藏整个组件
+    const tFn2 = setTimeout(() => {
+      toastDom.showWrap = false
+      resolve()
+    }, duration)
+
+    ToastHistory = {
+      el: toastDom.$el,
+      destory: () => {
+        clearTimeout(tFn1)
+        clearTimeout(tFn2)
+        toastDom.$el.remove()
+      }
+    }
+  })
+}
+
+// 注册为全局组件的函数
+function registryToast() {
+  vue.prototype.$toast = showToast
+}
+
+export default registryToast

+ 48 - 0
plugins/gift-friends/src/entry.js

@@ -0,0 +1,48 @@
+/**
+ * description: 打包入口文件,可输出js插件供外部html调用
+ */
+
+import GiftFriendsDialog from './components/GiftSubmitDialog.vue'
+import './assets/style/dialog.css'
+import './assets/style/gift.css'
+
+function install(Vue) {
+  // 注册全局组件
+  Vue.component('gift-friends-dialog', GiftFriendsDialog)
+
+  // 创建弹窗实例
+  const ModalConstructor = Vue.extend(GiftFriendsDialog)
+  Vue.prototype.$GiftFriendsDialog = 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,
+  GiftFriendsDialog
+}

+ 15 - 0
plugins/gift-friends/src/index.js

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

+ 17 - 0
plugins/gift-friends/src/main.js

@@ -0,0 +1,17 @@
+import Vue from 'vue'
+import ElementUI from 'element-ui'
+import App from './App.vue'
+import router from './router'
+import GiftFriendsDialogPlugin from './utils/plugins/index.js'
+import Toast from './components/toast/index'
+import 'element-ui/lib/theme-chalk/index.css'
+import './assets/style/common.scss'
+import './assets/style/reset-ele.scss'
+import './assets/style/dialog.css'
+
+Vue.use(Toast).use(ElementUI).use(GiftFriendsDialogPlugin)
+
+new Vue({
+  router,
+  render: h => h(App)
+}).$mount('#app')

+ 25 - 0
plugins/gift-friends/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

+ 41 - 0
plugins/gift-friends/src/utils/plugins/index.js

@@ -0,0 +1,41 @@
+import GiftFriendsDialog from '../../components/GiftSubmitDialog.vue'
+import '../../assets/style/gift.css'
+
+const GiftFriendsDialogPlugin = {
+  install(Vue) {
+    // 注册全局组件
+    Vue.component('gift-friends-dialog', GiftFriendsDialog)
+
+    const DialogConstructor = Vue.extend(GiftFriendsDialog)
+    Vue.prototype.$GiftFriendsDialog = 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 GiftFriendsDialogPlugin

+ 53 - 0
plugins/gift-friends/src/views/test.vue

@@ -0,0 +1,53 @@
+<template>
+  <div class="btn-group">
+    <button class="btn" @click="handle">
+      手动触发绑定弹框
+    </button>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'TestPage',
+  data() {
+    return {
+      visible: false
+    }
+  },
+  methods: {
+    // 手动触发绑定弹框
+    handle() {
+      // 打开弹框
+      this.$GiftFriendsDialog({
+        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/gift-friends/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: 'GiftFriends', // 库的全局变量名
+        fileName: 'jy-gift-friends' // 输出的文件名
+      },
+      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://jybx-webtest.jydev.jianyu360.com/',
+          changeOrigin: true,
+          rewrite: path => path.replace(/^\/jyapi/, '')
+        },
+        '/api': {
+          target: 'https://jybx-webtest.jydev.jianyu360.com/',
+          changeOrigin: true,
+          rewrite: path => path.replace(/^\/api/, '')
+        }
+      }
+    }
+  }
+})

+ 84 - 0
pnpm-lock.yaml

@@ -84,6 +84,9 @@ importers:
       '@jy/pc-ui':
         specifier: workspace:^
         version: link:../../packages/pc-ui
+      '@jy/plugin-gift-friends':
+        specifier: workspace:^
+        version: link:../../plugins/gift-friends
       '@jy/util':
         specifier: workspace:^
         version: link:../../packages/util
@@ -803,6 +806,73 @@ importers:
         specifier: ^2.6.14
         version: 2.7.16
 
+  plugins/gift-friends:
+    dependencies:
+      element-ui:
+        specifier: ^2.15.14-rc
+        version: 2.15.14(vue@2.7.16)
+      qs:
+        specifier: ^6.11.2
+        version: 6.14.0
+    devDependencies:
+      '@jonny1994/postcss-px-to-viewport':
+        specifier: ^1.1.0
+        version: 1.1.0(postcss@8.5.3)
+      '@nabla/vite-plugin-eslint':
+        specifier: ^2.0.2
+        version: 2.0.5(eslint@8.57.1)(vite@4.5.9)
+      '@rushstack/eslint-patch':
+        specifier: ^1.1.0
+        version: 1.10.5
+      '@vitejs/plugin-vue2':
+        specifier: ^2.2.0
+        version: 2.3.3(vite@4.5.9)(vue@2.7.16)
+      '@vue/eslint-config-prettier':
+        specifier: ^7.0.0
+        version: 7.1.0(eslint@8.57.1)(prettier@2.8.8)
+      autoprefixer:
+        specifier: ^10.4.14
+        version: 10.4.20(postcss@8.5.3)
+      eslint:
+        specifier: ^8.57.0
+        version: 8.57.1
+      eslint-plugin-vue:
+        specifier: ^9.22.0
+        version: 9.32.0(eslint@8.57.1)
+      less:
+        specifier: ^4.1.3
+        version: 4.2.2
+      prettier:
+        specifier: ^2.5.1
+        version: 2.8.8
+      rollup-plugin-visualizer:
+        specifier: ^5.9.2
+        version: 5.14.0(rollup@0.58.2)
+      sass:
+        specifier: ^1.63.2
+        version: 1.85.1
+      terser:
+        specifier: ^5.14.2
+        version: 5.39.0
+      unplugin-vue-components:
+        specifier: ^0.25.1
+        version: 0.25.2(rollup@0.58.2)(vue@2.7.16)
+      vite:
+        specifier: ^4.5.3
+        version: 4.5.9(less@4.2.2)(sass@1.85.1)(terser@5.39.0)
+      vite-plugin-compression:
+        specifier: ^0.5.1
+        version: 0.5.1(vite@4.5.9)
+      vite-plugin-css-injected-by-js:
+        specifier: ^3.1.0
+        version: 3.5.2(vite@4.5.9)
+      vite-plugin-ejs:
+        specifier: 1.6.4
+        version: 1.6.4
+      vite-plugin-externals:
+        specifier: ^0.6.2
+        version: 0.6.2(vite@4.5.9)
+
   plugins/login-auth:
     dependencies:
       '@jy/emiiter':
@@ -8168,6 +8238,20 @@ packages:
     resolution: {integrity: sha512-Us9M2L4cO/zMBqVkJtnj353nQhMju9slHm62NprKTmdF3HH8wYOtNvDFq/JB2+ZRoGLzdvYDiATlMHs98XBM1g==}
     dev: true
 
+  /element-ui@2.15.14(vue@2.7.16):
+    resolution: {integrity: sha512-2v9fHL0ZGINotOlRIAJD5YuVB8V7WKxrE9Qy7dXhRipa035+kF7WuU/z+tEmLVPBcJ0zt8mOu1DKpWcVzBK8IA==}
+    peerDependencies:
+      vue: ^2.5.17
+    dependencies:
+      async-validator: 1.8.5
+      babel-helper-vue-jsx-merge-props: 2.0.3
+      deepmerge: 1.5.2
+      normalize-wheel: 1.0.1
+      resize-observer-polyfill: 1.5.1
+      throttle-debounce: 1.1.0
+      vue: 2.7.16
+    dev: false
+
   /element-ui@2.15.23-rc(vue@2.7.16):
     resolution: {integrity: sha512-mWd/5eVhzBBQ4cX0ZI1PRo7d4jq4v8rj2dIK+edzHg+vSS4Y/KAl/Sq5XXvySu3q42L/gPx7DGR+ZLgNTfe4BQ==}
     peerDependencies: