Jelajahi Sumber

高频分析大部分功能

wukai 4 bulan lalu
induk
melakukan
4209ee1d9d
3 mengubah file dengan 1015 tambahan dan 142 penghapusan
  1. 19 0
      src/api/biz/anal.js
  2. 911 0
      src/views/biz/anal/high.vue
  3. 85 142
      src/views/biz/config/index.vue

+ 19 - 0
src/api/biz/anal.js

@@ -0,0 +1,19 @@
+import request from '@/utils/request'
+
+// 查询高频出入境列表
+export function listHighFrequencyInOut(params) {
+  return request({
+    url: '/biz/anal/high',
+    method: 'get',
+    params: params
+  })
+}
+
+// 查询高频出入境统计信息
+export function getInOutStats(params) {
+  return request({
+    url: '/biz/anal/stats',
+    method: 'get',
+    params: params
+  })
+}

+ 911 - 0
src/views/biz/anal/high.vue

@@ -0,0 +1,911 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
+      <el-form-item label="查询方式" prop="queryType">
+        <el-select v-model="queryParams.queryType" placeholder="请选择查询方式" clearable style="width: 150px;" @change="handleQuery">
+          <el-option label="按年查询" value="year" />
+          <el-option label="按月查询" value="month" />
+          <el-option label="按时段查询" value="range" />
+        </el-select>
+      </el-form-item>
+      <el-form-item v-if="queryParams.queryType === 'year'" label="年份" prop="year">
+        <el-date-picker
+          v-model="queryParams.year"
+          type="year"
+          value-format="YYYY"
+          placeholder="请选择年份"
+          @change="handleQuery">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item v-if="queryParams.queryType === 'month'" label="月份" prop="month">
+        <el-date-picker
+          v-model="queryParams.month"
+          type="month"
+          value-format="YYYY-MM"
+          placeholder="请选择月份"
+          @change="handleQuery">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item v-if="queryParams.queryType === 'range'" label="时段" prop="dateRange">
+        <el-date-picker
+          v-model="queryParams.dateRange"
+          type="daterange"
+          value-format="YYYY-MM-DD"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          @change="handleQuery">
+        </el-date-picker>
+      </el-form-item>
+    </el-form>
+
+
+
+    <!-- 统计数据卡片 -->
+    <el-row :gutter="20" class="mb20">
+      <el-col :span="6">
+        <el-card class="stat-card total-people">
+          <div class="stat-item">
+            <div class="stat-number">{{ statistics.totalPeople }}</div>
+            <div class="stat-label">总人数</div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="stat-card total-entries">
+          <div class="stat-item">
+            <div class="stat-number">{{ statistics.totalEntries }}</div>
+            <div class="stat-label">总次数</div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="stat-card avg-frequency">
+          <div class="stat-item">
+            <div class="stat-number">{{ statistics.avgFrequency }}</div>
+            <div class="stat-label">平均频次</div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="stat-card max-frequency">
+          <div class="stat-item">
+            <div class="stat-number">{{ statistics.maxFrequency }}</div>
+            <div class="stat-label">最高频次</div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="10" style="margin-bottom: 20px;">
+      <div style="height: 500px; width: 49%; margin-right: 1%;">
+        <div class="table_caption" style="height: 30px; line-height: 30px;">高频出入境统计图(Top10)</div>
+        <div style="height: 470px; width: 100%; border: 1px solid #ededed; background: #fff;" ref="top10">
+          <div id="top10Chart" style="height: 100%; width: 100%;"></div>
+        </div>
+      </div>
+      <div style="height: 500px; width: 49%;">
+        <div class="table_caption" style="height: 30px; line-height: 30px;">出入境频次分布</div>
+        <div style="height: 470px; width: 100%; border: 1px solid #ededed; background: #fff;" ref="pieChartContainer">
+          <div id="pieChart" style="height: 100%; width: 100%;"></div>
+        </div>
+      </div>
+    </el-row>
+
+    <el-table v-loading="loading" :data="list" style="margin-top: 20px;">
+      <el-table-column label="序号" type="index" width="50" align="center" />
+      <el-table-column label="姓名" prop="fullName" align="center" />
+      <el-table-column label="证件号码" prop="idNumber" align="center" />
+      <el-table-column label="出入境次数" prop="count" align="center" />
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      v-model:page="queryParams.pageNum"
+      v-model:limit="queryParams.pageSize"
+      @pagination="handlePagination"
+    />
+  </div>
+</template>
+
+<script setup name="High">
+import * as echarts from 'echarts'
+import { listHighFrequencyInOut, getInOutStats } from "@/api/biz/anal";
+
+const { proxy } = getCurrentInstance();
+
+const list = ref([]);
+const loading = ref(false);
+
+const total = ref(0);
+const barChartContainer = ref(null);
+const pieChartContainer = ref(null);
+const top10 = ref(null); // 添加Top10容器引用
+
+const statistics = ref({
+  totalPeople: 0,
+  totalEntries: 0,
+  avgFrequency: 0,
+  maxFrequency: 0
+});
+
+const data = reactive({
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    queryType: 'year',
+    year: new Date().getFullYear().toString(),
+    month: `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`, // 当前年月
+    dateRange: [
+      new Date(new Date().setDate(new Date().getDate() - 6)).toISOString().split('T')[0], // 7天前
+      new Date().toISOString().split('T')[0] // 今天
+    ]
+  }
+});
+
+const { queryParams } = toRefs(data);
+
+// 监听查询方式变化,设置默认值
+watch(() => queryParams.value.queryType, (newVal) => {
+  if (newVal === 'month' && (!queryParams.value.month || queryParams.value.month === null)) {
+    // 当选择按月查询时,默认选中当月
+    const now = new Date();
+    queryParams.value.month = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
+    // 立即查询
+    handleQuery();
+  } else if (newVal === 'range' && (!queryParams.value.dateRange || queryParams.value.dateRange.length === 0)) {
+    // 当选择按时段查询时,默认最近7天
+    const now = new Date();
+    const startDate = new Date();
+    startDate.setDate(now.getDate() - 6); // 最近7天
+    
+    const startStr = `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}-${String(startDate.getDate()).padStart(2, '0')}`;
+    const endStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
+    
+    queryParams.value.dateRange = [startStr, endStr];
+    // 立即查询
+    handleQuery();
+  } else if (newVal === 'year') {
+    // 当选择按年查询时,也立即查询
+    handleQuery();
+  }
+});
+
+/** 查询高频出入境列表 */
+function getList() {
+  loading.value = true;
+
+  // 准备查询参数
+  const params = {};
+  if (queryParams.value.queryType === 'year') {
+    params.year = queryParams.value.year;
+  } else if (queryParams.value.queryType === 'month') {
+    params.month = queryParams.value.month;
+  } else if (queryParams.value.queryType === 'range') {
+    if (queryParams.value.dateRange && queryParams.value.dateRange.length === 2) {
+      params.startDate = queryParams.value.dateRange[0];
+      params.endDate = queryParams.value.dateRange[1];
+    }
+  }
+
+  // 同时调用列表接口和统计接口
+  Promise.all([
+    listHighFrequencyInOut(params),
+    getInOutStats(params)
+  ]).then(responses => {
+    // 处理列表数据
+    const listResponse = responses[0];
+    list.value = listResponse.rows || [];
+    total.value = listResponse.total || 0;
+
+    // 处理统计信息
+    const statsResponse = responses[1];
+    if (statsResponse.code === 200 && statsResponse.data) {
+      const stats = statsResponse.data;
+      statistics.value = {
+        totalPeople: stats.totalPeople || 0,
+        totalEntries: stats.totalCount || 0,  // 后端返回的是totalCount字段
+        avgFrequency: Number(stats.avgFrequency).toFixed(2) || 0,
+        maxFrequency: stats.maxFrequency || 0
+      };
+    } else {
+      // 如果新接口调用失败,使用原有方式计算统计数据
+      calculateStatistics();
+    }
+
+    // 更新图表
+    nextTick(() => {
+      renderBarChart();
+      renderPieChart();
+      renderTop10Chart(); // 添加Top10图表渲染
+    });
+
+    loading.value = false;
+  }).catch(() => {
+    loading.value = false;
+  });
+}
+
+/** 处理分页事件 */
+function handlePagination() {
+  // 分页只更新表格数据,不重新获取统计和图表数据
+  loading.value = true;
+
+  // 准备查询参数(仅用于表格数据)
+  const params = {};
+  if (queryParams.value.queryType === 'year') {
+    params.year = queryParams.value.year;
+  } else if (queryParams.value.queryType === 'month') {
+    params.month = queryParams.value.month;
+  } else if (queryParams.value.queryType === 'range') {
+    if (queryParams.value.dateRange && queryParams.value.dateRange.length === 2) {
+      params.startDate = queryParams.value.dateRange[0];
+      params.endDate = queryParams.value.dateRange[1];
+    }
+  }
+  
+  // 添加分页参数
+  params.pageNum = queryParams.value.pageNum;
+  params.pageSize = queryParams.value.pageSize;
+
+  listHighFrequencyInOut(params).then(response => {
+    list.value = response.rows || [];
+    total.value = response.total || 0;
+    
+    loading.value = false;
+  }).catch(() => {
+    loading.value = false;
+  });
+}
+
+/** 计算统计数据 */
+function calculateStatistics() {
+  if (!list.value || list.value.length === 0) {
+    statistics.value = {
+      totalPeople: 0,
+      totalEntries: 0,
+      avgFrequency: 0,
+      maxFrequency: 0
+    };
+    return;
+  }
+
+  let totalEntries = 0;
+  let maxFreq = 0;
+
+  list.value.forEach(item => {
+    totalEntries += item.count || 0;
+    if ((item.count || 0) > maxFreq) {
+      maxFreq = item.count;
+    }
+  });
+
+  statistics.value = {
+    totalPeople: list.value.length,
+    totalEntries: totalEntries,
+    avgFrequency: list.value.length > 0 ? (totalEntries / list.value.length).toFixed(2) : 0,
+    maxFrequency: maxFreq
+  };
+}
+
+/** 从前端数据计算频次分布 */
+function calculateFrequencyDistributionFromList() {
+  // 按频次区间分组
+  const ranges = [
+    { name: '1-5次', count: 0, color: '#5470c6' },
+    { name: '6-10次', count: 0, color: '#91cc75' },
+    { name: '11-20次', count: 0, color: '#fac858' },
+    { name: '21-50次', count: 0, color: '#ee6666' },
+    { name: '50次以上', count: 0, color: '#73c0de' }
+  ];
+
+  list.value.forEach(item => {
+    const freq = item.count || 0;
+    if (freq >= 1 && freq <= 5) {
+      ranges[0].count++;
+    } else if (freq > 5 && freq <= 10) {
+      ranges[1].count++;
+    } else if (freq > 10 && freq <= 20) {
+      ranges[2].count++;
+    } else if (freq > 20 && freq <= 50) {
+      ranges[3].count++;
+    } else if (freq > 50) {
+      ranges[4].count++;
+    }
+  });
+
+  return ranges.filter(range => range.count > 0).map(range => ({
+    value: range.count,
+    name: range.name,
+    itemStyle: { color: range.color }
+  }));
+}
+
+/** 渲染柱状图 */
+function renderBarChart() {
+  if (!barChartContainer.value) return;
+
+  const myChart = echarts.init(barChartContainer.value);
+
+  const names = list.value.slice(0, 10).map(item => item.fullName || '未知');
+  const frequencies = list.value.slice(0, 10).map(item => item.count || 0);
+
+  const option = {
+    title: {
+      text: '前10名高频出入境人员',
+      left: 'center'
+    },
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      }
+    },
+    xAxis: {
+      type: 'category',
+      data: names,
+      axisLabel: {
+        rotate: 45,
+        fontSize: 12
+      }
+    },
+    yAxis: {
+      type: 'value',
+      name: '出入境次数'
+    },
+    series: [{
+      data: frequencies,
+      type: 'bar',
+      itemStyle: {
+        color: '#409eff'
+      }
+    }]
+  };
+
+  myChart.setOption(option);
+}
+
+/** 渲染饼图 */
+function renderPieChart() {
+  if (!pieChartContainer.value) return;
+
+  const myChart = echarts.init(pieChartContainer.value);
+
+  // 准备查询参数
+  const params = {};
+  if (queryParams.value.queryType === 'year') {
+    params.year = queryParams.value.year;
+  } else if (queryParams.value.queryType === 'month') {
+    params.month = queryParams.value.month;
+  } else if (queryParams.value.queryType === 'range') {
+    if (queryParams.value.dateRange && queryParams.value.dateRange.length === 2) {
+      params.startDate = queryParams.value.dateRange[0];
+      params.endDate = queryParams.value.dateRange[1];
+    }
+  }
+
+  // 调用后端接口获取频次分布数据
+  getInOutStats(params).then(response => {
+    let distributionData = [];
+    
+    if (response.code === 200 && response.data) {
+      const stats = response.data;
+      
+      // 检查是否有频次分布数据,优先使用后端返回的分布数据
+      if (stats.frequencyDistribution && typeof stats.frequencyDistribution === 'object') {
+        // 后端返回的是对象格式的频次分布数据,例如 {"1-5": 1023, "5-10": 1276, ...}
+        const dist = stats.frequencyDistribution;
+        distributionData = [];
+        
+        // 将对象格式转换为饼图需要的数组格式,并按区间排序
+        const rangeOrder = ['1-5', '5-10', '10-20', '20-30', '30+']; // 定义排序顺序
+        
+        // 先处理预定义的区间
+        for (const range of rangeOrder) {
+          if (dist.hasOwnProperty(range)) {
+            // 根据区间范围选择颜色
+            let color;
+            if (range.startsWith('1-5')) color = '#5470c6';
+            else if (range.startsWith('5-10')) color = '#91cc75';
+            else if (range.startsWith('10-20')) color = '#fac858';
+            else if (range.startsWith('20-30')) color = '#ee6666';
+            else if (range.startsWith('30')) color = '#73c0de'; // 30+
+            else color = '#9da4b0'; // 默认颜色
+            
+            distributionData.push({
+              value: dist[range],
+              name: range,
+              itemStyle: { color: color }
+            });
+          }
+        }
+        
+        // 处理不在预定义区间中的其他区间
+        for (const [range, count] of Object.entries(dist)) {
+          if (!rangeOrder.includes(range)) {
+            // 根据区间范围选择颜色
+            let color;
+            if (range.startsWith('1-5')) color = '#5470c6';
+            else if (range.startsWith('5-10')) color = '#91cc75';
+            else if (range.startsWith('10-20')) color = '#fac858';
+            else if (range.startsWith('20-30')) color = '#ee6666';
+            else if (range.startsWith('30')) color = '#73c0de'; // 30+
+            else color = '#9da4b0'; // 默认颜色
+            
+            distributionData.push({
+              value: count,
+              name: range,
+              itemStyle: { color: color }
+            });
+          }
+        }
+      } else {
+        // 如果后端没有返回频次分布数据,使用统计数据中的信息或者前端计算
+        // 尝试使用已有的统计数据创建分布
+        distributionData = calculateFrequencyDistributionFromList();
+      }
+    } else {
+      // 如果API调用失败,使用前端计算
+      distributionData = calculateFrequencyDistributionFromList();
+    }
+
+    const option = {
+      tooltip: {
+        trigger: 'item'
+      },
+      legend: {
+        orient: 'vertical',
+        left: 'right'
+      },
+      series: [{
+        name: '人数',
+        type: 'pie',
+        radius: '50%',
+        data: distributionData,
+        emphasis: {
+          itemStyle: {
+            shadowBlur: 10,
+            shadowOffsetX: 0,
+            shadowColor: 'rgba(0, 0, 0, 0.5)'
+          }
+        }
+      }]
+    };
+
+    myChart.setOption(option);
+  }).catch(() => {
+    // 错误处理,使用前端计算方式
+    const distributionData = calculateFrequencyDistributionFromList();
+    
+    const option = {
+      tooltip: {
+        trigger: 'item'
+      },
+      legend: {
+        orient: 'vertical',
+        left: 'right'
+      },
+      series: [{
+        name: '人数',
+        type: 'pie',
+        radius: '50%',
+        data: distributionData,
+        emphasis: {
+          itemStyle: {
+            shadowBlur: 10,
+            shadowOffsetX: 0,
+            shadowColor: 'rgba(0, 0, 0, 0.5)'
+          }
+        }
+      }]
+    };
+
+    myChart.setOption(option);
+  });
+}
+
+/** 渲染Top10图表 */
+function renderTop10Chart() {
+  if (!top10.value) return;
+
+  const myChart = echarts.init(top10.value);
+
+  const top10Data = list.value.slice(0, 10);
+  let data1 = [];
+  let data2 = [];
+  let data3 = [];
+
+  for (let index = 0; index < top10Data.length; index++) {
+    const element = top10Data[index];
+    data3.push(element.count || 0);
+    data2.push(index+1);
+    data1.push({
+      name: element.fullName || '未知',
+      value: element.count || 0
+    });
+  }
+
+  let data4 = [100, 100, 100, 100, 100, 100, 100, 100, 100, 100];
+  let style = {
+      width: 24,
+      height: 18,
+      padding: [2, 2, 0, 0],
+      fontSize: 12,
+      align: "center",
+      color: "#ffffff"
+  };
+
+  var option = {
+      tooltip: {
+          show: true,
+          trigger: "item",
+          padding: [8, 15],
+          backgroundColor: "rgba(12, 51, 115,0.8)",
+          borderColor: "rgba(3, 11, 44, 0.5)",
+          textStyle: {
+              color: "rgba(255, 255, 255, 1)",
+              fontFamily: "SourceHanSansCN-Normal",
+              fontSize: 14
+          },
+          formatter: function (params) {
+            if(params.seriesIndex==0) return "";
+            const name = params.name;
+            const value = params.value;
+            return `${name}<br/>出入境次数:${value}`;
+          }
+      },
+      legend: {
+          show: false
+      },
+      grid: {
+          left: "10%",
+          right: "8%",
+          top: "1%",
+          bottom: "2%"
+      },
+      xAxis: [
+          {
+              splitLine: {
+                  show: false
+              },
+              type: "value",
+              show: false,
+              axisLine: {
+                  show: false
+              }
+          }
+      ],
+      yAxis: [
+          {
+              show: true,
+              splitLine: {
+                  show: false
+              },
+              axisLine: {
+                  show: false
+              },
+              type: "category",
+              axisTick: {
+                  show: false
+              },
+              axisPointer: { show: false },
+              triggerEvent: false,
+              inverse: true,
+              data: data2,
+              axisLabel: {
+                  show: true,
+                  margin: 20,
+                  textStyle: {
+                      color: "rgba(255, 255, 255, 1)",
+                      fontFamily: "SourceHanSansCN-Normal",
+                      fontSize: 14
+                  },
+                  formatter: function (params, index) {
+                      const id = index + 1;
+                      if (id < 4) {
+                          return [`{rank${id}|${id}}`].join("\n");
+                      } else {
+                          return [`{rank|${id}}`].join("\n");
+                      }
+                  },
+                  rich: {
+                      rank1: {
+                        ...style,
+                        backgroundColor: {
+                          image:
+                              "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAASCAYAAABB7B6eAAAAAXNSR0IArs4c6QAAARZJREFUOE/tlb1KA0EUhc+5u41F1lbB1tY0Gt8iPoD2eQFtLXwAU/sMIoKQQd0tUghiqW1awZQWQiSGTLzjzrKbymKmCRkYhjt/3zDnzhza/VYXIlegbIEJQEGzTf9i0TGtZVz1+VjGsGmP9/kdaoW2k324zcGljT3o3wBdP6YptpcB1p06DAA0BdeAUisvfCWy17CPweMpgblelYocVIMyC2+x8XnM6+dJLAAAecH0pxsP4DJTBjEBT5jOjmIBbvA1OeFw+B0egOQS5uEsWhat4ks+yOYQ0a80xG9qaQrNz6rQdjZfQe4FArzRFO0m4DDb/XWZC0B2nKFASnOp+YEzG41TQFIA3nh0bmVA75gl58zzUR2wADMYXqBI8VUUAAAAAElFTkSuQmCC"
+                        }
+                      },
+                      rank2: {
+                        ...style,
+                        backgroundColor: {
+                          image:
+                              "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAASCAYAAABB7B6eAAAAAXNSR0IArs4c6QAAAPFJREFUOE/tlTFKA0EUhr9/UuUGK9imTW4SD6CNlc2GgJgubBYPoLWHEEHwBjaWprUVIsQ+m2YnZHYdZtdydxrxwcDMMLwP5r33/7I5U9ADKEHQXLY6m9Z9eHZ79+4LuNKMZ4KQXWmDlLi7bgAHUcpJC2CsS9wPAKU+W5XSrv4Bvsg/Nbxny41yyihfVFfgiT3numbXew18J8IbYhoPULX8SzwAvFJwFgvwyJALXVL0D4A7vllE66K/OMmZKTH1eHRXU6vUibuPo1y/I417UtO1UiZNwJIRA3OL7Kk3l1C6f5lL7RsNE3KG80lJpjkfIeAAkjJvqT1lfksAAAAASUVORK5CYII="
+                        }
+                      },
+                      rank3: {
+                        ...style,
+                        backgroundColor: {
+                          image:
+                              "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAASCAYAAABB7B6eAAAAAXNSR0IArs4c6QAAAPBJREFUOE/tlT9OwzAUh7+fYWfpAFIZOrBSpblHegC69wLtihAHoDOHQAgabsDSsV1ZKzUnaDvFla00JFG31AviWZZly3rfs98/2ZQE8Yq4xokBVJnHfXU9dSYyDGMN+PR6CpFN2XjlTmk7gDMsU8xNE2BLi9sDUFya6jnuBf+AemAYZnww0RN5kC8q/PlOhwfdsju7DzzAhTEsEElIgAN9hQR8s2UYCvDGFSP12IcAvBAxlQJF0d/M5Bz50b6aGqwGRRaU5XrOEsP9WQBipZh+vVzPuUM8Y+iWZfvYUNzNi0bzaTab38xdc8mjIn6qgAME+UlV+Na7MwAAAABJRU5ErkJggg=="
+                        }
+                      },
+                      rank: {
+                        ...style,
+                        backgroundColor: {
+                          image:
+                              "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAASCAYAAABB7B6eAAAAAXNSR0IArs4c6QAAAQdJREFUOE/tlb9KxEAQh7/fWvkGCra2+hK59ixWbLSx8gW0FfEBFHOVna0JhyD4BjaWprUVvM5SDzQjm0tCTsQmt424zbDsn29mmPmNBiMbCi4RKxIgCFbuF/vz2QTHQe51S2cpSe1F9ec9AcGpSea1OgcYpGat1/0iqKLOfOVmu/QP+F4QiPOs4JATlSFPC09RABrc2Ae7+Y7eogDqonmYlgxjAkI/3cUDOO4/p2zFATjGy6/sXe3rPQbg7LrgKFoV/cFOTlIrJVR1ZE+xw2G5D5LXEbsktUeJjQUBitxrc15NL2wdxylizTUR1AMldKRrhsvSLML2TrMP/s7ePZs4Hm/rqQv4AucUYfTiSAZXAAAAAElFTkSuQmCC"
+                        }
+                      },
+                      a: {
+                          padding: [0, 0, 35, 10]
+                      },
+
+                      b: {
+                          backgroundColor: {
+                              type: "linear",
+                              x: 0,
+                              y: 0,
+                              x2: 1,
+                              y2: 0,
+                              colorStops: [
+                                  {
+                                      offset: 0,
+                                      color: "#29DFF6"
+                                  },
+                                  {
+                                      offset: 1,
+                                      color: "#00A8FF"
+                                  }
+                              ],
+                              global: false
+                          },
+                          color: "#D1E7FF",
+                          width: 60,
+                          height: 60,
+                          padding: [5, 0, 0, 0],
+                          borderRadius: 30,
+                          fontSize: 14,
+                          align: "center",
+                          fontWeight: 500
+                      }
+                  }
+              }
+          },
+          {
+              type: "category",
+              inverse: true,
+              axisTick: "none",
+              axisLine: "none",
+              show: true,
+              position: "right",
+              axisLabel: {
+                  inside: true,
+                  padding: [-24, 0, 0, 0],
+                  margin: 0,
+                  show: true,
+                  textStyle: {
+                      verticalAlign: "top",
+                      color: "#445482",
+                      fontFamily: "SourceHanSansCN-Normal",
+                      fontSize: 14
+                  },
+                  formatter: function (value) {
+                      return value + "";
+                  }
+              },
+              data: data3
+          }
+      ],
+      series: [
+          {
+              name: "背景",
+              type: "bar",
+              barWidth: 12,
+              barGap: "-100%",
+              data: data4,
+              itemStyle: {
+                  normal: {
+                      color: "#D1E7FF",
+                      barBorderRadius: 10
+                  }
+              }
+          },
+          {
+              show: true,
+              name: "",
+              type: "bar",
+              data: data1,
+              barWidth: 12,
+              showBackground: false,
+              backgroundStyle: {
+                  color: {
+                      type: "linear",
+                      x: 0,
+                      y: 0,
+                      x2: 1,
+                      y2: 1,
+                      colorStops: [
+                          {
+                              offset: 0,
+                              color: "#D1E7FF"
+                          },
+                          {
+                              offset: 1,
+                              color: "#D1E7FF"
+                          }
+                      ]
+                  }
+              },
+              label: {
+                  show: true,
+                  offset: [5, -17],
+                  color: "#132852",
+                  fontFamily: "SourceHanSansCN-Normal",
+                  fontSize: 14,
+                  fontWeight: 500,
+                  position: "left",
+                  align: "left",
+                  formatter: function (params) {
+                      return params.data.name;
+                  }
+              },
+              itemStyle: {
+                  barBorderRadius: [5, 5, 5, 5],
+                  color: {
+                      type: "linear",
+                      x: 0,
+                      y: 0,
+                      x2: 1,
+                      y2: 0,
+                      colorStops: [
+                          {
+                              offset: 0,
+                              color: "#29DFF6"
+                          },
+                          {
+                              offset: 1,
+                              color: "#00A8FF"
+                          }
+                      ]
+                  }
+              }
+          }
+      ],
+      dataZoom: [
+          {
+              width: 15,
+              yAxisIndex: [0, 1],
+              show: false,
+              type: "slider"
+          }
+      ]
+  };
+
+  myChart.setOption(option);
+}
+
+/** 搜索按钮操作 */
+function handleQuery() {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+
+/** 重置按钮操作 */
+function resetQuery() {
+  proxy.resetForm("queryRef");
+  queryParams.value.queryType = 'year';
+  queryParams.value.year = new Date().getFullYear().toString();
+  queryParams.value.month = null;
+  queryParams.value.dateRange = null;
+  handleQuery();
+}
+
+// 页面加载时获取数据
+onMounted(() => {
+  // 初始化时直接查询,使用reactive中设置的默认值
+  handleQuery();
+});
+</script>
+
+<style scoped>
+/* 统计卡片样式 */
+.stat-card {
+  text-align: center;
+  border-radius: 12px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  transition: all 0.3s ease;
+  height: 100%;
+}
+
+.stat-card:hover {
+  transform: translateY(-3px);
+  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
+}
+
+/* 不同统计卡片的颜色 */
+.stat-card.total-people {
+  background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
+}
+
+.stat-card.total-entries {
+  background: linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%);
+}
+
+.stat-card.avg-frequency {
+  background: linear-gradient(135deg, #fffbe6 0%, #fff5a0 100%);
+}
+
+.stat-card.max-frequency {
+  background: linear-gradient(135deg, #fff2f0 0%, #ffccc7 100%);
+}
+
+.stat-item {
+  padding: 15px 10px;
+}
+
+.stat-number {
+  font-size: 22px;
+  font-weight: bold;
+  margin-bottom: 5px;
+}
+
+/* 不同统计数字的颜色 */
+.stat-number {
+  color: #1890ff;
+}
+
+.stat-card:nth-child(2) .stat-number {
+  color: #52c41a;
+}
+
+.stat-card:nth-child(3) .stat-number {
+  color: #faad14;
+}
+
+.stat-card:nth-child(4) .stat-number {
+  color: #f5222d;
+}
+
+.stat-label {
+  font-size: 13px;
+  color: #606266;
+  font-weight: 500;
+}
+
+.chart-container {
+  margin-bottom: 20px;
+}
+
+#chart, #pieChart {
+  width: 100%;
+}
+
+.table_caption {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+  text-align: center;
+  margin-bottom: 10px;
+}
+
+.app-container {
+  padding: 15px;
+}
+
+.el-row {
+  margin-bottom: 15px;
+}
+
+.el-table {
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.el-card {
+  border-radius: 8px;
+  border: none;
+}
+</style>

+ 85 - 142
src/views/biz/config/index.vue

@@ -3,125 +3,65 @@
     <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
       <el-form-item label="年份" prop="holidayYear">
         <el-date-picker clearable
-          v-model="queryParams.holidayYear"
-          type="date"
-          value-format="YYYY-MM-DD"
-          placeholder="请选择年份">
+                        v-model="queryParams.holidayYear"
+                        type="year"
+                        value-format="YYYY"
+                        placeholder="请选择年份"
+                        @change="handleYearChange">
         </el-date-picker>
       </el-form-item>
-      <el-form-item label="节假日名称" prop="holidayName">
-        <el-input
-          v-model="queryParams.holidayName"
-          placeholder="请输入节假日名称"
-          clearable
-          @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="开始日期" prop="startDate">
-        <el-date-picker clearable
-          v-model="queryParams.startDate"
-          type="date"
-          value-format="YYYY-MM-DD"
-          placeholder="请选择开始日期">
-        </el-date-picker>
-      </el-form-item>
-      <el-form-item label="结束日期" prop="endDate">
-        <el-date-picker clearable
-          v-model="queryParams.endDate"
-          type="date"
-          value-format="YYYY-MM-DD"
-          placeholder="请选择结束日期">
-        </el-date-picker>
-      </el-form-item>
-      <el-form-item label="创建人" prop="createBy">
-        <el-input
-          v-model="queryParams.createBy"
-          placeholder="请输入创建人"
-          clearable
-          @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="创建时间" prop="createTime">
-        <el-date-picker clearable
-          v-model="queryParams.createTime"
-          type="date"
-          value-format="YYYY-MM-DD"
-          placeholder="请选择创建时间">
-        </el-date-picker>
-      </el-form-item>
-      <el-form-item label="更新人" prop="updateBy">
-        <el-input
-          v-model="queryParams.updateBy"
-          placeholder="请输入更新人"
-          clearable
-          @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="更新时间" prop="updateTime">
-        <el-date-picker clearable
-          v-model="queryParams.updateTime"
-          type="date"
-          value-format="YYYY-MM-DD"
-          placeholder="请选择更新时间">
-        </el-date-picker>
-      </el-form-item>
-      <el-form-item>
-        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-      </el-form-item>
     </el-form>
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
         <el-button
-          type="primary"
-          plain
-          icon="Plus"
-          @click="handleAdd"
-          v-hasPermi="['biz:config:add']"
-        >新增</el-button>
+            type="primary"
+            plain
+            icon="Plus"
+            @click="handleAdd"
+            v-hasPermi="['biz:config:add']"
+        >新增
+        </el-button>
       </el-col>
       <el-col :span="1.5">
         <el-button
-          type="success"
-          plain
-          icon="Edit"
-          :disabled="single"
-          @click="handleUpdate"
-          v-hasPermi="['biz:config:edit']"
-        >修改</el-button>
+            type="success"
+            plain
+            icon="Edit"
+            :disabled="single"
+            @click="handleUpdate"
+            v-hasPermi="['biz:config:edit']"
+        >修改
+        </el-button>
       </el-col>
       <el-col :span="1.5">
         <el-button
-          type="danger"
-          plain
-          icon="Delete"
-          :disabled="multiple"
-          @click="handleDelete"
-          v-hasPermi="['biz:config:remove']"
-        >删除</el-button>
+            type="danger"
+            plain
+            icon="Delete"
+            :disabled="multiple"
+            @click="handleDelete"
+            v-hasPermi="['biz:config:remove']"
+        >删除
+        </el-button>
       </el-col>
       <el-col :span="1.5">
         <el-button
-          type="warning"
-          plain
-          icon="Download"
-          @click="handleExport"
-          v-hasPermi="['biz:config:export']"
-        >导出</el-button>
+            type="warning"
+            plain
+            icon="Download"
+            @click="handleExport"
+            v-hasPermi="['biz:config:export']"
+        >导出
+        </el-button>
       </el-col>
       <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
     <el-table v-loading="loading" :data="configList" @selection-change="handleSelectionChange">
-      <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="自增ID" align="center" prop="autoId" />
-      <el-table-column label="年份" align="center" prop="holidayYear" width="180">
-        <template #default="scope">
-          <span>{{ parseTime(scope.row.holidayYear, '{y}-{m}-{d}') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="节假日名称" align="center" prop="holidayName" />
+      <el-table-column type="selection" width="55" align="center"/>
+      <el-table-column label="年份" align="center" prop="holidayYear"/>
+      <el-table-column label="节假日名称" align="center" prop="holidayName"/>
       <el-table-column label="开始日期" align="center" prop="startDate" width="180">
         <template #default="scope">
           <span>{{ parseTime(scope.row.startDate, '{y}-{m}-{d}') }}</span>
@@ -132,67 +72,59 @@
           <span>{{ parseTime(scope.row.endDate, '{y}-{m}-{d}') }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="创建人" align="center" prop="createBy" />
-      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
-        <template #default="scope">
-          <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="更新人" align="center" prop="updateBy" />
-      <el-table-column label="更新时间" align="center" prop="updateTime" width="180">
-        <template #default="scope">
-          <span>{{ parseTime(scope.row.updateTime, '{y}-{m}-{d}') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="备注" align="center" prop="remark"/>
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template #default="scope">
-          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['biz:config:edit']">修改</el-button>
-          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['biz:config:remove']">删除</el-button>
+          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['biz:config:edit']">
+            修改
+          </el-button>
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['biz:config:remove']">删除
+          </el-button>
         </template>
       </el-table-column>
     </el-table>
-    
+
     <pagination
-      v-show="total>0"
-      :total="total"
-      v-model:page="queryParams.pageNum"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
+        v-show="total>0"
+        :total="total"
+        v-model:page="queryParams.pageNum"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
     />
 
     <!-- 添加或修改节假日配置对话框 -->
     <el-dialog :title="title" v-model="open" width="500px" append-to-body>
-      <el-form ref="configRef" :model="form" :rules="rules" label-width="80px">
+      <el-form ref="configRef" :model="form" :rules="rules" label-width="120px">
         <el-form-item label="年份" prop="holidayYear">
           <el-date-picker clearable
-            v-model="form.holidayYear"
-            type="date"
-            value-format="YYYY-MM-DD"
-            placeholder="请选择年份">
+                          v-model="form.holidayYear"
+                          type="year"
+                          value-format="YYYY"
+                          placeholder="请选择年份">
           </el-date-picker>
         </el-form-item>
         <el-form-item label="节假日名称" prop="holidayName">
-          <el-input v-model="form.holidayName" placeholder="请输入节假日名称" />
+          <el-input v-model="form.holidayName" placeholder="请输入节假日名称"/>
         </el-form-item>
         <el-form-item label="开始日期" prop="startDate">
           <el-date-picker clearable
-            v-model="form.startDate"
-            type="date"
-            value-format="YYYY-MM-DD"
-            placeholder="请选择开始日期">
+                          v-model="form.startDate"
+                          type="date"
+                          value-format="YYYY-MM-DD"
+                          placeholder="请选择开始日期">
           </el-date-picker>
         </el-form-item>
         <el-form-item label="结束日期" prop="endDate">
           <el-date-picker clearable
-            v-model="form.endDate"
-            type="date"
-            value-format="YYYY-MM-DD"
-            placeholder="请选择结束日期">
+                          v-model="form.endDate"
+                          type="date"
+                          value-format="YYYY-MM-DD"
+                          placeholder="请选择结束日期">
           </el-date-picker>
         </el-form-item>
         <el-form-item label="备注" prop="remark">
-          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"/>
         </el-form-item>
       </el-form>
       <template #footer>
@@ -206,9 +138,9 @@
 </template>
 
 <script setup name="Config">
-import { listConfig, getConfig, delConfig, addConfig, updateConfig } from "@/api/biz/config";
+import {addConfig, delConfig, getConfig, listConfig, updateConfig} from "@/api/biz/config";
 
-const { proxy } = getCurrentInstance();
+const {proxy} = getCurrentInstance();
 
 const configList = ref([]);
 const open = ref(false);
@@ -225,7 +157,7 @@ const data = reactive({
   queryParams: {
     pageNum: 1,
     pageSize: 10,
-    holidayYear: null,
+    holidayYear: new Date().getFullYear().toString(),
     holidayName: null,
     startDate: null,
     endDate: null,
@@ -235,11 +167,10 @@ const data = reactive({
     updateTime: null,
     remark: null
   },
-  rules: {
-  }
+  rules: {}
 });
 
-const { queryParams, form, rules } = toRefs(data);
+const {queryParams, form, rules} = toRefs(data);
 
 /** 查询节假日配置列表 */
 function getList() {
@@ -306,6 +237,10 @@ function handleUpdate(row) {
   const _autoId = row.autoId || ids.value
   getConfig(_autoId).then(response => {
     form.value = response.data;
+    // 确保年份字段正确显示
+    if (response.data.holidayYear) {
+      form.value.holidayYear = response.data.holidayYear.toString();
+    }
     open.value = true;
     title.value = "修改节假日配置";
   });
@@ -335,12 +270,13 @@ function submitForm() {
 /** 删除按钮操作 */
 function handleDelete(row) {
   const _autoIds = row.autoId || ids.value;
-  proxy.$modal.confirm('是否确认删除节假日配置编号为"' + _autoIds + '"的数据项?').then(function() {
+  proxy.$modal.confirm('是否确认删除节假日配置编号为"' + _autoIds + '"的数据项?').then(function () {
     return delConfig(_autoIds);
   }).then(() => {
     getList();
     proxy.$modal.msgSuccess("删除成功");
-  }).catch(() => {});
+  }).catch(() => {
+  });
 }
 
 /** 导出按钮操作 */
@@ -350,5 +286,12 @@ function handleExport() {
   }, `config_${new Date().getTime()}.xlsx`)
 }
 
+// 年份选择变化事件
+function handleYearChange(value) {
+  console.log('选择的年份:', value);
+  // 年份变化时重新查询数据
+  handleQuery();
+}
+
 getList();
 </script>