ZN
2026-03-20 a5f5e2c3fa5953a5474e4ad5d504c23acab94900
src/views/personnelManagement/analytics/index.vue
@@ -1,5 +1,5 @@
<template>
  <div class="app-container analytics-container">
  <div class="app-container analytics-container" v-loading="loading">
    <!-- 关键指标卡片 -->
    <el-row :gutter="20" class="metrics-cards">
@@ -17,12 +17,12 @@
                <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 class="card-trend" :class="item.trend > 0 ? 'positive' : 'negative'" v-if="item.showTrend !== false">-->
<!--                <el-icon>-->
<!--                  <component :is="item.trend > 0 ? 'ArrowUp' : 'ArrowDown'" />-->
<!--                </el-icon>-->
<!--                {{ Math.abs(item.trend) }}%-->
<!--              </div>-->
            </div>
          </div>
        </el-card>
@@ -64,21 +64,6 @@
    <!-- 第二行图表 -->
    <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">
@@ -98,18 +83,18 @@
</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 dayjs from 'dayjs'
import {listDept} from "@/api/system/dept.js";
import {
  findStaffAnalysisMonthlyTurnoverRateFor12Months,
  findStaffLeaveReasonAnalysis,
  findStaffAnalysisTotalStatistic
} from "@/api/personnelManagement/staffAnalytics.js";
import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js";
import { findStaffLeaveListPage } from "@/api/personnelManagement/staffLeave.js";
// 响应式数据
const loading = ref(false)
@@ -150,25 +135,205 @@
    trend: 0
  },
  {
    label: '编制达成率',
    value: 0,
    unit: '%',
    icon: 'DataAnalysis',
    type: 'success',
    trend: 0
  },
  {
    label: '在职员工数',
    value: 0,
    unit: '人',
    icon: 'PieChart',
    type: 'warning',
    trend: 0
    trend: 0,
    showTrend: false
  }
])
// 部门数据
const departmentData = ref([])
// 员工流失原因分析数据
const staffLeaveReasons = ref([])
// 12个月员工流动流失率分析数据
const turnoverRateStatistics = ref([])
const turnoverRateStatisticsRaw = ref([])
const turnoverSeriesNormalized = ref(false)
const staffCounts = ref({
  onJobTotal: 0,
  leaveTotal: 0,
  totalStaff: 0,
  leaveLast12Months: 0,
  joinLeaveRatio: 1
})
const safeNum = (val) => {
  const num = Number(val)
  return Number.isFinite(num) ? num : 0
}
const round2 = (val) => {
  const num = safeNum(val)
  return Number(num.toFixed(2))
}
const getListTotal = (res) => safeNum(res?.data?.total ?? res?.data?.count ?? 0)
const getLast12MonthRanges = () => {
  const end = dayjs()
  const start = end.subtract(11, 'month').startOf('month')
  const ranges = []
  for (let i = 0; i < 12; i++) {
    const cur = start.add(i, 'month')
    ranges.push({
      month: cur.format('YYYY-MM'),
      start: cur.startOf('month').format('YYYY-MM-DD'),
      end: cur.endOf('month').format('YYYY-MM-DD')
    })
  }
  return ranges
}
// 获取部门数据
const getDepartmentData = async () => {
  try {
    const res = await listDept()
    if (res && res.data) {
      departmentData.value = res.data
    }
  } catch (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) {
      turnoverRateStatisticsRaw.value = res.data || []
    }
  } catch (error) {
    console.error('获取12个月员工流动流失率分析数据失败:', error)
  }
}
const getStaffAnalysisTotalStatistic = async () => {
  try {
    const ranges = getLast12MonthRanges()
    const leave12Range = {
      leaveDateStart: ranges[0].start,
      leaveDateEnd: ranges[ranges.length - 1].end
    }
    const [totalRes, onJobRes, leaveRes, leave12Res] = await Promise.all([
      findStaffAnalysisTotalStatistic(),
      staffOnJobListPage({ current: 1, size: 1, staffState: 1 }),
      findStaffLeaveListPage({ current: 1, size: 1 }),
      findStaffLeaveListPage({ current: 1, size: 1, ...leave12Range })
    ])
    const totalFlowRate = safeNum(totalRes?.data?.totalFlowRate)
    const totalTurnoverRate = safeNum(totalRes?.data?.totalTurnoverRate)
    const joinLeaveRatio =
      totalTurnoverRate > 0 && totalFlowRate > 0 ? totalFlowRate / totalTurnoverRate : 1
    const onJobTotal = getListTotal(onJobRes)
    const leaveTotal = getListTotal(leaveRes)
    const totalStaff = onJobTotal + leaveTotal
    const leaveLast12Months = getListTotal(leave12Res)
    staffCounts.value = {
      onJobTotal,
      leaveTotal,
      totalStaff,
      leaveLast12Months,
      joinLeaveRatio: safeNum(joinLeaveRatio)
    }
    const turnoverRateNew =
      totalStaff > 0 ? (leaveLast12Months / totalStaff) * 100 : 0
    const flowRateNew =
      totalStaff > 0 ? (leaveLast12Months * staffCounts.value.joinLeaveRatio / totalStaff) * 100 : 0
    keyMetrics.value[0].value = round2(flowRateNew)
    keyMetrics.value[1].value = round2(turnoverRateNew)
    keyMetrics.value[2].value = onJobTotal || safeNum(totalRes?.data?.currentOnJobCount)
  } catch (error) {
    console.error('获取员工分析总统计数据失败:', error)
  }
}
const getLeaveCountsByMonthForLast12Months = async () => {
  const ranges = getLast12MonthRanges()
  const tasks = ranges.map((r) =>
    findStaffLeaveListPage({
      current: 1,
      size: 1,
      leaveDateStart: r.start,
      leaveDateEnd: r.end
    })
  )
  const results = await Promise.allSettled(tasks)
  const counts = {}
  results.forEach((res, idx) => {
    const month = ranges[idx].month
    counts[month] = res.status === 'fulfilled' ? getListTotal(res.value) : 0
  })
  const sum = Object.values(counts).reduce((acc, cur) => acc + safeNum(cur), 0)
  return { counts, sum }
}
const applyNormalizedTurnoverStatistics = async () => {
  const raw = Array.isArray(turnoverRateStatisticsRaw.value) ? turnoverRateStatisticsRaw.value : []
  if (raw.length === 0) {
    turnoverRateStatistics.value = []
    turnoverSeriesNormalized.value = false
    return
  }
  const totalStaff = safeNum(staffCounts.value.totalStaff)
  const leaveLast12Months = safeNum(staffCounts.value.leaveLast12Months)
  const ratio = safeNum(staffCounts.value.joinLeaveRatio) || 1
  if (totalStaff <= 0 || leaveLast12Months < 0) {
    turnoverRateStatistics.value = raw
    turnoverSeriesNormalized.value = false
    return
  }
  try {
    const { counts, sum } = await getLeaveCountsByMonthForLast12Months()
    if (sum > leaveLast12Months) {
      turnoverRateStatistics.value = raw
      turnoverSeriesNormalized.value = false
      return
    }
    turnoverRateStatistics.value = raw.map((item) => {
      const monthKey = String(item?.month ?? '')
      const leaveCount = safeNum(counts[monthKey])
      const turnoverRate = totalStaff > 0 ? (leaveCount / totalStaff) * 100 : 0
      const flowRate = totalStaff > 0 ? (leaveCount * ratio / totalStaff) * 100 : 0
      return {
        ...item,
        flowRate: round2(flowRate),
        turnoverRate: round2(turnoverRate)
      }
    })
    turnoverSeriesNormalized.value = true
  } catch (e) {
    turnoverRateStatistics.value = raw
    turnoverSeriesNormalized.value = false
  }
}
// 启动自动刷新
const startAutoRefresh = () => {
@@ -199,52 +364,30 @@
  }
}
// 生成模拟数据
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 = 20
  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()
    loading.value = true
    // 等待所有数据加载完成
    await Promise.all([
      getDepartmentData(),
      getStaffLeaveReasonAnalysis(),
      getMonthlyTurnoverRateFor12Months(),
      getStaffAnalysisTotalStatistic()
    ])
    await applyNormalizedTurnoverStatistics()
    await nextTick()
    renderAllCharts()
    if (!autoRefreshEnabled.value) {
      ElMessage.success('数据刷新成功')
    }
  } catch (error) {
    console.error('刷新数据失败:', error)
    ElMessage.error('刷新数据失败')
    console.error('数据刷新失败:', error)
    ElMessage.error('数据刷新失败')
  } finally {
    loading.value = false
  }
