| | |
| | | <template> |
| | | <div class="app-container analytics-container"> |
| | | <div class="app-container analytics-container" v-loading="loading"> |
| | | |
| | | <!-- å
³é®ææ å¡ç --> |
| | | <el-row :gutter="20" class="metrics-cards"> |
| | |
| | | <component :is="item.icon" /> |
| | | </el-icon> |
| | | </div> |
| | | <div class="card-info"> |
| | | <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> |
| | |
| | | |
| | | <!-- 第äºè¡å¾è¡¨ --> |
| | | <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> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, onUnmounted } from 'vue' |
| | | import { ref, onMounted, onUnmounted, nextTick } 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' |
| | | import { staffOnJobListPage } from '@/api/personnelManagement/staffOnJob.js' |
| | | import {listDept} from "@/api/system/dept.js"; |
| | | import { |
| | | findStaffAnalysisMonthlyTurnoverRateFor12Months, |
| | | findStaffLeaveReasonAnalysis, |
| | | findStaffAnalysisTotalStatistic |
| | | } from "@/api/personnelManagement/staffAnalytics.js"; |
| | | |
| | | // ååºå¼æ°æ® |
| | | const loading = ref(false) |
| | |
| | | trend: 0 |
| | | }, |
| | | { |
| | | label: 'ç¼å¶è¾¾æç', |
| | | value: 0, |
| | | unit: '%', |
| | | icon: 'DataAnalysis', |
| | | type: 'success', |
| | | trend: 0 |
| | | }, |
| | | { |
| | | label: 'å¨èåå·¥æ°', |
| | | value: 0, |
| | | unit: '人', |
| | |
| | | |
| | | // é¨é¨æ°æ® |
| | | const departmentData = ref([]) |
| | | // åå·¥æµå¤±åå åææ°æ® |
| | | const staffLeaveReasons = ref([]) |
| | | // 12个æåå·¥æµå¨æµå¤±çåææ°æ® |
| | | const turnoverRateStatistics = ref([]) |
| | | |
| | | // è·åå¨èåå·¥æ° |
| | | const getStaffCount = async () => { |
| | | // è·åé¨é¨æ°æ® |
| | | const getDepartmentData = async () => { |
| | | try { |
| | | const res = await staffOnJobListPage({ staffState: 1, current: 1, size: 1 }) |
| | | const res = await listDept() |
| | | if (res && res.data) { |
| | | keyMetrics.value[3].value = res.data.total || 0 |
| | | departmentData.value = res.data |
| | | } |
| | | } catch (error) { |
| | | console.error('è·åå¨èåå·¥æ°å¤±è´¥:', error) |
| | | console.error('è·åé¨é¨æ°æ®å¤±è´¥:', error) |
| | | } |
| | | } |
| | | |
| | | const getStaffLeaveReasonAnalysis = async () => { |
| | | try { |
| | | const res = await findStaffLeaveReasonAnalysis() |
| | | if (res && res.data) { |
| | | staffLeaveReasons.value = res.data || [] |
| | | } |
| | | } catch (error) { |
| | | console.error('è·ååå·¥æµå¤±åå åæå¤±è´¥:', error) |
| | | } |
| | | } |
| | | |
| | | // ä¿®æ¹ä¸ºè¿åPromiseç弿¥å½æ° |
| | | const getMonthlyTurnoverRateFor12Months = async () => { |
| | | try { |
| | | const res = await findStaffAnalysisMonthlyTurnoverRateFor12Months() |
| | | if (res && res.data) { |
| | | turnoverRateStatistics.value = res.data || [] |
| | | } |
| | | } catch (error) { |
| | | console.error('è·å12个æåå·¥æµå¨æµå¤±çåææ°æ®å¤±è´¥:', error) |
| | | } |
| | | } |
| | | |
| | | const getStaffAnalysisTotalStatistic = async () => { |
| | | try { |
| | | const res = await findStaffAnalysisTotalStatistic() |
| | | if (res && res.data) { |
| | | keyMetrics.value[0].value = res.data.totalFlowRate || 0 |
| | | keyMetrics.value[1].value = res.data.totalTurnoverRate || 0 |
| | | keyMetrics.value[2].value = res.data.currentOnJobCount || 0 |
| | | } |
| | | } catch (error) { |
| | | console.error('è·åå工忿»ç»è®¡æ°æ®å¤±è´¥:', error) |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | // çææ¨¡ææ°æ® |
| | | 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) |
| | | |
| | | // çæé¨é¨æ°æ® |
| | | 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 ? 'å¼å¸¸' : 'æ£å¸¸' |
| | | })) |
| | | } |
| | | |
| | | // å·æ°æ°æ® |
| | | // ä¿®æ¹ä¸ºå¼æ¥å½æ°ï¼ç¡®ä¿æ°æ®å è½½å®æå忏²æå¾è¡¨ |
| | | const refreshData = async () => { |
| | | loading.value = true |
| | | try { |
| | | // 模æAPIè°ç¨å»¶è¿ |
| | | await new Promise(resolve => setTimeout(resolve, 500)) |
| | | |
| | | generateMockData() |
| | | loading.value = true |
| | | |
| | | // çå¾
æææ°æ®å è½½å®æ |
| | | await Promise.all([ |
| | | getDepartmentData(), |
| | | getStaffLeaveReasonAnalysis(), |
| | | getMonthlyTurnoverRateFor12Months(), |
| | | getStaffAnalysisTotalStatistic() |
| | | ]) |
| | | |
| | | await nextTick() |
| | | renderAllCharts() |
| | | |
| | | |
| | | if (!autoRefreshEnabled.value) { |
| | | ElMessage.success('æ°æ®å·æ°æå') |
| | | } |
| | | } catch (error) { |
| | | console.error('å·æ°æ°æ®å¤±è´¥:', error) |
| | | ElMessage.error('å·æ°æ°æ®å¤±è´¥') |
| | | console.error('æ°æ®å·æ°å¤±è´¥:', error) |
| | | ElMessage.error('æ°æ®å·æ°å¤±è´¥') |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | |
| | | if (attritionChartRef.value) { |
| | | attritionChart = echarts.init(attritionChartRef.value) |
| | | } |
| | | |
| | | renderAllCharts() |
| | | |
| | | // åå§åæ¶ä¹å
å è½½æ°æ®å渲æå¾è¡¨ |
| | | refreshData() |
| | | }, 300) |
| | | } |
| | | |
| | |
| | | renderAttritionChart() |
| | | } |
| | | |
| | | // 渲æåå·¥æµå¨çè¶å¿å¾ |
| | | // ä¿®æ¹ä¸ºä½¿ç¨APIè¿åçå®é
æ°æ® |
| | | 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)) |
| | | |
| | | |
| | | // 使ç¨APIè¿åçå®é
æ°æ® |
| | | const months = turnoverRateStatistics.value.map(item => item.month) |
| | | const turnoverData = turnoverRateStatistics.value.map(item => item.flowRate || 0) |
| | | const attritionData = turnoverRateStatistics.value.map(item => item.turnoverRate || 0) |
| | | |
| | | const option = { |
| | | title: { |
| | | text: 'åå·¥æµå¨çè¶å¿', |
| | |
| | | } |
| | | ] |
| | | } |
| | | |
| | | |
| | | turnoverChart.setOption(option) |
| | | } |
| | | |
| | | // 渲æé¨é¨äººååå¸å¾ |
| | | const renderDepartmentChart = () => { |
| | | if (!departmentChart) return |
| | | |
| | | |
| | | const data = departmentData.value.map(item => ({ |
| | | name: item.department, |
| | | value: item.currentStaff |
| | | name: item.deptName, |
| | | value: item.staffCount |
| | | })) |
| | | |
| | | |
| | | const option = { |
| | | title: { |
| | | text: 'é¨é¨äººååå¸', |
| | |
| | | } |
| | | ] |
| | | } |
| | | |
| | | |
| | | departmentChart.setOption(option) |
| | | } |
| | | |
| | | // 渲æç¼å¶è¾¾æçå¾ |
| | | const renderStaffingChart = () => { |
| | | if (!staffingChart) return |
| | | |
| | | const departments = departmentData.value.map(item => item.department) |
| | | |
| | | const departments = departmentData.value.map(item => item.deptName) |
| | | const rates = departmentData.value.map(item => item.staffingRate) |
| | | |
| | | |
| | | const option = { |
| | | title: { |
| | | text: 'ç¼å¶è¾¾æç', |
| | |
| | | } |
| | | ] |
| | | } |
| | | |
| | | |
| | | staffingChart.setOption(option) |
| | | } |
| | | |
| | | // 渲æåå·¥æµå¤±åå åæå¾ |
| | | const renderAttritionChart = () => { |
| | | if (!attritionChart) return |
| | | |
| | | const reasons = ['èªèµå¾
é', 'èä¸åå±', 'å·¥ä½ç¯å¢', '个人åå ', 'å
¶ä»'] |
| | | const data = reasons.map(() => Math.floor(Math.random() * 20 + 5)) |
| | | |
| | | |
| | | const reasons = staffLeaveReasons.value.map(item => item.reasonText) |
| | | const data = staffLeaveReasons.value.map(item => item.count) |
| | | |
| | | const option = { |
| | | title: { |
| | | text: 'åå·¥æµå¤±åå åæ', |
| | |
| | | } |
| | | ] |
| | | } |
| | | |
| | | |
| | | attritionChart.setOption(option) |
| | | } |
| | | |
| | | |
| | | // çå½å¨æ |
| | | onMounted(() => { |
| | | generateMockData() |
| | | getStaffCount() |
| | | initCharts() |
| | | startAutoRefresh() |
| | | }) |
| | |
| | | .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; |
| | | } |
| | |
| | | .page-header h2 { |
| | | font-size: 20px; |
| | | } |
| | | |
| | | |
| | | .card-number { |
| | | font-size: 24px; |
| | | } |
| | | |
| | | |
| | | .chart-container { |
| | | height: 250px; |
| | | } |