✨ feat: 考勤分析增加薪资支出
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
</div> -->
|
||||
<!-- 筛选器 -->
|
||||
<div class="filters flex items-center gap-4">
|
||||
<el-dropdown trigger="hover">
|
||||
<!-- <el-dropdown trigger="hover">
|
||||
<el-button class="filter-btn whitespace-nowrap">
|
||||
记录类型<el-icon class="el-icon--right">
|
||||
<ArrowDown />
|
||||
@@ -40,7 +40,7 @@
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-dropdown> -->
|
||||
<!-- <el-dropdown trigger="click">
|
||||
<el-button class="filter-btn whitespace-nowrap">
|
||||
状态筛选<el-icon class="el-icon--right">
|
||||
@@ -123,6 +123,23 @@
|
||||
<div id="heatmapChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12" class="mt-6">
|
||||
<el-card shadow="hover" class="chart-card">
|
||||
<template #header>
|
||||
<h3 class="chart-title text-lg font-medium">薪资支出趋势</h3>
|
||||
</template>
|
||||
<div id="salaryTrendChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12" class="mt-6">
|
||||
<el-card shadow="hover" class="chart-card">
|
||||
<template #header>
|
||||
<h3 class="chart-title text-lg font-medium">薪资支出排行(按人)</h3>
|
||||
</template>
|
||||
<div id="salaryRankChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<!-- 数据表格 -->
|
||||
@@ -130,7 +147,6 @@
|
||||
<el-card shadow="hover" class="table-card">
|
||||
<el-table :data="tableList" class="full-width-table">
|
||||
<el-table-column v-for="col in tableColumns" :key="col.key" :prop="col.key" :label="col.label">
|
||||
|
||||
<template #default="{ row }" v-if="col.key === 'recordType'">
|
||||
<div class="flex items-center">
|
||||
<el-icon :class="getTypeIconColor(row.recordType)">
|
||||
@@ -248,13 +264,21 @@ const statistics = ref([
|
||||
trend: 8.3,
|
||||
icon: 'Document',
|
||||
iconColor: 'text-green-500'
|
||||
},
|
||||
{
|
||||
label: '薪资总支出',
|
||||
value: '100,000',
|
||||
trend: 12.5,
|
||||
icon: 'Money',
|
||||
iconColor: 'text-blue-500'
|
||||
}
|
||||
]);
|
||||
|
||||
const setStatistics = ({ totalDuration, totalRecords, averageDuration }) => {
|
||||
const setStatistics = ({ totalDuration, totalRecords, averageDuration, totalWage }) => {
|
||||
statistics.value[0].value = totalRecords
|
||||
statistics.value[1].value = averageDuration
|
||||
statistics.value[2].value = totalDuration
|
||||
statistics.value[3].value = totalWage
|
||||
}
|
||||
|
||||
const setTrend = ({ attendanceData, overtimeData, travelData, xAxis }) => {
|
||||
@@ -444,6 +468,85 @@ const setDuration = ({ durationData, xAxis }) => {
|
||||
});
|
||||
}
|
||||
|
||||
// 新增:1. 薪资支出趋势折线图配置
|
||||
const setSalaryTrend = ({ xAxis, salaryData }) => {
|
||||
salaryTrendChart.value.setOption({
|
||||
animation: false,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: '{b}<br/>薪资支出:¥{c}', // 显示日期+薪资(带¥符号)
|
||||
axisPointer: { type: 'shadow' }
|
||||
},
|
||||
legend: { data: ['薪资支出'], left: 'center', top: 0 },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxis,
|
||||
axisLabel: { rotate: 30, margin: 10 } // 日期旋转避免重叠
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '薪资(元)',
|
||||
min: 0,
|
||||
axisLabel: { formatter: '¥{value}' } // y轴显示¥符号
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '薪资支出',
|
||||
type: 'line',
|
||||
data: salaryData,
|
||||
lineStyle: { color: '#e11d48', width: 2 }, // 红色系(区分其他图表)
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
itemStyle: { color: '#e11d48' }
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
// 新增:2. 薪资支出按人柱状图配置(横向,参考出勤排行)
|
||||
const setSalaryRank = ({ users, salaries }) => {
|
||||
salaryRankChart.value.setOption({
|
||||
animation: false,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: '{b}<br/>总薪资:¥{c}',
|
||||
axisPointer: { type: 'shadow' }
|
||||
},
|
||||
legend: { data: ['总薪资'], left: 'left', top: 0 },
|
||||
grid: { left: '15%', right: '8%', bottom: '3%', containLabel: true }, // 左移适配用户名
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '薪资(元)',
|
||||
min: 0,
|
||||
axisLabel: { formatter: '¥{value}' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: users,
|
||||
axisLabel: { rotate: 0, margin: 15 }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '总薪资',
|
||||
type: 'bar',
|
||||
data: salaries,
|
||||
itemStyle: {
|
||||
color: '#e11d48',
|
||||
borderRadius: [0, 4, 4, 0] // 横向柱状图右侧圆角
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
formatter: '¥{c}', // 标签显示¥符号
|
||||
fontSize: 12
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const getTypeIcon = (type) => {
|
||||
const icons = {
|
||||
attendance: Check,
|
||||
@@ -461,6 +564,8 @@ const trendChart = ref(null);
|
||||
const durationChart = ref(null);
|
||||
const heatmapChart = ref(null);
|
||||
const attendanceRankChart = ref(null);
|
||||
const salaryTrendChart = ref(null); // 薪资支出趋势折线图
|
||||
const salaryRankChart = ref(null); // 薪资支出按人柱状图
|
||||
|
||||
const initCharts = () => {
|
||||
trendChart.value = echarts.init(document.querySelector('#trendChart'));
|
||||
@@ -468,6 +573,8 @@ const initCharts = () => {
|
||||
attendanceRankChart.value = echarts.init(document.querySelector('#attendanceRankChart'));
|
||||
durationChart.value = echarts.init(document.querySelector('#durationChart'));
|
||||
heatmapChart.value = echarts.init(document.querySelector('#heatmapChart'));
|
||||
salaryTrendChart.value = echarts.init(document.querySelector('#salaryTrendChart'));
|
||||
salaryRankChart.value = echarts.init(document.querySelector('#salaryRankChart'));
|
||||
};
|
||||
|
||||
const updateCharts = () => {
|
||||
@@ -480,6 +587,11 @@ const updateCharts = () => {
|
||||
setAttendanceRank(attendanceRankData)
|
||||
const durationData = formatters.duration(list.value)
|
||||
setDuration(durationData)
|
||||
// 新增:更新薪资图表
|
||||
const salaryTrendData = formatters.salaryTrend(list.value);
|
||||
setSalaryTrend(salaryTrendData);
|
||||
const salaryRankData = formatters.salaryRank(list.value);
|
||||
setSalaryRank(salaryRankData);
|
||||
}
|
||||
|
||||
const formatters = {
|
||||
@@ -489,11 +601,13 @@ const formatters = {
|
||||
const totalRecords = list.length
|
||||
// 可能出现NAN,所以需要处理
|
||||
const averageDuration = totalRecords > 0 ? (totalDuration / totalRecords).toFixed(2) : 0
|
||||
const totalWage = list.reduce((acc, item) => acc + item.wage, 0)
|
||||
console.log(totalDuration, totalRecords, averageDuration)
|
||||
return {
|
||||
totalDuration,
|
||||
totalRecords,
|
||||
averageDuration
|
||||
averageDuration,
|
||||
totalWage
|
||||
}
|
||||
},
|
||||
trend: (list) => {
|
||||
@@ -693,6 +807,55 @@ const formatters = {
|
||||
xAxis: Object.keys(map),
|
||||
durationData: Object.values(map)
|
||||
}
|
||||
},
|
||||
// 新增:1. 薪资支出趋势数据格式化(按日期分组)
|
||||
salaryTrend: (list) => {
|
||||
const salaryData = [];
|
||||
const dateMap = new Map(); // 去重日期并排序
|
||||
|
||||
// 按日期累加薪资
|
||||
list.forEach(item => {
|
||||
const { recordDate, wage = 0 } = item; // 兼容无wage字段的情况
|
||||
if (!dateMap.has(recordDate)) dateMap.set(recordDate, true);
|
||||
|
||||
const exists = salaryData.find(i => i.date === recordDate);
|
||||
if (exists) {
|
||||
exists.salary += parseFloat(wage);
|
||||
} else {
|
||||
salaryData.push({ date: recordDate, salary: parseFloat(wage) });
|
||||
}
|
||||
});
|
||||
|
||||
// 日期排序,确保折线图顺序正确
|
||||
const xAxis = Array.from(dateMap.keys()).sort();
|
||||
// 补全缺失日期的薪资(为0)
|
||||
const filledSalaryData = xAxis.map(date => {
|
||||
const item = salaryData.find(i => i.date === date);
|
||||
return item ? item.salary.toFixed(2) : 0;
|
||||
});
|
||||
|
||||
return { xAxis, salaryData: filledSalaryData };
|
||||
},
|
||||
|
||||
// 新增:2. 薪资支出按人排行数据格式化(取前10)
|
||||
salaryRank: (list) => {
|
||||
const userSalary = {};
|
||||
|
||||
// 按用户名累加总薪资
|
||||
list.forEach(item => {
|
||||
const { nickName, wage = 0 } = item;
|
||||
userSalary[nickName] = (userSalary[nickName] || 0) + parseFloat(wage);
|
||||
});
|
||||
|
||||
// 按薪资降序排序,取前10名
|
||||
const sortedUsers = Object.entries(userSalary)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10);
|
||||
|
||||
return {
|
||||
users: sortedUsers.map(item => item[0]), // 用户名
|
||||
salaries: sortedUsers.map(item => item[1].toFixed(2)) // 总薪资(保留2位小数)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user