Files
klp-oa/klp-ui/src/views/wms/hrm/report/employee.vue
砂糖 ea73305ebb feat(员工信息): 添加时间范围筛选功能并新增员工报表页面
在员工信息各页面添加入职/转正/调岗/离职时间范围筛选功能
新增员工报表页面,包含统计卡片、图表和数据表格
2026-03-21 17:45:37 +08:00

730 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="employee-report" v-loading="loading">
<!-- 第一行时间段筛选 -->
<div class="filter-section">
<el-form :inline="true" class="demo-form-inline">
<el-form-item label="调动时间">
<el-date-picker
v-model="dateRange"
type="datetimerange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
format="yyyy-MM-dd hh:mm:ss"
value-format="yyyy-MM-dd hh:mm:ss"
/>
</el-form-item>
<el-form-item label="入职时间">
<el-date-picker
v-model="employeeDateRange"
type="datetimerange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
format="yyyy-MM-dd hh:mm:ss"
value-format="yyyy-MM-dd hh:mm:ss"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 第二行员工情况统计信息和图表 -->
<div class="stats-section">
<div class="stats-cards">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value">{{ stats.totalEmployees }}</div>
<div class="stat-label">总人数</div>
</div>
</el-card>
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value">{{ stats.entryCount }}</div>
<div class="stat-label">入职人数</div>
</div>
</el-card>
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value">{{ stats.leaveCount }}</div>
<div class="stat-label">离职人数</div>
</div>
</el-card>
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value">{{ stats.regularCount }}</div>
<div class="stat-label">转正人数</div>
</div>
</el-card>
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value">{{ stats.transferCount }}</div>
<div class="stat-label">调岗次数</div>
</div>
</el-card>
</div>
</div>
<!-- 第三行互动筛选图表区 -->
<div class="interactive-charts-section">
<el-card shadow="hover" class="chart-card">
<!-- <div slot="header" class="clearfix">
<span>学历分布</span>
</div> -->
<div id="educationChart" style="width: 100%; height: 300px;"></div>
</el-card>
<el-card shadow="hover" class="chart-card">
<!-- <div slot="header" class="clearfix">
<span>年龄段分布</span>
</div> -->
<div id="ageChart" style="width: 100%; height: 300px;"></div>
</el-card>
<el-card shadow="hover" class="chart-card">
<!-- <div slot="header" class="clearfix">
<span>性别分布</span>
</div> -->
<div id="genderChart" style="width: 100%; height: 300px;"></div>
</el-card>
</div>
<!-- 第四行多轴折线图 -->
<div class="trend-section">
<!-- <el-card shadow="hover"> -->
<!-- <div slot="header" class="clearfix">
<span>员工变动趋势</span>
</div> -->
<div id="trendChart" style="width: 100%; height: 400px;"></div>
<!-- </el-card> -->
</div>
<!-- 第五行明细数据表 -->
<div class="details-section">
<el-tabs v-model="activeTab">
<el-tab-pane label="当前员工" name="current">
<el-table :data="currentEmployees" style="width: 100%" height="400">
<el-table-column prop="name" label="员工姓名" />
<el-table-column prop="dept" label="部门" />
<el-table-column prop="jobType" label="职位" />
<el-table-column prop="entryTime" label="入职日期" />
<!-- <el-table-column prop="status" label="状态" /> -->
<el-table-column prop="education" label="学历" />
<el-table-column prop="age" label="年龄" />
<el-table-column prop="gender" label="性别" />
</el-table>
</el-tab-pane>
<el-tab-pane label="离职记录" name="leave">
<el-table :data="leaveRecords" style="width: 100%" height="400">
<el-table-column prop="wmsEmployeeInfo.name" label="员工姓名" />
<el-table-column prop="wmsEmployeeInfo.dept" label="部门" />
<el-table-column prop="wmsEmployeeInfo.jobType" label="职位" />
<el-table-column prop="changeTime" label="离职日期" />
<el-table-column prop="changeReason" label="离职原因" />
</el-table>
</el-tab-pane>
<el-tab-pane label="入职记录" name="entry">
<el-table :data="entryRecords" style="width: 100%" height="400">
<el-table-column prop="wmsEmployeeInfo.name" label="员工姓名" />
<el-table-column prop="wmsEmployeeInfo.dept" label="部门" />
<el-table-column prop="wmsEmployeeInfo.jobType" label="职位" />
<el-table-column prop="entryDate" label="入职日期" />
<el-table-column prop="wmsEmployeeInfo.education" label="学历" />
<el-table-column prop="wmsEmployeeInfo.gender" label="性别" />
</el-table>
</el-tab-pane>
<el-tab-pane label="转正记录" name="regular">
<el-table :data="regularRecords" style="width: 100%" height="400">
<el-table-column prop="wmsEmployeeInfo.name" label="员工姓名" />
<el-table-column prop="wmsEmployeeInfo.dept" label="部门" />
<el-table-column prop="wmsEmployeeInfo.jobType" label="职位" />
<el-table-column prop="regularDate" label="转正日期" />
</el-table>
</el-tab-pane>
<el-tab-pane label="调岗记录" name="transfer">
<el-table :data="transferRecords" style="width: 100%" height="400">
<el-table-column prop="wmsEmployeeInfo.name" label="员工姓名" />
<el-table-column prop="transferTime" label="调岗日期" />
<el-table-column prop="oldDept" label="原部门" />
<el-table-column prop="oldJobType" label="原职位" />
<el-table-column prop="newDept" label="新部门" />
<el-table-column prop="newJobType" label="新职位" />
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import { listEmployeeInfo } from '@/api/wms/employeeInfo'
import { listEmployeeChange } from '@/api/wms/employeeChange'
import { listEmployeeTransfer } from '@/api/wms/employeeTransfer'
export default {
name: 'EmployeeReport',
data() {
return {
dateRange: this.getMonthRange(), // 默认选中本月
employeeDateRange: ['', ''],
shortcuts: [
{
text: '本月',
value: () => {
return this.getMonthRange()
}
},
{
text: '上个月',
value: () => {
const date = new Date()
const firstDay = new Date(date.getFullYear(), date.getMonth() - 1, 1, 0, 0, 0)
const lastDay = new Date(date.getFullYear(), date.getMonth(), 0, 23, 59, 59)
// 格式化日期为yyyy-MM-dd HH:mm:ss格式
const formatDate = (date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
return [formatDate(firstDay), formatDate(lastDay)]
}
},
{
text: '近三个月',
value: () => {
const date = new Date()
const lastDay = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59)
const firstDay = new Date(date.getFullYear(), date.getMonth() - 2, 1, 0, 0, 0)
// 格式化日期为yyyy-MM-dd HH:mm:ss格式
const formatDate = (date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
return [formatDate(firstDay), formatDate(lastDay)]
}
},
{
text: '本年',
value: () => {
const date = new Date()
const firstDay = new Date(date.getFullYear(), 0, 1, 0, 0, 0)
const lastDay = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59)
// 格式化日期为yyyy-MM-dd HH:mm:ss格式
const formatDate = (date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
return [formatDate(firstDay), formatDate(lastDay)]
}
}
],
stats: {
totalEmployees: 0,
regularEmployees: 0,
probationEmployees: 0,
entryCount: 0,
leaveCount: 0,
regularCount: 0,
transferCount: 0
},
currentEmployees: [],
leaveRecords: [],
entryRecords: [],
regularRecords: [],
transferRecords: [],
activeTab: 'current',
charts: {},
loading: false,
}
},
mounted() {
// 默认设置为当前月
this.dateRange = this.getMonthRange()
this.initCharts()
this.handleQuery()
},
methods: {
getMonthRange() {
const date = new Date()
// 当月第一天00:00:00
const firstDay = new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0)
// 当月最后一天23:59:59
const lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0, 23, 59, 59)
// 格式化日期为yyyy-MM-dd HH:mm:ss格式
const formatDate = (date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
return [formatDate(firstDay), formatDate(lastDay)]
},
initCharts() {
this.charts.educationChart = echarts.init(document.getElementById('educationChart'))
this.charts.ageChart = echarts.init(document.getElementById('ageChart'))
this.charts.genderChart = echarts.init(document.getElementById('genderChart'))
this.charts.trendChart = echarts.init(document.getElementById('trendChart'))
// 监听窗口大小变化,自适应图表
window.addEventListener('resize', () => {
Object.values(this.charts).forEach(chart => chart.resize())
})
},
handleQuery() {
const queryParams = {
startTime: this.dateRange ? this.dateRange[0] : '',
endTime: this.dateRange ? this.dateRange[1] : '',
entryStartTime: this.employeeDateRange ? this.employeeDateRange[0] : '',
entryEndTime: this.employeeDateRange ? this.employeeDateRange[1] : '',
}
this.loadData(queryParams)
},
async loadData(queryParams) {
this.loading = true
try {
await Promise.all([
this.loadEmployeeData(queryParams),
this.loadChangeData(queryParams),
this.loadTransferData(queryParams)
])
} finally {
this.loading = false
}
},
resetQuery() {
this.dateRange = this.getMonthRange()
this.handleQuery()
},
loadEmployeeData(queryParams) {
console.log(queryParams)
return new Promise((resolve) => {
listEmployeeInfo(queryParams).then(response => {
const data = response.rows
this.currentEmployees = data
// 统计当前员工信息
this.stats.totalEmployees = data.length
this.stats.regularEmployees = data.filter(item => item.status === '已转正').length
this.stats.probationEmployees = data.filter(item => item.status === '试用期').length
// 生成学历分布图表
this.updateEducationChart()
// 生成年龄段分布图表
this.updateAgeChart()
// 生成性别分布图表
this.updateGenderChart()
resolve()
})
})
},
loadChangeData(queryParams) {
return new Promise((resolve) => {
listEmployeeChange({
changeStartTime: queryParams.startTime,
changeEndTime: queryParams.endTime
}).then(response => {
const data = response.rows
// 分类处理异动记录
this.entryRecords = data.filter(item => item.changeType == 0)
this.leaveRecords = data.filter(item => item.changeType == 1)
this.regularRecords = data.filter(item => item.changeType == 2)
// 统计异动数据
this.stats.entryCount = this.entryRecords.length
this.stats.leaveCount = this.leaveRecords.length
this.stats.regularCount = this.regularRecords.length
// 更新趋势图表
this.updateTrendChart()
resolve()
})
})
},
loadTransferData(queryParams) {
return new Promise((resolve) => {
listEmployeeTransfer({
transferStartTime: queryParams.startTime,
transferEndTime: queryParams.endTime
}).then(response => {
const data = response.rows
console.log(data, '调岗记录')
this.transferRecords = data
this.stats.transferCount = data.length
// 更新趋势图表
this.updateTrendChart()
resolve()
})
})
},
updateEducationChart() {
// 统计学历分布
const educationData = {}
this.currentEmployees.forEach(emp => {
const edu = emp.education || '未知'
educationData[edu] = (educationData[edu] || 0) + 1
})
const option = {
title: {
text: '学历分布',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '学历',
type: 'pie',
radius: '50%',
data: Object.entries(educationData).map(([name, value]) => ({ name, value })),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
}
this.charts.educationChart.setOption(option)
},
updateAgeChart() {
// 统计年龄段分布
const ageGroups = {
'20岁以下': 0,
'20-30岁': 0,
'30-40岁': 0,
'40-50岁': 0,
'50岁以上': 0
}
this.currentEmployees.forEach(emp => {
const age = parseInt(emp.age) || 0
if (age < 20) ageGroups['20岁以下']++
else if (age < 30) ageGroups['20-30岁']++
else if (age < 40) ageGroups['30-40岁']++
else if (age < 50) ageGroups['40-50岁']++
else ageGroups['50岁以上']++
})
const option = {
title: {
text: '年龄段分布',
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
xAxis: {
type: 'category',
data: Object.keys(ageGroups)
},
yAxis: {
type: 'value'
},
series: [
{
name: '人数',
type: 'bar',
data: Object.values(ageGroups),
itemStyle: {
color: '#409EFF'
}
}
]
}
this.charts.ageChart.setOption(option)
},
updateGenderChart() {
// 统计性别分布
const genderData = {
'男': 0,
'女': 0
}
this.currentEmployees.forEach(emp => {
if (emp.gender === '男' || emp.gender === '女') {
genderData[emp.gender]++
}
})
const option = {
title: {
text: '性别分布',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '性别',
type: 'pie',
radius: '50%',
data: Object.entries(genderData).map(([name, value]) => ({ name, value })),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
}
this.charts.genderChart.setOption(option)
},
updateTrendChart() {
// 生成时间序列数据
const timeRange = this.generateTimeRange()
const trendData = {
entry: new Array(timeRange.length).fill(0),
leave: new Array(timeRange.length).fill(0),
regular: new Array(timeRange.length).fill(0),
transfer: new Array(timeRange.length).fill(0)
}
// 提取日期部分的辅助函数
const getDatePart = (datetime) => {
if (!datetime) return ''
return datetime.split(' ')[0]
}
// 填充入职数据
this.entryRecords.forEach(record => {
const datePart = getDatePart(record.changeTime)
const index = timeRange.indexOf(datePart)
if (index !== -1) trendData.entry[index]++
})
// 填充离职数据
this.leaveRecords.forEach(record => {
const datePart = getDatePart(record.changeTime)
const index = timeRange.indexOf(datePart)
if (index !== -1) trendData.leave[index]++
})
// 填充转正数据
this.regularRecords.forEach(record => {
const datePart = getDatePart(record.changeTime)
const index = timeRange.indexOf(datePart)
if (index !== -1) trendData.regular[index]++
})
// 填充调岗数据
this.transferRecords.forEach(record => {
const datePart = getDatePart(record.transferTime)
const index = timeRange.indexOf(datePart)
if (index !== -1) trendData.transfer[index]++
})
const option = {
title: {
text: '员工变动趋势',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['入职', '离职', '转正', '调岗'],
bottom: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: timeRange
},
yAxis: {
type: 'value'
},
series: [
{
name: '入职',
type: 'line',
stack: 'Total',
data: trendData.entry,
itemStyle: {
color: '#67C23A'
}
},
{
name: '离职',
type: 'line',
stack: 'Total',
data: trendData.leave,
itemStyle: {
color: '#F56C6C'
}
},
{
name: '转正',
type: 'line',
stack: 'Total',
data: trendData.regular,
itemStyle: {
color: '#409EFF'
}
},
{
name: '调岗',
type: 'line',
stack: 'Total',
data: trendData.transfer,
itemStyle: {
color: '#E6A23C'
}
}
]
}
this.charts.trendChart.setOption(option)
},
generateTimeRange() {
// 生成时间范围内的日期数组
const start = this.dateRange && this.dateRange[0] ? new Date(this.dateRange[0]) : new Date()
const end = this.dateRange && this.dateRange[1] ? new Date(this.dateRange[1]) : new Date()
const timeRange = []
// 如果没有设置时间范围默认显示最近30天
if (!this.dateRange || !this.dateRange[0] || !this.dateRange[1]) {
const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
start.setTime(thirtyDaysAgo.getTime())
}
const current = new Date(start)
while (current <= end) {
const dateStr = current.toISOString().split('T')[0]
timeRange.push(dateStr)
current.setDate(current.getDate() + 1)
}
return timeRange
}
}
}
</script>
<style scoped>
.employee-report {
padding: 20px;
background-color: #f5f7fa;
}
.filter-section {
margin-bottom: 20px;
background-color: #fff;
padding: 20px;
border-radius: 4px;
}
.stats-section {
margin-bottom: 20px;
}
.stats-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
text-align: center;
}
.stat-item {
padding: 10px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #409EFF;
margin-bottom: 5px;
}
.stat-label {
font-size: 14px;
color: #606266;
}
.stats-chart {
background-color: #fff;
border-radius: 4px;
padding: 20px;
}
.interactive-charts-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.chart-card {
background-color: #fff;
border-radius: 4px;
padding: 20px;
}
.trend-section {
margin-bottom: 20px;
background-color: #fff;
border-radius: 4px;
padding: 20px;
}
.details-section {
background-color: #fff;
border-radius: 4px;
padding: 20px;
}
</style>