Explorar el Código

refactor(用户行为页;文件管理页): 用户行为页面饼图改为多个饼图形式;新增文件管理页

fxs hace 1 mes
padre
commit
5630f8e6fa

+ 1 - 0
components.d.ts

@@ -7,6 +7,7 @@ export {}
 /* prettier-ignore */
 declare module 'vue' {
   export interface GlobalComponents {
+    CommonFileUpload: typeof import('./src/components/form/CommonFileUpload.vue')['default']
     CustomDialog: typeof import('./src/components/common/CustomDialog.vue')['default']
     CustomFilter: typeof import('./src/components/form/CustomFilter.vue')['default']
     CustomForm: typeof import('./src/components/form/CustomForm.vue')['default']

+ 113 - 0
src/components/form/CommonFileUpload.vue

@@ -0,0 +1,113 @@
+<template>
+  <el-dialog v-model="dialogVisible" title="文件上传" width="500px" :close-on-click-modal="false" :before-close="handleClose">
+    <!-- 文件上传组件 -->
+    <el-upload
+      class="upload-demo"
+      drag
+      :auto-upload="false"
+      :limit="1"
+      :on-change="handleFileChange"
+      :on-exceed="handleExceed"
+      :file-list="fileList"
+    >
+      <el-icon class="el-icon--upload">
+        <upload-filled />
+      </el-icon>
+      <div class="el-upload__text">拖拽文件到此处或<em>点击选择</em></div>
+      <template #tip>
+        <slot name="tip">
+          <div class="el-upload__tip">每次只能上传一个文件</div>
+        </slot>
+      </template>
+    </el-upload>
+
+    <!-- 对话框底部按钮 -->
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="dialogVisible = false" class="cancelUpload">取消</el-button>
+        <el-button type="primary" @click="handleSubmit" :disabled="!fileList.length">
+          上传文件
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { UploadFilled } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import type { UploadFile } from 'element-plus'
+
+// 组件状态
+const dialogVisible = ref(false) // 控制对话框显示
+const fileList = ref<UploadFile[]>([]) // 文件列表
+
+// 定义组件事件
+const emit = defineEmits<{
+  // 文件提交事件,父组件可以监听并实现自定义上传逻辑
+  (e: 'submit', file: File): void
+}>()
+
+// 打开对话框方法(暴露给父组件调用)
+const openDialog = () => {
+  dialogVisible.value = true
+}
+
+// 关闭对话框前的处理
+const handleClose = () => {
+  fileList.value = [] // 清空文件列表
+  dialogVisible.value = false
+}
+
+// 文件选择变化的处理
+const handleFileChange = (file: UploadFile) => {
+  // 始终只保留最新选择的文件
+  fileList.value = [file]
+}
+
+// 超出文件数量限制的处理
+const handleExceed = () => {
+  ElMessage.warning('每次只能上传一个文件,请先移除当前文件')
+}
+
+// 提交文件处理
+const handleSubmit = () => {
+  if (!fileList.value.length) {
+    ElMessage.warning('请先选择要上传的文件')
+    return
+  }
+
+  const rawFile = fileList.value[0].raw
+  if (!rawFile) {
+    ElMessage.error('文件获取失败')
+    return
+  }
+
+  // 触发提交事件,将文件对象传递给父组件
+  emit('submit', rawFile)
+
+  // 重置组件状态(不清空文件列表,让父组件在上传成功后处理)
+  // dialogVisible.value = false
+}
+
+// 暴露方法给父组件
+defineExpose({
+  openDialog,
+  closeDialog: handleClose
+})
+</script>
+
+<style scoped>
+.upload-demo {
+  text-align: center;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+}
+.cancelUpload {
+  margin-right: 10px;
+}
+</style>

+ 5 - 2
src/components/form/FileUpload.vue

@@ -14,6 +14,7 @@ import { reactive, ref } from 'vue'
 
 interface UploadProps {
   title: string
+  needTip?: boolean
 }
 
 // 上传动画
@@ -29,7 +30,9 @@ const tipVisible = ref(false)
 const emits = defineEmits(['uploadSuccess'])
 
 // props
-defineProps<UploadProps>()
+withDefaults(defineProps<UploadProps>(), {
+  needTip: true
+})
 
 /**
  * @description: 上传文件相关的所有信息
@@ -148,7 +151,7 @@ defineExpose({
         >
           <el-icon class="el-icon--upload"><upload-filled /></el-icon>
           <div class="el-upload__text">拖拽上传或者<em>点击文件上传</em></div>
-          <template #tip>
+          <template #tip v-if="needTip">
             <div class="el-upload__tip">
               请上传一个JSON格式文件,上传格式请点击<span @click="openTip" class="openTip"
                 >这里</span

+ 0 - 1
src/components/table/CustomTable.vue

@@ -228,7 +228,6 @@ const queryTableData = () => {
         }
         return state
       })
-      console.log(filteredTable.length)
       paginationConfig.total = filteredTable.length
       tableData.splice(0, tableData.length, ...filteredTable)
     } else {

+ 7 - 1
src/hooks/useRequest.ts

@@ -72,7 +72,13 @@ export function useRequest() {
     userAdsOverview: `/user/userAdsOverview`, // 用户广告数据总览
     userAdsDaily: `/user/userAdsDaily`, // 广告每日数据曲线
     userAdsDetail: `/user/userAdsDetail`, // 广告数据列表
-    adListChart: `/user/userAdsCake` // 广告列表图表数据
+    adListChart: `/user/userAdsCake`, // 广告列表图表数据
+
+    //   文件管理
+    fileList: `/file/fileList`, // 文件列表
+    fileUploadToServer: `/upload`, // 上传到服务器
+    fileUploadToTencent: `/file/localFileToService`, // 上传到腾讯云
+    fileManageDeleteFile: `/file/fileDelete` // 删除文件
   }
 
   /**

+ 1 - 0
src/hooks/useTableChart.ts

@@ -75,6 +75,7 @@ export function useTableChart(
       Object.assign(params, queryFormData.value)
       if (queryFormData.value.createTime) {
         console.log(queryFormData.value.createTime)
+
         params.createTime = queryFormData.value.createTime.join(',')
       }
 

+ 172 - 101
src/hooks/useUserBehaviorChart.ts

@@ -1,4 +1,5 @@
-import { computed, type Ref, ref } from 'vue'
+import { type Ref, ref } from 'vue'
+// import type { EChartsOption } from 'echarts/types/dist/shared'
 import type { EChartsOption } from 'echarts/types/dist/shared'
 import axiosInstance from '@/utils/axios/axiosInstance.ts'
 import type { ResponseInfo } from '@/types/res.ts'
@@ -28,7 +29,8 @@ export function useUserBehaviorChart(
   chartNeedFields: Array<string>,
   isPie: Ref<boolean>,
   chartInstance: Ref<MChartType | null>,
-  isLog: Ref<boolean>
+  isLog: Ref<boolean>,
+  chartsOptions: Ref<EChartsOption | null>
 ) {
   const chartInfo = ref<ChartInfo>({
     PieChart: [],
@@ -38,13 +40,57 @@ export function useUserBehaviorChart(
     }
   })
 
+  // 生成接近正方形的行列数
+  function computeGrid(n: number) {
+    const cols = Math.ceil(Math.sqrt(n))
+    const rows = Math.ceil(n / cols)
+    return { rows, cols }
+  }
+
+  // 根据行列数和索引计算中心点,返回百分比数字
+  function generateCenters(n: number): { x: number; y: number }[] {
+    const { rows, cols } = computeGrid(n)
+    const centers: { x: number; y: number }[] = []
+    for (let i = 0; i < n; i++) {
+      const row = Math.floor(i / cols)
+      const col = i % cols
+      const x = ((col + 0.5) / cols) * 80
+      const y = ((row + 0.5) / rows) * 100
+      centers.push({ x, y })
+    }
+    return centers
+  }
+
+  const transformData = (data: { name: string; value: number }[]) => {
+    const result: {
+      [key: string]: {
+        [key: string]: number
+      }
+    } = {}
+
+    data.forEach(({ name, value }) => {
+      const [timePart, adPart] = name.split('&&')
+      const timeKey = timePart.trim()
+
+      const adKey = adPart.trim()
+
+      if (!result[timeKey]) {
+        result[timeKey] = {}
+      }
+
+      result[timeKey][adKey] = value
+    })
+
+    return result
+  }
+
   const updateChartData = async () => {
     try {
       if (chartNeedFields.length > 0) {
-        const hasFilter = chartNeedFields.every((item) => {
+        const noFilter = chartNeedFields.every((item) => {
           return queryFormData.value[item] === ''
         })
-        if (hasFilter) {
+        if (noFilter) {
           const tip = filterInfo
             .filter((item) => {
               return chartNeedFields.includes(item.name as any)
@@ -64,13 +110,12 @@ export function useUserBehaviorChart(
 
       const params = {} as any
       Object.assign(params, queryFormData.value)
-      // if (queryFormData.value.createTime) {
-      //   params.createTime = queryFormData.value.createTime
-      //     .map((item: any) => {
-      //       return new Date(item).getTime().toString()
-      //     })
-      //     .join(',')
-      // }
+      if (queryFormData.value.createTime) {
+        params.createTime = queryFormData.value.createTime
+          .map((item: Date) => new Date(item).getTime() / 1000)
+
+          .join(',')
+      }
 
       const res = (await axiosInstance.post(url, params)) as ResponseInfo
       if (res.code !== 0) {
@@ -97,85 +142,116 @@ export function useUserBehaviorChart(
             labelList: [],
             valueList: []
           }
-          return
-        }
-
-        chartInfo.value.PieChart = data.map((item) => ({
-          name: item.name,
-          value: item.count
-        }))
-        chartInfo.value.BarChart = {
-          labelList: data.map((item) => item.name),
-          valueList: data.map((item) => item.count)
+        } else {
+          chartInfo.value.PieChart = data.map((item) => ({
+            name: item.name,
+            value: item.count
+          }))
+          chartInfo.value.BarChart = {
+            labelList: data.map((item) => item.name),
+            valueList: data.map((item) => item.count)
+          }
         }
       }
+      updateOptions()
     } catch (err) {
       console.log(err)
     } finally {
-      console.log(chartInstance)
       if (chartInstance.value) {
         chartInstance.value.stopLoading()
       }
     }
   }
 
-  const pieChartOptions = computed<EChartsOption>(() => {
-    const colors = generateUniqueColors(chartInfo.value.PieChart.length)
+  const createPieChartOptions = (): EChartsOption => {
     const pieData = chartInfo.value.PieChart
-    const seriesData: {
-      [key: string]: any[]
-    } = {}
-    pieData.forEach((item) => {
-      const name = item.name.split('&&')[0]
-      if (!seriesData[name]) {
-        seriesData[name] = []
+
+    // 判断是不是只有一个筛选条件
+    let hasCount = 0
+    for (const [k, v] of Object.entries(queryFormData.value)) {
+      if (chartNeedFields.includes(k) && v) {
+        hasCount++
       }
-      seriesData[name].push(item)
-    })
-    const resultSeries: any[] = []
-    let count = 1
-    for (const [k, v] of Object.entries(seriesData)) {
-      console.log(`${count * 5}% `, `${count * 10}% `)
-      resultSeries.push({
-        name: k,
-        type: 'pie',
-        radius: [`${count * 10}% `, `${(count + 1) * 10}% `],
-        center: ['50%', '50%'],
-        avoidLabelOverlap: false,
-        itemStyle: {
-          borderRadius: 10,
-          borderColor: '#fff',
-          borderWidth: 1
+    }
+    if (hasCount === 1) {
+      return {
+        color: generateUniqueColors(pieData.length), // 根据条目数生成配色
+        tooltip: {
+          trigger: 'item',
+          formatter: '{b}: {c} ({d}%)'
         },
-        label: {
-          show: false,
-          position: 'center'
+        legend: {
+          orient: 'vertical',
+          left: 10,
+          top: 'center',
+          data: pieData.map((item) => item.name)
         },
-        emphasis: {
-          label: {
-            show: true,
-            fontSize: 20,
-            fontWeight: 'bold'
+        series: [
+          {
+            name: '在线时长分布',
+            type: 'pie',
+            radius: ['40%', '70%'], // 内外半径,也可以用 '50%' 单值
+            center: ['50%', '50%'], // 居中
+            avoidLabelOverlap: false,
+            itemStyle: {
+              borderRadius: 6,
+              borderColor: '#fff',
+              borderWidth: 2
+            },
+            label: {
+              show: true,
+              position: 'outside',
+              formatter: '{b}: {c} ({d}%)'
+            },
+            emphasis: {
+              label: {
+                show: true,
+                fontSize: 18,
+                fontWeight: 'bold'
+              }
+            },
+            data: pieData.map(({ name, value }) => ({
+              name,
+              value
+            }))
           }
-        },
-        labelLine: {
-          show: false
-        },
-        data: v
-      })
-      count++
+        ]
+      }
     }
-    console.log(resultSeries)
+    // if(queryFormData.value.)
+    const tsData = transformData(pieData)
+
+    const timeKeys = Object.keys(tsData)
+    const count = timeKeys.length
+    const centers = generateCenters(count)
+
+    const colors = generateUniqueColors(count)
+    // 1. 生成 series
+    const series = timeKeys.map((timeKey, idx) => ({
+      name: timeKey,
+      type: 'pie',
+      radius: '30%', // 每个饼图半径
+      center: [`${centers[idx].x}%`, `${centers[idx].y}%`],
+      data: Object.entries(tsData[timeKey]).map(([adKey, val]) => ({
+        name: adKey.split(':')[1],
+        value: val
+      })),
+      label: { formatter: '{b}: {c} ({d}%)' }
+    }))
+    const titles = timeKeys.map((timeKey, idx) => {
+      const { x, y } = centers[idx]
+
+      return {
+        text: timeKey,
+        left: `${x}%`,
+        top: `${y - 3}%`,
+        textAlign: 'center'
+      }
+    })
+
     return {
       color: colors,
-      // tooltip: {
-      //   trigger: 'item',
-      //   formatter: (params: any) => {
-      //     const data = chartInfo.value.PieChart[params.dataIndex]
-      //     return `${data.name}<br/>
-      //             数量: ${data.value}`
-      //   }
-      // },
+      title: titles,
       tooltip: {
         trigger: 'item',
         formatter: '{a} <br/>{b}: {c} ({d})'
@@ -186,31 +262,24 @@ export function useUserBehaviorChart(
         right: 10,
         top: 20,
         bottom: 20
-        // top: '8%',
-        // left: 'center',
-        // itemGap: 35,
-        // textStyle: {
-        //   fontSize: 14
-        // },
-        // padding: [0, 50]
       },
-      series: resultSeries
-    }
-  })
+      series: series
+    } as EChartsOption
+  }
 
-  const barChartOptions = computed<EChartsOption>(() => {
+  const createBarChartsOptions = (): EChartsOption => {
     const barChartInfo = chartInfo.value.BarChart
     const yType: any = isLog.value ? 'log' : 'value'
     return {
       xAxis: {
         type: 'category',
-        data: barChartInfo.labelList,
-        axisLabel: {
-          interval: 30,
-          // rotate: 30,
-          // width: 10, // 增加标签宽度
-          overflow: 'truncate' // 过长时显示省略号
-        }
+        data: barChartInfo.labelList
+        // axisLabel: {
+        //   interval: 30,
+        //   // rotate: 30,
+        //   width: 10, // 增加标签宽度
+        //   overflow: 'truncate' // 过长时显示省略号
+        // }
       },
       yAxis: {
         type: yType,
@@ -234,7 +303,7 @@ export function useUserBehaviorChart(
         },
         {
           type: 'slider',
-          show: true,
+          show: false,
           yAxisIndex: 0,
           filterMode: 'empty',
           width: 12,
@@ -273,26 +342,28 @@ export function useUserBehaviorChart(
             show: true,
             position: 'top',
             formatter: '{c}'
-          },
-          itemStyle: {
-            // color: (params) => {
-            //   return colors[params.dataIndex]
-            // }
           }
         }
       ]
     }
-  })
+  }
 
-  const chartOptions = computed(() => {
+  const updateOptions = () => {
     const pieChartInfo = chartInfo.value.PieChart.length ?? null
     const barChartInfo = chartInfo.value.BarChart.valueList.length ?? null
-    if (!pieChartInfo && !barChartInfo) return null
-    return isPie.value ? pieChartOptions.value : barChartOptions.value
-  })
+
+    if (!pieChartInfo && !barChartInfo) {
+      chartsOptions.value = null
+      return
+    }
+    const pieChartOptions = createPieChartOptions()
+    const barChartOptions = createBarChartsOptions()
+
+    chartsOptions.value = isPie.value ? pieChartOptions : barChartOptions
+  }
 
   return {
     updateChartData,
-    chartOptions
+    updateOptions
   }
 }

+ 20 - 0
src/router/fileManage.ts

@@ -0,0 +1,20 @@
+export default [
+  {
+    path: '/fileManage',
+    redirect: '/fileManage/fileList',
+    name: 'FileManage',
+    children: [
+      {
+        path: 'fileList',
+        name: 'FileList',
+        icon: 'PieChart',
+        cnName: '文件列表',
+        component: () => import('@/views/FileManage/FileList.vue'),
+        meta: {
+          activeMenu: 'fileList',
+          needKeepAlive: true
+        }
+      }
+    ]
+  }
+]

+ 2 - 1
src/router/index.ts

@@ -13,6 +13,7 @@ import { authLogin } from '@/utils/axios/auth'
 import HomeRoutes from './home'
 import LoginRoutes from './login'
 import AppManage from './appManage'
+import FileManage from './fileManage.ts'
 
 const routes = [
   ...LoginRoutes,
@@ -21,7 +22,7 @@ const routes = [
     name: 'Index',
     redirect: '/home/overView',
     component: () => import('@/views/IndexView.vue'),
-    children: [...HomeRoutes, ...AppManage]
+    children: [...HomeRoutes, ...AppManage, ...FileManage]
   },
   {
     path: '/',

+ 13 - 8
src/utils/axios/axiosInstance.ts

@@ -101,15 +101,20 @@ axiosInstance.interceptors.response.use(
   },
   function (error) {
     if (error) {
-      const code = error.response.data.code
-      const msg = errorCodeMsg[code] ?? '服务器错误,请稍后再试'
+      // 这里try,因为后端不一定返回什么格式的数据
+      try {
+        const code = error.response.data.code
+        const msg = errorCodeMsg[code] ?? '服务器错误,请稍后再试'
 
-      // 对响应错误做点什么
-      ElMessage({
-        type: MessageType.Error,
-        message: msg,
-        duration: 1500
-      })
+        // 对响应错误做点什么
+        ElMessage({
+          type: MessageType.Error,
+          message: msg,
+          duration: 1500
+        })
+      } catch (err) {
+        console.error(err)
+      }
     }
     return Promise.reject(error)
   }

+ 299 - 0
src/views/FileManage/FileList.vue

@@ -0,0 +1,299 @@
+<script setup lang="ts">
+import CommonFileUpload from '@/components/form/CommonFileUpload.vue'
+import type CustomTable from '@/components/table/CustomTable.vue'
+import { useRequest } from '@/hooks/useRequest.ts'
+
+import {
+  FilterType,
+  type QueryInfo,
+  type TableFieldInfo,
+  type TablePaginationSetting,
+  type TableToolsConfig
+} from '@/types/table.ts'
+import axiosInstance from '@/utils/axios/axiosInstance.ts'
+import { ElMessageBox } from 'element-plus'
+import { ref, reactive } from 'vue'
+import HeaderCard from '@/components/dataAnalysis/HeaderCard.vue'
+import type { ReqConfig } from '@/types/dataAnalysis'
+
+const { AllApi } = useRequest()
+
+// 表格请求配置
+const requestConfig = reactive<ReqConfig>({
+  url: AllApi.fileList,
+  otherOptions: {}
+})
+
+// --- Refs ---
+const headerCard = ref<InstanceType<typeof HeaderCard> | null>(null)
+const uploadRef = ref<InstanceType<typeof CommonFileUpload> | null>(null)
+const fileListTable = ref<InstanceType<typeof CustomTable> | null>(null)
+
+// 表格分页设置
+const pagingConfig = reactive<TablePaginationSetting>({
+  limit: 20,
+  currentPage: 1,
+  total: 0,
+  pageSizeList: [20, 30]
+})
+
+// 表格字段信息
+const tableFieldsInfo = reactive<Array<TableFieldInfo>>([
+  {
+    name: 'id',
+    cnName: 'ID',
+    isShow: false,
+    needSort: false
+  },
+  {
+    name: 'file_name',
+    cnName: '文件名',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'local_path',
+    cnName: '本地路径',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'cos_path',
+    cnName: 'COS路径',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'createdAt',
+    cnName: '创建时间',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'updatedAt',
+    cnName: '更新时间',
+    isShow: true,
+    needSort: false
+  }
+])
+
+/**
+ * 触发新增
+ */
+const addNewEvent = () => {
+  // eventDialog.value.addForm()
+  uploadRef.value?.openDialog()
+}
+
+// 工具栏配置
+const tableToolsConfig: TableToolsConfig = {
+  add: true,
+  filterFields: true,
+  refresh: true,
+  download: false
+}
+
+const deleteFile = async (row: any) => {
+  try {
+    const confirmRes = await ElMessageBox.confirm('确认删除该配置吗', '警告', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+      closeOnClickModal: false
+    })
+    if (!confirmRes) return
+    const delRes = (await axiosInstance.post(AllApi.fileManageDeleteFile, {
+      fileId: row.id
+    })) as { code: number }
+    if (delRes.code !== 0) {
+      ElMessage.error('删除失败')
+      return
+    }
+    ElMessage.success('删除成功')
+  } catch (err) {
+    console.error(err)
+  } finally {
+    fileListTable.value?.updateTableData()
+  }
+}
+
+// 查询字段设置
+const queryInfo: Array<QueryInfo> = [
+  {
+    name: 'search',
+    label: '文件名',
+    type: FilterType.INPUT,
+    placeholder: '请输入文件名搜索',
+    default: ''
+  }
+]
+
+const handleSubmit = async (file: File) => {
+  const tableData = fileListTable.value?.outGetTableData()
+  if (tableData) {
+    const hasSameFileName = tableData.some((item) => item.file_name === file.name)
+    if (hasSameFileName) {
+      ElMessage.error('文件名重复')
+      return
+    }
+  }
+  try {
+    const uploadUrl = AllApi.fileUploadToServer
+    const tencentUrl = AllApi.fileUploadToTencent
+    const formData = new FormData()
+    formData.append('file', file)
+    const uploadRes = (await axiosInstance.post(uploadUrl, formData)) as {
+      code: number
+      path: string
+    }
+    if (uploadRes.code !== 0) {
+      ElMessage.error('上传失败')
+      return
+    }
+    const uploadToTencent = (await axiosInstance.post(tencentUrl, {
+      filePath: uploadRes.path
+    })) as {
+      code: number
+      path: string
+    }
+    if (uploadToTencent.code !== 0) {
+      ElMessage.error('上传失败')
+      return
+    }
+    ElMessage.success('上传成功')
+    fileListTable.value?.updateTableData()
+  } catch (err) {
+    console.error(err)
+  } finally {
+    uploadRef.value?.closeDialog()
+  }
+}
+</script>
+
+<template>
+  <div class="fileList">
+    <div class="breadcrumbBox">
+      <HeaderCard
+        ref="headerCard"
+        :need-breadcrumb="false"
+        :title="'文件列表'"
+        :need-pf-select="false"
+      ></HeaderCard>
+    </div>
+
+    <div class="fileListBox">
+      <CustomTable
+        ref="fileListTable"
+        :request-config="requestConfig"
+        :open-page-query="true"
+        :pagination-config="pagingConfig"
+        :table-fields-info="tableFieldsInfo"
+        :open-filter-query="true"
+        :open-remote-query="true"
+        :tools="tableToolsConfig"
+        :query-info="queryInfo"
+        @add-new-item="addNewEvent"
+      >
+        <template #tableOperation>
+          <el-table-column label="操作" align="center">
+            <template #default="scope">
+              <el-button
+                size="small"
+                class="operationBtn"
+                type="danger"
+                @click="deleteFile(scope.row)"
+                >删除</el-button
+              >
+            </template>
+          </el-table-column>
+        </template>
+      </CustomTable>
+    </div>
+
+    <div class="uploadFileBox">
+      <CommonFileUpload ref="uploadRef" @submit="handleSubmit">
+        <template #tip>
+          <div class="el-upload__tip">每次只能上传一个文件,且文件名不能重复</div>
+        </template>
+      </CommonFileUpload>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+/* Styles adapted with renamed classes */
+.fileList {
+  /* Renamed from eventManage */
+  width: 98%;
+  margin: 1% auto;
+  background-color: white;
+  box-sizing: border-box;
+  position: relative; /* Keep relative positioning for the absolute positioned handleFileActions */
+}
+
+.breadcrumbBox {
+  background-color: white;
+  box-sizing: border-box;
+  height: 64px;
+  font-size: 16px;
+  color: #17233d;
+  font-weight: 600;
+  /* padding: 0 24px; */ /* Kept original style */
+  line-height: 64px;
+}
+
+.fileListBox {
+  /* Renamed from eventTableBox */
+  box-sizing: border-box;
+  /* Add padding-bottom if needed to avoid overlap with potential footers */
+  padding: 0 24px 24px;
+}
+
+.handleFileActions {
+  /* Renamed from handleEvent */
+  position: absolute;
+  /* width: 12%; */ /* Kept original style */
+  background-color: white;
+  box-sizing: border-box;
+  /* height: 48px; */ /* Kept original style */
+  font-size: 16px;
+  font-weight: 600;
+  top: 20px; /* Kept original position */
+  right: 24px; /* Kept original position */
+  /* position: relative; */ /* Kept original style (commented) */
+  /* justify-content: flex-end; */ /* Kept original style (commented) */
+  z-index: 10; /* Ensure buttons are above the table box if overlap occurs */
+}
+
+.fileGroup {
+  width: 100%; /* Kept original style */
+  display: flex; /* Added to align buttons nicely */
+  justify-content: flex-end; /* Align buttons to the right */
+}
+
+.fileBtn {
+  margin-left: 10px; /* Changed from margin-right for flex-end alignment */
+}
+.fileBtn:first-child {
+  margin-left: 0; /* Remove margin for the first button */
+}
+
+/* Basic styling for the new table */
+.file-list-table {
+  width: 100%;
+  border-collapse: collapse;
+  margin-top: 20px; /* Add some space below the header/action buttons area */
+}
+
+.file-list-table th,
+.file-list-table td {
+  border: 1px solid #dfe6ec; /* Example border */
+  padding: 8px 12px;
+  text-align: left;
+}
+
+.file-list-table th {
+  background-color: #f8f8f9; /* Example header background */
+  font-weight: 600;
+}
+</style>

+ 20 - 6
src/views/Home/Analysis/UserBehavior.vue

@@ -10,8 +10,9 @@
 import type { HeaderCardProps } from '@/types/dataAnalysis'
 import type { QueryInfo, SelectInfo } from '@/types/table'
 import { FilterType } from '@/types/table'
+import type { EChartsOption } from 'echarts/types/dist/shared'
 
-import { reactive, ref } from 'vue'
+import { reactive, type Ref, ref } from 'vue'
 import { useRequest } from '@/hooks/useRequest'
 
 import { useCommonStore } from '@/stores/useCommon'
@@ -202,14 +203,17 @@ const backupSelect = reactive([])
 
 watchPageChange(() => [selectInfo.pf, selectInfo.gid], backupSelect, updateAllReq)
 
-const { updateChartData, chartOptions } = useUserBehaviorChart(
+const chartsOptions: Ref<EChartsOption | null> = ref(null)
+
+const { updateChartData, updateOptions } = useUserBehaviorChart(
   AllApi.userBehaviorPieChart,
   queryFormData,
   filterInfo,
   chartNeedFields,
   isPie,
   chartRef,
-  isLog
+  isLog,
+  chartsOptions
 )
 </script>
 
@@ -231,20 +235,30 @@ const { updateChartData, chartOptions } = useUserBehaviorChart(
         <div class="chartContainer">
           <div class="chartsTools">
             <div class="chartVal">
-              <el-radio-group class="formChangeRadioGroup" v-model="isLog" size="small">
+              <el-radio-group
+                class="formChangeRadioGroup"
+                v-model="isLog"
+                @change="updateOptions"
+                size="small"
+              >
                 <el-radio-button label="对数" :value="true" />
                 <el-radio-button label="原值" :value="false" />
               </el-radio-group>
             </div>
             <div class="chartForm">
-              <el-radio-group class="formChangeRadioGroup" v-model="isPie" size="small">
+              <el-radio-group
+                class="formChangeRadioGroup"
+                v-model="isPie"
+                @change="updateOptions"
+                size="small"
+              >
                 <el-radio-button label="饼图" :value="true" />
                 <el-radio-button label="柱状图" :value="false" />
               </el-radio-group>
             </div>
           </div>
           <div class="chartDisplay">
-            <PieBorderRadius ref="chartRef" :options="chartOptions"></PieBorderRadius>
+            <PieBorderRadius ref="chartRef" :options="chartsOptions"></PieBorderRadius>
           </div>
         </div>
       </div>

+ 5 - 0
src/views/IndexView.vue

@@ -48,6 +48,10 @@ const navBarMenuList = [
   {
     name: 'AppManage',
     title: '应用管理'
+  },
+  {
+    name: 'FileManage',
+    title: '文件管理'
   }
 ]
 
@@ -124,6 +128,7 @@ const createdMenuList = () => {
     return item.name === navBarSelect.value // 根据顶部导航栏的选中情况来选择选中哪个具体的路由信息,可以打印自己看一下
   })
   basePath.value = activeMenu?.path // 找到需要激活的菜单的路由,后续用来拼接需要跳转的路由
+  console.log(indexRoutesChild)
   menuList.splice(0, menuList.length, ...(activeMenu?.children as Array<any>)) // 清空原来的路由信息,并且加入新选中的
 }