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