| | |
| | | size="small" |
| | | @click="exportReport" |
| | | icon="el-icon-download" |
| | | type="primary" |
| | | > |
| | | 导出报表 |
| | | </el-button> |
| | |
| | | |
| | | <!-- 统计概览 --> |
| | | <el-row :gutter="20" class="stats-overview"> |
| | | <el-col :span="6"> |
| | | <el-col :xs="12" :sm="6" class="stat-col"> |
| | | <el-card shadow="hover" class="stat-card"> |
| | | <div class="stat-content"> |
| | | <div class="stat-icon attendance-icon"> |
| | |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-col :xs="12" :sm="6" class="stat-col"> |
| | | <el-card shadow="hover" class="stat-card"> |
| | | <div class="stat-content"> |
| | | <div class="stat-icon present-icon"> |
| | |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-col :xs="12" :sm="6" class="stat-col"> |
| | | <el-card shadow="hover" class="stat-card"> |
| | | <div class="stat-content"> |
| | | <div class="stat-icon abnormal-icon"> |
| | |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-col :xs="12" :sm="6" class="stat-col"> |
| | | <el-card shadow="hover" class="stat-card"> |
| | | <div class="stat-content"> |
| | | <div class="stat-icon rate-icon"> |
| | |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 图表区域 --> |
| | | <!-- 图表区域 - 优化布局 --> |
| | | <el-row :gutter="20" class="charts-section"> |
| | | <el-col :span="12"> |
| | | <el-card header="出勤趋势" shadow="never"> |
| | | <el-col :xs="24" :lg="12" class="chart-col"> |
| | | <el-card class="chart-card" shadow="never"> |
| | | <template #header> |
| | | <div class="chart-header"> |
| | | <span class="chart-title">出勤趋势分析</span> |
| | | <div class="chart-legend"> |
| | | <span class="legend-item"> |
| | | <span class="legend-color bar-color"></span> |
| | | 出勤天数 |
| | | </span> |
| | | <span class="legend-item"> |
| | | <span class="legend-color line-color"></span> |
| | | 出勤率 |
| | | </span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <div id="attendanceTrendChart" class="chart-container"></div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-card header="考勤分布" shadow="never"> |
| | | <el-col :xs="24" :lg="12" class="chart-col"> |
| | | <el-card class="chart-card" shadow="never"> |
| | | <template #header> |
| | | <div class="chart-header"> |
| | | <span class="chart-title">考勤分布</span> |
| | | </div> |
| | | </template> |
| | | <div id="attendanceDistributionChart" class="chart-container"></div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 详细统计表格 --> |
| | | <el-card header="详细统计" class="detail-table-card" shadow="never"> |
| | | <el-table :data="detailedStats" border style="width: 100%"> |
| | | <el-table-column prop="month" label="月份" /> |
| | | <el-table-column prop="workDays" label="应出勤天数" /> |
| | | <el-table-column prop="actualDays" label="实际出勤" /> |
| | | <el-table-column prop="lateTimes" label="迟到次数" > |
| | | <template #default="scope"> |
| | | <el-tag v-if="scope.row.lateTimes > 0" type="warning" size="small"> |
| | | {{ scope.row.lateTimes }} |
| | | </el-tag> |
| | | <span v-else>{{ scope.row.lateTimes }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="leaveEarlyTimes" label="早退次数" > |
| | | <template #default="scope"> |
| | | <el-tag v-if="scope.row.leaveEarlyTimes > 0" type="warning" size="small"> |
| | | {{ scope.row.leaveEarlyTimes }} |
| | | </el-tag> |
| | | <span v-else>{{ scope.row.leaveEarlyTimes }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="absenceDays" label="缺勤天数" > |
| | | <template #default="scope"> |
| | | <el-tag v-if="scope.row.absenceDays > 0" type="danger" size="small"> |
| | | {{ scope.row.absenceDays }} |
| | | </el-tag> |
| | | <span v-else>{{ scope.row.absenceDays }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="businessTripDays" label="出差天数" > |
| | | <template #default="scope"> |
| | | <el-tag v-if="scope.row.businessTripDays > 0" type="primary" size="small"> |
| | | {{ scope.row.businessTripDays }} |
| | | </el-tag> |
| | | <span v-else>{{ scope.row.businessTripDays }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="attendanceRate" label="出勤率" > |
| | | <template #default="scope"> |
| | | <el-progress |
| | | :percentage="scope.row.attendanceRate" |
| | | :show-text="false" |
| | | :color="getProgressColor(scope.row.attendanceRate)" |
| | | /> |
| | | <span>{{ scope.row.attendanceRate }}%</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <el-card class="detail-card" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span class="card-title">月度详细统计</span> |
| | | <el-button size="mini" type="text" @click="toggleTableExpand"> |
| | | {{ tableExpanded ? '收起' : '展开' }} |
| | | <i :class="tableExpanded ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i> |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | <el-collapse-transition> |
| | | <div v-show="tableExpanded"> |
| | | <el-table |
| | | :data="detailedStats" |
| | | border |
| | | style="width: 100%" |
| | | class="stats-table" |
| | | :row-class-name="getRowClassName" |
| | | > |
| | | <!-- 表格列定义保持不变 --> |
| | | <el-table-column prop="month" label="月份" align="center" /> |
| | | <el-table-column prop="workDays" label="应出勤天数" align="center" /> |
| | | <el-table-column prop="actualDays" label="实际出勤" align="center" /> |
| | | <el-table-column prop="lateTimes" label="迟到次数" align="center"> |
| | | <template #default="scope"> |
| | | <el-tag v-if="scope.row.lateTimes > 0" type="warning" size="small"> |
| | | {{ scope.row.lateTimes }} |
| | | </el-tag> |
| | | <span v-else>{{ scope.row.lateTimes }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="leaveEarlyTimes" label="早退次数" align="center"> |
| | | <template #default="scope"> |
| | | <el-tag v-if="scope.row.leaveEarlyTimes > 0" type="warning" size="small"> |
| | | {{ scope.row.leaveEarlyTimes }} |
| | | </el-tag> |
| | | <span v-else>{{ scope.row.leaveEarlyTimes }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="absenceDays" label="缺勤天数" align="center"> |
| | | <template #default="scope"> |
| | | <el-tag v-if="scope.row.absenceDays > 0" type="danger" size="small"> |
| | | {{ scope.row.absenceDays }} |
| | | </el-tag> |
| | | <span v-else>{{ scope.row.absenceDays }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="businessTripDays" label="出差天数" align="center"> |
| | | <template #default="scope"> |
| | | <el-tag v-if="scope.row.businessTripDays > 0" type="primary" size="small"> |
| | | {{ scope.row.businessTripDays }} |
| | | </el-tag> |
| | | <span v-else>{{ scope.row.businessTripDays }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="attendanceRate" label="出勤率" align="center" min-width="100"> |
| | | <template #default="scope"> |
| | | <div class="progress-cell"> |
| | | <el-progress |
| | | :percentage="scope.row.attendanceRate" |
| | | :show-text="false" |
| | | :color="getProgressColor(scope.row.attendanceRate)" |
| | | class="rate-progress" |
| | | /> |
| | | <span class="rate-text" :class="getRateTextClass(scope.row.attendanceRate)"> |
| | | {{ scope.row.attendanceRate }}% |
| | | </span> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </el-collapse-transition> |
| | | </el-card> |
| | | |
| | | <!-- 异常记录 --> |
| | | <el-card header="异常记录" class="abnormal-records-card" shadow="never"> |
| | | <el-table :data="abnormalRecords" border style="width: 100%"> |
| | | <el-table-column prop="date" label="日期" /> |
| | | <el-table-column prop="type" label="异常类型" > |
| | | <el-card class="abnormal-card" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span class="card-title">异常记录明细</span> |
| | | </div> |
| | | </template> |
| | | <el-table :data="abnormalRecords" border class="abnormal-table"> |
| | | <!-- 异常记录表格列定义保持不变 --> |
| | | <el-table-column prop="date" label="日期" align="center" /> |
| | | <el-table-column prop="type" label="异常类型" align="center"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getAbnormalType(scope.row.type)" size="small"> |
| | | <el-tag :type="getAbnormalType(scope.row.type)" size="small" effect="light"> |
| | | {{ scope.row.type }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="description" label="描述" min-width="200" /> |
| | | <el-table-column prop="duration" label="时长" /> |
| | | <el-table-column prop="status" label="状态" > |
| | | <el-table-column prop="description" label="描述" align="center" min-width="200" /> |
| | | <el-table-column prop="duration" label="时长" align="center" /> |
| | | <el-table-column prop="status" label="状态" align="center"> |
| | | <template #default="scope"> |
| | | <el-tag |
| | | :type="scope.row.status === '已处理' ? 'success' : 'warning'" |
| | | size="small" |
| | | effect="light" |
| | | > |
| | | {{ scope.row.status }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" > |
| | | <el-table-column label="操作" align="center" width="80"> |
| | | <template #default="scope"> |
| | | <el-button type="text" size="mini" @click="viewAbnormalDetail(scope.row)"> |
| | | 查看 |
| | | 详情 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | detailedStats: [], |
| | | abnormalRecords: [], |
| | | trendChart: null, |
| | | distributionChart: null |
| | | distributionChart: null, |
| | | tableExpanded: true |
| | | } |
| | | }, |
| | | mounted() { |
| | |
| | | }) |
| | | }, |
| | | beforeDestroy() { |
| | | // 销毁图表实例 |
| | | // 销毁图表实例[3](@ref) |
| | | if (this.trendChart) { |
| | | this.trendChart.dispose() |
| | | } |
| | |
| | | }, |
| | | methods: { |
| | | initData() { |
| | | // 初始化概览数据 |
| | | // 初始化数据逻辑保持不变 |
| | | this.overview = { |
| | | totalDays: this.stats.totalDays || 22, |
| | | presentDays: this.stats.presentDays || 20, |
| | |
| | | attendanceRate: this.stats.attendanceRate || 90.9 |
| | | } |
| | | |
| | | // 初始化详细统计 |
| | | this.detailedStats = [ |
| | | { month: '2024-12', workDays: 22, actualDays: 20, lateTimes: 2, |
| | | leaveEarlyTimes: 1, absenceDays: 0, businessTripDays: 3, attendanceRate: 90.9 }, |
| | |
| | | leaveEarlyTimes: 0, absenceDays: 0, businessTripDays: 1, attendanceRate: 95.7 } |
| | | ] |
| | | |
| | | // 初始化异常记录 |
| | | this.abnormalRecords = [ |
| | | { date: '2024-12-15', type: '迟到', description: '早上迟到30分钟', duration: '30分钟', status: '已处理' }, |
| | | { date: '2024-12-08', type: '早退', description: '下午提前1小时离开', duration: '1小时', status: '已处理' }, |
| | |
| | | const option = { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | backgroundColor: 'rgba(255, 255, 255, 0.95)', |
| | | borderColor: '#ebeef5', |
| | | borderWidth: 1, |
| | | textStyle: { |
| | | color: '#606266' |
| | | }, |
| | | formatter: function(params) { |
| | | let result = params[0].axisValue + '<br/>' |
| | | let result = `<div style="font-weight: 600; margin-bottom: 8px;">${params[0].axisValue}</div>` |
| | | params.forEach(param => { |
| | | result += `${param.seriesName}: ${param.value}<br/>` |
| | | const icon = param.seriesType === 'bar' ? '●' : '◆' |
| | | result += `<div>${icon} ${param.seriesName}: <span style="font-weight: 600; color: ${param.color}">${param.value}${param.seriesName === '出勤率' ? '%' : ''}</span></div>` |
| | | }) |
| | | return result |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['出勤天数', '异常天数', '出勤率'], |
| | | bottom: 10 |
| | | bottom: 10, |
| | | textStyle: { |
| | | color: '#606266' |
| | | }, |
| | | itemWidth: 12, |
| | | itemHeight: 12 |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | right: '3%', |
| | | bottom: '15%', |
| | | top: '10%', |
| | | top: '15%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: ['10月', '11月', '12月'] |
| | | data: ['10月', '11月', '12月'], |
| | | axisLine: { |
| | | lineStyle: { |
| | | color: '#dcdfe6' |
| | | } |
| | | }, |
| | | axisLabel: { |
| | | color: '#606266' |
| | | } |
| | | }, |
| | | yAxis: [ |
| | | { |
| | | type: 'value', |
| | | name: '天数', |
| | | min: 0, |
| | | max: 30 |
| | | max: 30, |
| | | axisLine: { |
| | | show: true, |
| | | lineStyle: { |
| | | color: '#dcdfe6' |
| | | } |
| | | }, |
| | | axisLabel: { |
| | | color: '#606266', |
| | | formatter: '{value}' |
| | | }, |
| | | splitLine: { |
| | | lineStyle: { |
| | | color: '#f0f2f5', |
| | | type: 'dashed' |
| | | } |
| | | } |
| | | }, |
| | | { |
| | | type: 'value', |
| | | name: '出勤率(%)', |
| | | min: 0, |
| | | max: 100, |
| | | axisLine: { |
| | | show: true, |
| | | lineStyle: { |
| | | color: '#dcdfe6' |
| | | } |
| | | }, |
| | | axisLabel: { |
| | | color: '#606266', |
| | | formatter: '{value}%' |
| | | }, |
| | | splitLine: { |
| | | show: false |
| | | } |
| | | } |
| | | ], |
| | |
| | | { |
| | | name: '出勤天数', |
| | | type: 'bar', |
| | | barWidth: '30%', |
| | | barWidth: '25%', |
| | | data: [22, 19, 20], |
| | | itemStyle: { |
| | | color: '#409EFF' |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: '#409EFF' }, |
| | | { offset: 1, color: '#66b1ff' } |
| | | ]), |
| | | borderRadius: [2, 2, 0, 0] |
| | | }, |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowColor: 'rgba(64, 158, 255, 0.5)' |
| | | } |
| | | } |
| | | }, |
| | | { |
| | | name: '异常天数', |
| | | type: 'bar', |
| | | barWidth: '30%', |
| | | barWidth: '25%', |
| | | data: [1, 2, 2], |
| | | itemStyle: { |
| | | color: '#F56C6C' |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: '#F56C6C' }, |
| | | { offset: 1, color: '#f78989' } |
| | | ]), |
| | | borderRadius: [2, 2, 0, 0] |
| | | }, |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowColor: 'rgba(245, 108, 108, 0.5)' |
| | | } |
| | | } |
| | | }, |
| | | { |
| | |
| | | type: 'line', |
| | | yAxisIndex: 1, |
| | | data: [95.7, 90.5, 90.9], |
| | | symbol: 'circle', |
| | | symbolSize: 8, |
| | | itemStyle: { |
| | | color: '#67C23A' |
| | | color: '#67C23A', |
| | | borderColor: '#fff', |
| | | borderWidth: 2 |
| | | }, |
| | | lineStyle: { |
| | | width: 3 |
| | | } |
| | | width: 3, |
| | | shadowColor: 'rgba(103, 194, 58, 0.3)', |
| | | shadowBlur: 8, |
| | | shadowOffsetY: 2 |
| | | }, |
| | | emphasis: { |
| | | scale: true |
| | | }, |
| | | animationEasing: 'cubicInOut', |
| | | animationDuration: 2000 |
| | | } |
| | | ] |
| | | ], |
| | | animation: true, |
| | | animationDuration: 1500 |
| | | } |
| | | this.trendChart.setOption(option) |
| | | }, |
| | |
| | | const option = { |
| | | tooltip: { |
| | | trigger: 'item', |
| | | formatter: '{a} <br/>{b}: {c} ({d}%)' |
| | | backgroundColor: 'rgba(255, 255, 255, 0.95)', |
| | | borderColor: '#ebeef5', |
| | | borderWidth: 1, |
| | | textStyle: { |
| | | color: '#606266' |
| | | }, |
| | | formatter: '{a} <br/>{b}: {c}天 ({d}%)' |
| | | }, |
| | | legend: { |
| | | orient: 'vertical', |
| | | right: 10, |
| | | top: 'center', |
| | | data: ['正常出勤', '迟到', '早退', '缺勤', '出差'] |
| | | textStyle: { |
| | | color: '#606266', |
| | | fontSize: 12 |
| | | }, |
| | | itemWidth: 12, |
| | | itemHeight: 12, |
| | | formatter: function(name) { |
| | | return `${name}` |
| | | } |
| | | }, |
| | | series: [ |
| | | { |
| | |
| | | type: 'pie', |
| | | radius: ['40%', '70%'], |
| | | center: ['40%', '50%'], |
| | | avoidLabelOverlap: false, |
| | | avoidLabelOverlap: true, |
| | | itemStyle: { |
| | | borderColor: '#fff', |
| | | borderWidth: 2 |
| | | borderWidth: 2, |
| | | borderRadius: 3, |
| | | shadowBlur: 5, |
| | | shadowColor: 'rgba(0, 0, 0, 0.1)' |
| | | }, |
| | | label: { |
| | | show: false, |
| | | position: 'center' |
| | | show: true, |
| | | formatter: '{b}\n{d}%', |
| | | textStyle: { |
| | | fontSize: 12, |
| | | fontWeight: 'normal' |
| | | } |
| | | }, |
| | | emphasis: { |
| | | label: { |
| | | show: true, |
| | | fontSize: 18, |
| | | fontSize: 14, |
| | | fontWeight: 'bold' |
| | | }, |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowOffsetX: 0, |
| | | shadowColor: 'rgba(0, 0, 0, 0.2)' |
| | | } |
| | | }, |
| | | labelLine: { |
| | | show: false |
| | | length: 15, |
| | | length2: 10, |
| | | smooth: true |
| | | }, |
| | | data: [ |
| | | { value: 20, name: '正常出勤', itemStyle: { color: '#67C23A' } }, |
| | |
| | | { value: 1, name: '早退', itemStyle: { color: '#F56C6C' } }, |
| | | { value: 0, name: '缺勤', itemStyle: { color: '#909399' } }, |
| | | { value: 3, name: '出差', itemStyle: { color: '#409EFF' } } |
| | | ] |
| | | ], |
| | | animationType: 'scale', |
| | | animationEasing: 'elasticOut', |
| | | animationDelay: function(idx) { |
| | | return Math.random() * 200 |
| | | } |
| | | } |
| | | ] |
| | | ], |
| | | animation: true, |
| | | animationDuration: 1000 |
| | | } |
| | | this.distributionChart.setOption(option) |
| | | }, |
| | | |
| | | setupChartResize() { |
| | | // 监听窗口变化,重新渲染图表 |
| | | const handleResize = () => { |
| | | // 使用节流函数优化性能[3](@ref) |
| | | const handleResize = this.throttle(() => { |
| | | if (this.trendChart) { |
| | | this.trendChart.resize() |
| | | } |
| | | if (this.distributionChart) { |
| | | this.distributionChart.resize() |
| | | } |
| | | } |
| | | }, 300) |
| | | |
| | | window.addEventListener('resize', handleResize) |
| | | this.$once('hook:beforeDestroy', () => { |
| | |
| | | }) |
| | | }, |
| | | |
| | | throttle(func, wait) { |
| | | let timeout = null |
| | | return function() { |
| | | const context = this |
| | | const args = arguments |
| | | if (!timeout) { |
| | | timeout = setTimeout(() => { |
| | | timeout = null |
| | | func.apply(context, args) |
| | | }, wait) |
| | | } |
| | | } |
| | | }, |
| | | |
| | | toggleTableExpand() { |
| | | this.tableExpanded = !this.tableExpanded |
| | | }, |
| | | |
| | | getRowClassName({ rowIndex }) { |
| | | return rowIndex % 2 === 1 ? 'even-row' : 'odd-row' |
| | | }, |
| | | |
| | | getRateTextClass(rate) { |
| | | if (rate >= 95) return 'rate-excellent' |
| | | if (rate >= 90) return 'rate-good' |
| | | if (rate >= 80) return 'rate-average' |
| | | return 'rate-poor' |
| | | }, |
| | | |
| | | // 其他方法保持不变 |
| | | handlePeriodChange(period) { |
| | | this.reportPeriod = period |
| | | this.updateChartData() |
| | | }, |
| | | |
| | | updateChartData() { |
| | | // 根据选择的周期更新图表数据 |
| | | let data |
| | | switch (this.reportPeriod) { |
| | | case 'month': |
| | |
| | | default: |
| | | data = this.getMonthlyData() |
| | | } |
| | | |
| | | this.updateCharts(data) |
| | | }, |
| | | |
| | | getMonthlyData() { |
| | | // 模拟月度数据 |
| | | return { |
| | | xAxis: ['10月', '11月', '12月'], |
| | | attendance: [22, 19, 20], |
| | |
| | | }, |
| | | |
| | | getQuarterlyData() { |
| | | // 模拟季度数据 |
| | | return { |
| | | xAxis: ['Q1', 'Q2', 'Q3', 'Q4'], |
| | | attendance: [65, 62, 58, 61], |
| | |
| | | }, |
| | | |
| | | getYearlyData() { |
| | | // 模拟年度数据 |
| | | return { |
| | | xAxis: ['2022', '2023', '2024'], |
| | | xAxis: ['2022', '2025', '2024'], |
| | | attendance: [240, 248, 252], |
| | | abnormal: [25, 18, 15], |
| | | rate: [90.2, 92.5, 94.1] |
| | |
| | | option.series[0].data = data.attendance |
| | | option.series[1].data = data.abnormal |
| | | option.series[2].data = data.rate |
| | | this.trendChart.setOption(option) |
| | | this.trendChart.setOption(option, { notMerge: false }) |
| | | } |
| | | }, |
| | | |
| | |
| | | |
| | | viewAbnormalDetail(record) { |
| | | this.$message.info(`查看异常记录: ${record.date} - ${record.type}`) |
| | | // 这里可以打开详情对话框 |
| | | }, |
| | | |
| | | exportReport() { |
| | | this.$message.success('报表导出功能开发中') |
| | | // 这里可以实现导出PDF或Excel功能 |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | <style scoped> |
| | | .personal-attendance-report { |
| | | padding: 20px; |
| | | background: #fff; |
| | | border-radius: 8px; |
| | | padding: 24px; |
| | | background: #f8f9fa; |
| | | min-height: 100vh; |
| | | } |
| | | |
| | | .report-header { |
| | |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 24px; |
| | | padding-bottom: 16px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | padding: 0; |
| | | } |
| | | |
| | | .report-header h4 { |
| | | margin: 0; |
| | | color: #303133; |
| | | font-size: 20px; |
| | | font-size: 24px; |
| | | font-weight: 600; |
| | | background: linear-gradient(135deg, #409EFF, #67C23A); |
| | | -webkit-background-clip: text; |
| | | -webkit-text-fill-color: transparent; |
| | | } |
| | | |
| | | .header-actions { |
| | |
| | | } |
| | | |
| | | .stats-overview { |
| | | margin-bottom: 24px; |
| | | margin-bottom: 32px; |
| | | } |
| | | |
| | | .stat-col { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .stat-card { |
| | | border-radius: 8px; |
| | | transition: all 0.3s ease; |
| | | border-radius: 12px; |
| | | border: none; |
| | | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| | | background: #fff; |
| | | position: relative; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .stat-card::before { |
| | | content: ''; |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | height: 3px; |
| | | background: linear-gradient(90deg, var(--gradient-start), var(--gradient-end)); |
| | | } |
| | | |
| | | .stat-card:hover { |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
| | | transform: translateY(-4px); |
| | | box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15); |
| | | } |
| | | |
| | | .stat-content { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 16px; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .stat-icon { |
| | | width: 60px; |
| | | height: 60px; |
| | | border-radius: 50%; |
| | | width: 64px; |
| | | height: 64px; |
| | | border-radius: 16px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-right: 16px; |
| | | font-size: 24px; |
| | | font-size: 28px; |
| | | color: white; |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
| | | } |
| | | |
| | | .attendance-icon { |
| | | background: linear-gradient(135deg, #409EFF, #79BBFF); |
| | | --gradient-start: #409EFF; |
| | | --gradient-end: #66b1ff; |
| | | background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end)); |
| | | } |
| | | |
| | | .present-icon { |
| | | background: linear-gradient(135deg, #67C23A, #95D475); |
| | | --gradient-start: #67C23A; |
| | | --gradient-end: #85ce61; |
| | | background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end)); |
| | | } |
| | | |
| | | .abnormal-icon { |
| | | background: linear-gradient(135deg, #E6A23C, #EEBD6D); |
| | | --gradient-start: #E6A23C; |
| | | --gradient-end: #ebb563; |
| | | background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end)); |
| | | } |
| | | |
| | | .rate-icon { |
| | | background: linear-gradient(135deg, #F56C6C, #F89898); |
| | | --gradient-start: #F56C6C; |
| | | --gradient-end: #f78989; |
| | | background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end)); |
| | | } |
| | | |
| | | .stat-info { |
| | |
| | | } |
| | | |
| | | .stat-value { |
| | | font-size: 28px; |
| | | font-weight: bold; |
| | | font-size: 32px; |
| | | font-weight: 700; |
| | | color: #303133; |
| | | margin-bottom: 4px; |
| | | line-height: 1; |
| | | } |
| | | |
| | | .stat-label { |
| | | color: #909399; |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .charts-section { |
| | | margin-bottom: 32px; |
| | | } |
| | | |
| | | .chart-col { |
| | | margin-bottom: 24px; |
| | | } |
| | | |
| | | .chart-container { |
| | | width: 100%; |
| | | height: 300px; |
| | | .chart-card { |
| | | border-radius: 12px; |
| | | border: none; |
| | | background: #fff; |
| | | box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); |
| | | } |
| | | |
| | | .detail-table-card, |
| | | .abnormal-records-card { |
| | | margin-bottom: 20px; |
| | | .chart-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 0; |
| | | } |
| | | |
| | | .chart-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .chart-legend { |
| | | display: flex; |
| | | gap: 16px; |
| | | align-items: center; |
| | | } |
| | | |
| | | .legend-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | font-size: 12px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .legend-color { |
| | | width: 12px; |
| | | height: 12px; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .bar-color { |
| | | background: linear-gradient(135deg, #409EFF, #66b1ff); |
| | | } |
| | | |
| | | .line-color { |
| | | background: linear-gradient(135deg, #67C23A, #85ce61); |
| | | } |
| | | |
| | | .chart-container { |
| | | width: 40vw; |
| | | height: 320px; |
| | | position: relative; |
| | | } |
| | | |
| | | .detail-card, |
| | | .abnormal-card { |
| | | border-radius: 12px; |
| | | border: none; |
| | | background: #fff; |
| | | box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); |
| | | margin-bottom: 24px; |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 0; |
| | | } |
| | | |
| | | .card-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .stats-table { |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .stats-table :deep(.el-table__row) { |
| | | transition: background-color 0.3s; |
| | | } |
| | | |
| | | .stats-table :deep(.el-table__row:hover) { |
| | | background-color: #f5f7fa; |
| | | } |
| | | |
| | | .stats-table :deep(.even-row) { |
| | | background-color: #fafbfc; |
| | | } |
| | | |
| | | .progress-cell { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .rate-progress { |
| | | flex: 1; |
| | | } |
| | | |
| | | .rate-text { |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | min-width: 40px; |
| | | } |
| | | |
| | | .rate-excellent { color: #67C23A; } |
| | | .rate-good { color: #E6A23C; } |
| | | .rate-average { color: #F56C6C; } |
| | | .rate-poor { color: #909399; } |
| | | |
| | | .abnormal-table { |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | /* 响应式设计 */ |
| | | @media (max-width: 1200px) { |
| | | .stats-overview .el-col { |
| | | margin-bottom: 16px; |
| | | .chart-container { |
| | | height: 280px; |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 768px) { |
| | | .personal-attendance-report { |
| | | padding: 12px; |
| | | padding: 16px; |
| | | } |
| | | |
| | | .report-header { |
| | | flex-direction: column; |
| | | align-items: flex-start; |
| | | gap: 12px; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .header-actions { |
| | |
| | | } |
| | | |
| | | .stat-content { |
| | | padding: 12px; |
| | | padding: 16px; |
| | | flex-direction: column; |
| | | text-align: center; |
| | | } |
| | | |
| | | .stat-icon { |
| | | width: 50px; |
| | | height: 50px; |
| | | font-size: 20px; |
| | | margin-right: 12px; |
| | | margin-right: 0; |
| | | margin-bottom: 12px; |
| | | width: 56px; |
| | | height: 56px; |
| | | font-size: 24px; |
| | | } |
| | | |
| | | .stat-value { |
| | | font-size: 28px; |
| | | } |
| | | |
| | | .chart-container { |
| | | height: 240px; |
| | | } |
| | | |
| | | .chart-header { |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | align-items: flex-start; |
| | | } |
| | | |
| | | .chart-legend { |
| | | align-self: stretch; |
| | | justify-content: space-around; |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 480px) { |
| | | .stat-col { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .stat-value { |
| | |
| | | } |
| | | |
| | | .chart-container { |
| | | height: 250px; |
| | | height: 200px; |
| | | } |
| | | } |
| | | |
| | |
| | | opacity: 0; |
| | | } |
| | | |
| | | /* 表格样式优化 */ |
| | | .el-table { |
| | | border-radius: 4px; |
| | | overflow: hidden; |
| | | /* 滚动条样式优化 */ |
| | | :deep(::-webkit-scrollbar) { |
| | | width: 6px; |
| | | height: 6px; |
| | | } |
| | | |
| | | .el-table::before { |
| | | display: none; |
| | | :deep(::-webkit-scrollbar-track) { |
| | | background: #f1f1f1; |
| | | border-radius: 3px; |
| | | } |
| | | |
| | | /* 卡片标题样式 */ |
| | | .el-card__header { |
| | | background: #f8f9fa; |
| | | border-bottom: 1px solid #ebeef5; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | :deep(::-webkit-scrollbar-thumb) { |
| | | background: #c1c1c1; |
| | | border-radius: 3px; |
| | | } |
| | | |
| | | :deep(::-webkit-scrollbar-thumb:hover) { |
| | | background: #a8a8a8; |
| | | } |
| | | </style> |