Browse Source

左侧统计

CzRger 1 year ago
parent
commit
9a3213f6d6

+ 3 - 3
package.json

@@ -9,17 +9,17 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@turf/turf": "^6.5.0",
     "@types/node": "^20.1.0",
     "animate.css": "^4.1.1",
     "axios": "^1.3.4",
     "element-plus": "^2.3.1",
+    "ol": "^6.5.0",
     "sass": "^1.60.0",
     "uuid": "^9.0.0",
     "vue": "^3.2.47",
     "vue-router": "^4.1.6",
-    "vuex": "^4.1.0",
-    "ol": "^6.5.0",
-    "@turf/turf": "^6.5.0"
+    "vuex": "^4.1.0"
   },
   "devDependencies": {
     "@types/uuid": "^9.0.2",

+ 47 - 0
src/components/vue-count-to/requestAnimationFrame.js

@@ -0,0 +1,47 @@
+let lastTime = 0
+const prefixes = 'webkit moz ms o'.split(' ') // 各浏览器前缀
+
+let requestAnimationFrame
+let cancelAnimationFrame
+
+// 判断是否是服务器环境
+const isServer = typeof window === 'undefined'
+if (isServer) {
+  requestAnimationFrame = function() {
+    return
+  }
+  cancelAnimationFrame = function() {
+    return
+  }
+} else {
+  requestAnimationFrame = window.requestAnimationFrame
+  cancelAnimationFrame = window.cancelAnimationFrame
+  let prefix
+  // 通过遍历各浏览器前缀,来得到requestAnimationFrame和cancelAnimationFrame在当前浏览器的实现形式
+  for (let i = 0; i < prefixes.length; i++) {
+    if (requestAnimationFrame && cancelAnimationFrame) { break }
+    prefix = prefixes[i]
+    requestAnimationFrame = requestAnimationFrame || window[prefix + 'RequestAnimationFrame']
+    cancelAnimationFrame = cancelAnimationFrame || window[prefix + 'CancelAnimationFrame'] || window[prefix + 'CancelRequestAnimationFrame']
+  }
+
+  // 如果当前浏览器不支持requestAnimationFrame和cancelAnimationFrame,则会退到setTimeout
+  if (!requestAnimationFrame || !cancelAnimationFrame) {
+    requestAnimationFrame = function(callback) {
+      const currTime = new Date().getTime()
+      // 为了使setTimteout的尽可能的接近每秒60帧的效果
+      const timeToCall = Math.max(0, 16 - (currTime - lastTime))
+      const id = window.setTimeout(() => {
+        callback(currTime + timeToCall)
+      }, timeToCall)
+      lastTime = currTime + timeToCall
+      return id
+    }
+
+    cancelAnimationFrame = function(id) {
+      window.clearTimeout(id)
+    }
+  }
+}
+
+export { requestAnimationFrame, cancelAnimationFrame }

+ 202 - 0
src/components/vue-count-to/vue-countTo.vue

@@ -0,0 +1,202 @@
+<template>
+  {{ state.displayValue }}
+</template>
+
+<script setup>  // vue3.2新的语法糖, 编写代码更加简洁高效
+import { onMounted, onUnmounted, reactive } from "@vue/runtime-core";
+import { watch, computed } from 'vue';
+import { requestAnimationFrame, cancelAnimationFrame } from './requestAnimationFrame.js'
+// 定义父组件传递的参数
+const props = defineProps({
+  startVal: {
+    type: Number,
+    required: false,
+    default: 0
+  },
+  endVal: {
+    type: Number,
+    required: false,
+    default: 2021
+  },
+  duration: {
+    type: Number,
+    required: false,
+    default: 5000
+  },
+  autoPlay: {
+    type: Boolean,
+    required: false,
+    default: true
+  },
+  decimals: {
+    type: Number,
+    required: false,
+    default: 0,
+    validator (value) {
+      return value >= 0
+    }
+  },
+  decimal: {
+    type: String,
+    required: false,
+    default: '.'
+  },
+  separator: {
+    type: String,
+    required: false,
+    default: ','
+  },
+  prefix: {
+    type: String,
+    required: false,
+    default: ''
+  },
+  suffix: {
+    type: String,
+    required: false,
+    default: ''
+  },
+  useEasing: {
+    type: Boolean,
+    required: false,
+    default: true
+  },
+  easingFn: {
+    type: Function,
+    default(t, b, c, d) {
+      return c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b;
+    }
+  }
+})
+
+const isNumber = (val) => {
+  return !isNaN(parseFloat(val))
+}
+
+// 格式化数据,返回想要展示的数据格式
+const formatNumber = (val) => {
+  val = val.toFixed(props.default)
+  val += ''
+  const x = val.split('.')
+  let x1 = x[0]
+  const x2 = x.length > 1 ? props.decimal + x[1] : ''
+  const rgx = /(\d+)(\d{3})/
+  if (props.separator && !isNumber(props.separator)) {
+    while (rgx.test(x1)) {
+      x1 = x1.replace(rgx, '$1' + props.separator + '$2')
+    }
+  }
+  return props.prefix + x1 + x2 + props.suffix
+}
+
+// 相当于vue2中的data中所定义的变量部分
+const state = reactive({
+  localStart: props.startVal,
+  displayValue: formatNumber(props.startVal),
+  printVal: null,
+  paused: false,
+  localDuration: props.duration,
+  startTime: null,
+  timestamp: null,
+  remaining: null,
+  rAF: null
+})
+
+// 定义一个计算属性,当开始数字大于结束数字时返回true
+const stopCount = computed(() => {
+  return props.startVal > props.endVal
+})
+// 定义父组件的自定义事件,子组件以触发父组件的自定义事件
+const emits = defineEmits(['onMountedcallback', 'callback'])
+
+const startCount = () => {
+  state.localStart = props.startVal
+  state.startTime = null
+  state.localDuration = props.duration
+  state.paused = false
+  state.rAF = requestAnimationFrame(count)
+}
+
+watch(() => props.startVal, () => {
+  if (props.autoPlay) {
+    startCount()
+  }
+})
+
+watch(() => props.endVal, () => {
+  if (props.autoPlay) {
+    startCount()
+  }
+})
+// dom挂在完成后执行一些操作
+onMounted(() => {
+  if (props.autoPlay) {
+    startCount()
+  }
+  emits('onMountedcallback')
+})
+// 暂停计数
+const pause = () => {
+  cancelAnimationFrame(state.rAF)
+}
+// 恢复计数
+const resume = () => {
+  state.startTime = null
+  state.localDuration = +state.remaining
+  state.localStart = +state.printVal
+  requestAnimationFrame(count)
+}
+
+const pauseResume = () => {
+  if (state.paused) {
+    resume()
+    state.paused = false
+  } else {
+    pause()
+    state.paused = true
+  }
+}
+
+const reset = () => {
+  state.startTime = null
+  cancelAnimationFrame(state.rAF)
+  state.displayValue = formatNumber(props.startVal)
+}
+
+const count = (timestamp) => {
+  if (!state.startTime) state.startTime = timestamp
+  state.timestamp = timestamp
+  const progress = timestamp - state.startTime
+  state.remaining = state.localDuration - progress
+  // 是否使用速度变化曲线
+  if (props.useEasing) {
+    if (stopCount.value) {
+      state.printVal = state.localStart - props.easingFn(progress, 0, state.localStart - props.endVal, state.localDuration)
+    } else {
+      state.printVal = props.easingFn(progress, state.localStart, props.endVal - state.localStart, state.localDuration)
+    }
+  } else {
+    if (stopCount.value) {
+      state.printVal = state.localStart - ((state.localStart - props.endVal) * (progress / state.localDuration))
+    } else {
+      state.printVal = state.localStart + (props.endVal - state.localStart) * (progress / state.localDuration)
+    }
+  }
+  if (stopCount.value) {
+    state.printVal = state.printVal < props.endVal ? props.endVal : state.printVal
+  } else {
+    state.printVal = state.printVal > props.endVal ? props.endVal : state.printVal
+  }
+
+  state.displayValue = formatNumber(state.printVal)
+  if (progress < state.localDuration) {
+    state.rAF = requestAnimationFrame(count)
+  } else {
+    emits('callback')
+  }
+}
+// 组件销毁时取消动画
+onUnmounted(() => {
+  cancelAnimationFrame(state.rAF)
+})
+</script>

+ 6 - 0
src/utils/util.ts

@@ -127,3 +127,9 @@ export const formatUrlByInfo = (url: string) => {
   })
   return _url
 }
