From 6c30136297a33de868d734ff4c8b1fc218748275 Mon Sep 17 00:00:00 2001 From: spring <2396852758@qq.com> Date: 星期四, 21 八月 2025 13:37:59 +0800 Subject: [PATCH] 人员统计分析 --- src/views/personnelManagement/analytics/index.vue | 698 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 698 insertions(+), 0 deletions(-) diff --git a/src/views/personnelManagement/analytics/index.vue b/src/views/personnelManagement/analytics/index.vue new file mode 100644 index 0000000..06b868b --- /dev/null +++ b/src/views/personnelManagement/analytics/index.vue @@ -0,0 +1,698 @@ +<template> + <div class="app-container analytics-container"> + + <!-- 鍏抽敭鎸囨爣鍗$墖 --> + <el-row :gutter="20" class="metrics-cards"> + <el-col :span="6" v-for="(item, index) in keyMetrics" :key="index"> + <el-card class="metric-card" :class="item.type"> + <div class="card-content"> + <div class="card-icon"> + <el-icon :size="32"> + <component :is="item.icon" /> + </el-icon> + </div> + <div class="card-info"> + <div class="card-number"> + <el-skeleton-item v-if="loading" variant="text" style="width: 60px; height: 32px;" /> + <span v-else>{{ item.value }}{{ item.unit }}</span> + </div> + <div class="card-label">{{ item.label }}</div> + <div class="card-trend" :class="item.trend > 0 ? 'positive' : 'negative'"> + <el-icon> + <component :is="item.trend > 0 ? 'ArrowUp' : 'ArrowDown'" /> + </el-icon> + {{ Math.abs(item.trend) }}% + </div> + </div> + </div> + </el-card> + </el-col> + </el-row> + + <!-- 鍥捐〃鍖哄煙 --> + <el-row :gutter="20" class="charts-section"> + <!-- 鍛樺伐娴佸姩鐜囪秼鍔垮浘 --> + <el-col :span="12"> + <el-card class="chart-card"> + <template #header> + <div class="card-header"> + <span>鍛樺伐娴佸姩鐜囪秼鍔�</span> + <el-tag type="info">杩�12涓湀</el-tag> + </div> + </template> + <div class="chart-container"> + <div ref="turnoverChartRef" class="chart"></div> + </div> + </el-card> + </el-col> + + <!-- 閮ㄩ棬浜哄憳鍒嗗竷 --> + <el-col :span="12"> + <el-card class="chart-card"> + <template #header> + <div class="card-header"> + <span>閮ㄩ棬浜哄憳鍒嗗竷</span> + <el-tag type="success">褰撳墠鐘舵��</el-tag> + </div> + </template> + <div class="chart-container"> + <div ref="departmentChartRef" class="chart"></div> + </div> + </el-card> + </el-col> + </el-row> + + <!-- 绗簩琛屽浘琛� --> + <el-row :gutter="20" class="charts-section"> + <!-- 缂栧埗杈炬垚鐜� --> + <el-col :span="12"> + <el-card class="chart-card"> + <template #header> + <div class="card-header"> + <span>缂栧埗杈炬垚鐜�</span> + <el-tag type="warning">鍚勯儴闂ㄥ姣�</el-tag> + </div> + </template> + <div class="chart-container"> + <div ref="staffingChartRef" class="chart"></div> + </div> + </el-card> + </el-col> + + <!-- 鍛樺伐娴佸け鍘熷洜鍒嗘瀽 --> + <el-col :span="12"> + <el-card class="chart-card"> + <template #header> + <div class="card-header"> + <span>鍛樺伐娴佸け鍘熷洜鍒嗘瀽</span> + <el-tag type="danger">骞村害缁熻</el-tag> + </div> + </template> + <div class="chart-container"> + <div ref="attritionChartRef" class="chart"></div> + </div> + </el-card> + </el-col> + </el-row> + </div> +</template> + +<script setup> +import { ref, reactive, onMounted, onUnmounted } from 'vue' +import { ElMessage } from 'element-plus' +import { + Refresh, + User, + TrendCharts, + DataAnalysis, + PieChart, + ArrowUp, + ArrowDown +} from '@element-plus/icons-vue' +import * as echarts from 'echarts' + +// 鍝嶅簲寮忔暟鎹� +const loading = ref(false) +const autoRefreshEnabled = ref(true) +const autoRefreshInterval = ref(null) + +// 鍥捐〃寮曠敤 +const turnoverChartRef = ref(null) +const departmentChartRef = ref(null) +const staffingChartRef = ref(null) +const attritionChartRef = ref(null) + +// 鍥捐〃瀹炰緥 +let turnoverChart = null +let departmentChart = null +let staffingChart = null +let attritionChart = null + +// 鑷姩鏇存柊闂撮殧锛�10鍒嗛挓锛� +const AUTO_REFRESH_INTERVAL = 10 * 60 * 1000 + +// 鍏抽敭鎸囨爣鏁版嵁 +const keyMetrics = ref([ + { + label: '鍛樺伐娴佸姩鐜�', + value: 0, + unit: '%', + icon: 'TrendCharts', + type: 'primary', + trend: 0 + }, + { + label: '鍛樺伐娴佸け鐜�', + value: 0, + unit: '%', + icon: 'User', + type: 'danger', + trend: 0 + }, + { + label: '缂栧埗杈炬垚鐜�', + value: 0, + unit: '%', + icon: 'DataAnalysis', + type: 'success', + trend: 0 + }, + { + label: '鍦ㄨ亴鍛樺伐鏁�', + value: 0, + unit: '浜�', + icon: 'PieChart', + type: 'warning', + trend: 0 + } +]) + +// 閮ㄩ棬鏁版嵁 +const departmentData = ref([]) + +// 鍚姩鑷姩鍒锋柊 +const startAutoRefresh = () => { + if (autoRefreshInterval.value) { + clearInterval(autoRefreshInterval.value) + } + if (autoRefreshEnabled.value) { + autoRefreshInterval.value = setInterval(() => { + refreshData() + }, AUTO_REFRESH_INTERVAL) + } +} + +// 鍋滄鑷姩鍒锋柊 +const stopAutoRefresh = () => { + if (autoRefreshInterval.value) { + clearInterval(autoRefreshInterval.value) + autoRefreshInterval.value = null + } +} + +// 鍒囨崲鑷姩鍒锋柊鐘舵�� +const toggleAutoRefresh = (value) => { + if (value) { + startAutoRefresh() + } else { + stopAutoRefresh() + } +} + +// 鐢熸垚妯℃嫙鏁版嵁 +const generateMockData = () => { + // 鐢熸垚鍏抽敭鎸囨爣鏁版嵁 + keyMetrics.value[0].value = (Math.random() * 5 + 2).toFixed(1) + keyMetrics.value[0].trend = (Math.random() * 3 - 1.5).toFixed(1) + + keyMetrics.value[1].value = (Math.random() * 3 + 1).toFixed(1) + keyMetrics.value[1].trend = (Math.random() * 2 - 1).toFixed(1) + + keyMetrics.value[2].value = (Math.random() * 15 + 85).toFixed(1) + keyMetrics.value[2].trend = (Math.random() * 3 - 1.5).toFixed(1) + + keyMetrics.value[3].value = Math.floor(Math.random() * 50 + 200) + keyMetrics.value[3].trend = (Math.random() * 2 - 1).toFixed(1) + + // 鐢熸垚閮ㄩ棬鏁版嵁 + const departments = ['鎶�鏈儴', '閿�鍞儴', '浜轰簨閮�', '璐㈠姟閮�', '鐢熶骇閮�', '甯傚満閮�'] + departmentData.value = departments.map(dept => ({ + department: dept, + currentStaff: Math.floor(Math.random() * 30 + 20), + plannedStaff: Math.floor(Math.random() * 10 + 35), + staffingRate: Math.floor(Math.random() * 20 + 80), + turnoverRate: (Math.random() * 4 + 1).toFixed(1), + attritionRate: (Math.random() * 2 + 0.5).toFixed(1), + newHires: Math.floor(Math.random() * 5 + 1), + resignations: Math.floor(Math.random() * 3 + 1), + status: Math.random() > 0.7 ? '寮傚父' : '姝e父' + })) +} + +// 鍒锋柊鏁版嵁 +const refreshData = async () => { + loading.value = true + try { + // 妯℃嫙API璋冪敤寤惰繜 + await new Promise(resolve => setTimeout(resolve, 500)) + + generateMockData() + renderAllCharts() + + if (!autoRefreshEnabled.value) { + ElMessage.success('鏁版嵁鍒锋柊鎴愬姛') + } + } catch (error) { + console.error('鍒锋柊鏁版嵁澶辫触:', error) + ElMessage.error('鍒锋柊鏁版嵁澶辫触') + } finally { + loading.value = false + } +} + +// 鍒濆鍖栧浘琛� +const initCharts = () => { + setTimeout(() => { + if (turnoverChartRef.value) { + turnoverChart = echarts.init(turnoverChartRef.value) + } + if (departmentChartRef.value) { + departmentChart = echarts.init(departmentChartRef.value) + } + if (staffingChartRef.value) { + staffingChart = echarts.init(staffingChartRef.value) + } + if (attritionChartRef.value) { + attritionChart = echarts.init(attritionChartRef.value) + } + + renderAllCharts() + }, 300) +} + +// 娓叉煋鎵�鏈夊浘琛� +const renderAllCharts = () => { + renderTurnoverChart() + renderDepartmentChart() + renderStaffingChart() + renderAttritionChart() +} + +// 娓叉煋鍛樺伐娴佸姩鐜囪秼鍔垮浘 +const renderTurnoverChart = () => { + if (!turnoverChart) return + + const months = ['1鏈�', '2鏈�', '3鏈�', '4鏈�', '5鏈�', '6鏈�', '7鏈�', '8鏈�', '9鏈�', '10鏈�', '11鏈�', '12鏈�'] + const turnoverData = months.map(() => (Math.random() * 5 + 2).toFixed(1)) + const attritionData = months.map(() => (Math.random() * 3 + 1).toFixed(1)) + + const option = { + title: { + text: '鍛樺伐娴佸姩鐜囪秼鍔�', + left: 'center', + textStyle: { fontSize: 16, fontWeight: 'normal' } + }, + tooltip: { + trigger: 'axis', + axisPointer: { type: 'cross' } + }, + legend: { + data: ['娴佸姩鐜�', '娴佸け鐜�'], + bottom: 10 + }, + grid: { + left: '3%', + right: '4%', + bottom: '15%', + top: '15%', + containLabel: true + }, + xAxis: { + type: 'category', + data: months, + boundaryGap: false + }, + yAxis: { + type: 'value', + axisLabel: { formatter: '{value}%' } + }, + series: [ + { + name: '娴佸姩鐜�', + type: 'line', + data: turnoverData, + smooth: true, + lineStyle: { color: '#409EFF' }, + itemStyle: { color: '#409EFF' } + }, + { + name: '娴佸け鐜�', + type: 'line', + data: attritionData, + smooth: true, + lineStyle: { color: '#F56C6C' }, + itemStyle: { color: '#F56C6C' } + } + ] + } + + turnoverChart.setOption(option) +} + +// 娓叉煋閮ㄩ棬浜哄憳鍒嗗竷鍥� +const renderDepartmentChart = () => { + if (!departmentChart) return + + const data = departmentData.value.map(item => ({ + name: item.department, + value: item.currentStaff + })) + + const option = { + title: { + text: '閮ㄩ棬浜哄憳鍒嗗竷', + left: 'center', + textStyle: { fontSize: 16, fontWeight: 'normal' } + }, + tooltip: { + trigger: 'item', + formatter: '{a} <br/>{b}: {c}浜� ({d}%)' + }, + legend: { + orient: 'vertical', + left: 'left', + top: 'middle' + }, + series: [ + { + name: '浜哄憳鏁伴噺', + type: 'pie', + radius: ['40%', '70%'], + center: ['60%', '50%'], + data: data, + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)' + } + } + } + ] + } + + departmentChart.setOption(option) +} + +// 娓叉煋缂栧埗杈炬垚鐜囧浘 +const renderStaffingChart = () => { + if (!staffingChart) return + + const departments = departmentData.value.map(item => item.department) + const rates = departmentData.value.map(item => item.staffingRate) + + const option = { + title: { + text: '缂栧埗杈炬垚鐜�', + left: 'center', + textStyle: { fontSize: 16, fontWeight: 'normal' } + }, + tooltip: { + trigger: 'axis', + axisPointer: { type: 'shadow' } + }, + grid: { + left: '3%', + right: '4%', + bottom: '15%', + top: '15%', + containLabel: true + }, + xAxis: { + type: 'category', + data: departments, + axisLabel: { rotate: 45 } + }, + yAxis: { + type: 'value', + axisLabel: { formatter: '{value}%' }, + max: 100 + }, + series: [ + { + name: '杈炬垚鐜�', + type: 'bar', + data: rates, + itemStyle: { + color: function(params) { + const value = params.value + if (value >= 90) return '#67C23A' + if (value >= 80) return '#E6A23C' + return '#F56C6C' + } + } + } + ] + } + + staffingChart.setOption(option) +} + +// 娓叉煋鍛樺伐娴佸け鍘熷洜鍒嗘瀽鍥� +const renderAttritionChart = () => { + if (!attritionChart) return + + const reasons = ['钖祫寰呴亣', '鑱屼笟鍙戝睍', '宸ヤ綔鐜', '涓汉鍘熷洜', '鍏朵粬'] + const data = reasons.map(() => Math.floor(Math.random() * 20 + 5)) + + const option = { + title: { + text: '鍛樺伐娴佸け鍘熷洜鍒嗘瀽', + left: 'center', + textStyle: { fontSize: 16, fontWeight: 'normal' } + }, + tooltip: { + trigger: 'item', + formatter: '{a} <br/>{b}: {c}浜� ({d}%)' + }, + legend: { + orient: 'vertical', + left: 'left', + top: 'middle' + }, + series: [ + { + name: '娴佸け浜烘暟', + type: 'pie', + radius: '50%', + center: ['60%', '50%'], + data: reasons.map((reason, index) => ({ + name: reason, + value: data[index] + })), + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)' + } + } + } + ] + } + + attritionChart.setOption(option) +} + +// 鐢熷懡鍛ㄦ湡 +onMounted(() => { + generateMockData() + initCharts() + startAutoRefresh() +}) + +onUnmounted(() => { + stopAutoRefresh() +}) +</script> + +<style scoped> +.analytics-container { + padding: 20px; + background-color: #f5f7fa; + min-height: 100vh; +} + +.page-header { + text-align: center; + margin-bottom: 30px; + padding: 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 12px; + color: white; +} + +.page-header h2 { + color: white; + margin-bottom: 10px; + font-size: 28px; + font-weight: 600; +} + +.page-header p { + color: rgba(255, 255, 255, 0.9); + font-size: 14px; + margin: 0 0 15px 0; +} + +.header-controls { + display: flex; + justify-content: center; + align-items: center; + gap: 20px; +} + +.refresh-btn { + margin-left: 20px; +} + +.metrics-cards { + margin-bottom: 30px; +} + +.metric-card { + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + border: none; + overflow: hidden; +} + +.metric-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); +} + +.metric-card.primary { + border-left: 4px solid #409EFF; + background: linear-gradient(135deg, #409EFF 0%, #36a3f7 100%); +} + +.metric-card.danger { + border-left: 4px solid #F56C6C; + background: linear-gradient(135deg, #F56C6C 0%, #f78989 100%); +} + +.metric-card.success { + border-left: 4px solid #67C23A; + background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%); +} + +.metric-card.warning { + border-left: 4px solid #E6A23C; + background: linear-gradient(135deg, #E6A23C 0%, #ebb563 100%); +} + +.card-content { + display: flex; + align-items: center; + padding: 20px; +} + +.card-icon { + margin-right: 20px; + color: white; +} + +.card-info { + flex: 1; +} + +.card-number { + font-size: 32px; + font-weight: 600; + color: white; + margin-bottom: 5px; +} + +.card-label { + font-size: 14px; + color: rgba(255, 255, 255, 0.9); + margin-bottom: 5px; +} + +.card-trend { + font-size: 12px; + display: flex; + align-items: center; + gap: 4px; +} + +.card-trend.positive { + color: #67C23A; +} + +.card-trend.negative { + color: #F56C6C; +} + +.charts-section { + margin-bottom: 30px; +} + +.chart-card { + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border: none; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 600; + color: #303133; + padding: 15px 20px; + border-bottom: 1px solid #ebeef5; +} + +.chart-container { + height: 350px; + padding: 20px; +} + +.chart { + width: 100%; + height: 100%; +} + +/* 鍝嶅簲寮忚璁� */ +@media (max-width: 768px) { + .analytics-container { + padding: 10px; + } + + .page-header { + padding: 15px; + } + + .page-header h2 { + font-size: 24px; + } + + .header-controls { + flex-direction: column; + gap: 15px; + } + + .refresh-btn { + margin-left: 0; + } + + .metrics-cards .el-col { + margin-bottom: 15px; + } + + .charts-section .el-col { + margin-bottom: 20px; + } + + .chart-container { + height: 300px; + } +} + +@media (max-width: 480px) { + .page-header h2 { + font-size: 20px; + } + + .card-number { + font-size: 24px; + } + + .chart-container { + height: 250px; + } +} +</style> -- Gitblit v1.9.3