@@ -265,8 +408,9 @@
    if (attritionChartRef.value) {
      attritionChart = echarts.init(attritionChartRef.value)
    }
    renderAllCharts()
    // 初始化时也先加载数据再渲染图表
    refreshData()
  }, 300)
}
@@ -278,14 +422,15 @@
  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: '员工流动率趋势',
@@ -312,10 +457,17 @@
      data: months,
      boundaryGap: false
    },
    yAxis: {
      type: 'value',
      axisLabel: { formatter: '{value}%' }
    },
    yAxis: turnoverSeriesNormalized.value
      ? {
          type: 'value',
          axisLabel: { formatter: '{value}%' },
          min: 0,
          max: 100
        }
      : {
          type: 'value',
          axisLabel: { formatter: '{value}%' }
        },
    series: [
      {
        name: '流动率',
@@ -335,19 +487,19 @@
      }
    ]
  }
  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: '部门人员分布',
@@ -380,17 +532,17 @@
      }
    ]
  }
  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: '编制达成率',
@@ -434,17 +586,17 @@
      }
    ]
  }
  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: '员工流失原因分析',
@@ -480,13 +632,12 @@
      }
    ]
  }
  attritionChart.setOption(option)
}
// 生命周期
onMounted(() => {
  generateMockData()
  initCharts()
  startAutoRefresh()
})
@@ -651,32 +802,32 @@
  .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;
  }
@@ -686,11 +837,11 @@
  .page-header h2 {
    font-size: 20px;
  }
  .card-number {
    font-size: 24px;
  }
  .chart-container {
    height: 250px;
  }