整合前端
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div ref="chart" style="width:100%;height:300px"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
export default {
|
||||
name: 'BarChart',
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'monthly' // 'monthly' 或 'insurance'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.chart = echarts.init(this.$refs.chart);
|
||||
this.draw();
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.chart) {
|
||||
this.chart.dispose();
|
||||
this.chart = null;
|
||||
}
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
handler() {
|
||||
this.draw();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
draw() {
|
||||
if (!this.chart || !this.data || this.data.length === 0) {
|
||||
this.showEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.type === 'insurance') {
|
||||
this.drawInsuranceChart();
|
||||
} else {
|
||||
this.drawMonthlyChart();
|
||||
}
|
||||
},
|
||||
|
||||
drawMonthlyChart() {
|
||||
const months = this.data.map(i => i.month);
|
||||
const netSalaryData = this.data.map(i => i.netSalary || 0);
|
||||
const grossSalaryData = this.data.map(i => i.grossSalary || 0);
|
||||
const unitExpenseData = this.data.map(i => i.unitExpense || 0);
|
||||
|
||||
this.chart.setOption({
|
||||
title: { show: false },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function(params) {
|
||||
let result = params[0].axisValue + '<br/>';
|
||||
params.forEach(param => {
|
||||
result += param.marker + param.seriesName + ': ¥' + Number(param.value).toLocaleString() + '<br/>';
|
||||
});
|
||||
return result;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['实发工资', '应发工资', '单位支出'],
|
||||
bottom: 0
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
top: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: months,
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: function(value) {
|
||||
return '¥' + (value / 10000).toFixed(1) + 'w';
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '实发工资',
|
||||
type: 'bar',
|
||||
data: netSalaryData,
|
||||
itemStyle: { color: '#409EFF' }
|
||||
},
|
||||
{
|
||||
name: '应发工资',
|
||||
type: 'bar',
|
||||
data: grossSalaryData,
|
||||
itemStyle: { color: '#67C23A' }
|
||||
},
|
||||
{
|
||||
name: '单位支出',
|
||||
type: 'bar',
|
||||
data: unitExpenseData,
|
||||
itemStyle: { color: '#E6A23C' }
|
||||
}
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
drawInsuranceChart() {
|
||||
const itemNames = this.data.map(i => i.itemName);
|
||||
const personalData = this.data.map(i => i.personal || 0);
|
||||
const enterpriseData = this.data.map(i => i.enterprise || 0);
|
||||
|
||||
this.chart.setOption({
|
||||
title: { show: false },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function(params) {
|
||||
let result = params[0].axisValue + '<br/>';
|
||||
params.forEach(param => {
|
||||
result += param.marker + param.seriesName + ': ¥' + Number(param.value).toLocaleString() + '<br/>';
|
||||
});
|
||||
return result;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['个人缴纳', '企业缴纳'],
|
||||
bottom: 0
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
top: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: itemNames,
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: function(value) {
|
||||
return '¥' + (value / 10000).toFixed(1) + 'w';
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '个人缴纳',
|
||||
type: 'bar',
|
||||
data: personalData,
|
||||
itemStyle: { color: '#409EFF' }
|
||||
},
|
||||
{
|
||||
name: '企业缴纳',
|
||||
type: 'bar',
|
||||
data: enterpriseData,
|
||||
itemStyle: { color: '#F56C6C' }
|
||||
}
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
showEmptyState() {
|
||||
if (!this.chart) return;
|
||||
|
||||
this.chart.setOption({
|
||||
title: {
|
||||
text: '暂无数据',
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
textStyle: {
|
||||
color: '#999',
|
||||
fontSize: 16
|
||||
}
|
||||
},
|
||||
xAxis: { show: false },
|
||||
yAxis: { show: false },
|
||||
series: []
|
||||
});
|
||||
},
|
||||
|
||||
handleResize() {
|
||||
if (this.chart) {
|
||||
this.chart.resize();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-table :data="pagedData" border>
|
||||
<el-table-column prop="deptName" label="部门名称" min-width="120" />
|
||||
<el-table-column prop="employeeCount" label="员工数量" width="100" align="center" />
|
||||
<el-table-column prop="totalNetSalary" label="总实发工资" min-width="120" align="right" />
|
||||
<el-table-column prop="totalGrossSalary" label="总应发工资" min-width="120" align="right" />
|
||||
<el-table-column prop="avgNetSalary" label="人均实发工资" min-width="120" align="right" />
|
||||
<el-table-column prop="avgGrossSalary" label="人均应发工资" min-width="120" align="right" />
|
||||
<el-table-column prop="yearOnYearGrowthRate" label="同比增长率" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span :class="getGrowthClass(scope.row.yearOnYearGrowthRate)">
|
||||
{{ scope.row.yearOnYearGrowthRate }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-if="total > pageSize"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
:current-page="currentPage"
|
||||
layout="prev, pager, next, total"
|
||||
@current-change="onPageChange"
|
||||
style="text-align:right; margin-top:10px"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DepartmentTable',
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 10
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentPage: 1
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
pagedData() {
|
||||
const start = (this.currentPage - 1) * this.pageSize;
|
||||
return this.data.slice(start, start + this.pageSize);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onPageChange(page) {
|
||||
this.currentPage = page;
|
||||
},
|
||||
getGrowthClass(growthRate) {
|
||||
if (!growthRate) return '';
|
||||
if (growthRate.startsWith('+')) {
|
||||
return 'growth-positive';
|
||||
} else if (growthRate.startsWith('-')) {
|
||||
return 'growth-negative';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.growth-positive {
|
||||
color: #67c23a;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.growth-negative {
|
||||
color: #f56c6c;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<div ref="chart" style="width:100%;height:300px"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
export default {
|
||||
name: 'LineChart',
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
validator: function(value) {
|
||||
return Array.isArray(value) && value.every(item =>
|
||||
item && typeof item.month === 'string' &&
|
||||
typeof item.total === 'number' &&
|
||||
typeof item.avg === 'number'
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
handler(newData) {
|
||||
if (newData && newData.length > 0 && this.chart) {
|
||||
this.updateChart();
|
||||
} else if (this.chart) {
|
||||
// 如果没有数据,显示空状态
|
||||
this.showEmptyState();
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initChart();
|
||||
// 监听窗口大小变化,重新绘制图表
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.chart) {
|
||||
this.chart.dispose();
|
||||
this.chart = null;
|
||||
}
|
||||
// 移除事件监听器
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
if (this.$refs.chart) {
|
||||
this.chart = echarts.init(this.$refs.chart);
|
||||
if (this.data && this.data.length > 0) {
|
||||
this.updateChart();
|
||||
} else {
|
||||
this.showEmptyState();
|
||||
}
|
||||
}
|
||||
},
|
||||
updateChart() {
|
||||
if (!this.chart || !this.data || this.data.length === 0) {
|
||||
this.showEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
const months = this.data.map(item => item.month);
|
||||
const netSalaryData = this.data.map(item => Number(item.netSalary || 0));
|
||||
const grossSalaryData = this.data.map(item => Number(item.grossSalary || 0));
|
||||
const avgNetSalaryData = this.data.map(item => Number(item.avgNetSalary || 0));
|
||||
|
||||
this.chart.setOption({
|
||||
title: {
|
||||
show: false // 清除空状态的标题
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function(params) {
|
||||
let result = params[0].axisValue + '<br/>';
|
||||
params.forEach(param => {
|
||||
result += param.marker + param.seriesName + ': ¥' + Number(param.value).toLocaleString() + '<br/>';
|
||||
});
|
||||
return result;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['实发工资总额', '应发工资总额', '人均实发工资'],
|
||||
bottom: 0
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
top: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: months,
|
||||
show: true, // 确保x轴显示
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '总额(万元)',
|
||||
position: 'left',
|
||||
axisLabel: {
|
||||
formatter: function(value) {
|
||||
return (value / 10000).toFixed(1) + 'w';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '人均(元)',
|
||||
position: 'right',
|
||||
axisLabel: {
|
||||
formatter: '¥{value}'
|
||||
}
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '实发工资总额',
|
||||
type: 'line',
|
||||
yAxisIndex: 0,
|
||||
data: netSalaryData,
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: '#409EFF'
|
||||
},
|
||||
lineStyle: {
|
||||
width: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '应发工资总额',
|
||||
type: 'line',
|
||||
yAxisIndex: 0,
|
||||
data: grossSalaryData,
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: '#67C23A'
|
||||
},
|
||||
lineStyle: {
|
||||
width: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '人均实发工资',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: avgNetSalaryData,
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: '#E6A23C'
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
type: 'dashed'
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
showEmptyState() {
|
||||
if (!this.chart) return;
|
||||
|
||||
this.chart.setOption({
|
||||
title: {
|
||||
text: '暂无数据',
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
textStyle: {
|
||||
color: '#999',
|
||||
fontSize: 16
|
||||
}
|
||||
},
|
||||
xAxis: { show: false },
|
||||
yAxis: { show: false },
|
||||
series: []
|
||||
});
|
||||
},
|
||||
handleResize() {
|
||||
if (this.chart) {
|
||||
this.chart.resize();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div ref="chart" style="width:100%;height:300px"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
export default {
|
||||
name: 'PieChart',
|
||||
props: {
|
||||
data: { type: Array, default: () => [] },
|
||||
categories: { type: Array, default: () => [] },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
handler(newData) {
|
||||
if (this.chart) {
|
||||
this.updateChart();
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initChart();
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.chart) {
|
||||
this.chart.dispose();
|
||||
this.chart = null;
|
||||
}
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
this.chart = echarts.init(this.$refs.chart);
|
||||
this.updateChart();
|
||||
},
|
||||
updateChart() {
|
||||
if (!this.chart) return;
|
||||
|
||||
if (!this.data || this.data.length === 0) {
|
||||
this.showEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
this.chart.setOption({
|
||||
title: { show: false },
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function(params) {
|
||||
const percentage = params.data.percentage ?
|
||||
` (${params.data.percentage}%)` :
|
||||
` (${params.percent}%)`;
|
||||
return params.marker + params.name + ': ¥' +
|
||||
Number(params.value).toLocaleString() + percentage;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
top: 'middle',
|
||||
textStyle: {
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['65%', '50%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 5,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '16',
|
||||
fontWeight: 'bold',
|
||||
formatter: function(params) {
|
||||
return params.name + '\
|
||||
¥' + Number(params.value).toLocaleString();
|
||||
}
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: this.data
|
||||
}],
|
||||
});
|
||||
},
|
||||
showEmptyState() {
|
||||
if (!this.chart) return;
|
||||
|
||||
this.chart.setOption({
|
||||
title: {
|
||||
text: '暂无数据',
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
textStyle: {
|
||||
color: '#999',
|
||||
fontSize: 16
|
||||
}
|
||||
},
|
||||
series: []
|
||||
});
|
||||
},
|
||||
handleResize() {
|
||||
if (this.chart) {
|
||||
this.chart.resize();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-title">{{ title }}</div>
|
||||
<div class="stat-value">{{ value }}</div>
|
||||
<div class="stat-sub" :class="getSubClass">
|
||||
<i v-if="growth === 'up'" class="el-icon-arrow-up stat-icon" />
|
||||
<i v-else-if="growth === 'down'" class="el-icon-arrow-down stat-icon" />
|
||||
<i v-else class="el-icon-minus stat-icon" />
|
||||
<span>{{ subText }}</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'StatisticCard',
|
||||
props: {
|
||||
title: String,
|
||||
value: String,
|
||||
subText: String,
|
||||
growth: { type: String, default: 'neutral' }, // 'up', 'down', 'neutral'
|
||||
},
|
||||
computed: {
|
||||
getSubClass() {
|
||||
return {
|
||||
'stat-sub-up': this.growth === 'up',
|
||||
'stat-sub-down': this.growth === 'down',
|
||||
'stat-sub-neutral': this.growth === 'neutral'
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
height: 140px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 6px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.stat-sub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stat-sub-up {
|
||||
color: #67C23A;
|
||||
}
|
||||
|
||||
.stat-sub-down {
|
||||
color: #F56C6C;
|
||||
}
|
||||
|
||||
.stat-sub-neutral {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
margin-right: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-table :data="pagedData" border>
|
||||
<el-table-column prop="unitName" label="单位名称" min-width="120" />
|
||||
<el-table-column prop="employeeCount" label="员工数量" width="100" align="center" />
|
||||
<el-table-column prop="totalNetSalary" label="总实发工资" min-width="120" align="right" />
|
||||
<el-table-column prop="totalGrossSalary" label="总应发工资" min-width="120" align="right" />
|
||||
<el-table-column prop="totalUnitExpense" label="单位总支出" min-width="120" align="right" />
|
||||
<el-table-column prop="avgNetSalary" label="人均实发工资" min-width="120" align="right" />
|
||||
<el-table-column prop="yearOnYearGrowthRate" label="同比增长率" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span :class="getGrowthClass(scope.row.yearOnYearGrowthRate)">
|
||||
{{ scope.row.yearOnYearGrowthRate }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="salaryPeriod" label="工资期间" width="100" align="center" />
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-if="total > pageSize"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
:current-page="currentPage"
|
||||
layout="prev, pager, next, total"
|
||||
@current-change="onPageChange"
|
||||
style="text-align:right; margin-top:10px"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'UnitTable',
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 10
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentPage: 1
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
pagedData() {
|
||||
const start = (this.currentPage - 1) * this.pageSize;
|
||||
return this.data.slice(start, start + this.pageSize);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onPageChange(page) {
|
||||
this.currentPage = page;
|
||||
},
|
||||
getGrowthClass(growthRate) {
|
||||
if (!growthRate) return '';
|
||||
if (growthRate.startsWith('+')) {
|
||||
return 'growth-positive';
|
||||
} else if (growthRate.startsWith('-')) {
|
||||
return 'growth-negative';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.growth-positive {
|
||||
color: #67c23a;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.growth-negative {
|
||||
color: #f56c6c;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
322
ruoyi-ui/src/views/oa/finance/salary/dashboard/index.vue
Normal file
322
ruoyi-ui/src/views/oa/finance/salary/dashboard/index.vue
Normal file
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<div style="padding:20px">
|
||||
<!-- 时间范围选择 -->
|
||||
<el-row class="mb8">
|
||||
<el-col :span="4">
|
||||
<el-date-picker
|
||||
v-model="selectedTime"
|
||||
type="month"
|
||||
placeholder="选择年月"
|
||||
value-format="yyyy-MM-01"
|
||||
>
|
||||
</el-date-picker>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 卡片数据展示 -->
|
||||
<el-row :gutter="20" v-loading="loading" style="margin-bottom: 20px">
|
||||
<el-col :span="24">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="4" v-for="card in cards" :key="card.title">
|
||||
<statistic-card
|
||||
:title="card.title"
|
||||
:value="card.value"
|
||||
:sub-text="card.subText"
|
||||
:growth="card.growth"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域第一行 -->
|
||||
<el-row :gutter="20" style="margin-bottom: 20px">
|
||||
<el-col :span="12">
|
||||
<el-card v-loading="monthlyExpenseLoading">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>月度支出汇总</span>
|
||||
</div>
|
||||
<bar-chart :data="monthlyExpenseData" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card v-loading="salaryStructureLoading">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>工资构成分析</span>
|
||||
</div>
|
||||
<pie-chart :data="salaryStructureData" :categories="salaryStructureCategories" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域第二行 -->
|
||||
<el-row :gutter="20" style="margin-bottom: 20px">
|
||||
<el-col :span="12">
|
||||
<el-card v-loading="trendLoading">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>工资趋势分析</span>
|
||||
</div>
|
||||
<line-chart :data="trendData" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card v-loading="insuranceLoading">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>社保公积金统计</span>
|
||||
</div>
|
||||
<bar-chart :data="insuranceData" :type="'insurance'" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 统计表格 -->
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-card v-loading="unitTableLoading">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>单位统计</span>
|
||||
</div>
|
||||
<unit-table :data="unitTableData" :total="unitTableTotal" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card v-loading="deptTableLoading">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>部门统计</span>
|
||||
</div>
|
||||
<department-table :data="deptTableData" :total="deptTableTotal" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAllData } from '@/api/oa/finance/dashboard';
|
||||
import BarChart from './components/BarChart.vue';
|
||||
import DepartmentTable from './components/DepartmentTable.vue';
|
||||
import UnitTable from './components/UnitTable.vue';
|
||||
import LineChart from './components/LineChart.vue';
|
||||
import PieChart from './components/PieChart.vue';
|
||||
import StatisticCard from './components/StatisticCard.vue';
|
||||
|
||||
export default {
|
||||
name: 'SalaryDashboard',
|
||||
components: {
|
||||
StatisticCard,
|
||||
BarChart,
|
||||
PieChart,
|
||||
LineChart,
|
||||
DepartmentTable,
|
||||
UnitTable
|
||||
},
|
||||
created() {
|
||||
const now = new Date();
|
||||
this.selectedTime = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-01`;
|
||||
},
|
||||
watch: {
|
||||
selectedTime: {
|
||||
handler(val) {
|
||||
if (val) {
|
||||
const date = new Date(val);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const salaryPeriod = `${year}-${month}`;
|
||||
this.fetchData(salaryPeriod);
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTime: null,
|
||||
loading: false,
|
||||
monthlyExpenseLoading: false,
|
||||
salaryStructureLoading: false,
|
||||
trendLoading: false,
|
||||
insuranceLoading: false,
|
||||
unitTableLoading: false,
|
||||
deptTableLoading: false,
|
||||
cards: [],
|
||||
monthlyExpenseData: [],
|
||||
salaryStructureCategories: [],
|
||||
salaryStructureData: [],
|
||||
trendData: [],
|
||||
insuranceData: [],
|
||||
unitTableData: [],
|
||||
unitTableTotal: 0,
|
||||
deptTableData: [],
|
||||
deptTableTotal: 0,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async fetchData(salaryPeriod) {
|
||||
this.loading = true;
|
||||
this.monthlyExpenseLoading = true;
|
||||
this.salaryStructureLoading = true;
|
||||
this.trendLoading = true;
|
||||
this.insuranceLoading = true;
|
||||
this.unitTableLoading = true;
|
||||
this.deptTableLoading = true;
|
||||
|
||||
try {
|
||||
const response = await getAllData({
|
||||
salaryPeriod: salaryPeriod
|
||||
});
|
||||
|
||||
if(response.code === 200) {
|
||||
const { data } = response;
|
||||
const { cardData, chartData, unitStats, deptStats } = data;
|
||||
|
||||
// 处理卡片数据
|
||||
this.processCardData(cardData);
|
||||
|
||||
// 处理图表数据
|
||||
this.processChartData(chartData);
|
||||
|
||||
// 处理表格数据
|
||||
this.processTableData(unitStats, deptStats);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
this.$message.error('获取数据失败,请稍后重试');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.monthlyExpenseLoading = false;
|
||||
this.salaryStructureLoading = false;
|
||||
this.trendLoading = false;
|
||||
this.insuranceLoading = false;
|
||||
this.unitTableLoading = false;
|
||||
this.deptTableLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 处理卡片数据
|
||||
processCardData(cardData) {
|
||||
this.cards = [
|
||||
{
|
||||
title: '总实发工资',
|
||||
value: this.formatCurrency(cardData.totalNetSalary),
|
||||
subText: `较上月${this.getGrowthText(cardData.lastMonthNetSalaryRate)}`,
|
||||
growth: this.getGrowthDirection(cardData.lastMonthNetSalaryRate)
|
||||
},
|
||||
{
|
||||
title: '总应发工资',
|
||||
value: this.formatCurrency(cardData.totalGrossSalary),
|
||||
subText: `较上月${this.getGrowthText(cardData.lastMonthGrossSalaryRate)}`,
|
||||
growth: this.getGrowthDirection(cardData.lastMonthGrossSalaryRate)
|
||||
},
|
||||
{
|
||||
title: '单位总支出',
|
||||
value: this.formatCurrency(cardData.totalUnitExpense),
|
||||
subText: `较上月${this.getGrowthText(cardData.lastMonthUnitExpenseRate)}`,
|
||||
growth: this.getGrowthDirection(cardData.lastMonthUnitExpenseRate)
|
||||
},
|
||||
{
|
||||
title: '人均实发工资',
|
||||
value: this.formatCurrency(cardData.avgNetSalary),
|
||||
subText: `较上月${this.getGrowthText(cardData.lastMonthAvgNetSalaryRate)}`,
|
||||
growth: this.getGrowthDirection(cardData.lastMonthAvgNetSalaryRate)
|
||||
},
|
||||
{
|
||||
title: '同比增长率',
|
||||
value: `${cardData.yearOnYearGrowthRate || 0}%`,
|
||||
subText: '较去年同期',
|
||||
growth: this.getGrowthDirection(cardData.yearOnYearGrowthRate)
|
||||
},
|
||||
{
|
||||
title: '员工总数',
|
||||
value: `${cardData.totalEmployeeCount || 0}人`,
|
||||
subText: `共${cardData.unitCount || 0}个单位`,
|
||||
growth: 'neutral'
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
// 处理图表数据
|
||||
processChartData(chartData) {
|
||||
// 月度支出数据
|
||||
this.monthlyExpenseData = chartData.monthlyExpenses?.map(item => ({
|
||||
month: `${item.month}月`,
|
||||
netSalary: item.totalNetSalary,
|
||||
grossSalary: item.totalGrossSalary,
|
||||
unitExpense: item.totalUnitExpense
|
||||
})) || [];
|
||||
|
||||
// 工资构成分析数据
|
||||
this.salaryStructureCategories = chartData.salaryStructures?.map(item => item.itemName) || [];
|
||||
this.salaryStructureData = chartData.salaryStructures?.map(item => ({
|
||||
name: item.itemName,
|
||||
value: item.totalAmount,
|
||||
percentage: item.percentage
|
||||
})) || [];
|
||||
|
||||
// 趋势分析数据
|
||||
if (chartData.trendData) {
|
||||
this.trendData = chartData.trendData.months?.map((month, index) => ({
|
||||
month: `${month}月`,
|
||||
netSalary: chartData.trendData.netSalaryTrend?.[index] || 0,
|
||||
grossSalary: chartData.trendData.grossSalaryTrend?.[index] || 0,
|
||||
avgNetSalary: chartData.trendData.avgNetSalaryTrend?.[index] || 0
|
||||
})) || [];
|
||||
}
|
||||
|
||||
// 社保公积金统计数据
|
||||
this.insuranceData = chartData.insuranceStats?.map(item => ({
|
||||
itemName: item.itemName,
|
||||
personal: item.personalTotal,
|
||||
enterprise: item.enterpriseTotal,
|
||||
total: item.total
|
||||
})) || [];
|
||||
},
|
||||
|
||||
// 处理表格数据
|
||||
processTableData(unitStats, deptStats) {
|
||||
// 单位统计数据
|
||||
this.unitTableData = unitStats?.rows?.map(item => ({
|
||||
unitName: item.unitName,
|
||||
employeeCount: item.employeeCount,
|
||||
totalNetSalary: this.formatCurrency(item.totalNetSalary),
|
||||
totalGrossSalary: this.formatCurrency(item.totalGrossSalary),
|
||||
totalUnitExpense: this.formatCurrency(item.totalUnitExpense),
|
||||
avgNetSalary: this.formatCurrency(item.avgNetSalary),
|
||||
yearOnYearGrowthRate: `${item.yearOnYearGrowthRate >= 0 ? '+' : ''}${item.yearOnYearGrowthRate}%`,
|
||||
salaryPeriod: item.salaryPeriod
|
||||
})) || [];
|
||||
this.unitTableTotal = unitStats?.total || 0;
|
||||
|
||||
// 部门统计数据
|
||||
this.deptTableData = deptStats?.rows?.map(item => ({
|
||||
deptName: item.deptName,
|
||||
employeeCount: item.employeeCount,
|
||||
totalNetSalary: this.formatCurrency(item.totalNetSalary),
|
||||
totalGrossSalary: this.formatCurrency(item.totalGrossSalary),
|
||||
avgNetSalary: this.formatCurrency(item.avgNetSalary),
|
||||
avgGrossSalary: this.formatCurrency(item.avgGrossSalary),
|
||||
yearOnYearGrowthRate: `${item.yearOnYearGrowthRate >= 0 ? '+' : ''}${item.yearOnYearGrowthRate}%`
|
||||
})) || [];
|
||||
this.deptTableTotal = deptStats?.total || 0;
|
||||
},
|
||||
|
||||
// 格式化货币
|
||||
formatCurrency(amount) {
|
||||
if (!amount) return '¥ 0';
|
||||
return `¥ ${Number(amount).toLocaleString()}`;
|
||||
},
|
||||
|
||||
// 获取增长文本
|
||||
getGrowthText(rate) {
|
||||
if (!rate) return '无变化';
|
||||
const direction = rate >= 0 ? '增长' : '下降';
|
||||
return `${direction} ${Math.abs(rate)}%`;
|
||||
},
|
||||
|
||||
// 获取增长方向
|
||||
getGrowthDirection(rate) {
|
||||
if (!rate) return 'neutral';
|
||||
return rate >= 0 ? 'up' : 'down';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user