整合前端

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

View File

@@ -0,0 +1,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>

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

File diff suppressed because it is too large Load Diff

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

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
<template>
<div ref="chart" style="width:100%;height:300px"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: 'BarChart',
props: {
data: {
type: Array,
default: () => []
},
type: {
type: String,
default: 'monthly' // 'monthly' 或 'insurance'
}
},
data() {
return {
chart: null
};
},
mounted() {
this.chart = echarts.init(this.$refs.chart);
this.draw();
window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
if (this.chart) {
this.chart.dispose();
this.chart = null;
}
window.removeEventListener('resize', this.handleResize);
},
watch: {
data: {
handler() {
this.draw();
},
deep: true
}
},
methods: {
draw() {
if (!this.chart || !this.data || this.data.length === 0) {
this.showEmptyState();
return;
}
if (this.type === 'insurance') {
this.drawInsuranceChart();
} else {
this.drawMonthlyChart();
}
},
drawMonthlyChart() {
const months = this.data.map(i => i.month);
const netSalaryData = this.data.map(i => i.netSalary || 0);
const grossSalaryData = this.data.map(i => i.grossSalary || 0);
const unitExpenseData = this.data.map(i => i.unitExpense || 0);
this.chart.setOption({
title: { show: false },
tooltip: {
trigger: 'axis',
formatter: function(params) {
let result = params[0].axisValue + '<br/>';
params.forEach(param => {
result += param.marker + param.seriesName + ': ¥' + Number(param.value).toLocaleString() + '<br/>';
});
return result;
}
},
legend: {
data: ['实发工资', '应发工资', '单位支出'],
bottom: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: months,
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
axisLabel: {
formatter: function(value) {
return '¥' + (value / 10000).toFixed(1) + 'w';
}
}
},
series: [
{
name: '实发工资',
type: 'bar',
data: netSalaryData,
itemStyle: { color: '#409EFF' }
},
{
name: '应发工资',
type: 'bar',
data: grossSalaryData,
itemStyle: { color: '#67C23A' }
},
{
name: '单位支出',
type: 'bar',
data: unitExpenseData,
itemStyle: { color: '#E6A23C' }
}
],
});
},
drawInsuranceChart() {
const itemNames = this.data.map(i => i.itemName);
const personalData = this.data.map(i => i.personal || 0);
const enterpriseData = this.data.map(i => i.enterprise || 0);
this.chart.setOption({
title: { show: false },
tooltip: {
trigger: 'axis',
formatter: function(params) {
let result = params[0].axisValue + '<br/>';
params.forEach(param => {
result += param.marker + param.seriesName + ': ¥' + Number(param.value).toLocaleString() + '<br/>';
});
return result;
}
},
legend: {
data: ['个人缴纳', '企业缴纳'],
bottom: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: itemNames,
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
axisLabel: {
formatter: function(value) {
return '¥' + (value / 10000).toFixed(1) + 'w';
}
}
},
series: [
{
name: '个人缴纳',
type: 'bar',
data: personalData,
itemStyle: { color: '#409EFF' }
},
{
name: '企业缴纳',
type: 'bar',
data: enterpriseData,
itemStyle: { color: '#F56C6C' }
}
],
});
},
showEmptyState() {
if (!this.chart) return;
this.chart.setOption({
title: {
text: '暂无数据',
left: 'center',
top: 'middle',
textStyle: {
color: '#999',
fontSize: 16
}
},
xAxis: { show: false },
yAxis: { show: false },
series: []
});
},
handleResize() {
if (this.chart) {
this.chart.resize();
}
}
},
};
</script>

View File

