From db42d47f5692ef64e5436c5a6d29dcb537b44596 Mon Sep 17 00:00:00 2001
From: zouyu <2723363702@qq.com>
Date: 星期一, 26 一月 2026 16:36:13 +0800
Subject: [PATCH] 浪潮对接单点登录:mis调整

---
 src/views/personnelManagement/analytics/index.vue |  702 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 702 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..9e6e449
--- /dev/null
+++ b/src/views/personnelManagement/analytics/index.vue
@@ -0,0 +1,702 @@
+<template>
+  <div class="app-container analytics-container" v-loading="loading">
+
+    <!-- 鍏抽敭鎸囨爣鍗$墖 -->
+    <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'" 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>
+      </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="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, onMounted, onUnmounted, nextTick } from 'vue'
+import { ElMessage } from 'element-plus'
+import * as echarts from 'echarts'
+import {listDept} from "@/api/system/dept.js";
+import {
+  findStaffAnalysisMonthlyTurnoverRateFor12Months,
+  findStaffLeaveReasonAnalysis,
+  findStaffAnalysisTotalStatistic
+} from "@/api/personnelManagement/staffAnalytics.js";
+
+// 鍝嶅簲寮忔暟鎹�
+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: 'PieChart',
+    type: 'warning',
+    trend: 0,
+    showTrend: false
+  }
+])
+
+// 閮ㄩ棬鏁版嵁
+const departmentData = ref([])
+// 鍛樺伐娴佸け鍘熷洜鍒嗘瀽鏁版嵁
+const staffLeaveReasons = ref([])
+// 12涓湀鍛樺伐娴佸姩娴佸け鐜囧垎鏋愭暟鎹�
+const turnoverRateStatistics = ref([])
+
+// 鑾峰彇閮ㄩ棬鏁版嵁
+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)
+  }
+}
+
+// 淇敼涓鸿繑鍥濸romise鐨勫紓姝ュ嚱鏁�
+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 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 refreshData = async () => {
+  try {
+    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('鏁版嵁鍒锋柊澶辫触')
+  } 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)
+    }
+
+    // 鍒濆鍖栨椂涔熷厛鍔犺浇鏁版嵁鍐嶆覆鏌撳浘琛�
+    refreshData()
+  }, 300)
+}
+
+// 娓叉煋鎵�鏈夊浘琛�
+const renderAllCharts = () => {
+  renderTurnoverChart()
+  renderDepartmentChart()
+  renderStaffingChart()
+  renderAttritionChart()
+}
+
+// 淇敼涓轰娇鐢ˋPI杩斿洖鐨勫疄闄呮暟鎹�
+const renderTurnoverChart = () => {
+  if (!turnoverChart) return
+
+  // 浣跨敤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: '鍛樺伐娴佸姩鐜囪秼鍔�',
+      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.deptName,
+    value: item.staffCount
+  }))
+
+  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.deptName)
+  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 = staffLeaveReasons.value.map(item => item.reasonText)
+  const data = staffLeaveReasons.value.map(item => item.count)
+
+  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(() => {
+  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