spring
3 天以前 6c30136297a33de868d734ff4c8b1fc218748275
人员统计分析
已添加1个文件
698 ■■■■■ 文件已修改
src/views/personnelManagement/analytics/index.vue 698 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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 ? '异常' : '正常'
  }))
}
// åˆ·æ–°æ•°æ®
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>