yuelujie hace 4 meses
padre
commit
a0fcd80904
Se han modificado 86 ficheros con 7395 adiciones y 587 borrados
  1. 6 2
      apps/bigmember_pc/package.json
  2. 54 1
      apps/bigmember_pc/src/api/modules/pay.js
  3. BIN
      apps/bigmember_pc/src/assets/images/gift-record/vip-bg.png
  4. BIN
      apps/bigmember_pc/src/assets/images/gift-record/vip-product.png
  5. BIN
      apps/bigmember_pc/src/assets/images/icon/postSet.png
  6. BIN
      apps/bigmember_pc/src/assets/images/tell-example.png
  7. 46 0
      apps/bigmember_pc/src/assets/js/vueWaterfallEasy.js
  8. 105 37
      apps/bigmember_pc/src/components/coupon/BuySubmit.vue
  9. 73 26
      apps/bigmember_pc/src/components/coupon/BuySubmitSticky.vue
  10. 5 5
      apps/bigmember_pc/src/components/dialog/Dialog.vue
  11. 504 0
      apps/bigmember_pc/src/components/dialog/GiftSubmitDialog.vue
  12. 280 0
      apps/bigmember_pc/src/components/dialog/NotifyFriendsDialog.vue
  13. 9 5
      apps/bigmember_pc/src/main.js
  14. 23 13
      apps/bigmember_pc/src/router/router-interceptors.js
  15. 12 0
      apps/bigmember_pc/src/router/svip-routers.js
  16. 5 3
      apps/bigmember_pc/src/store/workspace.js
  17. 33 0
      apps/bigmember_pc/src/store/workspace/gift-friends.js
  18. 70 0
      apps/bigmember_pc/src/views/gift-record/ProductNotice.vue
  19. 109 0
      apps/bigmember_pc/src/views/gift-record/components/MyGiftRecord.vue
  20. 63 0
      apps/bigmember_pc/src/views/gift-record/components/MyReceiveRecord.vue
  21. 367 0
      apps/bigmember_pc/src/views/gift-record/index.vue
  22. 23 10
      apps/bigmember_pc/src/views/order/components/common/footer.vue
  23. 191 118
      apps/bigmember_pc/src/views/order/components/vipsubscribe/info.vue
  24. 24 10
      apps/bigmember_pc/src/views/portrayal/Loading.vue
  25. 105 85
      apps/bigmember_pc/src/views/search/purchase/model/base.js
  26. 245 178
      apps/bigmember_pc/src/views/vipsubscribe/Buy.vue
  27. 161 61
      apps/bigmember_pc/src/views/workspace/components/AccountInfo.vue
  28. 111 20
      apps/bigmember_pc/src/views/workspace/dashboard.vue
  29. 56 0
      apps/mobile/src/api/modules/pay.js
  30. BIN
      apps/mobile/src/assets/image/icon/circle-plus@2x.png
  31. BIN
      apps/mobile/src/assets/image/icon/delete@2x.png
  32. BIN
      apps/mobile/src/assets/image/icon/notify@3x.png
  33. BIN
      apps/mobile/src/assets/image/vip-subscribe/poster-banner.png
  34. BIN
      apps/mobile/src/assets/image/vip-subscribe/poster-save.png
  35. 14 0
      apps/mobile/src/assets/style/pic-icon.scss
  36. 131 6
      apps/mobile/src/components/mine/MineHeader.vue
  37. 5 0
      apps/mobile/src/data/links.js
  38. 38 0
      apps/mobile/src/router/modules/giving.js
  39. 3 1
      apps/mobile/src/router/modules/order.js
  40. 9 0
      apps/mobile/src/router/modules/static.js
  41. 3 1
      apps/mobile/src/store/index.js
  42. 9 1
      apps/mobile/src/store/modules/createOrder.js
  43. 24 0
      apps/mobile/src/utils/utils.js
  44. 474 0
      apps/mobile/src/views/giving/friend.vue
  45. 364 0
      apps/mobile/src/views/giving/notify.vue
  46. 332 0
      apps/mobile/src/views/giving/record.vue
  47. 222 0
      apps/mobile/src/views/giving/share.vue
  48. 27 0
      apps/mobile/src/views/order/components/vipsubscribe/FooterNoticeBar.vue
  49. 95 4
      apps/mobile/src/views/order/components/vipsubscribe/Introduction.vue
  50. 39 0
      apps/mobile/src/views/static/SVipGiftNotice.vue
  51. 5 0
      plugins/gift-friends/.browserslistrc
  52. 5 0
      plugins/gift-friends/.editorconfig
  53. 7 0
      plugins/gift-friends/.env.development
  54. 7 0
      plugins/gift-friends/.env.production
  55. 16 0
      plugins/gift-friends/.eslintignore
  56. 42 0
      plugins/gift-friends/.gitignore
  57. 5 0
      plugins/gift-friends/.npmrc
  58. 138 0
      plugins/gift-friends/README.md
  59. 89 0
      plugins/gift-friends/index.html
  60. 44 0
      plugins/gift-friends/package.json
  61. 30 0
      plugins/gift-friends/postcss.config.js
  62. 47 0
      plugins/gift-friends/src/App.vue
  63. 29 0
      plugins/gift-friends/src/api/api.js
  64. 4 0
      plugins/gift-friends/src/api/index.js
  65. 46 0
      plugins/gift-friends/src/api/interceptors.js
  66. 5 0
      plugins/gift-friends/src/api/service.js
  67. BIN
      plugins/gift-friends/src/assets/images/icon-delete.png
  68. 32 0
      plugins/gift-friends/src/assets/style/_mixin.scss
  69. 35 0
      plugins/gift-friends/src/assets/style/_variables.scss
  70. 310 0
      plugins/gift-friends/src/assets/style/common.scss
  71. 57 0
      plugins/gift-friends/src/assets/style/dialog.css
  72. 211 0
      plugins/gift-friends/src/assets/style/gift.css
  73. 91 0
      plugins/gift-friends/src/assets/style/pic-icon.scss
  74. 353 0
      plugins/gift-friends/src/assets/style/reset-ele.scss
  75. 175 0
      plugins/gift-friends/src/components/Dialog.vue
  76. 433 0
      plugins/gift-friends/src/components/GiftSubmitDialog.vue
  77. 43 0
      plugins/gift-friends/src/components/toast/Toast.vue
  78. 58 0
      plugins/gift-friends/src/components/toast/index.js
  79. 51 0
      plugins/gift-friends/src/entry.js
  80. 13 0
      plugins/gift-friends/src/index.js
  81. 17 0
      plugins/gift-friends/src/main.js
  82. 25 0
      plugins/gift-friends/src/router/index.js
  83. 42 0
      plugins/gift-friends/src/utils/plugins/index.js
  84. 53 0
      plugins/gift-friends/src/views/test.vue
  85. 103 0
      plugins/gift-friends/vite.config.js
  86. 305 0
      pnpm-lock.yaml

+ 6 - 2
apps/bigmember_pc/package.json

@@ -16,21 +16,24 @@
     "@jianyu/easy-inject-qiankun": "^0.1.11",
     "@jianyu/icon": "^0.1.7",
     "@jianyu/reset.css": "~0.1.1",
-    "@jy/vue-anti": "workspace:^",
     "@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",
     "dayjs": "^1.11.7",
     "echarts": "4.8.0",
     "element-ui": "^2.15.23-rc",
     "excellentexport": "^3.8.1",
+    "html2canvas": "^1.4.1",
     "js-cookie": "^3.0.1",
     "lodash": "^4.17.21",
     "moment": "^2.29.1",
     "qs": "^6.11.2",
     "svga": "^2.0.6",
     "v-charts": "1.19.0",
+    "vue-clipboard2": "^0.3.3",
     "vue-cookies": "^1.7.4",
     "vue-meta-info": "^0.1.7",
     "vuex": "^3.6.2"
@@ -54,6 +57,7 @@
     "vite-plugin-ejs": "1.6.4",
     "vite-plugin-eslint": "^1.8.1",
     "vite-plugin-externals": "^0.6.2",
-    "vite-plugin-legacy-qiankun": "^0.0.12"
+    "vite-plugin-legacy-qiankun": "^0.0.12",
+    "vue-waterfall-easy": "^2.4.4"
   }
 }

+ 54 - 1
apps/bigmember_pc/src/api/modules/pay.js

@@ -1,5 +1,5 @@
-import request from '@/api'
 import qs from 'qs'
+import request from '@/api'
 
 export function getPhoneCaptcha() {
   return request({
@@ -75,3 +75,56 @@ export function getPDFPackBalance() {
     method: 'post',
   })
 }
+
+// 超级订阅赠送
+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)
+  })
+}
+
+// 超级订阅赠送活动时间
+export function getConfigurationTime() {
+  return request({
+    url: '/subscribepay/vip/gift/configuration',
+    method: 'post'
+  })
+}
+
+// 超级订阅赠送记录查询
+export function getGiftRecordList(data) {
+  return request({
+    url: '/subscribepay/vip/gift/list',
+    method: 'post',
+    data: qs.stringify(data)
+  })
+}
+
+// 超级订阅赠送记录查询详情
+export function getGiftRecordDetail(data) {
+  return request({
+    url: '/subscribepay/vip/gift/informInfo',
+    method: 'post',
+    data: qs.stringify(data)
+  })
+}

BIN
apps/bigmember_pc/src/assets/images/gift-record/vip-bg.png


BIN
apps/bigmember_pc/src/assets/images/gift-record/vip-product.png


BIN
apps/bigmember_pc/src/assets/images/icon/postSet.png


BIN
apps/bigmember_pc/src/assets/images/tell-example.png


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 46 - 0
apps/bigmember_pc/src/assets/js/vueWaterfallEasy.js


+ 105 - 37
apps/bigmember_pc/src/components/coupon/BuySubmit.vue

@@ -3,22 +3,34 @@
     <div class="price-preview">
       <slot name="preview">
         <div class="preview-item total">
-          <div class="p-label">商品总价:</div>
-          <div class="p-value">&yen; {{ calcTotal }}</div>
+          <div class="p-label">
+            商品总价:
+          </div>
+          <div class="p-value">
+            &yen; {{ calcTotal }}
+          </div>
         </div>
-        <div class="preview-item discount" v-if="productionDiscount">
-          <div class="p-label">优惠金额:</div>
-          <div class="p-value">-&yen; {{ calcDiscount }}</div>
+        <div v-if="productionDiscount" class="preview-item discount">
+          <div class="p-label">
+            优惠金额:
+          </div>
+          <div class="p-value">
+            -&yen; {{ calcDiscount }}
+          </div>
         </div>
         <div class="preview-item pay">
-          <div class="p-label">实付金额:</div>
-          <div class="p-value">&yen; {{ calcPay }}</div>
+          <div class="p-label">
+            实付金额:
+          </div>
+          <div class="p-value">
+            &yen; {{ calcPay }}
+          </div>
         </div>
       </slot>
     </div>
     <div class="price-buy-info-group">
       <div>
-        <slot name="buy-tip-group"></slot>
+        <slot name="buy-tip-group" />
       </div>
       <div>
         <div class="price-agreement">
@@ -26,25 +38,30 @@
             <span>已阅读并同意</span>
             <div class="links">
               <el-link
-                type="primary"
                 v-for="(link, index) in agreementLinks"
                 :key="index"
+                type="primary"
                 target="_black"
                 :underline="linkUnderline"
                 :href="link.url"
-                >{{ link.label }}</el-link
               >
+                {{ link.label }}
+              </el-link>
             </div>
           </el-checkbox>
         </div>
         <div class="price-submit-container">
+          <div v-if="showGift" class="submit-button-tip-text">
+            支持送好友超级订阅,快快购买后送给好友吧!
+          </div>
           <el-button
+            v-show="plainButtonText"
             plain
             class="submit-button preview-button"
             @click="onCancel"
-            v-show="plainButtonText"
-            >{{ plainButtonText }}</el-button
           >
+            {{ plainButtonText }}
+          </el-button>
           <el-button
             type="primary"
             class="submit-button price-submit"
@@ -52,46 +69,51 @@
             :loading="loading"
             @click="buySubmit"
           >
-            <div class="confirm-button-text">{{ submitText }}</div>
+            <div class="confirm-button-text">
+              {{ submitText }}
+            </div>
             <!-- <div class="confirm-button-tip-text" v-if="submitTipText">{{submitTipText}}</div> -->
           </el-button>
         </div>
-        <div class="confirm-button-tip-text in-page" v-if="submitTipText">
+        <div v-if="submitTipText" class="confirm-button-tip-text in-page">
           完成支付后可在【我的订单】中开电子发票
         </div>
       </div>
     </div>
     <BuySubmitSticky
-      v-model="agreement"
       v-if="useStickyBar"
-      :linkUnderline="linkUnderline"
-      :confirmButtonDisabled="confirmButtonDisabled"
-      :productionTotal="calcTotal"
-      :productionPay="calcPay"
-      :plainButtonText="plainButtonText"
-      :agreementLinks="agreementLinks"
+      v-model="agreement"
+      :link-underline="linkUnderline"
+      :confirm-button-disabled="confirmButtonDisabled"
+      :production-total="calcTotal"
+      :production-pay="calcPay"
+      :plain-button-text="plainButtonText"
+      :agreement-links="agreementLinks"
       :submit-text="submitText"
-      :submitTipText="submitTipText"
+      :submit-tip-text="submitTipText"
+      :buy-type="buyType"
       :loading="loading"
-      :basicSelector="basicSelector"
-      :alwaysShowSticky= "alwaysShowSticky"
+      :basic-selector="basicSelector"
+      :always-show-sticky="alwaysShowSticky"
+      :is-show-gift="showGift"
       @cancel="onCancel"
       @submit="buySubmit"
     >
       <template #sticky-footer-tip>
-        <slot name="sticky-footer-tip"></slot>
+        <slot name="sticky-footer-tip" />
       </template>
     </BuySubmitSticky>
   </div>
 </template>
 
 <script>
