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