浏览代码

更新主页图表数据配置;更新主页图表数据获取方式;更新图表数据筛选功能;更新表格固定表头功能;优化表格横向滑动功能,现在只要表格可见,横向滚动条即可用;更新table组件目录结构;

fxs 7 月之前
父节点
当前提交
04ff2028a4

+ 2 - 0
components.d.ts

@@ -26,6 +26,7 @@ declare module 'vue' {
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
     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']
@@ -45,6 +46,7 @@ declare module 'vue' {
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     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']
   }
 }

+ 68 - 32
src/components/echarts/HomeAnalysisLine.vue

@@ -12,6 +12,7 @@ interface Props {
   loading: boolean
   legend: Array<LegendInfo>
   data: Array<any>
+  xAxisDataField: string // 用于标明x轴数据字段是来自数据中的哪个字段
 }
 
 const props = defineProps<Props>()
@@ -21,18 +22,22 @@ const props = defineProps<Props>()
  * @param {*} params 表格数据
  * @return {*}
  */
-// const formatterTooltip = (params: any) => {
-//   let circle = `<span style="display:inline-block;margin-right:5px;border-radius:50%;width:10px;height:10px;left:5px;background-color:`
-//   let result = `<span style="font-weight:bold;">${params[0].axisValueLabel}</span>`
+// const formatterTooltip = (params: Object | Array<any>) => {
+//   if (Array.isArray(params)) {
+//     let circle = `<span style="display:inline-block;margin-right:5px;border-radius:50%;width:10px;height:10px;left:5px;background-color:`
+//     let result = `<span style="font-weight:bold;">${params[0].axisValueLabel}</span>`
 
-//   params.map((item, index) => {
-//     let data = `${circle}${colorList[index]}"></span>
+//     params.map((item, index) => {
+//       let data = `${circle}${colorList[index]}"></span>
 //     <span >
 //     <span  style="display: inline-block;  box-sizing: border-box;
 //   padding-right: 50px;">${item['seriesName']}</span><span>${item['value']}<span>`
-//     result += `<br/>${data}`
-//   })
-//   return result
+//       result += `<br/>${data}`
+//     })
+//     return result
+//   } else {
+//     return `{b0}: {c0}<br />{b1}: {c1}`
+//   }
 // }
 
 // 颜色
@@ -58,11 +63,6 @@ let baseOptions: EChartsOption = {
     bottom: '3%',
     containLabel: true,
   },
-  toolbox: {
-    feature: {
-      saveAsImage: {},
-    },
-  },
   xAxis: {
     type: 'category',
 
@@ -79,6 +79,7 @@ let baseOptions: EChartsOption = {
   },
   tooltip: {
     trigger: 'axis',
+    // formatter: formatterTooltip,
   },
   series: [],
 }
@@ -156,27 +157,55 @@ const initChart = () => {
   chartSizeOb.observe(chartRef.value!)
 }
 
