Browse Source

Merge remote-tracking branch 'origin/dev_suwenjiang' into dev_suwenjiang

suwenjiang 2 months ago
parent
commit
7e75d24918

BIN
src/assets/img/scan/pic.png


+ 101 - 0
src/composables/useScanCode.js

@@ -0,0 +1,101 @@
+import {Html5Qrcode, Html5QrcodeScanner, Html5QrcodeScanType, Html5QrcodeSupportedFormats} from "html5-qrcode";
+
+export const useScanCode = (elementId) => {
+    const router = useRouter()
+
+    let scanInstance = null; // 扫码实例
+    let results = ref(null); // 扫码结果
+
+    const initScanInstance = () => {
+        if(!elementId) throw error('请传入放置扫码功能的元素ID')
+        if (!scanInstance) {
+            // reader放置扫码功能的元素ID
+            scanInstance = new Html5Qrcode(elementId, {
+                formatsToSupport: [
+                    Html5QrcodeSupportedFormats.QR_CODE,
+                ],
+            })
+        }
+    }
+    const openQrcode = async () => {
+        Html5Qrcode.getCameras()
+            .then(devices => {
+                if (devices && devices.length) {// 当前环境下能识别出摄像头,并且摄像头的数据可能不止一个
+                    initScanInstance()
+
+                    // isShow.value = true
+                    scanInstance.start(
+                        {facingMode: "environment"},
+                        {
+                            focusMode: 'continuous',
+                            fps: 1, // 可选,每n秒帧扫描一次
+                            qrbox: { // 扫描的UI框
+                                width: 250,
+                                height: 250
+                            },
+                            videoConstraints: {
+                                // width: 375,
+                                // height: (window.visualViewport.height - 50),
+                                aspectRatio: window.visualViewport.height / window.visualViewport.width,
+                                facingMode: "environment",
+                            }
+                        },
+                        (decodedText, decodedResult) => {
+                            showToast('识别成功')
+                            // 扫描结果
+                            results.value = {
+                                decodedText,
+                                decodedResult
+                            }
+                            // scanInstance.stop()
+                        },
+                        (errorMessage, error) => {
+                            closeQrcode(errorMessage)
+                        }
+                    )
+                }
+            })
+            .catch((err) => {
+                // 错误信息处理仅供参考,具体情况看输出!!!
+                let errorStr = ''
+                if (typeof err === "string") {
+                    errorStr = err
+                } else {
+                    if (err.name === "NotAllowedError")
+                        errorStr = "您需要授予相机访问权限"
+                    if (err.name === "NotFoundError") {
+                        errorStr = "未检测到摄像头"
+                    }
+                    if (err.name === "NotSupportedError")
+                        errorStr = "摄像头访问只支持在安全的上下文中,如https或localhost"
+                    if (err.name === "NotReadableError") errorStr = "摄像头被占用"
+                    if (err.name === "OverconstrainedError")
+                        errorStr = "安装摄像头不合适"
+                    if (err.name === "StreamApiNotSupportedError")
+                        errorStr = "此浏览器不支持流API"
+                }
+                showToast(errorStr)
+                closeQrcode()
+            })
+    }
+
+    const closeQrcode = () => {
+        // if (scanInstance) scanInstance.stop()
+        console.log(router, 'router')
+        router.back()
+    }
+
+
+    onMounted(() => {
+        initScanInstance()
+        openQrcode()
+    })
+    onUnmounted(() => {
+        if (scanInstance) scanInstance.stop()
+    })
+
+    return {
+        results,
+        closeQrcode
+    }
+}

+ 11 - 0
src/layouts/scan.vue

@@ -0,0 +1,11 @@
+<template>
+  <div class="w-full h-full">
+    <slot />
+  </div>
+</template>
+<script setup lang="ts">
+
+</script>
+<style scoped lang="scss">
+
+</style>

+ 5 - 34
src/pages/follow/components/follow-list/index.vue

