Procházet zdrojové kódy

feat: 移动端线索留资弹窗完善

cuiyalong před 3 týdny
rodič
revize
ba8285aeee

+ 0 - 2
plugins/leave-source/src/entry.js

@@ -3,7 +3,6 @@
  */
 
 import PCContentCard from './lib/pc/components/content-card.vue'
-import MobileContentCard from './lib/mobile/content-card.vue'
 import PCLeaveDialog from './lib/pc/content-dialog.vue'
 import MobileLeavePopup from './lib/mobile/content-popup.vue'
 import registryToast from './components/toast/index'
@@ -21,7 +20,6 @@ export default {
   registryToast,
   PCContentCard,
   PCContentStatic,
-  MobileContentCard,
   PCLeaveDialog,
   MobileLeavePopup,
 }

+ 1 - 3
plugins/leave-source/src/index.js

@@ -1,5 +1,4 @@
-import PCContentCard from './lib/pc/content-card.vue'
-import MobileContentCard from './lib/mobile/content-card.vue'
+import PCContentCard from './lib/pc/components/content-card.vue'
 import PCLeaveDialog from './lib/pc/content-dialog.vue'
 import MobileLeavePopup from './lib/mobile/content-popup.vue'
 import registryToast from './components/toast/index'
@@ -7,7 +6,6 @@ import registryToast from './components/toast/index'
 export default {
   registryToast,
   PCContentCard,
-  MobileContentCard,
   PCLeaveDialog,
   MobileLeavePopup,
 }

+ 38 - 0
plugins/leave-source/src/lib/mobile/components/CenterLayout.vue