+/**
+ * @description: 创建一条series数据
+ * @return {*}
+ */
+const createSeries = (): SeriesOption[] => {
+  let finalSeriesData: SeriesOption[] = []
+  // 图例中包含了图例的name和颜色,那么根据这个name,
+  // 去data中找到对应字段的值,形成一个series需要的data
+  props.legend.forEach((item, index) => {
+    let newSeries: SeriesOption = cloneDeep(baseSeries)
+    let cData = cloneDeep(props.data)
+
+    // 取出一个legend的里面的name,找到data中每条数据对应字段的值
+    let newData = cData.map((item: any) => item[props.legend[index].value])
+    newSeries.name = item.value
+    newSeries.data = newData
+    newSeries.lineStyle!.color = item.color // 这里是线的颜色
+    newSeries.itemStyle!.color = colorList[index]
+    finalSeriesData.push(newSeries)
+  })
+  return finalSeriesData
+}
+
+/**
+ * @description: 创建xAxis的data
+ * @return {*}
+ */
+const createXAxisData = (): string[] => {
+  if (props.data.length > 0) {
+    let result: string[] = props.data.map(
+      (item: any) => item[props.xAxisDataField],
+    )
+    return result
+  }
+  return []
+}
+
+/**
+ * @description: 初始化选项
+ * @return {*}
+ */
 const initOptions = () => {
   if (!isExistChart()) return
   chartInstance.value!.clear()
   if (props.data.length > 0) {
-    // ;(baseOptions.series as SeriesOption[])[0].data = props.data
-    let finalSeriesData: SeriesOption[] = []
-    props.legend.forEach((item, index) => {
-      let newSeries: SeriesOption = cloneDeep(baseSeries)
-      let cData = cloneDeep(props.data)
-
-      let newData = cData.map((item: any) => {
-        return item[props.legend[index].value]
-      })
-      newSeries.name = item.value
-      newSeries.data = newData
-      newSeries.lineStyle!.color = item.color
-      newSeries.itemStyle!.color = colorList[index]
-      finalSeriesData.push(newSeries)
-    })
+    let finalSeriesData = createSeries()
+    let xAxisData = createXAxisData()
     baseOptions.series = finalSeriesData
-    console.log(JSON.parse(JSON.stringify(baseOptions)))
+    ;(baseOptions.xAxis as any).data = xAxisData
   } else {
     baseOptions = {
       title: {
@@ -188,16 +217,20 @@ const initOptions = () => {
       },
     }
   }
-  console.log(cloneDeep(baseOptions))
+
   chartInstance.value!.setOption(baseOptions)
 }
 
+/**
+ * @description: 观察loading状态,如果变为false证明加载完毕,需要重新设置一次数据
+ * @return {*}
+ */
 watch(
   () => props.loading,
   (newState: boolean) => {
     changeChartsLoading(newState)
     if (newState === false) {
-      initChart()
+      if (!chartInstance.value) initChart()
       initOptions()
       console.log(props.data)
     }
@@ -207,13 +240,16 @@ watch(
   { deep: true },
 )
 
+/**
+ * @description: 当图例的信息变化的时候也需要重新设置选项
+ * @return {*}
+ */
 watch(
   () => props.legend,
   newLegned => {
     initOptions()
     console.log(newLegned)
   },
-  { deep: true },
 )
 </script>
 

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

@@ -8,9 +8,16 @@ interface Props {
   }
   selectStyle?: string
 }
+
 withDefaults(defineProps<Props>(), {
   selectStyle: 'width: 150px; margin-right: 10px',
 })
+
+const emits = defineEmits(['changeSelect'])
+
+const changeSelect = (newVal: any, name: string) => {
+  emits('changeSelect', newVal, name)
+}
 </script>
 
 <template>
@@ -25,6 +32,7 @@ withDefaults(defineProps<Props>(), {
         <el-select
           v-model="selectItem.value"
           placeholder="Select"
+          @change="changeSelect(selectItem.value, selectItem.name)"
           v-bind="$attrs"
           :multiple="
             selectItem.type === CSelectType.Multiple ||

+ 125 - 316
src/components/table/Table.vue

@@ -2,42 +2,86 @@
 import type {
   BaseFieldItem,
   TableFields,
-  TableFilterItem,
   TableProps,
 } from '@/types/Tables/table'
-import type { FormInstance } from 'element-plus'
-import type { TableData, BaseFieldInfo } from '@/types/Tables/table'
 
-import { TableFilterType } from '@/types/Tables/table'
-import { resetReactive } from '@/utils/common'
-import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
-import { Search } from '@element-plus/icons-vue'
+import type { TableData } from '@/types/Tables/table'
+import type { PaginationConfig } from '@/types/Tables/pagination'
+
+import { computed, onMounted, reactive, ref, watch } from 'vue'
+
 import { Plus, Operation } from '@element-plus/icons-vue'
 import { useTable } from '@/hooks/useTable'
-import { useDate } from '@/hooks/useDate'
+import { useTableScroll } from '@/hooks/useTableScroll'
 
 import customIndicatorDialog from '../dialog/customIndicatorDialog.vue'
+import TableQueryForm from './TableQueryForm.vue'
+
+type TableQueryForm = InstanceType<typeof TableQueryForm>
+type CustomIndicatorDialog = InstanceType<typeof customIndicatorDialog>
+
+const props = withDefaults(defineProps<TableProps>(), {
+  remotePagination: false,
+})
 
-type CustomIndicatorDialog = typeof customIndicatorDialog
+// table 容器
+const tableContent = ref<HTMLElement | null>(null)
 
-const props = withDefaults(defineProps<TableProps>(), {})
+// table的横向滚动条
+const elScrollBarH = ref<HTMLElement | null>(null)
 
-const emits = defineEmits(['updateCustomIndicator'])
+// 表头
+const tableHeaderRef = ref<HTMLElement | null>(null)
 
-const { isFixedField } = useTable()
-const { disableDate, shortcuts } = useDate()
+// 表格整体容器
+const tableContainer = ref<HTMLElement | null>(null)
+
+// 自定义指标对话框Ref
+const customIndicatorDialogRef = ref<CustomIndicatorDialog>()
 
+// 表格上方查询表单
+const tableQueryFormRef = ref<TableQueryForm>()
+
+const emits = defineEmits([
+  'updateCustomIndicator',
+  'pageSizeChange',
+  'curPageChange',
+])
+
+const {
+  isFixedField,
+  initTableFields,
+  initIndicatorFields,
+  setCacheTableData,
+} = useTable()
+
+const { initScroll, setScrollAndHeader, obScroll } = useTableScroll(
+  elScrollBarH,
+  tableContent,
+  tableContainer,
+  tableHeaderRef,
+)
+
+const rowHeight = 56 // 表格行高
 const tableFieldLWidth = 200 // 长单元格的宽度
 const tableFieldSWidth = 150 // 短单元格的宽度
 
 // 表格数据
 const tableData = reactive<Array<TableData>>(props.tableData)
 
-// table 容器
-const tableContent = ref<HTMLElement>()
+// 缓存的表格数据,如果使用了远程分页,则将每一页的表格数据缓存下来
+const cacheTableData = reactive<Array<Array<TableData>>>([])
 
-// table的横向滚动条
-const elScrollBarH = ref<HTMLElement>()
+// 表格分页信息
+const paginationConfig = reactive<PaginationConfig>({
+  curPage: 1,
+  curPageSize: 0,
+  pageSizeList: [],
+  total: 0,
+})
+
+// 表格可见性的观察者
+const tableVisOb = new IntersectionObserver(obScroll)
 
 // 表格字段信息
 const tableFieldsInfo = reactive<Array<TableFields>>([])
@@ -45,35 +89,9 @@ const tableFieldsInfo = reactive<Array<TableFields>>([])
 // 自定义指标的字段信息,为了不影响原本的表格字段信息
 const indicatorFields = reactive<Array<BaseFieldItem<TableFields>>>([])
 
-// 表格整体容器
-const tableContainer = ref<HTMLElement>()
-
 // 自定义指标侧边栏的默认选中
 const defaultActiveNav = ref<string>('')
 
-// 表单数据
-const filterFormData = reactive<{
-  [key: string]: any
-}>({})
-
-// 表单搜索框所对应的字段,因为可能是多选,会在初始化的时候给初值
-const searchSelected = ref('')
-
-// 表单ref
-const filterFormRef = ref<FormInstance>()
-
-// 过滤表单字段的状态信息
-const filterFieldsStateList = reactive<
-  Array<{
-    label: string
-    state: boolean
-    value: any
-  }>
->([])
-
-// 当前已经选中的展示的过滤字段
-const filterFields = ref<Array<any>>([])
-
 // 批量操作选中的值
 const batchOper = ref<string>()
 
@@ -94,171 +112,58 @@ const batchOperList = reactive<
   },
 ])
 
-/**
- * @description: 初始化自定义指标的字段信息
- * @return {*}
- */
-const initIndicatorFields = () => {
-  resetReactive(indicatorFields)
-  indicatorFields.splice(
-    0,
-    indicatorFields.length,
-    ...JSON.parse(JSON.stringify(props.tableFields)),
-  )
-  defaultActiveNav.value = indicatorFields[0].name
-}
-
-/**
- * @description: 初始化表格字段,把所有字段都放到一个数组中
- * @return {*}
- */
-const initTableFields = () => {
-  resetReactive(tableFieldsInfo)
-  if (props.sortedTableFields.length > 0) {
-    tableFieldsInfo.splice(
-      0,
-      tableFieldsInfo.length,
-      ...props.sortedTableFields,
-    )
+// 分页后的表格数据
+const paginationTableData = computed<Array<TableData>>(() => {
+  let result: Array<TableData> = []
+  if (props.remotePagination) {
+    result = cacheTableData[paginationConfig.curPage - 1]
   } else {
-    indicatorFields.forEach(item => {
-      item.children.forEach(child => {
-        if (child.fixed || child.state) {
-          item.value.push(child.name)
-          child.state = true
-        }
-        tableFieldsInfo.push(JSON.parse(JSON.stringify(child)))
-      })
-    })
-  }
-}
-
-// 自定义指标中需要固定的指标
-
-/**
- * @description: 初始化查询表单
- * @return {*}
- */
-const initfilterForm = () => {
-  // filterFormData
-  resetReactive(filterFormData)
-  resetReactive(filterFieldsStateList)
-  for (let [k, v] of Object.entries(props.filtersInfo)) {
-    let val = JSON.parse(JSON.stringify(v)) as TableFilterItem
-    // 如果是搜索框,需要赋初值字段,即作为搜索的字段
-    if (k === 'search') {
-      searchSelected.value = val.value
-    }
-    // 把时间类型给个默认值
-    if (val.type === TableFilterType.Date) {
-      val.value = [shortcuts[0].value()[0], shortcuts[0].value()[1]]
-    }
-
-    // 表单给初始值
-    filterFormData[k] = val.value ? val.value : ''
-
-    // 初始化查询表单字段的状态信息
-    filterFieldsStateList.push({
-      label: val.label,
-      state: true,
-      value: val.name,
-    })
+    let curPage = paginationConfig.curPage
+    let pageSize = paginationConfig.curPageSize
+    result = tableData.slice((curPage - 1) * pageSize, curPage * pageSize)
   }
-}
-
-/**
- * @description: 初始化过滤字段,把所有字段展示出来
- * @return {*}
- */
-const initFilterFields = () => {
-  resetReactive(filterFields.value)
-  filterFieldsStateList.forEach(v => {
-    filterFields.value.push(v.value)
-  })
-}
-
-/**
- * @description: 重置查询表单
- * @return {*}
- */
-const resetFilterForm = () => {
-  filterFormRef.value?.resetFields()
-}
-
-/**
- * @description:根据现有的筛选字段重新生成查询参数
- * @return {[key: string]: any}  查询参数
- */
-const createQueryParams = (): {
-  [key: string]: any
-} => {
-  let queryParams: {
-    [key: string]: any
-  } = {}
-  for (let i in filterFormData) {
-    // 当前字段没有被隐藏,则加入查询参数
-    if (filterFields.value.includes(i)) {
-      queryParams[i] = filterFormData[i]
-    }
-  }
-  return queryParams
-}
+  return result
+})
 
 /**
  * @description: 查询表格
  * @return {*}
  */
-const queryTable = () => {
-  let queryParams: {
-    [key: string]: any
-  } = createQueryParams()
-}
+const queryTable = () => {}
+
+const resetTable = () => {}
 
 /**
- * @description: 判定当前横向滑动条是否在表格内,有BUG
- *
+ * @description: 初始化分页配置项
  * @return {*}
  */
-const isIntable = () => {
-  if (!tableContainer.value && !tableContent.value) return false
-  const { scrollTop, clientHeight, scrollHeight } =
-    tableContainer.value as HTMLElement
-
-  return scrollTop + clientHeight < scrollHeight
+const initPagination = () => {
+  Object.assign(paginationConfig, { ...props.paginationConfig })
+  paginationConfig.curPageSize = paginationConfig.pageSizeList[0]
 }
 
 /**
- * @description: 初始化滑动条
+ * @description: 表格分页大小改变
  * @return {*}
  */
-const initScroll = () => {
-  let sc = document.querySelector('.el-scrollbar__bar') as HTMLElement
-
-  elScrollBarH.value = sc
-  if (isIntable()) {
-    elScrollBarH.value.style.position = 'fixed'
-    elScrollBarH.value.style.left =
-      tableContent.value?.getBoundingClientRect().left + 'px'
+const tableSizeChange = (size: number) => {
+  // 如果当前是远程分页,则需要通知父组件重新请求数据
+  if (props.remotePagination) {
+    emits('pageSizeChange', size)
   }
+  paginationConfig.curPage = 1
 }
 
 /**
- * @description: 设置横向滑动条的位置
- * 只有滑动位置到达表格内部时,才需要固定横向滑动条,否则让滚动条回到原来的初始位置,即表格的底部
+ * @description: 表格页数改变
  * @return {*}
  */
-const setElScrollBar = () => {
-  if (isIntable()) {
-    elScrollBarH.value!.style.position = 'fixed'
-    elScrollBarH.value!.style.left =
-      tableContent.value?.getBoundingClientRect().left + 'px'
-  } else {
-    elScrollBarH.value!.style.position = 'absolute'
+const tablePageChange = (page: number) => {
+  if (props.remotePagination) {
+    emits('curPageChange', page)
   }
 }
 
-const customIndicatorDialogRef = ref<CustomIndicatorDialog>()
-
 /**
  * @description: 展示自定义指标弹窗
  * @return {*}
@@ -283,10 +188,18 @@ watch(
   () => props.tableData,
   newData => {
     tableData.splice(0, tableData.length, ...newData)
-    initfilterForm()
-    initFilterFields()
-    initIndicatorFields()
-    initTableFields()
+    initPagination()
+    setCacheTableData(
+      props.remotePagination,
+      paginationConfig,
+      cacheTableData,
+      newData,
+    )
+
+    tableQueryFormRef.value?.initfilterForm()
+    tableQueryFormRef.value?.initFilterFields()
+    initIndicatorFields(indicatorFields, props.tableFields, defaultActiveNav)
+    initTableFields(tableFieldsInfo, props.sortedTableFields, indicatorFields)
   },
   {
     deep: true,
@@ -294,129 +207,19 @@ watch(
 )
 onMounted(() => {
   initScroll()
+  tableVisOb.observe(tableContent.value as HTMLElement)
 })
 </script>
 
 <template>
-  <div class="tableContainer" ref="tableContainer" @scroll="setElScrollBar">
+  <div class="tableContainer" ref="tableContainer" @scroll="setScrollAndHeader">
     <div class="testContainer">
-      <div class="filterContainer">
-        <div class="filterContent">
-          <el-form
-            :model="filterFormData"
-            label-width="0"
-            ref="filterFormRef"
-            :inline="true"
-          >
-            <!-- <div class="filterFields"> -->
-            <el-form-item>
-              <el-input
-                :readonly="true"
-                placeholder="筛选字段"
-                prefix-icon="Filter"
-                style="width: 220px; margin-bottom: 10px"
-              >
-                <template #append>
-                  <el-select
-                    v-model="filterFields"
-                    multiple
-                    collapse-tags
-                    collapse-tags-tooltip
-                    placeholder="Select"
-                    style="width: 110px"
-                  >
-                    <el-option
-                      v-for="item in filterFieldsStateList"
-                      :key="item.value"
-                      :label="item.label"
-                      :value="item.value"
-                    />
-                  </el-select>
-                </template>
-              </el-input>
-            </el-form-item>
-            <!-- </div> -->
-            <template v-for="item in filtersInfo" :key="item.name">
-              <el-form-item :prop="item.name">
-                <el-input
-                  v-model="filterFormData[item.name]"
-                  style="max-width: 600px"
-                  placeholder="输入关键字搜索"
-                  class="input-with-select"
-                  v-if="
-                    filterFields.includes(item.name) &&
-                    item.type === TableFilterType.Search
-                  "
-                >
-                  <template #prepend>
-                    <el-select v-model="searchSelected" style="width: 115px">
-                      <el-option
-                        v-for="option in item.options"
-                        :label="option.label"
-                        :value="option.value"
-                      />
-                    </el-select>
-                  </template>
-                  <template #append>
-                    <el-button :icon="Search" />
-                  </template>
-                </el-input>
-
-                <el-select
-                  class="filterItem"
-                  v-if="
-                    filterFields.includes(item.name) &&
-                    item.type === TableFilterType.Select
-                  "
-                  v-model="filterFormData[item.name]"
-                  placeholder="Select"
-                  style="width: 240px"
-                >
-                  <template #label="{ label, value }">
-                    <span>{{ item.label }}: </span>
-                    <span style="margin-left: 10px">{{ value }}</span>
-                  </template>
-                  <el-option
-                    v-for="option in item.options"
-                    :key="option.value"
-                    :label="option.label"
-                    :value="option.value"
-                  />
-                </el-select>
-
-                <el-date-picker
-                  v-if="
-                    filterFields.includes(item.name) &&
-                    item.type === TableFilterType.Date
-                  "
-                  v-model="filterFormData[item.name]"
-                  type="daterange"
-                  unlink-panels
-                  :disableDate="disableDate"
-                  range-separator="至"
-                  :default-value="[item.startDate, item.endDate]"
-                  start-placeholder="开始时间"
-                  end-placeholder="结束时间"
-                  :shortcuts="shortcuts"
-                  size="small"
-                />
-              </el-form-item>
-            </template>
-          </el-form>
-        </div>
-        <div class="filterButtonContainer">
-          <el-button class="queryBtn" color="#197afb" @click="queryTable"
-            >查询</el-button
-          >
-          <el-button
-            class="queryBtn"
-            color="#626aef"
-            plain
-            @click="resetFilterForm"
-            >重置</el-button
-          >
-        </div>
-      </div>
+      <TableQueryForm
+        ref="tableQueryFormRef"
+        :filters-info="props.filtersInfo"
+        @query-table="queryTable"
+        @reset-table="resetTable"
+      ></TableQueryForm>
       <div class="operationContainer">
         <div class="tableOperationLeft">
           <slot name="addItem">
@@ -456,11 +259,12 @@ onMounted(() => {
       </div>
       <div class="tableContent" ref="tableContent">
         <el-table
-          v-bind="{ ...$attrs, data: tableData }"
+          v-bind="{ ...$attrs, data: paginationTableData }"
           style="width: 100%"
           border
           table-layout="fixed"
           :scrollbar-always-on="true"
+          :row-style="() => `height:${rowHeight}px`"
         >
           <template v-for="item in tableFieldsInfo" :key="item.name">
             <el-table-column
@@ -468,14 +272,14 @@ onMounted(() => {
               label="操作"
               fixed
               width="160"
+              show-overflow-tooltip
             >
               <template #default="scope">
-                <slot name="operations" :row="scope.row">
-                  <!-- {{ scope.row[item.prop] }} -->
-                </slot>
+                <slot name="operations" :row="scope.row"> </slot>
               </template>
             </el-table-column>
             <el-table-column
+              show-overflow-tooltip
               :width="
                 isFixedField(props.fixedFields, item)
                   ? tableFieldLWidth
@@ -488,14 +292,19 @@ onMounted(() => {
             >
             </el-table-column>
           </template>
-          <!-- <el-table-column label="操作" fixed width="200">
-            <template #default="scope">
-              <slot name="operations" :row="scope.row">
-          
-              </slot>
-            </template>
-          </el-table-column> -->
         </el-table>
+        <div class="paginationBox">
+          <el-pagination
+            v-model:current-page="paginationConfig.curPage"
+            v-model:page-size="paginationConfig.curPageSize"
+            :page-sizes="paginationConfig.pageSizeList"
+            :size="'default'"
+            layout="total, sizes, prev, pager, next, jumper"
+            :total="paginationConfig.total"
+            @size-change="tableSizeChange"
+            @current-change="tablePageChange"
+          />
+        </div>
       </div>
     </div>
     <customIndicatorDialog

+ 279 - 0
src/components/table/TableQueryForm.vue

@@ -0,0 +1,279 @@
+<script setup lang="ts">
+import type { TableFilterItem } from '@/types/Tables/table'
+import { TableFilterType } from '@/types/Tables/table'
+
+import { reactive, ref } from 'vue'
+
+import { resetReactive } from '@/utils/common'
+import { useDate } from '@/hooks/useDate'
+import { Search } from '@element-plus/icons-vue'
+import type { FormInstance } from 'element-plus'
+
+const { shortcuts, disableDate } = useDate()
+
+interface TableQueryFormProps {
+  filtersInfo: {
+    [key: string]: TableFilterItem
+  }
+}
+
+const props = defineProps<TableQueryFormProps>()
+
+const emits = defineEmits(['queryTable', 'resetTable'])
+
+// 表单ref
+const filterFormRef = ref<FormInstance>()
+
+// 表单数据
+const filterFormData = reactive<{
+  [key: string]: any
+}>({})
+
+// 过滤表单字段的状态信息
+const filterFieldsStateList = reactive<
+  Array<{
+    label: string
+    state: boolean
+    value: any
+  }>
+>([])
+
+// 表单搜索框所对应的字段,因为可能是多选,会在初始化的时候给初值
+const searchSelected = ref('')
+
+// 当前已经选中的展示的过滤字段
+const filterFields = ref<Array<any>>([])
+
+/**
+ * @description: 初始化查询表单
+ * @return {*}
+ */
+const initfilterForm = () => {
+  resetReactive(filterFormData)
+  resetReactive(filterFieldsStateList)
+  for (let [k, v] of Object.entries(props.filtersInfo)) {
+    let val = JSON.parse(JSON.stringify(v)) as TableFilterItem
+    // 如果是搜索框,需要赋初值字段,即作为搜索的字段
+    if (k === 'search') {
+      searchSelected.value = val.value
+    }
+    // 把时间类型给个默认值
+    if (val.type === TableFilterType.Date) {
+      val.value = [shortcuts[0].value()[0], shortcuts[0].value()[1]]
+    }
+
+    // 表单给初始值
+    filterFormData[k] = val.value ? val.value : ''
+
+    // 初始化查询表单字段的状态信息
+    filterFieldsStateList.push({
+      label: val.label,
+      state: true,
+      value: val.name,
+    })
+  }
+}
+
+/**
+ * @description: 初始化过滤字段,把所有字段展示出来
+ * @return {*}
+ */
+const initFilterFields = () => {
+  resetReactive(filterFields.value)
+  filterFieldsStateList.forEach(v => {
+    filterFields.value.push(v.value)
+  })
+}
+
+/**
+ * @description:根据现有的筛选字段重新生成查询参数
+ * @return {[key: string]: any}  查询参数
+ */
+const createQueryParams = (): {
+  [key: string]: any
+} => {
+  let queryParams: {
+    [key: string]: any
+  } = {}
+  for (let i in filterFormData) {
+    // 当前字段没有被隐藏,则加入查询参数
+    if (filterFields.value.includes(i)) {
+      queryParams[i] = filterFormData[i]
+    }
+  }
+  return queryParams
+}
+
+/**
+ * @description: 查询表格
+ * @return {*}
+ */
+const queryTable = () => {
+  let queryParams: {
+    [key: string]: any
+  } = createQueryParams()
+  emits('queryTable', queryParams)
+}
+
+/**
+ * @description: 重置查询表单
+ * @return {*}
+ */
+const resetFilterForm = () => {
+  filterFormRef.value?.resetFields()
+  emits('resetTable')
+}
+
+defineExpose({
+  initFilterFields,
+  initfilterForm,
+})
+</script>
+
+<template>
+  <div class="filterContainer">
+    <div class="filterContent">
+      <el-form
+        :model="filterFormData"
+        label-width="0"
+        ref="filterFormRef"
+        :inline="true"
+      >
+        <el-form-item>
+          <el-input
+            :readonly="true"
+            placeholder="筛选字段"
+            prefix-icon="Filter"
+            style="width: 220px; margin-bottom: 10px"
+          >
+            <template #append>
+              <el-select
+                v-model="filterFields"
+                multiple
+                collapse-tags
+                collapse-tags-tooltip
+                placeholder="Select"
+                style="width: 110px"
+              >
+                <el-option
+                  v-for="item in filterFieldsStateList"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </el-select>
+            </template>
+          </el-input>
+        </el-form-item>
+        <!-- </div> -->
+        <template v-for="item in filtersInfo" :key="item.name">
+          <el-form-item :prop="item.name">
+            <el-input
+              v-model="filterFormData[item.name]"
+              style="max-width: 600px"
+              placeholder="输入关键字搜索"
+              class="input-with-select"
+              v-if="
+                filterFields.includes(item.name) &&
+                item.type === TableFilterType.Search
+              "
+            >
+              <template #prepend>
+                <el-select v-model="searchSelected" style="width: 115px">
+                  <el-option
+                    v-for="option in item.options"
+                    :label="option.label"
+                    :value="option.value"
+                  />
+                </el-select>
+              </template>
+              <template #append>
+                <el-button :icon="Search" />
+              </template>
+            </el-input>
+
+            <el-select
+              class="filterItem"
+              v-if="
+                filterFields.includes(item.name) &&
+                item.type === TableFilterType.Select
+              "
+              v-model="filterFormData[item.name]"
+              placeholder="Select"
+              style="width: 240px"
+            >
+              <template #label="{ label, value }">
+                <span>{{ item.label }}: </span>
+                <span style="margin-left: 10px">{{ value }}</span>
+              </template>
+              <el-option
+                v-for="option in item.options"
+                :key="option.value"
+                :label="option.label"
+                :value="option.value"
+              />
+            </el-select>
+
+            <el-date-picker
+              v-if="
+                filterFields.includes(item.name) &&
+                item.type === TableFilterType.Date
+              "
+              v-model="filterFormData[item.name]"
+              type="daterange"
+              unlink-panels
+              :disableDate="disableDate"
+              range-separator="至"
+              :default-value="[item.startDate, item.endDate]"
+              start-placeholder="开始时间"
+              end-placeholder="结束时间"
+              :shortcuts="shortcuts"
+              size="small"
+            />
+          </el-form-item>
+        </template>
+      </el-form>
+    </div>
+    <div class="filterButtonContainer">
+      <el-button class="queryBtn" color="#197afb" @click="queryTable"
+        >查询</el-button
+      >
+      <el-button class="queryBtn" color="#626aef" plain @click="resetFilterForm"
+        >重置</el-button
+      >
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.filterContainer {
+  width: 100%;
+  padding: 10px 20px;
+  display: flex;
+  align-items: center;
+  margin: 0 auto;
+}
+
+.filterContent {
+  width: 90%;
+  display: flex;
+}
+
+.filterButtonContainer {
+  height: 100%;
+  display: flex;
+}
+
+.queryBtn {
+  margin-right: 8px;
+}
+
+.filterItem {
+  margin-bottom: 10px;
+  margin-right: 8px;
+}
+
+.filterFields {
+  width: 112px;
+}
+</style>

+ 134 - 49
src/hooks/HomeSelect/useAnalysisSelect.ts

@@ -6,61 +6,108 @@ import type {
   CSelectRadio,
   CSelectDate,
   CSelectLegend,
-  CSelectMutipleTag,
 } from '@/types/HomeTypes'
 import { reactive } from 'vue'
 
-export function useAnalysisSelect() {
-  //  总览数据的选择框的信息格式
-  interface AnalysisSelectInfo extends CSelectInfo {
-    mediaSelect: CSelectRadio
-    dateSelect: CSelectDate
-    customIndicatorSelect: CSelectRadio
-  }
+//  项目分析菜单选择数据
+interface ProjctAnalySeleInfo extends CSelectInfo {
+  mediaSelect: CSelectRadio
+  dateSelect: CSelectDate
+  projectSelect: CSelectRadio
+}
 
-  // 图例信息格式
-  interface LegendSelectInfo extends CSelectInfo {
-    leftLegned: CSelectLegend
-    rightLegned: CSelectLegend
-  }
-  // 左侧工具栏选择信息格式
-  interface LeftToolsSelectInfo extends CSelectInfo {
-    dataSource: CSelectRadio
-  }
+// 产品分析菜单选择数据
+interface ProductAnalySeleInfo extends CSelectInfo {
+  mediaSelect: CSelectRadio
+  dateSelect: CSelectDate
+}
 
-  // 右侧工具栏选择信息格式
-  interface RightToolsSelectInfo extends CSelectInfo {
-    dataCategory: CSelectRadio
-  }
+// 每个菜单的选择数据格式
+interface AnalysisSelectInfo {
+  [key: string]: CSelectInfo
+  projAnalysis: ProjctAnalySeleInfo
+  productAnalysis: ProductAnalySeleInfo
+}
+
+// 图例信息格式
+interface LegendSelectInfo extends CSelectInfo {
+  leftLegned: CSelectLegend
+  rightLegned: CSelectLegend
+}
+// 左侧工具栏选择信息格式
+interface LeftToolsSelectInfo extends CSelectInfo {
+  dataSource: CSelectRadio
+}
+
+// 右侧工具栏选择信息格式
+interface RightToolsSelectInfo extends CSelectInfo {
+  dataCategory: CSelectRadio
+}
+
+// 项目分析图表数据请求格式
+interface ProjectChartDataParams {
+  [key: string]: any
+  project: string
+  media: string
+  startTime: string
+  endTime: string
+}
+
+// 产品分析图表数据请求格式
+interface ProductChartDataParams {
+  [key: string]: any
+  media: string
+  startTime: string
+  endTime: string
+  indicator: string
+  topType: string
+}
+
+// 图表数据请求格式
+type ChartDataParams = ProjectChartDataParams | ProductChartDataParams
+export type { ProjectChartDataParams, ProductChartDataParams, ChartDataParams }
+export type {
+  ProjctAnalySeleInfo,
+  ProductAnalySeleInfo,
+  AnalysisSelectInfo,
+  LegendSelectInfo,
+  LeftToolsSelectInfo,
+  RightToolsSelectInfo,
+}
+
+import { resetTimeToMidnight } from '@/utils/common'
+import { useDate } from '../useDate'
+const { shortcuts } = useDate()
 
+export function useAnalysisSelect() {
   // 媒体选择选项
-  const mediaSelectOptions = reactive<Array<BaseOption>>([
+  const mediaSelectOptions: Array<BaseOption> = [
     {
       label: '全部媒体',
       value: 'all',
     },
     {
-      label: '全部媒体',
-      value: 'all',
+      label: '媒体1',
+      value: 'media1',
     },
-  ])
+  ]
 
   // 日期选择
-  const dateSelectOptions = reactive<Array<DateOption>>([
+  const dateSelectOptions: Array<DateOption> = [
     {
       label: '今天',
-      value: new Date(),
+      value: [resetTimeToMidnight(new Date()), resetTimeToMidnight(new Date())],
     },
     {
-      label: '昨天',
-      value: new Date(),
+      label: '上一周',
+      value: shortcuts[0].value().map(resetTimeToMidnight),
     },
-  ])
+  ]
 
   // 项目选择
-  const projSelectOptions = reactive<Array<BaseOption>>([
+  const projSelectOptions: Array<BaseOption> = [
     {
-      label: '测试1',
+      label: '全部项目',
       value: 'ce1',
     },
     {
@@ -83,10 +130,16 @@ export function useAnalysisSelect() {
       label: '测试6',
       value: 'ce6',
     },
-  ])
+  ]
 
-  // 广告数据选择栏数据
-  const analysisSelectInfo = reactive<AnalysisSelectInfo>({
+  // 项目分析菜单选择栏数据
+  const projctAnalySeleInfo = reactive<ProjctAnalySeleInfo>({
+    projectSelect: {
+      type: CSelectType.Radio,
+      name: 'project',
+      value: projSelectOptions[0].value,
+      options: projSelectOptions,
+    },
     mediaSelect: {
       type: CSelectType.Radio,
       name: 'media',
@@ -99,17 +152,26 @@ export function useAnalysisSelect() {
       value: dateSelectOptions[0].value,
       options: dateSelectOptions,
     },
-    customIndicatorSelect: {
-      tagText: '全部项目',
+  })
+
+  // 产品分析菜单选择栏数据
+  const productAnalySeleInfo = reactive<ProductAnalySeleInfo>({
+    mediaSelect: {
       type: CSelectType.Radio,
-      name: 'project',
-      value: projSelectOptions[0].value,
-      options: projSelectOptions,
+      name: 'media',
+      value: mediaSelectOptions[0].value,
+      options: mediaSelectOptions,
+    },
+    dateSelect: {
+      type: CSelectType.Radio,
+      name: 'date',
+      value: dateSelectOptions[0].value,
+      options: dateSelectOptions,
     },
   })
 
   //   左边图例的选项
-  const leftLegnedOptions = reactive<Array<BaseOption>>([
+  const leftLegnedOptions: Array<BaseOption> = [
     {
       label: '消耗',
       value: 'cost',
@@ -118,10 +180,10 @@ export function useAnalysisSelect() {
       label: '展示数',
       value: 'show_count',
     },
-  ])
+  ]
 
   // 右边图例选项
-  const rightLegnedOptions = reactive<Array<BaseOption>>([
+  const rightLegnedOptions: Array<BaseOption> = [
     {
       label: '支付数',
       value: 'pay_count',
@@ -130,7 +192,7 @@ export function useAnalysisSelect() {
       label: '转化率',
       value: 'convert_rate',
     },
-  ])
+  ]
 
   // 图例的选择框信息
   const legendSelectInfo = reactive<LegendSelectInfo>({
@@ -151,12 +213,12 @@ export function useAnalysisSelect() {
   })
 
   // 数据来源选项
-  const dataSourceOptions = reactive<Array<BaseOption>>([
+  const dataSourceOptions: Array<BaseOption> = [
     {
       label: '消耗',
       value: 'consume',
     },
-  ])
+  ]
 
   // 左侧工具栏选择框信息
   const leftToolsSelectInfo = reactive<LeftToolsSelectInfo>({
@@ -169,7 +231,7 @@ export function useAnalysisSelect() {
   })
 
   //  数据分类选项
-  const categoryOptions = reactive<Array<BaseOption>>([
+  const categoryOptions: Array<BaseOption> = [
     {
       label: '汇总',
       value: 'all',
@@ -178,7 +240,7 @@ export function useAnalysisSelect() {
       label: 'Top5产品',
       value: 'top5',
     },
-  ])
+  ]
 
   //   右侧工具栏选择框信息
   const rightToolsSelectInfo = reactive<RightToolsSelectInfo>({
@@ -190,10 +252,33 @@ export function useAnalysisSelect() {
     },
   })
 
+  // 图表请求基础信息
+  const analysisReqParams: {
+    [key: string]: ChartDataParams
+    projAnalysis: ProjectChartDataParams
+    productAnalysis: ProductChartDataParams
+  } = {
+    projAnalysis: {
+      project: '',
+      media: '',
+      startTime: '',
+      endTime: '',
+    },
+    productAnalysis: {
+      media: '',
+      startTime: '',
+      endTime: '',
+      indicator: '',
+      topType: '',
+    },
+  }
+
   return {
-    analysisSelectInfo,
+    projctAnalySeleInfo,
+    productAnalySeleInfo,
     legendSelectInfo,
     leftToolsSelectInfo,
     rightToolsSelectInfo,
+    analysisReqParams,
   }
 }

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

@@ -3,18 +3,22 @@ import { useAnalysisSelect } from './useAnalysisSelect'
 
 const { overviewSelectInfo } = useOverviewSelect()
 const {
-  analysisSelectInfo,
+  projctAnalySeleInfo,
+  productAnalySeleInfo,
   legendSelectInfo,
   leftToolsSelectInfo,
   rightToolsSelectInfo,
+  analysisReqParams,
 } = useAnalysisSelect()
 
 export function useHomeSelect() {
   return {
     overviewSelectInfo,
-    analysisSelectInfo,
+    projctAnalySeleInfo,
+    productAnalySeleInfo,
     legendSelectInfo,
     leftToolsSelectInfo,
     rightToolsSelectInfo,
+    analysisReqParams,
   }
 }

+ 22 - 14
src/hooks/HomeSelect/useOverviewSelect.ts

@@ -9,16 +9,20 @@ import type {
 } from '@/types/HomeTypes'
 import { reactive } from 'vue'
 
-export function useOverviewSelect() {
-  //  总览数据的选择框的信息格式
-  interface OverviewSelectInfo extends CSelectInfo {
-    mediaSelect: CSelectRadio
-    dateSelect: CSelectDate
-    customIndicatorSelect: CSelectMutipleTag
-  }
+//  总览数据的选择框的信息格式
+interface OverviewSelectInfo extends CSelectInfo {
+  mediaSelect: CSelectRadio
+  dateSelect: CSelectDate
+  customIndicatorSelect: CSelectMutipleTag
+}
+import { resetTimeToMidnight } from '@/utils/common'
+import { useDate } from '../useDate'
+const { shortcuts } = useDate()
+export type { OverviewSelectInfo }
 
+export function useOverviewSelect() {
   // 媒体选择选项
-  const mediaSelectOptions = reactive<Array<BaseOption>>([
+  const mediaSelectOptions: Array<BaseOption> = [
     {
       label: '全部媒体',
       value: 'all',
@@ -27,18 +31,22 @@ export function useOverviewSelect() {
       label: '全部媒体',
       value: 'all',
     },
-  ])
+  ]
 
   // 日期选择
-  const dateSelectOptions = reactive<Array<DateOption>>([
+  const dateSelectOptions: Array<DateOption> = [
     {
       label: '今天',
-      value: new Date(),
+      value: [resetTimeToMidnight(new Date()), resetTimeToMidnight(new Date())],
+    },
+    {
+      label: '上一周',
+      value: shortcuts[0].value().map(resetTimeToMidnight),
     },
-  ])
+  ]
 
   // 自定义指标选择
-  const indicatorSelectOptions = reactive<Array<BaseOption>>([
+  const indicatorSelectOptions: Array<BaseOption> = [
     {
       label: '测试1',
       value: 'ce1',
@@ -63,7 +71,7 @@ export function useOverviewSelect() {
       label: '测试6',
       value: 'ce6',
     },
-  ])
+  ]
 
   // 广告数据选择栏数据
   const overviewSelectInfo = reactive<OverviewSelectInfo>({

+ 1 - 10
src/hooks/useRequest.ts

@@ -1,13 +1,3 @@
-/*
- * @Author: fxs bjnsfxs@163.com
- * @Date: 2024-08-20 17:24:06
- * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-10-22 16:57:39
- * @FilePath: \Quantity-Creation-Management-System\src\hooks\useRequest.ts
- * @Description:
- *
- */
-
 import type { AxiosResponse } from 'axios'
 import type { ResponseInfo } from '@/types/axios'
 
@@ -30,6 +20,7 @@ export function useRequest() {
     mockAdTTProj: `http://127.0.0.1:8003/mock/ad/ttproj`,
     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`,
   }
 
   /**

+ 75 - 2
src/hooks/useTable.ts

@@ -7,7 +7,10 @@ import type {
   TableInfoItem,
   SaveFields,
 } from '@/types/Tables/table'
+import type { Ref } from 'vue'
+import type { PaginationConfig } from '@/types/Tables/pagination'
 
+import { resetReactive } from '@/utils/common'
 import axiosInstance from '@/utils/axios/axiosInstance'
 export function useTable() {
   const getData = async (
@@ -16,11 +19,12 @@ export function useTable() {
   ): Promise<Array<TableData>> => {
     try {
       const res = (await axiosInstance.get(url, config)) as ResponseInfo
-
+      console.log(res)
       if (res.code !== 0) throw new Error('请求失败')
       return res.data as TableData[]
-    } catch {
+    } catch (err) {
       console.log('数据请求失败')
+      console.log(err)
       return []
     }
   }
@@ -110,11 +114,80 @@ export function useTable() {
     })
   }
 
+  /**
+   * @description: 初始化自定义指标的字段信息
+   * @return {*}
+   */
+  const initIndicatorFields = (
+    indicatorFields: Array<BaseFieldItem<TableFields>>,
+    propsTableFields: Array<BaseFieldItem<TableFields>>,
+    defaultActiveNav: Ref<string>,
+  ) => {
+    resetReactive(indicatorFields)
+    indicatorFields.splice(
+      0,
+      indicatorFields.length,
+      ...JSON.parse(JSON.stringify(propsTableFields)),
+    )
+    defaultActiveNav.value = indicatorFields[0].name
+  }
+
+  /**
+   * @description: 初始化表格字段,把所有字段都放到一个数组中
+   * @return {*}
+   */
+  const initTableFields = (
+    tableFieldsInfo: Array<TableFields>,
+    sortedTableFields: Array<TableFields>,
+    indicatorFields: Array<BaseFieldItem<TableFields>>,
+  ) => {
+    resetReactive(tableFieldsInfo)
+    // 如果传入了排序字段,则直接使用排序字段,说明已经有缓存了
+    if (sortedTableFields.length > 0) {
+      tableFieldsInfo.splice(0, tableFieldsInfo.length, ...sortedTableFields)
+    } else {
+      // 没传入排序字段,则根据固定字段和自定义指标字段进行排序
+      indicatorFields.forEach(item => {
+        item.children.forEach(child => {
+          if (child.fixed || child.state) {
+            item.value.push(child.name)
+            child.state = true
+          }
+          tableFieldsInfo.push(JSON.parse(JSON.stringify(child)))
+        })
+      })
+    }
+  }
+
+  /**
+   * @description: 缓存表格数据。只有是远程查询的时候才会开启缓存
+   * @param {*} newData 新的表格数据
+   * @return {*}
+   */
+  const setCacheTableData = (
+    isRemote: boolean,
+    paginationConfig: PaginationConfig,
+    cacheTableData: Array<Array<TableData>>,
+    newData: Array<TableData>,
+  ) => {
+    if (isRemote) {
+      // 这页没有数据,就缓存一下
+      let curPage = paginationConfig.curPage
+      let curPageSize = paginationConfig.curPageSize
+      if (!cacheTableData[curPage] || cacheTableData[curPage].length === 0) {
+        cacheTableData[curPage] = newData.splice(0, curPageSize) // 缓存数据,后面的splice是防止返回的数据超出当前页长度
+      }
+    }
+  }
+
   return {
     getData,
     isFixedField,
     saveTableInfo,
     getTableInfo,
     updateTableFields,
+    initTableFields,
+    initIndicatorFields,
+    setCacheTableData,
   }
 }

+ 101 - 0
src/hooks/useTableScroll.ts

@@ -0,0 +1,101 @@
+import type { Ref } from 'vue'
+export function useTableScroll(
+  elScrollBarH: Ref<HTMLElement | null>,
+  tableContent: Ref<HTMLElement | null>,
+  tableContainer: Ref<HTMLElement | null>,
+  tableHeaderRef: Ref<HTMLElement | null>,
+) {
+  /**
+   * @description: 设置滚动条的位置
+   * 这里调整的是滚动条的包裹容器,他相对于表格容器来定位,
+   * 而内部的实际的滚动条相对于这个容器使用tranlate来调整位置
+   * 所以在调回的时候,直接把left设置为0,仍然能保持滚动条的相对位置
+   * @param {*} state true:固定在可视区域,false:固定在表格容器内
+   * @return {*}
+   */
+  const setScrollPos = (state: boolean) => {
+    if (state) {
+      if (!elScrollBarH.value || !tableContent.value) return
+
+      elScrollBarH.value.style.position = 'fixed'
+      // 这里去找表格的content区域,把他相对于视口的left值设置给滚动条的容器
+      elScrollBarH.value.style.left =
+        tableContent.value?.getBoundingClientRect().left + 'px'
+    } else {
+      elScrollBarH.value!.style.left = 0 + 'px'
+      elScrollBarH.value!.style.position = 'absolute'
+    }
+  }
+
+  /**
+   * @description: 观察表格是否在可视区域,设置滚动条的对应位置
+   * @param {*} entries 观察实例,包含当前观察的元素的状态
+   * @return {*}
+   */
+  const obScroll = (entries: IntersectionObserverEntry[]) => {
+    setScrollPos(entries[0].intersectionRatio > 0)
+  }
+
+  /**
+   * @description: 判定当前横向滑动条是否在表格内,有BUG
+   * 滑动距离加可见区域高度不大于表格总高度,则说明在表格内
+   * @return {*}
+   */
+  const isIntable = () => {
+    if (!tableContent.value || !tableContainer.value) return false
+    const { scrollTop, offsetHeight, scrollHeight } =
+      tableContainer.value as HTMLElement
+    return scrollTop + offsetHeight < scrollHeight
+  }
+
+  /**
+   * @description: 初始化滑动条
+   * @return {*}
+   */
+  const initScroll = () => {
+    let sc = document.querySelector('.el-scrollbar__bar') as HTMLElement
+    let header = document.querySelector('.el-table__header-wrapper')
+    if (sc) {
+      elScrollBarH.value = sc
+    }
+    if (header) {
+      tableHeaderRef.value = header as HTMLElement
+    }
+  }
+
+  /**
+   * @description: 设置横向滑动条的位置和表头的位置
+   * 只有滑动位置到达表格内部时,才需要固定横向滑动条,否则让滚动条回到原来的初始位置,即表格的底部
+   * 只有滚动条超出表头的原本位置时,才固定表头
+   * @return {*}
+   */
+  const setScrollAndHeader = () => {
+    setScrollPos(isIntable())
+
+    if (tableHeaderRef.value) {
+      const { scrollTop } = tableContainer.value as HTMLElement
+      let contentOffsetTop = tableContent.value!.offsetTop // 父容器距离顶部的高度
+      let contentScollHeight = tableContent.value!.scrollHeight // 整个父容器的高度
+      // 超出表头原本位置时且小于整个表的高度,则固定表头
+      if (
+        scrollTop >= contentOffsetTop &&
+        scrollTop <= contentOffsetTop + contentScollHeight
+      ) {
+        tableHeaderRef.value.style.position = 'fixed'
+        tableHeaderRef.value.style.top = '125px'
+        tableHeaderRef.value.style.zIndex = '666'
+      } else {
+        // 还原
+        // 不设置top是因为在static下,top无效
+        tableHeaderRef.value.style.position = 'static'
+        tableHeaderRef.value.style.zIndex = '1'
+      }
+    }
+  }
+
+  return {
+    initScroll,
+    setScrollAndHeader,
+    obScroll,
+  }
+}

+ 2 - 2
src/types/HomeTypes/index.ts

@@ -27,7 +27,7 @@ interface BaseOption extends CSelectOption {
 
 // 日期下拉框选项类型
 interface DateOption extends CSelectOption {
-  value: Date
+  value: string[]
 }
 
 // 总览数据选择框格式
@@ -51,7 +51,7 @@ interface CSelectRadio extends CSelectItem {
 interface CSelectDate extends CSelectItem {
   type: CSelectType.Radio
   options: Array<DateOption>
-  value: Date
+  value: string[]
 }
 
 // 多选带

+ 15 - 0
src/types/Tables/pagination.ts

@@ -0,0 +1,15 @@
+// 这个是pagination组件需要的参数
+interface PaginationConfig {
+  curPage: number // 当前页码
+  curPageSize: number // 当前每页多少条数据
+  pageSizeList: number[] //  每页多少条数据的选择列表
+  total: number // 总共多少条数据
+}
+
+// 这个是需要传给表格的props参数
+interface TablePaginationProps {
+  total: number // 总共多少条数据
+  pageSizeList: number[] // 每页多少条数据的选择列表
+}
+
+export type { TablePaginationProps, PaginationConfig }

+ 4 - 1
src/types/Tables/table.ts

@@ -1,4 +1,5 @@
 import type { AllAdMgeTtTablesData, AllAdmgeTtFields } from './tableData/ttAd'
+import type { TablePaginationProps } from './pagination'
 
 // 表格查询表单中表单项的类型
 enum TableFilterType {
@@ -81,7 +82,9 @@ interface TableProps {
   // tableFields: Array<TableFields>
   tableFields: Array<BaseFieldItem<TableFields>> // 表格字段信息,分段展示
   sortedTableFields: Array<TableFields> // 排序过后的表格字段,整体的
-  fixedFields: Array<string>
+  fixedFields: Array<string> // 需要固定显示的字段
+  remotePagination?: boolean // 是否需要远程分页
+  paginationConfig: TablePaginationProps // 分页配置
 }
 
 // 保存的表格信息格式

+ 10 - 2
src/types/echarts/homeAnalysisChart.ts

@@ -1,4 +1,4 @@
-interface homeAnalysisChartData {
+interface ProjectAnalysisData {
   stat_datetime: string // 日期时间字符串
   active: string // 活跃状态
   active_cost: string // 活跃成本
@@ -17,9 +17,17 @@ interface homeAnalysisChartData {
   convert_rate: string // 转化率
 }
 
+interface ProductAnalysisData {
+  cost: string // 模拟生成的cost数据
+  name: string // 固定值
+  date: string // 生成类似 "2024-10-01" 格式的日期
+}
+
+type HomeAnalysisChartData = ProjectAnalysisData | ProductAnalysisData
+
 interface LegendInfo {
   value: string
   color: string
 }
 
-export type { homeAnalysisChartData, LegendInfo }
+export type { HomeAnalysisChartData, LegendInfo }

+ 81 - 14
src/views/Home/Home.vue

@@ -2,11 +2,12 @@
 import type { overviewAdvertisingData } from '@/types/HomeTypes'
 import type { BaseMenu } from '@/types/Promotion/Menu'
 import type {
-  homeAnalysisChartData,
+  HomeAnalysisChartData,
   LegendInfo,
 } from '@/types/echarts/homeAnalysisChart'
+import type { AnalysisSelectInfo } from '@/hooks/HomeSelect/useAnalysisSelect'
 
-import { computed, reactive, ref, watch } from 'vue'
+import { computed, KeepAlive, reactive, ref, watch } from 'vue'
 import { useRequest } from '@/hooks/useRequest'
 
 import Menu from '@/components/navigation/Menu.vue'
@@ -23,12 +24,31 @@ const { AllApi } = useRequest()
 
 const {
   overviewSelectInfo,
-  analysisSelectInfo,
+  projctAnalySeleInfo,
+  productAnalySeleInfo,
   legendSelectInfo,
   leftToolsSelectInfo,
   rightToolsSelectInfo,
+  analysisReqParams,
 } = useHomeSelect()
 
+// 图表的数据配置
+const analysisDataConfig: {
+  [key: string]: {
+    url: string
+    xAxisDataField: string
+  }
+} = {
+  projAnalysis: {
+    url: AllApi.mockChart,
+    xAxisDataField: 'stat_datetime',
+  },
+  productAnalysis: {
+    url: AllApi.mockChartProduct,
+    xAxisDataField: 'date',
+  },
+}
+
 // 广告数据
 const overviewAdvertisingData = reactive<Array<overviewAdvertisingData>>([
   {
@@ -81,8 +101,38 @@ const dataAnalysisMenuList = reactive<Array<BaseMenu>>([
   },
 ])
 
+// 菜单选择
 const analysisiMenuActive = ref<string>('projAnalysis')
 
+// 菜单的选择数据
+const analysisSelectInfo = reactive<AnalysisSelectInfo>({
+  projAnalysis: projctAnalySeleInfo,
+  productAnalysis: productAnalySeleInfo,
+})
+
+// 图表的加载状态
+const chartLoading = ref<boolean>(true)
+
+// 图表数据
+const chartData = reactive<Array<HomeAnalysisChartData>>([])
+
+/**
+ * @description: 当选择的数据发生变化时,重新获取数据
+ * @param {*} newVal 新的值
+ * @param {*} name 字段名
+ * @return {*}
+ */
+const changeChartSelect = (newVal: string, name: string) => {
+  // 对于date字段特殊处理一下
+  if (name === 'date') {
+    analysisReqParams[analysisiMenuActive.value]['startTime'] = newVal[0]
+    analysisReqParams[analysisiMenuActive.value]['endTime'] = newVal[1]
+  } else {
+    analysisReqParams[analysisiMenuActive.value][name] = newVal
+  }
+  getChartData()
+}
+
 /**
  * @description: 根据已选择自定义指标动态的加入广告数据概况
  * @return {*}
@@ -106,16 +156,22 @@ const overviewDataSelected = computed(() => {
  */
 const analysisiMenuChange = (newMenu: string) => {
   analysisiMenuActive.value = newMenu
+  getChartData()
 }
 
-const chartLoading = ref<boolean>(true)
-
-const chartData = reactive<Array<homeAnalysisChartData>>([])
-
+/**
+ * @description: 获取图表数据
+ * @return {*}
+ */
 const getChartData = async () => {
   try {
     chartLoading.value = true
-    const res = (await axiosInstance.get(AllApi.mockChart)) as ResponseInfo
+    const res = (await axiosInstance.get(
+      analysisDataConfig[analysisiMenuActive.value].url,
+      {
+        params: analysisReqParams[analysisiMenuActive.value],
+      },
+    )) as ResponseInfo
     if (res.code !== 0) throw new Error('获取图表数据失败')
     chartData.splice(0, chartData.length, ...res.data)
   } catch (err) {
@@ -209,7 +265,10 @@ getChartData()
         <div class="lineChartContainer">
           <div class="chartBoxHeader">
             <div class="layoutItem"></div>
-            <CSelect :select-info="analysisSelectInfo"></CSelect>
+            <CSelect
+              :select-info="analysisSelectInfo[analysisiMenuActive]"
+              @change-select="changeChartSelect"
+            ></CSelect>
           </div>
           <div class="chartBoxTools">
             <div
@@ -217,10 +276,16 @@ getChartData()
               v-if="analysisiMenuActive !== 'projAnalysis'"
             >
               <div class="dataSource">
-                <CSelect :select-info="leftToolsSelectInfo"></CSelect>
+                <CSelect
+                  @change-select="changeChartSelect"
+                  :select-info="leftToolsSelectInfo"
+                ></CSelect>
               </div>
               <div class="dataCategory">
-                <CSelect :select-info="rightToolsSelectInfo"></CSelect>
+                <CSelect
+                  @change-select="changeChartSelect"
+                  :select-info="rightToolsSelectInfo"
+                ></CSelect>
               </div>
             </div>
             <div class="legendTools" v-else>
@@ -236,11 +301,15 @@ getChartData()
               :legend="analysisLegendInfo"
               :data="chartData"
               :loading="chartLoading"
+              :x-axis-data-field="
+                analysisDataConfig[analysisiMenuActive].xAxisDataField
+              "
             ></HomeAnalysisLine>
           </div>
         </div>
       </div>
     </div>
+    <!-- {{ allChartSelectInfo }} -->
     <div class="topArea"></div>
   </div>
 </template>
@@ -300,7 +369,7 @@ getChartData()
   display: flex;
   align-items: center;
   justify-content: space-between;
-  width: 100%;
+
   position: absolute;
   top: 16px;
   z-index: 500;
@@ -311,13 +380,11 @@ getChartData()
 }
 
 .analysisContent {
-  width: 100%;
   height: 500px;
   position: relative;
 }
 
 .lineChartContainer {
-  width: 100%;
   height: 100%;
   padding: 16px 16px 0;
 }

+ 23 - 11
src/views/Promotion/adManage/ttad.vue

@@ -11,8 +11,8 @@ import type {
   TableInfoItem,
   BaseFieldItem,
   TableFields,
-  SaveFields,
 } from '@/types/Tables/table'
+import type { TablePaginationProps } from '@/types/Tables/pagination'
 import type { BaseMenu } from '@/types/Promotion/Menu'
 import type {
   AdmgeTTAccData,
@@ -562,6 +562,11 @@ const tableInfo = reactive<TableInfo>({
   },
 })
 
+const tablePaginationConfig = reactive<TablePaginationProps>({
+  total: 0,
+  pageSizeList: [10, 20, 40],
+})
+
 // 自定义的操作字段
 const operations: Operation = {
   account: [
@@ -647,16 +652,22 @@ const updateTableInfo = () => {
  * @description: 更新表格数据
  * @return {*}
  */
-const updateTableData = () => {
-  updateTableInfo()
-
-  getData(tableReqInfo[activeMenu.value]).then((res: Array<TableData>) => {
-    tableInfo[activeMenu.value].data.splice(
-      0,
-      tableInfo[activeMenu.value].data.length,
-      ...res,
-    )
-  })
+const updateTableData = async () => {
+  try {
+    updateTableInfo()
+
+    getData(tableReqInfo[activeMenu.value]).then((res: Array<TableData>) => {
+      tableInfo[activeMenu.value].data.splice(
+        0,
+        tableInfo[activeMenu.value].data.length,
+        ...res,
+      )
+      tablePaginationConfig.total = res.length
+    })
+  } catch (err) {
+    ElMessage.error('获取数据失败')
+    console.log(err)
+  }
 }
 
 /**
@@ -722,6 +733,7 @@ onMounted(() => {
     </div>
     <div class="ttAdContent">
       <Table
+        :pagination-config="tablePaginationConfig"
         :table-fields="tableInfo[activeMenu].fields"
         :sorted-table-fields="tableInfo[activeMenu].tableSortedFields"
         :table-data="tableInfo[activeMenu].data"