Răsfoiți Sursa

新增MenuTable组件;新增表格导出功能;更新主页图表表现形式,现在会有两个Y轴,分别展示两种数据

fxs 7 luni în urmă
părinte
comite
e4e590b625

+ 4 - 0
components.d.ts

@@ -40,6 +40,7 @@ declare module 'vue' {
     IconMaterialSymbolsLightLogout: typeof import('~icons/material-symbols-light/logout')['default']
     IconMdiPassword: typeof import('~icons/mdi/password')['default']
     Menu: typeof import('./src/components/navigation/Menu.vue')['default']
+    MenuTable: typeof import('./src/components/promotion/MenuTable.vue')['default']
     MyButton: typeof import('./src/components/form/MyButton.vue')['default']
     MyInput: typeof import('./src/components/form/MyInput.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
@@ -48,4 +49,7 @@ declare module 'vue' {
     TableQueryForm: typeof import('./src/components/table/TableQueryForm.vue')['default']
     TimeLineChart: typeof import('./src/components/echarts/TimeLineChart.vue')['default']
   }
+  export interface ComponentCustomProperties {
+    vLoading: typeof import('element-plus/es')['ElLoadingDirective']
+  }
 }

+ 105 - 1
package-lock.json

@@ -14,7 +14,8 @@
         "lodash": "^4.17.21",
         "pinia": "^2.2.4",
         "vue": "^3.5.11",
-        "vue-router": "^4.4.5"
+        "vue-router": "^4.4.5",
+        "xlsx": "^0.18.5"
       },
       "devDependencies": {
         "@iconify-json/ic": "^1.2.0",
@@ -1996,6 +1997,15 @@
         "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
       }
     },
+    "node_modules/adler-32": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz",
+      "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/ajv": {
       "version": "6.12.6",
       "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
@@ -2136,6 +2146,19 @@
         "node": ">=6"
       }
     },
+    "node_modules/cfb": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz",
+      "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "adler-32": "~1.3.0",
+        "crc-32": "~1.2.0"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/chalk": {
       "version": "4.1.2",
       "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
@@ -2206,6 +2229,15 @@
         "node": ">=12"
       }
     },
+    "node_modules/codepage": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
+      "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/color-convert": {
       "version": "2.0.1",
       "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
@@ -2260,6 +2292,18 @@
         "url": "https://github.com/sponsors/mesqueeb"
       }
     },
+    "node_modules/crc-32": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz",
+      "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+      "license": "Apache-2.0",
+      "bin": {
+        "crc32": "bin/crc32.njs"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.3",
       "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -2924,6 +2968,15 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/frac": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz",
+      "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/fs-extra": {
       "version": "10.1.0",
       "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -4291,6 +4344,18 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/ssf": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz",
+      "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "frac": "~1.1.2"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/string-width": {
       "version": "4.2.3",
       "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
@@ -4955,6 +5020,24 @@
         "node": ">= 8"
       }
     },
+    "node_modules/wmf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz",
+      "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/word": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz",
+      "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/word-wrap": {
       "version": "1.2.5",
       "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -4983,6 +5066,27 @@
         "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
       }
     },
