فهرست منبع

更新总览页,更新数据获取逻辑

fxs 9 ماه پیش
والد
کامیت
2a8cec093a

+ 5 - 0
src/assets/base.css

@@ -27,3 +27,8 @@ img {
 .el-button + .el-button {
   margin-left: 0 !important;
 }
+
+body {
+  font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei',
+    微软雅黑, Arial, sans-serif;
+}

+ 13 - 2
src/components/Table.vue

@@ -24,6 +24,16 @@ const emits = defineEmits(['addNewItem'])
 // 表格数据
 const tableData: Array<any> = reactive([])
 
+// 没有开启分页查询的时候使用的数据
+const tableDataNoPaging = computed(() => {
+  let curPage = props.paginationConfig.currentPage
+  let limit = props.paginationConfig.limit
+  let begin = curPage * limit - limit
+  //这里不减一是因为,slice方法裁切是左闭右开数组
+  let end = curPage * limit
+  return tableData.slice(begin, end)
+})
+
 // 查询表单的数据
 const queryFormData = reactive<any>({})
 
@@ -188,7 +198,7 @@ onMounted(() => {
           <el-icon><Plus /></el-icon>新增
         </el-button>
       </div>
-      <div class="rightTools">
+      <div class="rightTools" v-if="needRightTools">
         <el-button color="#f0f1f3" size="default" class="rightToolsItem">
           <el-icon><Download /></el-icon>下载
         </el-button>
@@ -203,8 +213,9 @@ onMounted(() => {
     </div>
 
     <div class="tableBox">
+      <!-- 没有分页的时候需要重新计算一下data -->
       <el-table
-        :data="openPageQuery ? tableData[paginationConfig.currentPage] : tableData"
+        :data="openPageQuery ? tableData[paginationConfig.currentPage] : tableDataNoPaging"
         style="width: 100%"
         class="tableBody"
       >

+ 80 - 15
src/components/dataAnalysis/HeaderCard.vue

@@ -10,19 +10,24 @@
 import DropDownSelection from './DropDownSelection.vue'
 import { useTableStore } from '@/stores/useTable'
 import type { DropDownInfo } from '@/types/dataAnalysis'
-import { reactive } from 'vue'
+import { reactive, ref } from 'vue'
+import { resetTimeToMidnight } from '@/utils/common/time'
 
 interface HeaderCardProps {
-  title: string
+  title: string // title信息
+  defaultPf: string // 默认选择的pf
+  defaultGame: string // 默认选择的游戏
+  openDateSelect: boolean // 是否开启时间选择
 }
 
-defineProps<HeaderCardProps>()
+const props = defineProps<HeaderCardProps>()
 
+const emits = defineEmits(['changePf', 'changeGame'])
 const { allGameInfo } = useTableStore()
 
 // 平台下拉框信息
 const platFormOptionInfo = reactive<DropDownInfo>({
-  defaultSelect: 'web',
+  defaultSelect: props.defaultPf,
   title: '请选择平台',
   optionsList: [
     {
@@ -40,6 +45,40 @@ const platFormOptionInfo = reactive<DropDownInfo>({
   ]
 })
 
+// 快速选择日期
+const shortcuts = [
+  {
+    text: '上一周',
+    value: () => {
+      const end = new Date()
+      const start = new Date()
+      start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
+      return [start, end]
+    }
+  },
+  {
+    text: '上个月',
+    value: () => {
+      const end = new Date()
+      const start = new Date()
+      start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
+      return [start, end]
+    }
+  },
+  {
+    text: '近三个月',
+    value: () => {
+      const end = new Date()
+      const start = new Date()
+      start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
+      return [start, end]
+    }
+  }
+]
+
+// 选择的日期
+const selectDate = ref(shortcuts[0].value())
+
 // 把游戏的信息转换成对应格式
 const gameInfoList = allGameInfo.map((item) => {
   return { value: item.gid, label: item.gameName }
@@ -47,22 +86,26 @@ const gameInfoList = allGameInfo.map((item) => {
 
 // 游戏下拉框信息
 const gameOptionInfo = reactive<DropDownInfo>({
-  defaultSelect: '1001',
+  defaultSelect: props.defaultGame,
   title: '请选择游戏',
   optionsList: gameInfoList
 })
 
+const dateChange = (val: any) => {
+  console.log(selectDate.value)
+}
+
 const changePf = (val: any) => {
-  console.log(val)
+  emits('changePf', val)
 }
 
 const changeGame = (val: any) => {
-  console.log(val)
+  emits('changeGame', val)
 }
 </script>
 
 <template>
-  <div class="header">
+  <div class="headerCard">
     <span class="title">{{ title }}</span>
     <el-divider direction="vertical" />
     <div class="selectBox">
@@ -74,18 +117,33 @@ const changeGame = (val: any) => {
           :optionsList="platFormOptionInfo.optionsList"
         ></DropDownSelection>
       </div>
-      <DropDownSelection
-        @changeSelect="changeGame"
-        :defaultSelect="gameOptionInfo.defaultSelect"
-        :title="gameOptionInfo.title"
-        :optionsList="gameOptionInfo.optionsList"
-      ></DropDownSelection>
+      <div class="selectItem">
+        <DropDownSelection
+          @changeSelect="changeGame"
+          :defaultSelect="gameOptionInfo.defaultSelect"
+          :title="gameOptionInfo.title"
+          :optionsList="gameOptionInfo.optionsList"
+        ></DropDownSelection>
+      </div>
+    </div>
+    <div v-if="props.openDateSelect" class="datePicker">
+      <el-date-picker
+        v-model="selectDate"
+        @change="dateChange"
+        type="daterange"
+        unlink-panels
+        range-separator="至"
+        start-placeholder="Start date"
+        end-placeholder="End date"
+        :shortcuts="shortcuts"
+        :size="'small'"
+      />
     </div>
   </div>
 </template>
 
 <style scoped>
-.header {
+.headerCard {
   box-sizing: border-box;
   width: 100%;
   height: 60px;
@@ -107,4 +165,11 @@ const changeGame = (val: any) => {
 .selectItem {
   width: 50%;
 }
+
+.datePicker {
+  display: inline-flex;
+  align-items: center;
+  /* 可以将他单独的右对齐 */
+  margin-left: auto;
+}
 </style>

+ 97 - 30
src/components/dataAnalysis/StatisticText.vue

@@ -1,4 +1,13 @@
 <!--
+ * @Author: fxs bjnsfxs@163.com
+ * @Date: 2024-08-26 13:57:37
+ * @LastEditors: fxs bjnsfxs@163.com
+ * @LastEditTime: 2024-08-27 16:49:56
+ * @FilePath: \Game-Backstage-Management-System\src\components\dataAnalysis\StatisticText.vue
+ * @Description: 
+ * 
+-->
+<!--
 
 * @FileDescription: 统计数据组件,用于展示核心数据
 
@@ -10,40 +19,94 @@
 import type { StaticDataInfo, StaticDataItemInfo } from '@/types/dataAnalysis'
 import { decimalToPercentage } from '@/utils/common'
 import { StaticDataType } from '@/types/dataAnalysis'
+import { onMounted, reactive, watch, watchEffect } from 'vue'
+import axiosInstance from '@/utils/axios/axiosInstance'
+import { useRequest } from '@/hooks/useRequest'
 
+const { analysisResCode } = useRequest()
 const props = defineProps<StaticDataInfo>()
+const dataList = reactive<Array<StaticDataItemInfo>>([])
 
 const formatterTime = (val: any) => {}
+
+/**
+ * @description: 用于获取数据
+ * @tip  这里暂时只请求当日的数据,没有比较
+ * @return {*}
+ */
+const getData = () => {
+  axiosInstance.post(props.requestConfig.url, props.requestConfig.otherOptions).then((data) => {
+    analysisResCode(data).then((info) => {
+      console.log(info)
+      // dataList.splice(0, dataList.length, info.data)
+    })
+  })
+}
+
+// 模拟数据
+// 上方总览数据
+let staticData = reactive<Array<StaticDataItemInfo>>([
+  {
+    dataTitle: '累计设备',
+    dataType: StaticDataType.NUM,
+    value: 30,
+    compareVal: 100
+  },
+  {
+    dataTitle: '近7日活跃设备',
+    dataType: StaticDataType.NUM,
+    value: 80,
+    compareVal: 20
+  },
+  {
+    dataTitle: '近30日活跃设备',
+    dataType: StaticDataType.NUM,
+    value: 200,
+    compareVal: 300
+  },
+  {
+    dataTitle: '近7日单设备日均使用时长',
+    dataType: StaticDataType.TEXT,
+    value: '00:00:00'
+  }
+])
+
+/**
+ * @description: 监听requesconfig的变化,一旦变化就重新请求
+ * @return {*}
+ */
+// watchEffect(() => getData())
+
+onMounted(() => {
+  // getData()
+  // 模拟一下
+  dataList.splice(0, dataList.length, ...staticData)
+})
 </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>
+    <div class="dataBody">
+      <div class="dataItem" v-for="item in 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.compareVal">
+            <span>
+              {{ decimalToPercentage((item.value - item.compareVal) / item.compareVal) }}
+              <el-icon v-if="(item.value - item.compareVal) / item.compareVal >= 0" color="#5fbb5d">
+                <CaretTop />
+              </el-icon>
+              <el-icon v-else color="#ed4014">
+                <CaretBottom />
+              </el-icon>
+            </span>
           </span>
-        </span>
+        </div>
       </div>
     </div>
   </div>
@@ -52,19 +115,22 @@ const formatterTime = (val: any) => {}
 <style scoped>
 .dataBox {
   width: 100%;
-  display: flex;
-  /* background-color: lightblue; */
-  padding: 0 24px;
+
+  padding: 20px 24px;
   box-sizing: border-box;
+}
+
+.dataBody {
+  display: flex;
+  width: 100%;
+
   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 {
@@ -77,6 +143,7 @@ const formatterTime = (val: any) => {}
 .value {
   font-size: 28px;
   padding-right: 12px;
+  line-height: 48px;
 }
 
 .chartStaticValue {

+ 155 - 24
src/components/dataAnalysis/TemporalTrend.vue

@@ -7,34 +7,37 @@
 -->
 
 <script setup lang="ts">
-import { onMounted, reactive, ref } from 'vue'
+import { onMounted, reactive, ref, watchEffect } from 'vue'
 import {
   StaticDataType,
   type StaticDataItemInfo,
-  type TemporalTrendProps
+  type TemporalTrendProps,
+  type StaticReqConfig
 } from '@/types/dataAnalysis'
+import type { TablePaginationSetting, TableFieldInfo } from '@/types/table'
+import Table from '../Table.vue'
 import TimeLineChart from '../echarts/TimeLineChart.vue'
 import StatisticText from './StatisticText.vue'
+import { useRequest } from '@/hooks/useRequest'
+import axiosInstance from '@/utils/axios/axiosInstance'
 
+const { AllApi, analysisResCode } = useRequest()
 const props = defineProps<TemporalTrendProps>()
-const activeTab = ref()
+const activeTab = ref() // 激活的Tab
+const iconSize = ref(20) // 图标的尺寸
 
 const yesterdayData = reactive<Array<StaticDataItemInfo>>([
   {
     dataTitle: '昨日',
     value: 16,
     dataType: StaticDataType.NUM,
-    compareInfo: {
-      compareVal: 30
-    }
+    compareVal: 30
   },
   {
     dataTitle: '昨日此时',
     value: 16,
     dataType: StaticDataType.NUM,
-    compareInfo: {
-      compareVal: 10
-    }
+    compareVal: 10
   },
   {
     dataTitle: '今日',
@@ -43,6 +46,80 @@ const yesterdayData = reactive<Array<StaticDataItemInfo>>([
   }
 ])
 
+// 配置总览数据的请求参数
+const compareDataReqConfig = reactive<StaticReqConfig>({
+  url: '',
+  otherOptions: ''
+})
+
+// 配置请求参数
+const tableRequestConfig = reactive({
+  url: AllApi.mock,
+  other: {
+    appSecret: '6YJSuc50uJ18zj45'
+  }
+})
+
+// 配置分页数据
+const paginationConfig = reactive<TablePaginationSetting>({
+  limit: 6, // 每页展示个数
+  currentPage: 1, // 当前页码
+  total: 0, // 数据总数
+  pagesizeList: [6], // 页数大小列表
+  loading: true, // 加载图标
+  hasLodingData: 0 // 已经加载的数据
+})
+
+// 字段信息
+const filedsInfo = reactive<Array<TableFieldInfo>>([
+  {
+    name: 'date',
+    cnName: '日期',
+    isShow: true
+  },
+  {
+    name: '新增设备',
+    cnName: 'newEquip',
+    isShow: true
+  }
+])
+
+// 选择的展示形式
+const selectShape = ref('trend')
+
+// 图表上方总览数据
+const overViewData = reactive<Array<StaticDataItemInfo>>([])
+
+/**
+ * @description: 改变图表形式的时候
+ * @param {*} name 图表的展现形式,可以使table或者trend
+ * @return {*}
+ */
+const changeSelectShape = (name: string) => {
+  selectShape.value = name
+}
+
+const getData = async (url: string) => {
+  axiosInstance.post(url, props.requestConfig).then((data) => {
+    analysisResCode(data).then((info) => {
+      console.log(info)
+    })
+  })
+}
+
+/**
+ * @description: 当标签页切换的时候,重新获取数据
+ * @param {*} TabPaneName 对应的tabName,去tabinfo中找到对应的url
+ * @return {*}
+ */
+const tabChange = (TabPaneName: string) => {
+  let url = props.tabInfo.find((item) => item.tabTitle === TabPaneName)?.url
+  if (url) getData(url)
+  else throw new Error('No match url')
+}
+
+// watchEffect(() => getData(props.requestConfig.url))
+
 onMounted(() => {
   activeTab.value = props.defaultActive
 })
@@ -51,17 +128,17 @@ onMounted(() => {
 <template>
   <div class="trendBox">
     <div class="boxHeader">
-      <span class="title">数据趋势</span>
+      <span class="headerTitle">{{ title }}</span>
     </div>
     <el-divider />
-    <div class="toolsBox">
-      <el-tabs class="tabsBox" v-model="activeTab">
-        <!-- 一定要加上lazy=true,因为在这时chart组件已经被渲染完成了,但是他此时是被隐藏的,无法获取宽高
+    <!-- 一定要加上lazy=true,因为在这时chart组件已经被渲染完成了,但是他此时是被隐藏的,无法获取宽高
             所以让他延迟渲染,等到真正切换到这个标签时,再去渲染里面的内容
 
             同时需要配合v-if去重新渲染这个组件,不然当窗口大小变化的时候,resize方法被调用,但是display为none,
             导致切换过去尺寸又没了
           -->
+    <div class="chartsBox">
+      <el-tabs class="tabsBox" v-model="activeTab" @tab-change="tabChange">
         <el-tab-pane
           class="tabItem"
           :lazy="true"
@@ -70,17 +147,51 @@ onMounted(() => {
           :name="item.tabTitle"
           :key="item.tabTitle"
         >
-          <div class="yesterDayDataBox">
-            <StatisticText
-              :value-class="'chartStaticValue'"
-              :data-list="yesterdayData"
-            ></StatisticText>
+          <div class="chartContent" v-if="selectShape === 'trend'">
+            <div class="yesterDayDataBox">
+              <StatisticText
+                :request-config="compareDataReqConfig"
+                :value-class="'chartStaticValue'"
+                :data-list="yesterdayData"
+              ></StatisticText>
+            </div>
+            <TimeLineChart
+              :legend-data="chartInfo.legendData"
+              :series-data="chartInfo.seriesData"
+              :x-axis-data="chartInfo.xAxisData"
+              class="chart"
+              v-if="activeTab === item.tabTitle"
+            ></TimeLineChart>
+          </div>
+          <div class="tableContent" v-else>
+            <Table
+              :need-right-tools="false"
+              :pagination-config="paginationConfig"
+              :table-fields-info="filedsInfo"
+              :request-config="tableRequestConfig"
+              :need-left-tools="false"
+              :open-filter-query="false"
+              :open-page-query="false"
+            ></Table>
           </div>
-          <TimeLineChart v-if="activeTab === item.tabTitle"></TimeLineChart>
         </el-tab-pane>
       </el-tabs>
+      <div class="toolsBox">
+        <span class="toolItem" @click="changeSelectShape('trend')">
+          <el-icon :size="iconSize" :color="selectShape === 'trend' ? 'blue' : 'gray'"
+            ><TrendCharts
+          /></el-icon>
+        </span>
+        <span class="toolItem" @click="changeSelectShape('table')" style="margin-right: 10px">
+          <el-icon :size="iconSize" :color="selectShape === 'table' ? 'blue' : 'gray'"
+            ><Grid
+          /></el-icon>
+        </span>
+        <span class="toolItem">
+          <el-icon :size="iconSize"><Download /></el-icon>
+        </span>
+      </div>
     </div>
-    <div class="chartsBox"></div>
   </div>
 </template>
 
@@ -91,13 +202,17 @@ onMounted(() => {
   /* 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);
+  background-color: white;
 }
 
-.toolsBox {
+.headerTitle {
+  font-size: 14px;
+  color: #1c2438;
+  font-weight: 600;
+}
+.chartsBox {
   width: 100%;
+  position: relative;
 }
 
 .tabItem {
@@ -111,4 +226,20 @@ onMounted(() => {
   left: 24px;
   top: 0;
 }
+
+.chart {
+  top: 40px;
+}
+
+.toolsBox {
+  position: absolute;
+  right: 0;
+  top: 15px;
+  /* width: 100px; */
+}
+
+.toolItem {
+  padding-right: 10px;
+  cursor: pointer;
+}
 </style>

+ 90 - 148
src/components/echarts/TimeLineChart.vue

@@ -1,124 +1,55 @@
 <script setup lang="ts">
-import { onMounted, ref, shallowRef } from 'vue'
+import { onMounted, reactive, ref, shallowRef } from 'vue'
 import echarts from '.'
 import { nextTick } from 'vue'
 import { debounceFunc } from '@/utils/common'
+import type { OptionsProps } from '@/types/dataAnalysis'
 
-// 图标所需要的props
-interface OptionInfo {}
+// 图需要的props
+const props = defineProps<OptionsProps>()
 
-// 图实例
+// 图实例
 const linechart = shallowRef()
 
-// 图Ref
+// 图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'
-  }
+// 颜色
+const colorList = [
+  '#00e070',
+  '#1495eb',
+  '#993333', // Dark Red
+  '#FFD700', // Bright Yellow
+  '#FF00FF', // Magenta
+  '#FFA500', // Orange
+  '#800080', // Dark Purple
+  '#A52A2A', // Brown
+  '#FF4500', // Orange Red
+  '#FF6347', // Tomato
+  '#B22222', // Firebrick
+  '#FF1493' // Deep Pink
 ]
 
-// 生成时间刻度
-function generateHourlyArray(count: number) {
-  const result = []
-  for (let i = 1; i <= count; i++) {
-    result.push(`${i}:00`)
-  }
+// 格式化tooltip
+const formatterTooltip = (params: Array<any>) => {
+  let circle = `<span style="display:inline-block;margin-right:5px;border-radius:50%;width:10px;height:10px;left:5px;background-color:`
+  let result = `${params[0].axisValueLabel}`
+  params.map((item, index) => {
+    let data = `${circle}${colorList[index]}"></span> ${item['seriesName']}`
+    result += `<br/>${data}`
+  })
   return result
-}
 
-// 时间刻度lable
-const timeFrameLabel = generateHourlyArray(24)
+  // let data0 = `${circle}#1495eb"></span> ${params[0]['seriesName']}`
+  // let data1 = `${circle}#00cc66"></span> ${params[1]['seriesName']}`
+  // return `${params[0].axisValueLabel}<br/>${data0}<br/>${data1}`
+}
 
 // 选项
 const options = {
   xAxis: {
     type: 'category',
-    data: timeFrameLabel,
+    data: [],
     axisTick: {
       alignWithLabel: true
     }
@@ -127,69 +58,80 @@ const options = {
     type: 'value'
   },
   tooltip: {
-    trigger: 'axis'
+    trigger: 'axis',
+    formatter: formatterTooltip
   },
   legend: {
     orient: 'horizontal', //图例布局方式:水平 'horizontal' 、垂直 'vertical'
     x: 'right', // 横向放置位置,选项:'center'、'left'、'right'、'number'(横向值 px)
     y: 'top', // 纵向放置位置,选项:'top'、'bottom'、'center'、'number'(纵向值 px)
-    data: ['今日新增', '昨日新增']
+    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
-    }
-  ]
+  grid: {
+    left: '24px',
+    right: '10px',
+    containLabel: true
+  },
+  series: []
 }
 
-// 传过来的配置
-// defineProps<OptionInfo>()
+// 尺寸变化
 const changeSize = () => {
   nextTick(() => {
     linechart.value?.resize()
   })
 }
 
+// 初始化options
+const initOptions = () => {
+  // 基本的series数据配置
+  let baseSeries = {
+    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
+  }
+  // 最终的siries数组
+  let finalSeriesList: any = []
+  // const finalSeriesList: Array<any> = []
+
+  props.legendData.map((item, index) => {
+    const seriesClone = JSON.parse(JSON.stringify(baseSeries))
+    // 设置克隆的属性
+    seriesClone.name = item
+    seriesClone.data = props.seriesData[index]
+    seriesClone.itemStyle.borderColor = colorList[index]
+    seriesClone.lineStyle.color = colorList[index]
+
+    // 将克隆后的对象添加到 finalSeriesList 中
+    finalSeriesList.push(seriesClone)
+  })
+
+  options.series = finalSeriesList
+  options.legend.data = props.legendData as any
+  options.xAxis.data = props.xAxisData as any
+
+  linechart.value.setOption(options)
+}
+
 onMounted(() => {
   nextTick(() => {
     linechart.value = echarts.init(chart.value)
-    linechart.value.setOption(options)
+    initOptions()
     window.addEventListener('resize', debounceFunc(changeSize, 500))
   })
 })

+ 1 - 0
src/hooks/useRequest.ts

@@ -10,6 +10,7 @@ export function useRequest() {
   const baseIp = 'http://server.ichunhao.cn'
 
   const AllApi = {
+    mock: `http://127.0.0.1:8003/mock`,
     getGameTable: `${baseIp}/user/getGidConfig`, // 获取游戏列表
     getUserTable: `${baseIp}/user/userList`, // 获取用户列表
     addGame: `${baseIp}/user/addGidConfig`, // 添加/修改 游戏配置

+ 1 - 0
src/hooks/useTable.ts

@@ -19,6 +19,7 @@ export function useTable(tableData: Array<any>, paginationSetting: TablePaginati
               if (isPagination) {
                 tableData[paginationSetting.currentPage] = data
               } else {
+                console.log(data)
                 tableData.splice(0, tableData.length, ...data)
               }
 

+ 21 - 3
src/router/home.ts

@@ -1,8 +1,18 @@
-import GameManageView from '../views/Home/GameManageView.vue'
+/*
+ * @Author: fxs bjnsfxs@163.com
+ * @Date: 2024-08-20 14:24:58
+ * @LastEditors: fxs bjnsfxs@163.com
+ * @LastEditTime: 2024-08-27 17:18:07
+ * @FilePath: \Game-Backstage-Management-System\src\router\home.ts
+ * @Description:
+ *
+ */
 
-import PlayerManageView from '../views/Home/PlayerManageView.vue'
+import GameManageView from '../views/Home/InfoManage/GameManageView.vue'
+import PlayerManageView from '../views/Home/InfoManage/PlayerManageView.vue'
 import HomeView from '@/views/Home/HomeView.vue'
-import OverView from '@/views/Home/OverView.vue'
+import OverView from '@/views/Home/Overview/OverView.vue'
+import KeepView from '@/views/Home/Analysis/KeepView.vue'
 
 export default [
   {
@@ -36,6 +46,14 @@ export default [
         meta: {
           needKeepAlive: true
         }
+      },
+      {
+        path: 'keepView',
+        name: 'KeepView',
+        component: KeepView,
+        meta: {
+          needKeepAlive: true
+        }
       }
     ]
   }

+ 37 - 8
src/types/dataAnalysis.ts

@@ -1,3 +1,12 @@
+/*
+ * @Author: fxs bjnsfxs@163.com
+ * @Date: 2024-08-23 14:58:29
+ * @LastEditors: fxs bjnsfxs@163.com
+ * @LastEditTime: 2024-08-27 16:10:30
+ * @FilePath: \Game-Backstage-Management-System\src\types\dataAnalysis.ts
+ * @Description:用于dataAnalysis相关组件的type
+ *
+ */
 // 下拉选项的信息
 export interface DropDownItem {
   value: any
@@ -18,21 +27,27 @@ export enum StaticDataType {
   DATE = 'date'
 }
 
-export interface CompareInfo {
-  compareVal: any
-}
-
 // 数据分析页面头部数据信息
 export interface StaticDataItemInfo {
   dataTitle: string // 标题
   dataType: StaticDataType // 什么类型的数据
   value: any // 值
-  compareInfo?: CompareInfo
+  compareVal?: any
+}
+
+/**
+ * @description: 数据展示组件的请求配置
+ *
+ * @return {*}
+ */
+export interface StaticReqConfig {
+  url: string
+  otherOptions: any
 }
 
 // 数据分析页面头部数据所需要的props
 export interface StaticDataInfo {
-  dataList: Array<StaticDataItemInfo>
+  requestConfig: StaticReqConfig // 请求参数配置
   titleClass?: string // 标题的样式
   valueClass?: string // 值的样式
 }
@@ -43,8 +58,22 @@ export interface TabInfo {
   url: string // 到时候需要获取数据的url
 }
 
+/**
+ * @description: 图表所需要的props,其中的legnedData毓seriesData的length要一致
+ *              series中的name依赖毓lengdata
+ * @return {*}
+ */
+export interface OptionsProps {
+  xAxisData: Array<any> // x轴的刻度lable
+  legendData: Array<any> // 图例的信息
+  seriesData: Array<any> // 数据
+}
+
 // 时间趋势组件所需要的props
 export interface TemporalTrendProps {
-  tabInfo: Array<TabInfo>
-  defaultActive: string // 默认选中
+  tabInfo: Array<TabInfo> // 用于切换的tab的信息
+  requestConfig: StaticReqConfig // 请求参数配置,需要监听,一旦变化,需要重新获取数据
+  defaultActive: string // 默认选中的tab
+  chartInfo: OptionsProps // 图表的option配置
+  title: string // 上方显示的title
 }

+ 1 - 0
src/types/table.ts

@@ -67,6 +67,7 @@ export interface TableFieldInfo {
 // props的参数格式
 export interface PropsParams {
   needLeftTools: boolean // 是否需要左侧的工具栏
+  needRightTools: boolean // 是否需要右侧工具栏
   openFilterQuery: boolean // 是否开启上方查询功能
   openPageQuery: boolean // 是否开启分页查询
   queryInfo?: Array<QueryInfo> // 上方查询功能所需要的信息

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

@@ -12,6 +12,7 @@ export function debounceFunc<T extends (...args: any[]) => any>(
   }
 }
 
+// 小数转百分比
 export function decimalToPercentage(decimal: number, decimalPlaces: number = 0): string {
   // Convert the decimal to a percentage by multiplying by 100
   const percentage = decimal * 100
@@ -22,3 +23,27 @@ export function decimalToPercentage(decimal: number, decimalPlaces: number = 0):
   // Append the '%' symbol and return the result
   return `${formattedPercentage}%`
 }
+
+// 生成时间刻度
+export function generateHourlyArray(count: number) {
+  const result = []
+  for (let i = 1; i <= count; i++) {
+    result.push(`${i}:00`)
+  }
+  return result
+}
+
+// 格式化时间,20240816=>2024-8-16
+export function formatDate(dateString: string) {
+  // 从字符串中提取年份、月份和日期
+  const year = dateString.slice(0, 4)
+  const month = dateString.slice(4, 6)
+  const day = dateString.slice(6, 8)
+
+  // 将月份和日期转换为整数以去除前导零
+  const formattedMonth = parseInt(month, 10)
+  const formattedDay = parseInt(day, 10)
+
+  // 生成新的日期字符串
+  return `${year}-${formattedMonth}-${formattedDay}`
+}

+ 15 - 10
src/utils/common/time.ts

@@ -1,19 +1,24 @@
+/*
+ * @Author: fxs bjnsfxs@163.com
+ * @Date: 2024-08-26 14:06:51
+ * @LastEditors: fxs bjnsfxs@163.com
+ * @LastEditTime: 2024-08-27 16:44:39
+ * @FilePath: \Game-Backstage-Management-System\src\utils\common\time.ts
+ * @Description:
+ *
+ */
 // 待定格式
-export function resetTimeToMidnight(dateTimeStr: string): string {
+export function resetTimeToMidnight(dateTime: Date): string {
   // 创建一个 Date 对象来解析输入的日期时间字符串
-  const date = new Date(dateTimeStr)
 
   // 将时间部分设置为 00:00:00
-  date.setHours(0, 0, 0, 0)
+  dateTime.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')
+  const year = dateTime.getFullYear()
+  const month = String(dateTime.getMonth() + 1).padStart(2, '0') // 月份从0开始,需要加1
+  const day = String(dateTime.getDate()).padStart(2, '0')
 
   // 返回格式化的字符串
-  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+  return `${year}-${month}-${day}`
 }

+ 18 - 0
src/views/Home/Analysis/KeepView.vue

@@ -0,0 +1,18 @@
+<!--
+ * @Author: fxs bjnsfxs@163.com
+ * @Date: 2024-08-27 17:11:23
+ * @LastEditors: fxs bjnsfxs@163.com
+ * @LastEditTime: 2024-08-27 17:13:44
+ * @FilePath: \Game-Backstage-Management-System\src\views\Home\Analysis\KeepView.vue
+ * @Description: 
+ * 
+-->
+<script setup lang="ts">
+import Table from '@/components/Table.vue'
+import HeaderCard from '@/components/dataAnalysis/HeaderCard.vue'
+</script>
+<template>
+  <div class="KeepViewBox"></div>
+</template>
+
+<style scoped></style>

+ 10 - 0
src/views/Home/HomeView.vue

@@ -31,6 +31,16 @@ const menuList = [
         title: '玩家管理'
       }
     ]
+  },
+  {
+    title: '数据分析',
+    icon: 'DataAnalysis',
+    children: [
+      {
+        pathName: 'KeepView',
+        title: '留存分析'
+      }
+    ]
   }
 ]
 

+ 1 - 0
src/views/Home/GameManageView.vue → src/views/Home/InfoManage/GameManageView.vue

@@ -211,6 +211,7 @@ const submiteGameChange = () => {
   <div class="gameMangeBox">
     <Table
       ref="gameTableRef"
+      :need-right-tools="true"
       :need-left-tools="true"
       :open-filter-query="false"
       :open-page-query="false"

+ 1 - 0
src/views/Home/PlayerManageView.vue → src/views/Home/InfoManage/PlayerManageView.vue

@@ -248,6 +248,7 @@ onMounted(() => {
   <div class="gameMangeBox">
     <Table
       ref="playerTableRef"
+      :need-right-tools="true"
       :need-left-tools="false"
       :open-page-query="true"
       :open-filter-query="true"

+ 0 - 83
src/views/Home/OverView.vue

@@ -1,83 +0,0 @@
-<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">
-      <HeaderCard :title="'数据总览'"></HeaderCard>
-    </div>
-    <div class="staticBox">
-      <StatisticText :dataList="staticData"></StatisticText>
-    </div>
-    <div class="temporalTrendBox">
-      <TemporalTrend :defaultActive="'新增用户'" :tabInfo="tabInfoList"></TemporalTrend>
-    </div>
-  </div>
-</template>
-
-<style scoped>
-.overViewBox {
-  width: 98%;
-  margin: 1% auto;
-  background-color: white;
-  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>

+ 221 - 0
src/views/Home/Overview/OverView.vue

@@ -0,0 +1,221 @@
+<script setup lang="ts">
+import HeaderCard from '@/components/dataAnalysis/HeaderCard.vue'
+import { onMounted, reactive, watch } from 'vue'
+import type {
+  StaticDataItemInfo,
+  TabInfo,
+  OptionsProps,
+  StaticReqConfig
+} from '@/types/dataAnalysis'
+import { StaticDataType } from '@/types/dataAnalysis'
+import StatisticText from '@/components/dataAnalysis/StatisticText.vue'
+import TemporalTrend from '@/components/dataAnalysis/TemporalTrend.vue'
+import { formatDate, generateHourlyArray } from '@/utils/common'
+
+// 目前选择的信息
+// 这里不太合理,应该根据返回的数据中的pf和game赋值,因为这个可能会变
+const selectInfo = reactive({
+  pf: 'web',
+  game: '1001',
+
+  periodSelect: '新增用户',
+  onMonthSelect: '留存用户'
+})
+
+// 总览请求参数的配置
+const overViewDataReqConfig = reactive<StaticReqConfig>({
+  url: '',
+  otherOptions: {
+    pf: 'web',
+    gid: '1001'
+  }
+})
+
+// 分时段组件请求参数配置
+const periodDataReqConfig = reactive<StaticReqConfig>({
+  url: '',
+  otherOptions: {}
+})
+
+// 一个月内组件请求参数配置
+const monthDataReqConfig = reactive<StaticReqConfig>({
+  url: '',
+  otherOptions: {}
+})
+
+/**
+ * @description: 监听pf和game的变化,数据变化后立即重新请求所有相关数据
+ * @tip watch监听reactive的数据时,必须以getter形式,不然会警告
+ * @return {*}
+ */
+watch(
+  () => [selectInfo.game, selectInfo.pf],
+  () => {}
+)
+
+/**
+ * @description: 选择游戏改变
+ * @param {*} game 游戏名
+ * @return {*}
+ */
+const changeGame = (game: string) => {
+  selectInfo.game = game
+}
+
+/**
+ * @description: 选择的平台改变
+ * @param {*} pf  平台名
+ * @return {*}
+ */
+const changePf = (pf: string) => {
+  selectInfo.pf = pf
+}
+
+// 模拟时段数据需要的tab信息
+const periodTabList = reactive<Array<TabInfo>>([
+  {
+    tabTitle: '新增用户',
+    url: 'example'
+  },
+  {
+    tabTitle: '活跃用户',
+    url: 'example'
+  }
+])
+
+// 模拟一个月内数据需要的tab信息
+const monthTabList = reactive<Array<TabInfo>>([
+  {
+    tabTitle: '新增用户',
+    url: 'example'
+  },
+  {
+    tabTitle: '活跃用户',
+    url: 'example'
+  },
+  {
+    tabTitle: '留存率',
+    url: 'example'
+  }
+])
+
+// 模拟时间段图表数据
+const periodData = reactive<OptionsProps>({
+  xAxisData: generateHourlyArray(24),
+  legendData: ['今日新增', '昨日新增'],
+  seriesData: [
+    [1, 23, 3, 4, 5, 566, 7, 7],
+    [1, 23, 34, 42, 5, 6, 71, 7]
+  ]
+})
+
+// 一个月的日期模拟
+const oneMonthDate = [
+  '20240801',
+  '20240802',
+  '20240803',
+  '20240804',
+  '20240805',
+  '20240806',
+  '20240807',
+  '20240808',
+  '20240809',
+  '20240810',
+  '20240811',
+  '20240812',
+  '20240813',
+  '20240814',
+  '20240815',
+  '20240816',
+  '20240817',
+  '20240818',
+  '20240819',
+  '20240820',
+  '20240821',
+  '20240822',
+  '20240823',
+  '20240824',
+  '20240825',
+  '20240826',
+  '20240827',
+  '20240828',
+  '20240829',
+  '20240830',
+  '20240831'
+]
+
+// 模拟30天内图表数据
+const oneMonthData = reactive<OptionsProps>({
+  xAxisData: oneMonthDate.map((item) => formatDate(item)),
+  legendData: ['今日新增', '昨日新增'],
+  seriesData: [
+    [1, 23, 3, 4, 5, 566, 7, 7],
+    [1, 23, 34, 42, 5, 6, 71, 7]
+  ]
+})
+
+onMounted(() => {})
+</script>
+<template>
+  <div class="overViewBox">
+    <div class="header">
+      <HeaderCard
+        :open-date-select="true"
+        :default-game="selectInfo.game"
+        :default-pf="selectInfo.pf"
+        @change-game="changeGame"
+        @change-pf="changePf"
+        :title="'数据总览'"
+      ></HeaderCard>
+    </div>
+    <div class="staticBox">
+      <StatisticText :request-config="overViewDataReqConfig"></StatisticText>
+    </div>
+    <div class="periodTrendBox">
+      <TemporalTrend
+        :request-config="periodDataReqConfig"
+        :title="'时段分布'"
+        :chart-info="periodData"
+        :defaultActive="'新增用户'"
+        :tabInfo="periodTabList"
+      ></TemporalTrend>
+    </div>
+    <div class="oneMonthTrendBox">
+      <TemporalTrend
+        :request-config="monthDataReqConfig"
+        :title="'30日内趋势'"
+        :chart-info="oneMonthData"
+        :defaultActive="'新增用户'"
+        :tabInfo="monthTabList"
+      ></TemporalTrend>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.overViewBox {
+  width: 98%;
+  margin: 1% auto;
+  /* background-color: white; */
+  box-sizing: border-box;
+  /* border: 1px solid #e5e6eb; */
+}
+
+.header,
+.staticBox,
+.periodTrendBox,
+.oneMonthTrendBox {
+  box-sizing: border-box;
+  background-color: white;
+  width: 100%;
+  padding: 0 24px;
+}
+.header {
+  padding: 0;
+  box-shadow: none;
+}
+
+.staticBox {
+  margin-bottom: 2px;
+}
+</style>