@@ -0,0 +1,85 @@
<template>
<div>
<el-table :data="pagedData" border>
<el-table-column prop="deptName" label="部门名称" min-width="120" />
<el-table-column prop="employeeCount" label="员工数量" width="100" align="center" />
<el-table-column prop="totalNetSalary" label="总实发工资" min-width="120" align="right" />
<el-table-column prop="totalGrossSalary" label="总应发工资" min-width="120" align="right" />
<el-table-column prop="avgNetSalary" label="人均实发工资" min-width="120" align="right" />
<el-table-column prop="avgGrossSalary" label="人均应发工资" min-width="120" align="right" />
<el-table-column prop="yearOnYearGrowthRate" label="同比增长率" width="100" align="center">
<template slot-scope="scope">
<span :class="getGrowthClass(scope.row.yearOnYearGrowthRate)">
{{ scope.row.yearOnYearGrowthRate }}
</span>
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="total > pageSize"
:page-size="pageSize"
:total="total"
:current-page="currentPage"
layout="prev, pager, next, total"
@current-change="onPageChange"
style="text-align:right; margin-top:10px"
/>
</div>
</template>
<script>
export default {
name: 'DepartmentTable',
props: {
data: {
type: Array,
default: () => []
},
total: {
type: Number,
default: 0
},
pageSize: {
type: Number,
default: 10
}
},
data() {
return {
currentPage: 1
};
},
computed: {
pagedData() {
const start = (this.currentPage - 1) * this.pageSize;
return this.data.slice(start, start + this.pageSize);
},
},
methods: {
onPageChange(page) {
this.currentPage = page;
},
getGrowthClass(growthRate) {
if (!growthRate) return '';
if (growthRate.startsWith('+')) {
return 'growth-positive';
} else if (growthRate.startsWith('-')) {
return 'growth-negative';
}
return '';
}
},
};
</script>
<style scoped>
.growth-positive {
color: #67c23a;
font-weight: bold;
}
.growth-negative {
color: #f56c6c;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,197 @@
<template>
<div ref="chart" style="width:100%;height:300px"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: 'LineChart',
props: {
data: {
type: Array,
default: () => [],
validator: function(value) {
return Array.isArray(value) && value.every(item =>
item && typeof item.month === 'string' &&
typeof item.total === 'number' &&
typeof item.avg === 'number'
);
}
}
},
data() {
return {
chart: null,
};
},
watch: {
data: {
handler(newData) {
if (newData && newData.length > 0 && this.chart) {
this.updateChart();
} else if (this.chart) {
// 如果没有数据,显示空状态
this.showEmptyState();
}
},
deep: true,
},
},
mounted() {
this.initChart();
// 监听窗口大小变化,重新绘制图表
window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
if (this.chart) {
this.chart.dispose();
this.chart = null;
}
// 移除事件监听器
window.removeEventListener('resize', this.handleResize);
},
methods: {
initChart() {
if (this.$refs.chart) {
this.chart = echarts.init(this.$refs.chart);
if (this.data && this.data.length > 0) {
this.updateChart();
} else {
this.showEmptyState();
}
}
},
updateChart() {
if (!this.chart || !this.data || this.data.length === 0) {
this.showEmptyState();
return;
}
const months = this.data.map(item => item.month);
const netSalaryData = this.data.map(item => Number(item.netSalary || 0));
const grossSalaryData = this.data.map(item => Number(item.grossSalary || 0));
const avgNetSalaryData = this.data.map(item => Number(item.avgNetSalary || 0));
this.chart.setOption({
title: {
show: false // 清除空状态的标题
},
tooltip: {
trigger: 'axis',
formatter: function(params) {
let result = params[0].axisValue + '<br/>';
params.forEach(param => {
result += param.marker + param.seriesName + ': ¥' + Number(param.value).toLocaleString() + '<br/>';
});
return result;
}
},
legend: {
data: ['实发工资总额', '应发工资总额', '人均实发工资'],
bottom: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: months,
show: true, // 确保x轴显示
axisLabel: {
rotate: 45
}
},
yAxis: [
{
type: 'value',
name: '总额(万元)',
position: 'left',
axisLabel: {
formatter: function(value) {
return (value / 10000).toFixed(1) + 'w';
}
}
},
{
type: 'value',
name: '人均(元)',
position: 'right',
axisLabel: {
formatter: '¥{value}'
}
}
],
series: [
{
name: '实发工资总额',
type: 'line',
yAxisIndex: 0,
data: netSalaryData,
smooth: true,
itemStyle: {
color: '#409EFF'
},
lineStyle: {
width: 3
}
},
{
name: '应发工资总额',
type: 'line',
yAxisIndex: 0,
data: grossSalaryData,
smooth: true,
itemStyle: {
color: '#67C23A'
},
lineStyle: {
width: 3
}
},
{
name: '人均实发工资',
type: 'line',
yAxisIndex: 1,
data: avgNetSalaryData,
smooth: true,
itemStyle: {
color: '#E6A23C'
},
lineStyle: {
width: 2,
type: 'dashed'
}
},
],
});
},
showEmptyState() {
if (!this.chart) return;
this.chart.setOption({
title: {
text: '暂无数据',
left: 'center',
top: 'middle',
textStyle: {
color: '#999',
fontSize: 16
}
},
xAxis: { show: false },
yAxis: { show: false },
series: []
});
},
handleResize() {
if (this.chart) {
this.chart.resize();
}
}
},
};
</script>