+    "node_modules/xlsx": {
+      "version": "0.18.5",
+      "resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz",
+      "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "adler-32": "~1.3.0",
+        "cfb": "~1.2.1",
+        "codepage": "~1.15.0",
+        "crc-32": "~1.2.1",
+        "ssf": "~0.11.2",
+        "wmf": "~1.0.1",
+        "word": "~0.3.0"
+      },
+      "bin": {
+        "xlsx": "bin/xlsx.njs"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/xml-name-validator": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz",

+ 2 - 1
package.json

@@ -22,7 +22,8 @@
     "lodash": "^4.17.21",
     "pinia": "^2.2.4",
     "vue": "^3.5.11",
-    "vue-router": "^4.4.5"
+    "vue-router": "^4.4.5",
+    "xlsx": "^0.18.5"
   },
   "devDependencies": {
     "@iconify-json/ic": "^1.2.0",

+ 31 - 5
src/components/echarts/HomeAnalysisLine.vue

@@ -7,6 +7,7 @@ import { cloneDeep } from 'lodash'
 import { nextTick } from 'vue'
 import { debounceFunc } from '@/utils/common'
 import echarts from './index'
+import type { YAXisOption } from 'echarts/types/dist/shared'
 
 interface Props {
   loading: boolean
@@ -17,6 +18,8 @@ interface Props {
 
 const props = defineProps<Props>()
 
+const MAX_INTERVAL = 5 // y轴最大间隔数
+
 /**
  * @description: 格式化tooltip
  * @param {*} params 表格数据
@@ -27,8 +30,6 @@ const formatterTooltip = (params: Object | 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 = `<span style="font-weight:bold;">${params[0].axisValueLabel}</span>`
-    console.log(params)
-    console.log(props.legend)
     params.map((item, index) => {
       let data = `${circle}${props.legend[index].color}"></span>
     <span >
@@ -160,6 +161,27 @@ const initChart = () => {
 }
 
 /**
+ * @description: 创建Yaxis
+ * @return {*}
+ */
+const createYAxis = (): Array<YAXisOption> => {
+  let yAxis: Array<YAXisOption> = []
+
+  props.legend.forEach(item => {
+    yAxis.push({
+      name: item.cnName,
+      nameTextStyle: {
+        fontWeight: 'bold',
+      },
+      alignTicks: true, // 开启轴线对齐
+      type: 'value',
+      minInterval: 1,
+    })
+  })
+  return yAxis
+}
+
+/**
  * @description: 创建一条series数据
  * @return {*}
  */
@@ -167,7 +189,7 @@ const createSeries = (): SeriesOption[] => {
   let finalSeriesData: SeriesOption[] = []
   // 图例中包含了图例的name和颜色,那么根据这个name,
   // 去data中找到对应字段的值,形成一个series需要的data
-  props.legend.forEach(item => {
+  props.legend.forEach((item, index) => {
     let newSeries: SeriesOption = cloneDeep(baseSeries)
     let cData = cloneDeep(props.data)
 
@@ -175,6 +197,7 @@ const createSeries = (): SeriesOption[] => {
     let newData = cData.map((data: any) => data[item.value])
     newSeries.name = item.value
     newSeries.data = newData
+    newSeries.yAxisIndex = index
     newSeries.lineStyle!.color = item.color // 这里是线的颜色
     newSeries.itemStyle!.color = item.color
     finalSeriesData.push(newSeries)
@@ -187,7 +210,7 @@ const createSeries = (): SeriesOption[] => {
  * @description: 创建xAxis的data
  * @return {*}
  */
-const createXAxisData = (): string[] => {
+const createXAxis = (): string[] => {
   if (props.data.length > 0) {
     let result: string[] = props.data.map(
       (item: any) => item[props.xAxisDataField],
@@ -208,8 +231,11 @@ const initOptions = () => {
   chartInstance.value!.clear()
   if (props.data.length > 0) {
     let finalSeriesData = createSeries()
-    let xAxisData = createXAxisData()
+    let xAxisData = createXAxis()
+    let yAxisData = createYAxis()
+
     baseOptions.series = finalSeriesData
+    baseOptions.yAxis = yAxisData
     ;(baseOptions.xAxis as any).data = xAxisData
   } else {
     baseOptions = {

+ 2 - 0
src/components/navigation/Menu.vue

@@ -223,7 +223,9 @@ const routerDefaultActive = computed(() => {
   }
 
   .menuTitle {
+    width: 70px;
     font-weight: 700;
+    /* margin-right: 15px; */
   }
 
   .unSubmenuTitle {

+ 204 - 0
src/components/promotion/MenuTable.vue

@@ -0,0 +1,204 @@
+<script setup lang="ts">
+import type { BaseMenu } from '@/types/Promotion/Menu'
+import type { CustomIndicatorScheme } from '@/types/Tables/table'
+import type { BaseTableInfo } from '@/types/Tables/tablePageData'
+import type { MenuTableReq } from '@/types/Tables/MenuTable/menuTableReq'
+import type { TablePaginationProps } from '@/types/Tables/pagination'
+
+import Menu from '../navigation/Menu.vue'
+import Table from '../table/Table.vue'
+
+import { useMenuTable } from '@/hooks/useMenuTable'
+import { computed, onMounted, ref } from 'vue'
+import { useDate } from '@/hooks/useDate'
+
+import TableCustomIndicatorController from '@/utils/localStorage/tableCustomIndicatorController'
+import type { Operations } from '@/types/Tables/Operations/operations'
+
+// 自定义表格的类型
+type TableType = InstanceType<typeof Table>
+
+interface MenuTableProps {
+  saveTableName: string // 保存的表格的名称,后面会拼接上每个菜单的名,这个主要用于保存自定义指标方案
+  menuList: BaseMenu[] // 菜单列表
+  menuDefaultActive: string // 菜单默认选中
+  tableInfo: BaseTableInfo // 表格信息
+  tableReqInfo: MenuTableReq // 每个菜单的数据请求格式
+  tablePaginationConfig: TablePaginationProps // 表格分页配置
+  operations: Operations // 表格中需要进行的操作,会被直接添加到操作列,作为按钮存在
+  excludeFields?: {
+    [key: string]: string[]
+  } // 排除的字段
+}
+
+const { disableDate, shortcuts } = useDate()
+
+// props
+const props = withDefaults(defineProps<MenuTableProps>(), {
+  excludeFields: () => ({}),
+})
+
+// 表格ref
+const tableRef = ref<TableType | null>(null)
+
+// 当前菜单选中
+const activeMenu = ref(props.menuDefaultActive)
+
+// 自定义指标保存的方案
+const customIndicatorScheme = ref<Array<CustomIndicatorScheme>>([])
+
+// 每个表格的自定义方案的IO控制器
+const tableControllers: {
+  [key: string]: TableCustomIndicatorController
+} = {}
+
+// 选择的日期
+const selectDate = ref<Array<Date>>(shortcuts[0].value)
+
+// 修改或新增的弹窗控制
+const operationDialog = ref<boolean>(false)
+
+/**
+ * @description: 保存的文件名
+ * @return {*}
+ */
+const fileName = computed(() => {
+  return `${props.saveTableName}-${activeMenu.value}`
+})
+
+/**
+ * @description: 日期改变
+ * @param {*} newDate 新的日期
+ * @return {*}
+ */
+const dateChange = (newDate: Array<Date>) => {
+  emits('changeDate', newDate)
+}
+
+/**
+ * @description: 打开操作弹窗
+ * @return {*}
+ */
+const openOperationDialog = () => {
+  operationDialog.value = true
+}
+
+const { menuActiveChange, changeScheme, updateCustomIndicator, initData } =
+  useMenuTable(
+    props.saveTableName,
+    fileName,
+    activeMenu,
+    customIndicatorScheme,
+    tableControllers,
+    props.tableInfo,
+    props.tableReqInfo,
+    props.tablePaginationConfig,
+    tableRef,
+    props.menuList,
+  )
+
+const emits = defineEmits(['changeDate'])
+
+onMounted(() => {
+  initData()
+})
+</script>
+
+<template>
+  <div class="menuTableContainer">
+    <div class="menuTableHeader">
+      <Menu
+        :default-active="'account'"
+        :menu-list="menuList"
+        :menu-item-style="'margin-right:30px;'"
+        mode="horizontal"
+        @active-change="menuActiveChange"
+      ></Menu>
+      <div class="datePickerBox">
+        <el-date-picker
+          class="datePicker"
+          v-model="selectDate"
+          :disabled-date="disableDate"
+          :unlink-panels="false"
+          @change="dateChange"
+          type="daterange"
+          range-separator="至"
+          start-placeholder="Start date"
+          end-placeholder="End date"
+          :shortcuts="shortcuts"
+          :size="'small'"
+        >
+        </el-date-picker>
+      </div>
+    </div>
+    <div class="menuTableContent">
+      <Table
+        ref="tableRef"
+        :pagination-config="tablePaginationConfig"
+        :table-fields="tableInfo[activeMenu].fields"
+        :sorted-table-fields="tableInfo[activeMenu].tableSortedFields"
+        :table-data="tableInfo[activeMenu].data"
+        :fixed-fields="tableInfo[activeMenu].fixedFields"
+        :filters-info="tableInfo[activeMenu].filters"
+        :scheme-list="customIndicatorScheme"
+        :exclude-export-fields="props.excludeFields[activeMenu]"
+        @update-custom-indicator="updateCustomIndicator"
+        @change-scheme="changeScheme"
+      >
+        <template #operations>
+          <div>
+            <el-button
+              text
+              :type="item.type"
+              v-for="item in operations[activeMenu]"
+              @click="openOperationDialog"
+            >
+              {{ item.label }}
+            </el-button>
+          </div>
+        </template>
+      </Table>
+    </div>
+    <el-dialog
+      v-model="operationDialog"
+      :align-center="true"
+      title="Tips"
+      width="500"
+    >
+      <slot name="operationDialog">
+        <span>This is a message</span>
+      </slot>
+    </el-dialog>
+  </div>
+</template>
+
+<style scoped>
+.menuTableContainer {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+}
+
+.menuTableHeader {
+  width: 100%;
+  border-bottom: 1px solid #e0e0e0;
+  background-color: white;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.menuTableContent {
+  width: 100%;
+  height: 100%;
+  background-color: white;
+  margin: 0 auto;
+  overflow: hidden;
+}
+
+.datePickerBox {
+  /* width: 100px !important; */
+  margin-left: auto;
+  margin-right: 5%;
+}
+</style>

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

@@ -8,11 +8,20 @@ import type {
 import type { TableData } from '@/types/Tables/table'
 import type { PaginationConfig } from '@/types/Tables/pagination'
 
-import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
+import {
+  computed,
+  nextTick,
+  onMounted,
+  reactive,
+  ref,
+  watch,
+  type Ref,
+} from 'vue'
 
 import { Plus, Operation } from '@element-plus/icons-vue'
 import { useTable } from '@/hooks/useTable'
 import { useTableScroll } from '@/hooks/useTableScroll'
+import { generateUniqueFilename } from '@/utils/common'
 
 import customIndicatorDialog from '../dialog/customIndicatorDialog.vue'
 import TableQueryForm from './TableQueryForm.vue'
@@ -22,8 +31,12 @@ type CustomIndicatorDialog = InstanceType<typeof customIndicatorDialog>
 
 const props = withDefaults(defineProps<TableProps>(), {
   remotePagination: false,
+  excludeExportFields: () => ['action'],
 })
 
+// tableRef
+const tableRef = ref<HTMLElement | null>(null)
+
 // table 容器
 const tableContent = ref<HTMLElement | null>(null)
 
@@ -55,6 +68,7 @@ const {
   initTableFields,
   initIndicatorFields,
   setCacheTableData,
+  exportDataToExcel,
 } = useTable()
 
 const { initScroll, setScrollAndHeader, obScroll } = useTableScroll(
@@ -100,6 +114,9 @@ const tableSizeOb = new ResizeObserver((entries: ResizeObserverEntry[]) => {
   elScrollBarH.value.style.left = left + 'px'
 })
 
+// 表格加载状态
+const tableLoading = ref<boolean>(false)
+
 // 表格字段信息
 const tableFieldsInfo = reactive<Array<TableFields>>([])
 
@@ -132,6 +149,9 @@ const batchOperList = reactive<
   },
 ])
 
+// 导出对话框的可见性
+const exportDialogVisble = ref<boolean>(false)
+
 // 当前选中的方案,传给dialog
 const nowScheme = computed(() => {
   return {
@@ -140,6 +160,21 @@ const nowScheme = computed(() => {
   }
 })
 
+// 被排除的字段名
+const excludeFields = computed<Array<string>>(() => {
+  let result: Array<string> = []
+  props.tableFields.forEach(item => {
+    item.children.forEach(child => {
+      if (
+        props.excludeExportFields.includes(child.name) &&
+        child.name !== 'action'
+      )
+        result.push(child.label)
+    })
+  })
+  return result
+})
+
 // 分页后的表格数据
 const paginationTableData = computed<Array<TableData>>(() => {
   let result: Array<TableData> = []
@@ -240,9 +275,28 @@ const updateIndicatorScheme = () => {
   })
 }
 
-// const initDialog = () => {
-//   schemeActive.value = '默认'
-// }
+/**
+ * @description: 导出表格数据
+ * @return {*}
+ */
+const exportData = () => {
+  exportDataToExcel(
+    props.tableFields,
+    tableData,
+    generateUniqueFilename(),
+    props.excludeExportFields,
+  )
+  exportDialogVisble.value = false
+}
+
+/**
+ * @description: 更新表格状态
+ * @param {*} state
+ * @return {*}
+ */
+const changeTableLoading = (state: boolean) => {
+  tableLoading.value = state
+}
 
 watch(
   () => props.tableData,
@@ -256,7 +310,7 @@ watch(
       newData,
     )
 
-    tableQueryFormRef.value?.initfilterForm()
+    tableQueryFormRef.value?.initFilterForm()
     tableQueryFormRef.value?.initFilterFields()
     updateIndicatorScheme() // 更新指标方案
     schemeActive.value = '默认'
@@ -267,12 +321,15 @@ watch(
 )
 onMounted(() => {
   initScroll()
+  tableQueryFormRef.value?.initFilterForm()
+  tableQueryFormRef.value?.initFilterFields()
   tableVisOb.observe(tableContent.value as HTMLElement)
   tableSizeOb.observe(tableContainer.value as HTMLElement)
 })
 
 defineExpose({
   updateIndicatorScheme,
+  changeTableLoading,
 })
 </script>
 
@@ -309,7 +366,12 @@ defineExpose({
         </div>
         <div class="tableOperationRight">
           <slot name="exportData">
-            <el-button class="exportData w120 ml16" plain>导出数据</el-button>
+            <el-button
+              class="exportData w120 ml16"
+              plain
+              @click="exportDialogVisble = true"
+              >导出数据</el-button
+            >
           </slot>
           <slot name="customIndicator">
             <el-popover
@@ -343,6 +405,7 @@ defineExpose({
       </div>
       <div class="tableContent" ref="tableContent">
         <el-table
+          v-loading="tableLoading"
           v-bind="{ ...$attrs, data: paginationTableData }"
           style="width: 100%"
           border
@@ -350,6 +413,8 @@ defineExpose({
           :scrollbar-always-on="true"
           :row-style="() => `height:${rowHeight}px;color:#333;`"
           :header-row-style="() => `color:black`"
+          ref="tableRef"
+          id="table"
         >
           <template v-for="item in tableFieldsInfo" :key="item.name">
             <el-table-column
@@ -401,6 +466,34 @@ defineExpose({
       ref="customIndicatorDialogRef"
       @update-fields="applyCustomIndicator"
     ></customIndicatorDialog>
+    <div class="exportDialog">
+      <el-dialog
+        v-model="exportDialogVisble"
+        title="提示"
+        width="550"
+        :show-close="false"
+        align-center
+      >
+        <template #header>
+          <div class="exportDialogHeader">
+            <span class="exportDialogHeaderTitle">提示</span>
+          </div>
+        </template>
+        <div class="exportDialogBody">
+          <div style="font-size: 14px; font-weight: bold; margin-bottom: 8px">
+            导出时,将自动过滤掉部分暂不支持导出的指标列内容:
+          </div>
+          <div style="font-size: 12px">{{ excludeFields.join('、') }}</div>
+        </div>
+
+        <template #footer>
+          <div class="dialog-footer">
+            <el-button @click="exportDialogVisble = false">取消</el-button>
+            <el-button @click="exportData" color="#197afb"> 确定 </el-button>
+          </div>
+        </template>
+      </el-dialog>
+    </div>
   </div>
 </template>
 
@@ -510,4 +603,33 @@ defineExpose({
 .activeScheme {
   color: #409eff;
 }
+
+.exportDialogHeader {
+  box-sizing: content-box;
+  height: 18px;
+  padding: 10px 32px;
+  font-size: 18px;
+  font-weight: 700;
+  line-height: 18px;
+  border-bottom: 1px solid #e8eaec;
+}
+
+.exportDialogHeaderTitle {
+  display: block;
+  height: 18px;
+  font-size: 16px;
+  font-weight: 700;
+  line-height: 18px;
+  color: #333;
+  text-align: left;
+}
+
+.exportDialogBody {
+  width: 100%;
+  color: #606266;
+  font-size: 14px;
+  word-break: break-all;
+  // padding: 24px 32px 32px;
+  padding: 10px 32px;
+}
 </style>

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

@@ -48,7 +48,7 @@ const filterFields = ref<Array<any>>([])
  * @description: 初始化查询表单
  * @return {*}
  */
-const initfilterForm = () => {
+const initFilterForm = () => {
   resetReactive(filterFormData)
   resetReactive(filterFieldsStateList)
   for (let [k, v] of Object.entries(props.filtersInfo)) {
@@ -126,7 +126,7 @@ const resetFilterForm = () => {
 
 defineExpose({
   initFilterFields,
-  initfilterForm,
+  initFilterForm,
 })
 </script>
 

+ 29 - 18
src/hooks/usePromotion.ts → src/hooks/useMenuTable.ts

@@ -12,43 +12,54 @@ import type { BaseMenu } from '@/types/Promotion/Menu'
 import { useTable } from './useTable'
 import Table from '@/components/table/Table.vue'
 import TableCustomIndicatorController from '@/utils/localStorage/tableCustomIndicatorController'
+import axiosCanceler from '@/utils/axios/axiosCanceler'
 
 type TableType = InstanceType<typeof Table>
 
 const { getData, updateTableFields } = useTable()
 
-export function usePromotion(
-  fileName: ComputedRef<string>,
-  activeMenu: Ref<string>,
-  customIndicatorScheme: Ref<Array<CustomIndicatorScheme>>,
+export function useMenuTable(
+  saveName: string, // 保存的文件名
+  fileName: ComputedRef<string>, // 动态的文件名
+  activeMenu: Ref<string>, // 当前激活的菜单
+  customIndicatorScheme: Ref<Array<CustomIndicatorScheme>>, // 自定义指标方案
+  // 表格IO控制器
   tableControllers: {
     [key: string]: TableCustomIndicatorController
   },
-  tableInfo: Reactive<BaseTableInfo>,
-  tableReqInfo: { [key: string]: any },
-  tablePaginationConfig: Reactive<TablePaginationProps>,
-  tableRef: Ref<TableType | null>,
-  menuList: BaseMenu[],
+  tableInfo: Reactive<BaseTableInfo>, // 表格信息
+  tableReqInfo: { [key: string]: any }, // 表格请求信息
+  tablePaginationConfig: Reactive<TablePaginationProps>, // 表格分页配置
+  tableRef: Ref<TableType | null>, // 表格实例
+  menuList: BaseMenu[], // 菜单列表
 ) {
+  // 备份的上次请求的信息
+  let backupReqInfo: any = {}
+
   /**
    * @description: 更新表格数据
    * @return {*}
    */
   const updateTableData = async () => {
     try {
+      tableRef.value?.changeTableLoading(true)
       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
-      })
+      await 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)
+    } finally {
+      tableRef.value?.changeTableLoading(false)
     }
   }
 
@@ -126,7 +137,7 @@ export function usePromotion(
    */
   const initTableController = () => {
     menuList.forEach(item => {
-      let tableName = `ttad-${item.name}-tableInfo`
+      let tableName = `${saveName}-${item.name}`
       tableControllers[tableName] = new TableCustomIndicatorController(
         tableName,
         tableInfo[item.name],

+ 58 - 1
src/hooks/useTable.ts

@@ -6,14 +6,25 @@ import type { Ref } from 'vue'
 import type { PaginationConfig } from '@/types/Tables/pagination'
 
 import { resetReactive } from '@/utils/common'
+import { writeFileXLSX, utils } from 'xlsx'
+
 import axiosInstance from '@/utils/axios/axiosInstance'
+import axiosCanceler from '@/utils/axios/axiosCanceler'
 
 export function useTable() {
+  const tableReqList:string[] = []
+
   const getData = async (
     url: string,
     config: Object = {},
   ): Promise<Array<TableData>> => {
     try {
+      let reqInfo = url + JSON.stringify(config);
+      if(tableReqList.length > 0){
+        
+      }
+      tableReqList.push(reqInfo)
+
       const res = (await axiosInstance.get(url, config)) as ResponseInfo
 
       if (res.code !== 0) throw new Error('请求失败')
@@ -135,11 +146,57 @@ export function useTable() {
       }
     }
   }
+  /**
+   * @description: 导出表格数据位Excel未见
+   * @param {Array} propTablefields 表格字段信息
+   * @param {Array<any>} tableData 表格数据
+   * @param {string} fileName 保存的文件名
+   * @param {Array<string>} excludeExportFields 被排除的字段
+   * @return {*}
+   */
+  const exportDataToExcel = (
+    propTablefields: Array<BaseFieldItem<TableFields>>,
+    tableData: Array<any>,
+    fileName: string,
+    excludeExportFields: Array<string>,
+  ) => {
+    try {
+      // 构建导出的表格数据
+      const selectedData = tableData.slice(0, tableData.length)
+
+      // 表头,这里重新生成一下
+      // 这种形式:{name:'姓名', age:'年龄'}
+      let header: {
+        [key: string]: string
+      } = {}
+      propTablefields.forEach(item => {
+        item.children.forEach(child => {
+          if (!excludeExportFields.includes(child.name))
+            header[child.name] = child.label
+        })
+      })
+
+      // 拼接数据
+      const exportData = [header, ...selectedData]
+
+      // 转为sheet对象,skipheader要选上,因为上面拼了一个表头
+      const ws = utils.json_to_sheet(exportData, { skipHeader: true })
+
+      // 创建工作部并且附加到一个工作表中,这个工作表名可以自定义
+      const wb = utils.book_new()
+      utils.book_append_sheet(wb, ws, fileName)
+      // 导出
+      writeFileXLSX(wb, fileName)
+    } catch (err) {
+      console.log(err)
+      ElMessage.error('导出失败')
+    }
+  }
 
   return {
     getData,
     isFixedField,
-
+    exportDataToExcel,
     updateTableFields,
     initTableFields,
     initIndicatorFields,

+ 1 - 1
src/hooks/useTableScroll.ts

@@ -71,7 +71,7 @@ export function useTableScroll(
    */
   const setScrollAndHeader = () => {
     setScrollPos(isIntable())
-
+    console.log('直接走')
     if (tableHeaderRef.value) {
       const { scrollTop } = tableContainer.value as HTMLElement
       let contentOffsetTop = tableContent.value!.offsetTop // 父容器距离顶部的高度

+ 6 - 0
src/types/Tables/MenuTable/menuTableReq.ts

@@ -0,0 +1,6 @@
+// 没个菜单的请求信息格式
+interface MenuTableReq {
+  [key: string]: string
+}
+
+export type { MenuTableReq }

+ 0 - 1
src/types/Tables/Operations/operations.ts

@@ -2,7 +2,6 @@ import { EleBtnType } from '@/types/Button/buttonType'
 interface OperationItem {
   label: string
   type: EleBtnType
-  func: () => void
 }
 
 interface Operations {

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

@@ -86,6 +86,7 @@ interface TableProps {
   remotePagination?: boolean // 是否需要远程分页
   paginationConfig: TablePaginationProps // 分页配置
   schemeList: Array<CustomIndicatorScheme> // 自定义方案列表
+  excludeExportFields?: Array<string> // 需要排除导出的字段
 }
 
 // 保存的表格信息格式

+ 74 - 0
src/utils/axios/axiosCanceler.ts

@@ -0,0 +1,74 @@
+import type { AxiosRequestConfig } from 'axios'
+export class AxiosCanceler {
+  #pendingMap: Map<string, AbortController> // 存储每个请求的控制器
+
+  constructor() {
+    this.#pendingMap = new Map<string, AbortController>()
+  }
+
+  /**
+   * @description: 添加请求
+   * @param {AxiosRequestConfig} config 请求配置
+   */
+  public addPending(config: AxiosRequestConfig): void {
+    this.removePending(config)
+    const url = this.getPendingUrl(config)
+    const controller = new AbortController()
+    config.signal = controller.signal
+    if (!this.#pendingMap.has(url)) {
+      // 如果当前请求不在等待中,将其添加到等待中
+      this.#pendingMap.set(url, controller)
+    }
+  }
+
+  /**
+   * @description: 移除所有等待中的请求
+   * @return {*}
+   */
+  public removeAllPending(): void {
+    this.#pendingMap.forEach(abortController => {
+      if (abortController) {
+        abortController.abort()
+      }
+    })
+    this.reset()
+  }
+
+  /**
+   * @description: 移除请求
+   * @param {AxiosRequestConfig} config 请求配置
+   * @return {*}
+   */
+  public removePending(config: AxiosRequestConfig): void {
+    const url = this.getPendingUrl(config)
+    if (this.#pendingMap.has(url)) {
+      // 如果当前请求在等待中,取消它并将其从等待中移除
+      const abortController = this.#pendingMap.get(url)
+      if (abortController) {
+        abortController.abort(url)
+      }
+      this.#pendingMap.delete(url)
+    }
+  }
+
+  /**
+   * @description: 重置整个管理器
+   * @return {*}
+   */
+  public reset(): void {
+    this.#pendingMap.clear()
+  }
+
+  /**
+   * @description: 获取正在请求的请求地址和方法的拼接字符串
+   * @param {AxiosRequestConfig} config 请求配置
+   * @return {string} 请求地址和方法的拼接字符串
+   */
+  public getPendingUrl = (config: AxiosRequestConfig): string => {
+    return [config.method, config.url].join('&')
+  }
+}
+
+const axiosCanceler = new AxiosCanceler()
+
+export default axiosCanceler

+ 4 - 5
src/utils/axios/axiosInstance.ts

@@ -2,8 +2,8 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-20 17:18:52
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-10-15 11:53:42
- * @FilePath: \Game-Backstage-Management-System\src\utils\axios\axiosInstance.ts
+ * @LastEditTime: 2024-10-29 17:56:27
+ * @FilePath: \Quantity-Creation-Management-System\src\utils\axios\axiosInstance.ts
  * @Description:
  *
  */
@@ -115,11 +115,10 @@ axiosInstance.interceptors.response.use(
       message: msg,
       duration: 1500,
     })
-    // console.log(error)
-    // setLoginState(false)
-    // router.push('/login')
+
     return Promise.reject(error)
   },
 )
+
 // 导出实例
 export default axiosInstance

+ 13 - 16
src/utils/common/index.ts

@@ -2,7 +2,7 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-26 15:46:42
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-10-24 10:31:17
+ * @LastEditTime: 2024-10-29 14:24:21
  * @FilePath: \Quantity-Creation-Management-System\src\utils\common\index.ts
  * @Description:
  *
@@ -76,21 +76,6 @@ export function generateHourlyArray(count: number) {
   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}`
-// }
-
 /**
  * @description: 格式化一个时间为year-month-day的格式
  * @param {Date} date 日期
@@ -242,3 +227,15 @@ export const resetReactive = (
     })
   }
 }
+
+/**
+ * @description: 生成唯一文件名
+ * @param {string} extension 扩展名
+ * @return {string} 唯一文件名
+ */
+export const generateUniqueFilename = (extension: string = 'xlsx'): string => {
+  const timestamp = Date.now() // 获取当前时间戳(毫秒级)
+  const randomStr = Math.random().toString(36).substring(2, 8) // 生成6位随机字符串
+  const uniqueFilename = `${timestamp}_${randomStr}.${extension}`
+  return uniqueFilename
+}

+ 29 - 156
src/views/Promotion/adManage/ttad.vue

@@ -9,8 +9,6 @@ import type {
   FilterInfo,
   TableInfoItem,
   BaseFieldItem,
-  TableFields,
-  CustomIndicatorScheme,
 } from '@/types/Tables/table'
 import type { TablePaginationProps } from '@/types/Tables/pagination'
 import type { BaseMenu } from '@/types/Promotion/Menu'
@@ -34,14 +32,9 @@ import type {
 import { TableFilterType } from '@/types/Tables/table'
 import { useRequest } from '@/hooks/useRequest'
 import { useDate } from '@/hooks/useDate'
-import { computed, onMounted, reactive, ref } from 'vue'
-import { usePromotion } from '@/hooks/usePromotion'
+import { reactive, readonly } from 'vue'
 
-import TableCustomIndicatorController from '@/utils/localStorage/tableCustomIndicatorController'
-import Menu from '@/components/navigation/Menu.vue'
-import Table from '@/components/table/Table.vue'
-
-type TableType = InstanceType<typeof Table>
+import MenuTable from '@/components/promotion/MenuTable.vue'
 
 // 表格信息
 interface TableInfo extends BaseTableInfo {
@@ -79,15 +72,7 @@ interface Operation extends Operations {
 }
 
 const { AllApi } = useRequest()
-const { disableDate, shortcuts } = useDate()
-
-const tableRef = ref<TableType | null>(null)
-
-// 当前菜单选中
-const activeMenu = ref('account')
-
-// 修改或新增的弹窗控制
-const tableItemDialog = ref(false)
+const { shortcuts } = useDate()
 
 // 所有子字段信息
 const AllFields: AllFieldsInfo = {
@@ -519,6 +504,15 @@ const tableReqInfo: { [key: string]: any } = {
   advertise: AllApi.mockAdTTAd,
 }
 
+// 所有表格需要排除的字段信息
+const allExcludeFields: {
+  [key: string]: string[]
+} = {
+  account: ['accountBalance'],
+  project: [],
+  advertise: [],
+}
+
 // 表格信息
 const tableInfo = reactive<TableInfo>({
   account: {
@@ -577,16 +571,10 @@ const operations: Operation = {
     {
       label: '修改',
       type: EleBtnType.Primary,
-      func: () => {
-        tableItemDialog.value = true
-      },
     },
     {
       label: '删除',
       type: EleBtnType.Danger,
-      func: () => {
-        tableItemDialog.value = true
-      },
     },
   ],
   advertise: [],
@@ -608,140 +596,25 @@ const ttAdMenu: BaseMenu[] = [
     title: '广告',
   },
 ]
-
-// 选择的日期
-const selectDate = ref(shortcuts[0].value)
-
-// 自定义指标保存的方案
-const customIndicatorScheme = ref<Array<CustomIndicatorScheme>>([])
-
-// 每个表格的控制器
-const tableControllers: {
-  [key: string]: TableCustomIndicatorController
-} = {}
-
-/**
- * @description: 保存的文件名
- * @return {*}
- */
-const fileName = computed(() => {
-  return `ttad-${activeMenu.value}-tableInfo`
-})
-
-const { menuActiveChange, changeScheme, updateCustomIndicator, initData } =
-  usePromotion(
-    fileName,
-    activeMenu,
-    customIndicatorScheme,
-    tableControllers,
-    tableInfo,
-    tableReqInfo,
-    tablePaginationConfig,
-    tableRef,
-    ttAdMenu,
-  )
-/**
- * @description: 日期改变
- * @param {*} val
- * @return {*}
- */
-const dateChange = (val: Array<Date>) => {
-  //   emits('changeDate', val)
-  console.log(val)
-}
-
-onMounted(() => {
-  initData()
-})
+// 表格名
+const saveTableName = 'ttad-tableInfo'
 </script>
 
 <template>
-  <div class="ttAdContainer">
-    <div class="ttAdHeader">
-      <Menu
-        :default-active="'account'"
-        :menu-list="ttAdMenu"
-        :menu-item-style="'margin-right:30px;'"
-        mode="horizontal"
-        @active-change="menuActiveChange"
-      ></Menu>
-      <div class="datePickerBox">
-        <el-date-picker
-          class="datePicker"
-          v-model="selectDate"
-          :disabled-date="disableDate"
-          :unlink-panels="false"
-          @change="dateChange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="Start date"
-          end-placeholder="End date"
-          :shortcuts="shortcuts"
-          :size="'small'"
-        >
-        </el-date-picker>
-      </div>
-    </div>
-    <div class="ttAdContent">
-      <Table
-        ref="tableRef"
-        :pagination-config="tablePaginationConfig"
-        :table-fields="tableInfo[activeMenu].fields"
-        :sorted-table-fields="tableInfo[activeMenu].tableSortedFields"
-        :table-data="tableInfo[activeMenu].data"
-        :fixed-fields="tableInfo[activeMenu].fixedFields"
-        :filters-info="tableInfo[activeMenu].filters"
-        :scheme-list="customIndicatorScheme"
-        @update-custom-indicator="updateCustomIndicator"
-        @change-scheme="changeScheme"
-      >
-        <template #operations>
-          <div>
-            <el-button
-              text
-              :type="item.type"
-              v-for="item in operations[activeMenu]"
-              @click="item.func"
-            >
-              {{ item.label }}
-            </el-button>
-          </div>
-        </template>
-      </Table>
-    </div>
-    <el-dialog v-model="tableItemDialog" title="Tips" width="500">
-      <span>This is a message</span>
-    </el-dialog>
-  </div>
+  <MenuTable
+    :menu-default-active="ttAdMenu[0].name"
+    :menu-list="ttAdMenu"
+    :operations="operations"
+    :save-table-name="saveTableName"
+    :table-info="tableInfo"
+    :table-pagination-config="tablePaginationConfig"
+    :table-req-info="tableReqInfo"
+    :exclude-fields="allExcludeFields"
+  >
+    <template #operationDialog>
+      <div>测试后</div>
+    </template>
+  </MenuTable>
 </template>
 
-<style scoped>
-.ttAdContainer {
-  width: 100%;
-  height: 100%;
-  overflow: hidden;
-}
-
-.ttAdHeader {
-  width: 100%;
-  border-bottom: 1px solid #e0e0e0;
-  background-color: white;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-
-.ttAdContent {
-  width: 100%;
-  height: 100%;
-  background-color: white;
-  margin: 0 auto;
-  overflow: hidden;
-}
-
-.datePickerBox {
-  /* width: 100px !important; */
-  margin-left: auto;
-  margin-right: 5%;
-}
-</style>
+<style scoped></style>