@@ -0,0 +1,38 @@
+<template>
+  <div class="center-layout j-container">
+    <div
+      class="j-icon j-base-icon icon-mobile-close top-right-icon"
+      @click="closeIconClick"
+    />
+    <div class="j-main">
+      <slot name="default" />
+    </div>
+    <div class="j-footer">
+      <slot name="footer" />
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'CenterLayout',
+  methods: {
+    closeIconClick() {
+      this.$emit('closeIconClick')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.center-layout {
+  position: relative;
+
+  .top-right-icon {
+    position: absolute;
+    top: 8px;
+    right: 8px;
+    z-index: 2;
+  }
+}
+</style>

+ 32 - 0
plugins/leave-source/src/lib/mobile/components/InputPhone.vue

@@ -0,0 +1,32 @@
+<script setup>
+import { reactive, ref, watch } from 'vue'
+
+const props = defineProps({})
+
+const inputInfo = reactive({
+  maxlength: 11,
+  phone: ''
+})
+</script>
+
+<script>
+export default {
+  name: 'MobileInputPhone',
+}
+</script>
+
+<template>
+  <div class="input-phone-container">
+    <input
+      v-model="inputInfo.phone"
+      :maxlength="inputInfo.maxlength"
+      type="text"
+      class="input-phone"
+      placeholder="请输入手机号码"
+    >
+  </div>
+</template>
+
+<style scoped lang="scss">
+
+</style>

+ 1 - 3
plugins/leave-source/src/lib/mobile/components/PopupLayout.vue

@@ -58,8 +58,6 @@ export default {
   }
 }
 .popup-layout {
-  .j-main > * {
-    max-height: 60vh;
-  }
+  height: 72vh;
 }
 </style>

+ 124 - 0
plugins/leave-source/src/lib/mobile/components/QrCode.vue

@@ -0,0 +1,124 @@
+<template>
+  <div class="qr-code-wrapper">
+    <div class="qr-code">
+      <img ref="img" :src="qr" class="qr-img" alt="二维码" crossorigin="anonymous" @load="onImageLoad">
+    </div>
+    <div class="qr-bottom-container">
+      <div v-if="inWx" class="wx-bottom-text">
+        长按识别二维码
+      </div>
+      <div v-if="inH5" class="wx-bottom-text">
+        长按保存二维码
+      </div>
+      <button v-if="inApp" class="app-bottom-button" @click="saveQr">
+        <div class="button-main-text">
+          打开微信扫一扫
+        </div>
+        <div class="button-sub-text">
+          (二维码将自动保存到相册)
+        </div>
+      </button>
+    </div>
+  </div>
+</template>
+
+<script>
+import { showToast } from '@/utils/toast'
+
+export default {
+  name: 'QrCode',
+  props: {
+    platform: {
+      type: String,
+      default: 'h5',
+      validator(value) {
+        return ['app', 'wx', 'h5'].includes(value)
+      }
+    },
+    qr: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    inApp() {
+      return this.platform === 'app'
+    },
+    inWx() {
+      return this.platform === 'wx'
+    },
+    inH5() {
+      return this.platform === 'h5'
+    }
+  },
+  methods: {
+    onImageLoad(e) {
+      if (!this.inApp)
+        return
+      const img = this.$refs.img || e.target
+      this.imgBase64 = this.imageUrlToBase64(img)
+      this.saveQr(this.imgBase64)
+    },
+    imageUrlToBase64(img) {
+      const canvas = document.createElement('canvas')
+      canvas.width = img.width
+      canvas.height = img.height
+
+      const ctx = canvas.getContext('2d')
+      ctx.drawImage(img, 0, 0)
+
+      // 转换为 Base64(默认格式为 PNG)
+      const base64 = canvas.toDataURL('image/png')
+
+      return base64
+    },
+    saveQr(base64) {
+      try {
+        window.__compatibleAppFn(JyObj.savePic, base64, '保存图片/视频到相册时,需要用到相机权限、存储权限!')
+        setTimeout(() => {
+          showToast('二维码保存成功')
+        }, 1000)
+      }
+      catch (error) {
+        console.log(error)
+      }
+    },
+
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import "@/assets/style/_variables.scss";
+.qr-code {
+  width: 112px;
+  height: 112px;
+  border-radius: 4px;
+  border: 1px solid rgba(0, 0, 0, 0.1);
+}
+.qr-img {
+  display: block;
+  width: 100%;
+}
+.qr-bottom-container {
+  margin-top: 4px;
+}
+.wx-bottom-text {
+  color: #171826;
+  font-size: 16px;
+}
+
+.app-bottom-button {
+  color: #fff;
+  background-color: #2cb7ca;
+  padding: 4px 8px;
+  border: none;
+  border-radius: 4px;
+  .button-main-text {
+    font-size: 15px;
+  }
+  .button-sub-text {
+    font-size: 11px;
+  }
+}
+</style>

+ 42 - 0
plugins/leave-source/src/lib/mobile/components/QrContainer.vue

@@ -0,0 +1,42 @@
+<template>
+  <div class="qr-container">
+    <p class="qr-title">
+      添加客服,立享1V1管家式服务
+    </p>
+    <div class="qr-content">
+      <QrCode :qr="qr" :platform="platform" />
+    </div>
+  </div>
+</template>
+
+<script>
+import QrCode from './QrCode.vue'
+
+export default {
+  name: 'QrContainer',
+  components: {
+    QrCode
+  },
+  props: {
+    platform: String,
+    qr: {
+      type: String,
+      default: ''
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss">
+.qr-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-bottom: 20px;
+}
+::v-deep {
+  .qr-code {
+    margin: 0 auto;
+  }
+}
+</style>

+ 30 - 0
plugins/leave-source/src/lib/mobile/components/center-card.vue

@@ -0,0 +1,30 @@
+<template>
+  <div class="mobile-content-card">
+    <QrContainer :qr="qr" :platform="platform" />
+    <FooterCard column />
+  </div>
+</template>
+
+<script>
+import QrContainer from './QrContainer'
+import FooterCard from './footer'
+
+export default {
+  name: 'MobileContentCard',
+  components: {
+    QrContainer,
+    FooterCard
+  },
+  props: {
+    platform: String,
+    qr: {
+      type: String,
+      default: ''
+    },
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 38 - 0
plugins/leave-source/src/lib/mobile/components/content-card.vue

@@ -0,0 +1,38 @@
+<template>
+  <div class="mobile-content-card">
+    <ContentInfo :phone="phone" :qr="qr" :platform="platform" :use-customer3="useCustomer3" />
+    <FooterCard />
+  </div>
+</template>
+
+<script>
+import ContentInfo from './content'
+import FooterCard from './footer'
+
+export default {
+  name: 'MobileContentCard',
+  components: {
+    ContentInfo,
+    FooterCard
+  },
+  props: {
+    phone: {
+      type: String,
+      default: '13283800000'
+    },
+    qr: {
+      type: String,
+      default: ''
+    },
+    useCustomer3: {
+      type: Boolean,
+      default: false,
+    },
+    platform: String,
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 145 - 0
plugins/leave-source/src/lib/mobile/components/content.vue

@@ -0,0 +1,145 @@
+<template>
+  <div class="mobile-content-card">
+    <div class="hd-container">
+      <div class="header-tip">
+        以下三种方式任选:
+      </div>
+      <div class="step-list">
+        <div class="step-item step-item-1">
+          <span class="step-number-index">1. </span>
+          <span class="step-item-content">
+            <span class="step-left-text">
+              <span>联系客服:拨打 </span>
+              <span class="highlight-text" @click="doCallPhone">{{ phone }}</span>
+            </span>
+            <span class="step-right-icon" @click="doCallPhone">
+              <span class="j-icon j-icon-base icon-mobile-phone wh24" />
+            </span>
+          </span>
+        </div>
+        <div class="step-item step-item-2">
+          <span class="step-number-index">2. </span>
+          <span class="step-item-content">
+            <div class="step-left-text">添加企业微信:</div>
+            <QrCode :qr="qr" :platform="platform" />
+          </span>
+        </div>
+        <div class="step-item step-item-3">
+          <span class="step-number-index">3. </span>
+          <span v-if="useCustomer3" class="step-item-content">
+            <span class="step-left-text">
+              <span class="highlight-text underline" @click="askOnlineCustom">咨询在线客服 ></span>
+            </span>
+          </span>
+          <span v-else class="step-item-content">
+            <span class="step-left-text">客服主动联系您:</span>
+            <div class="step-right-content">
+              <InputPhone />
+            </div>
+          </span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import QrCode from './QrCode.vue'
+import InputPhone from './InputPhone.vue'
+import { mobileCustomPage } from '@/utils/utils'
+
+export default {
+  name: 'ContentInfo',
+  components: {
+    QrCode,
+    InputPhone
+  },
+  props: {
+    phone: {
+      type: String,
+      default: ''
+    },
+    qr: {
+      type: String,
+      default: ''
+    },
+    useCustomer3: {
+      type: Boolean,
+      default: false,
+    },
+    platform: String
+  },
+  methods: {
+    askOnlineCustom(platform = 'app') {
+      const url = mobileCustomPage(platform)
+      location.href = url
+    },
+    doCallPhone() {
+      const phone = this.phone
+      if (phone) {
+        this.callPhone(phone)
+      }
+    },
+    callPhone(phone) {
+      if (!phone)
+        return console.error('手机号为空')
+      if (this.platform === 'app') {
+        try {
+          JyObj.callPhone(phone)
+        }
+        catch (error) {
+          console.log(error)
+        }
+      }
+      else {
+        location.href = `tel:${phone}`
+      }
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.hd-container {
+  padding: 12px 16px;
+  .header-tip {
+    font-size: 14px;
+    line-height: 20px;
+    color: #5F5E64;
+  }
+  .step-list {
+    margin-top: 8px;
+    margin-bottom: 20px;
+    font-size: 16px;
+    line-height: 24px;
+    color: #171826;
+  }
+  .step-item {
+    display: flex;
+    &:not(:last-of-type) {
+      margin-bottom: 14px;
+    }
+    .underline {
+      text-decoration: underline;
+    }
+  }
+  .step-item-content {
+    margin-left: 4px;
+    flex: 1;
+  }
+
+  .step-item-1 {
+    align-items: center;
+    .step-item-content {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+    }
+  }
+  .step-item-2 {
+    .step-left-text {
+      margin-bottom: 4px;
+    }
+  }
+}
+</style>

+ 100 - 0
plugins/leave-source/src/lib/mobile/components/footer.vue

@@ -0,0 +1,100 @@
+<template>
+  <div class="leave-common-footer" :class="{ 'footer-column': column }">
+    <div class="leave-common-footer-content">
+      <div class="leave-common-title">
+        {{ list.title }}
+      </div>
+      <div class="leave-common-footer-list">
+        <div
+          v-for="item in list.list"
+          :key="item.title"
+          class="leave-common-footer-item"
+        >
+          <div class="leave-common-footer-item-icon">
+            <span class="j-icon" :class="item.icon" />
+          </div>
+          <div class="leave-common-footer-item-title">
+            <span class="text">{{ item.title }}</span>
+            <span v-if="!column" class="splitter">-</span>
+            <span class="subtext">{{ item.subtitle }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import dataList from '@/components/common/index.js'
+
+export default {
+  name: 'LeaveCommonFooter',
+  props: {
+    column: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      list: dataList
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.leave-common-footer {
+  background: url(@/assets/images/BG.png) no-repeat;
+  background-size: contain;
+  .leave-common-title {
+    font-size: 18px;
+    line-height: 30px;
+  }
+  .leave-common-footer-content {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    padding: 12px 16px;
+  }
+  .leave-common-footer-list {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    flex-wrap: wrap;
+    margin-top: 12px;
+    .leave-common-footer-item {
+      display: flex;
+      justify-content: center;
+      margin-bottom: 10px;
+      .leave-common-footer-item-icon {
+        img {
+          width: 16px;
+          height: 16px;
+        }
+      }
+      .leave-common-footer-item-title {
+        margin-left: 8px;
+        font-size: 12px;
+        color: #1d1d1d;
+        .text {
+          font-weight: bold;
+        }
+      }
+    }
+  }
+
+  &.footer-column {
+    .leave-common-footer-content {
+      align-items: center;
+    }
+    .leave-common-footer-item-title {
+      display: flex;
+      flex-direction: column;
+    }
+    .subtext {
+      font-weight: 400;
+    }
+  }
+}
+</style>

+ 0 - 24
plugins/leave-source/src/lib/mobile/content-card.vue

@@ -1,24 +0,0 @@
-<template>
-  <div>
-    content-card-mobile
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'ContentCard',
-  data() {
-    return {}
-  },
-  methods: {
-    // 手动触发绑定弹框
-    handle() {
-
-    }
-  }
-}
-</script>
-
-<style scoped lang="scss">
-
-</style>

+ 72 - 9
plugins/leave-source/src/lib/mobile/content-popup.vue

@@ -1,7 +1,10 @@
 <script setup>
+import { computed } from 'vue'
 import AnimatedOverlay from '../../components/dialog/AnimatedOverlay.vue'
 import PopupLayout from './components/PopupLayout.vue'
-import ContentCard from './content-card.vue'
+import CenterLayout from './components/CenterLayout.vue'
+import MobileContentCard from './components/content-card.vue'
+import MobileCenterCard from './components/center-card.vue'
 import { usePreLeaveInfo } from '@/utils/hooks'
 
 const props = defineProps({
@@ -18,6 +21,26 @@ const props = defineProps({
     type: String,
     default: '联系专属客服,申请免费体验'
   },
+  // 是否静态客服信息(非线索客服信息)
+  staticInfo: {
+    type: Boolean,
+    default: false,
+  },
+  useCustomer3: {
+    type: Boolean,
+    default: false,
+  },
+  platform: {
+    type: String,
+    default: '',
+  },
+})
+
+const center = computed(() => {
+  return props.staticInfo
+})
+const contentTransitionName = computed(() => {
+  return center.value ? 'fade' : 'slide-up'
 })
 
 const {
@@ -25,7 +48,9 @@ const {
   close,
   visible,
   configInfo,
-} = usePreLeaveInfo({ props })
+} = usePreLeaveInfo({
+  props
+})
 
 console.log(configInfo)
 
@@ -44,17 +69,37 @@ export default {
   <AnimatedOverlay
     class="mobile-leave-dialog"
     :visible="visible"
-    content-transition-name="slide-up"
+    :content-transition-name="contentTransitionName"
     @update:visible="updateVisible"
     @close="close"
   >
-    <PopupLayout :title="popupTitle">
-      <ContentCard />
-    </PopupLayout>
+    <div class="mobile-leave-dialog-content" :class="{ 'c-m-center': center, 'c-m-bottom': !center }">
+      <CenterLayout v-if="center" @closeIconClick="close">
+        <MobileCenterCard
+          :qr="configInfo.wxer"
+          :phone="configInfo.phone"
+          :platform="platform"
+        />
+      </CenterLayout>
+      <PopupLayout v-else :title="popupTitle" @closeIconClick="close">
+        <MobileContentCard
+          :qr="configInfo.wxer"
+          :phone="configInfo.phone"
+          :platform="platform"
+          :use-customer3="useCustomer3"
+        />
+      </PopupLayout>
+    </div>
   </AnimatedOverlay>
 </template>
 
 <style scoped lang="scss">
+::v-deep {
+  ::-webkit-scrollbar {
+    /*滚动条整体样式*/
+    width: 2px;
+  }
+}
 .mobile-leave-dialog {
   display: flex;
   align-items: flex-end; /* 内容置底 */
@@ -62,11 +107,29 @@ export default {
 ::v-deep {
   .overlay-content {
     width: 100%;
+  }
+}
+
+.mobile-leave-dialog-content {
+  box-sizing: border-box;
+  &.c-m-bottom {
+    width: 100%;
     max-height: 70vh;
-    background: #fff;
-    border-radius: 16px 16px 0 0;
+    border-radius: 12px 12px 0 0;
+    background-color: #fff;
     overflow-y: auto;
-    box-sizing: border-box;
+  }
+  &.c-m-center {
+    position: absolute;
+    top: 45%;
+    left: 50%;
+    width: 86%;
+    min-height: 200px;
+    transform: translate3d(-50%,-50%,0);
+    border-radius: 8px;
+    z-index: 2;
+    background-color: #fff;
+    overflow: hidden;
   }
 }
 </style>

+ 3 - 0
plugins/leave-source/src/utils/leave.js

@@ -7,6 +7,7 @@ import { isDOMElement } from '@/utils/utils'
 const instanceMap = {
   'wx': null,
   'app': null,
+  'h5': null,
   'pc': null,
   'phone-confirm': null,
 }
@@ -22,6 +23,7 @@ export function createInstance(platform, options = {}) {
     'wx': MobileLeavePopup,
     'app': MobileLeavePopup,
     'pc': PCLeaveDialog,
+    'h5': MobileLeavePopup,
     'phone-confirm': phoneConfirmDialog,
   }
   const MyComponent = componentMap[platform]
@@ -66,6 +68,7 @@ export async function doLeave(options = {}) {
   if (instance) {
     instance.source = source
     instance.sourceDesc = source_desc
+    instance.platform = platform
     // props赋值
     if (props) {
       for (const key in props) {

+ 10 - 0
plugins/leave-source/src/utils/utils.js

@@ -1,3 +1,13 @@
 export function isDOMElement(obj) {
   return obj && typeof obj === 'object' && obj.nodeType === 1
 }
+
+export function mobileCustomPage(platform) {
+  const map = {
+    app: '/jyapp/free/customer',
+    h5: '/jyapp/free/customer',
+    wx: '/big/wx/page/customer'
+  }
+
+  return map[platform] || map.app
+}