View File

@@ -0,0 +1,127 @@
<template>
<div ref="chart" style="width:100%;height:300px"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: 'PieChart',
props: {
data: { type: Array, default: () => [] },
categories: { type: Array, default: () => [] },
},
data() {
return {
chart: null,
};
},
watch: {
data: {
handler(newData) {
if (this.chart) {
this.updateChart();
}
},
deep: true,
},
},
mounted() {
this.initChart();
window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
if (this.chart) {
this.chart.dispose();
this.chart = null;
}
window.removeEventListener('resize', this.handleResize);
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.chart);
this.updateChart();
},
updateChart() {
if (!this.chart) return;
if (!this.data || this.data.length === 0) {
this.showEmptyState();
return;
}
this.chart.setOption({
title: { show: false },
tooltip: {
trigger: 'item',
formatter: function(params) {
const percentage = params.data.percentage ?
` (${params.data.percentage}%)` :
` (${params.percent}%)`;
return params.marker + params.name + ': ¥' +
Number(params.value).toLocaleString() + percentage;
}
},
legend: {
orient: 'vertical',
left: 'left',
top: 'middle',
textStyle: {
fontSize: 12
}
},
series: [{
type: 'pie',
radius: ['40%', '70%'],
center: ['65%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 5,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '16',
fontWeight: 'bold',
formatter: function(params) {
return params.name + '\
¥' + Number(params.value).toLocaleString();
}
}
},
labelLine: {
show: false
},
data: this.data
}],
});
},
showEmptyState() {
if (!this.chart) return;
this.chart.setOption({
title: {
text: '暂无数据',
left: 'center',
top: 'middle',
textStyle: {
color: '#999',
fontSize: 16
}
},
series: []
});
},
handleResize() {
if (this.chart) {
this.chart.resize();
}
}
},
};
</script>

View File

