整合前端

This commit is contained in:
砂糖
2026-04-13 17:04:38 +08:00
parent 69609a2cb1
commit 5d4794c9bd
915 changed files with 144259 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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