<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>
|