+
+
+//生成从minNum到maxNum的随机数
+export  const randomNum = (minNum: number,maxNum: number) => {
+  return parseInt(String(Math.random() * (maxNum - minNum + 1) + minNum),10);
+}

BIN
src/views/screen/img/left_bg-1.png


BIN
src/views/screen/img/left_bg-2.png


BIN
src/views/screen/img/left_bg-3.png


BIN
src/views/screen/img/left_icon-1.png


BIN
src/views/screen/img/left_icon-2.png


BIN
src/views/screen/img/left_icon-3.png


BIN
src/views/screen/img/left_icon-4.png


BIN
src/views/screen/img/left_icon-5.png


BIN
src/views/screen/img/left_icon-6.png


+ 155 - 3
src/views/screen/index.vue

@@ -64,6 +64,49 @@
         </div>
       </template>
     </div>
+    <div class="screen-left">
+      <div class="sl-head">
+        <img src="./img/left_icon-1.png"/>
+        <div>数据调用和使用</div>
+      </div>
+      <div class="sl-content">
+        <div class="slc-item">
+          <div class="slc-item-icon"><img src="./img/left_icon-2.png"/></div>
+          <div class="slc-item-title"><span>实时用户数</span></div>
+          <div  class="slc-item-count">
+            <CountTo :startVal="leftValue.ssyhs.startVal" :endVal="leftValue.ssyhs.endVal" :duration="1500"></CountTo>
+          </div>
+        </div>
+        <div class="slc-item">
+          <div class="slc-item-icon"><img src="./img/left_icon-3.png"/></div>
+          <div class="slc-item-title"><span>流动用户数</span></div>
+          <div  class="slc-item-count">
+            <CountTo :startVal="leftValue.ldyhs.startVal" :endVal="leftValue.ldyhs.endVal" :duration="1500"></CountTo>
+          </div>
+        </div>
+        <div class="slc-item">
+          <div class="slc-item-icon"><img src="./img/left_icon-4.png"/></div>
+          <div class="slc-item-title"><span>旅游用户数</span></div>
+          <div  class="slc-item-count">
+            <CountTo :startVal="leftValue.lyyhs.startVal" :endVal="leftValue.lyyhs.endVal" :duration="1500"></CountTo>
+          </div>
+        </div>
+        <div class="slc-item">
+          <div class="slc-item-icon"><img src="./img/left_icon-5.png"/></div>
+          <div class="slc-item-title"><span>过境用户数</span></div>
+          <div  class="slc-item-count">
+            <CountTo :startVal="leftValue.gjyhs.startVal" :endVal="leftValue.gjyhs.endVal" :duration="1500"></CountTo>
+          </div>
+        </div>
+        <div class="slc-item">
+          <div class="slc-item-icon"><img src="./img/left_icon-6.png"/></div>
+          <div class="slc-item-title"><span>境外用户数</span></div>
+          <div  class="slc-item-count">
+            <CountTo :startVal="leftValue.jwyhs.startVal" :endVal="leftValue.jwyhs.endVal" :duration="1500"></CountTo>
+          </div>
+        </div>
+      </div>
+    </div>
     <div v-show="showInfo" class="info-dialog" ref="ref_infoDialog">
       <template v-for="(item, index) in InfoMapper">
         <div class="info-dialog-block animate__animated">
