feat: 奖惩管理和薪资计算

This commit is contained in:
2025-03-10 15:10:49 +08:00
parent e3ed076396
commit 5ef7dfb4b1
7 changed files with 726 additions and 30 deletions

View File

@@ -29,4 +29,85 @@ export function uploadOssFile(data) {
'Content-Type': 'multipart/form-data'
}
})
}
export function getCalcHistory({ payTime }) {
return request({
url: '/oa/salary/list-staff',
method: 'get',
params: {
payTime
}
})
}
export function getWorkersCalcHistory({ payTime }) {
return request({
url: 'oa/salary/list-worker',
method: 'get',
params: {
payTime
}
})
}
export function getSalaryItemDetail(salaryId) {
return request({
url: '/oa/salaryItem/list',
method: 'get',
params: {
salaryId
}
})
}
export function deleteSalaryItem(id) {
return request({
url: `/oa/salaryItem/${id}`,
method: 'delete'
})
}
export function createSalaryItem(data) {
return request({
url: '/oa/salaryItem',
method: 'post',
data
})
}
/**
* 处理日期字符串
* @param {*} input
* @returns
*/
export function convertToDateString(input) {
// 验证输入格式
if (!/^\d{6}$/.test(input)) {
throw new Error("Invalid input format. Expected YYYYMM");
}
const year = input.substring(0, 4);
const month = input.substring(4, 6);
// 验证月份有效性
const monthNum = parseInt(month, 10);
if (monthNum < 1 || monthNum > 12) {
throw new Error("Invalid month value");
}
// 创建日期对象
const date = new Date(year, monthNum - 1, 1);
// 处理无效日期如2025-02-30
if (isNaN(date.getTime())) {
throw new Error("Invalid date combination");
}
// 格式化输出
return [
date.getFullYear(),
(date.getMonth() + 1).toString().padStart(2, "0"),
"01"
].join("-");
}

View File

@@ -0,0 +1,72 @@
<template>
<el-form ref="form" :model="formData" label-width="80px">
<el-form-item label="类型">
<el-input v-model="formData.type" />
</el-form-item>
<el-form-item label="奖惩标记">
<el-select v-model="formData.flag">
<el-option label="奖励" :value="1" />
<el-option label="惩罚" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="金额" prop="price">
<el-input-number v-model="formData.price" :min="0" />
</el-form-item>
<el-form-item label="日期" prop="signTime">
<el-date-picker v-model="formData.signTime" type="date" format="yyyy-MM-DD" placeholder="选择日期" />
</el-form-item>
<el-form-item label="备注" prop="reason">
<el-input v-model="formData.reason" type="textarea" />
</el-form-item>
<div class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="submit">确认创建</el-button>
</div>
</el-form>
</template>
<script>
export default {
data() {
return {
formData: {
type: '',
flag: 0,
reason: '',
signTime: '',
price: 0
}
}
},
methods: {
submit() {
this.$refs.form.validate(valid => {
if (valid) {
this.$emit('submit', this.formData)
this.resetForm();
}
})
},
resetForm() {
this.formData = {
type: '',
flag: 0,
reason: '',
signTime: '',
price: 0
}
},
handleCancel() {
this.resetForm();
this.$emit('cancel')
}
}
}
</script>

View File

@@ -0,0 +1,52 @@
<template>
<div>
<el-table
:data="tableData"
v-loading="loading"
@selection-change="$emit('selection-change', $event)"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="nickName" label="姓名" />
<el-table-column prop="realSalary" label="实发工资" />
<el-table-column prop="baseSalary" label="基本工资" />
<el-table-column prop="payTime" label="发薪日期" />
<el-table-column label="操作" width="180">
<template slot-scope="{ row }">
<el-button size="mini" @click="$emit('detail', row)">详情</el-button>
<!-- <el-button size="mini" type="danger" @click="$emit('delete', row)">删除</el-button> -->
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination"
:current-page="pagination.page"
:page-sizes="[10, 20, 50]"
:page-size="pagination.pageSize"
:total="pagination.total"
layout="total, sizes, prev, pager, next"
@current-change="page => $emit('page-change', { page, pageSize: pagination.pageSize })"
@size-change="pageSize => $emit('page-change', { page: 1, pageSize })"
/>
</div>
</template>
<script>
export default {
props: {
data: Array, // 全部数据
loading: Boolean,
pagination: Object // 包含 page/pageSize
},
computed: {
// 前端分页计算
tableData() {
const start = (this.pagination.page - 1) * this.pagination.pageSize
const end = start + this.pagination.pageSize
const newData = this.data.slice(start, end)
return newData
}
}
}
</script>

View File