@@ -0,0 +1,82 @@
<template>
<el-card class="stat-card">
<div class="stat-title">{{ title }}</div>
<div class="stat-value">{{ value }}</div>
<div class="stat-sub" :class="getSubClass">
<i v-if="growth === 'up'" class="el-icon-arrow-up stat-icon" />
<i v-else-if="growth === 'down'" class="el-icon-arrow-down stat-icon" />
<i v-else class="el-icon-minus stat-icon" />
<span>{{ subText }}</span>
</div>
</el-card>
</template>
<script>
export default {
name: 'StatisticCard',
props: {
title: String,
value: String,
subText: String,
growth: { type: String, default: 'neutral' }, // 'up', 'down', 'neutral'
},
computed: {
getSubClass() {
return {
'stat-sub-up': this.growth === 'up',
'stat-sub-down': this.growth === 'down',
'stat-sub-neutral': this.growth === 'neutral'
};
}
}
};
</script>
<style scoped>
.stat-card {
text-align: center;
padding: 20px;
height: 140px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.stat-title {
font-size: 14px;
color: #666;
margin-bottom: 8px;
font-weight: 500;
}
.stat-value {
font-size: 24px;
font-weight: bold;
margin-bottom: 6px;
color: #303133;
}
.stat-sub {
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
.stat-sub-up {
color: #67C23A;
}
.stat-sub-down {
color: #F56C6C;
}
.stat-sub-neutral {
color: #909399;
}
.stat-icon {
margin-right: 4px;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<div>
<el-table :data="pagedData" border>
<el-table-column prop="unitName" label="单位名称" min-width="120" />
<el-table-column prop="employeeCount" label="员工数量" width="100" align="center" />
<el-table-column prop="totalNetSalary" label="总实发工资" min-width="120" align="right" />
<el-table-column prop="totalGrossSalary" label="总应发工资" min-width="120" align="right" />
<el-table-column prop="totalUnitExpense" label="单位总支出" min-width="120" align="right" />
<el-table-column prop="avgNetSalary" label="人均实发工资" min-width="120" align="right" />
<el-table-column prop="yearOnYearGrowthRate" label="同比增长率" width="100" align="center">
<template slot-scope="scope">
<span :class="getGrowthClass(scope.row.yearOnYearGrowthRate)">
{{ scope.row.yearOnYearGrowthRate }}
</span>
</template>
</el-table-column>
<el-table-column prop="salaryPeriod" label="工资期间" width="100" align="center" />
</el-table>
<el-pagination
v-if="total > pageSize"
:page-size="pageSize"
:total="total"
:current-page="currentPage"
layout="prev, pager, next, total"
@current-change="onPageChange"
style="text-align:right; margin-top:10px"
/>
</div>
</template>
<script>
export default {
name: 'UnitTable',
props: {
data: {
type: Array,
default: () => []
},
total: {
type: Number,
default: 0
},
pageSize: {
type: Number,
default: 10
}
},
data() {
return {
currentPage: 1
};
},
computed: {
pagedData() {
const start = (this.currentPage - 1) * this.pageSize;
return this.data.slice(start, start + this.pageSize);
},
},
methods: {
onPageChange(page) {
this.currentPage = page;
},
getGrowthClass(growthRate) {
if (!growthRate) return '';
if (growthRate.startsWith('+')) {
return 'growth-positive';
} else if (growthRate.startsWith('-')) {
return 'growth-negative';
}
return '';
}
},
};
</script>
<style scoped>
.growth-positive {
color: #67c23a;
font-weight: bold;
}
.growth-negative {
color: #f56c6c;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,322 @@
<template>
<div style="padding:20px">
<!-- 时间范围选择 -->
<el-row class="mb8">
<el-col :span="4">
<el-date-picker
v-model="selectedTime"
type="month"
placeholder="选择年月"
value-format="yyyy-MM-01"
>
</el-date-picker>
</el-col>
</el-row>
<!-- 卡片数据展示 -->
<el-row :gutter="20" v-loading="loading" style="margin-bottom: 20px">
<el-col :span="24">
<el-row :gutter="20">
<el-col :span="4" v-for="card in cards" :key="card.title">
<statistic-card
:title="card.title"
:value="card.value"
:sub-text="card.subText"
:growth="card.growth"
/>
</el-col>
</el-row>
</el-col>
</el-row>
<!-- 图表区域第一行 -->
<el-row :gutter="20" style="margin-bottom: 20px">
<el-col :span="12">
<el-card v-loading="monthlyExpenseLoading">
<div slot="header" class="clearfix">
<span>月度支出汇总</span>
</div>
<bar-chart :data="monthlyExpenseData" />
</el-card>
</el-col>
<el-col :span="12">
<el-card v-loading="salaryStructureLoading">
<div slot="header" class="clearfix">
<span>工资构成分析</span>
</div>
<pie-chart :data="salaryStructureData" :categories="salaryStructureCategories" />
</el-card>
</el-col>
</el-row>
<!-- 图表区域第二行 -->
<el-row :gutter="20" style="margin-bottom: 20px">
<el-col :span="12">
<el-card v-loading="trendLoading">
<div slot="header" class="clearfix">
<span>工资趋势分析</span>
</div>
<line-chart :data="trendData" />
</el-card>
</el-col>
<el-col :span="12">
<el-card v-loading="insuranceLoading">
<div slot="header" class="clearfix">
<span>社保公积金统计</span>
</div>
<bar-chart :data="insuranceData" :type="'insurance'" />
</el-card>
</el-col>
</el-row>
<!-- 统计表格 -->
<el-row :gutter="20">
<el-col :span="12">
<el-card v-loading="unitTableLoading">
<div slot="header" class="clearfix">
<span>单位统计</span>
</div>
<unit-table :data="unitTableData" :total="unitTableTotal" />
</el-card>
</el-col>
<el-col :span="12">
<el-card v-loading="deptTableLoading">
<div slot="header" class="clearfix">
<span>部门统计</span>
</div>
<department-table :data="deptTableData" :total="deptTableTotal" />
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import { getAllData } from '@/api/oa/finance/dashboard';
import BarChart from './components/BarChart.vue';
import DepartmentTable from './components/DepartmentTable.vue';
import UnitTable from './components/UnitTable.vue';
import LineChart from './components/LineChart.vue';
import PieChart from './components/PieChart.vue';
import StatisticCard from './components/StatisticCard.vue';
export default {
name: 'SalaryDashboard',
components: {
StatisticCard,
BarChart,
PieChart,
LineChart,
DepartmentTable,
UnitTable
},
created() {
const now = new Date();
this.selectedTime = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-01`;
},
watch: {
selectedTime: {
handler(val) {
if (val) {
const date = new Date(val);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const salaryPeriod = `${year}-${month}`;
this.fetchData(salaryPeriod);
}
},
immediate: true
}
},
data() {
return {
selectedTime: null,
loading: false,
monthlyExpenseLoading: false,
salaryStructureLoading: false,
trendLoading: false,
insuranceLoading: false,
unitTableLoading: false,
deptTableLoading: false,
cards: [],
monthlyExpenseData: [],
salaryStructureCategories: [],
salaryStructureData: [],
trendData: [],
insuranceData: [],
unitTableData: [],
unitTableTotal: 0,
deptTableData: [],
deptTableTotal: 0,
};
},
methods: {
async fetchData(salaryPeriod) {
this.loading = true;
this.monthlyExpenseLoading = true;
this.salaryStructureLoading = true;
this.trendLoading = true;
this.insuranceLoading = true;
this.unitTableLoading = true;
this.deptTableLoading = true;
try {
const response = await getAllData({
salaryPeriod: salaryPeriod
});
if(response.code === 200) {
const { data } = response;
const { cardData, chartData, unitStats, deptStats } = data;
// 处理卡片数据
this.processCardData(cardData);
// 处理图表数据
this.processChartData(chartData);
// 处理表格数据
this.processTableData(unitStats, deptStats);
}
} catch (error) {
console.error('获取数据失败:', error);
this.$message.error('获取数据失败,请稍后重试');
} finally {
this.loading = false;
this.monthlyExpenseLoading = false;
this.salaryStructureLoading = false;
this.trendLoading = false;
this.insuranceLoading = false;
this.unitTableLoading = false;
this.deptTableLoading = false;
}
},
// 处理卡片数据
processCardData(cardData) {
this.cards = [
{
title: '总实发工资',
value: this.formatCurrency(cardData.totalNetSalary),
subText: `较上月${this.getGrowthText(cardData.lastMonthNetSalaryRate)}`,
growth: this.getGrowthDirection(cardData.lastMonthNetSalaryRate)
},
{
title: '总应发工资',
value: this.formatCurrency(cardData.totalGrossSalary),
subText: `较上月${this.getGrowthText(cardData.lastMonthGrossSalaryRate)}`,
growth: this.getGrowthDirection(cardData.lastMonthGrossSalaryRate)
},
{
title: '单位总支出',
value: this.formatCurrency(cardData.totalUnitExpense),
subText: `较上月${this.getGrowthText(cardData.lastMonthUnitExpenseRate)}`,
growth: this.getGrowthDirection(cardData.lastMonthUnitExpenseRate)
},
{
title: '人均实发工资',
value: this.formatCurrency(cardData.avgNetSalary),
subText: `较上月${this.getGrowthText(cardData.lastMonthAvgNetSalaryRate)}`,
growth: this.getGrowthDirection(cardData.lastMonthAvgNetSalaryRate)
},
{
title: '同比增长率',
value: `${cardData.yearOnYearGrowthRate || 0}%`,
subText: '较去年同期',
growth: this.getGrowthDirection(cardData.yearOnYearGrowthRate)
},
{
title: '员工总数',
value: `${cardData.totalEmployeeCount || 0}`,
subText: `${cardData.unitCount || 0}个单位`,
growth: 'neutral'
}
];
},
// 处理图表数据
processChartData(chartData) {
// 月度支出数据
this.monthlyExpenseData = chartData.monthlyExpenses?.map(item => ({
month: `${item.month}`,
netSalary: item.totalNetSalary,
grossSalary: item.totalGrossSalary,
unitExpense: item.totalUnitExpense
})) || [];
// 工资构成分析数据
this.salaryStructureCategories = chartData.salaryStructures?.map(item => item.itemName) || [];
this.salaryStructureData = chartData.salaryStructures?.map(item => ({
name: item.itemName,
value: item.totalAmount,
percentage: item.percentage
})) || [];
// 趋势分析数据
if (chartData.trendData) {
this.trendData = chartData.trendData.months?.map((month, index) => ({
month: `${month}`,
netSalary: chartData.trendData.netSalaryTrend?.[index] || 0,
grossSalary: chartData.trendData.grossSalaryTrend?.[index] || 0,
avgNetSalary: chartData.trendData.avgNetSalaryTrend?.[index] || 0
})) || [];
}
// 社保公积金统计数据
this.insuranceData = chartData.insuranceStats?.map(item => ({
itemName: item.itemName,
personal: item.personalTotal,
enterprise: item.enterpriseTotal,
total: item.total
})) || [];
},
// 处理表格数据
processTableData(unitStats, deptStats) {
// 单位统计数据
this.unitTableData = unitStats?.rows?.map(item => ({
unitName: item.unitName,
employeeCount: item.employeeCount,
totalNetSalary: this.formatCurrency(item.totalNetSalary),
totalGrossSalary: this.formatCurrency(item.totalGrossSalary),
totalUnitExpense: this.formatCurrency(item.totalUnitExpense),
avgNetSalary: this.formatCurrency(item.avgNetSalary),
yearOnYearGrowthRate: `${item.yearOnYearGrowthRate >= 0 ? '+' : ''}${item.yearOnYearGrowthRate}%`,
salaryPeriod: item.salaryPeriod
})) || [];
this.unitTableTotal = unitStats?.total || 0;
// 部门统计数据
this.deptTableData = deptStats?.rows?.map(item => ({
deptName: item.deptName,
employeeCount: item.employeeCount,
totalNetSalary: this.formatCurrency(item.totalNetSalary),
totalGrossSalary: this.formatCurrency(item.totalGrossSalary),
avgNetSalary: this.formatCurrency(item.avgNetSalary),
avgGrossSalary: this.formatCurrency(item.avgGrossSalary),
yearOnYearGrowthRate: `${item.yearOnYearGrowthRate >= 0 ? '+' : ''}${item.yearOnYearGrowthRate}%`
})) || [];
this.deptTableTotal = deptStats?.total || 0;
},
// 格式化货币
formatCurrency(amount) {
if (!amount) return '¥ 0';
return `¥ ${Number(amount).toLocaleString()}`;
},
// 获取增长文本
getGrowthText(rate) {
if (!rate) return '无变化';
const direction = rate >= 0 ? '增长' : '下降';
return `${direction} ${Math.abs(rate)}%`;
},
// 获取增长方向
getGrowthDirection(rate) {
if (!rate) return 'neutral';
return rate >= 0 ? 'up' : 'down';
}
}
};
</script>

View File

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

View File

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

View File

@@ -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 }
]
};

View File

@@ -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 }
]
};

View File

@@ -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 }
]
};

View File

@@ -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 }
]
};

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

File diff suppressed because it is too large Load Diff

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