327 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			327 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | |
|      <div class="chart-container">
 | |
|         <!-- 图表标题和时间范围选择器 -->
 | |
|         <div class="chart-header">
 | |
|             <h2>出勤趋势分析</h2>
 | |
|             <div class="chart-actions">
 | |
|                 <button @click="timeRange = 'week'" :class="{ active: timeRange === 'week' }">每周</button>
 | |
|                 <button @click="timeRange = 'month'" :class="{ active: timeRange === 'month' }">每月</button>
 | |
|             </div>
 | |
|         </div>
 | |
|         <!-- 图表内容区域 -->
 | |
|         <div ref="chartRef" class="chart-content"></div>
 | |
|     </div>
 | |
| </template>
 | |
| 
 | |
| <script setup>
 | |
| import { ref, onMounted, computed, watch } from 'vue';
 | |
| import * as echarts from 'echarts';
 | |
| 
 | |
| // 接收从父组件传入的数据
 | |
| const props = defineProps({
 | |
|   attendData: {
 | |
|     type: Object,
 | |
|     default: () => ({
 | |
|       week: {
 | |
|         xAxis: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
 | |
|         actualCount: [40, 20, 30, 15, 22, 63, 58],
 | |
|         expectedCount: [100, 556, 413, 115, 510, 115, 317]
 | |
|       },
 | |
|       month: {
 | |
|         xAxis: ['第1周', '第2周', '第3周', '第4周'],
 | |
|         actualData: [280, 360, 320, 400],
 | |
|         theoreticalData: [300, 400, 350, 450]
 | |
|       }
 | |
|     })
 | |
|   }
 | |
| });
 | |
| 
 | |
| // 图表DOM引用
 | |
| const chartRef = ref(null);
 | |
| // 图表实例
 | |
| let chartInstance = null;
 | |
| 
 | |
| // 时间范围状态
 | |
| const timeRange = ref('week');
 | |
| 
 | |
| // 根据时间范围计算当前显示的数据
 | |
| const chartData = computed(() => {
 | |
|   const dataForRange = props.attendData[timeRange.value] || props.attendData.week;
 | |
|   
 | |
|   // 处理字段名称差异
 | |
|   if (timeRange.value === 'week') {
 | |
|     return {
 | |
|       xAxis: dataForRange.xAxis || [],
 | |
|       actualCount: dataForRange.actualCount || [],
 | |
|       expectedCount: dataForRange.expectedCount || []
 | |
|     };
 | |
|   } else {
 | |
|     return {
 | |
|       xAxis: dataForRange.xAxis || [],
 | |
|       actualCount: dataForRange.actualData || [],
 | |
|       expectedCount: dataForRange.theoreticalData || []
 | |
|     };
 | |
|   }
 | |
| });
 | |
| 
 | |
| // 定义颜色常量
 | |
| const ACTUAL_COUNT_COLOR = '#029CD4'; // 蓝色 - 实际人数
 | |
| const EXPECTED_COUNT_COLOR = '#0052D9'; // 蓝色 - 应出勤人数
 | |
| 
 | |
| // 初始化图表
 | |
