整合前端
This commit is contained in:
450
ruoyi-ui/src/views/oa/finance/components/PieAndBar.vue
Normal file
450
ruoyi-ui/src/views/oa/finance/components/PieAndBar.vue
Normal file
@@ -0,0 +1,450 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<!-- 骨架屏 - 数据加载状态显示 -->
|
||||
<el-skeleton :loading="loading" animated>
|
||||
<template #template>
|
||||
<!-- 骨架屏:四个数据卡片 -->
|
||||
<el-row :gutter="20" style="margin-bottom: 20px">
|
||||
<el-col :span="6" v-for="n in 4" :key="n">
|
||||
<el-skeleton-item variant="text" style="width: 80%; height: 20px" />
|
||||
<el-skeleton-item variant="text" style="width: 60%; height: 16px; margin-top: 8px" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 骨架屏:三个图表占位 -->
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<el-skeleton-item variant="rect" style="width: 32%; height: 400px" />
|
||||
<el-skeleton-item variant="rect" style="width: 32%; height: 400px" />
|
||||
<el-skeleton-item variant="rect" style="width: 32%; height: 400px" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<!-- 四个财务摘要卡片 -->
|
||||
<el-row :gutter="20" style="margin-bottom: 20px">
|
||||
<el-col :span="6">
|
||||
<el-card>
|
||||
<div>本月支出</div>
|
||||
<div>人民币: {{ monthlyOutCNY | numFormat }} 元</div>
|
||||
<div>美元: {{ monthlyOutUSD | numFormat }} $</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card>
|
||||
<div>本月收入</div>
|
||||
<div>人民币: {{ monthlyInCNY | numFormat }} 元</div>
|
||||
<div>美元: {{ monthlyInUSD | numFormat }} $</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card>
|
||||
<div>项目支出</div>
|
||||
<div>人民币: {{ projectOutCNY | numFormat }} 元</div>
|
||||
<div>美元: {{ projectOutUSD | numFormat }} $</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card>
|
||||
<div>项目收入</div>
|
||||
<div>人民币: {{ projectInCNY | numFormat }} 元</div>
|
||||
<div>美元: {{ projectInUSD | numFormat }} $</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 三个图表并排展示 -->
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<div id="bar-chart" style="width: 32%; height: 400px"></div>
|
||||
<div id="line-chart" style="width: 32%; height: 400px"></div>
|
||||
<div id="pie-chart" style="width: 32%; height: 400px"></div>
|
||||
</div>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { barData, findFinanceList2 } from '@/api/oa/finance';
|
||||
import { getExchangeRate } from '@/api/oa/finance/exchangeRate';
|
||||
import { projectData2 } from '@/api/oa/project';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: 'FinanceDashboard',
|
||||
props: {
|
||||
// 选中的月份,格式为YYYY-MM
|
||||
selectedMonth: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
// 加载状态控制
|
||||
loading: true,
|
||||
|
||||
// 数据存储
|
||||
currentList: [], // 月度财务数据列表
|
||||
projectList: [], // 项目数据列表
|
||||
financeList: [], // 财务明细列表
|
||||
|
||||
// 财务汇总数据
|
||||
monthlyOutCNY: 0, // 本月人民币支出
|
||||
monthlyOutUSD: 0, // 本月美元支出
|
||||
monthlyInCNY: 0, // 本月人民币收入
|
||||
monthlyInUSD: 0, // 本月美元收入
|
||||
projectOutCNY: 0, // 项目人民币支出
|
||||
projectOutUSD: 0, // 项目美元支出
|
||||
projectInCNY: 0, // 项目人民币收入
|
||||
projectInUSD: 0, // 项目美元收入
|
||||
|
||||
// 月度数据映射表,包含汇率和收支数据
|
||||
monthlyDataMap: {},
|
||||
|
||||
// 财务查询参数
|
||||
queryFinanceParams: {
|
||||
// projectId: 0,
|
||||
financeType: '',
|
||||
pageNum: 1,
|
||||
pageSize: 9999
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* 格式化项目数据查询日期
|
||||
* @returns {String} 格式化为YYYY-MM-DD的日期字符串
|
||||
*/
|
||||
dateForProject () {
|
||||
const m = this.selectedMonth;
|
||||
// 处理YYYY-MM格式
|
||||
if (/^\d{4}-\d{2}$/.test(m)) return `${m}-01`;
|
||||
// 处理YYYY-MM-DD格式
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(m)) return m;
|
||||
// 默认返回今天
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取汇总数据的月份
|
||||
* @returns {String} 格式为YYYY-MM的月份字符串
|
||||
*/
|
||||
monthForSummary () {
|
||||
return this.dateForProject.slice(0, 7);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* 监听选中月份变化,重新加载所有数据
|
||||
*/
|
||||
selectedMonth () {
|
||||
this.loadAllData();
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// 加载数据
|
||||
this.loadAllData();
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 清除已初始化的图表实例
|
||||
*/
|
||||
clearCharts () {
|
||||
['bar-chart', 'line-chart', 'pie-chart'].forEach(id => {
|
||||
const dom = document.getElementById(id);
|
||||
if (dom) {
|
||||
// 销毁已存在的图表实例
|
||||
const instance = echarts.getInstanceByDom(dom);
|
||||
if (instance) echarts.dispose(instance);
|
||||
dom.innerHTML = ''; // 清空容器
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载所有月份的汇率数据
|
||||
* @param {Array} months - 需要加载汇率的月份列表
|
||||
*/
|
||||
async loadAllExchangeRates (months) {
|
||||
const exchangeRate = await getExchangeRate()
|
||||
for (const month of months) {
|
||||
// 初始化月份数据对象
|
||||
if (!this.monthlyDataMap[month]) {
|
||||
this.monthlyDataMap[month] = {};
|
||||
}
|
||||
|
||||
// 如果已有汇率数据则跳过
|
||||
if (this.monthlyDataMap[month].exchangeRate) continue;
|
||||
|
||||
// 获取并存储汇率数据
|
||||
this.monthlyDataMap[month].exchangeRate = exchangeRate;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载所有需要展示的数据
|
||||
*/
|
||||
async loadAllData () {
|
||||
this.loading = true;
|
||||
this.clearCharts();
|
||||
|
||||
try {
|
||||
// 1. 获取条形图数据
|
||||
const barRes = await barData(this.queryFinanceParams);
|
||||
this.currentList = barRes.data;
|
||||
|
||||
// 2. 提取所有月份并加载对应的汇率
|
||||
const months = this.currentList.map(item => item.month);
|
||||
// 获取每月的汇率
|
||||
await this.loadAllExchangeRates(months);
|
||||
|
||||
// 获取饼图数据
|
||||
const pieRes = await projectData2(this.dateForProject);
|
||||
this.projectList = pieRes.data;
|
||||
|
||||
// 3. 并行获取项目数据和财务明细数据
|
||||
const finRes = await findFinanceList2(this.queryFinanceParams)
|
||||
this.financeList = finRes.data;
|
||||
|
||||
// 4. 计算财务汇总数据
|
||||
this.computeSummaries();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
console.log('Data loading completed', this.monthlyDataMap);
|
||||
// 确保DOM更新后初始化图表
|
||||
this.$nextTick(() => this.initCharts());
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 计算财务汇总数据
|
||||
*/
|
||||
computeSummaries () {
|
||||
const currentMonth = this.monthForSummary;
|
||||
|
||||
// 初始化月度数据映射表中的收支数据
|
||||
this.currentList.forEach(item => {
|
||||
this.monthlyDataMap[item.month] = {
|
||||
...this.monthlyDataMap[item.month],
|
||||
outTotal: item.totalOut,
|
||||
inCNY: 0,
|
||||
inUSD: 0
|
||||
};
|
||||
});
|
||||
|
||||
// 重置汇总数据
|
||||
this.resetSummaryData();
|
||||
|
||||
// 计算汇总数据
|
||||
this.financeList.forEach(item => {
|
||||
const month = item.date.slice(0, 7);
|
||||
const amount = +item.amount;
|
||||
const isOutgoing = item.type === '0'; // 0表示支出
|
||||
const currency = item.currency === 'USD' ? 'USD' : 'CNY';
|
||||
|
||||
// 更新月度收入数据
|
||||
const monthData = this.monthlyDataMap[month];
|
||||
if (monthData && !isOutgoing) {
|
||||
monthData[`in${currency}`] += amount;
|
||||
}
|
||||
|
||||
// 更新当前月份的汇总数据
|
||||
if (month === currentMonth) {
|
||||
this.updateCurrentMonthSummary(item, isOutgoing, currency, amount);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置汇总数据为0
|
||||
*/
|
||||
resetSummaryData () {
|
||||
Object.assign(this, {
|
||||
monthlyOutCNY: 0, monthlyOutUSD: 0,
|
||||
monthlyInCNY: 0, monthlyInUSD: 0,
|
||||
projectOutCNY: 0, projectOutUSD: 0,
|
||||
projectInCNY: 0, projectInUSD: 0
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新当前月份的汇总数据
|
||||
* @param {Object} item - 财务明细项
|
||||
* @param {Boolean} isOutgoing - 是否为支出
|
||||
* @param {String} currency - 货币类型
|
||||
* @param {Number} amount - 金额
|
||||
*/
|
||||
updateCurrentMonthSummary (item, isOutgoing, currency, amount) {
|
||||
if (isOutgoing) {
|
||||
// 处理支出
|
||||
this[`monthlyOut${currency}`] += amount;
|
||||
// 如果有项目ID,计入项目支出
|
||||
if (item.projectId) {
|
||||
this[`projectOut${currency}`] += amount;
|
||||
}
|
||||
} else {
|
||||
// 处理收入
|
||||
this[`monthlyIn${currency}`] += amount;
|
||||
// 如果有项目ID,计入项目收入
|
||||
if (item.projectId) {
|
||||
this[`projectIn${currency}`] += amount;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化所有图表
|
||||
*/
|
||||
initCharts () {
|
||||
const months = this.currentList.map(item => item.month);
|
||||
|
||||
// 初始化条形图
|
||||
this.initBarChart(months);
|
||||
|
||||
// 初始化折线图
|
||||
this.initLineChart(months);
|
||||
|
||||
// 初始化饼图
|
||||
this.initPieChart();
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化条形图 - 展示月度收支情况
|
||||
* @param {Array} months - 月份列表
|
||||
*/
|
||||
initBarChart (months) {
|
||||
const barChart = echarts.init(document.getElementById('bar-chart'));
|
||||
barChart.setOption({
|
||||
title: { text: '月度收支', left: 'center' },
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['支出', '收入(CNY)', '收入(USD)'], bottom: 0 },
|
||||
xAxis: { type: 'category', data: months, axisLabel: { rotate: 45 } },
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{
|
||||
name: '支出',
|
||||
type: 'bar',
|
||||
data: months.map(month => this.monthlyDataMap[month]?.outTotal || 0)
|
||||
},
|
||||
{
|
||||
name: '收入(CNY)',
|
||||
type: 'bar',
|
||||
data: months.map(month => Math.round(this.monthlyDataMap[month]?.inCNY || 0))
|
||||
},
|
||||
{
|
||||
name: '收入(USD)',
|
||||
type: 'bar',
|
||||
data: months.map(month => Math.round(this.monthlyDataMap[month]?.inUSD || 0))
|
||||
}
|
||||
]
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化折线图 - 展示近六个月趋势
|
||||
* @param {Array} months - 月份列表
|
||||
*/
|
||||
initLineChart (months) {
|
||||
// 构造带汇率的月份标签
|
||||
const monthsWithRate = months.map(month => {
|
||||
const rate = this.monthlyDataMap[month]?.exchangeRate || 7.2;
|
||||
return `${month}(${Number(rate).toFixed(2)})`;
|
||||
});
|
||||
|
||||
const lineChart = echarts.init(document.getElementById('line-chart'));
|
||||
lineChart.setOption({
|
||||
title: { text: '近六个月趋势', left: 'center' },
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['收入(CNY)', '收入(USD)', '支出', '净收入'], bottom: 0 },
|
||||
xAxis: { type: 'category', data: monthsWithRate },
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{
|
||||
name: '收入(CNY)',
|
||||
type: 'line',
|
||||
data: months.map(month => Math.round(this.monthlyDataMap[month]?.inCNY || 0)),
|
||||
smooth: true
|
||||
},
|
||||
{
|
||||
name: '收入(USD)',
|
||||
type: 'line',
|
||||
data: months.map(month => Math.round(this.monthlyDataMap[month]?.inUSD || 0)),
|
||||
smooth: true
|
||||
},
|
||||
{
|
||||
name: '支出',
|
||||
type: 'line',
|
||||
data: months.map(month => this.monthlyDataMap[month]?.outTotal || 0),
|
||||
smooth: true
|
||||
},
|
||||
{
|
||||
name: '净收入',
|
||||
type: 'line',
|
||||
data: months.map(month => this.calculateNetIncome(month)),
|
||||
smooth: true,
|
||||
itemStyle: { color: '#E6A23C' }
|
||||
}
|
||||
]
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 计算指定月份的净收入
|
||||
* @param {String} month - 月份
|
||||
* @returns {Number} 净收入金额
|
||||
*/
|
||||
calculateNetIncome (month) {
|
||||
const monthData = this.monthlyDataMap[month] || {};
|
||||
const inCNY = Math.round(monthData.inCNY || 0);
|
||||
const inUSD = Math.round(monthData.inUSD || 0);
|
||||
const outTotal = monthData.outTotal || 0;
|
||||
const exchangeRate = monthData.exchangeRate || 7.2;
|
||||
|
||||
return Math.round(inCNY + inUSD * exchangeRate - outTotal);
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化饼图 - 展示项目支出占比
|
||||
*/
|
||||
initPieChart () {
|
||||
// 过滤出有支出的项目
|
||||
const pieData = this.projectList
|
||||
.filter(project => project.totalPrice > 0)
|
||||
.map(project => ({
|
||||
name: project.projectName,
|
||||
value: project.totalPrice,
|
||||
itemStyle: { color: project.color }
|
||||
}));
|
||||
|
||||
const pieChart = echarts.init(document.getElementById('pie-chart'));
|
||||
pieChart.setOption({
|
||||
title: { text: `项目支出占比(${this.monthForSummary})`, left: 'center' },
|
||||
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||||
series: [{
|
||||
name: '支出',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: pieData
|
||||
}]
|
||||
});
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
/**
|
||||
* 数字格式化过滤器
|
||||
* @param {Number} value - 需要格式化的数字
|
||||
* @returns {String} 格式化后的数字字符串
|
||||
*/
|
||||
numFormat (value) {
|
||||
return new Intl.NumberFormat('zh-CN').format(value.toFixed(2));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 可在此处添加自定义样式 */
|
||||
</style>
|
||||
118
ruoyi-ui/src/views/oa/finance/costing/components/rubbish.vue
Normal file
118
ruoyi-ui/src/views/oa/finance/costing/components/rubbish.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="display: flex; gap: 20px;">
|
||||
<el-select v-model="queryParams.signingCompany" placeholder="请选择签约公司" @change="getList">
|
||||
<el-option :value="undefined" label="全部" :key="all" />
|
||||
<el-option v-for="dict in dict.type.signing_company" :key="dict.value" :label="dict.label"
|
||||
:value="dict.value"></el-option>
|
||||
<el-option value="100" label="王瑞春账户" :key="100" />
|
||||
</el-select>
|
||||
|
||||
<el-button type="primary" @click="handleClick">新增</el-button>
|
||||
<el-button type="primary" @click="getList">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="出账名称" align="left" prop="financeTitle" />
|
||||
<el-table-column label="经手人" align="center" prop="financeParties" />
|
||||
<el-table-column label="签约公司" align="center" prop="signingCompany">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.signingCompany == '100'"">王瑞春账户</span>
|
||||
<dict-tag v-else :options="dict.type.signing_company" :value="scope.row.signingCompany" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总金额" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{'¥' + scope.row.detailList.reduce((acc, curr) => acc + Number(curr.price), 0)}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付类型" align="center" prop="payType" width="80">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.sys_pay_type" :value="scope.row.payType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="交易日期" align="center" prop="financeTime" width="120">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.financeTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建者" align="center" prop="createBy" width="80" />
|
||||
<el-table-column label="开票状态" align="center" prop="makeTime" width="80">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.makeTime == null || (scope.row.makePrice == null || scope.row.makePrice == '')"
|
||||
style="color: #cccccc">未开票</span>
|
||||
<span v-else style="color: #1ab394">已开票</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="120">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.remark == null || scope.row.remark == '' ? "暂无" : scope.row.remark }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="180" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-view" @click="handleLook(scope.row)">查看
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="financeUpdate(scope.row)">修改
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listFinance } from "@/api/oa/finance";
|
||||
|
||||
export default {
|
||||
dicts: ['sys_pay_type', 'signing_company'],
|
||||
data () {
|
||||
return {
|
||||
list: [],
|
||||
loading: false,
|
||||
total: 0,
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
projectId: 0,
|
||||
financeType: 0,
|
||||
signingCompany: undefined, // 默认选中全部
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getList () {
|
||||
this.loading = true;
|
||||
listFinance(this.queryParams).then(res => {
|
||||
this.list = res.rows;
|
||||
this.total = res.total;
|
||||
this.loading = false;
|
||||
})
|
||||
},
|
||||
handleLook (row) {
|
||||
this.$emit('look', row);
|
||||
},
|
||||
financeUpdate (row) {
|
||||
this.$emit('update', row);
|
||||
},
|
||||
handleDelete (row) {
|
||||
this.$emit('delete', row);
|
||||
},
|
||||
handleClick () {
|
||||
this.$emit('add', { signingCompany: this.queryParams.signingCompany });
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.getList();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
1204
ruoyi-ui/src/views/oa/finance/costing/list.vue
Normal file
1204
ruoyi-ui/src/views/oa/finance/costing/list.vue
Normal file
File diff suppressed because it is too large
Load Diff
524
ruoyi-ui/src/views/oa/finance/costing/other.vue
Normal file
524
ruoyi-ui/src/views/oa/finance/costing/other.vue
Normal file
@@ -0,0 +1,524 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div style="display: flex; gap: 20px;">
|
||||
<el-select v-model="queryParams.signingCompany" placeholder="请选择签约公司" @change="getList">
|
||||
<el-option :value="undefined" label="全部" key="all" />
|
||||
<el-option v-for="dict in dict.type.signing_company" :key="dict.value" :label="dict.label"
|
||||
:value="dict.value"></el-option>
|
||||
<el-option value="100" label="王瑞春账户" :key="100" />
|
||||
</el-select>
|
||||
|
||||
<el-button type="primary" @click="handleClick">新增</el-button>
|
||||
<el-button type="primary" @click="getList">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="出账名称" align="left" prop="financeTitle" />
|
||||
<el-table-column label="经手人" align="center" prop="financeParties" />
|
||||
<el-table-column label="签约公司" align="center" prop="signingCompany">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.signingCompany == '100'"">王瑞春账户</span>
|
||||
<dict-tag v-else :options="dict.type.signing_company" :value="scope.row.signingCompany" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支出类型" align="center" prop="outType">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.oa_out_finance" :value="scope.row.outType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总金额" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{'¥' + scope.row.detailList.reduce((acc, curr) => acc + Number(curr.price), 0)}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付类型" align="center" prop="payType" width="80">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.sys_pay_type" :value="scope.row.payType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="交易日期" align="center" prop="financeTime" width="120">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.financeTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建者" align="center" prop="createBy" width="80" />
|
||||
<el-table-column label="开票状态" align="center" prop="makeTime" width="80">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.makeTime == null || (scope.row.makePrice == null || scope.row.makePrice == '')"
|
||||
style="color: #cccccc">未开票</span>
|
||||
<span v-else style="color: #1ab394">已开票</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="120">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.remark == null || scope.row.remark == '' ? "暂无" : scope.row.remark }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="180" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-view" @click="handleLook(scope.row)">查看
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="financeUpdate(scope.row)">修改
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" />
|
||||
|
||||
<!-- 出入账提交信息弹出层 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="76%" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" v-loading="formLoading" label-width="120px">
|
||||
<el-row>
|
||||
<el-col :span="16">
|
||||
<el-form-item :label="financeType == 1 ? '入账名称' : '出账名称'" prop="financeTitle">
|
||||
<el-input v-model="form.financeTitle" placeholder="请输入账务名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="交易时间" prop="financeTime">
|
||||
<el-date-picker clearable v-model="form.financeTime" type="date" value-format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="请选择交易时间">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-form-item :label="financeType == 1 ? '付款方' : '经手人'" prop="financeParties">
|
||||
<el-input v-model="form.financeParties" placeholder="请输入经手人/付款方" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="支付类型" prop="payType">
|
||||
<el-select v-model="form.payType" placeholder="请选择支付类型">
|
||||
<el-option v-for="dict in dict.type.sys_pay_type" :key="dict.value" :label="dict.label"
|
||||
:value="dict.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="签约公司" prop="signingCompany">
|
||||
<el-select v-model="form.signingCompany" placeholder="请选择签约公司">
|
||||
<el-option v-for="dict in dict.type.signing_company" :key="dict.value" :label="dict.label"
|
||||
:value="dict.value"></el-option>
|
||||
<el-option value="100" label="王瑞春账户" :key="100" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="支出用途" prop="costCategory">
|
||||
<el-select v-model="form.outType" placeholder="请选择支出用途">
|
||||
<el-option v-for="dict in dict.type.oa_out_finance" :key="dict.value" :label="dict.label"
|
||||
:value="dict.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="开票比例" prop="makeRatio">
|
||||
<el-input v-model="form.makeRatio" placeholder="请输入开票比例" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="开票金额" prop="makePrice">
|
||||
<el-input v-model="form.makePrice" placeholder="请输入开票金额" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="开票时间" prop="makeTime">
|
||||
<el-input v-model="form.makeTime" placeholder="请输入开票金额" />
|
||||
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="开票情况说明" prop="makeExplain">
|
||||
<el-input v-model="form.makeExplain" placeholder="请输入开票情况说明" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="附件" prop="accessory">
|
||||
<file-upload v-model="form.accessory" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider content-position="center">明细</el-divider>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" icon="el-icon-plus" size="mini" plain @click="handleAddDetailList">添加明细
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" icon="el-icon-delete" size="mini" plain @click="handleDeleteDetailList">删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-table :data="detailList" :row-class-name="rowDetailListIndex"
|
||||
@selection-change="handleDetailListSelectionChange" ref="detailList">
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
<el-table-column label="序号" align="center" prop="index" width="50" />
|
||||
<el-table-column label="明细名称" prop="detailTitle">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.detailTitle" placeholder="请输入名称" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额(单位:元)" prop="price">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.price" placeholder="请输入金额" οnkeyup="value=value.replace(/[^\d]/g,'')"
|
||||
@input="updateBigPrice(scope.$index, scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="大写金额(零壹贰叁肆伍陆柒捌玖万仟佰拾亿元角分)" prop="bigPrice">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.bigPrice" placeholder="请输入大写金额" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button v-loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!--查看账目明细-->
|
||||
<el-dialog :title="title" :visible.sync="openLook" width="60%" append-to-body>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item :label="type == 1 ? '入账名称' : '出账名称'" :span="3" :labelStyle="lableBg">
|
||||
{{ form.financeTitle }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="交易时间" :span="1" :labelStyle="lableBg">
|
||||
{{ parseTime(form.financeTime, '{y}-{m}-{d}') }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="type == 1 ? '付款方' : '经手人'" :span="1" :labelStyle="lableBg">
|
||||
{{ form.financeParties }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="支付类型" :span="1" :labelStyle="lableBg">
|
||||
<dict-tag :options="dict.type.sys_pay_type" :value="form.payType" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="开票状态" :span="2" :labelStyle="lableBg">
|
||||
<span v-if="form.makeTime == null || form.makePrice == null" style="color: #cccccc">未开票</span>
|
||||
<span v-else style="color: #1ab394">
|
||||
已开票, 开票金额:¥ {{ form.makePrice }},
|
||||
开票比例:{{ form.makeRatio == null ? '未填' : form.makeRatio }},
|
||||
开票时间:{{ form.makeTime }},
|
||||
开票情况说明:{{ form.makeExplain == null ? '未填' : form.makeExplain }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建者" :span="1" :labelStyle="lableBg">
|
||||
{{ form.createBy }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="附件" :span="3" :labelStyle="lableBg">
|
||||
<!--附件-->
|
||||
<template>
|
||||
<file-preview v-model="form.accessory" />
|
||||
</template>
|
||||
|
||||
<!--附件-->
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-table :data="detailList" style="margin-top: 30px;">
|
||||
<el-table-column label="明细名称" prop="detailTitle">
|
||||
<template slot-scope="scope">
|
||||
<div>{{ scope.row.detailTitle }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额(单位:元)" prop="price">
|
||||
<template slot-scope="scope">
|
||||
<div>¥{{ scope.row.price }}元</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="大写金额" prop="bigPrice">
|
||||
<template slot-scope="scope">
|
||||
<div>{{ scope.row.bigPrice }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column label="备注" prop="remark" >
|
||||
<template slot-scope="scope">
|
||||
<div>{{scope.row.remark}}</div>
|
||||
</template>
|
||||
</el-table-column>-->
|
||||
</el-table>
|
||||
<h2 class="cl3">{{ type == 1 ? '入账合计' : '出账合计' }}:¥{{ priceSum }}元</h2>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addFinance, getFinance, listFinance, updateFinance } from "@/api/oa/finance";
|
||||
import FilePreview from "@/components/FilePreview";
|
||||
import { numberToCNY } from "@/utils/currencyFormatter";
|
||||
|
||||
export default {
|
||||
dicts: ['sys_project_type', 'sys_project_status', 'sys_pay_type', 'signing_company', 'oa_out_finance'],
|
||||
components: {
|
||||
FilePreview,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
list: [],
|
||||
loading: false,
|
||||
total: 0,
|
||||
lableBg: "background: #f0f9eb; width:150px; text-align: center;",
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
projectId: 0,
|
||||
financeType: 0,
|
||||
signingCompany: undefined, // 默认选中全部
|
||||
},
|
||||
priceSum: 0,
|
||||
buttonLoading: false,
|
||||
//入账弹出层标题
|
||||
title: '',
|
||||
//出入账弹出层
|
||||
open: false,
|
||||
type: '',
|
||||
//表单
|
||||
form: {},
|
||||
detailList: [],
|
||||
formLoading: false,
|
||||
financeType: '0', // 0 出账 1 入账
|
||||
// 表单校验
|
||||
rules: {
|
||||
financeTitle: [
|
||||
{ required: true, message: "账务名称不能为空", trigger: "blur" }
|
||||
],
|
||||
financeParties: [
|
||||
{ required: true, message: "经手人/付款方不能为空", trigger: "blur" }
|
||||
],
|
||||
financeType: [
|
||||
{ required: true, message: "进出账类型不能为空", trigger: "blur" }
|
||||
]
|
||||
},
|
||||
openLook: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getList () {
|
||||
this.loading = true;
|
||||
listFinance(this.queryParams).then(res => {
|
||||
this.list = res.rows;
|
||||
this.total = res.total;
|
||||
this.loading = false;
|
||||
})
|
||||
},
|
||||
/** 出入账明细序号 */
|
||||
rowDetailListIndex ({ row, rowIndex }) {
|
||||
row.index = rowIndex + 1;
|
||||
},
|
||||
isDecimal (n, len) {
|
||||
return new RegExp("^\\d+(\\.\\d{1," + len + "})?$").test(n);
|
||||
},
|
||||
/** 复选框选中数据 */
|
||||
handleDetailListSelectionChange (selection) {
|
||||
this.checkedDetailList = selection.map(item => item.index)
|
||||
},
|
||||
updateBigPrice (index, row) {
|
||||
|
||||
if (row.price !== '') {
|
||||
row.bigPrice = numberToCNY(parseFloat(row.price) || 0);
|
||||
} else {
|
||||
row.bigPrice = ''; // 如果价格为空,则大写金额也清空
|
||||
}
|
||||
},
|
||||
/**查看按钮操作**/
|
||||
handleLook (row) {
|
||||
this.type = row.financeType
|
||||
this.loading = true;
|
||||
this.title = "查看账目";
|
||||
this.reset();
|
||||
const financeId = row.financeId || this.ids
|
||||
getFinance(financeId).then(response => {
|
||||
this.loading = false;
|
||||
this.form = response.data;
|
||||
|
||||
this.detailList = response.data.detailList;
|
||||
//核算求和
|
||||
this.detailPriceSum(response.data.detailList);
|
||||
this.openLook = true;
|
||||
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 求和
|
||||
* obj是列表数据
|
||||
* */
|
||||
detailPriceSum (obj) {
|
||||
if (obj.length > 0) {
|
||||
let sum = []
|
||||
obj.forEach((vo, key) => {
|
||||
sum.push(parseFloat(vo.price))
|
||||
})
|
||||
this.priceSum = sum.reduce((accumulator, currentValue) => accumulator + currentValue);
|
||||
}
|
||||
},
|
||||
financeUpdate (row) {
|
||||
/** 入账按钮操作 */
|
||||
this.reset();
|
||||
this.open = true;
|
||||
this.title = "提交出账信息";
|
||||
this.isRubbish = false;
|
||||
this.financeType = '0';
|
||||
this.form = row;
|
||||
this.detailList = row.detailList;
|
||||
if (this.detailList.length == 0) {
|
||||
this.handleAddDetailList()
|
||||
}
|
||||
},
|
||||
/** 出入账明细添加按钮操作 */
|
||||
handleAddDetailList () {
|
||||
let obj = {};
|
||||
obj.detailTitle = "";
|
||||
obj.price = "";
|
||||
obj.remark = "";
|
||||
if (!this.detailList) {
|
||||
this.detailList = [];
|
||||
}
|
||||
console.log(this.detailList)
|
||||
this.detailList.push(obj);
|
||||
},
|
||||
/** 出入账明细删除按钮操作 */
|
||||
handleDeleteDetailList () {
|
||||
if (this.checkedDetailList.length == 0) {
|
||||
this.$modal.msgError("请先选择要删除的出入账明细数据");
|
||||
} else {
|
||||
const detailList = this.detailList;
|
||||
const checkedDetailList = this.checkedDetailList;
|
||||
this.detailList = detailList.filter(function (item) {
|
||||
return checkedDetailList.indexOf(item.index) == -1
|
||||
});
|
||||
}
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete (row) {
|
||||
const financeId = row.financeId;
|
||||
this.$modal.confirm('是否确认删除编号为"' + financeId + '"的数据项?').then(() => {
|
||||
this.loading = true;
|
||||
return delFinance(financeId);
|
||||
}).then(() => {
|
||||
this.getList()
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
handleClick () {
|
||||
this.handleAddRubbish({ signingCompany: this.queryParams.signingCompany })
|
||||
},
|
||||
handleAddRubbish ({ signingCompany }) {
|
||||
this.open = true;
|
||||
this.title = "提交出账信息";
|
||||
this.financeType = '0';
|
||||
this.form = {
|
||||
signingCompany,
|
||||
financeType: '0',
|
||||
financeId: undefined,
|
||||
financeTitle: undefined,
|
||||
financeParties: undefined,
|
||||
payType: undefined,
|
||||
financeTime: undefined,
|
||||
makeTime: undefined,
|
||||
makeRatio: undefined,
|
||||
makePrice: undefined,
|
||||
makeExplain: undefined,
|
||||
accessory: undefined,
|
||||
remark: undefined,
|
||||
createBy: undefined,
|
||||
createTime: undefined,
|
||||
updateBy: undefined,
|
||||
updateTime: undefined,
|
||||
fileList: [],
|
||||
};
|
||||
this.detailList = [];
|
||||
this.handleAddDetailList()
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm () {
|
||||
this.$refs["form"].validate(async valid => {
|
||||
if (valid) {
|
||||
try {
|
||||
this.buttonLoading = true;
|
||||
let arr = this.detailList;
|
||||
let boolValue = []
|
||||
arr.forEach((vo, i) => {
|
||||
let ii = i + 1
|
||||
if (vo.detailTitle == null || vo.detailTitle == undefined || vo.detailTitle == "") {
|
||||
this.$modal.msgWarning("第" + ii + "条明细名称不能为空!");
|
||||
} else if (vo.price == null || vo.price == undefined || vo.price == "") {
|
||||
this.$modal.msgWarning("第" + ii + "条明细金额不能为空,且必须是两位小数点数字!");
|
||||
} else if (vo.bigPrice == null || vo.bigPrice == undefined || vo.bigPrice == "") {
|
||||
this.$modal.msgWarning("第" + ii + "条明细大写金额不能为空!");
|
||||
} else {
|
||||
boolValue.push(1)
|
||||
}
|
||||
})
|
||||
|
||||
if (arr.length == boolValue.length) {
|
||||
this.form.detailList = arr;
|
||||
if (this.form.financeId != null) {
|
||||
const response = await updateFinance(this.form);
|
||||
this.$modal.msgSuccess(response.msg);
|
||||
this.open = false;
|
||||
} else {
|
||||
this.form.financeType = this.financeType;
|
||||
await addFinance(this.form);
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
this.open = false;
|
||||
}
|
||||
this.getList();
|
||||
}
|
||||
} finally {
|
||||
this.buttonLoading = false;
|
||||
}
|
||||
} else {
|
||||
console.log("验证失败")
|
||||
}
|
||||
})
|
||||
},
|
||||
// 取消按钮
|
||||
cancel () {
|
||||
this.open = false;
|
||||
this.reset();
|
||||
},
|
||||
// 表单重置
|
||||
reset () {
|
||||
this.form = {
|
||||
financeId: undefined,
|
||||
projectId: undefined,
|
||||
financeTitle: undefined,
|
||||
financeParties: undefined,
|
||||
payType: undefined,
|
||||
financeType: undefined,
|
||||
financeTime: undefined,
|
||||
makeTime: undefined,
|
||||
makeRatio: undefined,
|
||||
makePrice: undefined,
|
||||
makeExplain: undefined,
|
||||
accessory: undefined,
|
||||
remark: undefined,
|
||||
createBy: undefined,
|
||||
createTime: undefined,
|
||||
updateBy: undefined,
|
||||
updateTime: undefined,
|
||||
fileList: [],
|
||||
signingCompany: undefined
|
||||
};
|
||||
this.detailList = []
|
||||
this.resetForm("form");
|
||||
},
|
||||
},
|
||||
created () {
|
||||
this.getList();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
282
ruoyi-ui/src/views/oa/finance/dashboard/detail.vue
Normal file
282
ruoyi-ui/src/views/oa/finance/dashboard/detail.vue
Normal file
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form-item>
|
||||
<el-form-item label="项目名" prop="projectName">
|
||||
<el-input v-model="queryParams.projectName" placeholder="请输入项目名" clearable
|
||||
@keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 月份选择 -->
|
||||
<el-form-item label="月份" prop="selectedMonth">
|
||||
<el-date-picker v-model="selectedMonth" type="month" format="yyyy-MM" value-format="yyyy-MM"
|
||||
placeholder="选择月份" clearable @change="handleQuery" />
|
||||
</el-form-item>
|
||||
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div>
|
||||
<PieAndBar :selected-month="selectedMonth" />
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="costList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" type="index" />
|
||||
<el-table-column label="项目名" align="center" prop="projectName" />
|
||||
<el-table-column label="人力花费" align="center" prop="userCost">
|
||||
<template slot-scope="scope">
|
||||
<div>{{ formatNumberToWan(scope.row.userCost) }}万元</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="人天计算" align="center" prop="peopleDay" />
|
||||
<el-table-column label="物料花费" align="center" prop="materialCost">
|
||||
<template slot-scope="scope">
|
||||
<div>{{ formatNumberToWan(scope.row.materialCost) }}万元</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="综合成本" align="center" prop="cost">
|
||||
<template slot-scope="scope">
|
||||
<div>
|
||||
{{ formatNumberToWan(Number(scope.row.materialCost) + Number(scope.row.userCost) +
|
||||
Number(scope.row.claimCost)) }}万元
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="项目总款" align="center" prop="funds">
|
||||
<template slot-scope="scope">
|
||||
<div>{{ formatNumberToWan(scope.row.funds) }}万元</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-search" @click="showDetail(scope.row)">查看详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" />
|
||||
|
||||
|
||||
<!-- 详情描述弹窗 -->
|
||||
<el-dialog title="成本详情" :visible.sync="detail" width="950px" append-to-body>
|
||||
|
||||
<el-tabs v-model="activeName" type="card">
|
||||
<el-tab-pane label="物料花费详情" name="material">
|
||||
<el-table v-loading="loading" :data="detailData.materialList">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" type="index" />
|
||||
<el-table-column label="出账名称" align="center" prop="detailTitle" />
|
||||
<el-table-column label="金额" align="center" prop="price">
|
||||
<template slot-scope="scope">
|
||||
<div>-¥{{ scope.row.price }}元</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="经手人" align="center" prop="financeParties" />
|
||||
<el-table-column label="备注" align="center" prop="remark" />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="人力成本" name="user">
|
||||
<el-table :data="detailData.userCostList">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="索引" align="center" type="index" />
|
||||
<el-table-column label="人员名称" align="center" prop="nickName" />
|
||||
<el-table-column label="人员成本" align="center" prop="laborCost">
|
||||
<template slot-scope="scope">
|
||||
<div>{{ scope.row.laborCost }}元</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="人天计算" align="center" prop="attendanceNum" />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="detail = false">关闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCostDetailById, getCostDetailList } from "@/api/oa/finance";
|
||||
import { listProject } from "@/api/oa/project";
|
||||
import { formatNumberToWan } from "@/utils/currencyFormatter";
|
||||
import PieAndBar from "../components/PieAndBar.vue";
|
||||
|
||||
export default {
|
||||
name: "OaOutWarehouse",
|
||||
components: {
|
||||
PieAndBar
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
// 抽屉
|
||||
drawer: false,
|
||||
// 选中项目名称
|
||||
selectedProject: "",
|
||||
selectedMonth: undefined,
|
||||
// 查看详情弹窗
|
||||
outDetail: {},
|
||||
// 弹窗标志
|
||||
detail: false,
|
||||
// 绑定项目详情
|
||||
projectDetail: {},
|
||||
// 物料信息详情
|
||||
warehouseDetail: {},
|
||||
// 出库列表
|
||||
outWareHouseList: [],
|
||||
// 按钮loading
|
||||
buttonLoading: false,
|
||||
// 遮罩层
|
||||
loading: true,
|
||||
// 选中数组
|
||||
ids: [],
|
||||
// 非单个禁用
|
||||
single: true,
|
||||
// 非多个禁用
|
||||
multiple: true,
|
||||
// 显示搜索条件
|
||||
showSearch: true,
|
||||
// 总条数
|
||||
total: 0,
|
||||
// 仓库出库表格数据
|
||||
oaOutWarehouseList: [],
|
||||
// 项目列表
|
||||
projectList: [],
|
||||
// 库存数据
|
||||
oaWarehouseList: [],
|
||||
// 弹出层标题
|
||||
title: "",
|
||||
// 选择对象
|
||||
searchItem: {},
|
||||
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 是否绑定项目
|
||||
projectFlag: false,
|
||||
// 库存查询参数
|
||||
warehouseParams: {
|
||||
pageSize: 999,
|
||||
pageNum: 1
|
||||
},
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10
|
||||
},
|
||||
// 表单参数
|
||||
form: {
|
||||
projectId: ''
|
||||
},
|
||||
// 表单校验
|
||||
rules: {
|
||||
cost: [
|
||||
{ required: true, message: "成本不能为空", trigger: "blur" }
|
||||
],
|
||||
projectId: [
|
||||
{ required: true, message: "项目id不能为空", trigger: "blur" }
|
||||
],
|
||||
},
|
||||
costList: [],
|
||||
detailData: {},
|
||||
activeName: 'claim'
|
||||
};
|
||||
},
|
||||
created () {
|
||||
this.getList();
|
||||
},
|
||||
methods: {
|
||||
formatNumberToWan,
|
||||
/** 查询仓库出库列表 */
|
||||
getList () {
|
||||
this.loading = true;
|
||||
|
||||
getCostDetailList(this.queryParams).then(res => {
|
||||
console.log(res)
|
||||
this.costList = res.rows
|
||||
this.total = res.total
|
||||
this.loading = false
|
||||
})
|
||||
|
||||
listProject(this.queryParams).then(response => {
|
||||
this.projectList = response.rows
|
||||
});
|
||||
},
|
||||
// 取消按钮
|
||||
cancel () {
|
||||
this.open = false;
|
||||
this.reset();
|
||||
},
|
||||
// 表单重置
|
||||
reset () {
|
||||
this.form = {
|
||||
id: undefined,
|
||||
projectId: undefined,
|
||||
amount: undefined,
|
||||
remark: undefined,
|
||||
warehouseId: undefined,
|
||||
createTime: undefined,
|
||||
createBy: undefined,
|
||||
updateTime: undefined,
|
||||
updateBy: undefined,
|
||||
delFlag: undefined,
|
||||
selectLoading: false,
|
||||
outWareHouseList: []
|
||||
};
|
||||
this.resetForm("form");
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery () {
|
||||
this.queryParams.pageNum = 1;
|
||||
this.getList();
|
||||
},
|
||||
/** 重置按钮操作 */
|
||||
resetQuery () {
|
||||
this.resetForm("queryForm");
|
||||
this.handleQuery();
|
||||
},
|
||||
// 多选框选中数据
|
||||
handleSelectionChange (selection) {
|
||||
this.ids = selection.map(item => item.id)
|
||||
this.single = selection.length !== 1
|
||||
this.multiple = !selection.length
|
||||
},
|
||||
/** 新增按钮操作, 打开新增弹窗 */
|
||||
handleAdd () {
|
||||
this.reset();
|
||||
this.open = true;
|
||||
if (this.drawer) {
|
||||
// 如果抽屉是打开的说明是从项目处进入的新增,从而加入projectId
|
||||
this.projectFlag = true;
|
||||
this.form.projectId = this.selectedProject.projectId;
|
||||
console.log(this.form);
|
||||
}
|
||||
this.title = "新增项目成本";
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate (row) {
|
||||
this.loading = true;
|
||||
this.reset();
|
||||
const id = row.id || this.ids
|
||||
// getOaOutWarehouse(id).then(response => {
|
||||
// this.loading = false;
|
||||
// this.form = response.data;
|
||||
// this.open = true;
|
||||
// this.title = "修改仓库出库";
|
||||
// });
|
||||
},
|
||||
// 查看成本单独条目详情
|
||||
showDetail (row) {
|
||||
getCostDetailById(row.projectId).then(response => {
|
||||
this.detailData = response.data;
|
||||
this.detail = true;
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
699
ruoyi-ui/src/views/oa/finance/dashboard/other.vue
Normal file
699
ruoyi-ui/src/views/oa/finance/dashboard/other.vue
Normal file
@@ -0,0 +1,699 @@
|
||||
<template>
|
||||
<!-- 模板结构不变,仅调整样式和图表配置 -->
|
||||
<div class="app-container">
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" :model="filterForm" class="filter-form">
|
||||
<el-form-item label="选择月份:">
|
||||
<el-date-picker v-model="selectedMonth" type="month" placeholder="选择月份" format="yyyy-MM"
|
||||
value-format="yyyy-MM" @change="handleMonthChange"></el-date-picker>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 指标卡区域 -->
|
||||
<div class="summary-card-section">
|
||||
<el-card class="summary-card">
|
||||
<div class="summary-item">
|
||||
<span class="label">总支出</span>
|
||||
<span class="value">{{ summaryData.totalOut || 0.00 }}</span>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="summary-card">
|
||||
<div class="summary-item">
|
||||
<span class="label">平均单笔支出</span>
|
||||
<span class="value">{{ summaryData.averageOut || 0.00 }}</span>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="summary-card">
|
||||
<div class="summary-item">
|
||||
<span class="label">支出最多用途</span>
|
||||
<span class="value">{{ summaryData.topOutType || '无数据' }}</span>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="summary-card">
|
||||
<div class="summary-item">
|
||||
<span class="label">记录总数</span>
|
||||
<span class="value">{{ summaryData.totalCount || 0 }}</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- ECharts图表区域 -->
|
||||
<div class="chart-section">
|
||||
<div class="chart-item">
|
||||
<div class="chart-title">近六个月支出分析</div>
|
||||
<div id="monthlyChart" class="chart-container"></div>
|
||||
</div>
|
||||
<div class="chart-item">
|
||||
<div class="chart-title">按签约公司汇总</div>
|
||||
<div id="companyChart" class="chart-container"></div>
|
||||
</div>
|
||||
<div class="chart-item">
|
||||
<div class="chart-title">按用途汇总</div>
|
||||
<div id="outTypeChart" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 明细表格 -->
|
||||
<div class="table-section">
|
||||
<el-table :data="tableData" border stripe style="width: 100%" :loading="tableLoading">
|
||||
<el-table-column prop="signingCompany" label="签约公司" align="center" min-width="150"></el-table-column>
|
||||
<el-table-column prop="outType" label="支出用途" align="center" min-width="150"></el-table-column>
|
||||
<el-table-column prop="totalPrice" label="总金额(元)" align="center" min-width="120">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.totalPrice.toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="detailCount" label="明细条数" align="center" min-width="100"></el-table-column>
|
||||
<el-table-column label="操作" align="center" min-width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="small" @click="viewDetail(scope.row)">
|
||||
查看明细
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 明细弹窗 -->
|
||||
<el-dialog title="收支明细" :visible.sync="detailDialogVisible" width="60%" append-to-body>
|
||||
<el-table :data="currentDetailList" border stripe style="width: 100%">
|
||||
<el-table-column prop="detailTitle" label="明细标题" align="center"></el-table-column>
|
||||
<el-table-column prop="price" label="金额(元)" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.price }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="bigPrice" label="大写金额" align="center"></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" align="center"></el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { barData, listFinance } from "@/api/oa/finance";
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: 'FinanceAnalysis',
|
||||
data () {
|
||||
return {
|
||||
selectedMonth: '',
|
||||
filterForm: {},
|
||||
financeList: [],
|
||||
barChartData: [],
|
||||
summaryData: {
|
||||
totalOut: 0.00,
|
||||
averageOut: 0.00,
|
||||
topOutType: '',
|
||||
totalCount: 0
|
||||
},
|
||||
tableData: [],
|
||||
tableLoading: false,
|
||||
detailDialogVisible: false,
|
||||
currentDetailList: [],
|
||||
monthlyChart: null,
|
||||
companyChart: null,
|
||||
outTypeChart: null,
|
||||
signingCompanyDict: {},
|
||||
outTypeDict: {}
|
||||
}
|
||||
},
|
||||
dicts: ['signing_company'],
|
||||
mounted () {
|
||||
this.selectedMonth = this.formatCurrentMonth();
|
||||
this.loadDicts();
|
||||
this.initData();
|
||||
this.initCharts();
|
||||
},
|
||||
beforeDestroy () {
|
||||
if (this.monthlyChart) this.monthlyChart.dispose();
|
||||
if (this.companyChart) this.companyChart.dispose();
|
||||
if (this.outTypeChart) this.outTypeChart.dispose();
|
||||
},
|
||||
methods: {
|
||||
loadDicts () {
|
||||
this.$nextTick(() => {
|
||||
this.signingCompanyDict = this.getDictMap('signing_company');
|
||||
this.outTypeDict = this.getDictMap('out_type');
|
||||
});
|
||||
},
|
||||
convertDictToMap (dictList) {
|
||||
return dictList.reduce((map, item) => {
|
||||
map[item.value] = item.label;
|
||||
return map;
|
||||
}, {});
|
||||
},
|
||||
getDictMap (dictType) {
|
||||
const dictList = this.dict.type[dictType] || [];
|
||||
return this.convertDictToMap(dictList);
|
||||
},
|
||||
formatCurrentMonth () {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = (now.getMonth() + 1).toString().padStart(2, '0');
|
||||
return `${year}-${month}`;
|
||||
},
|
||||
getMonthRange (month) {
|
||||
if (!month) return { beginTime: '', endTime: '' };
|
||||
const [year, monthNum] = month.split('-').map(Number);
|
||||
const beginDate = new Date(year, monthNum - 1, 1);
|
||||
const endDate = new Date(year, monthNum, 0);
|
||||
const formatDate = (date) => {
|
||||
const y = date.getFullYear();
|
||||
const m = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const d = date.getDate().toString().padStart(2, '0');
|
||||
return `${y}-${m}-${d} ${date === beginDate ? '00:00:00' : '23:59:59'}`;
|
||||
};
|
||||
return {
|
||||
beginTime: formatDate(beginDate),
|
||||
endTime: formatDate(endDate)
|
||||
};
|
||||
},
|
||||
initData () {
|
||||
this.getFinanceList();
|
||||
this.getBarData();
|
||||
},
|
||||
getFinanceList () {
|
||||
this.tableLoading = true;
|
||||
const { beginTime, endTime } = this.getMonthRange(this.selectedMonth);
|
||||
listFinance({
|
||||
pageSize: 9999,
|
||||
beginCreateTime: beginTime,
|
||||
endCreateTime: endTime,
|
||||
projectId: 0
|
||||
}).then(res => {
|
||||
this.financeList = res.rows || [];
|
||||
this.handleSummaryData();
|
||||
this.handleTableData();
|
||||
this.$nextTick(() => {
|
||||
this.updateCompanyChart();
|
||||
this.updateOutTypeChart();
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('获取财务列表失败:', err);
|
||||
this.financeList = [];
|
||||
}).finally(() => {
|
||||
this.tableLoading = false;
|
||||
});
|
||||
},
|
||||
getBarData () {
|
||||
barData({
|
||||
pageSize: 9999
|
||||
}).then(res => {
|
||||
this.barChartData = res.data || [];
|
||||
this.updateMonthlyChart();
|
||||
}).catch(err => {
|
||||
console.error('获取月度收支数据失败:', err);
|
||||
this.barChartData = [];
|
||||
});
|
||||
},
|
||||
handleSummaryData () {
|
||||
if (!this.financeList.length) {
|
||||
this.summaryData = {
|
||||
totalOut: 0.00,
|
||||
averageOut: 0.00,
|
||||
topOutType: '无数据',
|
||||
totalCount: 0
|
||||
};
|
||||
return;
|
||||
}
|
||||
let totalOut = 0;
|
||||
let totalDetailCount = 0;
|
||||
const outTypeCountMap = {};
|
||||
this.financeList.forEach(item => {
|
||||
if (item.detailList && item.detailList.length) {
|
||||
item.detailList.forEach(detail => {
|
||||
totalOut += Number(detail.price) || 0;
|
||||
});
|
||||
totalDetailCount += item.detailList.length;
|
||||
const outTypeKey = item.outType || '未知用途';
|
||||
const typeTotal = item.detailList.reduce((sum, detail) => sum + Number(detail.price), 0);
|
||||
outTypeCountMap[outTypeKey] = (outTypeCountMap[outTypeKey] || 0) + typeTotal;
|
||||
}
|
||||
});
|
||||
const averageOut = totalDetailCount > 0 ? (totalOut / totalDetailCount).toFixed(2) : 0.00;
|
||||
let topOutTypeKey = '未知用途';
|
||||
let maxAmount = 0;
|
||||
Object.keys(outTypeCountMap).forEach(key => {
|
||||
if (outTypeCountMap[key] > maxAmount) {
|
||||
maxAmount = outTypeCountMap[key];
|
||||
topOutTypeKey = key;
|
||||
}
|
||||
});
|
||||
const topOutType = this.outTypeDict[topOutTypeKey] || topOutTypeKey;
|
||||
this.summaryData = {
|
||||
totalOut: totalOut.toFixed(2),
|
||||
averageOut: averageOut,
|
||||
topOutType: topOutType,
|
||||
totalCount: this.financeList.length
|
||||
};
|
||||
},
|
||||
handleTableData () {
|
||||
const groupMap = new Map();
|
||||
this.financeList.forEach(item => {
|
||||
const key = `${item.signingCompany || '未知公司'}_${item.outType || '未知用途'}`;
|
||||
const totalPrice = item.detailList.reduce((sum, detail) => sum + Number(detail.price), 0);
|
||||
if (groupMap.has(key)) {
|
||||
const existItem = groupMap.get(key);
|
||||
existItem.totalPrice += totalPrice;
|
||||
existItem.detailCount += item.detailList.length;
|
||||
} else {
|
||||
groupMap.set(key, {
|
||||
signingCompany: item.signingCompany || '未知公司',
|
||||
outType: item.outType || '未知用途',
|
||||
totalPrice,
|
||||
detailCount: item.detailList.length,
|
||||
originData: [item]
|
||||
});
|
||||
}
|
||||
});
|
||||
this.tableData = Array.from(groupMap.values()).map(item => ({
|
||||
...item,
|
||||
originData: item.originData.flat()
|
||||
}));
|
||||
},
|
||||
initCharts () {
|
||||
this.monthlyChart = echarts.init(document.getElementById('monthlyChart'));
|
||||
this.companyChart = echarts.init(document.getElementById('companyChart'));
|
||||
this.outTypeChart = echarts.init(document.getElementById('outTypeChart'));
|
||||
window.addEventListener('resize', () => {
|
||||
this.monthlyChart?.resize();
|
||||
this.companyChart?.resize();
|
||||
this.outTypeChart?.resize();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 财务风渐变(深蓝/金色为主)
|
||||
*/
|
||||
getFinanceGradient (color1, color2) {
|
||||
return new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: color1 },
|
||||
{ offset: 1, color: color2 }
|
||||
]);
|
||||
},
|
||||
/**
|
||||
* 近六个月支出图表(财务风折线图)
|
||||
*/
|
||||
updateMonthlyChart () {
|
||||
if (!this.barChartData.length) {
|
||||
this.monthlyChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
title: {
|
||||
text: '暂无数据',
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
textStyle: { color: '#333', fontSize: 14, fontWeight: 500 }
|
||||
},
|
||||
series: []
|
||||
});
|
||||
return;
|
||||
}
|
||||
const months = this.barChartData.map(item => item.month);
|
||||
const totalOut = this.barChartData.map(item => Number(item.totalOut));
|
||||
// 财务风配色:深蓝主色,金色辅助
|
||||
const mainColor = '#1e40af'; // 深蓝
|
||||
const accentColor = '#b45309'; // 深金
|
||||
const gridColor = '#e5e7eb'; // 浅灰网格
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
lineStyle: { color: accentColor, width: 1, type: 'dashed' }
|
||||
},
|
||||
formatter: '{b}:{c}元',
|
||||
backgroundColor: '#fff',
|
||||
borderColor: mainColor,
|
||||
borderWidth: 1,
|
||||
textStyle: { color: '#333', fontSize: 12 },
|
||||
padding: [8, 12],
|
||||
borderRadius: 2,
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
||||
},
|
||||
grid: {
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
bottom: '10%',
|
||||
top: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: months,
|
||||
axisLabel: {
|
||||
fontSize: 12,
|
||||
color: '#333'
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: { color: '#666', width: 1 }
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
length: 5,
|
||||
lineStyle: { color: '#666' }
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '金额(元)',
|
||||
nameTextStyle: { color: '#333', fontSize: 12 },
|
||||
axisLabel: {
|
||||
formatter: '{value}',
|
||||
color: '#333',
|
||||
fontSize: 12
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: { color: '#666', width: 1 }
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
length: 5,
|
||||
lineStyle: { color: '#666' }
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: { color: gridColor, width: 1, type: 'solid' }
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '总支出',
|
||||
type: 'line',
|
||||
data: totalOut,
|
||||
lineStyle: {
|
||||
color: mainColor,
|
||||
width: 2.5
|
||||
},
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
itemStyle: {
|
||||
color: mainColor,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1.5
|
||||
},
|
||||
emphasis: {
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
itemStyle: { color: accentColor }
|
||||
},
|
||||
smooth: true,
|
||||
areaStyle: {
|
||||
color: this.getFinanceGradient('rgba(30, 64, 175, 0.1)', 'rgba(30, 64, 175, 0.02)')
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
this.monthlyChart.setOption(option);
|
||||
},
|
||||
/**
|
||||
* 按公司汇总图表(财务风饼图)
|
||||
*/
|
||||
updateCompanyChart () {
|
||||
if (!this.tableData.length) {
|
||||
this.companyChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
title: {
|
||||
text: '暂无数据',
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
textStyle: { color: '#333', fontSize: 14, fontWeight: 500 }
|
||||
},
|
||||
series: []
|
||||
});
|
||||
return;
|
||||
}
|
||||
const companyData = this.tableData.map(item => ({
|
||||
name: this.signingCompanyDict[item.signingCompany] || item.signingCompany || '未知公司',
|
||||
value: item.totalPrice
|
||||
}));
|
||||
// 财务风饼图配色(深蓝/深金/深灰/墨蓝)
|
||||
const financeColors = [
|
||||
'#1e40af', '#b45309', '#1f2937', '#0369a1', '#78350f'
|
||||
];
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c}元 ({d}%)',
|
||||
backgroundColor: '#fff',
|
||||
borderColor: '#1e40af',
|
||||
borderWidth: 1,
|
||||
textStyle: { color: '#333', fontSize: 12 },
|
||||
padding: [8, 12],
|
||||
borderRadius: 2
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '签约公司',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '50%'],
|
||||
data: companyData,
|
||||
color: financeColors,
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}: {c}元',
|
||||
fontSize: 12,
|
||||
color: '#333',
|
||||
fontWeight: 500
|
||||
},
|
||||
labelLine: {
|
||||
length: 15,
|
||||
length2: 10,
|
||||
lineStyle: {
|
||||
color: '#666',
|
||||
width: 1
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0,0,0,0.2)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
this.companyChart.setOption(option);
|
||||
},
|
||||
/**
|
||||
* 按用途汇总图表(财务风饼图)
|
||||
*/
|
||||
updateOutTypeChart () {
|
||||
if (!this.tableData.length) {
|
||||
this.outTypeChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
title: {
|
||||
text: '暂无数据',
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
textStyle: { color: '#333', fontSize: 14, fontWeight: 500 }
|
||||
},
|
||||
series: []
|
||||
});
|
||||
return;
|
||||
}
|
||||
const outTypeData = this.tableData.reduce((acc, item) => {
|
||||
const outTypeKey = item.outType || '未知用途';
|
||||
const outTypeLabel = this.outTypeDict[outTypeKey] || outTypeKey;
|
||||
const exist = acc.find(i => i.name === outTypeLabel);
|
||||
if (exist) {
|
||||
exist.value += item.totalPrice;
|
||||
} else {
|
||||
acc.push({ name: outTypeLabel, value: item.totalPrice });
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
const financeColors = [
|
||||
'#0369a1', '#b45309', '#1e40af', '#1f2937', '#78350f'
|
||||
];
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c}元 ({d}%)',
|
||||
backgroundColor: '#fff',
|
||||
borderColor: '#1e40af',
|
||||
borderWidth: 1,
|
||||
textStyle: { color: '#333', fontSize: 12 },
|
||||
padding: [8, 12],
|
||||
borderRadius: 2
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 10,
|
||||
top: 'center',
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#333',
|
||||
fontWeight: 400
|
||||
},
|
||||
itemWidth: 12,
|
||||
itemHeight: 12,
|
||||
itemGap: 10
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '支出用途',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['60%', '50%'],
|
||||
data: outTypeData,
|
||||
color: financeColors,
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}: {c}元',
|
||||
fontSize: 12,
|
||||
color: '#333',
|
||||
fontWeight: 500
|
||||
},
|
||||
labelLine: {
|
||||
length: 15,
|
||||
length2: 10,
|
||||
lineStyle: {
|
||||
color: '#666',
|
||||
width: 1
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0,0,0,0.2)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
this.outTypeChart.setOption(option);
|
||||
},
|
||||
handleMonthChange () {
|
||||
this.initData();
|
||||
},
|
||||
viewDetail (row) {
|
||||
this.currentDetailList = row.originData.flatMap(item => item.detailList || []);
|
||||
this.detailDialogVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 财务风核心样式:稳重、规整、深蓝/深灰基调 */
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
background: #f9fafb;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
/* 筛选区样式 */
|
||||
.filter-section {
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 指标卡样式(财务报表风格) */
|
||||
.summary-card-section {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.summary-item .label {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.summary-item .value {
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
color: #1e40af;
|
||||
/* 深蓝主色 */
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* 图表区域样式(财务报表容器) */
|
||||
.chart-section {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chart-item {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
background: #fff;
|
||||
padding: 20px 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
color: #1e40af;
|
||||
/* 深蓝标题 */
|
||||
text-align: center;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
/* 表格区域样式(财务明细表格) */
|
||||
.table-section {
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
</style>
|
||||
1616
ruoyi-ui/src/views/oa/finance/index.vue
Normal file
1616
ruoyi-ui/src/views/oa/finance/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<el-tabs type="card" v-loading="loading" editable v-model="editableTabsValue" @edit="handleEdit" @tab-click="handleTabClick">
|
||||
<el-tab-pane v-for="account in accountList" :key="account.receiveAccountId" :label="account.receiveAccountName" :name="account.receiveAccountId.toString()"></el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
addOaReceiveAccount,
|
||||
AllListOaReceiveAccount,
|
||||
delOaReceiveAccount
|
||||
} from "@/api/oa/oaReceiveAccount";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
editableTabsValue: undefined,
|
||||
accountList: [],
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getAccountList();
|
||||
},
|
||||
methods: {
|
||||
getAccountList() {
|
||||
this.loading = true;
|
||||
AllListOaReceiveAccount().then(res => {
|
||||
this.loading = false;
|
||||
this.accountList = res.data.filter(item => item.receiveAccountId != '-1')
|
||||
})
|
||||
},
|
||||
addAccount(data) {
|
||||
addOaReceiveAccount(data).then(res => {
|
||||
this.getAccountList()
|
||||
this.$message.success('添加成功')
|
||||
this.$emit('tab-click', undefined)
|
||||
})
|
||||
},
|
||||
deleteAccount(id) {
|
||||
delOaReceiveAccount(id).then(res => {
|
||||
this.getAccountList()
|
||||
this.$message.success('删除成功')
|
||||
this.$emit('tab-click', undefined)
|
||||
})
|
||||
},
|
||||
handleEdit(targetName, action) {
|
||||
if (action === 'add') {
|
||||
// 弹出弹窗,输入账户名称
|
||||
this.$prompt('请输入账户名称', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
}).then(({ value }) => {
|
||||
console.log(value)
|
||||
this.addAccount({
|
||||
receiveAccountName: value,
|
||||
parentId: '-1',
|
||||
})
|
||||
})
|
||||
} else if (action === 'remove') {
|
||||
// 弹窗二次确认
|
||||
this.$confirm('确定删除该账户吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
}).then(() => {
|
||||
this.deleteAccount(targetName)
|
||||
})
|
||||
}
|
||||
},
|
||||
handleTabClick(tab, event) {
|
||||
console.log(tab.name,)
|
||||
this.$emit('tab-click', tab.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,392 @@
|
||||
<template>
|
||||
<div class="app-container" v-loading="loading">
|
||||
<el-row class="mb8">
|
||||
账户余额: {{ accountBalance }}
|
||||
<el-button type="primary" icon="el-icon-refresh" size="mini" @click="fetchData">刷新</el-button>
|
||||
<el-button type="primary" icon="el-icon-edit" size="mini" @click="handleUpdateAccount">修改账户余额</el-button>
|
||||
</el-row>
|
||||
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form-item label="对方科目" prop="oppositeSubject">
|
||||
<el-input
|
||||
v-model="queryParams.oppositeSubject"
|
||||
placeholder="请输入对方科目"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务摘要" prop="summary">
|
||||
<el-input
|
||||
v-model="queryParams.summary"
|
||||
placeholder="请输入业务摘要"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
@click="handleAdd"
|
||||
>新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
icon="el-icon-edit"
|
||||
size="mini"
|
||||
:disabled="single"
|
||||
@click="handleUpdate"
|
||||
>修改</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
:disabled="multiple"
|
||||
@click="handleDelete"
|
||||
>删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="el-icon-download"
|
||||
size="mini"
|
||||
@click="handleExport"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
余额计算<el-switch v-model="showBalance"/>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="journalAccountList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="日记账记录ID" align="center" prop="journalId" v-if="false"/>
|
||||
<el-table-column label="记账日期" align="center" prop="recordDate" width="180">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.recordDate, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="对方科目" align="center" prop="oppositeSubject" />
|
||||
<el-table-column label="业务摘要" align="center" prop="summary" />
|
||||
<el-table-column label="收入金额" align="center" prop="incomeAmount" />
|
||||
<el-table-column label="支出金额" align="center" prop="expendAmount" />
|
||||
<el-table-column label="账户余额" align="center" prop="balance" v-if="showBalance" />
|
||||
<el-table-column label="备注" align="center" prop="remark" />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
>修改</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<!-- 添加或修改日记账(绑定封账批次,未封账数据batch_id为NULL)对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="记账日期" prop="recordDate">
|
||||
<el-date-picker clearable
|
||||
v-model="form.recordDate"
|
||||
type="datetime"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="请选择记账日期">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="对方科目" prop="oppositeSubject">
|
||||
<el-input v-model="form.oppositeSubject" placeholder="请输入对方科目" />
|
||||
</el-form-item>
|
||||
<el-form-item label="业务摘要" prop="summary">
|
||||
<el-input v-model="form.summary" type="textarea" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
<el-form-item label="收入金额" prop="incomeAmount">
|
||||
<el-input v-model="form.incomeAmount" placeholder="请输入收入金额" />
|
||||
</el-form-item>
|
||||
<el-form-item label="支出金额" prop="expendAmount">
|
||||
<el-input v-model="form.expendAmount" placeholder="请输入支出金额" />
|
||||
</el-form-item>
|
||||
<el-form-item label="账户余额" prop="balance">
|
||||
<el-input v-model="form.balance" placeholder="请输入账户余额" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog title="修改账户" :visible.sync="openAccount" width="500px" append-to-body>
|
||||
<el-form ref="formAccount" :model="formAccount" label-width="80px">
|
||||
<el-form-item label="账户余额" prop="balance">
|
||||
<el-input v-model="formAccount.balance" placeholder="请输入账户余额" />
|
||||
</el-form-item>
|
||||
<el-form-item label="账户名称" prop="balance">
|
||||
<el-input v-model="formAccount.receiveAccountName" placeholder="请输入账户余额" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitFormAccount">确 定</el-button>
|
||||
<el-button @click="openAccount = false">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addJournalAccount, delJournalAccount, getJournalAccount, listJournalAccount, updateJournalAccount } from "@/api/oa/finance/journalAccount";
|
||||
import {
|
||||
getOaReceiveAccount,
|
||||
updateOaReceiveAccount
|
||||
} from "@/api/oa/oaReceiveAccount";
|
||||
|
||||
export default {
|
||||
name: "JournalAccount",
|
||||
props: {
|
||||
accountId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 按钮loading
|
||||
buttonLoading: false,
|
||||
// 遮罩层
|
||||
loading: true,
|
||||
// 选中数组
|
||||
ids: [],
|
||||
// 非单个禁用
|
||||
single: true,
|
||||
// 非多个禁用
|
||||
multiple: true,
|
||||
// 显示搜索条件
|
||||
showSearch: true,
|
||||
// 总条数
|
||||
total: 0,
|
||||
// 日记账(绑定封账批次,未封账数据batch_id为NULL)表格数据
|
||||
journalAccountList: [],
|
||||
// 弹出层标题
|
||||
title: "",
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
accountId: this.accountId,
|
||||
recordDate: undefined,
|
||||
oppositeSubject: undefined,
|
||||
summary: undefined,
|
||||
incomeAmount: undefined,
|
||||
expendAmount: undefined,
|
||||
balance: undefined,
|
||||
batchId: undefined,
|
||||
},
|
||||
accountBalance: 0,
|
||||
showBalance: false,
|
||||
// 表单参数
|
||||
form: {},
|
||||
// 表单校验
|
||||
rules: {
|
||||
},
|
||||
openAccount: false,
|
||||
formAccount: {},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchData();
|
||||
},
|
||||
watch: {
|
||||
accountId: {
|
||||
handler(newVal) {
|
||||
this.queryParams.accountId = newVal;
|
||||
this.showBalance = false;
|
||||
this.fetchData();
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 查询日记账(绑定封账批次,未封账数据batch_id为NULL)列表 */
|
||||
fetchData() {
|
||||
this.loading = true;
|
||||
Promise.all([
|
||||
this.getAccountBalance(),
|
||||
this.getList()
|
||||
]).then(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
async getList() {
|
||||
const response = await listJournalAccount(this.queryParams);
|
||||
this.journalAccountList = response.rows;
|
||||
this.total = response.total;
|
||||
},
|
||||
async getAccountBalance() {
|
||||
const response = await getOaReceiveAccount(this.accountId);
|
||||
this.accountBalance = response.data.balance;
|
||||
this.formAccount = response.data
|
||||
},
|
||||
// 取消按钮
|
||||
cancel() {
|
||||
this.open = false;
|
||||
this.reset();
|
||||
},
|
||||
// 表单重置
|
||||
reset() {
|
||||
console.log(this.accountId)
|
||||
this.form = {
|
||||
journalId: undefined,
|
||||
accountId: this.accountId,
|
||||
recordDate: undefined,
|
||||
oppositeSubject: undefined,
|
||||
summary: undefined,
|
||||
incomeAmount: undefined,
|
||||
expendAmount: undefined,
|
||||
balance: undefined,
|
||||
batchId: undefined,
|
||||
remark: undefined,
|
||||
createTime: undefined,
|
||||
createBy: undefined,
|
||||
updateTime: undefined,
|
||||
updateBy: undefined,
|
||||
delFlag: undefined
|
||||
};
|
||||
this.resetForm("form");
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1;
|
||||
this.getList();
|
||||
},
|
||||
/** 重置按钮操作 */
|
||||
resetQuery() {
|
||||
this.resetForm("queryForm");
|
||||
this.handleQuery();
|
||||
},
|
||||
// 多选框选中数据
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.journalId)
|
||||
this.single = selection.length!==1
|
||||
this.multiple = !selection.length
|
||||
},
|
||||
/** 新增按钮操作 */
|
||||
handleAdd() {
|
||||
this.reset();
|
||||
this.open = true;
|
||||
this.title = "添加日记账";
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate(row) {
|
||||
this.loading = true;
|
||||
this.reset();
|
||||
const journalId = row.journalId || this.ids
|
||||
getJournalAccount(journalId).then(response => {
|
||||
this.loading = false;
|
||||
this.form = response.data;
|
||||
this.open = true;
|
||||
this.title = "修改日记账";
|
||||
});
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
this.buttonLoading = true;
|
||||
if (this.form.journalId != null) {
|
||||
updateJournalAccount(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.open = false;
|
||||
this.fetchData();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
} else {
|
||||
addJournalAccount(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
this.open = false;
|
||||
this.fetchData();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
const journalIds = row.journalId || this.ids;
|
||||
this.$modal.confirm('是否确认删除日记账编号为"' + journalIds + '"的数据项?').then(() => {
|
||||
this.loading = true;
|
||||
return delJournalAccount(journalIds);
|
||||
}).then(() => {
|
||||
this.loading = false;
|
||||
this.fetchData();
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
/** 导出按钮操作 */
|
||||
handleExport() {
|
||||
this.download('oa/journalAccount/export', {
|
||||
...this.queryParams
|
||||
}, `journalAccount_${new Date().getTime()}.xlsx`)
|
||||
},
|
||||
handleUpdateAccount() {
|
||||
this.openAccount = true;
|
||||
// this.form.accountBalance = this.accountBalance;
|
||||
},
|
||||
submitFormAccount() {
|
||||
this.$refs["formAccount"].validate(valid => {
|
||||
if (valid) {
|
||||
this.buttonLoading = true;
|
||||
updateOaReceiveAccount(this.formAccount).then(response => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.openAccount = false;
|
||||
this.fetchData();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
49
ruoyi-ui/src/views/oa/finance/journal/index.vue
Normal file
49
ruoyi-ui/src/views/oa/finance/journal/index.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div style="padding: 20px;">
|
||||
<el-row>
|
||||
<AccountTab @tab-click="handleTabClick" />
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="10" v-if="accountId">
|
||||
<el-col :span="22">
|
||||
<!-- 账户信息 -->
|
||||
<JournalTable :accountId="accountId" />
|
||||
</el-col>
|
||||
<el-col :span="2">
|
||||
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row v-else>
|
||||
请选择账户查看日记账
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AccountTab from './components/AccountTab.vue';
|
||||
import JournalTable from './components/JournalTable.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AccountTab,
|
||||
JournalTable,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editableTabsValue: undefined,
|
||||
accountList: [],
|
||||
accountId: undefined,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
handleTabClick(accountId) {
|
||||
// console.log(accountId)
|
||||
this.accountId = accountId;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
431
ruoyi-ui/src/views/oa/finance/profit/index.vue
Normal file
431
ruoyi-ui/src/views/oa/finance/profit/index.vue
Normal file
@@ -0,0 +1,431 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 查询表单 -->
|
||||
<el-form :inline="true" :model="queryParams" class="demo-form-inline" label-width="100px">
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-form-item label="排序字段">
|
||||
<el-select v-model="queryParams.sortField" placeholder="请选择" clearable @change="handleQuery">
|
||||
<el-option label="盈亏额" value="profit_loss" />
|
||||
<el-option label="启动时间" value="begin_time" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-form-item label="排序类型">
|
||||
<el-select v-model="queryParams.sortOrder" placeholder="请选择" @change="handleQuery">
|
||||
<el-option label="正序" value="asc" />
|
||||
<el-option label="倒序" value="desc" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<div style="float: right; margin-left: 10px;">
|
||||
<el-tooltip class="item" effect="dark" content="刷新" placement="top">
|
||||
<el-button size="mini" circle icon="el-icon-refresh" @click="handleQuery" />
|
||||
</el-tooltip>
|
||||
<el-dropdown trigger="click" :hide-on-click="false" style="margin-left: 10px;">
|
||||
<el-tooltip class="item" effect="dark" content="显隐列" placement="top">
|
||||
<el-button size="mini" circle icon="el-icon-menu" />
|
||||
</el-tooltip>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item v-for="column in columns" :key="column.prop">
|
||||
<el-checkbox v-model="column.visible">{{ column.label }}</el-checkbox>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-row>
|
||||
<!-- <el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
|
||||
<el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item> -->
|
||||
</el-form>
|
||||
|
||||
<!-- 盈亏列表表格 -->
|
||||
<el-table :data="profitList" v-loading="loading" border style="width: 100%; margin-top: 20px;">
|
||||
<el-table-column fixed="left" prop="projectName" label="项目名称" min-width="120" v-if="columnVisibility.projectName"
|
||||
show-overflow-tooltip>
|
||||
<template slot="header" slot-scope="scope">
|
||||
<div style="text-align: center;">项目名称</div>
|
||||
<el-input v-model="queryParams.projectName" placeholder="筛选" clearable @input="handleQuery" size="mini"
|
||||
style="margin-top: 5px;" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="projectNum" label="项目编号" min-width="120" v-if="columnVisibility.projectNum">
|
||||
<template slot="header" slot-scope="scope">
|
||||
<div style="text-align: center;">项目编号</div>
|
||||
<el-input v-model="queryParams.projectNum" placeholder="筛选" clearable @input="handleQuery" size="mini"
|
||||
style="margin-top: 5px;" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="projectCode" label="项目代号" min-width="120" v-if="columnVisibility.projectCode">
|
||||
<template slot="header" slot-scope="scope">
|
||||
<div style="text-align: center;">项目代号</div>
|
||||
<el-input v-model="queryParams.projectCode" placeholder="筛选" clearable @input="handleQuery" size="mini"
|
||||
style="margin-top: 5px;" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="projectStatus" label="项目状态" min-width="100" v-if="columnVisibility.projectStatus">
|
||||
<template slot="header" slot-scope="scope">
|
||||
<div style="text-align: center;">项目状态</div>
|
||||
<el-select v-model="queryParams.projectStatus" placeholder="筛选" clearable @change="handleQuery" size="mini"
|
||||
style="width: 100%; margin-top: 5px;">
|
||||
<el-option label="进行中" :value="0" />
|
||||
<el-option label="完结" :value="1" />
|
||||
</el-select>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.projectStatus === '0' ? 'success' : 'info'">
|
||||
{{ scope.row.projectStatus === '0' ? '进行中' : '完结' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="signingCompany" label="签约公司" min-width="120" v-if="columnVisibility.signingCompany">
|
||||
<template slot="header" slot-scope="scope">
|
||||
<div style="text-align: center;">签约公司</div>
|
||||
<el-select v-model="queryParams.signingCompany" placeholder="筛选" clearable @change="handleQuery" size="mini"
|
||||
style="width: 100%; margin-top: 5px;">
|
||||
<el-option v-for="dict in dict.type.signing_company" :key="dict.value" :label="dict.label"
|
||||
:value="dict.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.signing_company" :value="scope.row.signingCompany" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="tradeType" label="贸易类型" min-width="100" v-if="columnVisibility.tradeType">
|
||||
<template slot="header" slot-scope="scope">
|
||||
<div style="text-align: center;">贸易类型</div>
|
||||
<el-select v-model="queryParams.tradeType" placeholder="筛选" clearable @change="handleQuery" size="mini"
|
||||
style="width: 100%; margin-top: 5px;">
|
||||
<el-option v-for="dict in dict.type.sys_trade_type" :key="dict.value" :label="dict.label"
|
||||
:value="dict.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.sys_trade_type" :value="scope.row.tradeType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="originalFunds" label="原始合同金额" min-width="120" v-if="columnVisibility.originalFunds">
|
||||
<template slot="header" slot-scope="scope">
|
||||
<div style="text-align: center;">原始合同金额</div>
|
||||
<div style="margin-top: 5px;">
|
||||
<el-input v-model="queryParams.minContractAmount" placeholder="最小" clearable @input="handleQuery"
|
||||
size="mini" style="width: 45%;" />
|
||||
<span style="margin: 0 2px;">-</span>
|
||||
<el-input v-model="queryParams.maxContractAmount" placeholder="最大" clearable @input="handleQuery"
|
||||
size="mini" style="width: 45%;" />
|
||||
</div>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.originalFunds == null ? scope.row.detailIncome : scope.row.originalFunds }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalIncomeCny" label="人民币金额(¥)" min-width="120"
|
||||
v-if="columnVisibility.contractAmountCny" />
|
||||
<el-table-column prop="totalExpenditure" label="支出(¥)" min-width="120" v-if="columnVisibility.totalExpenditure">
|
||||
<template slot="header" slot-scope="scope">
|
||||
<div style="text-align: center;">支出(¥)</div>
|
||||
<!-- <div style="margin-top: 5px;">
|
||||
<el-input v-model="queryParams.minTotalExpenditure" placeholder="最小" clearable @input="handleQuery" size="mini" style="width: 45%;" />
|
||||
<span style="margin: 0 2px;">-</span>
|
||||
<el-input v-model="queryParams.maxTotalExpenditure" placeholder="最大" clearable @input="handleQuery" size="mini" style="width: 45%;" />
|
||||
</div> -->
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="profitLoss" label="盈亏金额(元)" min-width="120" v-if="columnVisibility.profitLoss">
|
||||
<template slot="header" slot-scope="scope">
|
||||
<div style="text-align: center;">盈亏金额(元)</div>
|
||||
<div style="margin-top: 5px;">
|
||||
<el-input v-model="queryParams.minProfitLoss" placeholder="最小" clearable @input="handleQuery" size="mini"
|
||||
style="width: 45%;" />
|
||||
<span style="margin: 0 2px;">-</span>
|
||||
<el-input v-model="queryParams.maxProfitLoss" placeholder="最大" clearable @input="handleQuery" size="mini"
|
||||
style="width: 45%;" />
|
||||
</div>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.profitLoss !== null && scope.row.profitLoss !== undefined">
|
||||
<span :class="getProfitLossClass(scope.row)" :style="getProfitLossStyle(scope.row)">
|
||||
{{ parseFloat(scope.row.profitLoss).toFixed(2) }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="beginTime" label="启动时间" min-width="120" v-if="columnVisibility.beginTime">
|
||||
<template slot="header" slot-scope="scope">
|
||||
<div style="text-align: center;">启动时间</div>
|
||||
<el-date-picker v-model="queryParams.beginTimeRange" type="daterange" range-separator="至"
|
||||
start-placeholder="开始" end-placeholder="结束" value-format="yyyy-MM-dd" size="mini"
|
||||
style="width: 100%; margin-top: 5px;" @change="handleQuery" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="profitType" label="盈亏类型" min-width="100" v-if="columnVisibility.profitType">
|
||||
<template slot="header" slot-scope="scope">
|
||||
<div style="text-align: center;">盈亏类型</div>
|
||||
<el-select v-model="queryParams.profitType" placeholder="筛选" clearable @change="handleQuery" size="mini"
|
||||
style="width: 100%; margin-top: 5px;">
|
||||
<el-option label="盈利" value="profit" />
|
||||
<el-option label="亏损" value="loss" />
|
||||
</el-select>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.profitLoss !== null && scope.row.profitLoss !== undefined"
|
||||
:type="scope.row.profitLoss >= 0 ? 'success' : 'danger'">
|
||||
{{ scope.row.profitLoss >= 0 ? '盈利' : '亏损' }}
|
||||
</el-tag>
|
||||
<el-tag size="mini" style="margin-left: 6px;" v-if="getProfitRate(scope.row) !== null">{{
|
||||
getProfitRateTag(scope.row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination style="margin-top: 20px; text-align: right;" background
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="total" :page-size.sync="queryParams.pageSize"
|
||||
:current-page.sync="queryParams.pageNum" :page-sizes="[10, 20, 50, 100]" @size-change="handleSizeChange"
|
||||
@current-change="handlePageChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listProfit } from '@/api/oa/finance/profit';
|
||||
|
||||
export default {
|
||||
name: 'ProfitList',
|
||||
dicts: ['sys_project_code', 'sys_trade_type', 'signing_company'],
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
profitList: [],
|
||||
total: 0,
|
||||
// 盈亏阈值配置
|
||||
profitThresholds: {
|
||||
highProfit: 100000, // 高盈利阈值
|
||||
highLoss: -50000, // 高亏损阈值
|
||||
warningProfit: 50000, // 警告盈利阈值
|
||||
warningLoss: -20000 // 警告亏损阈值
|
||||
},
|
||||
queryParams: {
|
||||
projectName: '',
|
||||
projectNum: '',
|
||||
projectStatus: 1,
|
||||
isDomestic: '',
|
||||
minContractAmount: '',
|
||||
maxContractAmount: '',
|
||||
minUsdAmount: '',
|
||||
maxUsdAmount: '',
|
||||
minRmbAmount: '',
|
||||
maxRmbAmount: '',
|
||||
minProfitLoss: '',
|
||||
maxProfitLoss: '',
|
||||
beginTimeRange: [], // [beginTimeStart, beginTimeEnd]
|
||||
profitType: '',
|
||||
sortField: '',
|
||||
sortOrder: 'desc',
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
projectCode: '',
|
||||
minTotalExpenditure: '',
|
||||
maxTotalExpenditure: '',
|
||||
tradeType: '',
|
||||
signingCompany: '',
|
||||
},
|
||||
// 列信息
|
||||
columns: [
|
||||
{ key: 0, prop: 'projectName', label: `项目名称`, visible: true },
|
||||
{ key: 1, prop: 'projectNum', label: `项目编号`, visible: true },
|
||||
{ key: 2, prop: 'projectCode', label: `项目代号`, visible: true },
|
||||
{ key: 3, prop: 'projectStatus', label: `项目状态`, visible: true },
|
||||
{ key: 4, prop: 'tradeType', label: `贸易类型`, visible: true },
|
||||
{ key: 5, prop: 'signingCompany', label: `签约公司`, visible: true },
|
||||
{ key: 5, prop: 'originalFunds', label: `原始合同金额`, visible: true },
|
||||
{ key: 6, prop: 'contractAmountCny', label: `人民币金额(¥)`, visible: true },
|
||||
{ key: 7, prop: 'totalExpenditure', label: `支出(¥)`, visible: true },
|
||||
{ key: 8, prop: 'profitLoss', label: `盈亏金额(元)`, visible: true },
|
||||
{ key: 9, prop: 'beginTime', label: `启动时间`, visible: true },
|
||||
{ key: 10, prop: 'profitType', label: `盈亏类型`, visible: true }
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
columnVisibility () {
|
||||
return this.columns.reduce((acc, col) => {
|
||||
acc[col.prop] = col.visible;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getList () {
|
||||
this.loading = true
|
||||
// 拆分时间范围
|
||||
const params = { ...this.queryParams }
|
||||
if (params.beginTimeRange && params.beginTimeRange.length === 2) {
|
||||
params.beginTimeStart = params.beginTimeRange[0]
|
||||
params.beginTimeEnd = params.beginTimeRange[1]
|
||||
}
|
||||
delete params.beginTimeRange
|
||||
listProfit(params).then(res => {
|
||||
this.profitList = res.rows || []
|
||||
this.total = res.total || 0
|
||||
this.loading = false
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleQuery () {
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
resetQuery () {
|
||||
this.queryParams = {
|
||||
projectName: '',
|
||||
projectNum: '',
|
||||
projectStatus: '',
|
||||
isDomestic: '',
|
||||
minContractAmount: '',
|
||||
maxContractAmount: '',
|
||||
minUsdAmount: '',
|
||||
maxUsdAmount: '',
|
||||
minRmbAmount: '',
|
||||
maxRmbAmount: '',
|
||||
minProfitLoss: '',
|
||||
maxProfitLoss: '',
|
||||
beginTimeRange: [],
|
||||
profitType: '',
|
||||
sortField: '',
|
||||
sortOrder: 'desc',
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
projectCode: '',
|
||||
minTotalExpenditure: '',
|
||||
maxTotalExpenditure: '',
|
||||
tradeType: '',
|
||||
}
|
||||
this.getList()
|
||||
},
|
||||
handleSizeChange (val) {
|
||||
this.queryParams.pageSize = val
|
||||
this.getList()
|
||||
},
|
||||
handlePageChange (val) {
|
||||
this.queryParams.pageNum = val
|
||||
this.getList()
|
||||
},
|
||||
getProfitRate (row) {
|
||||
// 计算盈亏百分比,originalFunds为0则用detailIncome
|
||||
const base = row.originalFunds && row.originalFunds !== 0 ? row.originalFunds : row.detailIncome;
|
||||
if (!base || base === 0) return null;
|
||||
return row.profitLoss / base;
|
||||
},
|
||||
getProfitRateTag (row) {
|
||||
const rate = this.getProfitRate(row);
|
||||
console.log(rate, 'rate' + row.projectName, row);
|
||||
if (row.totalIncomeCny < 0) return '亏损';
|
||||
if (rate === null) return '-';
|
||||
if (rate >= 0.2) return '高盈利';
|
||||
if (rate <= -0.2) return '高亏损';
|
||||
if (rate < 0) return '亏损';
|
||||
if (rate >= 0 && rate <= 0.05) return '低盈利';
|
||||
return '盈利';
|
||||
},
|
||||
getProfitLossClass (row) {
|
||||
const rate = this.getProfitRate(row);
|
||||
if (rate === null) return '';
|
||||
if (row.totalIncomeCny < 0) return 'profit-high-loss';
|
||||
if (rate >= 0.2) {
|
||||
return 'profit-high-profit';
|
||||
} else if (rate <= -0.2) {
|
||||
return 'profit-high-loss';
|
||||
} else if (rate < 0) {
|
||||
return 'profit-warning-loss';
|
||||
} else if (rate >= 0 && rate <= 0.05) {
|
||||
return 'profit-warning-profit';
|
||||
} else {
|
||||
return 'profit-normal-profit';
|
||||
}
|
||||
},
|
||||
getProfitLossStyle (row) {
|
||||
const rate = this.getProfitRate(row);
|
||||
if (rate === null) return {};
|
||||
if (row.totalIncomeCny < 0) return { color: '#F56C6C', padding: '2px 6px', borderRadius: '4px' };
|
||||
if (rate >= 0.2) {
|
||||
return {
|
||||
color: '#67C23A',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '14px',
|
||||
backgroundColor: '#f0f9ff',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px'
|
||||
};
|
||||
} else if (rate <= -0.2) {
|
||||
return {
|
||||
color: '#F56C6C',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '14px',
|
||||
backgroundColor: '#fef0f0',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px'
|
||||
};
|
||||
} else if (rate < 0) {
|
||||
return {
|
||||
color: '#F56C6C',
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#fef0f0',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px'
|
||||
};
|
||||
} else if (rate >= 0 && rate <= 0.05) {
|
||||
return {
|
||||
color: '#E6A23C',
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#fdf6ec',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px'
|
||||
};
|
||||
} else {
|
||||
return { color: '#67C23A' };
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.getList()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-form-inline .el-form-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* 盈亏金额样式 */
|
||||
.profit-high-profit {
|
||||
border: 2px solid #67C23A;
|
||||
box-shadow: 0 2px 4px rgba(103, 194, 58, 0.2);
|
||||
}
|
||||
|
||||
.profit-high-loss {
|
||||
border: 2px solid #F56C6C;
|
||||
box-shadow: 0 2px 4px rgba(245, 108, 108, 0.2);
|
||||
}
|
||||
|
||||
.profit-warning-profit {
|
||||
border: 1px solid #E6A23C;
|
||||
box-shadow: 0 1px 3px rgba(230, 162, 60, 0.2);
|
||||
}
|
||||
|
||||
.profit-warning-loss {
|
||||
border: 1px solid #F56C6C;
|
||||
box-shadow: 0 1px 3px rgba(245, 108, 108, 0.2);
|
||||
}
|
||||
|
||||
.profit-normal-profit {
|
||||
/* 正常盈利样式 */
|
||||
}
|
||||
|
||||
.profit-normal-loss {
|
||||
/* 正常亏损样式 */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,326 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-timeline v-if="localActivities && localActivities.length > 0">
|
||||
<el-timeline-item
|
||||
v-for="(activity, index) in localActivities"
|
||||
:key="activity.id || index"
|
||||
:timestamp="activity.planPayDate"
|
||||
placement="top"
|
||||
>
|
||||
<el-card class="editable-card">
|
||||
<!-- 编辑状态 -->
|
||||
<div v-if="activity.isEditing" class="edit-mode">
|
||||
<div class="form-item">
|
||||
<el-input
|
||||
v-model="activity.detailName"
|
||||
placeholder="进度名称"
|
||||
size="small"
|
||||
></el-input>
|
||||
</div>
|
||||
|
||||
<div class="time-pickers">
|
||||
<el-date-picker
|
||||
v-model="activity.actualStartDate"
|
||||
type="date"
|
||||
:default-value="
|
||||
activity.actualStartDate
|
||||
? activity.actualStartDate
|
||||
: activity.planPayDate
|
||||
"
|
||||
placeholder="实际开始时间"
|
||||
value-format="yyyy-MM-dd"
|
||||
size="small"
|
||||
></el-date-picker>
|
||||
|
||||
<el-date-picker
|
||||
v-model="activity.actualEndDate"
|
||||
type="date"
|
||||
:default-value="
|
||||
activity.actualEndDate
|
||||
? activity.actualEndDate
|
||||
: activity.planPayDate
|
||||
"
|
||||
placeholder="实际结束时间"
|
||||
value-format="yyyy-MM-dd"
|
||||
size="small"
|
||||
:disabled-date="
|
||||
(endDate) =>
|
||||
activity.actualStartDate &&
|
||||
new Date(endDate) < new Date(activity.actualStartDate)
|
||||
"
|
||||
></el-date-picker>
|
||||
</div>
|
||||
|
||||
<!-- <div class="form-item">
|
||||
<el-select>
|
||||
|
||||
</el-select>
|
||||
</div> -->
|
||||
|
||||
<div class="actions">
|
||||
<el-button type="danger" size="mini" @click="cancelEdit(index)"
|
||||
>取消</el-button
|
||||
>
|
||||
<el-button type="success" size="mini" @click="confirmEdit(index)"
|
||||
>确认</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 展示状态 -->
|
||||
<div v-else class="view-mode">
|
||||
<div class="header">
|
||||
<span style="gap: 8px; display: flex">
|
||||
<span class="title">{{
|
||||
activity.detailName || "未命名进度"
|
||||
}}</span>
|
||||
<el-tag
|
||||
v-if="activity.detailStatus == 1"
|
||||
type="success"
|
||||
size="mini"
|
||||
effect="dark"
|
||||
>
|
||||
已完成
|
||||
</el-tag>
|
||||
</span>
|
||||
|
||||
<span style="gap: 8px; display: flex">
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="el-icon-edit"
|
||||
size="mini"
|
||||
@click.stop="toggleEdit(index)"
|
||||
></el-button>
|
||||
<el-button
|
||||
v-if="activity.detailStatus != 1"
|
||||
type="success"
|
||||
icon="el-icon-check"
|
||||
size="mini"
|
||||
@click.stop="markAsComplete(index)"
|
||||
></el-button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="time-info">
|
||||
<!-- 计划开始时间 -->
|
||||
<div v-if="activity.planStartDate" class="time-item">
|
||||
<i class="el-icon-circle-check"></i>
|
||||
计划开始:{{ formatDate(activity.planStartDate) }}
|
||||
</div>
|
||||
|
||||
<!-- 剩余时间 -->
|
||||
<div
|
||||
v-if="
|
||||
activity.remainTime !== undefined &&
|
||||
activity.detailStatus != 1
|
||||
"
|
||||
class="time-item"
|
||||
>
|
||||
<i
|
||||
class="el-icon-time"
|
||||
:style="{ color: remainTimeColor(activity) }"
|
||||
></i>
|
||||
<span :style="{ color: remainTimeColor(activity) }">
|
||||
剩余时间:{{ activity.remainTime }}天
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="activity.actualStartDate" class="time-item">
|
||||
<i class="el-icon-time"></i>
|
||||
{{
|
||||
formatDate(
|
||||
activity.actualStartDate
|
||||
? activity.actualStartDate
|
||||
: activity.planStartDate
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div v-if="activity.actualEndDate" class="time-item">
|
||||
<i class="el-icon-circle-check"></i>
|
||||
{{
|
||||
formatDate(
|
||||
activity.actualEndDate
|
||||
? activity.actualEndDate
|
||||
: activity.planEndDate
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-empty v-else description="暂无进度"></el-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatDate } from "@/utils";
|
||||
|
||||
export default {
|
||||
name: "EditableTimeline",
|
||||
props: {
|
||||
activities: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localActivities: this.activities.map((a) => ({
|
||||
...a,
|
||||
isEditing: false,
|
||||
})),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
// 修改toggleEdit方法保存原始数据
|
||||
toggleEdit(index) {
|
||||
this.localActivities = this.localActivities.map((a, i) => {
|
||||
if (i === index) {
|
||||
// 进入编辑模式时保存快照
|
||||
const newState = { ...a, isEditing: !a.isEditing };
|
||||
if (!a.isEditing) {
|
||||
newState._originalData = JSON.parse(JSON.stringify(a));
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
return a;
|
||||
});
|
||||
},
|
||||
|
||||
// 新增取消编辑方法
|
||||
cancelEdit(index) {
|
||||
const original = this.localActivities[index]._originalData;
|
||||
if (original) {
|
||||
// 恢复原始数据
|
||||
this.localActivities.splice(index, 1, {
|
||||
...original,
|
||||
isEditing: false,
|
||||
});
|
||||
this.$forceUpdate(); // 强制更新视图
|
||||
this.$message.info("已取消编辑");
|
||||
}
|
||||
},
|
||||
|
||||
// 修改确认保存方法清理临时数据
|
||||
async confirmEdit(index) {
|
||||
try {
|
||||
await this.$confirm("确定要保存修改吗?", "操作确认", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
const activity = this.localActivities[index];
|
||||
// 清理临时数据
|
||||
if (activity._originalData) {
|
||||
delete activity._originalData;
|
||||
}
|
||||
|
||||
this.toggleEdit(index);
|
||||
this.$emit("change", {
|
||||
list: this.localActivities,
|
||||
activity: activity,
|
||||
});
|
||||
} catch (error) {
|
||||
// 取消操作时自动捕获错误
|
||||
}
|
||||
},
|
||||
|
||||
// 剩余时间颜色判断
|
||||
remainTimeColor(activity) {
|
||||
if (activity.detailStatus == 1) return "#67C23A"; // 已完成绿色
|
||||
return activity.remainTime < 3 ? "#F56C6C" : "#909399"; // 小于3天红色
|
||||
},
|
||||
|
||||
async markAsComplete(index) {
|
||||
try {
|
||||
await this.$confirm("确定要完成该进度吗?", "操作确认", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
const activity = {
|
||||
...this.localActivities[index],
|
||||
detailStatus: 1,
|
||||
actualEndDate: formatDate(new Date()),
|
||||
};
|
||||
|
||||
this.$emit("change", {
|
||||
list: this.localActivities.splice(index, 1, activity),
|
||||
activity,
|
||||
});
|
||||
} catch {}
|
||||
},
|
||||
|
||||
formatDate(dateStr) {
|
||||
return dateStr;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activities(newVal) {
|
||||
this.localActivities = newVal.map((a) => ({
|
||||
...a,
|
||||
isEditing: false,
|
||||
}));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.editable-card {
|
||||
margin: 10px 0;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.time-info {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.time-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 6px 0;
|
||||
color: #666;
|
||||
|
||||
i {
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-mode {
|
||||
.form-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.time-pickers {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
/* 增加按钮间距 */
|
||||
.actions .el-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row justify="end" type="flex">
|
||||
<el-button @click="addTaskOpen = true" type="primary">
|
||||
新建付款项
|
||||
</el-button>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<h2>项目信息</h2>
|
||||
<el-descriptions :title="current.data.label">
|
||||
<el-descriptions-item label="项目名">{{
|
||||
current.data.projectName
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="项目编号">{{
|
||||
current.data.projectNum
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-row>
|
||||
|
||||
<el-row title="付款项概览">
|
||||
<h2>付款项概览</h2>
|
||||
<el-table :data="current.childNodes">
|
||||
<el-table-column prop="data.startTime" label="付款开始时间" width="160"></el-table-column>
|
||||
<el-table-column prop="data.endTime" label="付款结束时间" width="160"></el-table-column>
|
||||
<el-table-column prop="data.amount" label="付款金额" width="120"></el-table-column>
|
||||
<el-table-column prop="data.isVoid" label="状态" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.data.isVoid === 1 ? 'danger' : 'success'">
|
||||
{{ scope.row.data.isVoid === 1 ? '已作废' : '正常' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="data.remark" label="备注" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleUpdate(row)">更新</el-button>
|
||||
<el-button type="info" size="small" @click="handleDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-row>
|
||||
|
||||
<el-dialog :title="dialogTitle" :visible.sync="addTaskOpen">
|
||||
<el-form :model="addTaskForm" :rules="addTaskFormRules" ref="addTaskFormRef" label-width="100px">
|
||||
<el-form-item label="开始时间" prop="startTime" required>
|
||||
<el-date-picker v-model="addTaskForm.startTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="选择开始时间" style="width: 100%"></el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束时间" prop="endTime" required>
|
||||
<el-date-picker v-model="addTaskForm.endTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="选择结束时间" style="width: 100%"></el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="付款金额" prop="amount" required>
|
||||
<el-input-number v-model="addTaskForm.amount" :min="0" :precision="2" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="isVoid">
|
||||
<el-radio-group v-model="addTaskForm.isVoid">
|
||||
<el-radio :label="0">正常</el-radio>
|
||||
<el-radio :label="1">作废</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input type="textarea" v-model="addTaskForm.remark" :rows="3" placeholder="请输入备注信息"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm">提交</el-button>
|
||||
<el-button @click="addTaskOpen = false">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog title="付款项详情" :visible.sync="detailOpen" width="600px">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="付款进度ID">{{ currentDetail.paymentProgressId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="关联项目ID">{{ currentDetail.projectId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="付款开始时间">{{ currentDetail.startTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="付款结束时间">{{ currentDetail.endTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="付款金额">{{ currentDetail.amount }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="currentDetail.isVoid === 1 ? 'danger' : 'success'">
|
||||
{{ currentDetail.isVoid === 1 ? '已作废' : '正常' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注">{{ currentDetail.remark }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { createProgress, getProjectProgress, updateProgress } from "@/api/oa/progress";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
current: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 获取付款项列表
|
||||
async getProgressList () {
|
||||
try {
|
||||
const res = await getProjectProgress({ projectId: this.current.data.projectId });
|
||||
if (res.rows) {
|
||||
this.current.childNodes = res.rows.map(item => ({
|
||||
data: {
|
||||
...item,
|
||||
label: `付款项 ${item.amount}元`
|
||||
}
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error("获取付款项列表失败:" + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
// 打开新建对话框
|
||||
handleAdd () {
|
||||
this.dialogTitle = '新建付款项';
|
||||
this.addTaskForm = {
|
||||
startTime: "",
|
||||
endTime: "",
|
||||
amount: 0,
|
||||
isVoid: 0,
|
||||
remark: ""
|
||||
};
|
||||
this.addTaskOpen = true;
|
||||
},
|
||||
|
||||
// 打开更新对话框
|
||||
handleUpdate (row) {
|
||||
this.dialogTitle = '更新付款项';
|
||||
this.addTaskForm = {
|
||||
paymentProgressId: row.data.paymentProgressId,
|
||||
projectId: row.data.projectId,
|
||||
startTime: row.data.startTime,
|
||||
endTime: row.data.endTime,
|
||||
amount: row.data.amount,
|
||||
isVoid: row.data.isVoid,
|
||||
remark: row.data.remark
|
||||
};
|
||||
this.addTaskOpen = true;
|
||||
},
|
||||
|
||||
// 打开详情对话框
|
||||
handleDetail (row) {
|
||||
// this.currentDetail = row.data;
|
||||
// this.detailOpen = true;
|
||||
this.$emit('node-click', row.data, { data: row.data, level: 2 })
|
||||
},
|
||||
|
||||
// 提交表单
|
||||
submitForm () {
|
||||
this.$refs.addTaskFormRef.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const payload = {
|
||||
projectId: this.current.data.projectId,
|
||||
startTime: this.addTaskForm.startTime,
|
||||
endTime: this.addTaskForm.endTime,
|
||||
amount: this.addTaskForm.amount,
|
||||
isVoid: this.addTaskForm.isVoid,
|
||||
remark: this.addTaskForm.remark
|
||||
};
|
||||
|
||||
if (this.addTaskForm.paymentProgressId) {
|
||||
payload.paymentProgressId = this.addTaskForm.paymentProgressId;
|
||||
await updateProgress(payload);
|
||||
this.$message.success("更新成功");
|
||||
} else {
|
||||
await createProgress(payload);
|
||||
this.$message.success("添加成功");
|
||||
}
|
||||
|
||||
this.addTaskOpen = false;
|
||||
this.getProgressList();
|
||||
} catch (error) {
|
||||
this.$message.error((this.addTaskForm.paymentProgressId ? "更新" : "添加") + "失败:" + error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
addTaskOpen: false,
|
||||
detailOpen: false,
|
||||
dialogTitle: '新建付款项',
|
||||
currentDetail: {},
|
||||
addTaskForm: {
|
||||
startTime: "",
|
||||
endTime: "",
|
||||
amount: 0,
|
||||
isVoid: 0,
|
||||
remark: ""
|
||||
},
|
||||
addTaskFormRules: {
|
||||
startTime: [
|
||||
{ required: true, message: "请选择开始时间", trigger: "change" }
|
||||
],
|
||||
endTime: [
|
||||
{ required: true, message: "请选择结束时间", trigger: "change" },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value && this.addTaskForm.startTime && value < this.addTaskForm.startTime) {
|
||||
callback(new Error('结束时间不能早于开始时间'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
amount: [
|
||||
{ required: true, message: "请输入付款金额", trigger: "blur" },
|
||||
{ type: 'number', min: 0, message: "金额必须大于0", trigger: "blur" }
|
||||
],
|
||||
remark: [
|
||||
{ required: true, message: "请输入备注信息", trigger: "blur" },
|
||||
{ min: 1, max: 200, message: "备注长度在1到200个字符之间", trigger: "blur" }
|
||||
]
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-form {
|
||||
.el-date-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1500
ruoyi-ui/src/views/oa/finance/progress/index.vue
Normal file
1500
ruoyi-ui/src/views/oa/finance/progress/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -0,0 +1,140 @@
|
||||
<!-- SalaryPreview.vue -->
|
||||
<template>
|
||||
<div class="salary-preview-container">
|
||||
<!-- 基础信息描述 -->
|
||||
<el-descriptions :column="2" border class="preview-basic-info">
|
||||
<el-descriptions-item label="单位名称">{{ paylaod.unitName || '未识别' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发薪时间">{{ paylaod.salaryPeriod || '未识别' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 预览表格(含自动生成的合计行) -->
|
||||
<el-table
|
||||
max-height="500"
|
||||
:data="tableDataWithTotal"
|
||||
border
|
||||
style="width: 100%;"
|
||||
size="mini"
|
||||
:cell-style="handleTotalRowStyle"
|
||||
>
|
||||
<el-table-column
|
||||
v-for="(column, index) in tableColumns"
|
||||
:key="index"
|
||||
:prop="column.prop"
|
||||
:label="column.label"
|
||||
:width="column.width"
|
||||
:rowspan="column.rowspan"
|
||||
:colspan="column.colspan"
|
||||
></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SalaryPreview',
|
||||
// 接收父组件传递的「无合计行数据」「列配置」「基础信息」
|
||||
props: {
|
||||
/** 不含合计行的纯业务数据 */
|
||||
tableData: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
/** 表格列配置(含合并单元格规则) */
|
||||
tableColumns: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
/** 单位名称、发薪时间等基础信息 */
|
||||
paylaod: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({ unitName: '', salaryPeriod: '' })
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/** 生成「业务数据 + 合计行」的完整表格数据 */
|
||||
tableDataWithTotal() {
|
||||
if (this.tableData.length === 0) return [];
|
||||
|
||||
// 1. 复制业务数据(避免修改原props)
|
||||
const dataCopy = [...this.tableData];
|
||||
// 2. 计算合计行并追加到末尾
|
||||
dataCopy.push(this.calculateTotalRow());
|
||||
|
||||
return dataCopy;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 核心:根据列配置计算合计行(数值列求和,非数值列显示「合计」) */
|
||||
calculateTotalRow() {
|
||||
const totalRow = {};
|
||||
const numericProps = this.tableColumns
|
||||
.filter(col => col.isNumeric) // 筛选需要求和的数值列(需模板配置isNumeric)
|
||||
.map(col => col.prop);
|
||||
|
||||
// 1. 数值列求和(处理Excel中可能的字符串数字/空值)
|
||||
numericProps.forEach(prop => {
|
||||
totalRow[prop] = this.tableData.reduce((sum, row) => {
|
||||
// 数据清洗:字符串转数字、空值视为0、去除千分符
|
||||
const value = row[prop]
|
||||
? Number(String(row[prop]).replace(/,/g, '').trim())
|
||||
: 0;
|
||||
return sum + (isNaN(value) ? 0 : value);
|
||||
}, 0);
|
||||
// 金额格式化:保留2位小数
|
||||
totalRow[prop] = Number(totalRow[prop].toFixed(2));
|
||||
});
|
||||
|
||||
// 2. 非数值列:第一列显示「合计」,其他非数值列空值
|
||||
this.tableColumns.forEach(col => {
|
||||
if (!numericProps.includes(col.prop)) {
|
||||
totalRow[col.prop] = col.prop === this.tableColumns[0].prop
|
||||
? '合计'
|
||||
: '';
|
||||
}
|
||||
});
|
||||
|
||||
return totalRow;
|
||||
},
|
||||
|
||||
/** 给合计行添加特殊样式(加粗+浅灰背景) */
|
||||
handleTotalRowStyle({ row }) {
|
||||
const firstProp = this.tableColumns[0].prop;
|
||||
if (row[firstProp] === '合计') {
|
||||
return {
|
||||
'font-weight': 'bold',
|
||||
'background-color': '#f5f7fa'
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.salary-preview-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.preview-basic-info {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* 表头样式优化:减少内边距,统一高度 */
|
||||
::v-deep .el-table__header th {
|
||||
padding: 4px 4px !important; /* 表头内边距(默认12px),压缩60%+ */
|
||||
height: 36px !important; /* 表头行高(默认48px),压缩25% */
|
||||
font-size: 12px !important;
|
||||
background-color: #f9fafb !important; /* 浅背景提升可读性 */
|
||||
}
|
||||
/* 表格内容区:清除多余间距,确保行高一致 */
|
||||
::v-deep .el-table__body tr td {
|
||||
border-bottom: 1px solid #f2f2f2 !important; /* 细边框减少割裂感 */
|
||||
}
|
||||
/* 修复表格滚动条与内容间距问题 */
|
||||
::v-deep .el-table__body-wrapper {
|
||||
margin: 0 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,395 @@
|
||||
<template>
|
||||
<span>
|
||||
<!-- 导入按钮 -->
|
||||
<el-button type="primary" icon="el-icon-setting" size="mini" @click="showImportDialog = true"
|
||||
:loading="buttonLoading">导入</el-button>
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<el-dialog title="导入职工薪酬明细表" :visible.sync="showImportDialog" width="90%" :close-on-click-modal="false"
|
||||
:fullscreen="false">
|
||||
<!-- 模板类型选择表单 -->
|
||||
<el-form :model="form" label-width="100px" style="margin-bottom: 20px;">
|
||||
<el-form-item label="模板类型">
|
||||
<el-radio-group v-model="form.templateType">
|
||||
<el-radio :label="1">福安德模板(包括其他公司)</el-radio>
|
||||
<el-radio :label="2">昆山德睿福模板</el-radio>
|
||||
<el-radio :label="3">山东首达特模板</el-radio>
|
||||
<el-radio :label="4">西安擎峰模板</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- Excel文件上传组件 -->
|
||||
<el-upload ref="upload" action="" :auto-upload="false" :on-change="handleFileChange" :show-file-list="true"
|
||||
accept=".xlsx, .xls" :file-list="fileList">
|
||||
<el-button size="small" type="primary">选择Excel文件</el-button>
|
||||
<div slot="tip" class="el-upload__tip">只能上传xlsx/xls文件,且不超过5MB</div>
|
||||
</el-upload>
|
||||
|
||||
<!-- 开始导入按钮(选择文件后显示) -->
|
||||
<div style="margin-top: 20px;" v-if="fileList.length > 0">
|
||||
<el-button type="success" @click="handleImport">开始导入并校验</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 校验结果提示(校验后显示) -->
|
||||
<div v-if="validationResult.length > 0" style="margin-top: 15px;">
|
||||
<el-alert :title="validationSuccess ? '校验成功' : '校验失败'" :type="validationSuccess ? 'success' : 'error'"
|
||||
show-icon>
|
||||
<div v-for="(item, index) in validationResult" :key="index" class="validation-item">{{ item }}</div>
|
||||
</el-alert>
|
||||
</div>
|
||||
|
||||
<!-- 数据预览区域(校验成功后显示:使用独立组件) -->
|
||||
<salary-preview v-if="showPreview" :table-data="tableData" :table-columns="tableColumns"
|
||||
:paylaod="paylaod"></salary-preview>
|
||||
|
||||
<!-- 弹窗底部按钮 -->
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="cancelImport" v-loading="buttonLoading">取消</el-button>
|
||||
<el-button v-loading="buttonLoading" type="primary" @click="confirmImport"
|
||||
v-if="validationSuccess && tableData.length > 0">
|
||||
确认导入
|
||||
</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 引入依赖:接口、XLSX、模板配置、独立预览组件
|
||||
import { importSalaryMaster } from '@/api/oa/salaryMaster';
|
||||
import * as XLSX from 'xlsx';
|
||||
import SalaryPreview from './SalaryPreview.vue'; // 引入预览组件
|
||||
import kunshanDeruifuConfig from './template/kunshanDeruifuConfig';
|
||||
import shandongFuanDeConfig from './template/shandongFuanDeConfig';
|
||||
import shoudateConfig from './template/shoudateConfig';
|
||||
import xianqingfengConfig from './template/xianqingfengConfig';
|
||||
|
||||
export default {
|
||||
name: 'ImportExcel',
|
||||
components: { SalaryPreview }, // 注册预览组件
|
||||
data () {
|
||||
return {
|
||||
// 弹窗控制
|
||||
showImportDialog: false,
|
||||
// 表单数据(模板类型)
|
||||
form: { templateType: 1 }, // 1:福安德, 2:昆山德睿福
|
||||
// 上传文件相关
|
||||
fileList: [],
|
||||
file: null,
|
||||
// 预览表格相关:tableData直接不含合计行
|
||||
tableData: [], // 纯业务数据(无合计行)
|
||||
tableColumns: [], // 列配置(需含isNumeric标识数值列)
|
||||
// 校验相关
|
||||
validationResult: [],
|
||||
validationSuccess: false,
|
||||
showPreview: false,
|
||||
// 基础信息
|
||||
paylaod: { unitName: '', salaryPeriod: '' },
|
||||
// 模板配置中心(核心:需给数值列加isNumeric: true)
|
||||
templateConfigs: {
|
||||
1: shandongFuanDeConfig,
|
||||
2: kunshanDeruifuConfig,
|
||||
3: shoudateConfig,
|
||||
4: xianqingfengConfig
|
||||
},
|
||||
buttonLoading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 处理文件选择(仅保留最新文件) */
|
||||
handleFileChange (file, fileList) {
|
||||
this.fileList = fileList.slice(-1);
|
||||
this.file = file.raw;
|
||||
},
|
||||
|
||||
/** 触发导入流程(基础校验) */
|
||||
handleImport () {
|
||||
if (!this.form.templateType) {
|
||||
this.$message.warning('请选择模板类型');
|
||||
return;
|
||||
}
|
||||
if (!this.file) {
|
||||
this.$message.warning('请选择Excel文件');
|
||||
return;
|
||||
}
|
||||
this.readExcelFile();
|
||||
},
|
||||
|
||||
/** 读取Excel并解析为JSON */
|
||||
readExcelFile () {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (event) => {
|
||||
try {
|
||||
const workbook = XLSX.read(event.target.result, { type: 'binary' });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
// 解析为二维数组(header:1 不自动读表头)
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, raw: false });
|
||||
console.log('Excel数据:', jsonData);
|
||||
this.validateByTemplateConfig(jsonData);
|
||||
} catch (e) {
|
||||
this.$message.error('文件解析失败,请检查格式');
|
||||
console.error('Excel解析错误:', e);
|
||||
}
|
||||
};
|
||||
fileReader.readAsBinaryString(this.file);
|
||||
},
|
||||
|
||||
/** 核心:按模板配置校验数据 */
|
||||
validateByTemplateConfig (jsonData) {
|
||||
// 重置状态
|
||||
this.validationResult = [];
|
||||
this.validationSuccess = false;
|
||||
this.showPreview = false;
|
||||
this.tableData = [];
|
||||
this.tableColumns = [];
|
||||
|
||||
// 1. 获取当前模板配置
|
||||
const currentConfig = this.templateConfigs[this.form.templateType];
|
||||
if (!currentConfig) {
|
||||
this.validationResult.push('未找到对应模板配置,请联系开发');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 校验Excel非空
|
||||
if (jsonData.length === 0) {
|
||||
this.validationResult.push('Excel文件内容为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 提取单位名称和发薪时间(按Excel格式解析)
|
||||
const row1 = jsonData[1]?.filter(Boolean) || [];
|
||||
const excelCompanyName = jsonData[1] ? String(row1[0] || '').trim() : '';
|
||||
const excelDate = jsonData[1] ? String(row1[1] || '').trim() : '';
|
||||
this.paylaod = {
|
||||
unitName: excelCompanyName.split(':')[1] || '',
|
||||
// 确保是YYYY-MM格式
|
||||
salaryPeriod: (() => {
|
||||
// 提取原始周期字符串并进行初步处理
|
||||
const rawPeriod = excelDate.split(':')[1]
|
||||
? excelDate.split(':')[1].replace('年', '-').replace('月', '')
|
||||
: '';
|
||||
|
||||
// 如果原始周期为空,直接返回空字符串
|
||||
if (!rawPeriod) return '';
|
||||
|
||||
// 分割年和月(处理类似"2025-8"这样的格式)
|
||||
const [year, month] = rawPeriod.split('-');
|
||||
|
||||
// 校验年和月是否存在,确保格式正确
|
||||
if (year && month) {
|
||||
// 月份补零处理(确保是两位数)
|
||||
const formattedMonth = month.toString().padStart(2, '0');
|
||||
return `${year}-${formattedMonth}`;
|
||||
}
|
||||
|
||||
// 格式异常时返回原始处理结果(可根据实际需求调整)
|
||||
return rawPeriod;
|
||||
})()
|
||||
};
|
||||
|
||||
// 4. 校验表头完整性
|
||||
const actualHeaderRows = currentConfig.headerRowIndices.map(index => jsonData[index] || []);
|
||||
if (actualHeaderRows.some(row => row.length === 0)) {
|
||||
this.validationResult.push('表头信息不完整,请检查文件');
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 校验表头内容匹配
|
||||
this.validateHeaderContent(actualHeaderRows, currentConfig.headerRows);
|
||||
|
||||
// 6. 校验有效数据行(需至少1行数据+1行合计行,去掉合计行后有数据)
|
||||
if (jsonData.length <= currentConfig.dataStartIndex + 1) {
|
||||
this.validationResult.push('Excel文件中无有效数据行');
|
||||
return;
|
||||
}
|
||||
|
||||
// 7. 生成表格列配置(含合并规则)
|
||||
// this.tableColumns = this.generateTableColumnsFromConfig(currentConfig.columns, currentConfig.mergeRules);
|
||||
|
||||
this.tableColumns = currentConfig.columns;
|
||||
// 8. 格式化数据(直接移除合计行,tableData不含合计行)
|
||||
this.tableData = this.formatTableData(jsonData, currentConfig.dataStartIndex);
|
||||
|
||||
// 9. 校验结果处理
|
||||
if (this.validationResult.length === 0) {
|
||||
this.validationSuccess = true;
|
||||
this.validationResult.push('文件格式校验成功,可导入');
|
||||
this.showPreview = true;
|
||||
}
|
||||
},
|
||||
|
||||
/** 校验表头内容与模板匹配 */
|
||||
validateHeaderContent (actualRows, expectedRows) {
|
||||
actualRows.forEach((actualRow, rowIndex) => {
|
||||
const expectedRow = expectedRows[rowIndex] || [];
|
||||
// 清理表头文本(去空格、统一格式)
|
||||
const cleanedActual = actualRow.map(h => h ? String(h).replace(/\s+/g, ' ').trim() : null);
|
||||
const cleanedExpected = expectedRow.map(h => h ? String(h).replace(/\s+/g, ' ').trim() : null);
|
||||
|
||||
// 对比非空表头
|
||||
cleanedExpected.forEach((expected, colIndex) => {
|
||||
if (!expected) return;
|
||||
const actual = cleanedActual[colIndex] || '';
|
||||
if (actual !== expected) {
|
||||
this.validationResult.push(`第${rowIndex + 1}行表头第${colIndex + 1}列不匹配:期望"${expected}",实际"${actual}"`);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/** 生成表格列配置(含合并单元格) */
|
||||
generateTableColumnsFromConfig (columns, mergeRules) {
|
||||
const tableColumns = [];
|
||||
let currentColIndex = 0;
|
||||
|
||||
columns.forEach(column => {
|
||||
// 匹配合并规则(子列label含合并组名称)
|
||||
const mergeRule = mergeRules.find(rule => column.label.includes(rule.targetHeader));
|
||||
|
||||
// 处理合并列(生成父列+子列)
|
||||
if (mergeRule && currentColIndex % mergeRule.colspan === 0) {
|
||||
// 合并父列(如“个人缴纳”)
|
||||
tableColumns.push({
|
||||
label: mergeRule.targetHeader,
|
||||
prop: column.prop,
|
||||
rowspan: 1,
|
||||
colspan: mergeRule.colspan,
|
||||
width: 120,
|
||||
isNumeric: false // 合并父列非数值列
|
||||
});
|
||||
|
||||
// 合并子列(如“养老保险(个人)”)
|
||||
for (let i = 0; i < mergeRule.colspan; i++) {
|
||||
const subCol = columns[currentColIndex + i];
|
||||
tableColumns.push({
|
||||
...subCol, // 继承模板配置的label/prop/width/isNumeric
|
||||
rowspan: 1,
|
||||
colspan: 1
|
||||
});
|
||||
}
|
||||
currentColIndex += mergeRule.colspan;
|
||||
}
|
||||
// 处理普通列(占2行表头)
|
||||
else if (!mergeRule) {
|
||||
tableColumns.push({
|
||||
...column,
|
||||
rowspan: 2,
|
||||
colspan: 1,
|
||||
width: column.width || this.calculateColumnWidth(column.label)
|
||||
});
|
||||
currentColIndex += 1;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(tableColumns)
|
||||
return tableColumns;
|
||||
},
|
||||
|
||||
/** 格式化Excel数据(直接移除最后一行合计行) */
|
||||
formatTableData (jsonData, dataStartIndex) {
|
||||
const currentConfig = this.templateConfigs[this.form.templateType];
|
||||
const formattedData = [];
|
||||
|
||||
function isEmptyRow (row) {
|
||||
return row.every(cell => !cell || String(cell).trim() === '');
|
||||
}
|
||||
|
||||
// 直接过滤掉空行,重新赋值给原数组
|
||||
const jsonDataFilterd = jsonData.filter(row => !isEmptyRow(row));
|
||||
|
||||
// 遍历数据行(从dataStartIndex到倒数第二行,跳过合计行)
|
||||
for (let i = dataStartIndex; i < jsonDataFilterd.length - 1; i++) {
|
||||
const row = jsonDataFilterd[i];
|
||||
// 跳过空行
|
||||
if (!row || isEmptyRow(row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 按模板列配置生成行数据(key为后端所需prop)
|
||||
const formattedRow = {};
|
||||
row.forEach((cell, index) => {
|
||||
const colConfig = currentConfig.columns[index];
|
||||
if (colConfig) {
|
||||
formattedRow[colConfig.prop] = cell !== undefined ? cell : '';
|
||||
}
|
||||
});
|
||||
formattedData.push(formattedRow);
|
||||
}
|
||||
|
||||
console.log(formattedData)
|
||||
|
||||
// formattedData.pop()
|
||||
|
||||
return formattedData; // 最终不含合计行
|
||||
},
|
||||
|
||||
/** 自动计算列宽 */
|
||||
calculateColumnWidth (header) {
|
||||
const baseWidth = 80;
|
||||
const charWidth = 10;
|
||||
return Math.max(baseWidth, header.length * charWidth);
|
||||
},
|
||||
|
||||
/** 确认导入(直接用tableData,无需处理合计行) */
|
||||
async confirmImport () {
|
||||
const currentConfig = this.templateConfigs[this.form.templateType];
|
||||
if (!currentConfig) {
|
||||
this.$message.error('模板配置异常,无法导入');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 构造接口参数(tableData已不含合计行,直接映射)
|
||||
const salaryDetailList = this.tableData.map(row => {
|
||||
const formattedRow = {};
|
||||
// 只保留模板配置中的字段,避免多余参数
|
||||
currentConfig.columns.forEach(col => {
|
||||
formattedRow[col.prop] = row[col.prop] || '';
|
||||
});
|
||||
return formattedRow;
|
||||
});
|
||||
|
||||
const payload = {
|
||||
...this.paylaod,
|
||||
salaryTemplate: this.form.templateType,
|
||||
salaryDetailList
|
||||
};
|
||||
this.buttonLoading = true;
|
||||
|
||||
// 2. 调用接口
|
||||
try {
|
||||
await importSalaryMaster(payload);
|
||||
this.$message.success('数据导入成功');
|
||||
this.$emit('refresh'); // 通知父组件刷新
|
||||
this.cancelImport();
|
||||
} catch (error) {
|
||||
this.$message.error(`数据导入失败:${error.message || '未知错误'}`);
|
||||
console.error('导入错误:', error);
|
||||
} finally {
|
||||
this.buttonLoading = false;
|
||||
}
|
||||
},
|
||||
cancelImport () {
|
||||
this.showImportDialog = false;
|
||||
this.fileList = [];
|
||||
this.file = null;
|
||||
this.tableData = [];
|
||||
this.tableColumns = [];
|
||||
this.validationResult = [];
|
||||
this.validationSuccess = false;
|
||||
this.showPreview = false;
|
||||
this.tableData = [];
|
||||
this.tableColumns = [];
|
||||
this.paylaod = { unitName: '', salaryPeriod: '' };
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.validation-item {
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
// src/config/excelTemplates/kunshanDeruifuConfig.js
|
||||
export default {
|
||||
templateName: '昆山德睿福成套设备有限公司模板',
|
||||
companyName: '昆山德睿福成套设备有限公司',
|
||||
headerRows: [
|
||||
[
|
||||
"序号",
|
||||
"姓名",
|
||||
"基本\n工资",
|
||||
"岗位\n工资",
|
||||
"餐补",
|
||||
"住房\n补贴",
|
||||
"公交\n补贴",
|
||||
"出差\n天数",
|
||||
"出差\n补助",
|
||||
"奖金和福利",
|
||||
"请假\n扣款",
|
||||
"其他\n扣款",
|
||||
"应发工资",
|
||||
"个人缴纳",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"代扣\n个税",
|
||||
"奖金和福利已发放扣除",
|
||||
"实发工资",
|
||||
"企业缴纳",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"单位\n总支出"
|
||||
],
|
||||
[
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"养老保险",
|
||||
"医疗保险",
|
||||
"失业保险",
|
||||
"大病医疗",
|
||||
"住房公积金",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"养老保险",
|
||||
"医疗保险",
|
||||
"工伤保险",
|
||||
"失业保险",
|
||||
"生育保险",
|
||||
"住房公积金"
|
||||
]
|
||||
],
|
||||
columns: [
|
||||
{ prop: 'serialNumber', label: '序号' },
|
||||
{ prop: 'name', label: '姓名' },
|
||||
{ prop: 'basicSalary', label: '基本工资', isNumeric: true },
|
||||
{ prop: 'postSalary', label: '岗位工资', isNumeric: true },
|
||||
{ prop: 'mealAllowance', label: '餐补', isNumeric: true },
|
||||
{ prop: 'housingAllowance', label: '住房补贴', isNumeric: true },
|
||||
{ prop: 'busAllowance', label: '公交补贴', isNumeric: true },
|
||||
{ prop: 'businessDaysOther', label: '出差天数', isNumeric: true },
|
||||
{ prop: 'businessAllowance', label: '出差补助', isNumeric: true },
|
||||
{ prop: 'bonuses', label: '奖金和福利', isNumeric: true },
|
||||
{ prop: 'leaveDeduction', label: '请假扣款', isNumeric: true },
|
||||
{ prop: 'otherDeduction', label: '其他扣款', isNumeric: true },
|
||||
{ prop: 'grossSalary', label: '应发工资', isNumeric: true },
|
||||
{ prop: 'personalPension', label: '养老保险(个人缴纳)', isNumeric: true },
|
||||
{ prop: 'personalMedical', label: '医疗保险(个人缴纳)', isNumeric: true },
|
||||
{ prop: 'personalUnemployment', label: '失业保险(个人缴纳)', isNumeric: true },
|
||||
{ prop: 'personalBigMedical', label: '大病医疗(个人缴纳)', isNumeric: true },
|
||||
{ prop: 'personalHousingFund', label: '住房公积金(个人缴纳)', isNumeric: true },
|
||||
{ prop: 'personalTax', label: '代扣个税', isNumeric: true },
|
||||
{ prop: 'bonusesDeducted', label: '奖金和福利已发放扣除', isNumeric: true },
|
||||
{ prop: 'netSalary', label: '实发工资', isNumeric: true },
|
||||
{ prop: 'enterprisePension', label: '养老保险(企业缴纳)', isNumeric: true },
|
||||
{ prop: 'enterpriseMedical', label: '医疗保险(企业缴纳)', isNumeric: true },
|
||||
{ prop: 'enterpriseInjury', label: '工伤保险(企业缴纳)', isNumeric: true },
|
||||
{ prop: 'enterpriseUnemployment', label: '失业保险(企业缴纳)', isNumeric: true },
|
||||
{ prop: 'enterpriseMaternity', label: '生育保险(企业缴纳)', isNumeric: true },
|
||||
{ prop: 'enterpriseHousingFund', label: '住房公积金(企业缴纳)', isNumeric: true },
|
||||
{ prop: 'unitTotalExpense', label: '单位总支出', isNumeric: true }
|
||||
],
|
||||
headerRowIndices: [2, 3],
|
||||
dataStartIndex: 4,
|
||||
mergeRules: [
|
||||
{ targetHeader: '个人缴纳', colspan: 5 },
|
||||
{ targetHeader: '企业缴纳', colspan: 5 }
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
// src/config/excelTemplates/shandongFuanDeConfig.js
|
||||
export default {
|
||||
templateName: '山东福安德信息科技有限公司模板',
|
||||
companyName: '山东福安德信息科技有限公司', // 单位名称校验关键词
|
||||
headerRows: [ // 期望的表头结构(与Excel表头行对应)
|
||||
[
|
||||
"序号",
|
||||
"部门",
|
||||
"姓名",
|
||||
"出勤\n天数",
|
||||
"外出\n天数",
|
||||
"基本+岗位\n工资",
|
||||
"出差\n补助",
|
||||
"社保\n补助",
|
||||
"加班工资",
|
||||
null,
|
||||
null,
|
||||
"奖金和福利",
|
||||
"请假\n扣款",
|
||||
"其他\n扣款",
|
||||
"应发工资",
|
||||
"个人缴纳",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"个税",
|
||||
"奖金和福利已发放扣除",
|
||||
"实发工资",
|
||||
"企业缴纳",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"单位\n总支出"
|
||||
],
|
||||
[
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"时长/时",
|
||||
"标准",
|
||||
"总计",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"养老保险",
|
||||
"医疗保险",
|
||||
"失业保险",
|
||||
"住房公积金",
|
||||
"大病医疗",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"养老保险",
|
||||
"医疗保险",
|
||||
"工伤保险",
|
||||
"失业保险",
|
||||
"住房公积金",
|
||||
"大病医疗"
|
||||
]
|
||||
],
|
||||
columns: [
|
||||
{ prop: 'serialNumber', label: '序号' },
|
||||
{ prop: 'dept', label: '部门' },
|
||||
{ prop: 'name', label: '姓名' },
|
||||
{ prop: 'businessDays', label: '出勤天数', isNumeric: true },
|
||||
{ prop: 'businessDaysOther', label: '外出天数', isNumeric: true },
|
||||
{ prop: 'basicSalary', label: '基本+岗位工资', isNumeric: true, width: '120px' },
|
||||
{ prop: 'businessAllowance', label: '出差补助', isNumeric: true },
|
||||
{ prop: 'socialSecurityAllowance', label: '社保补助', isNumeric: true },
|
||||
{ prop: 'overtimeHours', label: '加班时长', isNumeric: true },
|
||||
{ prop: 'overtimeRate', label: '加班标准', isNumeric: true },
|
||||
{ prop: 'overtimeTotal', label: '加班工资', isNumeric: true },
|
||||
{ prop: 'bonuses', label: '奖金和福利', isNumeric: true },
|
||||
{ prop: 'leaveDeduction', label: '请假扣款', isNumeric: true },
|
||||
{ prop: 'otherDeduction', label: '其他扣款', isNumeric: true },
|
||||
{ prop: 'grossSalary', label: '应发工资', isNumeric: true },
|
||||
{ prop: 'personalPension', label: '个人养老保险', isNumeric: true },
|
||||
{ prop: 'personalMedical', label: '个人医疗保险', isNumeric: true },
|
||||
{ prop: 'personalUnemployment', label: '个人失业保险', isNumeric: true },
|
||||
{ prop: 'personalHousingFund', label: '个人住房公积金', isNumeric: true },
|
||||
{ prop: 'personalBigMedical', label: '个人大病医疗', isNumeric: true },
|
||||
{ prop: 'personalTax', label: '个税', isNumeric: true },
|
||||
{ prop: 'bonusesDeducted', label: '奖金和福利已发放扣除', isNumeric: true },
|
||||
{ prop: 'netSalary', label: '实发工资', isNumeric: true },
|
||||
{ prop: 'enterprisePension', label: '企业养老保险', isNumeric: true },
|
||||
{ prop: 'enterpriseMedical', label: '企业医疗保险', isNumeric: true },
|
||||
{ prop: 'enterpriseInjury', label: '企业工伤保险', isNumeric: true },
|
||||
{ prop: 'enterpriseUnemployment', label: '企业失业保险', isNumeric: true },
|
||||
{ prop: 'enterpriseHousingFund', label: '企业住房公积金', isNumeric: true },
|
||||
{ prop: 'enterpriseBigMedical', label: '企业大病医疗', isNumeric: true },
|
||||
{ prop: 'unitTotalExpense', label: '单位\n总支出', isNumeric: true }
|
||||
],
|
||||
headerRowIndices: [2, 3], // Excel中表头所在的行索引(第4、5行,0开始)
|
||||
dataStartIndex: 4, // 数据开始行索引(第6行)
|
||||
mergeRules: [ // 表头合并规则
|
||||
{ targetHeader: '个人缴纳', colspan: 5 },
|
||||
{ targetHeader: '企业缴纳', colspan: 6 }
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
// src/config/excelTemplates/kunshanDeruifuConfig.js
|
||||
export default {
|
||||
templateName: '山东首达特有限公司模板',
|
||||
companyName: '山东首达特有限公司',
|
||||
headerRows: [
|
||||
[
|
||||
"序号",
|
||||
"部门",
|
||||
"姓名",
|
||||
"出勤\n天数",
|
||||
"外出\n天数",
|
||||
"基本+岗位\n工资",
|
||||
"出差\n补助",
|
||||
"加班\n补助",
|
||||
"奖金和福利",
|
||||
"请假\n扣款",
|
||||
"其他\n扣款",
|
||||
"应发工资",
|
||||
"个人缴纳",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"个税",
|
||||
"奖金和福利\n已发放扣除",
|
||||
"实发工资",
|
||||
"企业缴纳",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"单位\n总支出",
|
||||
],
|
||||
[
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"养老保险",
|
||||
"医疗保险",
|
||||
"失业保险",
|
||||
"大病医疗",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"养老保险",
|
||||
"医疗保险",
|
||||
"工伤保险",
|
||||
"失业保险",
|
||||
"大病医疗"
|
||||
]
|
||||
],
|
||||
columns: [
|
||||
{ prop: 'serialNumber', label: '序号' },
|
||||
{ prop: 'dept', label: '部门' },
|
||||
{ prop: 'name', label: '姓名' },
|
||||
{ prop: 'businessDays', label: '出勤天数', isNumeric: true },
|
||||
{ prop: 'businessDaysOther', label: '外出天数', isNumeric: true },
|
||||
{ prop: 'basicSalary', label: '基本+岗位工资', isNumeric: true, width: '120px' },
|
||||
{ prop: 'businessAllowance', label: '出差\n补助', isNumeric: true },
|
||||
{ prop: 'overtimeTotal', label: '加班补助', isNumeric: true },
|
||||
{ prop: 'bonuses', label: '奖金和福利', isNumeric: true },
|
||||
{ prop: 'leaveDeduction', label: '请假扣款', isNumeric: true },
|
||||
{ prop: 'otherDeduction', label: '其他扣款', isNumeric: true },
|
||||
{ prop: 'grossSalary', label: '应发工资', isNumeric: true },
|
||||
{ prop: 'personalPension', label: '养老保险(个人缴纳)', isNumeric: true },
|
||||
{ prop: 'personalMedical', label: '医疗保险(个人缴纳)', isNumeric: true },
|
||||
{ prop: 'personalUnemployment', label: '失业保险(个人缴纳)', isNumeric: true },
|
||||
{ prop: 'personalBigMedical', label: '大病医疗(个人缴纳)', isNumeric: true },
|
||||
{ prop: 'personalTax', label: '个税', isNumeric: true },
|
||||
{ prop: 'bonusesDeducted', label: '奖金和福利已发放扣除', isNumeric: true },
|
||||
{ prop: 'netSalary', label: '实发工资', isNumeric: true },
|
||||
{ prop: 'enterprisePension', label: '养老保险(企业缴纳)', isNumeric: true },
|
||||
{ prop: 'enterpriseMedical', label: '医疗保险(企业缴纳)', isNumeric: true },
|
||||
{ prop: 'enterpriseInjury', label: '工伤保险(企业缴纳)', isNumeric: true },
|
||||
{ prop: 'enterpriseUnemployment', label: '失业保险(企业缴纳)', isNumeric: true },
|
||||
{ prop: 'enterpriseBigMedical', label: '大病医疗(企业缴纳)', isNumeric: true },
|
||||
{ prop: 'unitTotalExpense', label: '单位总支出', isNumeric: true }
|
||||
],
|
||||
headerRowIndices: [2, 3],
|
||||
dataStartIndex: 4,
|
||||
mergeRules: [
|
||||
{ targetHeader: '个人缴纳', colspan: 5 },
|
||||
{ targetHeader: '企业缴纳', colspan: 5 }
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
// src/config/excelTemplates/kunshanDeruifuConfig.js
|
||||
export default {
|
||||
templateName: '西安擎峰智创科技有限公司模板',
|
||||
companyName: '西安擎峰智创科技有限公司',
|
||||
headerRows: [
|
||||
[
|
||||
"序号",
|
||||
"部门",
|
||||
"姓名",
|
||||
"出勤\n天数",
|
||||
"外出\n天数",
|
||||
"基本+岗位\n工资",
|
||||
"出差\n补助",
|
||||
"加班\n补助",
|
||||
"奖金和\n福利",
|
||||
"请假\n扣款",
|
||||
"其他\n扣款",
|
||||
"应发工资",
|
||||
"个人缴纳",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"个税",
|
||||
"奖金和福利\n已发放扣除",
|
||||
"实发工资",
|
||||
"企业缴纳",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"单位\n总支出"
|
||||
],
|
||||
[
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"养老保险",
|
||||
"医疗保险",
|
||||
"失业保险",
|
||||
"大病医疗",
|
||||
"住房公积金",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"养老保险",
|
||||
"医疗保险",
|
||||
"工伤保险",
|
||||
"失业保险",
|
||||
"大病医疗",
|
||||
"住房公积金"
|
||||
]
|
||||
],
|
||||
columns: [
|
||||
{ prop: 'serialNumber', label: '序号' },
|
||||
{ prop: 'dept', label: '部门' },
|
||||
{ prop: 'name', label: '姓名' },
|
||||
{ prop: 'businessDays', label: '出勤天数', isNumeric: true },
|
||||
{ prop: 'businessDaysOther', label: '外出天数', isNumeric: true },
|
||||
{ prop: 'basicSalary', label: '基本+岗位工资', isNumeric: true, width: '120px' },
|
||||
{ prop: 'businessAllowance', label: '出差\n补助', isNumeric: true },
|
||||
{ prop: 'overtimeTotal', label: '加班补助', isNumeric: true },
|
||||
{ prop: 'bonuses', label: '奖金和福利', isNumeric: true },
|
||||
{ prop: 'leaveDeduction', label: '请假扣款', isNumeric: true },
|
||||
{ prop: 'otherDeduction', label: '其他扣款', isNumeric: true },
|
||||
{ prop: 'grossSalary', label: '应发工资', isNumeric: true },
|
||||
{ prop: 'personalPension', label: '养老保险(个人缴纳)', isNumeric: true },
|
||||
{ prop: 'personalMedical', label: '医疗保险(个人缴纳)', isNumeric: true },
|
||||
{ prop: 'personalUnemployment', label: '失业保险(个人缴纳)', isNumeric: true },
|
||||
{ prop: 'personalBigMedical', label: '大病医疗(个人缴纳)', isNumeric: true },
|
||||
{ prop: 'personalHousingFund', label: '住房公积金(个人缴纳)', isNumeric: true },
|
||||
{ prop: 'personalTax', label: '个税', isNumeric: true },
|
||||
{ prop: 'bonusesDeducted', label: '奖金和福利已发放扣除', isNumeric: true },
|
||||
{ prop: 'netSalary', label: '实发工资', isNumeric: true },
|
||||
{ prop: 'enterprisePension', label: '养老保险(企业缴纳)', isNumeric: true },
|
||||
{ prop: 'enterpriseMedical', label: '医疗保险(企业缴纳)', isNumeric: true },
|
||||
{ prop: 'enterpriseInjury', label: '工伤保险(企业缴纳)', isNumeric: true },
|
||||
{ prop: 'enterpriseUnemployment', label: '失业保险(企业缴纳)', isNumeric: true },
|
||||
{ prop: 'enterpriseBigMedical', label: '大病医疗(企业缴纳)', isNumeric: true },
|
||||
{ prop: 'enterpriseHousingFund', label: '住房公积金(企业缴纳)', isNumeric: true },
|
||||
{ prop: 'unitTotalExpense', label: '单位总支出', isNumeric: true }
|
||||
],
|
||||
headerRowIndices: [2, 3],
|
||||
dataStartIndex: 4,
|
||||
mergeRules: [
|
||||
{ targetHeader: '个人缴纳', colspan: 5 },
|
||||
{ targetHeader: '企业缴纳', colspan: 5 }
|
||||
]
|
||||
};
|
||||
333
ruoyi-ui/src/views/oa/finance/salary/record/detail.vue
Normal file
333
ruoyi-ui/src/views/oa/finance/salary/record/detail.vue
Normal file
@@ -0,0 +1,333 @@
|
||||
<template>
|
||||
<div class="app-container" v-loading="loading">
|
||||
<!-- 核心:使用 SalaryPreview 组件替代原表格(自动生成合计行) -->
|
||||
<salary-preview v-if="salaryDetailList.length > 0 && columns.length > 0" :table-data="salaryDetailList"
|
||||
:table-columns="columns" :paylaod="payload"></salary-preview>
|
||||
|
||||
<!-- 加载状态(数据未获取时显示) -->
|
||||
<el-empty v-else-if="!loading" description="暂无工资明细数据"></el-empty>
|
||||
<div v-else style="height: 100px;"></div>
|
||||
|
||||
<div>
|
||||
<el-form>
|
||||
<el-form-item label="审批留言">
|
||||
<el-input v-model="document.gmApproval" :disabled="!canD" type="textarea" :rows="3"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="canD">
|
||||
<el-button type="primary" @click="handleDocumentResolve">审批</el-button>
|
||||
<el-button type="primary" @click="handleDocumentReject">驳回</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 引入必要接口、模板配置、预览组件(删除多余依赖)
|
||||
import { addSalaryDetail, delSalaryDetail, getSalaryDetail, listSalaryDetail, updateSalaryDetail } from "@/api/oa/salaryDetail";
|
||||
import { getSalaryMaster, updateSalaryMaster } from "@/api/oa/salaryMaster";
|
||||
import SalaryPreview from "./components/SalaryPreview.vue";
|
||||
import kunshanDeruifuConfig from './components/template/kunshanDeruifuConfig';
|
||||
import shandongFuanDeConfig from './components/template/shandongFuanDeConfig';
|
||||
import shoudateConfig from './components/template/shoudateConfig';
|
||||
import xianqingfengConfig from './components/template/xianqingfengConfig';
|
||||
|
||||
export default {
|
||||
name: "SalaryDetail",
|
||||
// 注册预览组件(核心替换组件)
|
||||
components: { SalaryPreview },
|
||||
props: {
|
||||
// 接收父组件传递的主表ID(用于关联明细数据)
|
||||
masterId: {
|
||||
type: String,
|
||||
required: true, // 主ID为必传项,确保数据关联正确
|
||||
default: ''
|
||||
},
|
||||
canD: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
gmApproval: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
buttonLoading: false,
|
||||
loading: true,
|
||||
ids: [], // 批量操作选中的ID集合
|
||||
single: true, // 单选状态控制
|
||||
multiple: true, // 多选状态控制
|
||||
open: false, // 弹窗显示控制
|
||||
title: "", // 弹窗标题(新增/修改)
|
||||
total: 0, // 明细数据总数
|
||||
salaryDetailList: [], // 工资明细数据(传给预览组件的tableData)
|
||||
queryParams: { // 分页查询参数
|
||||
pageNum: 1,
|
||||
pageSize: 999, // 默认查询全部数据(明细无需分页)
|
||||
mainId: undefined,
|
||||
},
|
||||
form: {}, // 弹窗表单数据
|
||||
// 预览组件核心依赖(从主表接口获取)
|
||||
columns: [], // 表格列配置(来自模板)
|
||||
payload: { // 基础信息(单位名称、发薪时间)
|
||||
unitName: '',
|
||||
salaryPeriod: ''
|
||||
},
|
||||
document: {
|
||||
gmApproval: '',
|
||||
status: 1,
|
||||
},
|
||||
// 模板配置中心(关联主表模板类型)
|
||||
templateConfigs: {
|
||||
1: shandongFuanDeConfig, // 1: 山东福安德模板
|
||||
2: kunshanDeruifuConfig, // 2: 昆山德睿福模板
|
||||
3: shoudateConfig, // 3: 山东首达特模板
|
||||
4: xianqingfengConfig // 4: 西安擎峰模板
|
||||
},
|
||||
// 表单验证规则(补充基础必填校验)
|
||||
rules: {
|
||||
dept: [{ required: true, message: '请输入部门', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入员工姓名', trigger: 'blur' }],
|
||||
basicSalary: [{ required: true, message: '请输入基本工资', trigger: 'blur' }, { type: 'number', message: '请输入数字', trigger: 'blur' }],
|
||||
grossSalary: [{ required: true, message: '请输入应发工资', trigger: 'blur' }, { type: 'number', message: '请输入数字', trigger: 'blur' }],
|
||||
netSalary: [{ required: true, message: '请输入实发工资', trigger: 'blur' }, { type: 'number', message: '请输入数字', trigger: 'blur' }]
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
gmApproval: {
|
||||
handler (newVal) {
|
||||
this.document.gmApproval = newVal;
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
masterId: {
|
||||
handler (newVal) {
|
||||
this.queryParams.mainId = newVal;
|
||||
this.loading = true;
|
||||
this.getMaster().then(() => {
|
||||
this.getList();
|
||||
});
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
created () {
|
||||
// 初始化顺序:先获取主表信息(列配置、基础信息),再获取明细数据
|
||||
this.getMaster().then(() => {
|
||||
this.getList();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 获取主表信息(核心:获取列配置和基础信息)
|
||||
* 根据masterId获取单位名称、发薪时间、模板类型,组装预览组件所需的columns和payload
|
||||
*/
|
||||
async getMaster () {
|
||||
try {
|
||||
const response = await getSalaryMaster(this.masterId);
|
||||
const masterData = response.data || {};
|
||||
|
||||
// 1. 组装基础信息(传给预览组件的payload)
|
||||
this.payload = {
|
||||
unitName: masterData.unitName || '未识别单位',
|
||||
salaryPeriod: masterData.salaryPeriod || '未识别时间'
|
||||
};
|
||||
|
||||
// 2. 根据模板类型获取列配置(1=福安德,2=昆山德睿福)
|
||||
const templateType = masterData.salaryTemplate || 1; // 默认用福安德模板
|
||||
this.columns = this.templateConfigs[templateType]?.columns || [];
|
||||
|
||||
console.log(this.columns, templateType, this.templateConfigs)
|
||||
// 3. 若未获取到列配置,提示异常
|
||||
if (this.columns.length === 0) {
|
||||
this.$message.warning('未获取到表格列配置,请检查模板设置');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('获取主表信息失败:' + (error.message || '未知错误'));
|
||||
console.error('getMaster error:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/** 查询工资明细列表(传给预览组件的tableData) */
|
||||
getList () {
|
||||
this.loading = true;
|
||||
this.queryParams.mainId = this.masterId; // 关联主表ID
|
||||
|
||||
listSalaryDetail(this.queryParams).then(response => {
|
||||
this.salaryDetailList = response.rows || []; // 明细数据(无合计行)
|
||||
this.total = response.total || 0;
|
||||
}).catch(error => {
|
||||
this.$message.error('获取明细数据失败:' + (error.message || '未知错误'));
|
||||
console.error('getList error:', error);
|
||||
}).finally(() => {
|
||||
this.loading = false; // 无论成功失败,关闭加载状态
|
||||
});
|
||||
},
|
||||
|
||||
/** 取消弹窗(重置表单状态) */
|
||||
cancel () {
|
||||
this.open = false;
|
||||
this.reset();
|
||||
},
|
||||
|
||||
/** 重置表单(清空数据+清除验证状态) */
|
||||
reset () {
|
||||
this.form = {
|
||||
mainId: this.masterId, // 默认关联当前主表ID
|
||||
detailId: undefined,
|
||||
serialNumber: undefined,
|
||||
dept: undefined,
|
||||
name: undefined,
|
||||
basicSalary: undefined,
|
||||
postSalary: undefined,
|
||||
mealAllowance: undefined,
|
||||
housingAllowance: undefined,
|
||||
busAllowance: undefined,
|
||||
businessDaysOther: undefined,
|
||||
businessAllowance: undefined,
|
||||
socialSecurityAllowance: undefined,
|
||||
overtimeHours: undefined,
|
||||
overtimeRate: undefined,
|
||||
overtimeTotal: undefined,
|
||||
businessDays: undefined,
|
||||
bonuses: undefined,
|
||||
leaveDeduction: undefined,
|
||||
otherDeduction: undefined,
|
||||
grossSalary: undefined,
|
||||
personalPension: undefined,
|
||||
personalMedical: undefined,
|
||||
personalUnemployment: undefined,
|
||||
personalBigMedical: undefined,
|
||||
personalHousingFund: undefined,
|
||||
personalTax: undefined,
|
||||
bonusesDeducted: undefined,
|
||||
netSalary: undefined,
|
||||
enterprisePension: undefined,
|
||||
enterpriseMedical: undefined,
|
||||
enterpriseInjury: undefined,
|
||||
enterpriseUnemployment: undefined,
|
||||
enterpriseMaternity: undefined,
|
||||
enterpriseHousingFund: undefined,
|
||||
enterpriseBigMedical: undefined,
|
||||
unitTotalExpense: undefined,
|
||||
remark: undefined
|
||||
};
|
||||
this.$refs.form?.resetFields(); // 清除表单验证状态
|
||||
},
|
||||
|
||||
/** 多选框选中事件(控制批量操作按钮状态) */
|
||||
handleSelectionChange (selection) {
|
||||
this.ids = selection.map(item => item.detailId);
|
||||
this.single = selection.length !== 1; // 非单选状态
|
||||
this.multiple = selection.length === 0; // 无选中状态
|
||||
},
|
||||
|
||||
/** 新增明细(打开弹窗+重置表单) */
|
||||
handleAdd () {
|
||||
this.reset();
|
||||
this.open = true;
|
||||
this.title = "添加工资发放明细";
|
||||
},
|
||||
|
||||
/** 修改明细(获取当前数据+打开弹窗) */
|
||||
handleUpdate (row) {
|
||||
this.reset();
|
||||
const detailId = row?.detailId || this.ids[0]; // 单选或批量选中的第一个
|
||||
|
||||
getSalaryDetail(detailId).then(response => {
|
||||
this.form = response.data || {};
|
||||
this.open = true;
|
||||
this.title = "修改工资发放明细";
|
||||
}).catch(error => {
|
||||
this.$message.error('获取明细详情失败:' + (error.message || '未知错误'));
|
||||
});
|
||||
},
|
||||
|
||||
/** 提交表单(新增/修改统一处理) */
|
||||
submitForm () {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (!valid) return; // 验证不通过,不提交
|
||||
|
||||
this.buttonLoading = true;
|
||||
const request = this.form.detailId
|
||||
? updateSalaryDetail(this.form) // 有ID则修改
|
||||
: addSalaryDetail(this.form); // 无ID则新增
|
||||
|
||||
request.then(() => {
|
||||
this.$message.success(this.title + "成功");
|
||||
this.open = false;
|
||||
this.getList(); // 提交成功后,刷新明细列表
|
||||
}).catch(error => {
|
||||
this.$message.error(this.title + "失败:" + (error.message || '未知错误'));
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false; // 关闭按钮加载状态
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/** 删除明细(单条/批量删除) */
|
||||
handleDelete (row) {
|
||||
const detailIds = row?.detailId || this.ids;
|
||||
if (!detailIds.length) {
|
||||
this.$message.warning('请选择要删除的明细');
|
||||
return;
|
||||
}
|
||||
|
||||
this.$modal.confirm(`是否确认删除选中的${this.ids.length || 1}条工资明细?`).then(() => {
|
||||
return delSalaryDetail(detailIds);
|
||||
}).then(() => {
|
||||
this.$message.success("删除成功");
|
||||
this.getList(); // 删除后刷新列表
|
||||
this.ids = []; // 清空选中ID
|
||||
})
|
||||
},
|
||||
|
||||
/** 导出明细数据(保留业务必要功能) */
|
||||
handleExport () {
|
||||
this.download('oa/salaryDetail/export', {
|
||||
...this.queryParams
|
||||
}, `工资明细_${this.payload.unitName}_${this.payload.salaryPeriod}.xlsx`);
|
||||
},
|
||||
handleDocumentResolve () {
|
||||
this.document.salaryStatus = 1;
|
||||
updateSalaryMaster({ masterId: this.masterId, ...this.document }).then(response => {
|
||||
this.$message.success("审批成功");
|
||||
this.$emit('approval');
|
||||
})
|
||||
},
|
||||
handleDocumentReject () {
|
||||
this.document.salaryStatus = 2;
|
||||
updateSalaryMaster({ masterId: this.masterId, ...this.document }).then(response => {
|
||||
this.$message.success("审批成功");
|
||||
this.$emit('approval');
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 删除所有紧凑表格相关样式,保留基础布局样式 */
|
||||
.app-container {
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 弹窗底部按钮间距优化 */
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 适配小屏幕:确保预览组件横向滚动 */
|
||||
@media (max-width: 1200px) {
|
||||
.app-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1063
ruoyi-ui/src/views/oa/finance/salary/record/index.vue
Normal file
1063
ruoyi-ui/src/views/oa/finance/salary/record/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
312
ruoyi-ui/src/views/oa/finance/salary/record/list.vue
Normal file
312
ruoyi-ui/src/views/oa/finance/salary/record/list.vue
Normal file
@@ -0,0 +1,312 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 顶部:年份切换 + 操作栏 -->
|
||||
<el-row :gutter="10" style="margin-bottom: 20px;">
|
||||
<el-col :span="2">
|
||||
<el-select v-model="selectedYear" placeholder="选择年份" @change="getList">
|
||||
<el-option v-for="year in getYearOptions()" :key="year" :label="year + '年'" :value="year" />
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<ImportExcel @refresh="getList" />
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" icon="el-icon-download" size="mini" @click="handleDownloadTemplate">下载模板</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" icon="el-icon-refresh" size="mini" @click="getList">刷新</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 3*4 月份网格 -->
|
||||
<div v-loading="loading" class="salary-grid">
|
||||
<div v-for="month in 12" :key="month" class="salary-grid-item">
|
||||
<!-- 月份标题 -->
|
||||
<div class="salary-grid-header">
|
||||
{{ selectedYear }}年{{ month }}月
|
||||
</div>
|
||||
|
||||
<!-- 该月所有发薪记录 -->
|
||||
<div class="salary-grid-body">
|
||||
<div v-for="(item, idx) in getMonthRecordList(month)" :key="idx" class="record-item">
|
||||
<div class="record-info">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<dict-tag :options="dict.type.approval_status" :value="item.salaryStatus" />
|
||||
<div style="margin-left: 10px; font-size: 16px;">{{ item.unitName }}</div>
|
||||
</div>
|
||||
<div v-if="item.remark">备注:{{ item.remark }}</div>
|
||||
</div>
|
||||
|
||||
<div class="record-btns">
|
||||
<el-button size="mini" text icon="el-icon-view" @click="handleView(item)">查看</el-button>
|
||||
|
||||
<el-button size="mini" text icon="el-icon-document"
|
||||
v-if="isGaoZong && (item.salaryStatus == 0 || item.salaryStatus == null)"
|
||||
v-permission="['oa:finance:salary:gm']" @click="handleDocument(item)">审批</el-button>
|
||||
|
||||
<el-button size="mini" text icon="el-icon-edit" @click="handleUpdate(item)">修改</el-button>
|
||||
<el-button size="mini" text icon="el-icon-delete" @click="handleDelete(item)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!getMonthRecordList(month).length" class="empty-tip">
|
||||
暂无发薪记录
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当月新增按钮 -->
|
||||
<!-- <div class="salary-grid-footer">
|
||||
<el-button size="mini" type="primary" @click="handleAddByMonth(month)">
|
||||
新增{{ month }}月记录
|
||||
</el-button>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog :title="title" :visible.sync="detailOpen" width="90%" append-to-body>
|
||||
<Detail :masterId="detailId" :visible.sync="detailOpen" :canD="canD" :gmApproval="gmApproval" @approval="() => {
|
||||
this.getList();
|
||||
this.detailOpen = false;
|
||||
}" />
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新增/修改弹窗 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="单位名称" prop="unitName">
|
||||
<el-input v-model="form.unitName" placeholder="请输入单位名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发薪时间" prop="salaryPeriod">
|
||||
<el-date-picker value-format="yyyy-MM" type="month" v-model="form.salaryPeriod" placeholder="自动生成" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" placeholder="请输入备注" type="textarea" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addSalaryMaster, delSalaryMaster, getSalaryMaster, listSalaryMaster, updateSalaryMaster } from "@/api/oa/salaryMaster";
|
||||
import ImportExcel from './components/importExcel.vue';
|
||||
import Detail from './detail.vue';
|
||||
|
||||
export default {
|
||||
name: "SalaryMaster",
|
||||
components: { ImportExcel, Detail },
|
||||
dicts: ['approval_status'],
|
||||
data () {
|
||||
return {
|
||||
buttonLoading: false,
|
||||
canD: false,
|
||||
loading: true,
|
||||
ids: [],
|
||||
total: 0,
|
||||
salaryMasterList: [],
|
||||
detailId: '',
|
||||
detailOpen: false,
|
||||
title: "",
|
||||
open: false,
|
||||
selectedYear: new Date().getFullYear(),
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 1000, // 一次拉够
|
||||
unitName: undefined,
|
||||
salaryPeriod: undefined,
|
||||
},
|
||||
form: {},
|
||||
rules: {},
|
||||
gmApproval: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isGaoZong () {
|
||||
return this.$store.getters.id === '1859252208375152641' || this.$store.getters.id == 1;
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.getList();
|
||||
},
|
||||
methods: {
|
||||
// 年份选项
|
||||
getYearOptions () {
|
||||
const curr = new Date().getFullYear();
|
||||
return Array.from({ length: 10 }, (_, i) => curr - i);
|
||||
},
|
||||
|
||||
// 获取某月份**所有**记录
|
||||
getMonthRecordList (month) {
|
||||
const m = month < 10 ? '0' + month : month;
|
||||
const prefix = `${this.selectedYear}-${m}`;
|
||||
return this.salaryMasterList.filter(item =>
|
||||
item.salaryPeriod && item.salaryPeriod.startsWith(prefix)
|
||||
);
|
||||
},
|
||||
|
||||
// 查询
|
||||
getList () {
|
||||
this.loading = true;
|
||||
// this.queryParams.salaryPeriod = this.selectedYear + '';
|
||||
listSalaryMaster(this.queryParams).then(res => {
|
||||
this.salaryMasterList = res.rows;
|
||||
this.total = res.total;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
|
||||
// 按月份新增
|
||||
handleAddByMonth (month) {
|
||||
this.reset();
|
||||
const m = month < 10 ? '0' + month : month;
|
||||
this.form.salaryPeriod = `${this.selectedYear}-${m}`;
|
||||
this.open = true;
|
||||
this.title = `${this.selectedYear}年${month}月 - 添加工资记录`;
|
||||
},
|
||||
|
||||
// 下载模板
|
||||
handleDownloadTemplate () {
|
||||
this.$download.oss('1978038274090577922');
|
||||
},
|
||||
cancel () {
|
||||
this.open = false;
|
||||
this.reset();
|
||||
},
|
||||
reset () {
|
||||
this.form = {
|
||||
masterId: undefined,
|
||||
unitName: undefined,
|
||||
salaryPeriod: undefined,
|
||||
remark: undefined,
|
||||
};
|
||||
this.$refs.form?.clearValidate();
|
||||
},
|
||||
|
||||
// 修改
|
||||
handleUpdate (row) {
|
||||
this.loading = true;
|
||||
getSalaryMaster(row.masterId).then(res => {
|
||||
this.form = res.data;
|
||||
this.open = true;
|
||||
this.title = "修改工资记录";
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
|
||||
// 查看
|
||||
handleView (row) {
|
||||
this.detailId = row.masterId;
|
||||
this.detailOpen = true;
|
||||
this.title = "工资详情";
|
||||
this.canD = false;
|
||||
this.gmApproval = row.gmApproval;
|
||||
},
|
||||
|
||||
// 审批
|
||||
handleDocument (row) {
|
||||
this.detailId = row.masterId;
|
||||
this.detailOpen = true;
|
||||
this.title = "工资审批";
|
||||
this.canD = true;
|
||||
},
|
||||
|
||||
// 提交
|
||||
submitForm () {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (!valid) return;
|
||||
this.buttonLoading = true;
|
||||
const api = this.form.masterId ? updateSalaryMaster : addSalaryMaster;
|
||||
api(this.form).then(() => {
|
||||
this.$modal.msgSuccess(this.form.masterId ? '修改成功' : '新增成功');
|
||||
this.open = false;
|
||||
this.getList();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 删除
|
||||
handleDelete (row) {
|
||||
this.$modal.confirm('确认删除该条记录?').then(() => {
|
||||
this.loading = true;
|
||||
return delSalaryMaster(row.masterId);
|
||||
}).then(() => {
|
||||
this.getList();
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.salary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.salary-grid-item {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
.salary-grid-header {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
}
|
||||
|
||||
.salary-grid-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
padding: 8px 6px;
|
||||
border-bottom: 1px dashed #aaa;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.record-info {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.record-btns {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.salary-grid-footer {
|
||||
text-align: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user