Procházet zdrojové kódy

新增StatisticCard统计组件;新增AnimationNumber组件;更新首页数据总览数据获取方式,现在会带参请求;更新总览数据的自定义指标选项获取方式,现在根据后端返回数据生成;更新分析首页分析模块数据请求带参方式,现在会直接根据选中值带参;

fxs před 7 měsíci
rodič
revize
e2868fe951

+ 2 - 0
components.d.ts

@@ -7,6 +7,7 @@ export {}
 /* prettier-ignore */
 declare module 'vue' {
   export interface GlobalComponents {
+    AnimationNumber: typeof import('./src/components/animationNumber/AnimationNumber.vue')['default']
     CIcon: typeof import('./src/components/cIcon/cIcon.vue')['default']
     CSelect: typeof import('./src/components/form/CSelect.vue')['default']
     CustomIndicatorDialog: typeof import('./src/components/dialog/customIndicatorDialog.vue')['default']
@@ -46,6 +47,7 @@ declare module 'vue' {
     MyInput: typeof import('./src/components/form/MyInput.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    StatisticCard: typeof import('./src/components/statisticCard/StatisticCard.vue')['default']
     Table: typeof import('./src/components/table/Table.vue')['default']
     TableQueryForm: typeof import('./src/components/table/TableQueryForm.vue')['default']
     TimeLineChart: typeof import('./src/components/echarts/TimeLineChart.vue')['default']

+ 130 - 18
package-lock.json

@@ -1458,10 +1458,13 @@
       }
     },
     "node_modules/@types/web-bluetooth": {
-      "version": "0.0.16",
-      "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
-      "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
-      "license": "MIT"
+      "version": "0.0.20",
+      "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+      "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "peer": true
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
       "version": "8.9.0",
@@ -1887,15 +1890,18 @@
       "license": "MIT"
     },
     "node_modules/@vueuse/core": {
-      "version": "9.13.0",
-      "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz",
-      "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
+      "version": "11.2.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-11.2.0.tgz",
+      "integrity": "sha512-JIUwRcOqOWzcdu1dGlfW04kaJhW3EXnnjJJfLTtddJanymTL7lF1C0+dVVZ/siLfc73mWn+cGP1PE1PKPruRSA==",
+      "dev": true,
       "license": "MIT",
+      "optional": true,
+      "peer": true,
       "dependencies": {
-        "@types/web-bluetooth": "^0.0.16",
-        "@vueuse/metadata": "9.13.0",
-        "@vueuse/shared": "9.13.0",
-        "vue-demi": "*"
+        "@types/web-bluetooth": "^0.0.20",
+        "@vueuse/metadata": "11.2.0",
+        "@vueuse/shared": "11.2.0",
+        "vue-demi": ">=0.14.10"
       },
       "funding": {
         "url": "https://github.com/sponsors/antfu"
@@ -1905,8 +1911,11 @@
       "version": "0.14.10",
       "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
       "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "dev": true,
       "hasInstallScript": true,
       "license": "MIT",
+      "optional": true,
+      "peer": true,
       "bin": {
         "vue-demi-fix": "bin/vue-demi-fix.js",
         "vue-demi-switch": "bin/vue-demi-switch.js"
@@ -1928,21 +1937,27 @@
       }
     },
     "node_modules/@vueuse/metadata": {
-      "version": "9.13.0",
-      "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz",
-      "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
+      "version": "11.2.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-11.2.0.tgz",
+      "integrity": "sha512-L0ZmtRmNx+ZW95DmrgD6vn484gSpVeRbgpWevFKXwqqQxW9hnSi2Ppuh2BzMjnbv4aJRiIw8tQatXT9uOB23dQ==",
+      "dev": true,
       "license": "MIT",
+      "optional": true,
+      "peer": true,
       "funding": {
         "url": "https://github.com/sponsors/antfu"
       }
     },
     "node_modules/@vueuse/shared": {
-      "version": "9.13.0",
-      "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz",
-      "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
+      "version": "11.2.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-11.2.0.tgz",
+      "integrity": "sha512-VxFjie0EanOudYSgMErxXfq6fo8vhr5ICI+BuE3I9FnX7ePllEsVrRQ7O6Q1TLgApeLuPKcHQxAXpP+KnlrJsg==",
+      "dev": true,
       "license": "MIT",
+      "optional": true,
+      "peer": true,
       "dependencies": {
-        "vue-demi": "*"
+        "vue-demi": ">=0.14.10"
       },
       "funding": {
         "url": "https://github.com/sponsors/antfu"
@@ -1952,8 +1967,11 @@
       "version": "0.14.10",
       "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
       "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "dev": true,
       "hasInstallScript": true,
       "license": "MIT",
+      "optional": true,
+      "peer": true,
       "bin": {
         "vue-demi-fix": "bin/vue-demi-fix.js",
         "vue-demi-switch": "bin/vue-demi-switch.js"
@@ -2441,6 +2459,100 @@
         "vue": "^3.2.0"
       }
     },
+    "node_modules/element-plus/node_modules/@types/web-bluetooth": {
+      "version": "0.0.16",
+      "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
+      "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
+      "license": "MIT"
+    },
+    "node_modules/element-plus/node_modules/@vueuse/core": {
+      "version": "9.13.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz",
+      "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.16",
+        "@vueuse/metadata": "9.13.0",
+        "@vueuse/shared": "9.13.0",
+        "vue-demi": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/element-plus/node_modules/@vueuse/core/node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/element-plus/node_modules/@vueuse/metadata": {
+      "version": "9.13.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz",
+      "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/element-plus/node_modules/@vueuse/shared": {
+      "version": "9.13.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz",
+      "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
+      "license": "MIT",
+      "dependencies": {
+        "vue-demi": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/element-plus/node_modules/@vueuse/shared/node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/emoji-regex": {
       "version": "8.0.0",
       "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",

+ 102 - 0
src/components/animationNumber/AnimationNumber.vue

@@ -0,0 +1,102 @@
+<script setup lang="ts">
+import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
+import { useNumber } from '@/hooks/useNumber'
+
+interface Props {
+  value: number
+  duration?: number
+  precision?: number // 数字精度
+  groupSeparator?: string // 千分位标识符
+  formatter?: (value: number) => string
+}
+
+const { formatterNumberGroup } = useNumber()
+
+const props = withDefaults(defineProps<Props>(), {
+  duration: 500,
+  value: 0,
+  precision: 2,
+  groupSeparator: ',',
+})
+
+const numberRef = ref<HTMLElement | null>(null) // 获取dom
+const nowNumber = ref<number>(props.value) // 现在的值
+const backupNumber = ref<number>(props.value) // 备份上一次的number
+const isVisible = ref<boolean>(false) // 当前是否可见
+
+// 使用 Intersection Observer API
+const observer = new IntersectionObserver(entries => {
+  entries.forEach(entry => {
+    isVisible.value = entry.isIntersecting
+  })
+})
+
+/**
+ * @description: 格式化过后的数字
+ * @return {*}
+ */
+const formatNumber = computed(() => {
+  if (props.formatter) {
+    return props.formatter(nowNumber.value)
+  }
+  return formatterNumberGroup(nowNumber.value, props.groupSeparator)
+})
+
+/**
+ * @description: 更新数字
+ * @param {*} nowNumber 当前数字
+ * @return {*}
+ */
+const updateNumber = (newVal: number) => {
+  // 如果不可见,直接赋值
+  if (isVisible.value) {
+    // 计算增长速度,同时防止出现负数要取绝对值
+    let speed = Math.abs(newVal - backupNumber.value) / props.duration
+    let startTime = Date.now() // 记录开始时间
+    if (newVal < backupNumber.value) speed = -speed // 看是减还是加
+    function update() {
+      // 按精度格式化
+      nowNumber.value = Number(
+        (nowNumber.value + speed).toFixed(props.precision),
+      )
+      // 记录结束时间
+      let nowTime = Date.now()
+      // 超过duration就直接赋值为新值
+      if (nowTime - startTime < props.duration) {
+        requestAnimationFrame(update)
+      } else {
+        nowNumber.value = Number(newVal.toFixed(props.precision))
+        return
+      }
+    }
+    update()
+  } else {
+    nowNumber.value = newVal
+  }
+}
+
+watch(
+  () => props.value,
+  (newVal: number, oldVal: number) => {
+    nowNumber.value = newVal
+    backupNumber.value = oldVal
+    updateNumber(newVal)
+    console.log(props.value)
+  },
+  { deep: true },
+)
+
+onMounted(() => {
+  observer.observe(numberRef.value!)
+})
+
+onUnmounted(() => {
+  if (numberRef.value) observer.unobserve(numberRef.value)
+})
+</script>
+
+<template>
+  <div ref="numberRef">{{ formatNumber }}</div>
+</template>
+
+<style scoped></style>

+ 0 - 2
src/components/form/CSelect.vue

@@ -26,9 +26,7 @@ const changeSelect = (newVal: any, name: string, item: CSelectItem) => {
   // 这里如果有图例的话需要去同步一下cnName
   if (item.CnName) {
     item.CnName = item.options.find(item => item.value === newVal)?.label
-    console.log(item.CnName)
   }
-  console.log(item)
   emits('changeSelect', newVal, name)
 }
 </script>

+ 97 - 0
src/components/statisticCard/StatisticCard.vue

@@ -0,0 +1,97 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useNumber } from '@/hooks/useNumber'
+import AnimationNumber from '../animationNumber/AnimationNumber.vue'
+
+interface Props {
+  title: string
+  value: number
+  transition?: boolean
+  duration?: number
+  groupSeparator?: string // 千分位标识符
+  precision?: number // 数字精度
+  formatter?: (value: number) => string
+}
+
+const { formatterNumberGroup } = useNumber()
+
+const props = withDefaults(defineProps<Props>(), {
+  value: 0,
+  duration: 500,
+  transition: false,
+  precision: 2,
+  groupSeparator: ',',
+})
+
+/**
+ * @description: 格式化数字
+ * @return {*}
+ */
+const formatterValue = computed(() => {
+  if (props.formatter) {
+    return props.formatter(props.value)
+  }
+  return formatterNumberGroup(props.value, props.groupSeparator)
+})
+</script>
+
+<template>
+  <div class="statisticContainer">
+    <div class="staticCard">
+      <div class="title">
+        <slot name="title">
+          {{ props.title }}
+        </slot>
+      </div>
+      <div class="content">
+        <slot>
+          <AnimationNumber
+            v-if="props.transition"
+            :value="Number(props.value.toFixed(props.precision))"
+            :duration="props.duration"
+            :precision="props.precision"
+          ></AnimationNumber>
+          <div v-else>{{ formatterValue }}</div>
+        </slot>
+      </div>
+      <div class="footer">
+        <slot name="footer"></slot>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.statisticContainer {
+  padding-left: 7.5px;
+  padding-right: 7.5px;
+}
+
+.staticCard {
+  padding: 10px 8px 20px 16px;
+  border-radius: 8px;
+}
+
+.staticCard > .title {
+  font-family: 'MicrosoftYaHei';
+  font-size: 12px;
+  line-height: 12px;
+  color: #999;
+}
+
+.staticCard > .content {
+  margin: 16px 0 18px;
+  font-size: 18px;
+  font-weight: 400;
+  line-height: 18px;
+  color: #333;
+  font-family: MicrosoftYaHei;
+}
+
+.staticCard > .footer {
+  font-family: MicrosoftYaHei;
+  font-size: 12px;
+  line-height: 12px;
+  color: #999;
+}
+</style>

+ 6 - 1
src/components/table/Table.vue

@@ -627,6 +627,11 @@ defineExpose({
   bottom: 5px;
 }
 
+.popoverContainer {
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  border: 1px solid #ebeef5;
+}
+
 .schemeItem {
   width: 100%;
   height: 40px;
@@ -647,7 +652,7 @@ defineExpose({
 }
 
 .activeScheme {
-  color: #409eff;
+  color: #197afb;
 }
 
 .exportDialogHeader {

+ 2 - 1
src/components/table/TableQueryForm.vue

@@ -169,7 +169,8 @@ defineExpose({
             :readonly="true"
             placeholder="筛选字段"
             prefix-icon="Filter"
-            style="width: 220px; margin-bottom: 10px"
+            style="width: 240px"
+            class="filterItem"
           >
             <template #append>
               <el-select

+ 0 - 22
src/hooks/HomeSelect/useAnalysisSelect.ts

@@ -258,33 +258,11 @@ export function useAnalysisSelect() {
     },
   })
 
-  // 图表请求基础信息
-  const analysisReqParams: {
-    [key: string]: ChartDataParams
-    projAnalysis: ProjectChartDataParams
-    productAnalysis: ProductChartDataParams
-  } = {
-    projAnalysis: {
-      project: '',
-      media: '',
-      startTime: '',
-      endTime: '',
-    },
-    productAnalysis: {
-      media: '',
-      startTime: '',
-      endTime: '',
-      indicator: '',
-      topType: '',
-    },
-  }
-
   return {
     projctAnalySeleInfo,
     productAnalySeleInfo,
     legendSelectInfo,
     leftToolsSelectInfo,
     rightToolsSelectInfo,
-    analysisReqParams,
   }
 }

+ 2 - 2
src/hooks/HomeSelect/useHomeSelect.ts

@@ -8,7 +8,7 @@ const {
   legendSelectInfo,
   leftToolsSelectInfo,
   rightToolsSelectInfo,
-  analysisReqParams,
+
 } = useAnalysisSelect()
 
 export function useHomeSelect() {
@@ -19,6 +19,6 @@ export function useHomeSelect() {
     legendSelectInfo,
     leftToolsSelectInfo,
     rightToolsSelectInfo,
-    analysisReqParams,
+
   }
 }

+ 15 - 0
src/hooks/useNumber.ts

@@ -0,0 +1,15 @@
+export function useNumber() {
+  /**
+   * @description: 按分隔符格式化数字
+   * @param {number} number 要格式化的数字
+   * @param {string} separator  分隔符
+   * @return {*}
+   */
+  const formatterNumberGroup = (number: number, separator: string = ',') => {
+    return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator)
+  }
+
+  return {
+    formatterNumberGroup,
+  }
+}

+ 1 - 0
src/hooks/useRequest.ts

@@ -22,6 +22,7 @@ export function useRequest() {
     mockAdTTAd: `http://127.0.0.1:8003/mock/ad/ttad`,
     mockChart: `http://127.0.0.1:8003/mock/home/chart`,
     mockChartProduct: `http://127.0.0.1:8003/mock/home/chartProduct`,
+    mockOverview: `http://127.0.0.1:8003/mock/home/overview`,
     getRefreshToken: '',
   }
 

+ 1 - 1
src/utils/axios/axiosInstance.ts

@@ -2,7 +2,7 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-20 17:18:52
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-10-29 17:56:27
+ * @LastEditTime: 2024-10-31 11:39:30
  * @FilePath: \Quantity-Creation-Management-System\src\utils\axios\axiosInstance.ts
  * @Description:
  *

+ 196 - 109
src/views/Home/Home.vue

@@ -9,7 +9,7 @@ import type {
 } from '@/types/echarts/homeAnalysisChart'
 import type { AnalysisSelectInfo } from '@/hooks/HomeSelect/useAnalysisSelect'
 
-import { computed, reactive, ref } from 'vue'
+import { computed, reactive, ref, watch } from 'vue'
 import { useRequest } from '@/hooks/useRequest'
 import { useHomeSelect } from '@/hooks/HomeSelect/useHomeSelect'
 
@@ -17,8 +17,10 @@ import Menu from '@/components/navigation/Menu.vue'
 import CSelect from '@/components/form/CSelect.vue'
 import HomeAnalysisLine from '@/components/echarts/HomeAnalysisLine.vue'
 import axiosInstance from '@/utils/axios/axiosInstance'
+import AnimationNumber from '@/components/animationNumber/AnimationNumber.vue'
 
 import '@/assets/css/statistic.css'
+import type { AxiosRequestConfig } from 'axios'
 
 const { AllApi } = useRequest()
 
@@ -28,65 +30,50 @@ const {
   productAnalySeleInfo,
   legendSelectInfo,
   leftToolsSelectInfo,
-  analysisReqParams,
 } = useHomeSelect()
 
+// 总览数据的自定义指标最多选择个数
+const maxSelectOverviewIndicator = 6
+
 // 图表的数据配置
 const analysisDataConfig: {
   [key: string]: {
     url: string
     xAxisDataField: string
+    config: AxiosRequestConfig
   }
 } = {
   projAnalysis: {
     url: AllApi.mockChart,
     xAxisDataField: 'stat_datetime',
+    config: {
+      params: {},
+    },
   },
   productAnalysis: {
     url: AllApi.mockChartProduct,
     xAxisDataField: 'date',
+    config: {
+      params: {},
+    },
   },
 }
 
-// 广告数据
-const overviewAdvertisingData = reactive<Array<overviewAdvertisingData>>([
-  {
-    name: 'ce1',
-    title: '消耗(元)11',
-    value: 98500.03,
-    rate: 24,
-  },
-  {
-    name: 'ce2',
-    title: '消耗(元)22',
-    value: 98500.03,
-    rate: 24,
-  },
-  {
-    name: 'ce3',
-    title: '消耗(元)33',
-    value: 98500.03,
-    rate: 24,
-  },
-  {
-    name: 'ce4',
-    title: '消耗(元)44',
-    value: 98500.03,
-    rate: 24,
-  },
-  {
-    name: 'ce5',
-    title: '消耗(元)55',
-    value: 98500.03,
-    rate: 24,
-  },
-  {
-    name: 'ce6',
-    title: '消耗(元)66',
-    value: 98500.03,
-    rate: 24,
+// 总览数据的请求配置
+const overviewDataConfig: {
+  url: string
+  config: AxiosRequestConfig
+} = {
+  url: AllApi.mockOverview,
+  config: {
+    params: {},
   },
-])
+}
+
+const statisticLoading = ref<boolean>(false) // 总览数据加载状态
+
+// 广告数据
+const overviewAdvertisingData = reactive<Array<overviewAdvertisingData>>([])
 
 // 分析栏导航菜单
 const dataAnalysisMenuList = reactive<Array<BaseMenu>>([
@@ -116,43 +103,46 @@ const chartLoading = ref<boolean>(true)
 const chartData = reactive<Array<HomeAnalysisChartData>>([])
 
 /**
- * @description: 当选择的数据发生变化时,重新获取数据
- * @param {*} newVal 新的值
- * @param {*} name 字段名
+ * @description: (暂时废弃,因为会导致更换指标的时候,数组的变化导致数据重新执行动画)
+ * 根据已选择自定义指标动态的加入广告数据概况
  * @return {*}
  */
-const changeChartSelect = (newVal: AllOptionsVal, name: string) => {
-  // 对于date字段特殊处理一下
-  if (name === 'date') {
-    analysisReqParams[analysisiMenuActive.value]['startTime'] = (
-      newVal as DateOptionVal
-    ).startTime
-    analysisReqParams[analysisiMenuActive.value]['endTime'] = (
-      newVal as DateOptionVal
-    ).endTime
-  } else {
-    analysisReqParams[analysisiMenuActive.value][name] = newVal
-  }
+// const overviewDataSelected = computed(() => {
+//   // // 这里这样搞是为了做出动态加入的效果,而不是始终在原位置显示
+//   let result: Array<overviewAdvertisingData> = []
+//   overviewSelectInfo.customIndicatorSelect.value.forEach((item: string) => {
+//     let finded = overviewAdvertisingData.find(data => data.name === item)
+//     if (finded) {
+//       result.push(finded)
+//     }
+//   })
+//   return result;
+// })
 
-  console.log(analysisReqParams[analysisiMenuActive.value])
-  getChartData()
+/**
+ * @description: 创建图表的请求参数
+ * @return {*}
+ */
+const createChartParams = () => {
+  let nowAnalysis = analysisSelectInfo[analysisiMenuActive.value]
+  let reqConfig = analysisDataConfig[analysisiMenuActive.value].config
+  for (let [_k, v] of Object.entries(nowAnalysis)) {
+    if (v.name === 'date') {
+      reqConfig.params['startTime'] = (v.value as DateOptionVal).startTime
+      reqConfig.params['endTime'] = (v.value as DateOptionVal).endTime
+    } else {
+      reqConfig.params[v.name] = v.value
+    }
+  }
 }
 
 /**
- * @description: 根据已选择自定义指标动态的加入广告数据概况
+ * @description: 当选择的数据发生变化时,重新获取数据
  * @return {*}
  */
-const overviewDataSelected = computed(() => {
-  // 这里这样搞是为了做出动态加入的效果,而不是始终在原位置显示
-  let result: Array<overviewAdvertisingData> = []
-  overviewSelectInfo.customIndicatorSelect.value.forEach((item: string) => {
-    let finded = overviewAdvertisingData.find(data => data.name === item)
-    if (finded) {
-      result.push(finded)
-    }
-  })
-  return result
-})
+const changeChartSelect = () => {
+  getChartData()
+}
 
 /**
  * @description: 数据分析栏菜单切换
@@ -161,6 +151,7 @@ const overviewDataSelected = computed(() => {
  */
 const analysisiMenuChange = (newMenu: string) => {
   analysisiMenuActive.value = newMenu
+
   getChartData()
 }
 
@@ -170,12 +161,11 @@ const analysisiMenuChange = (newMenu: string) => {
  */
 const getChartData = async () => {
   try {
+    createChartParams() // 更新请求参数
     chartLoading.value = true
     const res = (await axiosInstance.get(
       analysisDataConfig[analysisiMenuActive.value].url,
-      {
-        params: analysisReqParams[analysisiMenuActive.value],
-      },
+      analysisDataConfig[analysisiMenuActive.value].config,
     )) as ResponseInfo
     if (res.code !== 0) throw new Error('获取图表数据失败')
     chartData.splice(0, chartData.length, ...res.data)
@@ -192,7 +182,6 @@ const getChartData = async () => {
  */
 const analysisLegendInfo = computed<Array<LegendInfo>>(() => {
   let result: Array<LegendInfo> = []
-  console.log(legendSelectInfo[analysisiMenuActive.value])
   for (let [_k, v] of Object.entries(
     legendSelectInfo[analysisiMenuActive.value],
   )) {
@@ -205,7 +194,93 @@ const analysisLegendInfo = computed<Array<LegendInfo>>(() => {
   return result
 })
 
+/**
+ * @description: 创建新的自定义指标选择框的值
+ * @param {*} data 返回的数据
+ * @return {*}
+ */
+const createIndicatorSelectInfo = (data: Array<overviewAdvertisingData>) => {
+  let options = data.map(item => {
+    return {
+      value: item.name,
+      label: item.title,
+    }
+  })
+  // 形成新的值,也就是把所有的name都取出来
+  let values = data.slice(0, maxSelectOverviewIndicator).map(item => item.name)
+
+  // 更新自定义指标选项
+  let overviewOptions = overviewSelectInfo.customIndicatorSelect.options
+  overviewOptions.splice(0, overviewOptions.length, ...options)
+
+  // 更新值
+  let overviewValues = overviewSelectInfo.customIndicatorSelect.value
+  overviewValues.splice(0, overviewValues.length, ...values)
+}
+
+/**
+ * @description: 创建广告数据概况请求参数
+ * @return {*}
+ */
+const createOverviewReqParams = () => {
+  let result: { [key: string]: any } = {}
+  for (let [k, v] of Object.entries(overviewSelectInfo)) {
+    // 除了自定义指标,另外两个选择框的值就作为请求参数
+    if (k !== 'customIndicatorSelect') {
+      if (v.name === 'date') {
+        // date选择框需要单独处理一下
+        result['startTime'] = v.value.startTime
+        result['endTime'] = v.value.endTime
+      } else {
+        result[v.name] = v.value
+      }
+    }
+  }
+  overviewDataConfig.config.params = result
+}
+
+/**
+ * @description: 获取总览数据
+ * @return {*}
+ */
+const getOverviewData = async () => {
+  try {
+    statisticLoading.value = true
+    createOverviewReqParams() // 更新请求参数
+    let res = (await axiosInstance.get(
+      overviewDataConfig.url,
+      overviewDataConfig.config,
+    )) as ResponseInfo
+    if (res.code !== 0) throw new Error('获取广告数据概况失败')
+    let data = res.data as Array<overviewAdvertisingData>
+    createIndicatorSelectInfo(data) // 根据返回的数据去更新自定义指标选项和值
+    overviewAdvertisingData.splice(0, overviewAdvertisingData.length, ...data)
+  } catch (err) {
+    console.log(err)
+    ElMessage.error('获取广告数据概况失败')
+  } finally {
+    statisticLoading.value = false
+  }
+}
+
+/**
+ * @description: 改变总览数据框的筛选条件
+ * @param {*} _newSelect 新的筛选条件
+ * @param {*} name 筛选条件的名称
+ * @return {*}
+ */
+const changeOverviewSelect = (_newSelect: any, name: string) => {
+  if (name === 'customIndicator') {
+    // console.log(_newSelect)
+
+    return
+  }
+
+  getOverviewData()
+}
+import StatisticCard from '@/components/statisticCard/StatisticCard.vue'
 getChartData()
+getOverviewData()
 </script>
 
 <template>
@@ -218,44 +293,49 @@ getChartData()
         </div>
         <div class="overviewFilterBox">
           <CSelect
-            :multiple-limit="6"
+            :multiple-limit="maxSelectOverviewIndicator"
             size="default"
             :select-info="overviewSelectInfo"
+            @change-select="changeOverviewSelect"
           ></CSelect>
         </div>
       </div>
-      <div class="dataOverview">
+      <div class="dataOverview" v-loading="statisticLoading">
         <el-row :gutter="16">
-          <el-col :span="4" v-for="item in overviewDataSelected">
-            <template
+          <template v-for="item in overviewAdvertisingData">
+            <el-col
               v-if="
                 overviewSelectInfo.customIndicatorSelect.value.includes(
                   item.name,
                 )
               "
+              :span="4"
             >
-              <div class="statistic-card" html>
-                <el-statistic :value="item.value" :precision="2">
-                  <template #title>
-                    <div style="display: inline-flex; align-items: center">
-                      {{ item.title }}
-                    </div>
-                  </template>
-                </el-statistic>
-                <div class="statistic-footer">
-                  <div class="footer-item">
+              <StatisticCard
+                :title="item.title"
+                :value="item.value"
+                :transition="true"
+              >
+                <template #footer>
+                  <div>
                     <span>环比</span>
-                    <span class="green">
-                      {{ item.rate }}%
+                    <span v-if="item.rate < 0" class="green">
+                      <el-icon>
+                        <CaretBottom />
+                      </el-icon>
+                      {{ Math.abs(item.rate) }}%
+                    </span>
+                    <span v-else class="red">
                       <el-icon>
                         <CaretTop />
                       </el-icon>
+                      {{ item.rate }}%
                     </span>
                   </div>
-                </div>
-              </div>
-            </template>
-          </el-col>
+                </template>
+              </StatisticCard>
+            </el-col>
+          </template>
         </el-row>
       </div>
     </div>
@@ -284,11 +364,13 @@ getChartData()
               class="dataTools"
               v-if="analysisiMenuActive === 'productAnalysis'"
             >
-              <CSelect
-                class="dataSource"
-                @change-select="changeChartSelect"
-                :select-info="leftToolsSelectInfo"
-              ></CSelect>
+              <div class="dataToolItem"></div>
+              <div class="dataToolItem">
+                <CSelect
+                  class="dataSource"
+                  :select-info="leftToolsSelectInfo"
+                ></CSelect>
+              </div>
             </div>
             <div class="legendTools">
               <CSelect
@@ -311,7 +393,6 @@ getChartData()
         </div>
       </div>
     </div>
-    <!-- {{ allChartSelectInfo }} -->
     <div class="topArea"></div>
   </div>
 </template>
@@ -321,7 +402,7 @@ getChartData()
   padding-top: 3vh;
 }
 .advertisingDataOverview {
-  width: 85%;
+  width: 1320px;
   margin: 0 auto;
   padding: 16px 16px 24px;
   background-color: #fff;
@@ -354,12 +435,18 @@ getChartData()
 }
 
 .dataOverview {
-  width: 100%;
+  height: 120px;
+  padding: 10px 5px;
+}
+
+.statistic-card {
+  background-color: rgb(232, 243, 255);
+  margin: 10px 5px;
 }
 
 .advertisingDataAnalysis {
   position: relative;
-  width: 85%;
+  width: 1320px;
   margin: 0 auto;
   padding: 16px 16px 24px;
   background-color: #fff;
@@ -401,6 +488,7 @@ getChartData()
   /* display: flex; */
   /* align-items: center; */
   margin-top: 24px;
+  /* padding: 24px 0; */
 }
 
 .legendTools {
@@ -409,15 +497,14 @@ getChartData()
   align-items: center;
 }
 
+.dataToolItem {
+  height: 100%;
+}
+
 .dataTools {
   position: relative;
 
-  /* display: flex; */
-  /* justify-content: space-between; */
-}
-
-.dataSource {
-  position: absolute;
-  right: 0;
+  display: flex;
+  justify-content: space-between;
 }
 </style>

+ 35 - 5
src/views/Index.vue

@@ -1,13 +1,14 @@
 <script setup lang="ts">
 import type { BaseMenu } from '@/types/Promotion/Menu'
 
-import { onMounted } from 'vue'
+import { onMounted, ref } from 'vue'
 import { setLoginState } from '@/utils/localStorage/localStorage'
 import { removeAllToeken } from '@/utils/token/token'
 
 import Menu from '@/components/navigation/Menu.vue'
 import router from '@/router'
 import CIcon from '@/components/cIcon/cIcon.vue'
+
 // import cIcon from '@/components/cIcon/CIcon.vue'
 
 // 资源的加载路径
@@ -77,7 +78,7 @@ const userInfoTools = [
     url: '/',
   },
   {
-    label: '查看角色权限',
+    label: '修改密码',
     url: '/',
   },
 ]
@@ -125,7 +126,14 @@ onMounted(() => {})
           :offset="20"
         >
           <template #reference>
-            <cIcon :src="resourceInfo.defaultHead" :size="24"></cIcon>
+            <div class="headPopperRef">
+              <cIcon
+                class="popperHeadIcon"
+                :src="resourceInfo.defaultHead"
+                :size="24"
+              ></cIcon>
+              <el-icon class="popperArrowIcon"><ArrowDown /></el-icon>
+            </div>
           </template>
           <div class="userInfoContainer">
             <div class="userInfoContent">
@@ -271,6 +279,26 @@ onMounted(() => {})
   align-items: center;
 }
 
+.headPopperRef {
+  display: flex;
+  align-items: center;
+}
+
+.popperHeadIcon {
+  margin-right: 10px;
+}
+
+.popperArrowIcon {
+  /* 200ms的延迟是为了跟弹出框的动画延迟同步 */
+  transition: transform 0.2s linear 200ms;
+  transform: rotate(0deg);
+}
+
+.headPopperRef:hover > .popperArrowIcon {
+  transition: transform 0.2s linear 0s;
+  transform: rotate(180deg);
+}
+
 .userInfoContainer {
   width: 300px;
   box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
@@ -321,9 +349,11 @@ onMounted(() => {})
 
 .toolItem:hover {
   background-color: #ecf5ff;
-  color: #66b1ff;
 }
 
+.toolItem:hover > .userInfoText {
+  color: #197afb !important;
+}
 .userName {
   font-weight: 700;
   font-size: 14px;
@@ -374,7 +404,7 @@ onMounted(() => {})
 }
 
 .toolBtnItem:hover > .userInfoText {
-  color: #66b1ff;
+  color: #197afb;
 }
 
 .content {