fxs пре 9 месеци
родитељ
комит
fc65baa853

+ 15 - 1
components.d.ts

@@ -7,9 +7,10 @@ export {}
 /* prettier-ignore */
 declare module 'vue' {
   export interface GlobalComponents {
-    DropDownSelection: typeof import('./src/components/toolsBtn/DropDownSelection.vue')['default']
+    DropDownSelection: typeof import('./src/components/dataAnalysis/DropDownSelection.vue')['default']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+    ElCol: typeof import('element-plus/es')['ElCol']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
     ElDialog: typeof import('element-plus/es')['ElDialog']
@@ -24,10 +25,14 @@ declare module 'vue' {
     ElOption: typeof import('element-plus/es')['ElOption']
     ElPagination: typeof import('element-plus/es')['ElPagination']
     ElPopover: typeof import('element-plus/es')['ElPopover']
+    ElRow: typeof import('element-plus/es')['ElRow']
     ElSelect: typeof import('element-plus/es')['ElSelect']
+    ElStatistic: typeof import('element-plus/es')['ElStatistic']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
     ElTable: typeof import('element-plus/es')['ElTable']
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
+    ElTabPane: typeof import('element-plus/es')['ElTabPane']
+    ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
     ElText: typeof import('element-plus/es')['ElText']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
@@ -35,13 +40,22 @@ declare module 'vue' {
     HeaderCard: typeof import('./src/components/dataAnalysis/HeaderCard.vue')['default']
     IconIcBaselineVisibility: typeof import('~icons/ic/baseline-visibility')['default']
     IconIcBaselineVisibilityOff: typeof import('~icons/ic/baseline-visibility-off')['default']
+    IconLogosChrome: typeof import('~icons/logos/chrome')['default']
     IconMaterialSymbolsLightLogout: typeof import('~icons/material-symbols-light/logout')['default']
+    IconMdiHome: typeof import('~icons/mdi/home')['default']
     IconMdiPassword: typeof import('~icons/mdi/password')['default']
+    IconSvg: typeof import('./src/components/svgIcon/IconSvg.vue')['default']
     MyButton: typeof import('./src/components/form/MyButton.vue')['default']
     MyInput: typeof import('./src/components/form/MyInput.vue')['default']
     RegreshBtn: typeof import('./src/components/toolsBtn/RegreshBtn.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    StaticText: typeof import('./src/components/dataAnalysis/StaticText.vue')['default']
+    StatisticText: typeof import('./src/components/dataAnalysis/StatisticText.vue')['default']
+    SvgIcon: typeof import('./src/components/svgIcon/SvgIcon.vue')['default']
+    SvgIcon2: typeof import('./src/components/svgIcon/SvgIcon2.vue')['default']
     Table: typeof import('./src/components/Table.vue')['default']
+    TemporalTrend: typeof import('./src/components/dataAnalysis/TemporalTrend.vue')['default']
+    TimeLineChart: typeof import('./src/components/echarts/TimeLineChart.vue')['default']
   }
 }

Разлика између датотеке није приказан због своје велике величине
+ 523 - 59
package-lock.json


+ 3 - 0
package.json

@@ -19,14 +19,17 @@
     "crypto-js": "^4.2.0",
     "echarts": "^5.5.1",
     "element-plus": "^2.8.0",
+    "fast-glob": "^3.3.2",
     "less": "^4.2.0",
     "pinia": "^2.1.7",
+    "vite-plugin-svg-icons": "^2.0.1",
     "vue": "^3.4.29",
     "vue-router": "^4.3.3"
   },
   "devDependencies": {
     "@iconify-json/ep": "^1.1.16",
     "@iconify-json/ic": "^1.1.18",
+    "@iconify-json/logos": "^1.1.44",
     "@iconify-json/material-symbols-light": "^1.1.28",
     "@iconify-json/mdi": "^1.1.68",
     "@rushstack/eslint-patch": "^1.8.0",

+ 6 - 0
src/assets/platformIcon/chrome.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 15 15">
+	<path fill="currentColor" d="M2.503 1.907A7.47 7.47 0 0 1 7.5 0a7.5 7.5 0 0 1 6.635 4H7.5a3.5 3.5 0 0 0-3.23 2.149z" />
+	<path fill="currentColor" d="M1.745 2.69a7.503 7.503 0 0 0 3.41 11.937l2.812-3.658Q7.737 11 7.5 11a3.5 3.5 0 0 1-3.412-2.716a.5.5 0 0 1-.05-.092z" />
+	<path fill="currentColor" d="M6.215 14.89Q6.842 15 7.5 15a7.5 7.5 0 0 0 7.072-10.005L14.5 5H9.95A3.5 3.5 0 0 1 11 7.5a3.5 3.5 0 0 1-.953 2.405z" />
+	<path fill="currentColor" d="M5 7.5a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0" />
+</svg>

+ 3 - 0
src/assets/platformIcon/tiktok.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
+	<path fill="currentColor" d="M12 2a10 10 0 1 0 10 10A10.01 10.01 0 0 0 12 2m5.939 7.713v.646a.37.37 0 0 1-.38.37a5.36 5.36 0 0 1-2.903-1.108v4.728a3.94 3.94 0 0 1-1.18 2.81a4 4 0 0 1-2.87 1.17a4.1 4.1 0 0 1-2.862-1.17a3.98 3.98 0 0 1-1.026-3.805c.159-.642.48-1.232.933-1.713a3.58 3.58 0 0 1 2.79-1.313h.82v1.703a.348.348 0 0 1-.39.348a1.918 1.918 0 0 0-1.23 3.631c.27.155.572.246.882.267c.24.01.48-.02.708-.092a1.93 1.93 0 0 0 1.313-1.816V5.754a.36.36 0 0 1 .359-.36h1.415a.36.36 0 0 1 .359.34a3.3 3.3 0 0 0 1.282 2.245a3.25 3.25 0 0 0 1.641.636a.37.37 0 0 1 .338.35z" />
+</svg>

+ 6 - 0
src/assets/platformIcon/wechat.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
+	<g fill="none" fill-rule="evenodd">
+		<path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
+		<path fill="currentColor" d="M16 10c3.154 0 6 2.186 6 5.213c0 1.696-.92 3.153-2.26 4.08c-.044.265-.008.532-.008.799a.903.903 0 0 1-.897.908c-.68 0-1.31-.224-1.843-.645a7 7 0 0 1-.992.071c-3.154 0-6-2.186-6-5.213S12.846 10 16 10M9.5 3c3.777 0 7.149 2.535 7.474 6.062A8 8 0 0 0 16 9c-3.533 0-7 2.473-7 6.209q0 .531.09 1.027a9 9 0 0 1-.926-.094c-.67.549-1.443.858-2.31.858c-.518 0-.937-.434-.937-.97l.004-.197l.017-.397a2.1 2.1 0 0 0-.055-.59C3.17 13.667 2 11.794 2 9.624C2 5.807 5.525 3 9.5 3M14 13a1 1 0 1 0 0 2a1 1 0 0 0 0-2m4 0a1 1 0 1 0 0 2a1 1 0 0 0 0-2M7 6a1 1 0 1 0 0 2a1 1 0 0 0 0-2m5 0a1 1 0 1 0 0 2a1 1 0 0 0 0-2" />
+	</g>
+</svg>

+ 1 - 2
src/components/Table.vue

@@ -44,7 +44,7 @@ const selectFieldsList = computed(() => {
     return item.type === FilterType.SELECT
   })
 })
-
+console.log(selectFieldsList)
 // 所有类型为date的表单控件信息
 const dateFieldsList = computed(() => {
   return props.queryInfo?.filter((item) => item.type === FilterType.DATE)
@@ -150,7 +150,6 @@ onMounted(() => {
 
           <!-- 所有选择框 -->
           <el-form-item :label="item.label" v-for="item in selectFieldsList" class="filterItem">
-            <!-- {{ item.placeholder }} -->
             <el-select v-model="queryFormData[item.name]" :placeholder="item.placeholder">
               <el-option v-for="val in item.otherOption" :label="val.cnName" :value="val.value" />
             </el-select>

+ 46 - 0
src/components/dataAnalysis/DropDownSelection.vue

@@ -0,0 +1,46 @@
+<!--
+
+* @FileDescription: 下拉选择框,可用于分类字段或者切换平台等
+
+* @Author: FFF
+
+-->
+
+<script setup lang="ts">
+import type { DropDownInfo } from '@/types/dataAnalysis'
+import { onMounted, reactive, ref } from 'vue'
+
+const props = defineProps<DropDownInfo>()
+const emits = defineEmits(['changeSelect'])
+
+const selectVal = ref()
+
+onMounted(() => {
+  selectVal.value = props.defaultSelect
+})
+
+const changeSelect = (val: any) => {
+  emits('changeSelect', val)
+}
+</script>
+
+<template>
+  <div style="width: 150px; margin-right: 30px">
+    <el-select
+      style="width: 100%"
+      @change="changeSelect"
+      v-model="selectVal"
+      :placeholder="title"
+      size="small"
+    >
+      <el-option
+        v-for="item in optionsList"
+        :key="item.value"
+        :label="item.label"
+        :value="item.value"
+      />
+    </el-select>
+  </div>
+</template>
+
+<style scoped></style>

+ 96 - 3
src/components/dataAnalysis/HeaderCard.vue

@@ -7,11 +7,104 @@
 -->
 
 <script setup lang="ts">
-import DropDownSelection from '../toolsBtn/DropDownSelection.vue'
+import DropDownSelection from './DropDownSelection.vue'
+import { useTableStore } from '@/stores/useTable'
+import type { DropDownInfo } from '@/types/dataAnalysis'
+import { reactive } from 'vue'
+
+interface HeaderCardProps {
+  title: string
+}
+
+defineProps<HeaderCardProps>()
+
+const { allGameInfo } = useTableStore()
+
+// 平台下拉框信息
+const platFormOptionInfo = reactive<DropDownInfo>({
+  defaultSelect: 'web',
+  title: '请选择平台',
+  optionsList: [
+    {
+      value: 'web',
+      label: 'Web'
+    },
+    {
+      value: 'wx',
+      label: '微信'
+    },
+    {
+      value: 'tt',
+      label: '抖音'
+    }
+  ]
+})
+
+// 把游戏的信息转换成对应格式
+const gameInfoList = allGameInfo.map((item) => {
+  return { value: item.gid, label: item.gameName }
+})
+
+// 游戏下拉框信息
+const gameOptionInfo = reactive<DropDownInfo>({
+  defaultSelect: '1001',
+  title: '请选择游戏',
+  optionsList: gameInfoList
+})
+
+const changePf = (val: any) => {
+  console.log(val)
+}
+
+const changeGame = (val: any) => {
+  console.log(val)
+}
 </script>
 
 <template>
-  <DropDownSelection></DropDownSelection>
+  <div class="header">
+    <span class="title">{{ title }}</span>
+    <el-divider direction="vertical" />
+    <div class="selectBox">
+      <div class="selectItem">
+        <DropDownSelection
+          @changeSelect="changePf"
+          :defaultSelect="platFormOptionInfo.defaultSelect"
+          :title="platFormOptionInfo.title"
+          :optionsList="platFormOptionInfo.optionsList"
+        ></DropDownSelection>
+      </div>
+      <DropDownSelection
+        @changeSelect="changeGame"
+        :defaultSelect="gameOptionInfo.defaultSelect"
+        :title="gameOptionInfo.title"
+        :optionsList="gameOptionInfo.optionsList"
+      ></DropDownSelection>
+    </div>
+  </div>
 </template>
 
-<style scoped></style>
+<style scoped>
+.header {
+  box-sizing: border-box;
+  width: 100%;
+  height: 60px;
+  display: flex;
+  align-items: center;
+  padding: 20px 24px;
+}
+
+.title {
+  font-size: 16px;
+  color: #17233d;
+  font-weight: 600;
+}
+
+.selectBox {
+  display: inline-flex;
+}
+
+.selectItem {
+  width: 50%;
+}
+</style>

+ 93 - 0
src/components/dataAnalysis/StatisticText.vue

@@ -0,0 +1,93 @@
+<!--
+
+* @FileDescription: 统计数据组件,用于展示核心数据
+
+* @Author: FFF
+
+-->
+
+<script setup lang="ts">
+import type { StaticDataInfo, StaticDataItemInfo } from '@/types/dataAnalysis'
+import { decimalToPercentage } from '@/utils/common'
+import { StaticDataType } from '@/types/dataAnalysis'
+
+const props = defineProps<StaticDataInfo>()
+
+const formatterTime = (val: any) => {}
+</script>
+
+<template>
+  <div class="dataBox">
+    <div class="dataItem" v-for="item in props.dataList">
+      <div class="header">
+        <span :class="titleClass ? titleClass : 'dataTitle'">{{ item.dataTitle }}</span>
+      </div>
+      <div class="body">
+        <span :class="valueClass ? valueClass : 'value'">{{
+          item.dataType === StaticDataType.DATE ? formatterTime(item.value) : item.value
+        }}</span>
+        <span class="compare" v-if="item.compareInfo">
+          <span>
+            {{
+              decimalToPercentage(
+                (item.value - item.compareInfo.compareVal) / item.compareInfo.compareVal
+              )
+            }}
+            <el-icon
+              v-if="(item.value - item.compareInfo.compareVal) / item.compareInfo.compareVal >= 0"
+              color="#5fbb5d"
+            >
+              <CaretTop />
+            </el-icon>
+            <el-icon v-else color="#ed4014">
+              <CaretBottom />
+            </el-icon>
+          </span>
+        </span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.dataBox {
+  width: 100%;
+  display: flex;
+  /* background-color: lightblue; */
+  padding: 0 24px;
+  box-sizing: border-box;
+  box-shadow:
+    0 4px 8px 0 rgba(0, 0, 0, 0.02),
+    0 1px 3px 0 rgba(0, 0, 0, 0.02);
+}
+
+.dataItem {
+  /* width: 5%; */
+  margin-right: 32px;
+  /* background-color: lightcoral; */
+}
+
+.dataTitle {
+  min-width: 48px;
+  font-size: 12px;
+  color: #657180;
+  line-height: 16px;
+}
+
+.value {
+  font-size: 28px;
+  padding-right: 12px;
+}
+
+.chartStaticValue {
+  font-size: 24px;
+  color: #1c2438;
+  font-weight: 500;
+}
+
+.compare {
+  font-size: 12px;
+  box-sizing: border-box;
+  margin-left: 8px;
+}
+</style>

+ 114 - 0
src/components/dataAnalysis/TemporalTrend.vue

@@ -0,0 +1,114 @@
+<!--
+
+* @FileDescription: 时间趋势图
+
+* @Author: FFF
+
+-->
+
+<script setup lang="ts">
+import { onMounted, reactive, ref } from 'vue'
+import {
+  StaticDataType,
+  type StaticDataItemInfo,
+  type TemporalTrendProps
+} from '@/types/dataAnalysis'
+import TimeLineChart from '../echarts/TimeLineChart.vue'
+import StatisticText from './StatisticText.vue'
+
+const props = defineProps<TemporalTrendProps>()
+const activeTab = ref()
+
+const yesterdayData = reactive<Array<StaticDataItemInfo>>([
+  {
+    dataTitle: '昨日',
+    value: 16,
+    dataType: StaticDataType.NUM,
+    compareInfo: {
+      compareVal: 30
+    }
+  },
+  {
+    dataTitle: '昨日此时',
+    value: 16,
+    dataType: StaticDataType.NUM,
+    compareInfo: {
+      compareVal: 10
+    }
+  },
+  {
+    dataTitle: '今日',
+    value: 16,
+    dataType: StaticDataType.NUM
+  }
+])
+
+onMounted(() => {
+  activeTab.value = props.defaultActive
+})
+</script>
+
+<template>
+  <div class="trendBox">
+    <div class="boxHeader">
+      <span class="title">数据趋势</span>
+    </div>
+    <el-divider />
+    <div class="toolsBox">
+      <el-tabs class="tabsBox" v-model="activeTab">
+        <!-- 一定要加上lazy=true,因为在这时chart组件已经被渲染完成了,但是他此时是被隐藏的,无法获取宽高
+            所以让他延迟渲染,等到真正切换到这个标签时,再去渲染里面的内容
+
+            同时需要配合v-if去重新渲染这个组件,不然当窗口大小变化的时候,resize方法被调用,但是display为none,
+            导致切换过去尺寸又没了
+          -->
+        <el-tab-pane
+          class="tabItem"
+          :lazy="true"
+          v-for="item in tabInfo"
+          :label="item.tabTitle"
+          :name="item.tabTitle"
+          :key="item.tabTitle"
+        >
+          <div class="yesterDayDataBox">
+            <StatisticText
+              :value-class="'chartStaticValue'"
+              :data-list="yesterdayData"
+            ></StatisticText>
+          </div>
+          <TimeLineChart v-if="activeTab === item.tabTitle"></TimeLineChart>
+        </el-tab-pane>
+      </el-tabs>
+    </div>
+    <div class="chartsBox"></div>
+  </div>
+</template>
+
+<style scoped>
+.trendBox {
+  width: 100%;
+  /* display: flex; */
+  /* background-color: lightblue; */
+  padding: 24px;
+  box-sizing: border-box;
+  box-shadow:
+    0 4px 8px 0 rgba(0, 0, 0, 0.02),
+    0 1px 3px 0 rgba(0, 0, 0, 0.02);
+}
+
+.toolsBox {
+  width: 100%;
+}
+
+.tabItem {
+  position: relative;
+  width: 100%;
+  height: 365px;
+}
+
+.yesterDayDataBox {
+  position: absolute;
+  left: 24px;
+  top: 0;
+}
+</style>

+ 202 - 0
src/components/echarts/TimeLineChart.vue

@@ -0,0 +1,202 @@
+<script setup lang="ts">
+import { onMounted, ref, shallowRef } from 'vue'
+import echarts from '.'
+import { nextTick } from 'vue'
+import { debounceFunc } from '@/utils/common'
+
+// 图标所需要的props
+interface OptionInfo {}
+
+// 图标实例
+const linechart = shallowRef()
+
+// 图标Ref
+const chart = ref()
+
+// 模拟数据
+const mockData = [
+  {
+    time: 0,
+    value: 1223,
+    date: 'yesterday'
+  },
+  {
+    time: 1,
+    value: 12323,
+    date: 'yesterday'
+  },
+  {
+    time: 2,
+    value: 121323,
+    date: 'yesterday'
+  },
+  {
+    time: 3,
+    value: 121323,
+    date: 'yesterday'
+  },
+  {
+    time: 4,
+    value: 12233,
+    date: 'yesterday'
+  },
+  {
+    time: 5,
+    value: 12323,
+    date: 'yesterday'
+  },
+  {
+    time: 6,
+    value: 112323,
+    date: 'yesterday'
+  },
+  {
+    time: 7,
+    value: 112323,
+    date: 'yesterday'
+  },
+  {
+    time: 8,
+    value: 12123213,
+    date: 'yesterday'
+  },
+  {
+    time: 1,
+    value: 1223133,
+    date: 'today'
+  },
+  {
+    time: 2,
+    value: 112323,
+    date: 'today'
+  },
+  {
+    time: 3,
+    value: 112323,
+    date: 'today'
+  },
+  {
+    time: 4,
+    value: 123123,
+    date: 'today'
+  },
+  {
+    time: 5,
+    value: 121233,
+    date: 'today'
+  },
+  {
+    time: 6,
+    value: 121233,
+    date: 'today'
+  },
+  {
+    time: 7,
+    value: 1221323,
+    date: 'yesterday'
+  },
+  {
+    time: 8,
+    value: 121233,
+    date: 'yesterday'
+  }
+]
+
+// 生成时间刻度
+function generateHourlyArray(count: number) {
+  const result = []
+  for (let i = 1; i <= count; i++) {
+    result.push(`${i}:00`)
+  }
+  return result
+}
+
+// 时间刻度lable
+const timeFrameLabel = generateHourlyArray(24)
+
+// 选项
+const options = {
+  xAxis: {
+    type: 'category',
+    data: timeFrameLabel,
+    axisTick: {
+      alignWithLabel: true
+    }
+  },
+  yAxis: {
+    type: 'value'
+  },
+  tooltip: {
+    trigger: 'axis'
+  },
+  legend: {
+    orient: 'horizontal', //图例布局方式:水平 'horizontal' 、垂直 'vertical'
+    x: 'right', // 横向放置位置,选项:'center'、'left'、'right'、'number'(横向值 px)
+    y: 'top', // 纵向放置位置,选项:'top'、'bottom'、'center'、'number'(纵向值 px)
+    data: ['今日新增', '昨日新增']
+  },
+  series: [
+    {
+      symbol: 'circle',
+      symbolSize: 5,
+      showSymbol: false,
+      // itemStyle要在lineStyle前面,这个item会把line的样式覆盖掉,并且必须要有line,不然也会被item替换掉
+      itemStyle: {
+        color: 'rgb(255,255,255)',
+        borderColor: '#1495eb', // symbol边框颜色
+        borderWidth: 2 // symbol边框宽度
+      },
+      lineStyle: {
+        color: '#1495eb'
+      },
+
+      name: '今日新增',
+      data: [10, 22, 28, 43, 49],
+      type: 'line',
+      stack: 'x',
+      smooth: true
+    },
+    {
+      symbol: 'circle',
+      symbolSize: 5,
+      showSymbol: false,
+      itemStyle: {
+        color: 'rgb(255,255,255)',
+        borderColor: '#00cc66', // symbol边框颜色
+        borderWidth: 2 // symbol边框宽度
+      },
+      lineStyle: {
+        color: '#00cc66'
+      },
+
+      name: '昨日新增',
+      data: [5, 4, 3, 5, 10],
+      type: 'line',
+      stack: 'x',
+      smooth: true
+    }
+  ]
+}
+
+// 传过来的配置
+// defineProps<OptionInfo>()
+const changeSize = () => {
+  nextTick(() => {
+    linechart.value?.resize()
+  })
+}
+
+onMounted(() => {
+  nextTick(() => {
+    linechart.value = echarts.init(chart.value)
+    linechart.value.setOption(options)
+    window.addEventListener('resize', debounceFunc(changeSize, 500))
+  })
+})
+</script>
+
+<template>
+  <div class="chart" ref="chart" style="width: 100%; height: 365px"></div>
+</template>
+
+<style scoped></style>

+ 17 - 6
src/utils/echarts/CoreEcharts.ts → src/components/echarts/index.ts

@@ -1,17 +1,22 @@
 // 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
 import * as echarts from 'echarts/core'
-// 引入柱状图图表,图表后缀都为 Chart
-import { BarChart } from 'echarts/charts'
-// 引入标题,提示框,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
+
+// 引入柱状图和饼状图图表,图表后缀都为 Chart,具体为 图标名称+Chart (注意图标名称为首字母大写)
+import { BarChart, PieChart, LineChart } from 'echarts/charts'
+
+// 引入提示框,标题,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
 import {
   TitleComponent,
   TooltipComponent,
   GridComponent,
+  ToolboxComponent,
+  LegendComponent,
   DatasetComponent,
   TransformComponent
 } from 'echarts/components'
-// 标签自动布局全局过渡动画等特性
+// 标签自动布局全局过渡动画等特性
 import { LabelLayout, UniversalTransition } from 'echarts/features'
+
 // 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
 import { CanvasRenderer } from 'echarts/renderers'
 
@@ -20,12 +25,18 @@ echarts.use([
   TitleComponent,
   TooltipComponent,
   GridComponent,
+  ToolboxComponent,
+  LegendComponent,
   DatasetComponent,
   TransformComponent,
-  BarChart,
+  CanvasRenderer,
   LabelLayout,
   UniversalTransition,
-  CanvasRenderer
+
+  BarChart,
+  PieChart,
+  LineChart
 ])
 
+// 导出
 export default echarts

+ 0 - 13
src/components/toolsBtn/DropDownSelection.vue

@@ -1,13 +0,0 @@
-<!--
-
-* @FileDescription: 下拉选择框,可用于分类字段或者切换平台等
-
-* @Author: FFF
-
--->
-
-<script setup lang="ts"></script>
-
-<template></template>
-
-<style scoped></style>

+ 1 - 0
src/components/toolsBtn/FilterPopover.vue

@@ -19,6 +19,7 @@ defineProps<SettingInfo>()
           <el-icon :size="iconSize" class="rightToolsItem"><Setting /></el-icon>
         </template>
         <!-- 这里不是使用vlue是v-model去绑定数据 -->
+        <!-- 这里有点奇怪,按理说是不能这样绑定的 -->
         <el-checkbox v-for="item in tableFieldsInfo" :label="item.cnName" v-model="item.isShow" />
       </el-popover>
     </span>

+ 2 - 0
src/main.ts

@@ -6,10 +6,12 @@ import App from './App.vue'
 import router from './router'
 
 const app = createApp(App)
+
 for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
   app.component(key, component)
 }
 app.use(createPinia())
+
 app.use(router)
 
 app.mount('#app')

+ 9 - 2
src/stores/useTable.ts

@@ -23,7 +23,14 @@ export const useTableStore = defineStore('tableStore', () => {
   }
 
   // 所有的游戏信息
-  const allGameInfo = reactive<Array<QueryGameInfo>>([])
+  let allGameInfo = reactive<Array<QueryGameInfo>>([])
 
-  return { playerQueryInfo, allGameInfo, updateQueryInfo }
+  const setGameInfo = (list: Array<QueryGameInfo>) => {
+    // allGameInfo = list
+    allGameInfo.length = 0
+    allGameInfo.push(...list)
+    console.log(allGameInfo)
+  }
+
+  return { playerQueryInfo, allGameInfo, updateQueryInfo, setGameInfo }
 })

+ 46 - 6
src/types/dataAnalysis.ts

@@ -1,10 +1,50 @@
-export enum DropDownSelectFieldType {
+// 下拉选项的信息
+export interface DropDownItem {
+  value: any
+  label: string
+}
+
+// 下拉框所需要的props信息
+export interface DropDownInfo {
+  defaultSelect: any
+  title: string
+  optionsList: Array<DropDownItem>
+}
+
+// 数据分析页面头部数据类型
+export enum StaticDataType {
   TEXT = 'text',
-  IMG = 'img'
+  NUM = 'number',
+  DATE = 'date'
+}
+
+export interface CompareInfo {
+  compareVal: any
+}
+
+// 数据分析页面头部数据信息
+export interface StaticDataItemInfo {
+  dataTitle: string // 标题
+  dataType: StaticDataType // 什么类型的数据
+  value: any // 值
+  compareInfo?: CompareInfo
+}
+
+// 数据分析页面头部数据所需要的props
+export interface StaticDataInfo {
+  dataList: Array<StaticDataItemInfo>
+  titleClass?: string // 标题的样式
+  valueClass?: string // 值的样式
+}
+
+// 时间趋势图的tab信息
+export interface TabInfo {
+  tabTitle: string // tab的标题
+  url: string // 到时候需要获取数据的url
 }
 
-export interface FieldInfo {
-  fieldType: DropDownSelectFieldType
-  openShowList: boolean // 是否展开显示已选择的项目
-  fieldList: Array<any>
+// 时间趋势组件所需要的props
+export interface TemporalTrendProps {
+  tabInfo: Array<TabInfo>
+  defaultActive: string // 默认选中
 }

+ 24 - 0
src/utils/common/index.ts

@@ -0,0 +1,24 @@
+// 防抖
+export function debounceFunc<T extends (...args: any[]) => any>(
+  func: T,
+  delay: number
+): (...args: Parameters<T>) => void {
+  let timer: ReturnType<typeof setTimeout> | null
+  return (...args: Parameters<T>) => {
+    if (timer) clearTimeout(timer)
+    timer = setTimeout(() => {
+      func(...args)
+    }, delay)
+  }
+}
+
+export function decimalToPercentage(decimal: number, decimalPlaces: number = 0): string {
+  // Convert the decimal to a percentage by multiplying by 100
+  const percentage = decimal * 100
+
+  // Format the percentage to a fixed number of decimal places
+  const formattedPercentage = percentage.toFixed(decimalPlaces)
+
+  // Append the '%' symbol and return the result
+  return `${formattedPercentage}%`
+}

+ 19 - 0
src/utils/common/time.ts

@@ -0,0 +1,19 @@
+// 待定格式
+export function resetTimeToMidnight(dateTimeStr: string): string {
+  // 创建一个 Date 对象来解析输入的日期时间字符串
+  const date = new Date(dateTimeStr)
+
+  // 将时间部分设置为 00:00:00
+  date.setHours(0, 0, 0, 0)
+
+  // 格式化日期为 'YYYY-MM-DD HH:mm:ss' 格式
+  const year = date.getFullYear()
+  const month = String(date.getMonth() + 1).padStart(2, '0') // 月份从0开始,需要加1
+  const day = String(date.getDate()).padStart(2, '0')
+  const hours = String(date.getHours()).padStart(2, '0')
+  const minutes = String(date.getMinutes()).padStart(2, '0')
+  const seconds = String(date.getSeconds()).padStart(2, '0')
+
+  // 返回格式化的字符串
+  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+}

+ 37 - 0
src/utils/table/table.ts

@@ -0,0 +1,37 @@
+import { useTableStore } from '@/stores/useTable'
+import { useRequest } from '@/hooks/useRequest'
+import axiosInstance from '../axios/axiosInstance'
+
+const { AllApi } = useRequest()
+
+// 拿到所有游戏的信息
+export const getAllGameInfo = async () => {
+  try {
+    const tableStore = useTableStore() // 这一句不要直接卸载函数外面,会导致在pinia挂载之前被调用
+
+    if (tableStore.allGameInfo.length) return tableStore.allGameInfo
+    else {
+      const response = await axiosInstance.post(AllApi.getGameTable, {
+        appSecret: '6YJSuc50uJ18zj45'
+      })
+      const result = JSON.parse(JSON.stringify(response))
+      const data = result.data // 拿到返回的数据
+
+      if (result.code === 0) {
+        if (Array.isArray(data)) {
+          let gameInfoList = data.map((item) => {
+            return { gid: item.gid, gameName: item.gameName }
+          })
+          tableStore.setGameInfo(gameInfoList)
+          return gameInfoList
+        }
+      } else {
+        console.log('获取游戏列表失败')
+        return []
+      }
+    }
+  } catch (err) {
+    console.log(err)
+    throw new Error('获取游戏列表失败')
+  }
+}

+ 9 - 2
src/views/Home/HomeView.vue

@@ -1,7 +1,8 @@
 <script setup lang="ts">
 import { RouterView } from 'vue-router'
-import { ref } from 'vue'
+import { onMounted, ref } from 'vue'
 import { ElMessage } from 'element-plus'
+import { getAllGameInfo } from '@/utils/table/table'
 import router from '@/router'
 import { getAssetsImageUrl } from '@/utils/common/img'
 
@@ -37,6 +38,7 @@ const changeCollapse = () => {
   isCollapse.value = !isCollapse.value
 }
 
+// 登出
 const logOut = () => {
   ElMessage({
     type: 'success',
@@ -52,6 +54,11 @@ const iconUrl = {
   logo: getAssetsImageUrl('logo.svg'),
   defaultHead: getAssetsImageUrl('default/defaultHead.png')
 }
+
+const gameinfoLoad = ref(false)
+getAllGameInfo().then(() => {
+  gameinfoLoad.value = true
+})
 </script>
 
 <template>
@@ -90,7 +97,7 @@ const iconUrl = {
       </div>
     </div>
     <div class="content">
-      <RouterView />
+      <RouterView v-if="gameinfoLoad" />
     </div>
   </div>
 </template>

+ 71 - 3
src/views/Home/OverView.vue

@@ -1,7 +1,63 @@
-<script setup lang="ts"></script>
+<script setup lang="ts">
+import HeaderCard from '@/components/dataAnalysis/HeaderCard.vue'
+import { onMounted, reactive } from 'vue'
+import type { StaticDataItemInfo, TabInfo } from '@/types/dataAnalysis'
+import { StaticDataType } from '@/types/dataAnalysis'
+import StatisticText from '@/components/dataAnalysis/StatisticText.vue'
+import TemporalTrend from '@/components/dataAnalysis/TemporalTrend.vue'
+
+const getStaticData = async () => {}
+// 模拟数据
+let staticData = reactive<Array<StaticDataItemInfo>>([
+  {
+    dataTitle: '累计设备',
+    dataType: StaticDataType.NUM,
+    value: 0
+  },
+  {
+    dataTitle: '近7日活跃设备',
+    dataType: StaticDataType.NUM,
+    value: 0
+  },
+  {
+    dataTitle: '近30日活跃设备',
+    dataType: StaticDataType.NUM,
+    value: 0
+  },
+  {
+    dataTitle: '近7日单设备日均使用时长',
+    dataType: StaticDataType.TEXT,
+    value: '00:00:00'
+  }
+])
+
+// 模拟数据分析组件需要的tab信息
+const tabInfoList = reactive<Array<TabInfo>>([
+  {
+    tabTitle: '新增用户',
+    url: 'example'
+  },
+  {
+    tabTitle: '活跃用户',
+    url: 'example'
+  }
+])
+
+onMounted(() => {
+  getStaticData().then(() => {})
+})
+</script>
 <template>
   <div class="overViewBox">
-    <div class="header"></div>
+    <div class="header">
+      <HeaderCard :title="'数据总览'"></HeaderCard>
+    </div>
+    <div class="staticBox">
+      <StatisticText :dataList="staticData"></StatisticText>
+    </div>
+    <div class="temporalTrendBox">
+      <TemporalTrend :defaultActive="'新增用户'" :tabInfo="tabInfoList"></TemporalTrend>
+    </div>
   </div>
 </template>
 
@@ -10,6 +66,18 @@
   width: 98%;
   margin: 1% auto;
   background-color: white;
-  border: 1px solid #e5e6eb;
+  box-sizing: border-box;
+  /* border: 1px solid #e5e6eb; */
+}
+
+.header {
+  box-sizing: border-box;
+}
+
+.staticBox,
+.temporalTrendBox {
+  width: 100%;
+  box-sizing: border-box;
+  padding: 0 24px;
 }
 </style>

+ 7 - 37
src/views/Home/PlayerManageView.vue

@@ -234,44 +234,14 @@ const submiteOptionChange = (isEncrypt: boolean = false) => {
   )
 }
 
-// 拿到所有游戏的信息
-const getAllGameInfo = async () => {
-  try {
-    const response = await axiosInstance.post(AllApi.getGameTable, {
-      appSecret: '6YJSuc50uJ18zj45'
-    })
-    const result = JSON.parse(JSON.stringify(response))
-    const data = result.data // 拿到返回的数据
-
-    if (result.code === 0) {
-      if (Array.isArray(data)) {
-        let gameInfoList = data.map((item) => {
-          return { gid: item.gid, gameName: item.gameName }
-        })
-        tableStore.$patch({
-          allGameInfo: gameInfoList
-        })
-        gameInfoList.map((item) => {
-          allGameInfo.push({
-            name: item.gameName,
-            value: item.gid,
-            cnName: item.gameName
-          })
-        })
-        return gameInfoList
-      }
-    } else {
-      console.log('获取游戏列表失败')
-      return false
-    }
-  } catch (err) {
-    console.log(err)
-    throw new Error('获取游戏列表失败')
-  }
-}
-
 onMounted(() => {
-  getAllGameInfo()
+  tableStore.allGameInfo.map((item) => {
+    allGameInfo.push({
+      name: item.gameName,
+      value: item.gid,
+      cnName: item.gameName
+    })
+  })
 })
 </script>
 <template>

+ 4 - 1
tsconfig.app.json

@@ -7,7 +7,9 @@
     "src/**/*.vue",
     "src/components/toolsPopover/FilterPopover.vue",
     "src/components/input/TextInput.vue",
-    "src/components/input/TextInput.VUE"
+    "src/components/input/TextInput.VUE",
+    "src/components/svgIcon/SvgIcon.vue",
+    "src/components/svgIcon/SvgIcon.vue"
   ],
   "exclude": ["src/**/__tests__/*"],
   "compilerOptions": {
@@ -18,6 +20,7 @@
     "module": "ESNext",
     "lib": ["ES2020", "DOM", "DOM.Iterable"],
     "skipLibCheck": true,
+    "types": ["vite/client"],
 
     "moduleResolution": "bundler",
     "allowImportingTsExtensions": true,

Неке датотеке нису приказане због велике количине промена