Files
salaryAccountingBackend/src/views/payroll/sheets/index.vue
T

828 lines
25 KiB
Vue

<template>
<div class="payroll-sheets-container">
<el-card>
<template #header>
<div class="card-header">
<span class="title">工资条列表</span>
<div class="header-right">
<el-date-picker v-model="dateRange" type="month" placeholder="选择月份" :default-time="defaultTime"
:shortcuts="dateShortcuts" @change="handleDateChange" style="width: 200px; margin-right: 8px;" />
<el-input v-model="searchKeyword" placeholder="请输入姓名搜索" clearable @input="handleSearch"
style="width: 200px; margin-right: 8px;">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
<el-button type="success" @click="handleBatchShowToEmployee">批量展示给员工</el-button>
</div>
</div>
</template>
<el-table :data="payrollList" style="width: 100%" height="calc(100vh - 280px)" border stripe :header-cell-style="{
background: '#f5f7fa',
color: '#606266',
fontWeight: 'bold',
textAlign: 'center',
padding: '12px 0'
}" :cell-style="{
padding: '8px 0'
}" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column label="头像" width="80" align="center" fixed="left">
<template #default="scope">
<el-avatar :size="40" :src="scope.row.avatar" />
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" width="100" show-overflow-tooltip fixed="left" />
<el-table-column prop="baseSalary" label="基本工资" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.baseSalary) }}
</template>
</el-table-column>
<el-table-column prop="annualPerformanceBonus" label="年度绩效奖金" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.annualPerformanceBonus) }}
</template>
</el-table-column>
<el-table-column prop="monthlyPerformanceBonus" label="月度绩效奖金" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.monthlyPerformanceBonus) }}
</template>
</el-table-column>
<el-table-column prop="otherBonus" label="其他奖金" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.otherBonus) }}
</template>
</el-table-column>
<el-table-column prop="managerAllowance" label="总经理补贴" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.managerAllowance) }}
</template>
</el-table-column>
<el-table-column prop="transportationAllowance" label="交通补贴" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.transportationAllowance) }}
</template>
</el-table-column>
<el-table-column prop="otherAllowance" label="其他补贴" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.otherAllowance) }}
</template>
</el-table-column>
<el-table-column prop="senioritySalary" label="工龄工资" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.senioritySalary) }}
</template>
</el-table-column>
<el-table-column prop="overtimePay" label="加班工资" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.overtimePay) }}
</template>
</el-table-column>
<el-table-column prop="grossPaySummary" label="应发小计" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.grossPaySummary) }}
</template>
</el-table-column>
<el-table-column prop="annualPerformanceDeduction" label="年度绩效扣款" width="120" align="right"
show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.annualPerformanceDeduction) }}
</template>
</el-table-column>
<el-table-column prop="monthlyPerformanceDeduction" label="月度绩效扣款" width="120" align="right"
show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.monthlyPerformanceDeduction) }}
</template>
</el-table-column>
<el-table-column prop="personalLeaveDeduction" label="事假扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.personalLeaveDeduction) }}
</template>
</el-table-column>
<el-table-column prop="sickLeaveDeduction" label="病假扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.sickLeaveDeduction) }}
</template>
</el-table-column>
<el-table-column prop="lateEarlyDeduction" label="迟到早退扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.lateEarlyDeduction) }}
</template>
</el-table-column>
<el-table-column prop="missedCheckInOutDeduction" label="签到签退扣款" width="120" align="right"
show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.missedCheckInOutDeduction) }}
</template>
</el-table-column>
<el-table-column prop="absentCardDeduction" label="缺卡扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.absentCardDeduction) }}
</template>
</el-table-column>
<el-table-column prop="absenteeismDeduction" label="旷工扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.absenteeismDeduction) }}
</template>
</el-table-column>
<el-table-column prop="violationDeduction" label="违纪扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.violationDeduction) }}
</template>
</el-table-column>
<el-table-column prop="basicPensionDeduction" label="基本养老" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.basicPensionDeduction) }}
</template>
</el-table-column>
<el-table-column prop="unemploymentInsuranceDeduction" label="失业保险" width="120" align="right"
show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.unemploymentInsuranceDeduction) }}
</template>
</el-table-column>
<el-table-column prop="basicMedicalDeduction" label="基本医疗" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.basicMedicalDeduction) }}
</template>
</el-table-column>
<el-table-column prop="medicalDeductionAmount" label="大额医疗" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.medicalDeductionAmount) }}
</template>
</el-table-column>
<el-table-column prop="housingFundDeduction" label="住房公积金" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.housingFundDeduction) }}
</template>
</el-table-column>
<el-table-column prop="otherDeduction" label="其他扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.otherDeduction) }}
</template>
</el-table-column>
<el-table-column prop="individualTaxDeduction" label="个税" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.individualTaxDeduction) }}
</template>
</el-table-column>
<el-table-column prop="deductionTotal" label="扣款小计" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.deductionTotal) }}
</template>
</el-table-column>
<el-table-column prop="netSalary" label="实发工资" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.netSalary) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="scope">
<el-button type="primary" link size="small" @click="handleEdit(scope.row)">
修改
</el-button>
<el-button type="success" link size="small" @click="handleShowToEmployee(scope.row)">
展示给员工
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 20, 50, 100]"
:total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
@current-change="handleCurrentChange" />
</div>
</el-card>
<!-- 修改工资条对话框 -->
<div class="edit-payroll-dialog">
<el-dialog v-model="editDialogVisible" title="修改工资条" width="900px">
<div class="edit-form">
<el-form ref="editFormRef" :model="editForm" :rules="editRules" label-width="140px">
<!-- 仅保留可编辑字段 -->
<div class="form-section">
<div class="section-title">违纪扣款</div>
<div class="form-grid">
<el-form-item label="违纪扣款" prop="violationDeduction">
<el-input-number v-model="editForm.violationDeduction" :precision="2" :step="100" :min="0"
controls-position="right" />
</el-form-item>
</div>
</div>
<div class="form-section">
<div class="section-title">社保公积金</div>
<div class="form-grid">
<el-form-item label="基本养老保险" prop="basicPensionDeduction">
<el-input-number v-model="editForm.basicPensionDeduction" :precision="2" :step="100" :min="0"
controls-position="right" />
</el-form-item>
<el-form-item label="失业保险" prop="unemploymentInsuranceDeduction">
<el-input-number v-model="editForm.unemploymentInsuranceDeduction" :precision="2" :step="100" :min="0"
controls-position="right" />
</el-form-item>
<el-form-item label="基本医疗保险" prop="basicMedicalDeduction">
<el-input-number v-model="editForm.basicMedicalDeduction" :precision="2" :step="100" :min="0"
controls-position="right" />
</el-form-item>
<el-form-item label="补充医疗保险" prop="medicalDeductionAmount">
<el-input-number v-model="editForm.medicalDeductionAmount" :precision="2" :step="100" :min="0"
controls-position="right" />
</el-form-item>
<el-form-item label="住房公积金" prop="housingFundDeduction">
<el-input-number v-model="editForm.housingFundDeduction" :precision="2" :step="100" :min="0"
controls-position="right" />
</el-form-item>
</div>
</div>
<div class="form-section">
<div class="section-title">其他扣款</div>
<div class="form-grid">
<el-form-item label="个人所得税" prop="individualTaxDeduction">
<el-input-number v-model="editForm.individualTaxDeduction" :precision="2" :step="100" :min="0"
controls-position="right" />
</el-form-item>
<el-form-item label="其他扣款" prop="otherDeduction">
<el-input-number v-model="editForm.otherDeduction" :precision="2" :step="100" :min="0"
controls-position="right" />
</el-form-item>
</div>
</div>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmitEdit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage } from "element-plus";
import { Search } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
import {
getPayrollSheetsList,
updatePayrollSheet,
showPayrollSheet
} from "../../../api/modules/index";
// 权限检查
const userInfo = JSON.parse(sessionStorage.getItem("userInfo") || "{}");
// if (userInfo.roles !== 3) {
// ElMessage.error("您没有权限访问此页面");
// window.history.back();
// }
// 列表数据
const payrollList = ref([]);
const originalList = ref([]);
const searchKeyword = ref('');
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
// 日期范围
const dateRange = ref(null);
const defaultTime = new Date(2000, 1, 1, 0, 0, 0);
// 日期快捷选项
const dateShortcuts = [
{
text: '上个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
},
},
{
text: '本月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), 1);
},
},
{
text: '下个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() + 1, 1);
},
},
];
// 设置默认日期范围(上个月)
const setDefaultDateRange = () => {
const now = new Date();
dateRange.value = new Date(now.getFullYear(), now.getMonth() - 1, 1);
};
// 处理日期变化
const handleDateChange = (val) => {
if (val) {
currentPage.value = 1;
fetchList();
}
};
// 获取列表数据
const fetchList = async () => {
try {
const params = {
current: currentPage.value,
size: pageSize.value
};
if (dateRange.value) {
params.year = dateRange.value.getFullYear();
params.month = dateRange.value.getMonth() + 1;
}
if (searchKeyword.value) {
params.keyword = searchKeyword.value;
}
const res = await getPayrollSheetsList(params);
if (res.errcode === 0 && res.result) {
payrollList.value = res.result.records || [];
total.value = res.result.total || 0;
} else {
ElMessage.error(res.errmsg || "获取列表失败");
}
} catch (error) {
ElMessage.error("获取列表失败");
}
};
// 搜索
const handleSearch = () => {
currentPage.value = 1;
fetchList();
};
// 分页大小变化
const handleSizeChange = (val) => {
pageSize.value = val;
fetchList();
};
// 页码变化
const handleCurrentChange = (val) => {
currentPage.value = val;
fetchList();
};
// 格式化金额
const formatMoney = (value) => {
if (value === undefined || value === null) return '0.00';
return Number(value).toFixed(2);
};
// 获取状态标签
const getStatusLabel = (status) => {
const statusMap = {
0: '待确认',
1: '已确认',
2: '已发放'
};
return statusMap[status] || '未知';
};
// 获取状态标签样式
const getStatusType = (status) => {
const typeMap = {
0: 'warning',
1: 'success',
2: 'info'
};
return typeMap[status] || '';
};
// 修改对话框
const editDialogVisible = ref(false);
const editFormRef = ref(null);
const editForm = ref({
userid: '',
year: 0,
month: 0,
violationDeduction: 0,
basicPensionDeduction: 0,
unemploymentInsuranceDeduction: 0,
basicMedicalDeduction: 0,
medicalDeductionAmount: 0,
housingFundDeduction: 0,
individualTaxDeduction: 0,
otherDeduction: 0
});
// 表单验证规则
const editRules = {
violationDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
basicPensionDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
unemploymentInsuranceDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
basicMedicalDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
medicalDeductionAmount: [{ type: 'number', min: 0, message: '金额不能小于0' }],
housingFundDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
individualTaxDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
otherDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }]
};
// 打开修改对话框
const handleEdit = (row) => {
editForm.value = {
userid: row.userid,
year: row.year,
month: row.month,
violationDeduction: row.violationDeduction || 0,
basicPensionDeduction: row.basicPensionDeduction || 0,
unemploymentInsuranceDeduction: row.unemploymentInsuranceDeduction || 0,
basicMedicalDeduction: row.basicMedicalDeduction || 0,
medicalDeductionAmount: row.medicalDeductionAmount || 0,
housingFundDeduction: row.housingFundDeduction || 0,
individualTaxDeduction: row.individualTaxDeduction || 0,
otherDeduction: row.otherDeduction || 0
};
editDialogVisible.value = true;
};
// 提交修改
const handleSubmitEdit = async () => {
if (!editFormRef.value) return;
await editFormRef.value.validate(async (valid) => {
if (valid) {
try {
const res = await updatePayrollSheet(editForm.value);
if (res.errcode === 0) {
ElMessage.success('修改成功');
editDialogVisible.value = false;
fetchList(); // 刷新列表
} else {
ElMessage.error(res.errmsg || '修改失败');
}
} catch (error) {
ElMessage.error('修改失败');
}
}
});
};
// 展示给员工
const handleShowToEmployee = async (row) => {
try {
const params = {
year: Number(row.year),
month: Number(row.month),
userids: [row.userid]
};
const res = await showPayrollSheet(params);
if (res.errcode === 0) {
ElMessage.success('展示成功');
} else {
ElMessage.error(res.errmsg || '展示失败');
}
} catch (error) {
ElMessage.error('展示失败');
}
};
const selectedRows = ref([]);
// 处理多选变化
const handleSelectionChange = (selection) => {
selectedRows.value = selection;
};
// 批量展示给员工
const handleBatchShowToEmployee = async () => {
if (!selectedRows.value.length) {
ElMessage.warning('请选择要展示的工资条');
return;
}
try {
const params = {
year: Number(selectedRows.value[0].year),
month: Number(selectedRows.value[0].month),
userids: selectedRows.value.map(item => item.userid)
};
const res = await showPayrollSheet(params);
if (res.errcode === 0) {
ElMessage.success('批量展示成功');
} else {
ElMessage.error(res.errmsg || '批量展示失败');
}
} catch (error) {
ElMessage.error('批量展示失败');
}
};
// 页面加载时设置默认日期范围并获取数据
onMounted(() => {
setDefaultDateRange();
fetchList();
});
</script>
<style lang="scss" scoped>
.payroll-sheets-container {
height: calc(100vh - 120px);
padding: 20px;
background-color: var(--el-bg-color-page);
:deep(.el-card) {
height: 100%;
display: flex;
flex-direction: column;
border-radius: 8px;
box-shadow: var(--el-box-shadow-light);
.el-card__body {
flex: 1;
overflow: hidden;
padding: 0;
display: flex;
flex-direction: column;
}
}
:deep(.el-table) {
flex: 1;
.el-table__body-wrapper {
height: 100%;
}
.el-table__cell {
padding: 8px 0;
}
.el-table__header {
th {
background-color: #f5f7fa;
font-weight: bold;
color: #606266;
height: 48px;
padding: 12px 0;
}
}
.el-table__row {
height: 56px;
}
.el-table__fixed-right {
height: 100% !important;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
}
.el-table__fixed {
height: 100% !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
.title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
}
}
.pagination-container {
padding: 16px;
display: flex;
justify-content: flex-end;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
}
.payroll-detail {
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--el-border-color-light);
.employee-info {
display: flex;
align-items: center;
gap: 16px;
.info-content {
h3 {
margin: 0 0 8px;
font-size: 18px;
font-weight: 600;
}
p {
margin: 0;
color: var(--el-text-color-secondary);
}
}
}
.salary-period {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.detail-footer {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid var(--el-border-color-light);
.signature-section {
display: flex;
justify-content: space-around;
gap: 32px;
.signature-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
.label {
font-weight: 600;
color: var(--el-text-color-primary);
}
.signature-image {
height: 100px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
.no-signature {
height: 100px;
border: 1px dashed var(--el-border-color);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: var(--el-text-color-secondary);
}
}
}
}
}
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-dialog__header) {
margin: 0;
padding: 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog__footer) {
padding: 20px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
}
.salary-section {
margin-bottom: 24px;
.section-title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
margin-bottom: 16px;
padding-left: 8px;
border-left: 4px solid var(--el-color-primary);
}
:deep(.el-descriptions) {
.el-descriptions__label {
width: 120px;
font-weight: 500;
}
.el-descriptions__content {
text-align: right;
}
.highlight {
font-weight: 600;
color: var(--el-color-primary);
}
.red {
color: var(--el-color-danger);
}
}
}
.edit-payroll-dialog {
:deep(.el-dialog__body) {
padding: 0;
}
}
.edit-form {
padding: 20px;
max-height: calc(90vh - 120px);
overflow-y: auto;
.form-section {
margin-bottom: 32px;
background-color: #f8fafd;
border-radius: 8px;
padding: 20px 24px 8px 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
&:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 18px;
font-weight: 700;
color: #409eff;
margin-bottom: 20px;
padding-left: 8px;
border-left: 4px solid #409eff;
letter-spacing: 1px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px 32px;
}
}
:deep(.el-form-item) {
margin-bottom: 0;
.el-form-item__label {
font-weight: 500;
color: #333;
font-size: 15px;
}
.el-input-number {
width: 100%;
min-width: 120px;
.el-input__wrapper {
padding-right: 40px;
}
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 20px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
</style>