@@ -39,16 +39,6 @@ const listConfig = new Map([
     {
       empty: '暂无互关',
       apiUrl: '/website/tourism/fans/getFriends',
-      getResponse: (data) => {
-        try {
-          const {fansList, total} = data
-          return {
-            list: fansList ?? [],
-            total: total ?? 0
-          }
-        } catch (e) {
-        }
-      }
     }
   ],
   [
@@ -56,16 +46,6 @@ const listConfig = new Map([
     {
       empty: '暂无关注',
       apiUrl: '/website/tourism/fans/getMyConCern',
-      getResponse: (data) => {
-        try {
-          const {dataList, totalCount} = data
-          return {
-            list: dataList ?? [],
-            total: totalCount ?? 0
-          }
-        } catch (e) {
-        }
-      }
     }
   ],
   [
@@ -73,16 +53,6 @@ const listConfig = new Map([
     {
       empty: '暂无粉丝',
       apiUrl: '/website/tourism/fans/getMyFans',
-      getResponse: (data) => {
-        try {
-          const {dataList, totalCount} = data
-          return {
-            list: dataList ?? [],
-            total: totalCount ?? 0
-          }
-        } catch (e) {
-        }
-      }
     }
   ]
 ])
@@ -109,13 +79,14 @@ const onLoad = async () => {
     loading.value = true
     pageNum.value += 1;
 
-    const {data} = await request(`${currListConfig.value.apiUrl}`, {
+    const res = await request(`${currListConfig.value.apiUrl}`, {
       query: {
         pageNum: pageNum.value,
       },
     });
-    const {list: dataList, total: dataTotal} = currListConfig.value.getResponse(data)
-    // console.log(dataList, dataTotal, 'total==??', props.listType)
+    await handleResponse(res)
+    const {dataList, totalCount} = res.data
+    // console.log(dataList, totalCount, 'total==??', props.listType)
     const _list = dataList.map((o) => ({
       ...o,
       avatar: o.attentionIdDictMap.avatar,
@@ -125,7 +96,7 @@ const onLoad = async () => {
     }))
 
     list.value.push(..._list);
-    total.value = dataTotal;
+    total.value = totalCount;
     isFinished.value = list.value.length >= total.value;
   } catch (e) {
 

+ 66 - 74
src/pages/profile/index.vue

@@ -1,29 +1,26 @@
 <template>
   <div class="profile-page bg-[#f8f8f8]">
     <div
-      class="pt-66 pb-14 px-26 relative w-full bg-[url('~/assets/img/profile/profile_banner.png')] bg-no-repeat bg-cover"
-    >
-      <NuxtLink to="/chat/sweep" class="absolute top-17 right-14">
-        <img src="~/assets/img/profile/pofile_qr.png" height="33" width="33" alt="扫一扫" />
+        class="pt-66 pb-14 px-26 relative w-full bg-[url('~/assets/img/profile/profile_banner.png')] bg-no-repeat bg-cover">
+      <NuxtLink to="/scan" class="absolute top-17 right-14">
+        <img src="~/assets/img/profile/pofile_qr.png" height="33" width="33" alt="扫一扫"/>
       </NuxtLink>
-      <div class="w-335 left-20 top-66 right-20">
+      <div class="w-335 left-20 top-66 right-20 ">
         <NuxtLink to="/profile/userInfo">
           <div class="flex flex-col w-full text-black-3">
             <div class="flex flex-row items-center">
               <van-image
-                :src="userInfo.headImageUrl || defaultAvatar"
-                class="mr-9 shrink-0"
-                width="75px"
-                height="75px"
-                radius="37.5px"
+                  :src="userInfo.headImageUrl || defaultAvatar"
+                  class="mr-9 shrink-0"
+                  width="75px"
+                  height="75px"
+                  radius="37.5px"
               />
-              <div
-                class="text-2xl text-[#fff] font-bold text-ellipsis text-nowrap overflow-hidden ..."
-              >
+              <div class="text-2xl text-[#fff] font-bold text-ellipsis text-nowrap overflow-hidden ...">
                 {{ userInfo.showName }}
               </div>
             </div>
-            <div class="mt-24 text-base text-[#fff]">{{ userInfo.personalSign || '暂未填写' }}</div>
+            <div class="mt-24 text-base text-[#fff] ">{{ userInfo.personalSign }}</div>
           </div>
         </NuxtLink>
         <!--        <NuxtLink to="/profile/userInfo" class="flex items-center space-x-10">
@@ -45,20 +42,12 @@
       </div>
       <div class="mt-17 w-full grid grid-cols-4 gap-10">
         <template v-for="(tab, i) in tabList" :key="i">
-          <template v-if="tab.listType">
-            <NuxtLink :to="`/follow?listType=${tab.listType}`">
-              <div class="text-[#fff] text-base text-center">
-                <div class="font-semibold">{{ tab.numText }}</div>
-                <div class="text-sm">{{ tab.label }}</div>
-              </div>
-            </NuxtLink>
-          </template>
-          <template v-else>
-            <div class="text-[#fff] text-base text-center">
+          <NuxtLink :to=" tab.listType ? `/follow?listType=${tab.listType}` : null">
+            <div class="text-[#fff] text-base text-center ">
               <div class="font-semibold">{{ tab.numText }}</div>
               <div class="text-sm">{{ tab.label }}</div>
             </div>
-          </template>
+          </NuxtLink>
         </template>
       </div>
     </div>
@@ -66,12 +55,12 @@
       <div class="text-xl font-semibold ml-20">常用功能</div>
       <div class="grid grid-cols-4 gap-y-20 mt-20">
         <NuxtLink
-          class="flex flex-col items-center"
-          v-for="item in menuData"
-          :to="item.to"
-          :key="item.to"
+            class="flex flex-col items-center"
+            v-for="item in menuData"
+            :to="item.to"
+            :key="item.to"
         >
-          <img :src="item.icon" class="w-40 h-40" alt="" srcset="" />
+          <img :src="item.icon" class="w-40 h-40" alt="" srcset=""/>
           <div class="text-black-3 text-sm mt-5">{{ item.label }}</div>
         </NuxtLink>
       </div>
@@ -80,38 +69,41 @@
 </template>
 
 <script setup>
-import defaultAvatar from '~/assets/img/default_avatar.png'
-import profile_travel_order from '~/assets/img/profile/profile_travel_order.png'
-import profile_labour_order from '~/assets/img/profile/profile_labour_order.png'
-import profile_travel_note from '~/assets/img/profile/profile_travel_note.png'
-import profile_colection from '~/assets/img/profile/profile_colection.png'
-import profile_car_order from '~/assets/img/profile/profile_car_order.png'
-import profile_visa_order from '~/assets/img/profile/profile_visa_order.png'
-import profile_my_comment from '~/assets/img/profile/profile_my_comment.png'
-import { formatNumber } from '~/utils/formatNumber'
+import defaultAvatar from "~/assets/img/default_avatar.png";
+import profile_travel_order from "~/assets/img/profile/profile_travel_order.png";
+import profile_labour_order from "~/assets/img/profile/profile_labour_order.png";
+import profile_travel_note from "~/assets/img/profile/profile_travel_note.png";
+import profile_colection from "~/assets/img/profile/profile_colection.png";
+import profile_car_order from "~/assets/img/profile/profile_car_order.png";
+import profile_visa_order from "~/assets/img/profile/profile_visa_order.png";
+import profile_my_comment from "~/assets/img/profile/profile_my_comment.png";
 
-const userInfoStore = useUserInfoStore()
-const { userInfo } = storeToRefs(userInfoStore)
+const userInfoStore = useUserInfoStore();
+const {userInfo} = storeToRefs(userInfoStore);
 
 const tabList = ref([
-  { label: '互关', listType: 'friend', numText: '0' },
-  { label: '关注', listType: 'follow', numText: '0' },
-  { label: '粉丝', listType: 'fans', numText: '0' },
-  { label: '获赞', listType: null, numText: '0' }
+  {label: '互关', listType: 'friend', numText: '0'},
+  {label: '关注', listType: 'follow', numText: '0'},
+  {label: '粉丝', listType: 'fans', numText: '0'},
+  {label: '获赞', listType: null, numText: '0'},
 ])
 const getData = async () => {
   try {
-    const [{ data: friendsData }, { data: concernData }, { data: fansData }, { data: likeData }] =
-      await Promise.all([
-        request('/website/tourism/fans/getFriendsCount'),
-        request('/website/tourism/fans/getMyConcern'),
-        request('/website/tourism/fans/getMyFansCount'),
-        request('/website/tourism/fans/getMylikeCount')
-      ])
-    tabList.value[0].numText = formatNumber(friendsData)
-    tabList.value[1].numText = formatNumber(concernData)
-    tabList.value[2].numText = formatNumber(fansData)
-    tabList.value[3].numText = formatNumber(likeData)
+    const [
+      {data: friendsData},
+      {data: concernData},
+      {data: fansData},
+      {data: likeData},
+    ] = await Promise.all([
+      request('/website/tourism/fans/getFriendsCount'),
+      request('/website/tourism/fans/getMyConcern'),
+      request('/website/tourism/fans/getMyFansCount'),
+      request('/website/tourism/fans/getMylikeCount'),
+    ])
+    tabList.value[0].numText = formatNumber(friendsData);
+    tabList.value[1].numText = formatNumber(concernData);
+    tabList.value[2].numText = formatNumber(fansData);
+    tabList.value[3].numText = formatNumber(likeData);
   } catch (e) {
   } finally {
   }
@@ -120,45 +112,45 @@ const getData = async () => {
 const menuData = [
   {
     icon: profile_travel_order,
-    label: '旅游订单',
-    to: '/profile/travel-orders'
+    label: "旅游订单",
+    to: "/profile/travel-orders",
   },
   {
     icon: profile_visa_order,
-    label: '签证订单',
-    to: '/profile/visa-orders'
+    label: "签证订单",
+    to: "/profile/visa-orders",
   },
   {
     icon: profile_car_order,
-    label: '包车订单',
-    to: '/profile/car-orders'
+    label: "包车订单",
+    to: "/profile/car-orders",
   },
   {
     icon: profile_travel_note,
-    label: '我的游记',
-    to: '/profile/notes'
+    label: "我的游记",
+    to: "/profile/notes",
   },
   {
     icon: profile_colection,
-    label: '我的收藏',
-    to: '/profile/collection'
+    label: "我的收藏",
+    to: "/profile/collection",
   },
-  // {
-  //   icon: profile_my_comment,
-  //   label: "我的评论",
-  //   to: "/profile/my-comment",
-  // },
+  /*  {
+      icon: profile_my_comment,
+      label: "我的评论",
+      to: "/profile/my-comment",
+    },*/
   {
     icon: profile_my_comment,
     label: '我的消息',
     to: '/profile/my-news'
   }
-]
+];
 
 onMounted(() => {
   getData()
-  userInfoStore.getUserInfo()
-})
+  userInfoStore.getUserInfo();
+});
 </script>
 
 <style lang="scss" scoped>

+ 191 - 0
src/pages/scan/index.vue

@@ -0,0 +1,191 @@
+<script setup>
+definePageMeta({
+  layout: 'scan'
+})
+
+import {Html5Qrcode, Html5QrcodeSupportedFormats} from "html5-qrcode";
+
+const router = useRouter()
+
+const emit = defineEmits('oSuccess', 'onError')
+
+let scanInstance = null; // 扫码实例
+let results = ref(null); // 扫码结果
+
+const initScanInstance = () => {
+  if (!scanInstance) {
+    // reader放置扫码功能的元素ID
+    scanInstance = new Html5Qrcode('qr-reader', {
+      formatsToSupport: [
+        Html5QrcodeSupportedFormats.QR_CODE,
+      ],
+    })
+  }
+}
+const openQrcode = async () => {
+  Html5Qrcode.getCameras()
+      .then(devices => {
+        if (devices && devices.length) {// 当前环境下能识别出摄像头,并且摄像头的数据可能不止一个
+          initScanInstance()
+          scanInstance.start(
+              {facingMode: "environment"},
+              {
+                focusMode: 'continuous',
+                fps: 1, // 可选,每n秒帧扫描一次
+                qrbox: { // 扫描的UI框
+                  width: 250,
+                  height: 250
+                },
+                videoConstraints: {
+                  // width: 375,
+                  // height: (window.visualViewport.height - 50),
+                  aspectRatio: window.visualViewport.height / window.visualViewport.width,
+                  facingMode: "environment",
+                }
+              },
+              (decodedText, decodedResult) => {
+                showToast('识别成功')
+                handleSuccess({decodedText, decodedResult})
+              },
+              (errorMessage, error) => {
+                closeQrcode(errorMessage)
+              }
+          )
+        }
+      })
+      .catch((err) => {
+        // 错误信息处理仅供参考,具体情况看输出!!!
+        let errorStr = ''
+        if (typeof err === "string") {
+          errorStr = err
+        } else {
+          if (err.name === "NotAllowedError")
+            errorStr = "您需要授予相机访问权限"
+          if (err.name === "NotFoundError") {
+            errorStr = "未检测到摄像头"
+          }
+          if (err.name === "NotSupportedError")
+            errorStr = "摄像头访问只支持在安全的上下文中,如https或localhost"
+          if (err.name === "NotReadableError") errorStr = "摄像头被占用"
+          if (err.name === "OverconstrainedError")
+            errorStr = "安装摄像头不合适"
+          if (err.name === "StreamApiNotSupportedError")
+            errorStr = "此浏览器不支持流API"
+        }
+        showToast(errorStr)
+      })
+}
+const scanLoadImg = (e) => {
+  try {
+    initScanInstance()
+    scanInstance.scanFile(e.file, false)
+        .then(result => {
+          // 二维码结果
+          console.log(result, '上传扫码成功')
+          handleSuccess(result)
+        })
+        .catch(err => {
+          console.error(err, '上传扫码失败')
+        })
+  } catch (e) {
+    console.error(e, '失败')
+  }
+}
+
+const getResults = (result) => {
+
+  if (!result) {
+    return {success: false, groupId: null, overTime: false}
+  }
+
+  const url = new URL(result);
+  const urlParams = new URLSearchParams(url.search);
+
+  const hasGroupId = urlParams.has('groupId');
+  const hasTime = urlParams.has('time');
+  console.log(url.search, hasGroupId, hasTime, 'hasTime')
+  if (!hasGroupId || !hasTime) {
+    return {success: false, groupId: null, overTime: false}
+  }
+
+  const givenDate = new Date(urlParams.get('time'));
+  const currentDate = new Date();
+  const givenDateCst = new Date(givenDate.getTime() + (8 * 60 * 60 * 1000));
+  const currentDateCst = new Date(currentDate.getTime() + (8 * 60 * 60 * 1000));
+
+  const diffInMilliseconds = currentDateCst - givenDateCst;
+  const sevenDaysInMilliseconds = 7 * 24 * 60 * 60 * 1000;
+
+  return {
+    success: true,
+    groupId: urlParams.get('groupId'),
+    overTime: diffInMilliseconds > sevenDaysInMilliseconds
+  }
+}
+
+const handleSuccess = (result) => {
+  results.value = result
+  router.replace({
+    path: '/chat/qr-results',
+    query: result ? getResults(result) : result
+  })
+}
+
+const closeQrcode = () => {
+  if (scanInstance && scanInstance.isScanning) scanInstance.stop()
+  router.back()
+}
+
+onMounted(async () => {
+  initScanInstance()
+  await nextTick()
+  await openQrcode()
+})
+onUnmounted(() => {
+  if (scanInstance && scanInstance.isScanning) scanInstance.stop()
+})
+
+</script>
+
+<template>
+  <div>
+    <div v-if="false" class="h-200">
+      {{ results }}
+    </div>
+    <div class="overlay">
+      <div class="absolute w-full z-30 flex flex-row items-center justify-center p-12">
+        <div class="absolute left-12 font-bold" @click="closeQrcode">
+          <span class="iconfont icon-left text-white"></span>
+        </div>
+        <div class="text-base text-[#fff] font-bold">
+          扫描二维码
+        </div>
+      </div>
+      <div class="relative qr-reader z-20" id="qr-reader"></div>
+      <p class="absolute w-full p-12 text-center bottom-100 z-30 text-sm text-[#fff]">请将二维码对准扫码框中心</p>
+      <div id="qr-code-file"
+           class="absolute w-54 h-54 grid place-items-center bottom-40 z-30 rounded-full bg-[#000]/80 text-white left-1/2 -translate-x-1/2">
+        <van-uploader
+            :preview-image="false"
+            :after-read="scanLoadImg"
+            accept="image/*"
+            :multiple="false"
+            :max-count="1"
+        ><img src="~/assets/img/scan/pic.png" height="32" width="32" alt=""/>
+        </van-uploader>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.overlay {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  z-index: 999999;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.7);
+}
+</style>

+ 24 - 0
src/pages/scan/scan-result/index.vue

@@ -0,0 +1,24 @@
+<script setup>
+definePageMeta({
+  layout: 'scan'
+})
+const router = useRouter()
+const route = useRoute()
+console.log(route, 'route')
+</script>
+
+<template>
+<div>
+  <van-nav-bar
+      title="扫码结果"
+      left-arrow
+      @click-left="router.back()"
+  />
+
+  {{route.query}}
+</div>
+</template>
+
+<style scoped lang="scss">
+
+</style>

+ 0 - 31
src/utils/formatNumber.js

@@ -1,31 +0,0 @@
-import accounting from 'accounting'
-
-export const formatNumber = (n) => {
-  let num = Number(n ?? '')
-
-  // 如果数字大于或等于10,000万,则显示为9999w+
-  if (num >= 10000 * 10000) {
-    return '9999万+'
-  }
-  // 如果数字大于或等于1万,则转换为以“万”为单位,不四舍五入,最多保留两位小数
-  else if (num >= 10000) {
-    let w = Math.floor(num / 10000) // 取整,避免四舍五入
-    let remainder = num % 10000
-
-    // 计算小数部分并限制为最多两位
-    let decimalPart = ''
-    if (remainder > 0) {
-      // 将余数转换为两位小数,但不进行四舍五入
-      decimalPart = ('.' + Math.floor(remainder / 100)).slice(0, 2)
-      // 移除结尾的0,如果有的话
-      decimalPart = decimalPart.replace(/\.?0+$/, '')
-    }
-
-    return `${w}${decimalPart}万`
-  }
-
-  // 对于小于1万的数字直接输出
-  else {
-    return accounting.formatNumber(num)
-  }
-}

+ 44 - 1
src/utils/index.js

@@ -1,3 +1,5 @@
+import accounting from "accounting";
+
 const setIntervalImmediately = (fn, duration) =>
   setInterval((() => (fn(), fn))(), duration);
 
@@ -7,4 +9,45 @@ function formatImgSrc(srcArr) {
   }
   return "";
 }
-export { setIntervalImmediately, formatImgSrc };
+
+const isEmptyValue = (value) => {
+    if (value == null) return true
+
+    if (Array.isArray(value) && value.length === 0) return true
+
+    if (typeof value === 'object' && Object.keys(value).length === 0) return true
+
+    return false
+}
+
+const formatNumber = (n) => {
+    let num = Number(n ?? '')
+
+    // 如果数字大于或等于10,000万,则显示为9999w+
+    if (num >= 10000 * 10000) {
+        return '9999万+'
+    }
+    // 如果数字大于或等于1万,则转换为以“万”为单位,不四舍五入,最多保留两位小数
+    else if (num >= 10000) {
+        let w = Math.floor(num / 10000) // 取整,避免四舍五入
+        let remainder = num % 10000
+
+        // 计算小数部分并限制为最多两位
+        let decimalPart = ''
+        if (remainder > 0) {
+            // 将余数转换为两位小数,但不进行四舍五入
+            decimalPart = ('.' + Math.floor(remainder / 100)).slice(0, 2)
+            // 移除结尾的0,如果有的话
+            decimalPart = decimalPart.replace(/\.?0+$/, '')
+        }
+
+        return `${w}${decimalPart}万`
+    }
+
+    // 对于小于1万的数字直接输出
+    else {
+        return accounting.formatNumber(num)
+    }
+}
+
+export { setIntervalImmediately, formatImgSrc, formatNumber, isEmptyValue };

+ 18 - 0
src/utils/request.js

@@ -88,3 +88,21 @@ const showErrorMessage = (msg) => {
   // const { message } = useMessage()
   // message.error(msg)
 };
+
+export const handleResponse = (response, isNeedData = true) => {
+  return new Promise((resolve, reject) => {
+    const success = response.success
+    switch (success) {
+      case true: {
+        if (isNeedData) {
+          if (response.data && !isEmptyValue(response.data)) return resolve()
+          return reject(response)
+        }
+        return resolve()
+      }
+      default: {
+        return reject(response)
+      }
+    }
+  })
+}