@@ -0,0 +1,209 @@
<template>
<div class="salary-container">
<!-- 操作工具栏 -->
<el-row class="operation-bar">
<el-col :span="12">
<!-- <el-button type="primary" @click="openDialog('add')">新增</el-button> -->
<!-- <el-button :disabled="selectedRows.length === 0" @click="handleBatchDelete">批量删除</el-button> -->
<!-- <el-button @click="handleExport">导出</el-button> -->
</el-col>
<el-col :span="12" class="text-right">
<el-date-picker
v-model="currentMonth"
type="month"
value-format="yyyy-MM"
@change="handleMonthChange"
/>
</el-col>
</el-row>
<!-- 工资类型切换 -->
<el-tabs v-model="activeTab" @tab-click="fetchData">
<el-tab-pane label="员工工资" name="staff">
<salary-table
:data="tableData"
:loading="loading"
:pagination="pagination"
@selection-change="handleSelection"
@detail="showDetail"
@page-change="handlePageChange"
/>
</el-tab-pane>
<el-tab-pane label="工人工资" name="worker">
<salary-table
:data="tableData"
:loading="loading"
:pagination="pagination"
@selection-change="handleSelection"
@delete="handleDelete"
@page-change="handlePageChange"
/>
</el-tab-pane>
</el-tabs>
<!-- 编辑弹窗 -->
<el-drawer
:title="'奖惩详情'"
:visible.sync="dialogVisible"
size="50%"
>
<salary-form @submit="handleSubmit" @cancel="dialogVisible = false"></salary-form>
<el-table :data="salaryDetails">
<el-table-column label="奖惩标记" prop="flag">
<template slot-scope="scope">
<el-tag
:type="scope.row.flag === 1 ? 'success' : 'danger'"
disable-transitions
>
{{ scope.row.flag === 1 ? '奖励' : '惩罚' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="理由" prop="reason"></el-table-column>
<el-table-column label="涉及时间" prop="signTime"></el-table-column>
<el-table-column label="涉及金额" prop="price"></el-table-column>
<el-table-column label="类型" prop="type"></el-table-column>
<el-table-column label="操作" width="180">
<template slot-scope="scope">
<el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-drawer>
</div>
</template>
<script>
import SalaryTable from './components/SalaryTable'
import SalaryForm from './components/SalaryForm'
import {
getCalcHistory,
getWorkersCalcHistory,
deleteSalaryItem,
createSalaryItem,
getSalaryItemDetail
} from '@/api/oa/salary'
export default {
components: {
SalaryTable,
SalaryForm
},
data() {
return {
activeTab: 'staff',
currentMonth: this.getDefaultMonth(),
tableData: [],
loading: false,
pagination: {
page: 1,
pageSize: 10,
total: 0
},
selectedRows: [],
dialogVisible: false,
dialogMode: 'add',
currentEditData: null,
salaryDetails: [],
}
},
mounted() {
this.fetchData()
},
methods: {
getDefaultMonth() {
const d = new Date()
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
},
async fetchData() {
this.loading = true
try {
const params = {
payTime: `${this.currentMonth}-01`,
pageNum: this.pagination.page,
pageSize: this.pagination.pageSize
}
const api = this.activeTab === 'staff' ? getCalcHistory : getWorkersCalcHistory
const res = await api(params)
this.tableData = res.rows
this.pagination.total = res.total
} catch (error) {
this.$message.error('数据加载失败')
} finally {
this.loading = false
}
},
handleSelection(selection) {
this.selectedRows = selection
},
async handleDelete(row) {
try {
await this.$confirm('确认删除该记录吗?', '提示', { type: 'warning' })
await deleteSalaryItem(row.salaryItemId)
this.$message.success('删除成功')
this.fetchDetail(this.currentEditData.salaryId)
} catch (error) {
if (error !== 'cancel') {
this.$message.error('删除失败')
}
}
},
openDialog(mode, row = null) {
this.dialogMode = mode
this.currentEditData = row ? { ...row } : null
this.dialogVisible = true
},
async showDetail(row) {
this.dialogVisible = true;
this.currentEditData = row;
this.fetchDetail(row.salaryId)
},
fetchDetail(id) {
console.log('获取详情')
getSalaryItemDetail(id).then(res => {
this.salaryDetails = res.rows;
})
},
async handleSubmit(formData) {
console.log(formData);
const result = await createSalaryItem({
...formData,
salaryId: this.currentEditData.salaryId
});
this.fetchDetail(this.currentEditData.salaryId)
},
handlePageChange({ page, pageSize }) {
this.pagination.page = page
this.pagination.pageSize = pageSize
this.fetchData()
},
handleMonthChange() {
this.pagination.page = 1
this.fetchData()
}
},
}
</script>
<style scoped>
.salary-container {
padding: 20px;
}
.operation-bar {
margin-bottom: 20px;
}
.text-right {
text-align: right;
}
</style>

View File

@@ -56,15 +56,12 @@
<div class="result-area">
<!-- 历史记录 -->
<div class="history-section" v-if="!showCurrentResult">
<h3>计算历史</h3>
<h3>薪资列表</h3>
<el-table :data="historyData" stripe>
<el-table-column prop="calcMonth" label="月份" width="120"></el-table-column>
<el-table-column prop="calcTime" label="计算时间"></el-table-column>
<el-table-column prop="totalAmount" label="总金额">
<template slot-scope="scope">
<span style="color: #67C23A">¥ {{scope.row.totalAmount}}</span>
</template>
</el-table-column>
<el-table-column prop="nickName" label="员工"></el-table-column>
<el-table-column prop="baseSalary" label="基础工资"></el-table-column>
<el-table-column prop="realSalary" label="实发工资"></el-table-column>
<el-table-column prop="payTime" label="发薪时间" width="120"></el-table-column>
<el-table-column label="操作" width="120">
<template slot-scope="scope">
<el-button type="text" @click="showDetail(scope.row)">查看</el-button>
@@ -98,7 +95,7 @@
</template>
<script>
import { calculateSalary, uploadOssFile } from '@/api/oa/salary'
import { calculateSalary, uploadOssFile, getCalcHistory, convertToDateString } from '@/api/oa/salary'
export default {
data() {
@@ -122,6 +119,17 @@ export default {
return this.currentResult.reduce((sum, item) => sum + item.total, 0)
}
},
mounted() {
const firstDayOfLastMonth = new Date();
firstDayOfLastMonth.setMonth(firstDayOfLastMonth.getMonth() - 1, 1);
const firstDayStr = [
firstDayOfLastMonth.getFullYear(),
(firstDayOfLastMonth.getMonth() + 1).toString().padStart(2, '0'),
'01'
].join('-');
this.getHistoryData(firstDayStr)
},
methods: {
showUploadDialog() {
this.uploadVisible = true
@@ -136,31 +144,19 @@ export default {
try {
this.isUploading = true
// 第一步:上传文件
// const formData = new FormData()
// formData.append('file', this.fileList[0].raw)
// const { data } = await uploadOssFile(formData)
// const filePath = '/data' + data.url.split('/data')[1];
// console.log('上传成功,文件路径:', filePath, this.month)
// 第二步:提交计算
const { result } = await calculateSalary({
filePath: this.filePath,
monthStr: this.month
})
this.currentResult = result.details
this.currentMonth = this.month
this.showCurrentResult = true
// 更新历史记录
this.historyData.unshift({
calcMonth: this.month,
calcTime: new Date().toLocaleString(),
totalAmount: this.totalAmount
})
// 获取当前日期的第一天
try {
const firstDayStr = convertToDateString(this.month);
this.getHistoryData(firstDayStr)
} catch (e) {
console.log(e.message); // "Invalid month value"
}
this.$message.success('计算完成')
this.uploadVisible = false
@@ -191,7 +187,19 @@ export default {
showDetail(row) {
// 查看历史详情逻辑
console.log('查看详情', row)
}
},
// 获取计算历史
async getHistoryData(monthStr) {
try {
const { rows } = await getCalcHistory({
payTime: monthStr
})
this.historyData = rows
} catch (error) {
this.$message.error(error.message || '获取计算历史失败')
}
},
}
}
</script>

View File

@@ -0,0 +1,267 @@
<template>
<div class="salary-container">
<!-- 操作栏 -->
<div class="operation-bar">
<el-button type="primary" @click="showUploadDialog">新增计算</el-button>
</div>
<!-- 上传弹窗 -->
<el-dialog
title="薪资计算"
:visible.sync="uploadVisible"
width="30%"
@closed="resetUpload"
:close-on-click-modal="false">
<el-form ref="calcForm">
<el-form-item label="计算月份" required>
<el-date-picker
v-model="month"
type="month"
value-format="yyyyMM"
placeholder="选择月份"
style="width: 100%"
:disabled="isUploading">
</el-date-picker>
</el-form-item>
<el-form-item label="薪资文件" required>
<el-upload
class="upload-demo"
drag
:auto-upload="false"
:on-change="handleFileChange"
:file-list="fileList"
:disabled="isUploading">
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">仅支持xlsx格式文件</div>
</el-upload>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="uploadVisible = false" :disabled="isUploading">取消</el-button>
<el-button @click="handleUpload" :disabled="fileList.length === 0">确认上传</el-button>
<el-button
type="primary"
@click="handleSubmit"
:loading="isUploading"
:disabled="!filePath">
{{ isUploading ? '计算中...' : '开始计算' }}
</el-button>
</span>
</el-dialog>
<!-- 计算结果展示 -->
<div class="result-area">
<!-- 历史记录 -->
<div class="history-section" v-if="!showCurrentResult">
<h3>薪资列表</h3>
<el-table :data="historyData" stripe>
<el-table-column prop="nickName" label="员工"></el-table-column>
<el-table-column prop="baseSalary" label="基础工资"></el-table-column>
<el-table-column prop="realSalary" label="实发工资"></el-table-column>
<el-table-column prop="payTime" label="发薪时间" width="120"></el-table-column>
<el-table-column label="操作" width="120">
<template slot-scope="scope">
<el-button type="text" @click="showDetail(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 本次结果 -->
<div class="current-result" v-else>
<h3>{{ currentMonth }} 薪资计算结果</h3>
<el-table :data="currentResult" border>
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="baseSalary" label="基本工资"></el-table-column>
<el-table-column prop="bonus" label="奖金"></el-table-column>
<el-table-column prop="deduction" label="扣款"></el-table-column>
<el-table-column prop="total" label="实发工资">
<template slot-scope="scope">
<span class="highlight">¥ {{scope.row.total}}</span>
</template>
</el-table-column>
</el-table>
<div class="summary">
<span>合计人数{{currentResult.length}} </span>
<span class="total-amount">总金额¥ {{totalAmount}}</span>
</div>
</div>
</div>
</div>
</template>
<script>
import { calculateSalary, uploadOssFile, getWorkersCalcHistory, convertToDateString } from '@/api/oa/salary'
export default {
data() {
return {
uploadVisible: false,
isUploading: false,
month: '',
fileList: [],
currentResult: [],
showCurrentResult: false,
currentMonth: '',
historyData: [],
filePath: '',
}
},
computed: {
canSubmit() {
return this.month && this.fileList.length > 0
},
totalAmount() {
return this.currentResult.reduce((sum, item) => sum + item.total, 0)
}
},
mounted() {
const firstDayOfLastMonth = new Date();
firstDayOfLastMonth.setMonth(firstDayOfLastMonth.getMonth() - 1, 1);
const firstDayStr = [
firstDayOfLastMonth.getFullYear(),
(firstDayOfLastMonth.getMonth() + 1).toString().padStart(2, '0'),
'01'
].join('-');
this.getHistoryData(firstDayStr)
},
methods: {
showUploadDialog() {
this.uploadVisible = true
},
handleFileChange(file, fileList) {
// 限制单个文件上传
this.fileList = [fileList[fileList.length - 1]]
},
async handleSubmit() {
try {
this.isUploading = true
// 第二步:提交计算
const { result } = await calculateSalary({
filePath: this.filePath,
monthStr: this.month
})
// 获取当前日期的第一天
try {
const firstDayStr = convertToDateString(this.month);
this.getHistoryData(firstDayStr)
} catch (e) {
console.log(e.message); // "Invalid month value"
}
// this.currentResult = result.details
// this.currentMonth = this.month
// this.showCurrentResult = true
// // 更新历史记录
// this.historyData.unshift({
// calcMonth: this.month,
// calcTime: new Date().toLocaleString(),
// totalAmount: this.totalAmount
// })
this.$message.success('计算完成')
this.uploadVisible = false
} catch (error) {
this.$message.error(error.message || '计算失败')
} finally {
this.isUploading = false
}
},
// 上传文件
async handleUpload() {
const formData = new FormData()
formData.append('file', this.fileList[0].raw)
const { data } = await uploadOssFile(formData)
const filePath = '/data' + data.url.split('/data')[1];
this.filePath = filePath;
message.success('上传成功');
},
resetUpload() {
this.fileList = []
this.month = ''
},
showDetail(row) {
// 查看历史详情逻辑
console.log('查看详情', row)
},
// 获取计算历史
async getHistoryData(monthStr) {
try {
const { rows } = await getWorkersCalcHistory({
payTime: monthStr
})
this.historyData = rows
} catch (error) {
this.$message.error(error.message || '获取计算历史失败')
}
},
}
}
</script>
<style scoped>
.salary-container {
padding: 20px;
background: #f5f7fa;
}
.operation-bar {
margin-bottom: 20px;
padding: 15px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
}
.result-area {
background: #fff;
padding: 20px;
border-radius: 4px;
}
.history-section h3,
.current-result h3 {
color: #303133;
margin-bottom: 15px;
}
.summary {
margin-top: 20px;
text-align: right;
font-size: 14px;
}
.total-amount {
color: #67C23A;
font-weight: bold;
margin-left: 30px;
}
.highlight {
color: #E6A23C;
font-weight: 500;
}
.upload-demo {
text-align: center;
}
</style>

View File

@@ -295,6 +295,13 @@
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="日薪" prop="laborCost">
<el-input-number v-model="form.laborCost" placeholder="请输入日薪" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="备注">