| const initChart = () => {
 | |
|     if (chartRef.value && !chartInstance) {
 | |
|         chartInstance = echarts.init(chartRef.value);
 | |
|     }
 | |
| 
 | |
|     // 使用计算后的数据
 | |
|     const { xAxis, actualCount, expectedCount } = chartData.value;
 | |
|     
 | |
| 
 | |
|     const option = {
 | |
|         tooltip: {
 | |
|             trigger: 'axis',
 | |
|             backgroundColor: 'rgba(255,255,255,1)',
 | |
|             borderColor: '#ddd',
 | |
|             borderWidth: 1,
 | |
|             textStyle: {
 | |
|                 color: '#333',
 | |
|                 fontSize: 14
 | |
|             },
 | |
|             formatter: function(params) {
 | |
|                 const actualCount = params[0].value;
 | |
|                 const expectedCount = params[1].value;
 | |
|                 return `
 | |
|                     <div style="padding: 5px;">
 | |
|                         <div style="color: ${params[0].color};">实际人数: ${actualCount}</div>
 | |
|                         <div style="color: ${params[1].color};">应出勤人数: ${expectedCount}</div>
 | |
|                     </div>
 | |
|                 `;
 | |
|             }
 | |
|         },
 | |
|         legend: {
 | |
|             top: 30,
 | |
|             left: 'center',
 | |
|             itemWidth: 10,
 | |
|             itemHeight: 10,
 | |
|             itemGap: 25,
 | |
|             data: ['实际人数', '应出勤人数'],
 | |
|             textStyle: {
 | |
|                 color: '#666',
 | |
|                 fontSize: 12
 | |
|             }
 | |
|         },
 | |
|         grid: {
 | |
|             top: '30%',
 | |
|             right: '10%',
 | |
|             bottom: '10%',
 | |
|             left: '6%',
 | |
|             containLabel: true
 | |
|         },
 | |
|         xAxis: {
 | |
|             data: xAxis,
 | |
|             type: 'category',
 | |
|             boundaryGap: true,
 | |
|             axisLabel: {
 | |
|                 textStyle: {
 | |
|                     color: '#666',
 | |
|                     fontSize: 12
 | |
|                 }
 | |
|             },
 | |
|             axisTick: {
 | |
|                 show: false
 | |
|             },
 | |
|             axisLine: {
 | |
|                 lineStyle: {
 | |
|                     color: '#ddd'
 | |
|                 }
 | |
|             }
 | |
|         },
 | |
|         yAxis: [
 | |
|             {
 | |
|                 type: 'value',
 | |
|                 name: '人数',
 | |
|                 nameTextStyle: {
 | |
|                     color: '#666',
 | |
|                     fontSize: 12
 | |
|                 },
 | |
|                 interval: 100,
 | |
|                 axisLabel: {
 | |
|                     textStyle: {
 | |
|                         color: '#666',
 | |
|                         fontSize: 12
 | |
|                     }
 | |
|                 },
 | |
|                 axisTick: {
 | |
|                     show: false
 | |
|                 },
 | |
|                 axisLine: {
 | |
|                     show: false
 | |
|                 },
 | |
|                 splitLine: {
 | |
|                     lineStyle: {
 | |
|                         color: '#f0f0f0',
 | |
|                         type: 'dashed'
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         ],
 | |
|         series: [
 | |
|             {
 | |
|                 name: '实际人数',
 | |
|                 type: 'bar',
 | |
|                 barWidth: '40%',
 | |
|                 itemStyle: {
 | |
|                     color: ACTUAL_COUNT_COLOR
 | |
|                 },
 | |
|                 data: actualCount
 | |
|             },
 | |
|             {
 | |
|                 name: '应出勤人数',
 | |
|                 type: 'line',
 | |
|                 showSymbol: false,
 | |
|                 symbol: 'circle',
 | |
|                 symbolSize: 6,
 | |
|                 emphasis: {
 | |
|                     showSymbol: true,
 | |
|                     symbolSize: 10
 | |
|                 },
 | |
|                 lineStyle: {
 | |
|                     width: 2,
 | |
|                     color: EXPECTED_COUNT_COLOR
 | |
|                 },
 | |
|                 itemStyle: {
 | |
|                     color: EXPECTED_COUNT_COLOR,
 | |
|                     borderColor: '#fff',
 | |
|                     borderWidth: 2
 | |
|                 },
 | |
|                 data: expectedCount
 | |
|             }
 | |
|         ]
 | |
|     };
 | |
| 
 | |
|     chartInstance.setOption(option);
 | |
| };
 | |
| 
 | |
| // 响应窗口大小变化
 | |
| const handleResize = () => {
 | |
|     if (chartInstance) {
 | |
|         chartInstance.resize();
 | |
|     }
 | |
| };
 | |
| 
 | |
| // 监听时间范围变化,更新图表
 | |
| watch(timeRange, () => {
 | |
|     initChart();
 | |
| });
 | |
| 
 | |
| // 监听数据变化,更新图表
 | |
| watch(() => props.attendData, () => {
 | |
|     initChart();
 | |
| }, { deep: true });
 | |
| 
 | |
| // 生命周期钩子
 | |
| onMounted(() => {
 | |
|     initChart();
 | |
|     window.addEventListener('resize', handleResize);
 | |
| 
 | |
|     // 清理函数
 | |
|     return () => {
 | |
|         window.removeEventListener('resize', handleResize);
 | |
|         if (chartInstance) {
 | |
|             chartInstance.dispose();
 | |
|             chartInstance = null;
 | |
|         }
 | |
|     };
 | |
| });
 | |
| 
 | |
| </script>
 | |
| 
 | |
| <style scoped>
 | |
| .chart-container {
 | |
|     background-color: #fff;
 | |
|     border-radius: 8px;
 | |
|     overflow: hidden;
 | |
|     height: 435px;
 | |
|     width: 100%;
 | |
|     padding: 10px;
 | |
|     box-sizing: border-box;
 | |
| }
 | |
| 
 | |
| .chart-header {
 | |
|     display: flex;
 | |
|     justify-content: space-between;
 | |
|     align-items: center;
 | |
|     padding: 15px 20px;
 | |
|     border-bottom: 1px solid #f0f0f0;
 | |
| }
 | |
| 
 | |
| .chart-header h2 {
 | |
|     font-size: 16px;
 | |
|     font-weight: 600;
 | |
|     color: #333;
 | |
|     margin: 0;
 | |
| }
 | |
| 
 | |
| .chart-actions button {
 | |
|     background: none;
 | |
|     border: 1px solid #e0e0e0;
 | |
|     padding: 5px 12px;
 | |
|     border-radius: 4px;
 | |
|     margin-left: 8px;
 | |
|     cursor: pointer;
 | |
|     font-size: 12px;
 | |
|     transition: all 0.2s ease;
 | |
| }
 | |
| 
 | |
| .chart-actions button.active {
 | |
|     background-color: #1890ff;
 | |
|     color: white;
 | |
|     border-color: #1890ff;
 | |
| }
 | |
| 
 | |
| .chart-content {
 | |
|     width: 100%;
 | |
|     height: calc(100% - 54px);
 | |
|     padding: 10px;
 | |
| }
 | |
| 
 | |
| @media (max-width: 768px) {
 | |
|     .chart-container {
 | |
|         height: 435px;
 | |
|     }
 | |
| }
 | |
| 
 | |
| @media (max-width: 480px) {
 | |
|     .chart-container {
 | |
|         height: 400px;
 | |
|     }
 | |
| 
 | |
|     .chart-header {
 | |
|         flex-direction: column;
 | |
|         align-items: flex-start;
 | |
|         gap: 10px;
 | |
|     }
 | |
| 
 | |
|     .chart-actions {
 | |
|         width: 100%;
 | |
|         display: flex;
 | |
|         justify-content: space-between;
 | |
|     }
 | |
| 
 | |
|     .chart-actions button {
 | |
|         margin: 0;
 | |
|         flex: 1;
 | |
|         margin-right: 5px;
 | |
|     }
 | |
| 
 | |
|     .chart-actions button:last-child {
 | |
|         margin-right: 0;
 | |
|     }
 | |
| }
 | |
| 
 | |
| .model {
 | |
|     padding: 20px;
 | |
|     background-color: rgba(242, 248, 252, 1);
 | |
| }
 | |
| </style> | 