@@ -113,10 +156,11 @@ import {
 import {useStore} from 'vuex'
 import {bgImgMapper, infoMapper, logoIcon} from '../common/static'
 import MessageCom from './message/index.vue'
+import CountTo from '@/components/vue-count-to/vue-countTo.vue';
 
 export default defineComponent({
   name: 'App',
-  components: {MessageCom},
+  components: {MessageCom, CountTo},
   setup() {
     const store = useStore()
     const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
@@ -131,7 +175,29 @@ export default defineComponent({
       InfoMapper: infoMapper,
       infoInput: '',
       LogoIcon: logoIcon,
-      showMessage: false
+      showMessage: false,
+      leftValue: {
+        ssyhs: {
+          startVal: 0,
+          endVal: 0
+        },
+        ldyhs: {
+          startVal: 0,
+          endVal: 0
+        },
+        lyyhs: {
+          startVal: 0,
+          endVal: 0
+        },
+        gjyhs: {
+          startVal: 0,
+          endVal: 0
+        },
+        jwyhs: {
+          startVal: 0,
+          endVal: 0
+        },
+      }
     });
     const switchInfo = (ind) => {
       if (ind === state.infoIndex) {
@@ -158,7 +224,19 @@ export default defineComponent({
     const toHref = (val: { href: string | URL | undefined }) => {
       window.open(that.$util.formatUrlByInfo(val.href), '_blank')
     }
+    const initLeftCount = () => {
+      const handle = () => {
+        Object.entries(state.leftValue).forEach(([k, v], i) => {
+          v.endVal = that.$util.randomNum(0, 99999999)
+        })
+      }
+      handle()
+      setInterval(() => {
+        handle()
+      }, 5000)
+    }
     onMounted(() => {
+      initLeftCount()
       setInterval(() => {
         const len = BgImgMapper.length - 1
         if (state.bgImgIndex === len) {
@@ -220,13 +298,14 @@ export default defineComponent({
         animation-duration: 5s;
       }
     }
+    $screenLogoHeight: 147px;
     .screen-logo {
       z-index: 2;
       position: absolute;
       top: 0;
       left: 0;
       width: 1085px;
-      height: 147px;
+      height: $screenLogoHeight;
       background-image: url("../common/img/logo-bg.png");
       display: flex;
       align-items: center;
@@ -399,6 +478,79 @@ export default defineComponent({
         }
       }
     }
+    $screenLeftHeight: 511px;
+    .screen-left {
+      height: $screenLeftHeight;
+      position: absolute;
+      z-index: 2;
+      left: 27px;
+      top: calc((100% - #{$screenLogoHeight} - #{$screenHrefHeight} - #{$screenLeftHeight}) / 2 + #{$screenLogoHeight});
+      .sl-head {
+        background: url('./img/left_bg-1.png');
+        width: 401px;
+        height: 46px;
+        display: flex;
+        align-items: center;
+        >img {
+          margin: 0 10px 0 26px;
+        }
+        >div {
+          font-size: 26px;
+          font-family: Microsoft YaHei;
+          font-weight: bold;
+          color: #FFFFFF;
+          background: linear-gradient(0deg, #FFFFFF 0%, #c1eaff 100%);
+          -webkit-background-clip: text;
+          -webkit-text-fill-color: transparent;
+        }
+      }
+      .sl-content {
+        .slc-item {
+          margin-top: 10px;
+          background: url("./img/left_bg-2.png");
+          width: 404px;
+          height: 83px;
+          position: relative;
+          .slc-item-icon {
+            position: absolute;
+            left: 15px;
+          }
+          .slc-item-title {
+            background: url("./img/left_bg-3.png");
+            width: 276px;
+            height: 24px;
+            top: 12px;
+            left: 78px;
+            display: flex;
+            align-items: center;
+            position: absolute;
+            padding-left: 25px;
+            box-sizing: border-box;
+            >span {
+              width: 100px;
+              font-size: 18px;
+              font-family: Microsoft YaHei;
+              font-weight: 400;
+              font-style: italic;
+              color: #D1D6DF;
+              line-height: 32px;
+              background: linear-gradient(0deg, #ACDDFF 0%, #FFFFFF 100%);
+              -webkit-background-clip: text;
+              -webkit-text-fill-color: transparent;
+            }
+          }
+          .slc-item-count {
+            position: absolute;
+            right: 106px;
+            bottom: 12px;
+            font-size: 30px;
+            font-family: fantasy;
+            font-weight: 400;
+            color: #32DCFB;
+          }
+        }
+      }
+    }
     $infoItemWidth: 350px;
     $infoItemActiveWidth: 1425px;
     $infoItemSplit: 10px;

+ 2 - 2
vite.config.ts

@@ -21,7 +21,7 @@ export default defineConfig({
     proxy: {
       '/api/': {
         // target: 'http://localhost:8080/',
-        target: 'http://127.0.0.1:3333/',
+        target: 'http://127.0.0.1:3001/',
         // target: 'http://10.110.35.47/',
         changeOrigin: true,
         rewrite: path => {
@@ -30,7 +30,7 @@ export default defineConfig({
       },
       '/api-gateway/': {
         // target: 'http://localhost:8080/',
-        target: 'http://127.0.0.1:4444/',
+        target: 'http://127.0.0.1:3002/',
         // target: 'http://10.110.32.62/',
         changeOrigin: true,
         rewrite: path => {

+ 5 - 0
yarn.lock

@@ -2280,6 +2280,11 @@ vite@^4.2.0:
   optionalDependencies:
     fsevents "~2.3.2"
 
+vue-count-to@^1.0.13:
+  version "1.0.13"
+  resolved "https://registry.npmmirror.com/vue-count-to/-/vue-count-to-1.0.13.tgz#3e7573ea6e64c2b2972f64e0a2ab2e23c7590ff3"
+  integrity sha512-6R4OVBVNtQTlcbXu6SJ8ENR35M2/CdWt3Jmv57jOUM+1ojiFmjVGvZPH8DfHpMDSA+ITs+EW5V6qthADxeyYOQ==
+
 vue-demi@*:
   version "0.14.5"
   resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.5.tgz"