✨ feat: 考勤分析增加薪资支出
This commit is contained in:
@@ -25,7 +25,7 @@
|
|||||||
</div> -->
|
</div> -->
|
||||||
<!-- 筛选器 -->
|
<!-- 筛选器 -->
|
||||||
<div class="filters flex items-center gap-4">
|
<div class="filters flex items-center gap-4">
|
||||||
<el-dropdown trigger="hover">
|
<!-- <el-dropdown trigger="hover">
|
||||||
<el-button class="filter-btn whitespace-nowrap">
|
<el-button class="filter-btn whitespace-nowrap">
|
||||||
记录类型<el-icon class="el-icon--right">
|
记录类型<el-icon class="el-icon--right">
|
||||||
<ArrowDown />
|
<ArrowDown />
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown> -->
|
||||||
<!-- <el-dropdown trigger="click">
|
<!-- <el-dropdown trigger="click">
|
||||||
<el-button class="filter-btn whitespace-nowrap">
|
<el-button class="filter-btn whitespace-nowrap">
|
||||||
状态筛选<el-icon class="el-icon--right">
|
状态筛选<el-icon class="el-icon--right">
|
||||||
@@ -123,6 +123,23 @@
|
|||||||
<div id="heatmapChart" class="chart-container"></div>
|
<div id="heatmapChart" class="chart-container"></div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</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>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
<!-- 数据表格 -->
|
<!-- 数据表格 -->
|
||||||
@@ -130,7 +147,6 @@
|
|||||||
<el-card shadow="hover" class="table-card">
|
<el-card shadow="hover" class="table-card">
|
||||||
<el-table :data="tableList" class="full-width-table">
|
<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">
|
<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'">
|
<template #default="{ row }" v-if="col.key === 'recordType'">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<el-icon :class="getTypeIconColor(row.recordType)">
|
<el-icon :class="getTypeIconColor(row.recordType)">
|
||||||
@@ -248,13 +264,21 @@ const statistics = ref([
|
|||||||
trend: 8.3,
|
trend: 8.3,
|
||||||
icon: 'Document',
|
icon: 'Document',
|
||||||
iconColor: 'text-green-500'
|
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[0].value = totalRecords
|
||||||
statistics.value[1].value = averageDuration
|
statistics.value[1].value = averageDuration
|
||||||
statistics.value[2].value = totalDuration
|
statistics.value[2].value = totalDuration
|
||||||
|
statistics.value[3].value = totalWage
|
||||||
}
|
}
|
||||||
|
|
||||||
const setTrend = ({ attendanceData, overtimeData, travelData, xAxis }) => {
|
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 getTypeIcon = (type) => {
|
||||||
const icons = {
|
const icons = {
|
||||||
attendance: Check,
|
attendance: Check,
|
||||||
@@ -461,6 +564,8 @@ const trendChart = ref(null);
|
|||||||
const durationChart = ref(null);
|
const durationChart = ref(null);
|
||||||
const heatmapChart = ref(null);
|
const heatmapChart = ref(null);
|
||||||
const attendanceRankChart = ref(null);
|
const attendanceRankChart = ref(null);
|
||||||
|
const salaryTrendChart = ref(null); // 薪资支出趋势折线图
|
||||||
|
const salaryRankChart = ref(null); // 薪资支出按人柱状图
|
||||||
|
|
||||||
const initCharts = () => {
|
const initCharts = () => {
|
||||||
trendChart.value = echarts.init(document.querySelector('#trendChart'));
|
trendChart.value = echarts.init(document.querySelector('#trendChart'));
|
||||||
@@ -468,6 +573,8 @@ const initCharts = () => {
|
|||||||
attendanceRankChart.value = echarts.init(document.querySelector('#attendanceRankChart'));
|
attendanceRankChart.value = echarts.init(document.querySelector('#attendanceRankChart'));
|
||||||
durationChart.value = echarts.init(document.querySelector('#durationChart'));
|
durationChart.value = echarts.init(document.querySelector('#durationChart'));
|
||||||
heatmapChart.value = echarts.init(document.querySelector('#heatmapChart'));
|
heatmapChart.value = echarts.init(document.querySelector('#heatmapChart'));
|
||||||
|
salaryTrendChart.value = echarts.init(document.querySelector('#salaryTrendChart'));
|
||||||
|
salaryRankChart.value = echarts.init(document.querySelector('#salaryRankChart'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateCharts = () => {
|
const updateCharts = () => {
|
||||||
@@ -480,6 +587,11 @@ const updateCharts = () => {
|
|||||||
setAttendanceRank(attendanceRankData)
|
setAttendanceRank(attendanceRankData)
|
||||||
const durationData = formatters.duration(list.value)
|
const durationData = formatters.duration(list.value)
|
||||||
setDuration(durationData)
|
setDuration(durationData)
|
||||||
|
// 新增:更新薪资图表
|
||||||
|
const salaryTrendData = formatters.salaryTrend(list.value);
|
||||||
|
setSalaryTrend(salaryTrendData);
|
||||||
|
const salaryRankData = formatters.salaryRank(list.value);
|
||||||
|
setSalaryRank(salaryRankData);
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatters = {
|
const formatters = {
|
||||||
@@ -489,11 +601,13 @@ const formatters = {
|
|||||||
const totalRecords = list.length
|
const totalRecords = list.length
|
||||||
// 可能出现NAN,所以需要处理
|
// 可能出现NAN,所以需要处理
|
||||||
const averageDuration = totalRecords > 0 ? (totalDuration / totalRecords).toFixed(2) : 0
|
const averageDuration = totalRecords > 0 ? (totalDuration / totalRecords).toFixed(2) : 0
|
||||||
|
const totalWage = list.reduce((acc, item) => acc + item.wage, 0)
|
||||||
console.log(totalDuration, totalRecords, averageDuration)
|
console.log(totalDuration, totalRecords, averageDuration)
|
||||||
return {
|
return {
|
||||||
totalDuration,
|
totalDuration,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
averageDuration
|
averageDuration,
|
||||||
|
totalWage
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
trend: (list) => {
|
trend: (list) => {
|
||||||
@@ -693,6 +807,55 @@ const formatters = {
|
|||||||
xAxis: Object.keys(map),
|
xAxis: Object.keys(map),
|
||||||
durationData: Object.values(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