-import { Checkbox, Link, Button } from 'element-ui'
+import { mapActions, mapState } from 'vuex'
+import { Button, Checkbox, Link } from 'element-ui'
 import { formatPrice } from '@/utils/'
 import BuySubmitSticky from '@/components/coupon/BuySubmitSticky.vue'
 
 export default {
-  name: 'buy-submit',
+  name: 'BuySubmit',
   components: {
     [Checkbox.name]: Checkbox,
     [Link.name]: Link,
@@ -150,9 +172,26 @@ export default {
     basicSelector: {
       type: String,
       default: '.sticky-basic'
+    },
+    buyType: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      agreementLinks: [
+        {
+          label: '《剑鱼标讯线上购买与服务条款》',
+          url: '/front/staticPage/serviceterms.html'
+        }
+      ],
+      agreement: false,
+      isShowGift: false
     }
   },
   computed: {
+    ...mapState('user', ['identityList']),
     calcTotal() {
       return this.useFormatPrice
         ? this.formatPrice(this.productionTotal, -1, true)
@@ -171,21 +210,44 @@ export default {
     confirmButtonDisabled() {
       const state = this.pass && this.agreement
       return !state
+    },
+    showGift() {
+      let isPerson = false
+      if (this.identityList && this.identityList.length > 0) {
+        this.identityList.forEach((item) => {
+          if (item.checked === 1) {
+            if (item.positionType === 0) {
+              isPerson = true
+            }
+            else {
+              isPerson = false
+            }
+          }
+        })
+      }
+      const path = this.$route.path
+      const isVipPath = path.includes('/free/svip')
+      return this.isShowGift && this.buyType === 'buy' && isPerson && isVipPath
     }
   },
-  data() {
-    return {
-      agreementLinks: [
-        {
-          label: '《剑鱼标讯线上购买与服务条款》',
-          url: '/front/staticPage/serviceterms.html'
-        }
-      ],
-      agreement: false
-    }
+  created() {
+    this.fetchConfigTime()
   },
+
   methods: {
     formatPrice,
+    ...mapActions('workspace/giftFriends', ['getConfigTime']),
+
+    fetchConfigTime() {
+      try {
+        this.getConfigTime().then((res) => {
+          this.isShowGift = res || false
+        })
+      }
+      catch (error) {
+        console.error('加载配置时间失败:', error)
+      }
+    },
     onCancel() {
       this.$emit('cancel')
     },
@@ -266,6 +328,12 @@ $main: #2cb7ca;
     display: flex;
     align-items: center;
     justify-content: flex-end;
+    .submit-button-tip-text {
+      margin-right: 6px;
+      line-height: 24px;
+      font-size: 14px;
+      color: #2ABED1;
+    }
     .submit-button {
       display: flex;
       align-items: center;

+ 73 - 26
apps/bigmember_pc/src/components/coupon/BuySubmitSticky.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="buy-submit-sticky sticky-footer">
     <div class="sticky-footer-tip-container">
-      <slot name="sticky-footer-tip"></slot>
+      <slot name="sticky-footer-tip" />
     </div>
     <div class="sticky-content flex-r-c v-w1200">
       <div class="price-agreement">
@@ -9,37 +9,54 @@
           <span>已阅读并同意</span>
           <div class="links">
             <el-link
-              type="primary"
               v-for="(link, index) in agreementLinks"
               :key="index"
+              type="primary"
               target="_black"
               :underline="linkUnderline"
               :href="link.url"
-              >{{ link.label }}</el-link
             >
+              {{ link.label }}
+            </el-link>
           </div>
         </el-checkbox>
       </div>
       <div class="price-preview flex-r-c">
         <slot name="preview">
-          <div class="preview-item total">
-            <div class="p-label">商品总价:</div>
-            <div class="p-value">&yen; {{ productionTotal }}</div>
+          <div class="preview-item-top">
+            <div class="preview-item total">
+              <div class="p-label">
+                商品总价:
+              </div>
+              <div class="p-value">
+                &yen; {{ productionTotal }}
+              </div>
+            </div>
+            <div class="preview-item pay">
+              <div class="p-label">
+                实付金额:
+              </div>
+              <div class="p-value">
+                &yen; {{ productionPay }}
+              </div>
+            </div>
           </div>
-          <div class="preview-item pay">
-            <div class="p-label">实付金额:</div>
-            <div class="p-value">&yen; {{ productionPay }}</div>
+          <div v-if="isShowGift" class="preview-item-bottom">
+            <div class="preview-item-tip-text">
+              支持送好友超级订阅,快快购买后送给好友吧!
+            </div>
           </div>
         </slot>
       </div>
       <div class="price-submit-container">
         <el-button
+          v-show="plainButtonText"
           plain
           class="submit-button preview-button"
           @click="onCancel"
-          v-show="plainButtonText"
-          >{{ plainButtonText }}</el-button
         >
+          {{ plainButtonText }}
+        </el-button>
         <el-button
           type="primary"
           class="submit-button price-submit"
@@ -48,8 +65,10 @@
           @click="buySubmit"
         >
           <div class="button-content">
-            <div class="confirm-button-text">{{ submitText }}</div>
-            <div class="confirm-button-tip-text" v-if="submitTipText">
+            <div class="confirm-button-text">
+              {{ submitText }}
+            </div>
+            <div v-if="submitTipText" class="confirm-button-tip-text">
               {{ submitTipText }}
             </div>
           </div>
@@ -60,11 +79,11 @@
 </template>
 
 <script>
-import { Checkbox, Link, Button } from 'element-ui'
+import { Button, Checkbox, Link } from 'element-ui'
 import { debounce, throttle } from 'lodash'
 
 export default {
-  name: 'buy-submit-sticky',
+  name: 'BuySubmitSticky',
   components: {
     [Checkbox.name]: Checkbox,
     [Link.name]: Link,
@@ -128,6 +147,14 @@ export default {
     alwaysShowSticky: {
       type: Boolean,
       default: false
+    },
+    isShowGift: {
+      type: Boolean,
+      default: false
+    },
+    buyType: {
+      type: String,
+      default: 'buy'
     }
   },
   mounted() {
@@ -167,7 +194,7 @@ export default {
       }
     }, 50),
     appendDomToContainer(el, selector) {
-      var container
+      let container
       if (selector) {
         container = this.$root.$el.querySelector(selector) || this.$root.$el
       }
@@ -177,7 +204,7 @@ export default {
       container.appendChild(el)
     },
     removeDomFromContainer(el, selector) {
-      var container
+      let container
       if (selector) {
         container = this.$root.$el.querySelector(selector)
       }
@@ -186,13 +213,15 @@ export default {
       }
       try {
         container.removeChild(el)
-      } catch (error) {
+      }
+      catch (error) {
         // console.log(error)
       }
     },
     // 是否某个dom有部分离开当前屏
     isDOMLeave(el) {
-      if (!el) return
+      if (!el)
+        return
       const x = 64 // 头部偏移
       const offset = el.getBoundingClientRect()
       const offsetTop = offset.top
@@ -203,7 +232,8 @@ export default {
       let leave = false
       if (offsetTop - x < 0) {
         leave = true
-      } else {
+      }
+      else {
         // window.innerHeight 视口高度
         if (offsetBottom > window.innerHeight) {
           leave = true
@@ -213,7 +243,8 @@ export default {
     },
     // 是否在el的一部分在可视区域
     isInViewport(el) {
-      if (!el) return
+      if (!el)
+        return
       const offset = el.getBoundingClientRect()
       const x = 64 // 头部偏移
       const offsetTop = offset.top + x
@@ -230,7 +261,8 @@ export default {
       const mainFooter = $(this.basicSelector)
       const stickyFooter = $(this.$el)
 
-      if (!mainFooter.length) return
+      if (!mainFooter.length)
+        return
 
       const show = this.alwaysShowSticky || !this.isInViewport(mainFooter[0])
       if (show) {
@@ -246,11 +278,13 @@ export default {
         // bottom出现在视口
         const bottom = window.innerHeight - ob.top
         if (bottom > 0 && ob.top !== 0) {
-          $(this.$el).css({ bottom: parseInt(bottom) })
-        } else {
+          $(this.$el).css({ bottom: Number.parseInt(bottom) })
+        }
+        else {
           $(this.$el).css({ bottom: 0 })
         }
-      } else {
+      }
+      else {
         stickyFooter.hide()
       }
     }, 300),
@@ -293,9 +327,22 @@ $color_main: #2cb7ca;
     padding: 12px 0;
   }
   .price-preview {
+    display: flex;
+    flex-direction: column;
     flex: 1;
-    justify-content: flex-end;
+    justify-content: center;
     padding: 0 22px;
+    .preview-item-top {
+      display: flex;
+      justify-content: flex-end;
+      line-height: 32px;
+    }
+    .preview-item-bottom {
+      text-align: right;
+      line-height: 24px;
+      font-size: 14px;
+      color: #2ABED1;
+    }
     .preview-item {
       display: flex;
       align-items: center;

+ 5 - 5
apps/bigmember_pc/src/components/dialog/Dialog.vue

@@ -1,5 +1,6 @@
 <template>
   <el-dialog
+    v-component-change-mount="{ selector: comMount }"
     class="custom-dialog"
     :custom-class="customClass"
     v-bind="$props"
@@ -10,14 +11,13 @@
     :destroy-on-close="destroyOnClose"
     :before-close="beforeClose"
     @update:visible="update"
-    v-component-change-mount="{ selector: comMount }"
     @open="$emit('open')"
     @opened="$emit('opened')"
     @close="$emit('close')"
     @closed="$emit('closed')"
   >
-    <slot name="default"></slot>
-    <span slot="footer" v-if="showFooter" class="dialog-footer">
+    <slot name="default" />
+    <span v-if="showFooter" slot="footer" class="dialog-footer">
       <slot name="footer">
         <button class="action-button cancel" @click="onClickCancel">
           取消
@@ -35,7 +35,7 @@
 </template>
 
 <script>
-import { Dialog, Button } from 'element-ui'
+import { Button, Dialog } from 'element-ui'
 
 export default {
   name: 'CustomDialog',
@@ -66,7 +66,7 @@ export default {
       type: String,
       default: '30%'
     },
-    'show-close': {
+    showClose: {
       type: Boolean,
       default: false
     },

+ 504 - 0
apps/bigmember_pc/src/components/dialog/GiftSubmitDialog.vue

@@ -0,0 +1,504 @@
+<template>
+  <CustomDialog
+    width="600px"
+    top="18vh"
+    :title="title"
+    class="gift-submit-dialog"
+    :visible="visible"
+  >
+    <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" class="phone-no-register-tip">
+            {{ statusMessages[item.status] }}
+          </div>
+          <div v-else 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" class="dialog-footer-wrapper">
+      <button
+        class="action-button confirm"
+        :disabled="!isFormValid"
+        @click="onClickConfirm"
+      >
+        提交
+      </button>
+      <button class="action-button cancel" @click="$emit('close')">
+        取消
+      </button>
+    </span>
+  </CustomDialog>
+</template>
+
+<script>
+import CustomDialog from '@/components/dialog/Dialog.vue'
+import { getInfoByPhone, getSubDuration, setTransferSubDuration } from '@/api/modules/'
+
+export default {
+  name: 'GiftSubmitDialog',
+  components: {
+    CustomDialog
+  },
+  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 {
+      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: {}
+    }
+  },
+  computed: {
+    isFormValid() {
+      return this.personList.every(item => item.phoneValid && item.monthnumValid)
+    },
+    monthNumTotal() {
+      return this.personList.reduce((total, item) => {
+        return total + Number(item.monthnum) || 0
+      }, 0)
+    },
+    // 剩余可赠送时长
+    getGifted() {
+      return Number(this.subduration.gifted) - this.monthNumTotal || 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') {
+            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.confirmGiftData()
+      }
+    },
+    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()
+    }
+  }
+}
+</script>
+
+<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>

+ 280 - 0
apps/bigmember_pc/src/components/dialog/NotifyFriendsDialog.vue

@@ -0,0 +1,280 @@
+<template>
+  <CustomDialog
+    v-bind="$props"
+    width="564px"
+    top="18vh"
+    :title="title"
+    class="notify-friends-dialog"
+    :visible="visible"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    @update:visible="update"
+  >
+    <div class="methods-content">
+      <div class="methods-item">
+        <div class="methods-item-title">
+          方法1:复制链接后发给微信好友
+        </div>
+        <div class="methods-item-content">
+          <div class="methods-item-content-input">
+            <el-input v-model="localInfo.urlLink" :readonly="true" placeholder="请输入内容" />
+            <el-button class="copy-btn" type="primary" @click="copyLinkEvent">
+              复制链接
+            </el-button>
+          </div>
+        </div>
+      </div>
+      <div class="methods-item">
+        <div class="methods-item-title">
+          方法2:下载海报发给微信好友
+        </div>
+        <div v-if="!canvasImg" ref="contentToCapture" class="methods-item-content">
+          <img src="@/assets/images/gift-record/vip-product.png?v=1" alt="">
+          <div class="methods-item-content-info">
+            <img src="@/assets/images/auto.png" alt="">
+            <div class="methods-item-content-info-text">
+              <div class="phone-text">
+                {{ localInfo.giftUserPhone }}
+              </div>
+              <div class="gift-text">
+                送给{{ localInfo.recipientUserPhone }}超级订阅会员,{{ localInfo.duration }}个月内可免费获取全行业招采信息
+              </div>
+            </div>
+            <div class="methods-item-content-info-qrcode">
+              <img :src="localInfo.qRCodeLink" alt="">
+            </div>
+          </div>
+        </div>
+        <div v-else class="methods-item-content-canvas">
+          <img :src="canvasImg" alt="">
+        </div>
+        <div class="methods-item-content-info-download">
+          点击右键保存到本地
+        </div>
+      </div>
+    </div>
+    <template #footer>
+      <slot name="footer">
+        <button
+          class="action-button confirm"
+          @click="onClickConfirm"
+        >
+          我知道了
+        </button>
+      </slot>
+    </template>
+  </CustomDialog>
+</template>
+
+<script>
+import { Button, Input } from 'element-ui'
+import html2canvas from 'html2canvas'
+import CustomDialog from '@/components/dialog/Dialog.vue'
+
+export default {
+  name: 'NotifyFriendsDialog',
+  components: {
+    [Button.name]: Button,
+    [Input.name]: Input,
+    CustomDialog
+  },
+  props: {
+    visible: Boolean,
+    title: {
+      type: String,
+      default: '告知好友'
+    },
+    info: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  data() {
+    return {
+      localInfo: {
+        qRCodeLink: '',
+        ...this.info
+      },
+      canvasImg: ''
+    }
+  },
+  watch: {
+    info: {
+      handler(newVal) {
+        this.localInfo = { ...newVal } // 当 info 更新时同步到本地数据
+      },
+      deep: true
+    },
+    visible(newVal) {
+      if (newVal) {
+        this.$nextTick(() => {
+          this.generateImage()
+        })
+      }
+    }
+  },
+  methods: {
+    update(e) {
+      this.$emit('update:visible', e)
+    },
+    onClickConfirm() {
+      this.canvasImg = ''
+      this.update(false)
+    },
+    copyLinkEvent() {
+      const textLink = `好友${this.localInfo.giftUserPhone}送你超级订阅会员,${this.localInfo.duration}个月内可免费获取全行业招采信息。${this.localInfo.urlLink}`
+      this.$copyText(textLink).then(() => {
+        this.$toast('复制成功')
+      })
+    },
+    generateImage() {
+      const element = this.$refs.contentToCapture
+      if (!element)
+        return
+
+      // 添加资源代理配置
+      html2canvas(element, {
+        ignoreElements: el => el.tagName === 'LINK', // 忽略外部CSS文件
+        useCORS: true,
+        logging: false,
+        allowTaint: false,
+        onclone: (clonedDoc) => {
+          // 移除可能引发请求的link标签
+          clonedDoc.querySelectorAll('link').forEach(link => link.remove())
+        }
+      }).then((canvas) => {
+        this.canvasImg = canvas.toDataURL('image/png')
+      }).catch(console.error)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.notify-friends-dialog {
+  ::v-deep {
+    .el-dialog__header {
+      padding: 32px 32px 0;
+    }
+    .el-dialog__body {
+      padding: 0 32px 32px;
+    }
+    .el-dialog__footer {
+      padding: 0 0 32px;
+    }
+    .dialog-footer {
+      justify-content: center;
+      .action-button {
+        flex: none;
+        width: 132px;
+        height: 36px;
+        border-radius: 6px;
+        font-size: 16px;
+        line-height: 24px;
+        color: #FFFFFF;
+        background-color: #2ABED1;
+      }
+    }
+  }
+  .methods-content {
+    overflow: hidden;
+  }
+  .methods-item {
+    position: relative;
+    margin-top: 20px;
+  }
+  .methods-item-title {
+    margin-bottom: 12px;
+    line-height: 22px;
+    font-size: 14px;
+    color: #1D1D1D;
+  }
+  .methods-item-content {
+    border-radius: 16px;
+    overflow: hidden;
+    .methods-item-content-input {
+      position: relative;
+      .el-input {
+        padding: 6px 20px;
+        border-radius: 30px;
+        border: 1px solid #E3E4E5;
+        line-height: 22px;
+        font-size: 14px;
+        color: #1D1D1D;
+        ::v-deep {
+          .el-input__inner {
+            width: 324px;
+            height: 22px;
+            white-space: nowrap;
+            text-overflow: ellipsis;
+            border: none;
+            line-height: 22px;
+            padding: 0;
+          }
+        }
+      }
+      .copy-btn {
+        position: absolute;
+        right: 0;
+        padding: 0;
+        width: 140px;
+        height: 36px;
+        border-radius: 21px;
+        font-size: 16px;
+        line-height: 24px;
+        color: #FFFFFF;
+      }
+    }
+    .methods-item-content-info {
+      display: flex;
+      padding: 16px 16px 40px;
+      background: linear-gradient(to bottom, #FFFBF2, #FFF7E8);
+      >img {
+        margin: 6px 8px 0 0;
+        width: 48px;
+        height: 48px;
+      }
+      .methods-item-content-info-text {
+        flex: 1;
+      }
+      .phone-text {
+        margin-bottom: 8px;
+        line-height: 24px;
+        font-size: 16px;
+        color: #1D1D1D;
+      }
+      .gift-text {
+        line-height: 22px;
+        font-size: 14px;
+        color: #686868;
+      }
+      .methods-item-content-info-qrcode {
+        width: 60px;
+        height: 60px;
+        margin-left: 16px;
+        border: 1px solid rgba(0, 0, 0, .1);
+        background: #FFFFFF;
+        border-radius: 4px;
+        overflow: hidden;
+        >img {
+          width: 60px;
+          height: 60px;
+        }
+      }
+    }
+  }
+  .methods-item-content-info-download {
+    position: absolute;
+    bottom: 0;
+    left: 100px;
+    display: flex;
+    justify-content: center;
+    background: url(@/assets/images/gift-record/vip-bg.png) no-repeat;
+    background-size: 300px 24px;
+    width: 300px;
+    height: 24px;
+    line-height: 22px;
+    color: #FFFFFF;
+  }
+}
+</style>

+ 9 - 5
apps/bigmember_pc/src/main.js

@@ -1,16 +1,14 @@
 // import '@jianyu/easy-inject-qiankun/src/pre-mount.js'
 import 'virtual:uno.css'
 import Vue from 'vue'
-import App from './App.vue'
-import store from './store/'
-import router from './router/'
 import '@jianyu/reset.css/reset-pc.scss'
-import ElementUI from 'element-ui'
+import ElementUI, { Loading, Message, MessageBox } from 'element-ui'
 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 { Loading, Message, MessageBox } from 'element-ui'
+import { GiftFriendsDialogPlugin } from '@jy/plugin-gift-friends'
+
 import echarts from 'echarts'
 import './utils/jq-help'
 import ModalHelper from '@/utils/modelHlper'
@@ -21,6 +19,10 @@ import '@/utils/directive'
 import '@/utils/prototype'
 import MetaInfo from 'vue-meta-info'
 import JyIcon from '@jianyu/icon' // 需要单独引入icon/index.css
+import VueClipboard from 'vue-clipboard2'
+import App from './App.vue'
+import router from './router/'
+import store from './store/'
 import { initSentry } from './sentry'
 
 Vue.use(VueCookies)
@@ -28,6 +30,8 @@ Vue.use(Loading.directive)
 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 = []

+ 23 - 13
apps/bigmember_pc/src/router/router-interceptors.js

@@ -19,6 +19,7 @@ const powerCheckPathWhiteRegList = [
   /pdf/,
   /doc\/api/,
   /data_pack/,
+  /giftrecord/
 ]
 // 权限控制白名单-路由名
 const powerCheckWhiteList = [
@@ -62,7 +63,8 @@ const powerCheckWhiteList = [
 ]
 
 const regListCheck = function (regList, path) {
-  if (!Array.isArray(regList)) return false
+  if (!Array.isArray(regList))
+    return false
   return regList.some((reg) => {
     return reg.test(path)
   })
@@ -83,20 +85,21 @@ router.beforeEach(async (to, from, next) => {
     // 调用商机管理权限接口 查用户有无画像分析系统权限 有则执行下一步 无则返回首页
     if (entNiche.privatedata) {
       next()
-    } else {
+    }
+    else {
       location.href = location.origin
     }
     return
   }
   if (
-    powerCheckWhiteList.includes(to.name) ||
-    regListCheck(powerCheckPathWhiteRegList, to.path)
+    powerCheckWhiteList.includes(to.name)
+    || regListCheck(powerCheckPathWhiteRegList, to.path)
   ) {
     // 权限列表:https://app-jytest.jydev.jianyu360.com/jyapp/big-member/js/main_root_data.js
     // 4.企业全景分析
     // 13.企业中标动态
-    const hasEntPortPower =
-      info.memberStatus > 0 && (power.includes(4) || power.includes(13))
+    const hasEntPortPower
+      = info.memberStatus > 0 && (power.includes(4) || power.includes(13))
     if (hasEntPortPower) {
       // 大会员有画像权限用户,访问超级订阅画像,则重定向到大会员画像
       if (to.name === 'ent_ser_portrait') {
@@ -109,10 +112,12 @@ router.beforeEach(async (to, from, next) => {
             resource: to.query.resource
           }
         })
-      } else {
+      }
+      else {
         next()
       }
-    } else {
+    }
+    else {
       if (to.name === 'ent_portrait') {
         // 其他无画像权限用户,访问大会员画像,则重定向到超级订阅画像
         next({
@@ -122,20 +127,24 @@ router.beforeEach(async (to, from, next) => {
             ...to.query
           }
         })
-      } else {
+      }
+      else {
         next()
       }
     }
-  } else {
+  }
+  else {
     let href = '/big/page/index'
     const { pass, anchor } = powerCheck(info, power, to, from)
     if (pass) {
       next()
-    } else {
+    }
+    else {
       // TODO 可优化 临时判断是否旧项目大会员支付路由,skip
       if (to.fullPath.startsWith('/front/member/memberDetail')) {
         // skip
-      } else {
+      }
+      else {
         if (anchor) {
           href = `${href}#${anchor}`
         }
@@ -153,7 +162,8 @@ router.beforeEach((to, from, next) => {
     // 标题设置
     if (to?.meta?.title) {
       document.title = to.meta.title
-    } else {
+    }
+    else {
       document.title = '剑鱼标讯'
     }
   }

+ 12 - 0
apps/bigmember_pc/src/router/svip-routers.js

@@ -41,5 +41,17 @@ export default [
     path: '/free/terms/activity',
     name: 'activityTerms',
     component: () => import('@/views/vipsubscribe/terms/activity.vue')
+  },
+  // 超级订阅赠送好友记录-我赠送的
+  {
+    path: '/giftrecord/index',
+    name: 'gift-record-index',
+    component: () => import('@/views/gift-record/index.vue')
+  },
+  // 送好友超级订阅-产品须知
+  {
+    path: '/giftrecord/notice',
+    name: 'gift-product-notice',
+    component: () => import('@/views/gift-record/ProductNotice.vue')
   }
 ]

+ 5 - 3
apps/bigmember_pc/src/store/workspace.js

@@ -12,6 +12,7 @@ import asideOthers from './workspace/aside-others'
 import importantNews from './workspace/important-news'
 import industryReport from './workspace/industry-report'
 import businessTodo from './workspace/business-todo'
+import giftFriends from './workspace/gift-friends'
 
 export default {
   namespaced: true,
@@ -20,8 +21,8 @@ export default {
   actions: {},
   getters: {
     isNewEntniche(state, getters, rootState, rootGetters) {
-      const { 'user/entniche': entniche, 'user/isNewEntNiche': newEnt } =
-        rootGetters
+      const { 'user/entniche': entniche, 'user/isNewEntNiche': newEnt }
+        = rootGetters
       return entniche && newEnt
     },
     businessProfileShow(state, getters) {
@@ -72,6 +73,7 @@ export default {
     customerWatcher,
     importantNews,
     industryReport,
-    businessTodo
+    businessTodo,
+    giftFriends
   }
 }

+ 33 - 0
apps/bigmember_pc/src/store/workspace/gift-friends.js

@@ -0,0 +1,33 @@
+import { getConfigurationTime } from '@/api/modules'
+
+export default {
+  namespaced: true,
+  state: () => ({
+    // 是否过期
+    configTime: false
+  }),
+  mutations: {
+    changeConfigTime(state, configTime) {
+      state.configTime = configTime
+    }
+  },
+  actions: {
+    async getConfigTime({ commit }) {
+      const { error_code: code, data } = await getConfigurationTime()
+      if (code === 0) {
+        // 获取当前时间戳
+        const currentTime = Math.floor(Date.now() / 1000)
+        // 判断是否过期
+        const { startTime, endTime } = data
+        if (startTime < currentTime && currentTime < endTime) {
+          commit('changeConfigTime', true)
+          return true
+        }
+        else {
+          commit('changeConfigTime', false)
+          return false
+        }
+      }
+    }
+  }
+}

+ 70 - 0
apps/bigmember_pc/src/views/gift-record/ProductNotice.vue

@@ -0,0 +1,70 @@
+<template>
+  <div class="gift-product-notice">
+    <div class="content">
+      <h1>“送好友超级订阅”产品须知</h1>
+      <h3>请您在下单赠送过程中接受本须知之前,务必审慎阅读、充分理解各条款内容。</h3>
+      <p>
+        1.“送好友超级订阅”是剑鱼标讯推出的一项支持用户在线赠送超级订阅的功能,仅支持在个人身份下进行,若您在企业身份下,请切换至个人身份进行赠送。<br>
+        2.赠送人购买超级订阅后输入“被赠送好友”的手机号即可进行赠送,完成赠送后,赠送人可通过海报、链接分享等方式告知“被赠送好友”,“被赠送好友”登录剑鱼标讯平台即可查看赠送的超级订阅权益。<br>
+        3.购买超级订阅后赠送好友不支持退款,请确认无误后进行赠送。<br>
+        4.在使用本功能过程中,如果用户出现违规行为,剑鱼标讯可限制用户使用本功能,并有权撤销违规交易,必要时追究法律责任。<br>
+        5.如出现不可抗力或情势变更的情况,则平台可暂停本功能,并依相关法律法规的规定主张免责。<br>
+        6.如有其他问题,请拨打客服热线400-108-6670进行反馈。<br>
+        7.本活动最终解释权归北京剑鱼信息技术有限公司所有。<br>
+      </p>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'GiftProductNotice'
+}
+</script>
+
+<style lang="scss" scoped>
+.gift-product-notice {
+  padding-top: 32px;
+  padding-bottom: 32px;
+  .content {
+    width: 1200px;
+    margin: auto;
+    background-color: #fff;
+    min-height: 800px;
+    box-sizing: border-box;
+    padding: 40px 60px;
+    h1 {
+
+      font-size: 24px;
+      font-weight: 400;
+      line-height: 36px;
+      letter-spacing: 0em;
+      text-align: center;
+      color: #1d1d1d;
+      margin-bottom: 32px;
+    }
+    h3 {
+      font-size: 16px;
+      font-weight: 400;
+      line-height: 24px;
+      letter-spacing: 0em;
+      text-align: left;
+      color: #1d1d1d;
+      margin-bottom: 24px;
+    }
+    p {
+      font-size: 14px;
+      font-weight: 400;
+      line-height: 38px;
+      letter-spacing: 0em;
+      text-align: left;
+      color: #686868;
+    }
+  }
+  .link {
+    color: blue;
+    text-decoration: underline;
+    cursor: pointer;
+  }
+}
+</style>

+ 109 - 0
apps/bigmember_pc/src/views/gift-record/components/MyGiftRecord.vue

@@ -0,0 +1,109 @@
+<template>
+  <div class="my-gift-record">
+    <div class="gift-time">
+      赠送时间:{{ item[0].createTime }}
+    </div>
+    <div class="gift-record-list">
+      <div v-for="iitem in item" :key="iitem.id" class="gift-item">
+        <div class="gift-item-left">
+          <span class="gift-label">手机号:</span>
+          <span class="gift-value">{{ iitem.recipientUserPhone }}</span>
+          <span class="gift-split">|</span>
+          <span class="gift-label">时长:</span>
+          <span class="gift-value">{{ iitem.duration }}个月</span>
+        </div>
+        <div class="gift-item-right" @click="$emit('click', iitem)">
+          <span>告知朋友</span>
+          <span class="icon-post-set" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    item: {
+      type: Array,
+      default() {
+        return []
+      }
+    }
+  },
+  data() {
+    return {
+      // 这里可以添加一些数据属性,例如加载更多数据的状态等
+    }
+  },
+  methods: {
+    loadMore() {
+      // 这里可以添加加载更多数据的逻辑
+      console.log('Load more data')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.my-gift-record {
+  width: 359px;
+  padding: 8px 12px;
+  background-color: #FFFFFF;
+  border-radius: 8px;
+  border: 1px solid #ECECEC;
+  box-shadow: 0 0 28px 0 rgba(0, 0, 0, 0.08);
+  .gift-time {
+    margin-bottom: 4px;
+    font-size: 12px;
+    line-height: 20px;
+    color: #5F5E64;
+  }
+  .gift-record-list {
+    display: flex;
+    flex-direction: column;
+    .gift-item {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-top: 4px;
+      .gift-item-left {
+        display: flex;
+        align-items: center;
+        .gift-label {
+          font-size: 12px;
+          line-height: 20px;
+          color: #5F5E64;
+        }
+        .gift-value {
+          font-size: 14px;
+          line-height: 24px;
+          color: #171826;
+        }
+        .gift-split {
+          margin: 0 8px;
+          width: 3px;
+          height: 20px;
+          color: #C0C4CC;
+        }
+      }
+      .gift-item-right {
+        display: flex;
+        align-items: center;
+        font-size: 14px;
+        line-height: 20px;
+        color: #2ABED1;
+        cursor: pointer;
+        .icon-post-set {
+          display: block;
+          margin-left: 4px;
+          width: 16px;
+          height: 16px;
+          background: url('~@/assets/images/icon/postSet.png') no-repeat;
+          background-size: 16px 16px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 63 - 0
apps/bigmember_pc/src/views/gift-record/components/MyReceiveRecord.vue

@@ -0,0 +1,63 @@
+<template>
+  <div class="my-receive-record">
+    <div class="receive-time">
+      赠送时间:{{ item[0].createTime }}
+    </div>
+    <div class="receive-item">
+      <span class="receive-label">来自好友:</span>
+      <span class="receive-value">{{ item[0].giftUserPhone }}</span>
+    </div>
+    <div class="receive-item">
+      <span class="receive-label">得赠时长:</span>
+      <span class="receive-value">{{ item[0].duration }}个月</span>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+
+defineProps({
+  item: {
+    type: Array,
+    default: () => {
+      return []
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.my-receive-record {
+  margin: 0 20px 20px 0;
+  width: 359px;
+  height: max-content;
+  padding: 8px 12px;
+  background-color: #FFFFFF;
+  border-radius: 8px;
+  border: 1px solid #ECECEC;
+  box-shadow: 0 0 28px 0 rgba(0, 0, 0, 0.08);
+  .receive-time {
+    margin-bottom: 4px;
+    font-size: 12px;
+    line-height: 20px;
+    color: #5F5E64;
+  }
+  .receive-item {
+    display: flex;
+    align-items: center;
+    margin-top: 4px;
+    line-height: 24px;
+    .receive-label {
+      font-size: 12px;
+      line-height: 20px;
+      color: #5F5E64;
+    }
+    .receive-value {
+      font-size: 14px;
+      line-height: 24px;
+      color: #171826;
+    }
+  }
+}
+</style>

+ 367 - 0
apps/bigmember_pc/src/views/gift-record/index.vue

@@ -0,0 +1,367 @@
+<template>
+  <div ref="giftRecordIndex" class="gift-record-index">
+    <div class="gift-record-index__header">
+      <span>超级订阅赠送好友记录</span>
+    </div>
+    <div ref="giftMainContainer" class="gift-record-index__content">
+      <el-tabs v-model="activeName" @tab-click="handleClick">
+        <el-tab-pane label="我赠送的" name="gift">
+          <div v-if="giftList.length && giftListBool" class="gift-record-container gift-main">
+            <vueWaterfallEasy ref="waterfall" :max-cols="maxCols" class="gift-water-fall" :img-width="359" :imgs-arr="giftList" @scrollReachBottom="stopSrcollRequest" @click="clickFn">
+              <template slot-scope="props">
+                <!-- {{ props }} -->
+                <MyGiftRecord
+                  :key="props.index"
+                  :item="props.value.info"
+                  @click="onClickShare"
+                />
+              </template>
+              <div slot="waterfall-over" />
+            </vueWaterfallEasy>
+            <NotifyFriendsDialog :info="tellInfo" :visible="showNotifyFriendsDialog" @update:visible="showNotifyFriendsDialog = false" />
+          </div>
+          <Empty v-else class="record-container">
+            <div>{{ computedGiftEmptyInfo.defaultText }}</div>
+            <button slot="button" class="btn-primary" @click="goAction">
+              {{ computedGiftEmptyInfo.buttonText }}
+            </button>
+          </Empty>
+        </el-tab-pane>
+        <el-tab-pane label="我接收的" name="receive">
+          <div v-if="receiveList.length" class="gift-record-container">
+            <MyReceiveRecord
+              v-for="(item, index) in receiveList"
+              :key="index"
+              :item="item"
+            />
+          </div>
+          <Empty v-else class="record-container">
+            <div>{{ receiveEmptyInfo.defaultText }}</div>
+          </Empty>
+        </el-tab-pane>
+      </el-tabs>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref } from 'vue'
+import vueWaterfallEasy from 'vue-waterfall-easy'
+import MyGiftRecord from './components/MyGiftRecord.vue'
+import MyReceiveRecord from './components/MyReceiveRecord.vue'
+import Empty from '@/components/common/Empty.vue'
+import NotifyFriendsDialog from '@/components/dialog/NotifyFriendsDialog.vue'
+import { getGiftRecordList, getSubDuration, getUserAccountShow } from '@/api/modules/'
+import store from '@/store'
+import examplePng from '@/assets/images/icon/sujudaochu.png'
+
+const { proxy } = getCurrentInstance()
+
+const storeUser = store.state.user
+const { info } = storeUser
+const vipTime = ref({})
+const activeName = ref('gift')
+const showNotifyFriendsDialog = ref(false)
+const giftList = ref([])
+const giftListStash = ref([])
+const giftListBool = ref(true)
+const isFrist = ref(true) // 是否第一次加载数据
+const receiveList = ref([])
+const tellInfo = ref({})
+const maxCols = ref(3) // 初始化 maxCols 为 3
+const timer = ref(null)
+const lastWidth = ref(null) // 存储上一次的宽度值
+const subduration = ref({})
+
+const computedGiftEmptyInfo = computed(() => {
+  let endTime = ''
+  if (vipTime.value.list && vipTime.value.list.length) {
+    vipTime.value.list.forEach((item) => {
+      if (item.name === '超级订阅') {
+        endTime = item.endTime
+      }
+    })
+  }
+  if (info.vipStatus > 0) {
+    // 判断endTime与当前时间对比,是否小于一个月,如果小于一个月,则提示用户续费
+    if (endTime) {
+      const isOneMonthPassed = !subduration.value?.gifted
+      if (isOneMonthPassed) {
+        return {
+          defaultText: '您当前超级订阅即将到期,请续费后赠送好友。',
+          buttonText: '去续费'
+        }
+      }
+      else {
+        return {
+          defaultText: '您当前是超级订阅用户,立即去赠送好友。',
+          buttonText: '去赠送'
+        }
+      }
+    }
+    else {
+      return {
+        defaultText: '您当前不是超级订阅用户,需购买后赠送好友。',
+        buttonText: '去购买'
+      }
+    }
+  }
+  else {
+    return {
+      defaultText: '您当前不是超级订阅用户,需购买后赠送好友。',
+      buttonText: '去购买'
+    }
+  }
+})
+
+const receiveEmptyInfo = reactive({
+  defaultText: '暂无好友赠送记录'
+})
+
+function stopSrcollRequest() {
+  proxy.$refs.waterfall.waterfallOver()
+}
+
+function oneMonthPassed(givenDateString) {
+  const givenDate = new Date(givenDateString).getTime()
+  let currentDate = new Date()
+  // 增加一个月
+  currentDate.setMonth(currentDate.getMonth() + 1)
+  currentDate = new Date(currentDate).getTime()
+  // 比较增加一个月后的当前日期和给定日期
+  return currentDate >= givenDate
+}
+
+function handleClick(tab, event) {
+  console.log(tab, event, '点击了标签页:', tab.name)
+  if (tab.name === 'receive') {
+    giftList.value = []
+  }
+  else if (tab.name === 'gift') {
+    giftList.value = giftListStash.value
+  }
+}
+
+function onClickShare(data) {
+  tellInfo.value = data
+  showNotifyFriendsDialog.value = true
+}
+
+function clickFn(event, { index, value }) {
+  // 阻止a标签跳转
+  event.preventDefault()
+  // 只有当点击到图片时才进行操作
+  if (event.target.tagName.toLowerCase() === 'img') {
+    console.log('img clicked', index, value)
+  }
+}
+
+function goAction() {
+  // 获取按钮文本并进行空值检查
+  const buttonText = computedGiftEmptyInfo.value?.buttonText
+  if (!buttonText) {
+    console.warn('按钮文本为空或未定义')
+    return
+  }
+  // 定义按钮文本与操作的映射关系
+  const actionMap = {
+    去赠送: () => {
+      try {
+        proxy.$GiftFriendsDialog({
+          props: {
+            visible: true,
+            name: '送给朋友',
+            el: proxy.$refs.giftRecordIndex
+          }
+        })
+      }
+      catch (error) {
+        console.error('赠礼对话框调用失败:', error)
+      }
+    },
+    去续费: () => window.open('/swordfish/page_big_pc/free/svip/buy?type=renew', '_blank'),
+    去购买: () => window.open('/swordfish/page_big_pc/free/svip/buy?type=buy', '_blank')
+  }
+
+  // 执行对应的操作
+  const action = actionMap[buttonText]
+  if (typeof action === 'function') {
+    action()
+  }
+  else {
+    console.warn(`未知的按钮文本: "${buttonText}"`)
+  }
+}
+
+async function getRecordList(type) {
+  const params = {
+    giftType: type
+  }
+  const { error_code: code, error_msg: msg, data } = await getGiftRecordList(params)
+  if (code === 0) {
+    if (type === '1') {
+      giftList.value = giftListStash.value = data.list.map(item => ({
+        src: examplePng,
+        href: '',
+        info: item
+      })) || []
+    }
+    else {
+      receiveList.value = data.list || []
+    }
+  }
+  else {
+    this.$toast(msg)
+  }
+}
+
+async function getSubDurationEvent() {
+  const { error_code: code, data } = await getSubDuration()
+  if (code === 0) {
+    subduration.value = data || {}
+  }
+}
+
+// 获取超级订阅到期时间
+async function getSuperSubscriptionTime() {
+  const { error_code: code, error_msg: msg, data } = await getUserAccountShow()
+  if (code === 0) {
+    vipTime.value = data || {}
+  }
+  else {
+    this.$toast(msg)
+  }
+}
+
+onMounted(() => {
+  getRecordList('1')
+  getRecordList('2')
+  getSuperSubscriptionTime()
+  getSubDurationEvent()
+
+  const resizeObserver = new ResizeObserver((entries) => {
+    for (const entry of entries) {
+      // 计算 maxCols 的值
+      maxCols.value = Math.floor(entry.contentRect.width / 379)
+      if (lastWidth.value !== entry.contentRect.width) {
+        lastWidth.value = entry.contentRect.width
+        if (timer.value) {
+          clearTimeout(timer.value)
+        }
+        timer.value = setTimeout(() => {
+          if (!isFrist.value) {
+            giftListBool.value = false
+            nextTick(() => {
+              giftListBool.value = true
+            })
+          }
+          isFrist.value = false
+        }, 500)
+      }
+    }
+  })
+
+  // 监听 gift-record-container gift-main 的宽度变化
+  const giftMainContainer = proxy.$refs.giftMainContainer
+  if (giftMainContainer) {
+    resizeObserver.observe(giftMainContainer)
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+  .gift-record-index {
+    padding: 24px;
+    .gift-record-index__header {
+      line-height: 36px;
+      font-size: 24px;
+    }
+    .gift-record-index__content {
+      margin-top: 24px;
+      background: #fff;
+      border-radius: 8px;
+      .gift-main {
+        height: 598px;
+      }
+      .gift-record-container {
+        display: flex;
+        flex-wrap: wrap;
+        .gift-water-fall {
+          ::v-deep {
+            .vue-waterfall-easy {
+              left: 0!important;
+              margin-left: 0!important;
+            }
+            .img-box {
+              padding: 20px 0 0 0 !important;
+              .img-inner-box {
+                display: flex;
+                justify-content: flex-end;
+                box-shadow: none;
+                border-radius: 0;
+              }
+              a:focus, a:hover{
+                text-decoration: none;
+              }
+            }
+            .img-wraper {
+              display: none;
+            }
+          }
+        }
+      }
+      ::v-deep {
+        .el-tabs__header {
+          margin-bottom: 0;
+        }
+        .el-tabs__nav-scroll {
+          padding-left: 44px;
+          height: 50px;
+          .el-tabs__item {
+            height: 100%;
+            line-height: 50px;
+            font-size: 16px;
+            color: #686868;
+            &.is-active {
+              color: #2ABED1;
+            }
+          }
+          .el-tabs__nav {
+            height: 100%;
+          }
+        }
+        .el-tabs__nav-wrap::after{
+          height: 1px;
+          background-color: #ECECEC;
+        }
+        .el-tabs__content {
+          min-height: 598px;
+          padding: 0;
+          #pane-gift {
+            padding: 0px 10px 20px;
+          }
+          #pane-receive {
+            padding: 20px 12px 20px 32px;
+          }
+        }
+      }
+
+    }
+    .record-container {
+      ::v-deep {
+        .empty-main{
+          margin-top: 0;
+        }
+      }
+      .btn-primary {
+        margin-top: 40px;
+        width: 180px;
+        height: 46px;
+        border-radius: 8px;
+        background: #2ABED1;
+        color: #fff;
+        font-size: 16px;
+      }
+    }
+  }
+</style>

+ 23 - 10
apps/bigmember_pc/src/views/order/components/common/footer.vue

@@ -1,31 +1,37 @@
 <template>
   <div class="order-footer">
     <BuySubmit
-      :submitText="productUI.submitText"
+      :submit-text="productUI.submitText"
       :pass="canSubmitOrder"
-      :productionTotal="amount.origin"
-      :productionDiscount="amount.discount"
-      :productionPay="amount.pay"
-      @submit="submitCreatedProductOrder"
+      :production-total="amount.origin"
+      :production-discount="amount.discount"
+      :production-pay="amount.pay"
+      :buy-type="buyType"
       :loading="loadingStatus.some"
+      @submit="submitCreatedProductOrder"
     >
-      <template v-slot:buy-tip-group>
-        <router-view name="buy-tip"></router-view>
+      <template #buy-tip-group>
+        <router-view name="buy-tip" />
       </template>
     </BuySubmit>
   </div>
 </template>
 
 <script>
-import BuySubmit from '@/components/coupon/BuySubmit.vue'
 import { mapActions, mapGetters } from 'vuex'
+import BuySubmit from '@/components/coupon/BuySubmit.vue'
 import { formatNumber } from '@/utils'
 
 export default {
-  name: 'order-footer',
+  name: 'OrderFooter',
   components: {
     BuySubmit
   },
+  data() {
+    return {
+      buyType: ''
+    }
+  },
   computed: {
     ...mapGetters('order', [
       'canSubmitOrder',
@@ -42,8 +48,15 @@ export default {
       }
     }
   },
+  created() {
+    this.getType()
+  },
   methods: {
-    ...mapActions('order', ['submitCreatedProductOrder'])
+    ...mapActions('order', ['submitCreatedProductOrder']),
+    getType() {
+      const type = this.$route.query.type
+      this.buyType = type || 'buy'
+    }
   }
 }
 </script>

+ 191 - 118
apps/bigmember_pc/src/views/order/components/vipsubscribe/info.vue

@@ -1,17 +1,19 @@
 <template>
   <div class="vip-subscribe-info">
-    <section class="module-area-count-selector" v-show="moduleShow.selectArea">
+    <section v-show="moduleShow.selectArea" class="module-area-count-selector">
       <!-- 购买 -->
       <SelectorCard
-        class="vip-sub-list-item buy-area-count-section"
         v-if="buyType === 'buy'"
-        cardType="line"
+        class="vip-sub-list-item buy-area-count-section"
+        card-type="line"
       >
-        <div slot="header" class="vip-sub-item-title">购买区域</div>
+        <div slot="header" class="vip-sub-item-title">
+          购买区域
+        </div>
         <div class="vip-sub-item-content choice-area-content">
           <el-radio-group
-            class="radio-selector-group"
             v-model="selectedInfo.radio"
+            class="radio-selector-group"
             @change="onAreaCountRadioChange"
           >
             <el-radio class="radio-selector" label="num">
@@ -21,73 +23,92 @@
                   v-model="selectedInfo.areaCount"
                   :min="radioMin"
                   :max="radioMax"
-                  @change="onAreaCountChange($event, 'buy')"
                   integer
+                  @change="onAreaCountChange($event, 'buy')"
                 >
-                  <template #content-suffix-text>个省</template>
+                  <template #content-suffix-text>
+                    个省
+                  </template>
                 </JStepper>
               </div>
             </el-radio>
             <el-radio class="radio-selector" label="" style="margin-left:30px;">
               <div class="radio-content radio-content-text">
                 <span>全国</span>
-                <span class="radio-content-sub-text"
-                  >(购买{{ conf.maxAreaCount + 1 }}个省及以上)</span
-                >
+                <span class="radio-content-sub-text">(购买{{ conf.maxAreaCount + 1 }}个省及以上)</span>
               </div>
             </el-radio>
           </el-radio-group>
-          <div class="tip-text">支付成功后,可点击“立即订阅”前往设置订阅区域</div>
+          <div class="tip-text">
+            支付成功后,可点击“立即订阅”前往设置订阅区域
+          </div>
         </div>
       </SelectorCard>
       <!-- 续费 -->
       <SelectorCard
-        class="vip-sub-list-item renew-area-count-section"
         v-if="buyType === 'renew'"
-        cardType="line"
+        class="vip-sub-list-item renew-area-count-section"
+        card-type="line"
       >
-        <div slot="header" class="vip-sub-item-title">续费区域</div>
-        <div class="vip-sub-item-content">{{ selectedAreaText }}</div>
+        <div slot="header" class="vip-sub-item-title">
+          续费区域
+        </div>
+        <div class="vip-sub-item-content">
+          {{ selectedAreaText }}
+        </div>
       </SelectorCard>
       <!-- 升级 -->
-      <div class="upgrade-area-count-section" v-if="buyType === 'upgrade'">
-        <SelectorCard class="vip-sub-list-item" cardType="line">
-          <div slot="header" class="vip-sub-item-title">已购区域</div>
-          <div class="vip-sub-item-content">{{ alreadyBuyAreaText }}</div>
+      <div v-if="buyType === 'upgrade'" class="upgrade-area-count-section">
+        <SelectorCard class="vip-sub-list-item" card-type="line">
+          <div slot="header" class="vip-sub-item-title">
+            已购区域
+          </div>
+          <div class="vip-sub-item-content">
+            {{ alreadyBuyAreaText }}
+          </div>
         </SelectorCard>
         <SelectorCard
-          class="vip-sub-list-item"
           v-show="showAfterUpgradeAreaText"
-          cardType="line"
+          class="vip-sub-list-item"
+          card-type="line"
         >
-          <div slot="header" class="vip-sub-item-title">升级区域</div>
+          <div slot="header" class="vip-sub-item-title">
+            升级区域
+          </div>
           <div class="vip-sub-item-content">
             <JStepper
               v-model="selectedInfo.areaCount"
               :min="radioMin"
               :max="radioMax"
-              @change="onAreaCountChange($event, 'upgrade')"
               integer
+              @change="onAreaCountChange($event, 'upgrade')"
             >
-              <template #content-suffix-text>个省</template>
+              <template #content-suffix-text>
+                个省
+              </template>
             </JStepper>
             <span
               class="highlight-text i-want-to-upgrade-global"
               @click="iWantGlobal"
-              >我要升级至全国</span
-            >
+            >我要升级至全国</span>
           </div>
         </SelectorCard>
         <SelectorCard
-          class="vip-sub-list-item"
           v-show="showAfterUpgradeAreaText"
-          cardType="line"
+          class="vip-sub-list-item"
+          card-type="line"
         >
-          <div slot="header" class="vip-sub-item-title">升级后区域</div>
-          <div class="vip-sub-item-content">{{ selectedAreaText }}</div>
+          <div slot="header" class="vip-sub-item-title">
+            升级后区域
+          </div>
+          <div class="vip-sub-item-content">
+            {{ selectedAreaText }}
+          </div>
         </SelectorCard>
-        <SelectorCard class="vip-sub-list-item" cardType="line">
-          <div slot="header" class="vip-sub-item-title">有效日期</div>
+        <SelectorCard class="vip-sub-list-item" card-type="line">
+          <div slot="header" class="vip-sub-item-title">
+            有效日期
+          </div>
           <div class="vip-sub-item-content font-orange">
             {{ effectTimeText }}
           </div>
@@ -95,38 +116,39 @@
       </div>
     </section>
     <SelectorCard
-      class="vip-sub-list-item spec-list-line"
       v-if="moduleShow.selectSpec"
-      cardType="line"
+      class="vip-sub-list-item spec-list-line"
+      card-type="line"
     >
-      <div slot="header" class="vip-sub-item-title">选择购买周期</div>
+      <div slot="header" class="vip-sub-item-title">
+        选择购买周期
+      </div>
       <div class="vip-sub-item-content">
         <OrderSpecList
           :list="specList"
           :active="productSpec.id"
           @activeChange="specCardChange"
-        ></OrderSpecList>
+        />
         <div class="spec-list-bottom-tips">
-          <div class="spec-tips-item" v-show="tip.buyGiveMedicalTip">
+          <div v-show="tip.buyGiveMedicalTip" class="spec-tips-item">
             <div class="spec-tips-item-content">
               <span class="text">免费赠送 [医械通] </span>
               <a
                 class="link-button"
                 href="/page_workDesktop/work-bench/app/big/medical/Credentials"
                 target="_blank"
-                >了解更多</a
-              >
+              >了解更多</a>
             </div>
           </div>
         </div>
       </div>
     </SelectorCard>
     <SelectorCard
-      class="vip-sub-tip"
-      cardType="line"
       v-show="effectTimeTipTextShow"
+      class="vip-sub-tip"
+      card-type="line"
     >
-      <div slot="header" class="vip-sub-item-title"></div>
+      <div slot="header" class="vip-sub-item-title" />
       <div class="tip-content font-orange">
         <span>有效日期:</span>
         <span>{{ effectTimeText }}</span>
@@ -139,13 +161,17 @@
       center
       width="400px"
     >
-      <div class="dialog-title" slot="title">超级订阅更新提醒</div>
+      <div slot="title" class="dialog-title">
+        超级订阅更新提醒
+      </div>
       <div class="dialog-content">
         <p class="c-main">
           为满足用户精准获取商机的需求,剑鱼标讯推出超前项目推荐服务。原超级订阅用户中断续费后,将不再支持采购意向、拟建项目查看。超前项目推荐服务整合更多前期项目机会,提供更强服务。
         </p>
-        <div class="c-img"></div>
-        <p class="c-footer">扫一扫,立即联系客户经理了解超前项目推荐服务</p>
+        <div class="c-img" />
+        <p class="c-footer">
+          扫一扫,立即联系客户经理了解超前项目推荐服务
+        </p>
       </div>
       <div slot="footer">
         <span class="know-btn" @click="dialog.vipUpdate = false">我知道了</span>
@@ -155,23 +181,23 @@
 </template>
 
 <script>
-import { mapState, mapMutations, mapActions, mapGetters } from 'vuex'
+import { mapActions, mapGetters, mapMutations, mapState } from 'vuex'
+import { Dialog, Radio, RadioGroup } from 'element-ui'
+import { debounce } from 'lodash'
+import dayjs from 'dayjs'
 import SelectorCard from '@/components/selector/SelectorCard.vue'
 import OrderSpecList from '@/views/order/ui/spec/list.vue'
 import JStepper from '@/views/order/ui/spec/stepper.vue'
-import { Dialog, Radio, RadioGroup } from 'element-ui'
 import { fen2Yuan } from '@/utils'
-import { debounce } from 'lodash'
-import dayjs from 'dayjs'
 import {
-  getSVIPBuyInfo,
-  getEffectiveTime,
   createCommonOrder,
-  getMedicalIndustry
+  getEffectiveTime,
+  getMedicalIndustry,
+  getSVIPBuyInfo
 } from '@/api/modules'
 
 export default {
-  name: 'vip-subscribe-info',
+  name: 'VipSubscribeInfo',
   components: {
     [Dialog.name]: Dialog,
     [Radio.name]: Radio,
@@ -231,12 +257,12 @@ export default {
   },
   computed: {
     ...mapState('user', {
-      userInfo: (state) => state.userAccountInfo
+      userInfo: state => state.userAccountInfo
     }),
     ...mapState('order', {
-      productSpec: (state) => state.productSpec,
-      productSpecId: (state) => state.productSpec.id,
-      offersId: (state) => state.offers.id
+      productSpec: state => state.productSpec,
+      productSpecId: state => state.productSpec.id,
+      offersId: state => state.offers.id
     }),
     ...mapGetters('order', [
       'productInfo',
@@ -256,11 +282,13 @@ export default {
           cycleType = 1
           effectTimeAddType = 2
           value = '1个月'
-        } else if (spec.info?.includes('季')) {
+        }
+        else if (spec.info?.includes('季')) {
           cycleType = 2
           effectTimeAddType = 3
           value = '1季'
-        } else if (spec.info?.includes('年')) {
+        }
+        else if (spec.info?.includes('年')) {
           cycleType = 3
           effectTimeAddType = 4
           value = '1年'
@@ -295,7 +323,8 @@ export default {
     upgradeTipShow() {
       if (this.buyType === 'upgrade') {
         return !this.canUpgrade
-      } else {
+      }
+      else {
         return false
       }
     },
@@ -308,22 +337,27 @@ export default {
           // 购买了全国的老超级订阅用户,可以升级到新超级订阅
           if (this.oldVip) {
             return selectCount === -1
-          } else {
+          }
+          else {
             return false
           }
-        } else {
+        }
+        else {
           if (selectCount === -1) {
             return true
-          } else {
+          }
+          else {
             if (this.oldVip) {
               // 老超级订阅升级新超级订阅,可能包含0元升级
               return selectCount >= buyset.areacount
-            } else {
+            }
+            else {
               return selectCount > buyset.areacount
             }
           }
         }
-      } else {
+      }
+      else {
         return false
       }
     },
@@ -339,16 +373,19 @@ export default {
     alreadyBuyAreaCount() {
       if (this.buyType === 'buy') {
         return 0
-      } else {
+      }
+      else {
         if (this.oldVip) {
           // 老超级订阅已购的市升级时候按省份算(但需要补差价)
           const { areacount, newcitys } = this.buyInfo?.buyset || {}
           if (Array.isArray(newcitys)) {
             return Number(areacount) + newcitys.length
-          } else {
+          }
+          else {
             return areacount ?? 0
           }
-        } else {
+        }
+        else {
           return this.buyInfo?.buyset?.areacount ?? 0
         }
       }
@@ -360,7 +397,8 @@ export default {
         const { areacount, newcitys } = buyset
         if (areacount === -1) {
           return '全国'
-        } else {
+        }
+        else {
           if (Array.isArray(newcitys) && newcitys.length) {
             const arr = []
             if (areacount && areacount > 0) {
@@ -372,14 +410,17 @@ export default {
             arr.push(`${cityCount}个市(分布在${newcitys.length}个省内)`)
             // 8个省、3个市(分布在2个省内)
             return arr.join('、')
-          } else {
+          }
+          else {
             return `${this.alreadyBuyAreaCount}个省`
           }
         }
-      } else {
+      }
+      else {
         if (this.alreadyBuyAreaCount === -1 || gtMax) {
           return '全国'
-        } else {
+        }
+        else {
           return `${this.alreadyBuyAreaCount}个省`
         }
       }
@@ -392,28 +433,35 @@ export default {
         if (radio) {
           if (areaCount > this.conf.maxAreaCount) {
             return -1
-          } else {
+          }
+          else {
             return areaCount
           }
-        } else {
+        }
+        else {
           // -1表示全国
           return -1
         }
-      } else if (this.buyType === 'renew') {
+      }
+      else if (this.buyType === 'renew') {
         return this.alreadyBuyAreaCount
-      } else if (this.buyType === 'upgrade') {
+      }
+      else if (this.buyType === 'upgrade') {
         const { areaCount, radio } = this.selectedInfo
         if (radio) {
           if (areaCount + this.alreadyBuyAreaCount > this.conf.maxAreaCount) {
             return -1
-          } else {
+          }
+          else {
             return areaCount + this.alreadyBuyAreaCount
           }
-        } else {
+        }
+        else {
           // -1表示全国
           return -1
         }
-      } else {
+      }
+      else {
         return 1
       }
     },
@@ -421,7 +469,8 @@ export default {
       const gtMax = this.selectedAreaCount > this.conf.maxAreaCount
       if (this.selectedAreaCount === -1 || gtMax) {
         return '全国'
-      } else {
+      }
+      else {
         return `${this.selectedAreaCount}个省`
       }
     },
@@ -432,7 +481,8 @@ export default {
         return `${dayjs(startTime * 1000).format(formatStr)} - ${dayjs(
           endTime * 1000
         ).format(formatStr)}`
-      } else {
+      }
+      else {
         return ' - '
       }
     },
@@ -442,7 +492,8 @@ export default {
     radioMin() {
       if (this.buyType === 'upgrade' && this.oldVip) {
         return this.oldVip ? 0 : 1
-      } else {
+      }
+      else {
         // 新超级订阅最小值1
         return 1
       }
@@ -450,7 +501,8 @@ export default {
     radioMax() {
       if (this.buyType === 'upgrade') {
         return this.conf.maxAreaCount - this.alreadyBuyAreaCount + 1
-      } else {
+      }
+      else {
         return this.conf.maxAreaCount
       }
     },
@@ -465,9 +517,11 @@ export default {
       let p = true
       if (this.buyType === 'buy') {
         // do something
-      } else if (this.buyType === 'upgrade') {
+      }
+      else if (this.buyType === 'upgrade') {
         p = this.canUpgrade
-      } else if (this.buyType === 'renew') {
+      }
+      else if (this.buyType === 'renew') {
         // do something
       }
       return p
@@ -494,7 +548,8 @@ export default {
       const vipBuy = this.buyType === 'buy'
       if (breakRenewTip > 0 && vipBuy) {
         this.dialog.vipUpdate = true
-      } else {
+      }
+      else {
         this.dialog.vipUpdate = false
       }
     },
@@ -535,7 +590,8 @@ export default {
         .find('span[name=大会员]')
         .removeClass('active')
       window.trySelectNav('招标订阅')
-    } catch (error) {}
+    }
+    catch (error) {}
     this.checkMedicalTip()
   },
   methods: {
@@ -550,11 +606,12 @@ export default {
       let target = {}
       if (id) {
         // 获取目标信息
-        target =
-          this.specList.find((spec) => {
+        target
+          = this.specList.find((spec) => {
             return spec.id === id
           }) || {}
-      } else {
+      }
+      else {
         // 当前选中信息
         target = this.activeSpec
       }
@@ -601,7 +658,8 @@ export default {
       })
     }, 100),
     async checkMedicalTip() {
-      if (this.buyType !== 'buy') return
+      if (this.buyType !== 'buy')
+        return
       const params = this.token ? { token: this.token } : null
       const { data } = await getMedicalIndustry(params)
       if (data) {
@@ -637,12 +695,14 @@ export default {
           time: this.activeSpec.value,
           orderType: 1
         })
-      } else if (this.buyType === 'upgrade') {
+      }
+      else if (this.buyType === 'upgrade') {
         Object.assign(params, {
           type: 'upgrade',
           areaCount: this.selectedAreaCount
         })
-      } else if (this.buyType === 'renew') {
+      }
+      else if (this.buyType === 'renew') {
         Object.assign(params, {
           type: 'renew',
           time: this.activeSpec.value
@@ -686,13 +746,15 @@ export default {
           // 异步window.open会被浏览器拦截,此处改为location.href
           // window.open(`/front/subvip/orderPay/${orderCode}`)
           location.href = `/front/subvip/orderPay/${orderCode}`
-        } else {
+        }
+        else {
           // window.open(`/front/subvip/paySuccess/${orderCode}?payTime=${parseInt(Date.now() / 1000)}&from=vipUPgrade`)
-          location.href = `/front/subvip/paySuccess/${orderCode}?payTime=${parseInt(
+          location.href = `/front/subvip/paySuccess/${orderCode}?payTime=${Number.parseInt(
             Date.now() / 1000
           )}&from=vipUPgrade`
         }
-      } else {
+      }
+      else {
         this.$toast(msg)
       }
       return res
@@ -708,7 +770,8 @@ export default {
     onAreaCountChange: debounce(function (e, type) {
       if (type === 'buy') {
         this.overLimit(e)
-      } else if (type === 'upgrade') {
+      }
+      else if (type === 'upgrade') {
         const max = this.radioMax
         this.overLimit(e, max)
       }
@@ -722,11 +785,13 @@ export default {
         if (this.buyType === 'buy') {
           this.switchRadio()
           this.$toast('已自动为您切换至全国')
-        } else if (this.buyType === 'upgrade') {
+        }
+        else if (this.buyType === 'upgrade') {
           this.$toast(`最大值为${max}`)
           this.setRadioNum(max)
         }
-      } else if (limitedMin) {
+      }
+      else if (limitedMin) {
         this.$toast(`最小值为${this.radioMin}`)
         this.setRadioNum(this.radioMin)
       }
@@ -744,7 +809,8 @@ export default {
       const types = ['buy', 'upgrade', 'renew']
       if (types.includes(type)) {
         this.buyType = type || types[0]
-      } else {
+      }
+      else {
         this.buyType = types[0]
       }
       if (token) {
@@ -754,7 +820,8 @@ export default {
       if (this.buyType === 'upgrade') {
         this.moduleShow.selectArea = true
         this.moduleShow.selectSpec = false
-      } else {
+      }
+      else {
         this.moduleShow.selectSpec = true
         this.moduleShow.selectArea = true
       }
@@ -804,7 +871,8 @@ export default {
       const { data, success, errMsg } = await getEffectiveTime(payload)
       if (success) {
         Object.assign(this.effectTimeInfo, data)
-      } else {
+      }
+      else {
         if (errMsg) {
           this.$toast(errMsg)
         }
@@ -824,7 +892,8 @@ export default {
               if (areaLength === 0 || areaLength > this.conf.maxAreaCount) {
                 this.selectedInfo.radio = ''
                 this.setRadioNum(this.conf.maxAreaCount + 1)
-              } else {
+              }
+              else {
                 this.selectedInfo.radio = 'num'
                 this.setRadioNum(areaLength)
               }
@@ -843,10 +912,10 @@ export default {
           const { buyset } = this.buyInfo
           // 购买了全国的新超级订阅用户,升级不能再选择省份了
           if (
-            this.buyType === 'upgrade' &&
-            buyset &&
-            buyset.areacount === -1 &&
-            !this.oldVip
+            this.buyType === 'upgrade'
+            && buyset
+            && buyset.areacount === -1
+            && !this.oldVip
           ) {
             this.moduleShow.selectArea = false
           }
@@ -854,7 +923,8 @@ export default {
             this.setRadioNum(this.radioMin)
           }
         }
-      } catch (error) {
+      }
+      catch (error) {
         // loading.clear()
         console.log(error)
       }
@@ -864,7 +934,8 @@ export default {
       const { buyset, isvip, startTime: start, endTime: end } = info
       const { path } = this.$route
       const { redirected } = this.$route.query
-      if (redirected) return
+      if (redirected)
+        return
 
       const redirect = {
         enable: false,
@@ -886,13 +957,15 @@ export default {
             redirect.type = 'upgrade'
             await this.showOldVipTip()
           }
-        } else {
+        }
+        else {
           // 开通了,过期了
           if (this.buyType !== 'buy') {
             redirect.enable = true
           }
         }
-      } else {
+      }
+      else {
         // 未开通
         if (this.buyType !== 'buy') {
           redirect.enable = true
@@ -925,8 +998,8 @@ export default {
     },
     calcSpecPerDayPrice(spec, cycleType) {
       // 只根据当前省份下原价计算单价,有优惠按优惠价计算
-      const price =
-        spec.originalPrice > spec.discountPrice
+      const price
+        = spec.originalPrice > spec.discountPrice
           ? spec.discountPrice
           : spec.originalPrice
       let perDayPrice = 0
@@ -958,20 +1031,20 @@ export default {
       return perDayPrice
     },
     // 计算赠送天数
-    calcGiveDay (time, timeType) {
+    calcGiveDay(time, timeType) {
       // 只根据当前省份下原价计算单价
       let giveDay = 0
       // timeType 时间类型:1/天、2/月,3/年
-      if(timeType){
+      if (timeType) {
         switch (timeType) {
           case 1:
-            giveDay =  time
+            giveDay = time
             break
           case 2:
-            giveDay =  time * 30
+            giveDay = time * 30
             break
           case 3:
-            giveDay =   time * 365
+            giveDay = time * 365
             break
         }
       }

+ 24 - 10
apps/bigmember_pc/src/views/portrayal/Loading.vue

@@ -1,12 +1,13 @@
 <template>
-  <div class="portrayal-loading" v-loading="loading"></div>
+  <div v-loading="loading" class="portrayal-loading" />
 </template>
 
 <script>
 import { mapGetters } from 'vuex'
 import { getUsage } from '@/api/modules/'
+
 export default {
-  name: 'page-not-found',
+  name: 'PageNotFound',
   data() {
     return {
       loading: true,
@@ -16,7 +17,11 @@ export default {
     }
   },
   computed: {
-    ...mapGetters('user', ['power', 'bigmember', 'svip', 'isNewEntNiche'])
+    ...mapGetters('user', ['power', 'bigmember', 'svip', 'isNewEntNiche']),
+    inResourceBI() {
+      const inResourceBI = this.$route.query?.resource === 'BI'
+      return inResourceBI
+    }
   },
   created() {
     this.type = this.$route.params.type // ent企业画像 buyer采购单位画像
@@ -31,6 +36,9 @@ export default {
       const member = this.bigmember
       const sVip = this.svip
       const position = this.position ? `?position=${this.position}` : ''
+      if (this.inResourceBI) {
+        position += position ? '&resource=BI' : '?resource=BI'
+      }
       if (this.type === 'ent') {
         // 去超级订阅画像
         const vipLink = `/swordfish/page_big_pc/svip/ent_ser_portrait/${this.params}${position}`
@@ -38,9 +46,10 @@ export default {
         const memberLink = `/swordfish/page_big_pc/ent_portrait/${this.params}${position}`
         if (member) {
           // 如果是专家版、智慧版跳大会员页面
-          if (this.power.indexOf(4) !== -1) {
+          if (this.power.includes(4)) {
             window.location.replace(memberLink)
-          } else {
+          }
+          else {
             // 如果是商机版、自定义版
             // 如果同时是超级订阅 判断有没有画像查看次数
             if (sVip) {
@@ -52,24 +61,29 @@ export default {
                 if (usage >= total) {
                   // 如果次数已用完 跳大会员企业画像页面
                   window.location.replace(memberLink)
-                } else {
+                }
+                else {
                   // 没用完 超级订阅跳企业画像页面
                   window.location.replace(vipLink)
                 }
               }
-            } else {
+            }
+            else {
               window.location.replace(memberLink)
             }
           }
-        } else {
+        }
+        else {
           window.location.replace(vipLink)
         }
-      } else if (this.type === 'buyer') {
+      }
+      else if (this.type === 'buyer') {
         const buyerLink = `/swordfish/page_big_pc/unit_portrayal/${this.params}${position}`
         const entBuyerLink = `/entpc/unit_portrayal/${this.params}${position}`
         if (this.isNewEntNiche) {
           window.location.replace(entBuyerLink)
-        } else {
+        }
+        else {
           window.location.replace(buyerLink)
         }
       }

+ 105 - 85
apps/bigmember_pc/src/views/search/purchase/model/base.js

@@ -1,16 +1,16 @@
-import { computed, reactive, ref, provide, getCurrentInstance, nextTick } from 'vue'
+import { computed, getCurrentInstance, nextTick, provide, reactive, ref } from 'vue'
 import useQuickSearchModel from '@jy/data-models/modules/quick-search/model'
-import { useStore } from '@/store'
 import { useRoute } from 'vue-router/composables'
-// 搜索历史业务模型
 import useSearchHistoryModel from '@jy/data-models/modules/quick-search-history/model'
-import { getBuyerAssociation } from '@/api/modules'
 import { debounce } from 'lodash'
-import { InContainer, openLinkInWorkspace, scrollTargetView } from '@/utils'
-import { randomBgc, getShortName } from '@/utils/globalFunctions'
-import { ajaxEmployInfo, ajaxEmployOperate, getStatusCustomer, setStatusCustomer, renLingCustomerAddTags } from '@/api/modules/'
 import { dataCollectActionModel } from './modules/data-tags-actions'
 import { useSearchFilterModel } from './modules/filter'
+import { useStore } from '@/store'
+// 搜索历史业务模型
+import { getBuyerAssociation } from '@/api/modules'
+import { InContainer, openLinkInWorkspace, scrollTargetView } from '@/utils'
+import { getShortName, randomBgc } from '@/utils/globalFunctions'
+import { ajaxEmployInfo, ajaxEmployOperate, getStatusCustomer, renLingCustomerAddTags, setStatusCustomer } from '@/api/modules/'
 import { useSearchTabsModel } from '@/views/search/ent/model/modules/tabs'
 
 export default function () {
@@ -24,11 +24,12 @@ export default function () {
   const isInWeb = ref(InContainer.inWeb)
   // 营销BI嵌套
   const inResourceBI = useRoute().query.resource === 'BI'
+  console.log(inResourceBI, 'inResourceBI')
   // 是否是渠道商
   const cooperateCode = ref(false)
   // 一切都好渠道商,是否是渠道商
   const cookieInfo = document.cookie.split('; ')
-  cooperateCode.value = cookieInfo.some(item => item.indexOf('channelCode') > -1)
+  cooperateCode.value = cookieInfo.some(item => item.includes('channelCode'))
   // 是否是免费用户
   const isFree = computed(() => {
     return useStore().getters['user/isFree']
@@ -52,7 +53,7 @@ export default function () {
 
   // 页面tab切换Model
   const { searchTabs, doChangeTab } = useSearchTabsModel({ defaultTab: 4, isInApp: isInApp.value })
-  const  { searchvalue: searchValue } = useRoute().query
+  const { searchvalue: searchValue } = useRoute().query
 
   const inputKeywordsState = ref({
     input: searchValue || ''
@@ -82,9 +83,9 @@ export default function () {
    * @param [params] - 可选值,默认会和 getParams(params) 返回值进行合并
    */
   function doQuery(params = {}) {
-    return doRunQuery(getParams(params)).then(res => {
+    return doRunQuery(getParams(params)).then((res) => {
       // 获取采购单位历史搜索、历史浏览记录
-      getHistoryQuery({ type: '4,5'})
+      getHistoryQuery({ type: '4,5' })
     })
   }
 
@@ -102,13 +103,14 @@ export default function () {
     listState.pageNum = 1
     return doQuery(params).then(() => {
       if (list.value) {
-        const idArr = list.value.map(v => {
+        const idArr = list.value.map((v) => {
           return v.buyer
         })
         if (inResourceBI) {
           // BI收录功能
           getEmployInfo(idArr)
-        } else {
+        }
+        else {
           // 非BI监控信息
           getMonitorInfo(idArr)
         }
@@ -127,40 +129,42 @@ export default function () {
 
   // 处理列表数据
   const formatList = computed(() => {
-    return list.value.map(v => {
-      if(isFree.value) {
+    return list.value.map((v) => {
+      if (isFree.value) {
         v.power = [1]
-      } else {
-        if(v.isFollowed) {
+      }
+      else {
+        if (v.isFollowed) {
           v.power = [2, 3, 4]
-        } else {
+        }
+        else {
           v.power = [1, 3]
         }
       }
-      if(v.province) {
+      if (v.province) {
         if (v.city) {
-          v.region = v.province + ' ' + v.city
-        } else {
+          v.region = `${v.province} ${v.city}`
+        }
+        else {
           v.region = v.province
         }
-      } else {
-        v.region = v.city? v.city : '-'
+      }
+      else {
+        v.region = v.city ? v.city : '-'
       }
       return {
         randomBg: randomBgc(),
         name: v.buyer,
         shortname: getShortName(v.buyer),
         firstList: [{
-            key: '所在地:',
-            value: v.region,
-            show: true
-          },
-          {
-            key: '采购单位类型:',
-            value: v.buyerClass? v.buyerClass : '-',
-            show: true
-          }
-        ],
+          key: '所在地:',
+          value: v.region,
+          show: true
+        }, {
+          key: '采购单位类型:',
+          value: v.buyerClass ? v.buyerClass : '-',
+          show: true
+        }],
         ...v
       }
     })
@@ -172,7 +176,7 @@ export default function () {
       showSelect: false,
       headerActions: [],
       list: formatList.value,
-      listState: listState
+      listState
     }
   })
 
@@ -221,9 +225,9 @@ export default function () {
   }
 
   // 获取搜索历史业务数据模型
-  const searchHistoryModel = useSearchHistoryModel({ type: '4,5'})
+  const searchHistoryModel = useSearchHistoryModel({ type: '4,5' })
   const { getHistoryQuery, clearHistoryQuery, saveViewHistoryQuery, searchHistoryList, browseHistoryList } = searchHistoryModel
- 
+
   // 采购单位搜索历史列表
   const buyerSearchHistoryList = computed(() => {
     return searchHistoryList.value
@@ -238,9 +242,10 @@ export default function () {
 
   const buySearchList = ref([])
 
-  const getAssociationList = debounce(async function(value) {
-    if (!value) return
-    const { data } = await getBuyerAssociation({ name: value})
+  const getAssociationList = debounce(async (value) => {
+    if (!value)
+      return
+    const { data } = await getBuyerAssociation({ name: value })
     if (data?.list) {
       buySearchList.value = data.list
     }
@@ -264,18 +269,20 @@ export default function () {
     let BIPage = ''
     if (inResourceBI) {
       BIPage = '?resource=BI'
-    } else {
+    }
+    else {
       BIPage = ''
     }
     const seoUrl = `/dw/${seo_id}.html${BIPage}`
-    const url = `/swordfish/page_big_pc/free/loading/buyer/${buyer}`
+    const url = `/swordfish/page_big_pc/free/loading/buyer/${buyer}${BIPage}`
     // 渠道合作投放用户(匿名用户)
     if (cooperateCode.value && !isLogin.value) {
       return goLogin()
     }
     if (!isLogin.value) {
       window.open(seoUrl)
-    } else {
+    }
+    else {
       window.open(url)
     }
   }
@@ -302,11 +309,12 @@ export default function () {
   }
   // 添加监控(免费用户留资)
   const setMonitor = async (item) => {
-    if(isFree.value) {
-      if(collectElementRef.value) {
+    if (isFree.value) {
+      if (collectElementRef.value) {
         collectElementRef.value.isNeedSubmit('pc_buyer_monitor_freeuser', () => {})
       }
-    } else {
+    }
+    else {
       const { data, error_code: code, error_msg: msg } = await setStatusCustomer({
         name: item.buyer,
         city: item.city,
@@ -320,26 +328,31 @@ export default function () {
             if (!msg_open) {
               // 打开推送设置权限
               dialog.push = true
-            } else {
+            }
+            else {
               that.$toast('监控成功,您可前往“工作台-商机-业主监控”查看', 3000)
             }
             doSearch()
-          } else {
+          }
+          else {
             if (limit_status === 1) {
               // 有权限但已使用完次数:超级订阅用户
-              if(collectElementRef.value) {
+              if (collectElementRef.value) {
                 collectElementRef.value.isNeedSubmit('pc_buyer_monitor_limit', () => {})
               }
-            } else if (limit_status === 2) {
+            }
+            else if (limit_status === 2) {
               // 有权限但已使用完次数:非超级订阅用户
               // 监控业主个数已达上限
               // 您最多可监控**个业主,可联系客服,申请监控更多业主
               dialog.limit = true
-            } else {
-              that.$toast(msg ? msg : '监控失败', 2000)
+            }
+            else {
+              that.$toast(msg || '监控失败', 2000)
             }
           }
-        } else {
+        }
+        else {
           // 取消监控
           if (status) {
             dialog.cancel = false
@@ -347,18 +360,20 @@ export default function () {
             doSearch()
           }
         }
-      } else {
+      }
+      else {
         that.$toast(data.msg)
       }
     }
   }
   // 申请更多业主监控
   const toMonitorMore = (item) => {
-    if(!isFree.value) {
-      if(collectElementRef.value) {
+    if (!isFree.value) {
+      if (collectElementRef.value) {
         collectElementRef.value.isNeedSubmit('pc_buyer_monitor_more', () => {})
       }
-    } else {
+    }
+    else {
       dialog.more = true
     }
   }
@@ -389,11 +404,13 @@ export default function () {
     try {
       if (isInApp.value) {
         window.$BRACE.$emit('open-customer')
-      } else {
+      }
+      else {
         // 打开客服弹窗
         window.checkCustomerService()
       }
-    } catch (e) {
+    }
+    catch (e) {
       console.warn(e)
     }
   }
@@ -416,8 +433,8 @@ export default function () {
       idArr: idArr ? idArr.join(',') : ''
     })
     if (code === 0 && data) {
-      list.value = list.value.map(v => {
-        data.forEach(r => {
+      list.value = list.value.map((v) => {
+        data.forEach((r) => {
           if (r.id === v.buyer) {
             v.active = r.isEmploy ? 1 : 0
           }
@@ -434,13 +451,14 @@ export default function () {
       employType: 3
     })
     if (data?.status) {
-      list.value = list.value.map(v => {
+      list.value = list.value.map((v) => {
         if (item.buyer === v.buyer) {
           v.active = !v.active ? 1 : 0
         }
         return v
       })
-    } else {
+    }
+    else {
       that.$toast(error_msg)
     }
   }
@@ -458,23 +476,23 @@ export default function () {
     if (!item.isReceived) {
       const { top, left } = calcCardTopLeft(event)
       $('.tags-box')
-      .show(function () {
-        window.pushListActiveTags = []
-        $('.tag-labels').empty()
-        $('.clear-input').val('')
-        $('.tags-list').find('.tags-item').removeClass('tags-active')
-        $('.tag-placeholder').show()
-      })
-      .css({
-        top: top,
-        right: 'unset',
-        left: left
-      })
+        .show(() => {
+          window.pushListActiveTags = []
+          $('.tag-labels').empty()
+          $('.clear-input').val('')
+          $('.tags-list').find('.tags-item').removeClass('tags-active')
+          $('.tag-placeholder').show()
+        })
+        .css({
+          top,
+          right: 'unset',
+          left
+        })
       // 点击确定按钮,绑定标签
-      $('.tags-footer .button-confirm').on('click', function () {
+      $('.tags-footer .button-confirm').on('click', () => {
         if (!$('.tags-box').is(':hidden')) {
-          var lids = ''
-          var lname = ''
+          let lids = ''
+          let lname = ''
           $('.tags-item.tags-active').each(function () {
             if ($(this).attr('data-id')) {
               if (lids != '') {
@@ -491,7 +509,7 @@ export default function () {
           console.log(params, '6666')
           // 执行保存绑定标签操作
           if (params.label !== '') {
-            saveChooseTags(params, function () {
+            saveChooseTags(params, () => {
               that.$toast('认领成功!', 2000)
               $('.tags-footer .button-cancel').trigger('click')
               doSearch()
@@ -499,9 +517,10 @@ export default function () {
           }
         }
       })
-      
+
       // window.getUserTags()
-    } else {
+    }
+    else {
       // 取消认领
       const { data, error_code: code, error_msg: msg } = await renLingCustomerAddTags({
         name: item.recId,
@@ -511,23 +530,24 @@ export default function () {
       })
       if (code === 0) {
         if (data === false) {
-          that.$toast(msg ? msg : '取消认领失败!', 2000)
-        } else {
+          that.$toast(msg || '取消认领失败!', 2000)
+        }
+        else {
           that.$toast('取消认领成功!', 2000)
           doSearch()
-        } 
+        }
       }
     }
   }
 
   // 进入工作台
   const goWorkSpace = () => {
-    const purchaseData ={
+    const purchaseData = {
       keywords: inputKeywordsState.value.input,
       filterState: filterState.value,
       apiParams: getFormatAPIParams()
     }
-    const goHref_ = location.origin + '/jylab/purSearch/index.html'
+    const goHref_ = `${location.origin}/jylab/purSearch/index.html`
     // 保存数据,进入工作台
     sessionStorage.setItem('purchase-search-filter-storage', JSON.stringify(purchaseData))
     window.location.replace(`/page_workDesktop/work-bench/page?link=${encodeURIComponent(goHref_)}`)

+ 245 - 178
apps/bigmember_pc/src/views/vipsubscribe/Buy.vue

@@ -6,69 +6,75 @@
       :status="activity.status"
       @sub="onSubscribe"
       @updateCallback="updateCallback"
-    >
-    </LimitedBanner>
-    <Layout class="vip-subscribe-buy" contentWithState="full" :needAd="false">
-      <div class="vip-subscribe-title">{{ buyTypeText }}超级订阅</div>
+    />
+    <Layout class="vip-subscribe-buy" content-with-state="full" :need-ad="false">
+      <div class="vip-subscribe-title">
+        {{ buyTypeText }}超级订阅
+      </div>
       <div class="vip-subscribe-content">
         <div class="vip-sub-list">
           <AreaSelector
+            v-show="moduleShow.areaSelector"
             ref="areaSelector"
             class="vip-sub-list-item"
             :class="{ 'pd-b0': upgradeTipShow }"
-            v-show="moduleShow.areaSelector"
-            :showSearchInput="false"
-            :onlyProvince="true"
-            :showSelectResult="true"
-            selectorType="line"
+            :show-search-input="false"
+            :only-province="true"
+            :show-select-result="true"
+            selector-type="line"
             @onChange="onAreaChange"
           >
-            <div slot="header" class="vip-sub-item-title">购买区域</div>
+            <div slot="header" class="vip-sub-item-title">
+              购买区域
+            </div>
           </AreaSelector>
           <SelectorCard
-            class="vip-sub-tip"
             v-if="moduleShow.areaSelector"
-            :cardType="conf.selectorType"
+            class="vip-sub-tip"
+            :card-type="conf.selectorType"
           >
-            <div slot="header" class="vip-sub-item-title"></div>
-            <div class="tip-content font-red" v-show="upgradeTipShow">
+            <div slot="header" class="vip-sub-item-title" />
+            <div v-show="upgradeTipShow" class="tip-content font-red">
               请增加购买区域进行升级,当前选择省份数量未高于原套餐数,无法升级
             </div>
           </SelectorCard>
           <SelectorCard
-            class="vip-sub-list-item"
             v-if="moduleShow.specList"
-            :cardType="conf.selectorType"
+            class="vip-sub-list-item"
+            :card-type="conf.selectorType"
           >
-            <div slot="header" class="vip-sub-item-title">选择购买周期</div>
+            <div slot="header" class="vip-sub-item-title">
+              选择购买周期
+            </div>
             <div class="vip-sub-item-content">
               <SpecList
-                :list="specList"
                 v-model="specIdActive"
+                :list="specList"
                 @onclick="specChange"
                 @updateCallback="updateCallback"
               />
               <div class="spec-list-bottom-tips">
-                <div class="spec-tips-item" v-show="medical.buyGiveTip">
+                <div v-show="medical.buyGiveTip" class="spec-tips-item">
                   <div class="spec-tips-item-content">
                     <span class="text">免费赠送 [医械通] </span>
                     <a
                       class="link-button"
                       href="/page_workDesktop/work-bench/app/big/medical/Credentials"
                       target="_blank"
-                      >了解更多</a
-                    >
+                    >了解更多</a>
                   </div>
                 </div>
               </div>
             </div>
           </SelectorCard>
           <SelectorCard
-            class="vip-sub-list-item"
             v-show="moduleShow.coupon"
-            :cardType="conf.selectorType"
+            class="vip-sub-list-item"
+            :card-type="conf.selectorType"
           >
-            <div slot="header" class="vip-sub-item-title">选择优惠券</div>
+            <div slot="header" class="vip-sub-item-title">
+              选择优惠券
+            </div>
             <div class="vip-sub-item-content">
               <CouponCardList
                 ref="couponRef"
@@ -90,8 +96,10 @@
                                   @loaded="giftListLoaded" />
                               </div>
                             </SelectorCard> -->
-          <SelectorCard class="vip-sub-list-item" :cardType="conf.selectorType">
-            <div slot="header" class="vip-sub-item-title">手机号码</div>
+          <SelectorCard class="vip-sub-list-item" :card-type="conf.selectorType">
+            <div slot="header" class="vip-sub-item-title">
+              手机号码
+            </div>
             <div class="vip-sub-item-content">
               <CheckPhone v-model="userInfo.phone" :pass.sync="phoneRegPass" />
             </div>
@@ -101,15 +109,14 @@
           ref="buySubmitRef"
           :pass="allPass"
           :submit-text="submitText"
-          :productionTotal="computedPrice.total / 100"
-          :productionDiscount="computedPrice.discount / 100"
-          :productionPay="computedPrice.pay / 100"
+          :production-total="computedPrice.total / 100"
+          :production-discount="computedPrice.discount / 100"
+          :production-pay="computedPrice.pay / 100"
           @submit="submit"
-        >
-        </BuySubmit>
+        />
       </div>
       <div class="vip-subscribe-desc">
-        <Contrast></Contrast>
+        <Contrast />
       </div>
     </Layout>
     <el-dialog
@@ -119,13 +126,17 @@
       center
       width="400px"
     >
-      <div class="dialog-title" slot="title">超级订阅更新提醒</div>
+      <div slot="title" class="dialog-title">
+        超级订阅更新提醒
+      </div>
       <div class="dialog-content">
         <p class="c-main">
           为满足用户精准获取商机的需求,剑鱼标讯推出超前项目推荐服务。原超级订阅用户中断续费后,将不再支持采购意向、拟建项目查看。超前项目推荐服务整合更多前期项目机会,提供更强服务。
         </p>
-        <div class="c-img"></div>
-        <p class="c-footer">扫一扫,立即联系客户经理了解超前项目推荐服务</p>
+        <div class="c-img" />
+        <p class="c-footer">
+          扫一扫,立即联系客户经理了解超前项目推荐服务
+        </p>
       </div>
       <div slot="footer">
         <span class="know-btn" @click="buyDialog = false">我知道了</span>
@@ -135,6 +146,8 @@
 </template>
 
 <script>
+import { Dialog } from 'element-ui'
+import qs from 'qs'
 import Layout from '@/components/common/ContentLayout.vue'
 import SelectorCard from '@/components/selector/SelectorCard.vue'
 import AreaSelector from '@/components/selector/AreaSelector.vue'
@@ -145,22 +158,20 @@ import CheckPhone from '@/components/coupon/CheckPhone.vue'
 import BuySubmit from '@/components/coupon/BuySubmit.vue'
 import Contrast from '@/views/vipsubscribe/components/Contrast.vue'
 import LimitedBanner from '@/components/limited-banner/index.vue'
-import { Dialog } from 'element-ui'
-import qs from 'qs'
 
 import {
+  appointmentAdd,
   commodityDetail,
   commodityDiscounts,
   commodityPrice,
-  getSVIPBuyInfo,
-  getUserAccountInfo,
   createCommonOrder,
-  appointmentAdd,
-  getMedicalIndustry
+  getMedicalIndustry,
+  getSVIPBuyInfo,
+  getUserAccountInfo
 } from '@/api/modules/'
 
 export default {
-  name: 'vip-subscribe-buy',
+  name: 'VipSubscribeBuy',
   components: {
     Layout,
     SelectorCard,
@@ -174,6 +185,37 @@ export default {
     [Dialog.name]: Dialog,
     LimitedBanner
   },
+  beforeRouteEnter(to, from, next) {
+    console.log(to.query)
+    const map = {
+      buy: '开通',
+      renew: '续费',
+      upgrade: '升级'
+    }
+    const title = document.title
+    const { type } = to.query || {}
+    if (type && map[type]) {
+      // 修改头部高亮
+      document.title = `${map[type]}超级订阅`
+    }
+    next((vm) => {
+      vm.preTitle = title
+    })
+  },
+  beforeRouteUpdate(to, from, next) {
+    this.$nextTick(async () => {
+      this.getType()
+      await this.getUserBuyInfo()
+      await this.commodityDetail()
+      await this.updatePrice()
+      this.getUserAccountInfo()
+    })
+    next()
+  },
+  beforeRouteLeave(to, from, next) {
+    document.title = this.preTitle
+    next()
+  },
   data() {
     return {
       // 是否可适用优惠金额
@@ -275,7 +317,8 @@ export default {
     upgradeTipShow() {
       if (this.buyType === 'buy' || this.buyType === 'renew') {
         return false
-      } else {
+      }
+      else {
         return !this.canUpgrade
       }
     },
@@ -285,28 +328,34 @@ export default {
       console.log(buyset, selectCount, this.oldVip)
       if (this.buyType === 'buy' || this.buyType === 'renew') {
         return false
-      } else {
+      }
+      else {
         if (buyset.areacount === -1) {
           // 老版超级订阅全国全行业用户选择16个地区以上或者全国才能去支付
           if (this.oldVip) {
             if (
-              this.computedPrice.pay >= 0 &&
-              (selectCount >= 16 || selectCount === -1)
+              this.computedPrice.pay >= 0
+              && (selectCount >= 16 || selectCount === -1)
             ) {
               return true
-            } else {
+            }
+            else {
               return false
             }
-          } else {
+          }
+          else {
             return true
           }
-        } else {
+        }
+        else {
           if (selectCount === -1) {
             return true
-          } else {
+          }
+          else {
             if (this.oldVip) {
               return selectCount >= buyset.areacount
-            } else {
+            }
+            else {
               return selectCount > buyset.areacount
             }
           }
@@ -351,7 +400,8 @@ export default {
     selectedCount() {
       if (this.buyType === 'renew') {
         return this.buyInfo.buyset.areacount || 0
-      } else {
+      }
+      else {
         return this.selectedCountInfo.num
       }
     },
@@ -359,29 +409,33 @@ export default {
       const basicReg = this.phoneRegPass && !this.noSelect && this.autoPass
       if (this.buyType === 'buy') {
         return basicReg
-      } else if (this.buyType === 'upgrade') {
+      }
+      else if (this.buyType === 'upgrade') {
         if (this.oldVip) {
           return basicReg && this.canUpgrade && this.oldVip
-        } else {
+        }
+        else {
           return basicReg && (this.canUpgrade || this.oldVip)
         }
-      } else if (this.buyType === 'renew') {
+      }
+      else if (this.buyType === 'renew') {
         return basicReg && this.computedPrice.pay > 0
-      } else {
+      }
+      else {
         return false
       }
     },
     availableCouponList() {
       const list = this.couponList
       return list
-        .filter((v) => v?.usable)
+        .filter(v => v?.usable)
         .sort((a, b) => a.discount - b.discount)
     },
     unavailableCouponList() {
       const list = this.couponList
       const now = Date.now() / 1000
       // 不可用优惠优先展示进行中优惠,之后展示预热优惠。两类优惠按照优惠力度从从大到小展示
-      const unavailableList = list.filter((v) => !v?.usable)
+      const unavailableList = list.filter(v => !v?.usable)
       // 预热优惠
       const preArr = []
       // 进行中优惠
@@ -391,9 +445,11 @@ export default {
       unavailableList.forEach((item) => {
         if (item.starttime < now) {
           preArr.push(item)
-        } else if (item.starttime > now && item.endtime < now) {
+        }
+        else if (item.starttime > now && item.endtime < now) {
           penArr.push(item)
-        } else {
+        }
+        else {
           otherArr.push(item)
         }
       })
@@ -409,43 +465,12 @@ export default {
     isDiscounts() {
       console.log(JSON.stringify(this.activity))
       return (
-        this.activity &&
-        this.activity.stockNumber &&
-        this.activity.stockNumber > 0
+        this.activity
+        && this.activity.stockNumber
+        && this.activity.stockNumber > 0
       )
     }
   },
-  beforeRouteEnter(to, from, next) {
-    console.log(to.query)
-    const map = {
-      buy: '开通',
-      renew: '续费',
-      upgrade: '升级'
-    }
-    const title = document.title
-    const { type } = to.query || {}
-    if (type && map[type]) {
-      // 修改头部高亮
-      document.title = `${map[type]}超级订阅`
-    }
-    next((vm) => {
-      vm.preTitle = title
-    })
-  },
-  beforeRouteUpdate(to, from, next) {
-    this.$nextTick(async () => {
-      this.getType()
-      await this.getUserBuyInfo()
-      await this.commodityDetail()
-      await this.updatePrice()
-      this.getUserAccountInfo()
-    })
-    next()
-  },
-  beforeRouteLeave(to, from, next) {
-    document.title = this.preTitle
-    next()
-  },
   async created() {
     // this.initAdjustTime()
     this.setIntervalFn()
@@ -461,7 +486,8 @@ export default {
         .find('span[name=大会员]')
         .removeClass('active')
       window.trySelectNav('招标订阅')
-    } catch (error) {}
+    }
+    catch (error) {}
     this.checkMedicalTip()
   },
   destroyed() {
@@ -469,11 +495,12 @@ export default {
   },
   methods: {
     getType() {
-      var { type, token, phone } = this.$route.query
-      var types = ['buy', 'upgrade', 'renew']
+      const { type, token, phone } = this.$route.query
+      const types = ['buy', 'upgrade', 'renew']
       if (types.includes(type)) {
         this.buyType = type || types[0]
-      } else {
+      }
+      else {
         this.buyType = types[0]
       }
       if (token) {
@@ -485,9 +512,11 @@ export default {
       if (this.buyType === 'upgrade') {
         this.moduleShow.areaSelector = true
         this.moduleShow.specList = false
-      } else if (this.buyType === 'renew') {
+      }
+      else if (this.buyType === 'renew') {
         this.moduleShow.areaSelector = false
-      } else {
+      }
+      else {
         this.moduleShow.areaSelector = true
       }
     },
@@ -505,7 +534,8 @@ export default {
         if (count === '1') {
           this.selectInfo.area = { 安徽: [] }
           this.$refs.areaSelector.content.setCitySelected(this.selectInfo.area)
-        } else if (count === '-1') {
+        }
+        else if (count === '-1') {
           this.selectInfo.area = {}
           this.$refs.areaSelector.content.setCitySelected(this.selectInfo.area)
         }
@@ -515,9 +545,11 @@ export default {
       if (this.buyType === 'upgrade') {
         this.getInitSpec()
         // this.specChange({})
-      } else if (this.buyType === 'renew') {
+      }
+      else if (this.buyType === 'renew') {
         this.getInitSpec()
-      } else {
+      }
+      else {
         // 设置城市选择器所有不选中
         if (!this.selectInfo.area) {
           this.$refs.areaSelector.content.setAllNoSelected()
@@ -536,7 +568,7 @@ export default {
         productType: '超级订阅',
         spec: {
           area: this.selectInfo.area,
-          orderType: orderType
+          orderType
         }
       })
       if (!goodsInfo.data) {
@@ -549,14 +581,16 @@ export default {
         // 商品信息 start
         e.sku.forEach((ele) => {
           const item = {}
-          if (e.title.indexOf('超级订阅') > -1) {
-            if (ele.info.indexOf('月') > -1) {
+          if (e.title.includes('超级订阅')) {
+            if (ele.info.includes('月')) {
               item.cycleType = 1
               item.value = '1个月'
-            } else if (ele.info.indexOf('季') > -1) {
+            }
+            else if (ele.info.includes('季')) {
               item.cycleType = 2
               item.value = '1季'
-            } else if (ele.info.indexOf('年') > -1) {
+            }
+            else if (ele.info.includes('年')) {
               item.cycleType = 3
               item.value = '1年'
             }
@@ -565,8 +599,8 @@ export default {
             item.price = ele.originalPrice / 100
             item.productionId = ele.productId
             item._data = ele
-            const rules =
-              type === 'updata'
+            const rules
+              = type === 'updata'
                 ? this.specIdActive === ele.productId
                 : ele.choosed
             if (rules) {
@@ -593,21 +627,23 @@ export default {
                       //   this.isActivity = false
                       // }
                       if (
-                        Date.now() < this.activity.endTime &&
-                        (this.activity.activityType === 3 ||
-                          this.activity.activityType === 4 ||
-                          this.activity.activityType === 5)
+                        Date.now() < this.activity.endTime
+                        && (this.activity.activityType === 3
+                          || this.activity.activityType === 4
+                          || this.activity.activityType === 5)
                       ) {
                         this.isActivity = true
-                      } else {
+                      }
+                      else {
                         this.isActivity = false
                       }
                       if (
-                        Date.now() > a.preStartTime * 1000 &&
-                        a.preStartTime
+                        Date.now() > a.preStartTime * 1000
+                        && a.preStartTime
                       ) {
                         this.preStart = true
-                      } else {
+                      }
+                      else {
                         this.preStart = false
                       }
                       if (d.tag) {
@@ -620,11 +656,13 @@ export default {
                     couponList.push(d)
                   })
                 })
-              } else {
+              }
+              else {
                 this.couponActiveItem = {}
                 this.isActivity = false
               }
-            } else {
+            }
+            else {
               if (ele.activity) {
                 ele.activity.forEach((a) => {
                   a.discount.forEach((d) => {
@@ -648,7 +686,8 @@ export default {
       })
       if (this.specList.length === 0) {
         this.moduleShow.specList = false
-      } else {
+      }
+      else {
         if (this.buyType !== 'upgrade') {
           this.moduleShow.specList = true
         }
@@ -659,15 +698,17 @@ export default {
       this.calcSpecPrice() // 计算每日价格
       try {
         callback()
-      } catch (e) {}
+      }
+      catch (e) {}
     },
     async updataTips(item) {
       const { _data } = item
       // 获取默认优惠角标
       const defaultCouponInfo = this.couponList.find(
-        (coupon) => coupon.lotteryId === _data.lotteryId
+        coupon => coupon.lotteryId === _data.lotteryId
       )
-      if (!defaultCouponInfo) return
+      if (!defaultCouponInfo)
+        return
       // 更新角标信息
       this.specList.forEach((s) => {
         if (_data.productId === s.id) {
@@ -681,11 +722,11 @@ export default {
       let tipText = ''
       const couponListItem = []
       const chooseList = await commodityDiscounts({
-        id: id,
+        id,
         productType: '超级订阅',
         spec: {
           area: this.selectInfo.area,
-          orderType: orderType
+          orderType
         }
       })
       chooseList.data.forEach((e) => {
@@ -712,18 +753,20 @@ export default {
                     //   this.isActivity = false
                     // }
                     if (
-                      Date.now() < this.activity.endTime &&
-                      (this.activity.activityType === 3 ||
-                        this.activity.activityType === 4 ||
-                        this.activity.activityType === 5)
+                      Date.now() < this.activity.endTime
+                      && (this.activity.activityType === 3
+                        || this.activity.activityType === 4
+                        || this.activity.activityType === 5)
                     ) {
                       this.isActivity = true
-                    } else {
+                    }
+                    else {
                       this.isActivity = false
                     }
                     if (Date.now() > a.preStartTime * 1000 && a.preStartTime) {
                       this.preStart = true
-                    } else {
+                    }
+                    else {
                       this.preStart = false
                     }
                     if (d.tag) {
@@ -731,7 +774,8 @@ export default {
                     }
                     this.couponActiveItem = d
                   }
-                } else {
+                }
+                else {
                   d.choosed = false
                   if (d.lotteryId === this.couponActiveItem.lotteryId) {
                     // 商品选中的券确定当前选中活动
@@ -744,18 +788,20 @@ export default {
                     this.activity.status = a.isAppointment
                     this.activity.productId = ele.productId
                     if (
-                      Date.now() < this.activity.endTime &&
-                      (this.activity.activityType === 3 ||
-                        this.activity.activityType === 4 ||
-                        this.activity.activityType === 5)
+                      Date.now() < this.activity.endTime
+                      && (this.activity.activityType === 3
+                        || this.activity.activityType === 4
+                        || this.activity.activityType === 5)
                     ) {
                       this.isActivity = true
-                    } else {
+                    }
+                    else {
                       this.isActivity = false
                     }
                     if (Date.now() > a.preStartTime * 1000 && a.preStartTime) {
                       this.preStart = true
-                    } else {
+                    }
+                    else {
                       this.preStart = false
                     }
                     if (d.tag) {
@@ -769,7 +815,8 @@ export default {
                 couponListItem.push(d)
               })
             })
-          } else {
+          }
+          else {
             this.couponActiveItem = {}
             this.isActivity = false
           }
@@ -785,7 +832,8 @@ export default {
       this.couponList = couponListItem
       try {
         callback()
-      } catch (e) {}
+      }
+      catch (e) {}
     },
     async updatePrice() {
       // 计算价格接口
@@ -799,7 +847,7 @@ export default {
         productId: this.specActiveItem.id,
         spec: {
           area: this.selectInfo.area,
-          orderType: orderType,
+          orderType,
           time: this.specActiveItem.value
         }
       }
@@ -808,14 +856,16 @@ export default {
         this.computedPrice.total = data.originalPrice
         this.computedPrice.pay = data.discountPrice
         this.computedPrice.discount = data.discountAmount
-      } else {
+      }
+      else {
         this.computedPrice.total = 0
         this.computedPrice.pay = 0
         this.computedPrice.discount = 0
       }
     },
     async checkMedicalTip() {
-      if (this.buyType !== 'buy') return
+      if (this.buyType !== 'buy')
+        return
       const params = this.token ? { token: this.token } : null
       const { data } = await getMedicalIndustry(params)
       if (data) {
@@ -829,7 +879,8 @@ export default {
         if (data) {
           if (data.breakRenewTip > 0 && this.buyType === 'buy') {
             this.buyDialog = true
-          } else {
+          }
+          else {
             this.buyDialog = false
           }
         }
@@ -849,10 +900,10 @@ export default {
         const { buyset } = this.buyInfo
         // 购买了全国的新超级订阅用户,升级不能再选择省份了
         if (
-          this.buyType === 'upgrade' &&
-          buyset &&
-          buyset.areacount === -1 &&
-          !this.oldVip
+          this.buyType === 'upgrade'
+          && buyset
+          && buyset.areacount === -1
+          && !this.oldVip
         ) {
           this.moduleShow.selectArea = false
         }
@@ -876,7 +927,8 @@ export default {
       const { buyset, isvip, startTime: start, endTime: end } = info
       const { path } = this.$route
       const { redirected } = this.$route.query
-      if (redirected) return
+      if (redirected)
+        return
 
       const redirect = {
         enable: false,
@@ -898,13 +950,15 @@ export default {
             redirect.type = 'upgrade'
             await this.showOldVipTip()
           }
-        } else {
+        }
+        else {
           // 开通了,过期了
           if (this.buyType !== 'buy') {
             redirect.enable = true
           }
         }
-      } else {
+      }
+      else {
         // 未开通
         if (this.buyType !== 'buy') {
           redirect.enable = true
@@ -962,7 +1016,8 @@ export default {
             this.$set(e, 'tipText', '')
           }
         })
-      } else {
+      }
+      else {
         await this.chooseGoods('updata', this.couponActiveItem.productId)
       }
       this.loading = false
@@ -971,26 +1026,26 @@ export default {
 
     commonGetPrice(price, cycleType) {
       const priceInfo = {
-        price: price,
+        price,
         perDayPrice: 0
       }
       switch (cycleType) {
         // 1个月
         case 1: {
-          priceInfo.perDayPrice =
-            price === 0 ? 0 : (priceInfo.price / 30).toFixed(2)
+          priceInfo.perDayPrice
+            = price === 0 ? 0 : (priceInfo.price / 30).toFixed(2)
           break
         }
         // 1个季度
         case 2: {
-          priceInfo.perDayPrice =
-            price === 0 ? 0 : (priceInfo.price / (30 * 3)).toFixed(2)
+          priceInfo.perDayPrice
+            = price === 0 ? 0 : (priceInfo.price / (30 * 3)).toFixed(2)
           break
         }
         // 1年
         case 3: {
-          priceInfo.perDayPrice =
-            price === 0 ? 0 : (priceInfo.price / 365).toFixed(2)
+          priceInfo.perDayPrice
+            = price === 0 ? 0 : (priceInfo.price / 365).toFixed(2)
           break
         }
         default: {
@@ -1027,10 +1082,12 @@ export default {
           time: this.specActiveItem.value,
           orderType: 1
         }
-      } else if (this.buyType === 'upgrade') {
+      }
+      else if (this.buyType === 'upgrade') {
         if (buyInfo.buyset && buyInfo.buyset.upgrade === 1) {
           // buyInfo.buyset.upgrade === 1新版本升级
-        } else {
+        }
+        else {
           // 旧版本超级订阅升级
         }
 
@@ -1039,13 +1096,15 @@ export default {
           area: JSON.stringify(selectArea),
           orderType: 3
         }
-      } else if (this.buyType === 'renew') {
+      }
+      else if (this.buyType === 'renew') {
         return {
           ...param,
           time: this.specActiveItem.value,
           orderType: 2
         }
-      } else {
+      }
+      else {
         return {}
       }
     },
@@ -1081,7 +1140,7 @@ export default {
         activityType:
           this.couponActiveItem.type === null ? -1 : this.couponActiveItem.type,
         data: {
-          type: type,
+          type,
           order_phone: this.userInfo.phone,
           disWord: ''
         }
@@ -1095,7 +1154,6 @@ export default {
       return createCommonOrder(t)
     },
     async submit() {
-      // eslint-disable-next-line
       const { data, error_msg } = await this.submitXHR()
       if (data) {
         if (this.token) {
@@ -1107,14 +1165,16 @@ export default {
         this.autoPass = true
         if (data.needPay) {
           window.open(`/front/subvip/orderPay/${orderCode}`)
-        } else {
+        }
+        else {
           window.open(
-            `/front/subvip/paySuccess/${orderCode}?payTime=${parseInt(
+            `/front/subvip/paySuccess/${orderCode}?payTime=${Number.parseInt(
               Date.now() / 1000
             )}&from=vipUPgrade`
           )
         }
-      } else {
+      }
+      else {
         this.$toast(error_msg)
       }
     },
@@ -1128,7 +1188,8 @@ export default {
           this.$toast(
             '预约成功,活动开始前10分钟,将通过站内信再次通知您,请注意查收'
           )
-        } else {
+        }
+        else {
           this.$toast(msg)
         }
         this.appointmentStatus = data.status
@@ -1174,11 +1235,13 @@ export default {
           // console.log('活动未开始')
           // this.updatePrice()
           this.changeSubmitText(false)
-        } else if (this.serverInitTime === startTime) {
+        }
+        else if (this.serverInitTime === startTime) {
           this.updatePrice()
-        } else if (
-          this.serverInitTime >= startTime &&
-          this.serverInitTime < endTime
+        }
+        else if (
+          this.serverInitTime >= startTime
+          && this.serverInitTime < endTime
         ) {
           // 活动开始
           // console.log('活动开始')
@@ -1189,11 +1252,13 @@ export default {
           this.$nextTick(() => {
             if (stockNumber && stockNumber > 0) {
               this.changeSubmitText(true)
-            } else {
+            }
+            else {
               this.changeSubmitText(false)
             }
           })
-        } else if (this.serverInitTime >= endTime) {
+        }
+        else if (this.serverInitTime >= endTime) {
           // 活动结束
           setTimeout(() => {
             this.isActivity = false
@@ -1208,9 +1273,11 @@ export default {
     getOrdertype(v) {
       if (v === 'buy') {
         return 1
-      } else if (v === 'upgrade') {
+      }
+      else if (v === 'upgrade') {
         return 3
-      } else if (v === 'renew') {
+      }
+      else if (v === 'renew') {
         return 2
       }
     }

+ 161 - 61
apps/bigmember_pc/src/views/workspace/components/AccountInfo.vue

@@ -2,152 +2,241 @@
   <section class="user-info-card">
     <h4 class="user-info-title">
       <span>
-        <span class="user-info-title-fireworks"></span>
-        <span class="user-info-title-text highlight-text"
-        >欢迎您!<i class="user-nickname">{{ accountInfo.nickName }}</i></span>
+        <span class="user-info-title-fireworks" />
+        <span class="user-info-title-text highlight-text">欢迎您!<i class="user-nickname">{{ accountInfo.nickName }}</i></span>
       </span>
-      <span class="user-info-icon" v-if="accountInfo.vipType && (accountInfo.vipType === '超级订阅'  || accountInfo.vipType.includes('大会员'))">
-        <i :class="accountInfo.vipType === '超级订阅' ? 'vip-icon': 'bigmember-icon'"></i>
+      <span v-if="accountInfo.vipType && (accountInfo.vipType === '超级订阅' || accountInfo.vipType.includes('大会员'))" class="user-info-icon">
+        <i :class="accountInfo.vipType === '超级订阅' ? 'vip-icon' : 'bigmember-icon'" />
       </span>
     </h4>
     <p class="user-info-line user-info-type">
       <span class="user-info-line-label">账号类型:</span>
       <span class="user-info-line-value">{{ accountInfo.vipType }}</span>
-      <span class="u-i-line-tip" v-if="accountInfo.vipType === '注册用户'">您尚未开通会员</span>
+      <span v-if="accountInfo.vipType === '注册用户'" class="u-i-line-tip">您尚未开通会员</span>
     </p>
     <div
-      class="user-info-line user-info-free bg-gold"
       v-if="accountInfo.vipType === '注册用户'"
+      class="user-info-line user-info-free bg-gold"
     >
       <p>
         <span>
-          <i class="vip-icon"> </i>
+          <i class="vip-icon" />
           <span>开通超级订阅</span>
         </span>
-        <span class="activity-span" v-if="attr.subVipActMsg">
-          <i class="limit-time-icon"></i>
-          <span>{{attr.subVipActMsg}}</span>
+        <span v-if="attr.subVipActMsg" class="activity-span">
+          <i class="limit-time-icon" />
+          <span>{{ attr.subVipActMsg }}</span>
         </span>
       </p>
       <p class="handle-p">
         <span>立享27+项专属权益</span>
         <span
           class="handle-btn"
-          @click="buyVip('buy')">
+          @click="buyVip('buy')"
+        >
           立即开通</span>
       </p>
     </div>
     <div
-      class="user-info-line user-info-module"
       v-else-if="accountInfo.vipType === '超级订阅'"
+      class="user-info-line user-info-module"
     >
       <div class="user-info-line-value value-box bg-gold">
         <p>到期时间:{{ accountInfo.vipEntTime }}</p>
-        <p class="handle-btn  m-t-6"
-           @click="renewVip()">
-          去续费<i class="iconfont icon-more"></i>
+        <p
+          class="handle-btn  m-t-6"
+          @click="renewVip()"
+        >
+          去续费<i class="iconfont icon-more" />
         </p>
       </div>
-      <div class="gap"></div>
-      <div  class="user-info-line-value value-box bg-gold">
+      <div class="gap" />
+      <div class="user-info-line-value value-box bg-gold">
         <p>购买区域:{{ attr.buyMsg }}</p>
-        <p class="handle-btn m-t-6"
-           @click="updateVip">
-          {{ attr.upgrade ? '去升级' : '升级咨询'}}
-          <i class="iconfont icon-more"></i>
+        <p
+          class="handle-btn m-t-6"
+          @click="updateVip"
+        >
+          {{ attr.upgrade ? '去升级' : '升级咨询' }}
+          <i class="iconfont icon-more" />
         </p>
       </div>
     </div>
     <div
-      class="user-info-line user-info-module"
       v-else-if="accountInfo.vipType === '省份订阅包'"
+      class="user-info-line user-info-module"
     >
       <div class="user-info-line-value value-box bg-blue">
         <p>到期时间:{{ accountInfo.vipEntTime }}</p>
-        <p class="handle-btn m-t-6"
-           @click="openCustomer">
-          去续费<i class="iconfont icon-more"></i>
+        <p
+          class="handle-btn m-t-6"
+          @click="openCustomer"
+        >
+          去续费<i class="iconfont icon-more" />
         </p>
       </div>
-      <div class="gap"></div>
-      <div  class="user-info-line-value value-box" :class="attr.upgrade ? 'bg-blue' : 'bg-gold'">
+      <div class="gap" />
+      <div class="user-info-line-value value-box" :class="attr.upgrade ? 'bg-blue' : 'bg-gold'">
         <p>购买区域:{{ attr.buyMsg }}</p>
-        <p class="handle-btn m-t-6"  @click="areaVipUpdate">
-          {{attr.upgrade ? '去升级' : '开通超级订阅'}}
-          <i class="iconfont icon-more"></i>
+        <p class="handle-btn m-t-6" @click="areaVipUpdate">
+          {{ attr.upgrade ? '去升级' : '开通超级订阅' }}
+          <i class="iconfont icon-more" />
         </p>
       </div>
     </div>
-<!--    大会员或者商机管理-->
-    <div class="user-info-line user-info-module" v-else>
+    <!--    大会员或者商机管理 -->
+    <div v-else class="user-info-line user-info-module">
       <div class="user-info-line-value value-box bigmember bg-black">
         <p>到期时间:{{ accountInfo.vipEntTime }}</p>
-        <p class="handle-btn  m-t-6"
-           @click="renewConsult">续费咨询
-          <i class="iconfont icon-more"></i>
+        <p
+          class="handle-btn  m-t-6"
+          @click="renewConsult"
+        >
+          续费咨询
+          <i class="iconfont icon-more" />
         </p>
       </div>
     </div>
+    <!-- 引导赠送好友超级订阅 -->
+    <div v-if="isShowGift" class="gift-tip">
+      <span>支持送好友超级订阅</span>
+      <span class="gift-link" @click="goGiftLink">送给朋友></span>
+    </div>
   </section>
 </template>
 
 <script>
-import { getUserAccountShow } from '@/api/modules/'
+import { mapActions, mapState } from 'vuex'
+import { getSubDuration, getUserAccountShow } from '@/api/modules/'
+
 export default {
   name: 'UserAccount',
   data() {
     return {
       accountInfo: {
         nickName: '',
-      }
+      },
+      isShowGift: false,
+      subduration: {}
     }
   },
   computed: {
-    attr () {
+    ...mapState('user', ['info', 'identityList']),
+    attr() {
       return this.accountInfo.attr || {}
     }
   },
-  mounted() {
-    this.getAccount()
+  async mounted() {
+    await this.getAccount()
+    await this.fetchConfigTime()
+    this.getSubDurationEvent()
   },
   methods: {
+    ...mapActions('workspace/giftFriends', ['getConfigTime']),
+    async getSubDurationEvent() {
+      const { error_code: code, data } = await getSubDuration()
+      if (code === 0) {
+        this.subduration = data
+      }
+    },
+    fetchConfigTime() {
+      let isPerson = false
+      if (this.identityList && this.identityList.length > 0) {
+        this.identityList.forEach((item) => {
+          if (item.checked === 1) {
+            if (item.positionType === 0) {
+              isPerson = true
+            }
+            else {
+              isPerson = false
+            }
+          }
+        })
+      }
+      const isOnlyVipOrFree = this.accountInfo.vipType === '注册用户' || this.accountInfo.vipType === '超级订阅'
+      if (isPerson && isOnlyVipOrFree) {
+        try {
+          this.getConfigTime().then((res) => {
+            this.isShowGift = res || false
+          })
+        }
+        catch (error) {
+          console.error('加载配置时间失败:', error)
+        }
+      }
+      else {
+        this.isShowGift = false
+      }
+    },
+    goGiftLink() {
+      const vipTypeBool = this.accountInfo.vipType === '注册用户'
+      if (vipTypeBool) {
+        // 弹窗提醒购买超级订阅后赠送好友
+        this.$emit('showGiftDialog', 'buy')
+      }
+      else if (this.info.vipStatus > 0) {
+        const isOneMonthPassed = !this.subduration.gifted
+        if (isOneMonthPassed) {
+          // 弹窗提醒超级订阅不满一个月续费后赠送好友
+          this.$emit('showGiftDialog', 'renew')
+        }
+        else {
+          // 满足条件,可以直接赠送好友,打开送给朋友弹窗
+          this.$emit('showGiftDialog', 'gift')
+          // 赠送好友后,重新获取用户信息
+          this.getAccount()
+        }
+      }
+    },
+    // isOneMonthPassed(givenDateString) {
+    //   const givenDate = new Date(givenDateString).setHours(23, 59, 59, 999)
+    //   let currentDate = new Date()
+    //   // 增加一个月
+    //   currentDate.setMonth(currentDate.getMonth() + 1)
+    //   currentDate = new Date(currentDate).getTime()
+    //   // 比较增加一个月后的当前日期和给定日期
+    //   return currentDate >= givenDate
+    // },
     async getAccount() {
       const { data, error_code } = await getUserAccountShow()
-      if(error_code === 0 && data) {
-        const { nickname, list = []} = data
+      if (error_code === 0 && data) {
+        const { nickname, list = [] } = data
         const info = list[0] || {}
         this.accountInfo = {
           nickName: nickname,
           vipType: info.name,
-          vipEntTime:  info.endTime ? new Date(info.endTime * 1000 ).pattern('yyyy-MM-dd') : '',
+          vipEntTime: info.endTime ? new Date(info.endTime * 1000).pattern('yyyy-MM-dd') : '',
           attr: info.attr || {}
         }
       }
     },
-    buyVip (type) {
-      window.open('/swordfish/page_big_pc/free/svip/buy?type=' + type)
+    buyVip(type) {
+      window.open(`/swordfish/page_big_pc/free/svip/buy?type=${type}`)
     },
     // 超级订阅去续费,企业分配的需要跳转客服,自主购买的跳续费
-    renewVip () {
-      if(this.attr.renew) {
+    renewVip() {
+      if (this.attr.renew) {
         this.buyVip('renew')
-      } else {
+      }
+      else {
         this.openCustomer()
       }
     },
     // 升级超级订阅
-    updateVip () {
-      if(this.attr.upgrade) {
+    updateVip() {
+      if (this.attr.upgrade) {
         this.buyVip('upgrade')
-      } else {
+      }
+      else {
         this.openCustomer()
       }
     },
     // 省份订阅包升级
-    areaVipUpdate () {
-      if(this.attr.upgrade) {
+    areaVipUpdate() {
+      if (this.attr.upgrade) {
         this.openCustomer()
-      } else {
+      }
+      else {
         this.buyVip('buy')
       }
     },
@@ -155,18 +244,18 @@ export default {
     openCustomer() {
       this.contactCustomer(this)
     },
-    jumpPage (link) {
-      if(link) {
+    jumpPage(link) {
+      if (link) {
         window.open(link)
       }
     },
-    renewConsult () {
-      if(this.accountInfo.vipType === '商机管理') {
+    renewConsult() {
+      if (this.accountInfo.vipType === '商机管理') {
         this.openCustomer()
-      } else {
+      }
+      else {
         this.jumpPage(this.attr.pc)
       }
-
     }
   }
 }
@@ -308,6 +397,17 @@ export default {
       }
     }
   }
+  .gift-tip {
+    margin-top: 8px;
+    line-height: 22px;
+    font-size: 14px;
+    color: #1D1D1D;
+    .gift-link {
+      margin-left: 7px;
+      color: #FF3A20;
+      cursor: pointer;
+    }
+  }
   .bg-gold{
     background: linear-gradient(90deg,#FFECCE 0%, #FCD7B2 100%);
   }

+ 111 - 20
apps/bigmember_pc/src/views/workspace/dashboard.vue

@@ -1,49 +1,69 @@
 <template>
-  <el-container class="workspace-dashboard">
+  <el-container ref="workspaceDashboard" class="workspace-dashboard">
     <el-main>
-      <CommonUse class="main-module"></CommonUse>
+      <CommonUse class="main-module" />
       <div
-        class="main-module card-list-module"
         v-for="(moduleList, floor) in mainModuleList"
         :key="floor"
+        class="main-module card-list-module"
       >
         <component
+          :is="name"
           v-for="name in moduleList"
           :key="name"
-          :is="name"
-        ></component>
+        />
       </div>
-      <MyEquityList></MyEquityList>
+      <MyEquityList />
       <!-- <div class="main-module card-list-module" v-if="dataReportShow">
         <DataReport></DataReport>
       </div> -->
       <BusinessProfile
         v-if="businessProfileShow"
         class="main-module"
-      ></BusinessProfile>
+      />
     </el-main>
     <el-aside width="369px">
-      <AccountInfo></AccountInfo>
-      <ChatList></ChatList>
-      <MessageTips class="aside-module"></MessageTips>
-      <AsideOthers class="aside-module"></AsideOthers>
+      <AccountInfo ref="accountInfoRef" @showGiftDialog="showGiftDialog" />
+      <ChatList />
+      <MessageTips class="aside-module" />
+      <AsideOthers class="aside-module" />
     </el-aside>
     <ActivityDialog
-      @initNext="initNextDialog('checkUserShow')"
       ad="jy-pc-index-tap"
+      @initNext="initNextDialog('checkUserShow')"
     />
     <GuideIntroDialog />
     <CheckUserDialog v-if="dialog.checkUserShow" />
+    <GiftDialog
+      class="gift-tip-dialog"
+      :visible="showGift"
+      :close-click-modal="true"
+      title="温馨提示"
+      @close="showGift = false"
+    >
+      <p class="gift-tip-content">
+        {{ giftContent.giftTipText }}
+      </p>
+      <span slot="footer" class="dialog-footer">
+        <button
+          class="action-button confirm"
+          @click="onClickConfirm"
+        >
+          {{ giftContent.buttonText }}
+        </button>
+      </span>
+    </GiftDialog>
+    <!-- <GiftSubmitDialog :visible="showGiftSubmit" @close="showGiftSubmit = false" /> -->
   </el-container>
 </template>
 
 <script>
 import { mapGetters } from 'vuex'
 import { chunk } from 'lodash'
-import { Container, Aside, Main } from 'element-ui'
+import { Aside, Container, Main } from 'element-ui'
+import { tryCallHooks } from '@jianyu/easy-inject-qiankun'
 import MessageTips from './components/MessageTips.vue'
 import CommonUse from './components/CommonUse.vue'
-import CheckUserDialog from '@/components/dialog/CheckUserDialog'
 import SubscribeList from './components/SubscribeList.vue'
 // import MyCollections from './components/MyCollections.vue'
 // import ProjectFollow from './components/ProjectFollow.vue'
@@ -51,19 +71,22 @@ import SubscribeList from './components/SubscribeList.vue'
 // import DataReport from './components/DataReport.vue'
 import AsideOthers from './components/AsideOthers.vue'
 // import ClaimList from './components/ClaimList.vue'
-import ActivityDialog from '@/components/ad/activity-dialog.vue'
-import GuideIntroDialog from '@/components/ad/guide-intro-dialog.vue'
 import AccountInfo from './components/AccountInfo.vue'
 import ChatList from './components/ChatList.vue'
 import BusinessToDo from './components/BusinessToDo.vue'
 import NewsList from './components/NewsList.vue'
 import AnalysisReport from './components/AnalysisReport.vue'
 import MyEquityList from './components/MyEquityList.vue'
-const BusinessProfile = () => import('./components/BusinessProfile.vue')
+import GiftDialog from '@/components/dialog/Dialog.vue'
+// import GiftSubmitDialog from '@/components/dialog/GiftSubmitDialog.vue'
+import GuideIntroDialog from '@/components/ad/guide-intro-dialog.vue'
+import ActivityDialog from '@/components/ad/activity-dialog.vue'
+import CheckUserDialog from '@/components/dialog/CheckUserDialog'
 // const MyCustomer = () => import('./components/MyCustomer.vue')
 // const CustomerWatcher = () => import('./components/CustomerWatcher.vue')
 import { showFunctionGuide } from '@/api/modules/'
-import { tryCallHooks } from '@jianyu/easy-inject-qiankun'
+
+const BusinessProfile = () => import('./components/BusinessProfile.vue')
 export default {
   name: 'WorkspaceDashboard',
   components: {
@@ -72,6 +95,8 @@ export default {
     [Aside.name]: Aside,
     ActivityDialog,
     GuideIntroDialog,
+    GiftDialog,
+    // GiftSubmitDialog,
     MessageTips,
     CommonUse,
     BusinessProfile,
@@ -97,7 +122,14 @@ export default {
       dialog: {
         checkUserShow: false
       },
-      showSunshineGuide: false
+      showSunshineGuide: false,
+      showGift: false,
+      showGiftSubmit: false,
+      giftContent: {
+        giftTipText: '',
+        buttonText: '去购买',
+        type: 'buy'
+      }
     }
   },
   computed: {
@@ -148,6 +180,35 @@ export default {
     })
   },
   methods: {
+    showGiftDialog(data) {
+      if (data === 'gift') {
+        this.$GiftFriendsDialog({
+          props: {
+            visible: true,
+            name: '送给朋友',
+            el: this.$refs.workspaceDashboard
+          },
+          on: {
+            close: () => {
+              console.log(this.$refs.accountInfoRef, 'accountInfoRef')
+              this.$refs.accountInfoRef.getAccount()
+            }
+          }
+        })
+      }
+      else {
+        const freeText = '您当前不是超级订阅会员,无法赠送给好友。您可以先购买后再赠送给好友。'
+        const passText = '您当前剩余订阅周期不满1个月,无法赠送给好友。您可以先续费后再赠送给好友。'
+        this.giftContent.giftTipText = data === 'renew' ? passText : freeText
+        this.giftContent.buttonText = data === 'renew' ? '去续费' : '去购买'
+        this.giftContent.type = data
+        this.showGift = true
+      }
+    },
+    onClickConfirm() {
+      this.showGift = false
+      window.open(`/swordfish/page_big_pc/free/svip/buy?type=${this.giftContent.type}`)
+    },
     initNextDialog(key) {
       this.dialog[key] = true
       // 获取引导
@@ -172,7 +233,8 @@ export default {
             title: '阳光直采上线了!',
             content: '海量企业直发采购需求,供应商可直接对接采购部门'
           })
-        } catch (error) {
+        }
+        catch (error) {
           console.log(error)
         }
       }
@@ -180,6 +242,7 @@ export default {
   }
 }
 </script>
+
 <style lang="scss" scoped>
 .workspace-dashboard {
   padding: 24px;
@@ -194,6 +257,34 @@ export default {
     margin-top: 16px;
   }
 }
+.gift-tip-dialog {
+  ::v-deep {
+    .el-dialog {
+      width: 380px!important;
+    }
+    .el-dialog__header {
+      padding: 32px 32px 0;
+    }
+    .el-dialog__body {
+      padding: 20px 32px 32px;
+    }
+    .el-dialog__footer {
+      display: flex;
+      justify-content: center;
+      padding: 0 32px 32px;
+    }
+    .gift-tip-content {
+      text-align: center;
+    }
+    .dialog-footer {
+      .action-button.confirm {
+        width: 132px;
+        font-size: 16px;
+      }
+    }
+
+  }
+}
 .card-list-module {
   display: flex;
   justify-content: space-between;

+ 56 - 0
apps/mobile/src/api/modules/pay.js

@@ -349,3 +349,59 @@ export function setPhoneBind(data, type) {
     data
   })
 }
+
+// 赠送人超级订阅活动时间配置
+export function vipGiftActivityConfigAjax() {
+  return request({
+    url: `/subscribepay/vip/gift/configuration?t=${Date.now()}`,
+    method: 'POST'
+  })
+}
+
+// 赠送人超级订阅可赠资源查询
+export function getSubResourcesAjax() {
+  return request({
+    url: `/subscribepay/vip/gift/getSubDuration?t=${Date.now()}`,
+    method: 'POST'
+  })
+}
+
+// 根据手机号获取可赠信息
+export function getInfoByPhoneAjax(data) {
+  data = qs.stringify(data)
+  return request({
+    url: '/subscribepay/vip/gift/getInfoByPhone',
+    method: 'POST',
+    data
+  })
+}
+
+// 赠送超级订阅提交表单
+export function givingSuperVipAjax(data) {
+  data = qs.stringify(data)
+  return request({
+    url: '/subscribepay/vip/gift/transferSubDuration',
+    method: 'POST',
+    data
+  })
+}
+
+// 超级订阅赠送记录
+export function vipGiftRecordAjax(data) {
+  data = qs.stringify(data)
+  return request({
+    url: '/subscribepay/vip/gift/list',
+    method: 'POST',
+    data
+  })
+}
+
+// 超级订阅赠送记录详情
+export function vipGiftDetailAjax(data) {
+  data = qs.stringify(data)
+  return request({
+    url: '/subscribepay/vip/gift/informInfo',
+    method: 'POST',
+    data
+  })
+}

BIN
apps/mobile/src/assets/image/icon/circle-plus@2x.png


BIN
apps/mobile/src/assets/image/icon/delete@2x.png


BIN
apps/mobile/src/assets/image/icon/notify@3x.png


BIN
apps/mobile/src/assets/image/vip-subscribe/poster-banner.png


BIN
apps/mobile/src/assets/image/vip-subscribe/poster-save.png


+ 14 - 0
apps/mobile/src/assets/style/pic-icon.scss

@@ -250,6 +250,20 @@
 .icon-ai-by-search {
   background-image: url(@/assets/image/ai-search/bySearch.png);
 }
+
 .icon-ai-by-search-index {
   background-image: url(@/assets/image/ai-search/bySearchIndex.png);
 }
+
+.icon-circle-plus {
+  background-image: url(@/assets/image/icon/circle-plus@2x.png);
+  background-size: contain;
+}
+
+.icon-gray-delete {
+  background-image: url(@/assets/image/icon/delete@2x.png);
+}
+
+.icon-notify-active {
+  background-image: url(@/assets/image/icon/notify@3x.png);
+}

+ 131 - 6
apps/mobile/src/components/mine/MineHeader.vue

@@ -192,6 +192,14 @@
         </div>
       </div>
     </div>
+    <div class="gift-guide" v-if="isShowGiftEntry">
+      <div class="gift-guide-container">
+        <span class="gift-guide-text"
+          >支持送好友超级订阅,快快送给好友吧!</span
+        >
+        <span class="gift-guide-btn" @click="sendToFriend">送给朋友</span>
+      </div>
+    </div>
     <!--    订单tab-->
     <order-tabs />
   </div>
@@ -200,7 +208,12 @@
 <script>
 import OrderTabs from './OrderTabs.vue'
 import { Sticky } from 'vant'
-import { getAccountInfo, getUserAccountInfo } from '@/api/modules'
+import {
+  getAccountInfo,
+  getUserAccountInfo,
+  vipGiftActivityConfigAjax,
+  getSubResourcesAjax
+} from '@/api/modules'
 import { AppIcon } from '@/ui'
 import { openAppOrWxPage, dateFormatter } from '@/utils'
 import { LINKS } from '@/data'
@@ -222,7 +235,10 @@ export default {
       accountInfo: {
         hasVip: false,
         hasBigmember: false
-      }
+      },
+      // 是否是超级订阅赠送活动期间
+      vipGiftPeriod: false,
+      giftMonth: 0
     }
   },
   computed: {
@@ -292,8 +308,20 @@ export default {
     ...mapGetters('user', [
       'userCurrentIdentity',
       'userIdentityList',
-      'isLogin'
-    ])
+      'isLogin',
+      'userIdentityType',
+      'isSuper',
+      'isFree'
+    ]),
+    isShowGiftEntry() {
+      const personal = this.userIdentityType === 0
+      const isSuper = this.isSuper
+      const isFree = this.isFree && this.activeAccountType.name !== '省份订阅包'
+      const vipGiftPeriod = this.vipGiftPeriod
+      const activeTypeVip =
+        this.activeAccountType.name === '超级订阅' && isSuper
+      return vipGiftPeriod && personal && (activeTypeVip || isFree)
+    }
   },
   watch: {
     userCurrentIdentity: {
@@ -309,6 +337,8 @@ export default {
   },
   created() {
     this.init()
+    this.getVipActivityConfig()
+    this.getGiftSource()
   },
   methods: {
     ...mapActions('user', ['getUserIdentityList']),
@@ -475,6 +505,7 @@ export default {
         window.location.href = '/jyapp/free/customer'
       }
     },
+<<<<<<< HEAD
     bindPhoneJumpSetting() {
       return {
         props: {
@@ -573,6 +604,61 @@ export default {
         next: () => {
           this.renewConsult()
         }
+=======
+    // 获取超级订阅活动配置
+    async getVipActivityConfig() {
+      const { data } = await vipGiftActivityConfigAjax()
+      if (data) {
+        const { startTime, endTime } = data
+        // 校验时间有效性
+        const isValidPeriod =
+          startTime && endTime && parseInt(startTime) < parseInt(endTime)
+        const nowTime = Math.floor(Date.now() / 1000)
+        this.vipGiftPeriod =
+          isValidPeriod &&
+          nowTime >= parseInt(startTime) &&
+          nowTime <= parseInt(endTime)
+      }
+    },
+    // 送好友
+    sendToFriend() {
+      const nowTime = Date.now()
+      const expireTime = this.activeAccountType.vipEntTime
+      if (!this.accountInfo.hasVip) {
+        // 非超级订阅
+        return this.$dialog
+          .alert({
+            title: '温馨提示',
+            message:
+              '您当前不是超级订阅会员,无法赠送给好友,您可以先购买后再赠送给好友。',
+            confirmButtonText: '去购买',
+            className: 'j-confirm-dialog'
+          })
+          .then(() => {
+            this.$router.push('/order/create/svip?type=buy')
+          })
+      } else if (this.accountInfo.hasVip && this.giftMonth < 1) {
+        // 是超级订阅 & 有到期时间 & 到期时间距现在不足1个月
+        return this.$dialog
+          .alert({
+            title: '温馨提示',
+            message:
+              '您当前剩余订阅周期不满1个月,无法赠送给好友。您可以先续费后再赠送给好友。',
+            confirmButtonText: '去续费',
+            className: 'j-confirm-dialog'
+          })
+          .then(() => {
+            this.$router.push('/order/create/svip?type=renew')
+          })
+      } else {
+        this.$router.push('/giving/friend')
+      }
+    },
+    async getGiftSource() {
+      const { data } = await getSubResourcesAjax()
+      if (data) {
+        this.giftMonth = data?.gifted
+>>>>>>> main
       }
     }
   }
@@ -897,15 +983,54 @@ export default {
 .bold {
   font-weight: bold;
 }
+.gift-guide {
+  position: relative;
+  height: 58px;
+  //margin: -4px -8px -8px;
+  background: linear-gradient(272.3deg, #ffdbb9 17%, #fff2dd 100%);
+  //border-radius: 0 16px 0 0;
+  //overflow: hidden;
+  margin-bottom: -13px;
+  &-container {
+    position: absolute;
+    left: 0;
+    right: 0;
+    height: 100%;
+    margin: -4px -8px -8px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 6px 20px 20px;
+    background: rgba(255, 255, 255, 0.8);
+    backdrop-filter: blur(10px);
+    border-radius: 0 16px 0 0;
+  }
+  &-text {
+    color: #171826;
+    font-size: 12px;
+    line-height: 18px;
+  }
+  &-btn {
+    padding: 1px 12px;
+    border: 1px solid rgba(255, 255, 255, 0.5);
+    background: rgba(250, 231, 202, 1);
+    box-shadow: 0px 4px 12px 0px rgba(186, 105, 31, 0.32);
+    border-radius: 16px;
+    font-size: 14px;
+    line-height: 20px;
+    color: #171826;
+    box-sizing: border-box;
+  }
+}
 ::v-deep {
   .order-tabs {
     position: absolute;
     width: 100%;
     left: 0;
-    bottom: 0px;
+    bottom: 0;
     //height: 81px;
     border-top-right-radius: 16px;
-    box-shadow: 0 -8px 12px 0 rgba(92, 31, 5, 0.15);
+    //box-shadow: 0 -8px 12px 0 rgba(92, 31, 5, 0.15);
     background: linear-gradient(180deg, #fff 0%, #f5f6f7 100%);
   }
 }

+ 5 - 0
apps/mobile/src/data/links.js

@@ -30,6 +30,11 @@ export const LINKS = {
     h5: '/jyapp/account/phone/bind',
     wx: '/front/account/phone/bind'
   },
+  绑定手机号合并: {
+    app: '/jyapp/account/phone/bind?mode=mergeBind',
+    h5: '/jyapp/account/phone/bind?mode=mergeBind',
+    wx: '/front/account/phone/bind?mode=mergeBind'
+  },
   超级订阅设置页面: {
     app: '/jyapp/vipsubscribe/toSubVipSetPage?vSwitch=v',
     h5: '/jyapp/vipsubscribe/toSubVipSetPage?vSwitch=v',

+ 38 - 0
apps/mobile/src/router/modules/giving.js

@@ -0,0 +1,38 @@
+export default [
+  {
+    path: '/friend',
+    name: 'giving-friend',
+    component: () => import('@/views/giving/friend.vue'),
+    meta: {
+      title: '送给朋友',
+      header: true
+    }
+  },
+  {
+    path: '/record',
+    name: 'giving-record',
+    component: () => import('@/views/giving/record.vue'),
+    meta: {
+      title: '超级订阅赠送记录',
+      header: true
+    }
+  },
+  {
+    path: '/notify/:id',
+    name: 'giving-notify',
+    component: () => import('@/views/giving/notify.vue'),
+    meta: {
+      title: '告知好友',
+      header: true
+    }
+  },
+  {
+    path: '/share',
+    name: 'giving-share',
+    component: () => import('@/views/giving/share.vue'),
+    meta: {
+      title: '分享有礼',
+      header: true
+    }
+  }
+]

+ 3 - 1
apps/mobile/src/router/modules/order.js

@@ -32,7 +32,9 @@ export default [
           adsense: () =>
             import('@/views/create-order/components/vipsubscribe/Adsense'),
           desc: () =>
-            import('@/views/order/components/vipsubscribe/Introduction')
+            import('@/views/order/components/vipsubscribe/Introduction'),
+          footerNotice: () =>
+            import('@/views/order/components/vipsubscribe/FooterNoticeBar')
         })
       },
       {

+ 9 - 0
apps/mobile/src/router/modules/static.js

@@ -88,5 +88,14 @@ export default [
       header: false,
       title: '超级订阅中转页'
     }
+  },
+  {
+    path: '/svip/gift/notice',
+    name: 'subscribe_transfer',
+    component: () => import('@/views/static/SVipGiftNotice.vue'),
+    meta: {
+      header: true,
+      title: '送好友超级订阅”产品须知'
+    }
   }
 ]

+ 3 - 1
apps/mobile/src/store/index.js

@@ -15,7 +15,7 @@ if (import.meta.env.DEV) {
   Vue.use(Vuex)
 }
 
-export default new Vuex.Store({
+const store = new Vuex.Store({
   strict: true,
   state: {
     direction: 'reload', // 页面切换方向 (reload / back / forward)
@@ -54,3 +54,5 @@ export default new Vuex.Store({
     group
   }
 })
+export default store
+export const useStore = () => store

+ 9 - 1
apps/mobile/src/store/modules/createOrder.js

@@ -171,7 +171,8 @@ export default {
       offers: false,
       amount: false,
       submit: false
-    }
+    },
+    isShowGiftNotice: false
   }),
   mutations: {
     /**
@@ -366,6 +367,13 @@ export default {
      */
     resetLayout(state) {
       state.layout = Object.assign({}, LayoutConfig)
+    },
+    /**
+     * 是否展示超级订阅赠送信息
+     * @param state
+     */
+    setShowGiftNotice(state, data) {
+      state.isShowGiftNotice = data
     }
   },
   actions: {

+ 24 - 0
apps/mobile/src/utils/utils.js

@@ -914,3 +914,27 @@ export function bSort (arr, value) {
   }
   return arr
 }
+
+// 月份数转换为“年+月”的组合格式
+export function formatMonthsToYearsMonths(months, lang = 'zh') {
+  const units = {
+    zh: { year: '年', month: '个月' },
+    en: { year: ' year', month: ' month', plural: 's' },
+  };
+  const { year, month, plural } = units[lang] || units.zh;
+
+  const years = Math.floor(months / 12);
+  const remainingMonths = months % 12;
+
+  let result = [];
+  if (years > 0) {
+    const yearUnit = lang === 'en' && years > 1 ? year + plural : year;
+    result.push(`${years}${yearUnit}`);
+  }
+  if (remainingMonths > 0) {
+    const monthUnit = lang === 'en' && remainingMonths > 1 ? month + plural : month;
+    result.push(`${remainingMonths}${monthUnit}`);
+  }
+
+  return result.join(' ') || `0${month}`;
+}

+ 474 - 0
apps/mobile/src/views/giving/friend.vue

@@ -0,0 +1,474 @@
+<script setup>
+import { ref, computed, onMounted, getCurrentInstance } from 'vue'
+import { CellGroup, Cell, Field, Checkbox, Dialog } from 'vant'
+import {
+  getSubResourcesAjax,
+  getInfoByPhoneAjax,
+  givingSuperVipAjax
+} from '@/api/modules'
+
+const that = getCurrentInstance().proxy
+
+const accountInfo = ref({
+  province: '',
+  duration: 0
+})
+const checked = ref(false)
+
+const friendList = ref([
+  {
+    phone: '',
+    duration: '',
+    errors: {
+      phone: '',
+      duration: ''
+    },
+    notRegister: false
+  }
+])
+
+// 显示已赠、剩余信息
+const showGifted = computed(() => {
+  return friendList.value.some((item) => item.phone && item.duration)
+})
+
+// 表单整体校验
+const isFormValid = computed(() => {
+  return (
+    friendList.value.every(
+      (item) =>
+        /^1[3-9]\d{9}$/.test(item.phone) &&
+        !item.errors.phone &&
+        item.duration > 0
+    ) &&
+    checked.value &&
+    !showExceedWarn.value
+  )
+})
+
+// 计算已赠人数
+const giftedPeople = computed(() => {
+  return friendList.value.filter((item, index) => item.phone && item.duration)
+    .length
+})
+
+// 计算已赠时长
+const giftedDuration = computed(() => {
+  return friendList.value.reduce(
+    (total, item) => total + parseInt(item.duration || 0),
+    0
+  )
+})
+
+// 计算剩余时长
+const remainingDuration = computed(() => {
+  const calcDuration = accountInfo.value.duration - giftedDuration.value
+  return calcDuration > 0 ? calcDuration : 0
+})
+
+// 超出时长提示
+const showExceedWarn = computed(() => {
+  return giftedDuration.value > accountInfo.value.duration
+})
+
+// 添加好友
+const addFriend = () => {
+  const canAdd = friendList.value.every(
+    (item) => /^1[3-9]\d{9}$/.test(item.phone) && item.duration > 0
+  )
+  if (!canAdd) {
+    return that.$toast('请先完成当前未完善的手机号和时长信息')
+  }
+  friendList.value.push({
+    phone: '',
+    duration: '',
+    errors: {
+      phone: '',
+      duration: ''
+    },
+    notRegister: false
+  })
+}
+
+// 删除好友
+const removeFriend = (index) => {
+  if (friendList.value.length > 1) {
+    friendList.value.splice(index, 1)
+  }
+}
+
+// 手机号校验
+const validatePhone = (index) => {
+  const phone = friendList.value[index].phone
+  const isValid = /^1[3-9]\d{9}$/.test(phone)
+  friendList.value[index].errors.phone = isValid ? '' : '手机号格式不正确'
+  if (isValid) {
+    getInfoByPhoneAjax({
+      phone
+    }).then((res) => {
+      const { error_code: code, data } = res
+      if (code === 0 && data) {
+        if (data.status === -1) {
+          friendList.value[index].notRegister = true
+          friendList.value[index].errors.phone = ''
+        } else if (data.status === -2) {
+          friendList.value[index].notRegister = false
+          friendList.value[index].errors.phone =
+            '手机号已是超级订阅会员,且购买省份与当前省份不一致,不可赠送。'
+        } else if (data.status === -3) {
+          friendList.value[index].notRegister = false
+          friendList.value[index].errors.phone =
+            '不能将超级订阅赠送给自己,请更换手机号。'
+        } else if (data.status === -4) {
+          friendList.value[index].notRegister = false
+          friendList.value[index].errors.phone =
+            '当前用户是省份订阅包用户,暂时无法赠送。'
+        } else {
+          friendList.value[index].notRegister = false
+          friendList.value[index].errors.phone = ''
+        }
+      }
+    })
+  }
+}
+
+// 时长校验
+const validateDuration = (index) => {
+  const duration = friendList.value[index].duration
+  const isValid = duration > 0
+  friendList.value[index].errors.duration = isValid ? '' : '时长必须大于0'
+}
+
+// 须知跳转
+const toLink = () => {
+  const storageData = {
+    list: friendList.value,
+    checkbox: checked.value
+  }
+  sessionStorage.setItem('friendFormStorage', JSON.stringify(storageData))
+  that.$router.push('/static/svip/gift/notice')
+}
+
+// 取消
+const onCancel = () => {
+  history.back()
+}
+
+// 查询账号可赠资源
+const getSubResources = async () => {
+  const { error_code: code, data, error_msg: msg } = await getSubResourcesAjax()
+  if (code === 0 && data) {
+    accountInfo.value.province =
+      data?.areacount === -1 ? '全国' : `${data?.areacount}个省`
+    accountInfo.value.duration = data?.gifted
+  } else {
+    console.error(msg)
+  }
+}
+
+onMounted(async () => {
+  await getSubResources()
+  // 恢复数据
+  const storageData = sessionStorage.getItem('friendFormStorage')
+  if (storageData) {
+    const { list, checkbox } = JSON.parse(storageData)
+    friendList.value = list
+    checked.value = checkbox
+    sessionStorage.removeItem('friendFormStorage')
+  }
+})
+
+const givingSubscribeResource = async () => {
+  const result = friendList.value.reduce((acc, cur) => {
+    if (cur.phone && cur.duration) {
+      const duration = parseInt(cur.duration) || 0
+      acc[cur.phone] = (acc[cur.phone] || 0) + duration
+    }
+    return acc
+  }, {})
+  const params = {
+    phones: JSON.stringify(result)
+  }
+  const { error_code: code, error_msg: msg } = await givingSuperVipAjax(params)
+  if (code === 0) {
+    that.$router.push('/giving/record')
+  } else {
+    that.$toast(msg)
+  }
+}
+
+// 提交
+const onConfirm = () => {
+  if (!isFormValid.value) {
+    return
+  }
+  if (!checked.value) {
+    return
+  }
+  if (showExceedWarn.value) {
+    return
+  }
+  // 提交逻辑
+  Dialog.confirm({
+    title: '赠送确认',
+    message: '确定将剩余时长赠送给好友?',
+    className: 'j-confirm-dialog'
+  })
+    .then(() => {
+      // on confirm
+      givingSubscribeResource()
+    })
+    .catch(() => {
+      // on cancel
+    })
+}
+</script>
+
+<template>
+  <div class="j-container gift-friend">
+    <div class="j-main">
+      <CellGroup class="cell-group" inset :border="false">
+        <Cell
+          :border="false"
+          title-class="cell-title"
+          value-class="cell-value"
+          title="订阅区域"
+          :value="accountInfo.province"
+        ></Cell>
+        <Cell
+          :border="false"
+          title-class="cell-title"
+          value-class="cell-value"
+          title="可赠送时长(取整)"
+          :value="accountInfo.duration + '个月'"
+        ></Cell>
+      </CellGroup>
+      <div class="add-container">
+        <div class="flex flex-(items-center justify-between) add-header">
+          <span class="add-header-label">朋友列表</span>
+          <div
+            class="flex flex-items-center add-header-action"
+            @click.stop="addFriend"
+          >
+            <i class="j-icon j-base-icon icon-circle-plus"></i>
+            <span>添加</span>
+          </div>
+        </div>
+        <div class="add-tips">
+          说明:如手机号尚未注册剑鱼,赠送其超级订阅后,平台会自动帮其按照对应手机号注册。
+        </div>
+        <div class="add-list">
+          <CellGroup
+            v-for="(friend, index) in friendList"
+            :key="index"
+            class="cell-group"
+            inset
+            :border="false"
+          >
+            <div class="flex">
+              <div class="flex-1 field-container">
+                <Field
+                  v-model="friend.phone"
+                  label-class="field-label"
+                  :class="{ 'custom-field': friendList.length > 1 }"
+                  label-width="84"
+                  :border="false"
+                  type="tel"
+                  maxlength="11"
+                  label="手机号"
+                  placeholder="请输入手机号"
+                  :error-message="friend.errors.phone"
+                  @blur="validatePhone(index)"
+                />
+                <p
+                  v-if="friend.notRegister"
+                  class="unregistered-tips"
+                  :class="{ 'pad-tips': friendList.length > 1 }"
+                >
+                  手机号尚未注册剑鱼,赠送其超级订阅后,平台会自动帮其按照对应手机号注册。
+                </p>
+              </div>
+              <i
+                v-if="friendList.length > 1"
+                class="j-icon j-base-icon wh20 icon-gray-delete"
+                @click="removeFriend(index)"
+              />
+            </div>
+            <div class="flex flex-items-center">
+              <Field
+                v-model="friend.duration"
+                label-class="field-label"
+                label-width="84"
+                type="digit"
+                min="1"
+                maxlength="2"
+                label="时长(月)"
+                placeholder="请输入整数"
+                :error-message="friend.errors.duration"
+                @blur="validateDuration(index)"
+              />
+              <span class="add-duration-unit">个月</span>
+            </div>
+          </CellGroup>
+        </div>
+        <div v-show="showExceedWarn" class="add-exceed-warn">
+          人员时长总和大于可赠送时长,请调整后提交。
+        </div>
+      </div>
+    </div>
+    <div class="j-footer">
+      <div>
+        <Checkbox v-model="checked">
+          <div class="notice">
+            <span>已阅读并同意</span>
+            <span class="href" @click.stop="toLink">
+              《“送好友超级订阅”产品须知》
+            </span>
+          </div>
+          <template #icon>
+            <div
+              class="j-icon checkbox"
+              :class="checked ? 'checked' : ''"
+            ></div>
+          </template>
+        </Checkbox>
+        <p class="gifted-info" v-show="showGifted">
+          共赠送<em class="highlight-text">{{ giftedPeople }} </em>
+          人,赠送时长<em class="highlight-text">{{ giftedDuration }}</em>
+          个月,剩余<em class="highlight-text"> {{ remainingDuration }} </em>
+          个月可赠送
+        </p>
+      </div>
+      <div class="j-button-group height40">
+        <button class="j-button-cancel" @click="onCancel">取消</button>
+        <button
+          class="j-button-confirm"
+          :disabled="!isFormValid"
+          @click="onConfirm"
+        >
+          提交
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.gift-friend {
+  .add-header {
+    padding: 4px 20px 8px;
+    &-label {
+      font-size: 16px;
+      line-height: 24px;
+      color: #171826;
+    }
+    &-action {
+      color: $main;
+      font-size: 16px;
+      .icon-circle-plus {
+        margin-right: 8px;
+      }
+    }
+  }
+  .add-tips {
+    padding: 0 20px 8px 18px;
+    color: #9b9ca3;
+    font-size: 14px;
+    line-height: 20px;
+  }
+  .field-container {
+    position: relative;
+    &::after {
+      position: absolute;
+      box-sizing: border-box;
+      content: ' ';
+      pointer-events: none;
+      right: 16px;
+      bottom: 0;
+      left: 16px;
+      border-bottom: 1px solid #ebedf0;
+      -webkit-transform: scaleY(0.5);
+      transform: scaleY(0.5);
+    }
+  }
+  .add-exceed-warn {
+    padding: 10px 8px;
+    margin: 8px;
+    color: #fb483d;
+    font-size: 13px;
+    line-height: 20px;
+    background: rgba(254, 233, 232, 1);
+    text-align: center;
+    border-radius: 8px;
+  }
+  .icon-gray-delete {
+    width: 20px;
+    height: 20px;
+    margin: 10px 10px 0 0;
+  }
+  .add-duration-unit {
+    padding-right: 48px;
+    flex-shrink: 0;
+  }
+  .gifted-info {
+    padding: 10px 20px 0;
+    background: #fff;
+    font-size: 14px;
+    line-height: 20px;
+    color: #5f5e64;
+  }
+  .unregistered-tips {
+    padding: 0 48px 8px 104px;
+    font-size: 12px;
+    line-height: 18px;
+    color: $main;
+    &.pad-tips {
+      padding: 0 8px 8px 104px;
+    }
+  }
+  .j-button-cancel {
+    color: $main;
+    background: #fff;
+    border: 1px solid $main;
+  }
+  .j-button-cancel,
+  .j-button-confirm {
+    font-size: 16px;
+  }
+  ::v-deep {
+    .custom-field {
+      padding-right: 8px;
+    }
+    .cell-group {
+      margin: 8px;
+    }
+    .field-label {
+      font-size: 15px;
+    }
+    .cell-title {
+      font-size: 14px;
+      line-height: 20px;
+      color: #5f5e64;
+    }
+    .cell-value {
+      font-size: 16px;
+      line-height: 24px;
+      color: #171826;
+    }
+    .van-checkbox {
+      padding: 7px 16px;
+      background: #fff;
+      font-size: 12px;
+      line-height: 18px;
+      color: #5f5e64;
+      .checkbox {
+        width: 18px;
+        height: 18px;
+      }
+      .href {
+        color: $main;
+      }
+    }
+  }
+}
+</style>

+ 364 - 0
apps/mobile/src/views/giving/notify.vue

@@ -0,0 +1,364 @@
+<script setup>
+import { ref, nextTick, onMounted, getCurrentInstance } from 'vue'
+import html2canvas from 'html2canvas'
+import { savePic } from '@/utils/callFn/appFn'
+import { vipGiftDetailAjax } from '@/api/modules'
+
+const that = getCurrentInstance().proxy
+
+const routeId = that.$route.params?.id
+
+const giverInfo = ref({
+  phone: '',
+  headImg: '',
+  duration: '**',
+  codeImg: ''
+})
+const saveText = ref('')
+const posterUrl = ref('')
+const showPoster = ref(false)
+const skeletonLoading = ref(true)
+
+// 根据id查询赠送信息
+async function getPosterInfo() {
+  const {
+    error_code: code,
+    data,
+    error_msg: msg
+  } = await vipGiftDetailAjax({
+    giftId: routeId
+  })
+  if (code === 0) {
+    const {
+      duration,
+      giftUserPhone,
+      recipientUserPhone,
+      qRCodeLink: codeImg
+    } = data
+    const headImg = new URL('@/assets/image/public/auto.png', import.meta.url)
+      .href
+    giverInfo.value = {
+      duration,
+      giftUserPhone,
+      recipientUserPhone,
+      codeImg,
+      headImg
+    }
+    saveText.value = '长按图片可保存到相册'
+    if (that.$env.platform === 'app') {
+      skeletonLoading.value = false
+    }
+  } else {
+    that.$toast(msg)
+  }
+}
+
+// 生成海报
+const generatePoster = async () => {
+  const Loading = that.$toast.loading({
+    message: '生成中...',
+    forbidClick: true,
+    duration: 0
+  })
+
+  saveText.value = '微信扫一扫二维码领取会员'
+  try {
+    await nextTick() // 等待DOM更新
+    const poster = document.querySelector('.poster')
+    const canvas = await html2canvas(poster, {
+      useCORS: true,
+      scale: 2,
+      logging: false,
+      backgroundColor: '#FFFFFF'
+    })
+    const imgUrl = canvas.toDataURL('image/png')
+    if (that.$env.platform === 'app') {
+      const formatUrl = imgUrl.replace('data:image/png;base64,', '')
+      try {
+        savePic(
+          formatUrl,
+          '剑鱼标讯需要您的存储权限,用于帮助您下载、保存图片到本地。'
+        )
+        Loading.clear()
+        that.$toast('图片已保存至相册', 1500)
+      } catch (error) {
+        Loading.clear()
+        that.$toast('保存失败,请稍后重试', 1500)
+      }
+    }
+  } catch (e) {
+    Loading.clear()
+    that.$toast('保存失败,请稍后重试', 1500)
+  } finally {
+    saveText.value = '长按图片可保存到相册'
+  }
+}
+
+onMounted(async () => {
+  await getPosterInfo()
+  if (that.$env.platform !== 'app') {
+    initGeneratePoster()
+  }
+})
+
+let pressTimer = null
+const onStartPress = (e) => {
+  e.preventDefault()
+  pressTimer = setTimeout(generatePoster, 800)
+}
+
+const onTouchend = () => {
+  clearTimeout(pressTimer)
+}
+
+// 页面初始化生成海报(非app客户端场景)
+const initGeneratePoster = async () => {
+  skeletonLoading.value = false
+  const Loading = that.$toast.loading({
+    forbidClick: true,
+    duration: 0
+  })
+  saveText.value = '微信扫一扫二维码领取会员'
+  try {
+    await nextTick()
+    const poster = document.querySelector('.poster')
+    const posterImg = document.querySelector('.poster-img-box')
+    const width = poster.offsetWidth
+    const height = poster.offsetHeight
+    const canvas = await html2canvas(poster, {
+      useCORS: true,
+      scale: 3,
+      logging: false,
+      backgroundColor: '#FFFFFF',
+      height: height - 1
+    })
+    const imgUrl = canvas.toDataURL('image/png')
+    posterUrl.value = imgUrl
+    showPoster.value = true
+    posterImg.style.width = `${width}px`
+    posterImg.style.height = `${height}px`
+    Loading.clear()
+  } catch (error) {
+    console.error(error)
+    Loading.clear()
+  } finally {
+    saveText.value = '长按图片可保存到相册'
+  }
+}
+</script>
+
+<template>
+  <div class="j-container gift-notify">
+    <div
+      class="poster"
+      @touchstart.prevent="onStartPress"
+      @touchend.capture="onTouchend"
+      v-show="!showPoster"
+    >
+      <div class="poster-banner"></div>
+      <div class="poster-info">
+        <div
+          class="flex flex-(justify-between) giver-container"
+          v-if="!skeletonLoading"
+        >
+          <div class="flex flex-(items-center justify-center) giver-picture">
+            <img :src="giverInfo.headImg" alt="picture" />
+          </div>
+          <div class="giver-main">
+            <div class="giver-phone">{{ giverInfo.giftUserPhone }}</div>
+            <div class="giver-desc">
+              送给{{ giverInfo.recipientUserPhone }}超级订阅会员,{{
+                giverInfo.duration
+              }}个月内可免费获取全行业招采信息
+            </div>
+          </div>
+          <div class="giver-code">
+            <img :src="giverInfo.codeImg" alt="code" />
+          </div>
+        </div>
+        <div
+          class="flex flex-justify-between skeleton"
+          v-if="skeletonLoading && !showPoster"
+        >
+          <div class="skeleton-avatar"></div>
+          <div class="skeleton-content">
+            <div class="skeleton-title"></div>
+            <div class="skeleton-desc"></div>
+            <div class="skeleton-desc"></div>
+            <div class="skeleton-title"></div>
+          </div>
+          <div class="skeleton-code"></div>
+        </div>
+        <div class="save-container">
+          <!--  ,图片不能使用背景图片(生成的图片中出现线条,原因未知),需使用img 标签  -->
+          <img
+            class="save-bg"
+            src="@/assets/image/vip-subscribe/poster-save.png"
+          />
+          <span class="save-text">{{ saveText }}</span>
+        </div>
+      </div>
+    </div>
+    <div class="poster-img-box" v-show="showPoster">
+      <img class="poster-img" :src="posterUrl" alt="长按保存图片" />
+      <div class="save-container">
+        <img
+          class="save-bg"
+          src="@/assets/image/vip-subscribe/poster-save.png"
+        />
+        <span class="save-text">{{ saveText }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+img {
+  pointer-events: auto !important;
+}
+.gift-notify {
+  padding: 28px 24px;
+}
+.poster {
+  position: relative;
+  touch-action: none;
+  user-select: none;
+  box-shadow: 0px 2px 16px 0px rgba(101, 24, 0, 0.2);
+  border-radius: 16px;
+  overflow: hidden;
+}
+.poster-banner {
+  width: 100%;
+  height: 394px;
+  background: url(@/assets/image/vip-subscribe/poster-banner.png) no-repeat
+    center;
+  background-size: cover;
+}
+.poster-info {
+  background: linear-gradient(180deg, #ffffff 0%, #fff7e8 100%);
+}
+.giver-container {
+  padding: 16px;
+  .giver-picture {
+    flex-shrink: 0;
+    width: 48px;
+    height: 48px;
+    margin-right: 8px;
+    border-radius: 50%;
+    overflow: hidden;
+    img {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+  }
+  .giver-phone {
+    font-size: 14px;
+    line-height: 20px;
+    color: #1d1d1d;
+  }
+  .giver-desc {
+    margin-top: 2px;
+    font-size: 13px;
+    line-height: 20px;
+    color: #5f5e64;
+    text-align: justify;
+  }
+  .giver-code {
+    flex-shrink: 0;
+    width: 60px;
+    height: 60px;
+    margin-left: 8px;
+    border: 1px solid rgba(0, 0, 0, 0.1);
+    border-radius: 4px;
+    background: #fff;
+    overflow: hidden;
+  }
+}
+.save-container {
+  position: relative;
+  width: 250px;
+  height: 24px;
+  margin: 0 auto;
+  text-align: center;
+  line-height: 24px;
+  color: #fff;
+  font-size: 12px;
+  border: 0;
+  outline: 0;
+  .save-img {
+    width: 100%;
+    height: 100%;
+  }
+  .save-text {
+    width: 100%;
+    height: 24px;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+  }
+}
+.poster-img-box {
+  position: relative;
+  touch-action: none;
+  user-select: none;
+  box-shadow: 0px 2px 16px 0px rgba(101, 24, 0, 0.2);
+  border-radius: 16px;
+  overflow: hidden;
+  .poster-img {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    width: 100%;
+    height: 100%;
+    z-index: 10;
+  }
+  .save-container {
+    position: absolute;
+    left: 50%;
+    bottom: 0;
+    transform: translateX(-50%);
+    z-index: 11;
+  }
+}
+.skeleton {
+  padding: 16px 16px 18px;
+  &-avatar {
+    width: 48px;
+    height: 48px;
+    background: #e5e5e5;
+    border-radius: 50%;
+    border: 1px solid rgba(0, 0, 0, 0.05);
+  }
+  &-content {
+    padding: 0 10px 0 8px;
+    flex: 1;
+  }
+  &-title {
+    width: 88px;
+    height: 16px;
+    background: #e5e5e5;
+    border-radius: 4px;
+    &:nth-child(1) {
+      margin-bottom: 8px;
+    }
+  }
+  &-desc {
+    width: 100%;
+    height: 16px;
+    background: #e5e5e5;
+    border-radius: 4px;
+    margin-bottom: 6px;
+  }
+  &-code {
+    width: 60px;
+    height: 60px;
+    background: #e5e5e5;
+    border-radius: 4px;
+    border: 1px solid rgba(0, 0, 0, 0.05);
+  }
+}
+</style>

+ 332 - 0
apps/mobile/src/views/giving/record.vue

@@ -0,0 +1,332 @@
+<script setup>
+import { ref, computed, onMounted, getCurrentInstance, nextTick } from 'vue'
+import { Tabs, Tab } from 'vant'
+import { useStore } from '@/store'
+import AppEmpty from '@/ui/empty/index.vue'
+import {
+  getUserAccountInfo,
+  vipGiftRecordAjax,
+  getSubResourcesAjax
+} from '@/api/modules'
+import { dateFormatter, formatMonthsToYearsMonths } from '@/utils/utils'
+
+const { getters } = useStore()
+const that = getCurrentInstance().proxy
+
+const queryTab = that.$route.query?.activeTab
+
+// 是否是免费用户
+const isFree = computed(() => {
+  return getters['user/isFree']
+})
+// 是否超级订阅用户
+const isSuper = computed(() => {
+  return getters['user/isSuper']
+})
+// 用户超级订阅账号信息
+const superInfo = ref({})
+// 到期时间
+const expireTime = computed(() => {
+  return superInfo.value.endTime
+})
+// tab索引
+const activeName = ref(1)
+// 赠送列表
+const giftList = ref([])
+// 接收列表
+const receiveList = ref([])
+// 可赠月份
+const giftMonth = ref(0)
+// 赠送为空提示
+const giftEmpty = computed(() => {
+  if (isSuper.value) {
+    if (giftMonth.value < 1) {
+      return {
+        text: '您当前超级订阅即将到期,请续费后赠送好友。',
+        btn: '去续费',
+        link: '/order/create/svip?type=renew'
+      }
+    } else {
+      return {
+        text: '您当前是超级订阅用户,立即去赠送好友。',
+        btn: '去赠送',
+        link: '/giving/friend'
+      }
+    }
+  } else {
+    return {
+      text: '您当前不是超级订阅用户,需购买后赠送好友。',
+      btn: '去购买',
+      link: '/order/create/svip?type=buy'
+    }
+  }
+})
+// 通知好友
+const onNotify = (id) => {
+  that.$router.push(`/giving/notify/${id}`)
+}
+// 跳转页面
+const goTargetPage = (link) => {
+  that.$router.push(link)
+}
+
+// 获取赠送记录
+const getGiftRecordList = async (type = '1') => {
+  const { error_code: code, data } = await vipGiftRecordAjax({
+    giftType: type
+  })
+  if (code === 0 && data && data.list) {
+    if (type === '1') {
+      const convertedList = data.list
+        .map((subArr) => {
+          if (subArr.length === 0) return null
+          const { createTime } = subArr[0]
+          const arr = subArr.map(({ createTime, ...rest }) => rest)
+          return { arr, createTime }
+        })
+        .filter((item) => item !== null)
+      giftList.value = convertedList.map((item) => {
+        item.createTime = dateFormatter(item.createTime, 'yyyy.MM.dd HH:mm:ss')
+        item.arr = item.arr.map((v) => {
+          return {
+            id: v.id,
+            phone: v.recipientUserPhone,
+            duration: v.duration
+          }
+        })
+        return item
+      })
+    } else {
+      receiveList.value = data.list.flat().map((item) => {
+        return {
+          id: item.id,
+          phone: item.giftUserPhone,
+          duration: formatMonthsToYearsMonths(item.duration),
+          createTime: dateFormatter(item.createTime, 'yyyy.MM.dd HH:mm:ss')
+        }
+      })
+    }
+  } else {
+    giftList.value = []
+    receiveList.value = []
+  }
+}
+
+const onTabChange = (name) => {
+  activeName.value = name
+  getGiftRecordList(name)
+}
+
+onMounted(async () => {
+  if (isSuper.value) {
+    const { data } = await getUserAccountInfo()
+    if (data) {
+      superInfo.value = data?.list.find((v) => v.name === '超级订阅')
+    }
+  }
+  if (queryTab) {
+    activeName.value = queryTab
+  }
+  await nextTick()
+  getGiftRecordList(activeName.value)
+  getGiftSource()
+})
+
+const getGiftSource = async () => {
+  const { error_code: code, data } = await getSubResourcesAjax()
+  if (code === 0 && data) {
+    giftMonth.value = data?.gifted || 0
+  }
+}
+</script>
+
+<template>
+  <div class="j-container gift-record">
+    <Tabs class="j-container" v-model="activeName" @change="onTabChange">
+      <Tab title="我赠送的" name="1">
+        <div v-if="giftList.length" class="gift-list">
+          <div
+            class="gift-item"
+            v-for="gift in giftList"
+            :key="gift.createTime"
+          >
+            <div class="gift-item-time">赠送时间:{{ gift.createTime }}</div>
+            <div
+              v-for="item in gift.arr"
+              :key="item.id"
+              class="flex flex-items-center"
+            >
+              <div class="gift-item-info">
+                <span class="gift-item-label">手机号:</span>
+                <span class="gift-item-value">{{ item.phone }}</span>
+              </div>
+              <div class="gift-item-line"></div>
+              <div class="flex-1 gift-item-info">
+                <span class="gift-item-label">时长:</span>
+                <span class="gift-item-value">{{ item.duration }}个月</span>
+              </div>
+              <div
+                class="flex flex-items-center"
+                @click.stop="onNotify(item.id)"
+              >
+                <span class="gift-item-notify">告知朋友</span>
+                <i class="j-icon j-base-icon icon-notify-active"></i>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div v-else class="gift-empty">
+          <AppEmpty>
+            <p class="empty-text">{{ giftEmpty.text }}</p>
+            <button class="empty-btn" @click="goTargetPage(giftEmpty.link)">
+              {{ giftEmpty.btn }}
+            </button>
+          </AppEmpty>
+        </div>
+      </Tab>
+      <Tab title="我接收的" name="2">
+        <div v-if="receiveList.length" class="receive-list">
+          <ul
+            class="receive-item"
+            v-for="receive in receiveList"
+            :key="receive.id"
+          >
+            <li class="receive-item-time pb-4px">
+              赠送时间:{{ receive.createTime }}
+            </li>
+            <li class="flex flex-items-center pt-4px">
+              <span class="receive-item-label">来自好友:</span>
+              <span class="receive-item-value">{{ receive.phone }}</span>
+            </li>
+            <li class="flex flex-items-center pt-4px">
+              <span class="receive-item-label">得赠时长:</span>
+              <span class="receive-item-value">{{ receive.duration }}</span>
+            </li>
+          </ul>
+        </div>
+        <div v-else class="receive-empty">
+          <AppEmpty>
+            <span class="empty-text">暂无好友赠送记录</span>
+          </AppEmpty>
+        </div>
+      </Tab>
+    </Tabs>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@mixin labelStyle {
+  font-size: 12px;
+  line-height: 20px;
+  color: #5f5e64;
+}
+@mixin valueStyle {
+  font-size: 14px;
+  line-height: 24px;
+  color: #171826;
+}
+@mixin itemCard {
+  padding: 8px 12px;
+  margin-bottom: 8px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0px 2px 8px 0px rgba(54, 147, 179, 0.051);
+}
+.gift-record {
+  height: 100%;
+  .gift-list,
+  .receive-list {
+    padding: 8px;
+  }
+  .gift-item {
+    @include itemCard;
+    &-time {
+      margin-bottom: 8px;
+      @include labelStyle;
+    }
+    &-line {
+      height: 18px;
+      width: 1px;
+      background: #c0c4cc;
+      margin: 0 8px;
+      border-radius: 50%;
+    }
+    &-info {
+      display: flex;
+      align-items: center;
+      &:nth-child(1) {
+        width: 139px;
+      }
+    }
+    &-label {
+      @include labelStyle;
+      flex-shrink: 0;
+    }
+    &-value {
+      @include valueStyle;
+      flex-shrink: 0;
+    }
+    &-notify {
+      margin-left: 20px;
+      font-size: 14px;
+      line-height: 20px;
+      color: $main;
+    }
+    .icon-notify-active {
+      width: 16px;
+      height: 16px;
+      margin-left: 4px;
+    }
+  }
+  .receive-item {
+    @include itemCard;
+    &-time {
+      @include labelStyle;
+    }
+    &-label {
+      @include labelStyle;
+    }
+    &-value {
+      @include valueStyle;
+    }
+  }
+  .gift-empty,
+  .receive-empty {
+    height: 100%;
+    background: #fff;
+  }
+  .empty-text {
+    font-size: 14px;
+    line-height: 20px;
+    color: #5f5e64;
+    text-align: center;
+  }
+  .empty-btn {
+    width: 160px;
+    margin: 28px auto;
+    line-height: 40px;
+    border-radius: 8px;
+    background: $main;
+    color: #fff;
+    font-size: 16px;
+  }
+  ::v-deep {
+    .van-tabs__wrap {
+      height: 48px;
+    }
+    .van-tabs__content {
+      flex: 1;
+      overflow-y: scroll;
+      overflow-x: hidden;
+    }
+    .van-tab__pane {
+      height: 100%;
+    }
+    .van-tabs__line {
+      width: 24px;
+      height: 2px;
+      bottom: 20px;
+    }
+  }
+}
+</style>

+ 222 - 0
apps/mobile/src/views/giving/share.vue

@@ -0,0 +1,222 @@
+<script setup>
+import { getCurrentInstance, nextTick, onMounted, ref } from 'vue'
+import { openAppOrWxPage } from '@/utils'
+import { LINKS } from '@/data'
+import { vipGiftActivityConfigAjax, vipGiftDetailAjax } from '@/api/modules'
+import { dateFormatter } from '@/utils/utils'
+
+const that = getCurrentInstance().proxy
+
+const result = ref({
+  headImg: new URL('@/assets/image/public/auto.png', import.meta.url).href,
+  giftUserPhone: '',
+  recipientUserPhone: '',
+  duration: 1,
+  province: 1,
+  vipStartTime: '',
+  vipEndTime: '',
+  iType: 0
+})
+const vipGiftPeriod = ref(false)
+
+const query = that.$route.query
+const isBindPhone = ref(false)
+const isFollow = query.isSubscribe === 'true'
+const isSamePhone = ref(false)
+const giftId = query.giftId
+
+// 获取超级订阅活动配置
+async function getVipActivityConfig() {
+  const { data } = await vipGiftActivityConfigAjax()
+  if (data) {
+    const { startTime, endTime } = data
+    // 校验活动时间有效性
+    const isValidPeriod =
+      startTime &&
+      endTime &&
+      Number.parseInt(startTime) < Number.parseInt(endTime)
+    const nowTime = Math.floor(Date.now() / 1000)
+    vipGiftPeriod.value =
+      isValidPeriod &&
+      nowTime >= Number.parseInt(startTime) &&
+      nowTime <= Number.parseInt(endTime)
+  }
+}
+
+async function getDetail() {
+  // TODO: 接口请求
+  const {
+    error_code: code,
+    data,
+    error_msg: msg
+  } = await vipGiftDetailAjax({
+    giftId
+  })
+  if (code === 0) {
+    const {
+      areacount,
+      duration,
+      giftUserPhone,
+      recipientUserPhone,
+      vipStartTime,
+      vipEndTime,
+      itype,
+      isBinding,
+      isSame
+    } = data
+    const headImg = new URL('@/assets/image/public/auto.png', import.meta.url)
+      .href
+    const province = areacount === -1 ? '全国' : `${areacount}个省级区域`
+    const time = `${dateFormatter(vipStartTime, 'yyyy.MM.dd')}-${dateFormatter(
+      vipEndTime,
+      'yyyy.MM.dd'
+    )}`
+    isBindPhone.value = isBinding
+    isSamePhone.value = isSame
+    result.value = {
+      headImg,
+      province,
+      duration,
+      giftUserPhone,
+      recipientUserPhone,
+      time,
+      iType: itype
+    }
+  } else {
+    that.$toast(msg)
+  }
+}
+
+onMounted(() => {
+  getVipActivityConfig()
+  getDetail()
+})
+
+async function onViewNow() {
+  await nextTick()
+  if (isFollow) {
+    if (isBindPhone.value) {
+      if (isSamePhone.value) {
+        if (vipGiftPeriod.value) {
+          that.$router.push('/giving/record?activeTab=2')
+        } else {
+          that.$router.push('/tabbar/home')
+        }
+      } else {
+        that.$router.push('/tabbar/home')
+      }
+    } else {
+      openAppOrWxPage(LINKS.绑定手机号合并)
+    }
+  } else {
+    // return that.$toast('请先关注剑鱼标讯公众号')
+    location.href =
+      'https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=Mzk0MjIyMzY2Nw==&scene=110#wechat_redirect'
+  }
+}
+</script>
+
+<template>
+  <div class="j-container gift-result">
+    <div class="result-card">
+      <div class="card-container">
+        <div class="flex flex-(items-center justify-center) head-img">
+          <img :src="result.headImg" alt="head" />
+        </div>
+        <div class="gift-account">
+          <p>
+            你的好友&nbsp;&nbsp;<span class="highlight-text">{{
+              result.giftUserPhone
+            }}</span>
+          </p>
+          <p>
+            送您剑鱼标讯账号{{ result.recipientUserPhone }}
+            <span class="highlight-text">超级订阅</span> 服务
+          </p>
+        </div>
+        <div class="van-hairline--bottom" />
+        <ul class="gift-content">
+          <li class="gift-item">
+            <span class="gift-item-label"
+              ><em v-if="result.iType === 1">续费</em
+              ><em v-else>购买</em>区域:</span
+            >
+            <span class="gift-item-value">{{ result.province }}</span>
+          </li>
+          <li class="gift-item">
+            <span class="gift-item-label"
+              ><em v-if="result.iType === 1">续费</em
+              ><em v-else>订阅</em>周期:</span
+            >
+            <span class="gift-item-value">{{ result.duration }}个月</span>
+          </li>
+          <li class="gift-item">
+            <span class="gift-item-label">有效日期:</span>
+            <span class="gift-item-value">{{ result.time }}</span>
+          </li>
+        </ul>
+      </div>
+    </div>
+    <div class="view-now" @click="onViewNow">立即查看</div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.gift-result {
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(180deg, #fff4dd 0%, #ffffff 100%);
+}
+.result-card {
+  margin: 80px 20px 32px;
+  padding: 0 20px;
+  background: linear-gradient(180deg, #ffffff 0%, #fff9f2 100%);
+  box-shadow: 0px 16px 40px 0px rgba(253, 187, 96, 0.3);
+  border-radius: 16px;
+  .head-img {
+    width: 64px;
+    height: 64px;
+    margin: -32px auto 0;
+    border-radius: 50%;
+    overflow: hidden;
+    border: 1px solid rgba(0, 0, 0, 0.05);
+  }
+  .gift-account {
+    margin-top: 20px;
+    text-align: center;
+    font-size: 14px;
+    line-height: 20px;
+    color: #1d1d1d;
+    padding-bottom: 24px;
+    p:nth-child(1) {
+      margin-bottom: 8px;
+    }
+  }
+  .gift-content {
+    padding: 24px 26px 30px;
+  }
+  .gift-item:not(:last-child) {
+    margin-bottom: 8px;
+  }
+  .gift-item-label {
+    font-size: 14px;
+    line-height: 20px;
+    color: #5f5e64;
+  }
+  .gift-item-value {
+    font-size: 14px;
+    line-height: 20px;
+    color: #171826;
+  }
+}
+.view-now {
+  margin: 0 24px;
+  padding: 8px 0;
+  background: linear-gradient(92.4deg, #4dcdde 0%, #26a4ff 100%);
+  color: #fff;
+  font-size: 18px;
+  line-height: 32px;
+  border-radius: 30px;
+  text-align: center;
+}
+</style>

+ 27 - 0
apps/mobile/src/views/order/components/vipsubscribe/FooterNoticeBar.vue

@@ -0,0 +1,27 @@
+<template>
+  <div class="giving-tips" v-if="isShowGiftNotice">支持送好友超级订阅,快快购买后送给好友吧!</div>
+</template>
+<script>
+import { mapState } from 'vuex'
+
+export default {
+  name: 'VipSubscribeFooterNotice',
+  components: {},
+  data: () => ({}),
+  beforeCreate() {},
+  computed: {
+    ...mapState('createOrder', ['isShowGiftNotice'])
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.giving-tips{
+  padding: 6px 16px;
+  background: rgba(236, 250, 245, 1);
+  text-align: center;
+  font-size: 13px;
+  line-height: 20px;
+  color: $main;
+}
+</style>

+ 95 - 4
apps/mobile/src/views/order/components/vipsubscribe/Introduction.vue

@@ -1,5 +1,8 @@
 <template>
   <div class="vip-rights">
+    <div class="giving-tips" v-if="isShowGift">
+      支持送好友超级订阅,快快购买后送给好友吧!
+    </div>
     <div class="buy-tip">
       <div class="buy-tip-header">购买须知</div>
       <div class="buy-tip-text">
@@ -11,16 +14,94 @@
   </div>
 </template>
 <script>
+import { mapState, mapMutations, mapActions, mapGetters } from 'vuex'
+import { vipGiftActivityConfigAjax } from '@/api/modules'
+import { throttle } from 'lodash'
 export default {
   name: 'VipSubscribeIntroduction',
   components: {},
-  data: () => ({}),
+  data: () => ({
+    vipGiftPeriod: false,
+    type: '',
+    isScroll: false
+  }),
+  computed: {
+    ...mapState('createOrder', ['isShowGiftNotice']),
+    ...mapGetters('user', ['userIdentityType']),
+    isShowGift() {
+      return (
+        this.vipGiftPeriod &&
+        this.isScroll &&
+        (!this.type || this.type === 'buy') &&
+        this.userIdentityType === 0
+      )
+    }
+  },
   beforeCreate() {},
-  created() {},
+  async created() {
+    this.type = this.$route.query.type || ''
+    await this.getUserIdentityList()
+    await this.getVipActivityConfig()
+  },
+  mounted() {
+    const scrollEle = document.querySelector('.create-order-content')
+    if (scrollEle) {
+      scrollEle.addEventListener('scroll', this.handleScroll)
+    }
+  },
+  beforeDestroy() {
+    const scrollEle = document.querySelector('.create-order-content')
+    if (scrollEle) {
+      scrollEle.removeEventListener('scroll', this.handleScroll)
+    }
+  },
   methods: {
-    goViewrights(){
+    ...mapMutations('createOrder', ['updateLayout', 'setShowGiftNotice']),
+    ...mapActions('user', ['getUserIdentityList']),
+    goViewrights() {
       this.$router.push('/common/vipsubscribeRights')
-    }
+    },
+    // 获取超级订阅赠送好友活动配置
+    async getVipActivityConfig() {
+      const { data } = await vipGiftActivityConfigAjax()
+      if (data) {
+        const { startTime, endTime } = data
+        // 校验时间有效性
+        const isValidPeriod =
+          startTime && endTime && parseInt(startTime) < parseInt(endTime)
+        const nowTime = Math.floor(Date.now() / 1000)
+        this.vipGiftPeriod =
+          isValidPeriod &&
+          nowTime >= parseInt(startTime) &&
+          nowTime <= parseInt(endTime)
+        this.setShowGiftNotice(
+          this.vipGiftPeriod &&
+            (!this.type || this.type === 'buy') &&
+            this.userIdentityType === 0
+        )
+        this.updateLayout({
+          footerNotice:
+            (!this.type || this.type === 'buy') && this.userIdentityType === 0
+        })
+      }
+    },
+    handleScroll: throttle(function () {
+      const scrollTop = document.querySelector(
+        '.create-order-content'
+      )?.scrollTop
+      if (scrollTop > 0) {
+        this.isScroll = true
+        this.updateLayout({
+          footerNotice: false
+        })
+      } else {
+        this.isScroll = false
+        this.updateLayout({
+          footerNotice:
+            (!this.type || this.type === 'buy') && this.userIdentityType === 0
+        })
+      }
+    }, 300)
   }
 }
 </script>
@@ -66,5 +147,15 @@ export default {
     margin: auto;
     margin-bottom: 25px;
   }
+  .giving-tips {
+    margin-bottom: -1px;
+    margin-top: -4px;
+    padding: 6px 16px;
+    background: rgba(236, 250, 245, 1);
+    text-align: center;
+    font-size: 13px;
+    line-height: 20px;
+    color: $main;
+  }
 }
 </style>

+ 39 - 0
apps/mobile/src/views/static/SVipGiftNotice.vue

@@ -0,0 +1,39 @@
+<template>
+  <div class="vip-gift-notice">
+    <h1>“送好友超级订阅”产品须知</h1>
+    <h3>请您在下单赠送过程中接受本须知之前,务必审慎阅读、充分理解各条款内容。</h3>
+    <p>1.“送好友超级订阅”是剑鱼标讯推出的一项支持用户在线赠送超级订阅的功能,仅支持在个人身份下进行,若您在企业身份下,请切换至个人身份进行赠送。</p>
+    <p>2.赠送人购买超级订阅后输入“被赠送好友”的手机号即可进行赠送,完成赠送后,赠送人可通过海报、链接分享等方式告知“被赠送好友”,“被赠送好友”登录剑鱼标讯平台即可查看赠送的超级订阅权益。</p>
+    <p>3.购买超级订阅后赠送好友不支持退款,请确认无误后进行赠送。</p>
+    <p>4.在使用本功能过程中,如果用户出现违规行为,剑鱼标讯可限制用户使用本功能,并有权撤销违规交易,必要时追究法律责任。</p>
+    <p>5.如出现不可抗力或情势变更的情况,则平台可暂停本功能,并依相关法律法规的规定主张免责。</p>
+    <p>6.如有其他问题,请拨打客服热线400-108-6670进行反馈。</p>
+    <p>7.本活动最终解释权归北京剑鱼信息技术有限公司所有。</p>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.vip-gift-notice {
+  box-sizing: border-box;
+  padding: 16px;
+  -webkit-overflow-scrolling: touch;
+  text-align: justify;
+  h1 {
+    line-height: 28px;
+    font-size: 16px;
+    margin-bottom: 8px;
+    text-align: center;
+  }
+  h3{
+    margin-bottom: 6px;
+    line-height: 20px;
+    font-size: 13px;
+    color: #1d1d1d;
+  }
+  p {
+    line-height: 22px;
+    font-size: 13px;
+    color: #5f5e64;
+  }
+}
+</style>

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

+ 138 - 0
plugins/gift-friends/README.md

@@ -0,0 +1,138 @@
+# @jy/plugin-gift-friends
+
+> 赠送好友超级订阅插件
+
+## 目录结构
+
+```
+├── 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-gift-friends": "workspace:*"
+
+// 注册
+import { GiftFriendsDialogPlugin } from '@jy/plugin-gift-friends'
+Vue.use(GiftFriendsDialogPlugin)
+```
+
+2. jy项目通过build后放置/common-module/plugins/目录下引入
+
+```
+<script src="/common-module/plugins/js/jy-gift-friends.umd.js"></script>
+// 引入后注册(必须在new Vue()前注册)
+Vue.use(GiftFriends)
+```
+
+3.其它项目通过install私有包引入
+
+```
+pnpm add @jy/plugin-gift-friends@0.0.1
+
+import GiftFriends from '@jy/plugin-gift-friends'
+Vue.use(GiftFriends)
+```
+
+### Example
+
+```
+<template>
+  <div>
+    <button @click="handle">手动实例触发</button>
+  </div>
+</template>
+<script>
+export default {
+  methods: {
+    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-gift-friends.umd.js",
+"module": "./dist/jy-gift-friends.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
+```

+ 89 - 0
plugins/gift-friends/index.html

@@ -0,0 +1,89 @@
+<!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/lodash/4.17.21/lodash.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/element-ui/2.15.13-rc/lib/index.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/lodash/4.17.21/lodash.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>

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

@@ -0,0 +1,44 @@
+{
+  "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": {
+    "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>

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

@@ -0,0 +1,29 @@
+import qs from 'qs'
+import request from './index'
+
+// 超级订阅赠送
+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

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

@@ -0,0 +1,46 @@
+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) => {
+    return Promise.reject(error)
+  }
+)
+
+service.interceptors.response.use(
+  (response) => {
+    const res = response.data
+    if (response.status === 200 || response.error_code === 0) {
+      // 发送请求时配置 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) => {
+    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;
+    }
+  }
+}

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

@@ -0,0 +1,57 @@
+.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;
+}
+
+/* // 清除 input type=number 默认样式 */
+input::-webkit-outer-spin-button,
+input::-webkit-inner-spin-button {
+  -webkit-appearance: none;
+}
+input[type='number'] {
+  -moz-appearance: textfield;
+}

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

@@ -0,0 +1,211 @@
+.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!important;
+}
+.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;
+  text-align: center;
+}
+.gift-submit-confirm-dialog .el-dialog__header::after {
+	content: none;
+}
+.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;
+}

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

@@ -0,0 +1,175 @@
+<template>
+  <el-dialog
+    class="custom-dialog"
+    :custom-class="customClass"
+    v-bind="$props"
+    :show-close="showClose"
+    :visible="visible"
+    :modal-append-to-body="modalAppendToBody"
+    :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
+    },
+    modalAppendToBody: {
+      type: Boolean,
+      default: true
+    }
+  },
+  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> -->

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

@@ -0,0 +1,433 @@
+<template>
+  <div class="gift-submit-container">
+    <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>{{ getProvinceNum }}</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="text"
+                  class="custom-long-input"
+                  placeholder="请输入整数"
+                  @input="validateInput(item)"
+                  @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-show="item.status === -1" :ref="`phoneTip${index}`" class="phone-no-register-tip">
+              {{ statusMessages[item.status] }}
+            </div>
+            <div v-show="item.status !== -1 && item.status !== 1" :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:;" @click="openRouter">《“送好友超级订阅”产品须知》</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>
+    <CustomDialog
+      class="gift-submit-confirm-dialog"
+      width="380px"
+      top="28vh"
+      :visible="showConfirm"
+      title="赠送确认"
+      :modal-append-to-body="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>
+  </div>
+</template>
+
+<script>
+// import { Checkbox, 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
+  },
+  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 (!/^[1-9]\d*$/.test(value)) {
+        callback(new Error('请输入整数'))
+      }
+      else if (!/^[1-9]\d?$/.test(value)) {
+        callback(new Error('赠予时长应小于两位数'))
+      }
+      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: {
+        '0': '',
+        '1': '',
+        '-1': '提示:手机号尚未注册剑鱼,赠送其超级订阅后,平台会自动帮其按照对应手机号注册。',
+        '-2': '手机号已是超级订阅会员,且购买省份与当前省份不一致,不可赠送。',
+        '-3': '不能将超级订阅赠送给自己,请更换手机号。',
+        '-4': '当前用户是省份订阅包用户,暂时无法赠送。'
+      },
+      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
+      }
+    },
+    getProvinceNum() {
+      if (this.subduration.areacount === -1) {
+        return '全国'
+      }
+      else {
+        return `${this.subduration.areacount}个省`
+      }
+    }
+  },
+  created() {
+    this.getSubDurationEvent()
+  },
+  methods: {
+    openRouter() {
+      window.open('/page_workDesktop/work-bench/app/big/giftrecord/notice')
+    },
+    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) {
+              this.getInfoByPhoneEvent(index, item)
+            }
+            else {
+              item.phoneValid = false
+              // 隐藏手机号接口验证提示
+              item.status = this.statusMessages['0']
+            }
+          }
+          else if (fieldname === 'monthnum') {
+            this.addFormStyle(index, error)
+            item.monthnumValid = !error
+          }
+        })
+      }
+    },
+    validateInput(item) {
+      // 移除非数字字符
+      item.monthnum = item.monthnum.replace(/\D/g, '')
+
+      // 确保输入不超过两位数字
+      if (item.monthnum.length > 2) {
+        item.monthnum = item.monthnum.slice(0, 2)
+      }
+
+      // 将输入转换为数字
+      item.monthnum = Number.parseInt(item.monthnum, 10) || ''
+    },
+    // 给单个表单添加样式
+    addFormStyle(index, error) {
+      if (error) {
+        if (this.$refs[`phoneTip${index}`]) {
+          this.$refs[`phoneTip${index}`][0].classList.add('error-highlight')
+        }
+        if (this.$refs[`infoTip${index}`]) {
+          this.$refs[`infoTip${index}`][0].classList.add('error-highlight')
+        }
+      }
+      else {
+        if (this.$refs[`phoneTip${index}`]) {
+          this.$refs[`phoneTip${index}`][0].classList.remove('error-highlight')
+        }
+        if (this.$refs[`infoTip${index}`]) {
+          this.$refs[`infoTip${index}`][0].classList.remove('error-highlight')
+        }
+      }
+    },
+    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
+          }
+        }
+      }
+      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.map((item, index) => {
+        return this.$refs[`form${index}`][0].validate((valid) => {
+          if (!valid) {
+            this.$toast('请先完成当前未完善的手机号和时长信息')
+            this.addFormStyle(index, !valid)
+          }
+          else {
+            this.personList.push({
+              phone: '',
+              monthnum: '',
+              status: '',
+              error: '',
+              phoneValid: false,
+              monthnumValid: false
+            })
+            this.updateFormValidity()
+          }
+        })
+      })
+    },
+    /**
+     * 重置人员列表
+     */
+    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 (acc[cur.phone]) {
+          acc[cur.phone] += Number(cur.monthnum)
+        }
+        else {
+          acc[cur.phone] = Number(cur.monthnum)
+        }
+        return acc
+      }, {})
+      const { error_code: code, error_msg: msg } = await setTransferSubDuration({ phones: JSON.stringify(data) })
+      if (code === 0) {
+        this.$toast('赠送成功')
+        // 跳转到赠送好友记录列表页面
+        window.open('/page_workDesktop/work-bench/app/big/giftrecord/index')
+      }
+      else {
+        this.$toast(msg)
+      }
+      this.$emit('close')
+      this.resetPersonList()
+    },
+    closeFn() {
+      this.$emit('close')
+      this.show = false
+      this.resetPersonList()
+    },
+    closeConfirm() {
+      this.showConfirm = false
+    }
+  }
+}
+</script>

+ 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

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

@@ -0,0 +1,51 @@
+/**
+ * description: 打包入口文件,可输出js插件供外部html调用
+ */
+
+import GiftFriendsDialog from './components/GiftSubmitDialog.vue'
+import registryToast from './components/toast/index'
+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()
+    const element = document.querySelector(options.props.el) || document.body
+    element.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,
+  registryToast,
+  GiftFriendsDialog
+}

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

@@ -0,0 +1,13 @@
+import GiftFriendsDialog from './components/GiftSubmitDialog.vue'
+import GiftFriendsDialogPlugin from './utils/plugins/index.js'
+import './assets/style/dialog.css'
+import './assets/style/gift.css'
+
+// 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 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'
+// import './assets/style/gift.css'
+
+Vue.use(Toast).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

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

@@ -0,0 +1,42 @@
+import GiftFriendsDialog from '../../components/GiftSubmitDialog.vue'
+
+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()
+      const element = options.props.el.$el || options.props.el.__vue__.$el || document.body
+      console.log(element, 'element')
+      element.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', 'vuex', 'vue-router', 'element-ui', 'vant'],
+        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/, '')
+        }
+      }
+    }
+  }
+})

+ 305 - 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
@@ -104,7 +107,14 @@ importers:
         version: 2.15.23-rc(vue@2.7.16)
       excellentexport:
         specifier: ^3.8.1
+<<<<<<< HEAD
         version: 3.8.1
+=======
+        version: 3.9.9
+      html2canvas:
+        specifier: ^1.4.1
+        version: 1.4.1
+>>>>>>> main
       js-cookie:
         specifier: ^3.0.1
         version: 3.0.1
@@ -122,7 +132,14 @@ importers:
         version: 2.0.6
       v-charts:
         specifier: 1.19.0
+<<<<<<< HEAD
         version: 1.19.0(echarts@4.8.0)(vue@2.7.16)(zrender@4.3.2)
+=======
+        version: 1.19.0(echarts@4.8.0)(vue@2.7.16)(zrender@4.3.3)
+      vue-clipboard2:
+        specifier: ^0.3.3
+        version: 0.3.3
+>>>>>>> main
       vue-cookies:
         specifier: ^1.7.4
         version: 1.7.4
@@ -190,6 +207,9 @@ importers:
       vite-plugin-legacy-qiankun:
         specifier: ^0.0.12
         version: 0.0.12
+      vue-waterfall-easy:
+        specifier: ^2.4.4
+        version: 2.4.4
 
   apps/decrypt-js:
     devDependencies:
@@ -867,6 +887,70 @@ importers:
         specifier: ^0.6.2
         version: 0.6.2(vite@4.5.3)
 
+  plugins/gift-friends:
+    dependencies:
+      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':
@@ -3996,7 +4080,160 @@ packages:
     engines: {node: '>= 8'}
     dependencies:
       '@nodelib/fs.scandir': 2.1.5
+<<<<<<< HEAD
       fastq: 1.15.0
+=======
+      fastq: 1.19.0
+
+  /@parcel/watcher-android-arm64@2.5.1:
+    resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm64]
+    os: [android]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@parcel/watcher-darwin-arm64@2.5.1:
+    resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm64]
+    os: [darwin]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@parcel/watcher-darwin-x64@2.5.1:
+    resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [x64]
+    os: [darwin]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@parcel/watcher-freebsd-x64@2.5.1:
+    resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [x64]
+    os: [freebsd]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@parcel/watcher-linux-arm-glibc@2.5.1:
+    resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm]
+    os: [linux]
+    libc: [glibc]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@parcel/watcher-linux-arm-musl@2.5.1:
+    resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm]
+    os: [linux]
+    libc: [musl]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@parcel/watcher-linux-arm64-glibc@2.5.1:
+    resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm64]
+    os: [linux]
+    libc: [glibc]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@parcel/watcher-linux-arm64-musl@2.5.1:
+    resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm64]
+    os: [linux]
+    libc: [musl]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@parcel/watcher-linux-x64-glibc@2.5.1:
+    resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [x64]
+    os: [linux]
+    libc: [glibc]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@parcel/watcher-linux-x64-musl@2.5.1:
+    resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [x64]
+    os: [linux]
+    libc: [musl]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@parcel/watcher-win32-arm64@2.5.1:
+    resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm64]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@parcel/watcher-win32-ia32@2.5.1:
+    resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [ia32]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@parcel/watcher-win32-x64@2.5.1:
+    resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [x64]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@parcel/watcher@2.5.1:
+    resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
+    engines: {node: '>= 10.0.0'}
+    requiresBuild: true
+    dependencies:
+      detect-libc: 1.0.3
+      is-glob: 4.0.3
+      micromatch: 4.0.8
+      node-addon-api: 7.1.1
+    optionalDependencies:
+      '@parcel/watcher-android-arm64': 2.5.1
+      '@parcel/watcher-darwin-arm64': 2.5.1
+      '@parcel/watcher-darwin-x64': 2.5.1
+      '@parcel/watcher-freebsd-x64': 2.5.1
+      '@parcel/watcher-linux-arm-glibc': 2.5.1
+      '@parcel/watcher-linux-arm-musl': 2.5.1
+      '@parcel/watcher-linux-arm64-glibc': 2.5.1
+      '@parcel/watcher-linux-arm64-musl': 2.5.1
+      '@parcel/watcher-linux-x64-glibc': 2.5.1
+      '@parcel/watcher-linux-x64-musl': 2.5.1
+      '@parcel/watcher-win32-arm64': 2.5.1
+      '@parcel/watcher-win32-ia32': 2.5.1
+      '@parcel/watcher-win32-x64': 2.5.1
+    dev: true
+    optional: true
+>>>>>>> main
 
   /@pkgr/core@0.1.1:
     resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
@@ -4163,6 +4400,7 @@ packages:
     resolution: {integrity: sha512-mS6wQ6Do6/wmrF9aTFVpIJ3/IDXhg1EZcQFYHZLHqw6AzMBjTHWnCG35HxSqUNphh0EHqSM6wRTT8HsL1C0x5g==}
     cpu: [arm]
     os: [linux]
+    libc: [glibc]
     requiresBuild: true
     dev: true
     optional: true
@@ -4171,6 +4409,7 @@ packages:
     resolution: {integrity: sha512-EfI3hzYAy5vFNDqpXsNxXcgRDcFHUWSx5nnRSCKwXuQlI5J9dD84g2Usw81n3FLBNsGCegKGwwTVsSKK9cooSQ==}
     cpu: [arm]
     os: [linux]
+    libc: [musl]
     requiresBuild: true
     dev: true
     optional: true
@@ -4188,7 +4427,11 @@ packages:
     resolution: {integrity: sha512-9lhc4UZstsegbNLhH0Zu6TqvDfmhGzuCWtcTFXY10VjLLUe4Mr0Ye2L3rrtHaDd/J5+tFMEuo5LTCSCMXWfUKw==}
     cpu: [arm64]
     os: [linux]
+<<<<<<< HEAD
     libc: [glibc]
+=======
+    libc: [musl]
+>>>>>>> main
     requiresBuild: true
     dev: true
     optional: true
@@ -4197,7 +4440,11 @@ packages:
     resolution: {integrity: sha512-nDY6Yz5xS/Y4M2i9JLQd3Rofh5OR8Bn8qe3Mv/qCVpHFlwtZSBYSPaU4mrGazWkXrdQ98GB//H0BirGR/SKFSw==}
     cpu: [arm64]
     os: [linux]
+<<<<<<< HEAD
     libc: [musl]
+=======
+    libc: [glibc]
+>>>>>>> main
     requiresBuild: true
     dev: true
     optional: true
@@ -4251,7 +4498,11 @@ packages:
     resolution: {integrity: sha512-DNGZvZDO5YF7jN5fX8ZqmGLjZEXIJRdJEdTFMhiyXqyXubBa0WVLDWSNlQ5JR2PNgDbEV1VQowhVRUh+74D+RA==}
     cpu: [x64]
     os: [linux]
+<<<<<<< HEAD
     libc: [glibc]
+=======
+    libc: [musl]
+>>>>>>> main
     requiresBuild: true
     dev: true
     optional: true
@@ -8091,6 +8342,14 @@ packages:
     engines: {node: '>= 12'}
     dev: true
 
+  /clipboard@2.0.11:
+    resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==}
+    dependencies:
+      good-listener: 1.2.2
+      select: 1.1.2
+      tiny-emitter: 2.1.0
+    dev: false
+
   /clipboardy@2.3.0:
     resolution: {integrity: sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==}
     engines: {node: '>=8'}
@@ -8887,6 +9146,10 @@ packages:
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
     engines: {node: '>=0.4.0'}
 
+  /delegate@3.2.0:
+    resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==}
+    dev: false
+
   /delegates@1.0.0:
     resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
     dev: false
@@ -11343,6 +11606,12 @@ packages:
       vue: 2.7.14
     dev: false
 
+  /good-listener@1.2.2:
+    resolution: {integrity: sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==}
+    dependencies:
+      delegate: 3.2.0
+    dev: false
+
   /gopd@1.2.0:
     resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
     engines: {node: '>= 0.4'}
@@ -15217,8 +15486,17 @@ packages:
     resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==}
     dev: true
 
+<<<<<<< HEAD
   /selfsigned@2.1.1:
     resolution: {integrity: sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==}
+=======
+  /select@1.1.2:
+    resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==}
+    dev: false
+
+  /selfsigned@2.4.1:
+    resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==}
+>>>>>>> main
     engines: {node: '>=10'}
     dependencies:
       node-forge: 1.3.1
@@ -16016,8 +16294,17 @@ packages:
     resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==}
     dev: true
 
+<<<<<<< HEAD
   /tinybench@2.5.1:
     resolution: {integrity: sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==}
+=======
+  /tiny-emitter@2.1.0:
+    resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
+    dev: false
+
+  /tinybench@2.9.0:
+    resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+>>>>>>> main
     dev: true
 
   /tinyexec@0.3.0:
@@ -17400,8 +17687,19 @@ packages:
       vue: 2.7.16
     dev: false
 
+<<<<<<< HEAD
   /vue-cookies@1.7.4:
     resolution: {integrity: sha512-mOS5Btr8V9zvAtkmQ7/TfqJIropOx7etDAgBywPCmHjvfJl2gFbH2XgoMghleLoyyMTi5eaJss0mPN7arMoslA==}
+=======
+  /vue-clipboard2@0.3.3:
+    resolution: {integrity: sha512-aNWXIL2DKgJyY/1OOeITwAQz1fHaCIGvUFHf9h8UcoQBG5a74MkdhS/xqoYe7DNZdQmZRL+TAdIbtUs9OyVjbw==}
+    dependencies:
+      clipboard: 2.0.11
+    dev: false
+
+  /vue-cookies@1.8.6:
+    resolution: {integrity: sha512-e2kYaHj1Y/zVsBSM3KWlOoVJ5o3l4QZjytNU7xdCgmkw3521CMUerqHekBGZKXXC1oRxYljBeeOK2SCel6cKuw==}
+>>>>>>> main
     dev: false
 
   /vue-demi@0.14.6(vue@2.7.14):
@@ -17829,12 +18127,19 @@ packages:
     resolution: {integrity: sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==}
     dev: true
 
+<<<<<<< HEAD
   /vue@2.7.14:
     resolution: {integrity: sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==}
     deprecated: Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.
     dependencies:
       '@vue/compiler-sfc': 2.7.14
       csstype: 3.1.2
+=======
+  /vue-waterfall-easy@2.4.4:
+    resolution: {integrity: sha512-5OkpT2FPNC3rHBy858zk/nmJxqdPaGmj/KVbmA6dgcvtsovKMa+zuf/Z7F+S2NnObeavpIBztTWgcH3S42ZD+g==}
+    engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
+    dev: true
+>>>>>>> main
 
   /vue@2.7.16:
     resolution: {integrity: sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==}

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio