feat: 新增绩效系统核心模块并修复菜单路径问题
本次提交完成多项核心功能开发与优化: 1. 修复菜单路径计算逻辑,目录节点路径拼接祖先路径 2. 为绩效相关实体类添加日期格式化注解,统一参数绑定格式 3. 新增10+个绩效系统API接口,覆盖部门、薪资、考核等模块 4. 新增绩效系统首页、部门管理、薪资明细等多个页面组件
This commit is contained in:
509
klp-ui/src/views/perf/index/SalaryPanel.vue
Normal file
509
klp-ui/src/views/perf/index/SalaryPanel.vue
Normal file
@@ -0,0 +1,509 @@
|
||||
<template>
|
||||
<div class="salary-panel">
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-input v-model="query.employeeId" size="small" placeholder="员工ID" clearable style="width: 140px" @keyup.enter.native="loadList" />
|
||||
<el-select v-model="query.status" size="small" placeholder="状态" clearable style="width: 110px" @change="loadList">
|
||||
<el-option label="草稿" :value="'0'" />
|
||||
<el-option label="已确认" :value="'1'" />
|
||||
<el-option label="已归档" :value="'2'" />
|
||||
</el-select>
|
||||
<el-button size="small" type="primary" icon="el-icon-search" @click="loadList">查询</el-button>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAdd">新增</el-button>
|
||||
<el-button size="small" type="success" icon="el-icon-s-operation" :loading="generating" @click="handleBatchGenerate">一键生成</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<div class="table-wrap">
|
||||
<el-table :data="list" stripe size="small" v-loading="loading" empty-text="暂无薪资记录" class="inline-edit-table" border>
|
||||
<!-- 只读列 -->
|
||||
<!-- <el-table-column prop="employeeId" label="员工ID" width="90" /> -->
|
||||
<el-table-column prop="status" label="状态" width="70" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="statusType(scope.row.status)" size="small">{{ statusLabel(scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 薪资基数 -->
|
||||
<el-table-column label="底薪" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.baseSalary" class="cell-input cell-input--base" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="绩效基数" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.perfBase" class="cell-input cell-input--base" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
|
||||
<!-- 考核得分 -->
|
||||
<el-table-column label="考核得分" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.perfScore" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
|
||||
<!-- 系数 -->
|
||||
<el-table-column label="绩效系数" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.perfCoeff" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="车间系数" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.deptCoeff" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="岗位系数" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.posCoeff" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="固定系数" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.fixedCoeff" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="调整系数" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.adjCoeff" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="手动调整系数" width="100" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.adjCoeffManual" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="总系数" width="80" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.totalCoeff" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
|
||||
<!-- 绩效工资 -->
|
||||
<el-table-column label="绩效工资" width="100" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.perfWage" class="cell-input cell-input--amount" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
|
||||
<!-- 动态扣款列 -->
|
||||
<el-table-column
|
||||
v-for="item in deductionItems" :key="'ded-'+item.itemName"
|
||||
:label="item.itemName" width="100" align="right"
|
||||
><template slot-scope="s">
|
||||
<input v-model="s.row._d[item.itemName]" class="cell-input cell-input--deduct" @change="onDeductChange(s.row); markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="扣款合计" width="90" align="right">
|
||||
<template slot-scope="scope"><span class="cell-readonly cell-readonly--deduct">{{ scope.row._deductionsTotal }}</span></template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 动态奖励列 -->
|
||||
<el-table-column
|
||||
v-for="item in bonusItems" :key="'bon-'+item.itemName"
|
||||
:label="item.itemName" width="100" align="right"
|
||||
><template slot-scope="s">
|
||||
<input v-model="s.row._b[item.itemName]" class="cell-input cell-input--bonus" @change="onBonusChange(s.row); markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="奖励合计" width="90" align="right">
|
||||
<template slot-scope="scope"><span class="cell-readonly cell-readonly--bonus">{{ scope.row._bonusesTotal }}</span></template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 金额 -->
|
||||
<el-table-column label="其他金额" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.otherAmount" class="cell-input cell-input--amount" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="实领薪资" width="100" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.actualSalary" class="cell-input cell-input--amount" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
|
||||
<!-- 参考值 -->
|
||||
<el-table-column label="固薪参考" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.fixedSalaryRef" class="cell-input cell-input--ref" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="调薪参考" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.adjSalaryRef" class="cell-input cell-input--ref" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="包薪每班参考" width="100" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.baoPerShiftRef" class="cell-input cell-input--ref" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="包薪参考" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.baoSalaryRef" class="cell-input cell-input--ref" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="总薪参考" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.totalSalaryRef" class="cell-input cell-input--ref" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
|
||||
<!-- 备注 -->
|
||||
<el-table-column label="备注" min-width="140"><template slot-scope="s">
|
||||
<input v-model="s.row.remark" class="cell-input cell-input--remark" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
|
||||
<!-- 操作 -->
|
||||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button v-if="scope.row._dirty" type="text" size="mini" icon="el-icon-check" :loading="scope.row._saving" class="btn-save" @click="saveRow(scope.row)">保存</el-button>
|
||||
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDel(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
:current-page="query.pageNum"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size="query.pageSize"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listPerfSalary, addPerfSalary, updatePerfSalary, delPerfSalary } from '@/api/perf/salary'
|
||||
import { listEmployeeInfo } from '@/api/wms/employeeInfo'
|
||||
import { listPerfDeptItemConfig } from '@/api/perf/deptItemConfig'
|
||||
|
||||
export default {
|
||||
name: 'SalaryPanel',
|
||||
props: {
|
||||
deptId: { type: [Number, String], default: null },
|
||||
deptName: { type: String, default: '' },
|
||||
period: { type: String, default: '' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
list: [],
|
||||
total: 0,
|
||||
query: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
employeeId: '',
|
||||
status: ''
|
||||
},
|
||||
deductionItems: [],
|
||||
bonusItems: [],
|
||||
generating: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
deptId: { immediate: true, handler: 'onDeptChange' },
|
||||
period: 'onDeptChange'
|
||||
},
|
||||
methods: {
|
||||
fmt(val) {
|
||||
return val != null ? val : '-'
|
||||
},
|
||||
statusType(status) {
|
||||
return { '0': 'info', '1': 'warning', '2': 'success' }[status] || 'info'
|
||||
},
|
||||
statusLabel(status) {
|
||||
return { '0': '草稿', '1': '已确认', '2': '已归档' }[status] || '草稿'
|
||||
},
|
||||
onDeptChange() {
|
||||
this.loadItemConfig()
|
||||
this.loadList()
|
||||
},
|
||||
loadItemConfig() {
|
||||
if (!this.deptId || !this.period) return
|
||||
listPerfDeptItemConfig({ deptId: this.deptId, month: this.period + '-01' }).then(res => {
|
||||
const rows = res.rows || []
|
||||
this.deductionItems = rows.filter(r => r.itemType === 1 && r.isEnabled === 1)
|
||||
this.bonusItems = rows.filter(r => r.itemType === 2 && r.isEnabled === 1)
|
||||
}).catch(() => {
|
||||
this.deductionItems = []
|
||||
this.bonusItems = []
|
||||
})
|
||||
},
|
||||
/* 解析扣款/奖励 JSON 到行对象 */
|
||||
parseRowItems(row) {
|
||||
if (!row._d) {
|
||||
const d = {}
|
||||
try { Object.assign(d, JSON.parse(row.deductionsJson || '{}')) } catch {}
|
||||
this.deductionItems.forEach(item => { if (!(item.itemName in d)) d[item.itemName] = '' })
|
||||
this.$set(row, '_d', d)
|
||||
}
|
||||
if (!row._b) {
|
||||
const b = {}
|
||||
try { Object.assign(b, JSON.parse(row.bonusesJson || '{}')) } catch {}
|
||||
this.bonusItems.forEach(item => { if (!(item.itemName in b)) b[item.itemName] = '' })
|
||||
this.$set(row, '_b', b)
|
||||
}
|
||||
this.recalcTotals(row)
|
||||
},
|
||||
recalcTotals(row) {
|
||||
const sumD = Object.values(row._d || {}).reduce((s, v) => s + (parseFloat(v) || 0), 0)
|
||||
const sumB = Object.values(row._b || {}).reduce((s, v) => s + (parseFloat(v) || 0), 0)
|
||||
this.$set(row, '_deductionsTotal', sumD)
|
||||
this.$set(row, '_bonusesTotal', sumB)
|
||||
},
|
||||
onDeductChange(row) { this.recalcTotals(row) },
|
||||
onBonusChange(row) { this.recalcTotals(row) },
|
||||
markDirty(row) { this.$set(row, '_dirty', true) },
|
||||
|
||||
loadList() {
|
||||
if (!this.deptId || !this.period) return
|
||||
this.loading = true
|
||||
const params = {
|
||||
pageNum: this.query.pageNum,
|
||||
pageSize: this.query.pageSize,
|
||||
deptId: this.deptId,
|
||||
period: this.period,
|
||||
employeeId: this.query.employeeId || undefined,
|
||||
status: this.query.status || undefined
|
||||
}
|
||||
listPerfSalary(params).then(res => {
|
||||
this.list = (res.rows || []).map(r => {
|
||||
this.$set(r, '_dirty', false)
|
||||
this.$set(r, '_saving', false)
|
||||
this.parseRowItems(r)
|
||||
return r
|
||||
})
|
||||
this.total = res.total || 0
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.query.pageSize = val
|
||||
this.loadList()
|
||||
},
|
||||
handlePageChange(val) {
|
||||
this.query.pageNum = val
|
||||
this.loadList()
|
||||
},
|
||||
handleAdd() {
|
||||
const row = {
|
||||
employeeId: '',
|
||||
deptId: this.deptId,
|
||||
period: this.period,
|
||||
baseSalary: '',
|
||||
perfBase: '',
|
||||
perfScore: '',
|
||||
perfCoeff: '',
|
||||
deptCoeff: '',
|
||||
posCoeff: '',
|
||||
fixedCoeff: '',
|
||||
adjCoeff: '',
|
||||
adjCoeffManual: '',
|
||||
totalCoeff: '',
|
||||
perfWage: '',
|
||||
deductionsJson: '{}',
|
||||
bonusesJson: '{}',
|
||||
otherAmount: '',
|
||||
actualSalary: '',
|
||||
fixedSalaryRef: '',
|
||||
adjSalaryRef: '',
|
||||
baoPerShiftRef: '',
|
||||
baoSalaryRef: '',
|
||||
totalSalaryRef: '',
|
||||
status: '0',
|
||||
remark: '',
|
||||
_dirty: true,
|
||||
_saving: false,
|
||||
_d: {},
|
||||
_b: {},
|
||||
_deductionsTotal: 0,
|
||||
_bonusesTotal: 0
|
||||
}
|
||||
this.deductionItems.forEach(item => { row._d[item.itemName] = '' })
|
||||
this.bonusItems.forEach(item => { row._b[item.itemName] = '' })
|
||||
this.list.unshift(row)
|
||||
},
|
||||
saveRow(row) {
|
||||
this.$set(row, '_saving', true)
|
||||
const data = { ...row }
|
||||
// 序列化扣款/奖励 JSON
|
||||
data.deductionsJson = JSON.stringify(row._d || {})
|
||||
data.bonusesJson = JSON.stringify(row._b || {})
|
||||
data.deductionsTotal = row._deductionsTotal
|
||||
data.bonusesTotal = row._bonusesTotal
|
||||
// 清理内部字段
|
||||
delete data._d; delete data._b; delete data._dirty; delete data._saving
|
||||
delete data._deductionsTotal; delete data._bonusesTotal
|
||||
|
||||
const api = data.id ? updatePerfSalary : addPerfSalary
|
||||
api(data).then(res => {
|
||||
// 乐观更新:如果是新增,用返回值更新 id
|
||||
if (!data.id && res.data) {
|
||||
this.$set(row, 'id', res.data.id || res.data)
|
||||
}
|
||||
this.$set(row, '_dirty', false)
|
||||
this.$message.success('保存成功')
|
||||
}).catch(() => {
|
||||
this.$message.error('保存失败')
|
||||
this.loadList() // 失败则重新加载以回滚
|
||||
}).finally(() => {
|
||||
this.$set(row, '_saving', false)
|
||||
})
|
||||
},
|
||||
handleDel(row) {
|
||||
if (!row.id) {
|
||||
// 未保存的新行直接移除
|
||||
const idx = this.list.indexOf(row)
|
||||
if (idx >= 0) this.list.splice(idx, 1)
|
||||
return
|
||||
}
|
||||
this.$confirm(`确认删除该薪资记录吗?`, '提示', { type: 'warning' }).then(() => {
|
||||
delPerfSalary(row.id).then(() => {
|
||||
this.$message.success('删除成功')
|
||||
this.loadList()
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleBatchGenerate() {
|
||||
if (!this.deptName) {
|
||||
this.$message.warning('缺少部门名称')
|
||||
return
|
||||
}
|
||||
this.$confirm(`确认根据"${this.deptName}"下的在职员工,一键生成本月薪资记录吗?`, '提示', { type: 'info' }).then(() => {
|
||||
this.generating = true
|
||||
listEmployeeInfo({ dept: this.deptName, isLeave: 0, pageSize: 9999 }).then(empRes => {
|
||||
const employees = empRes.rows || []
|
||||
if (employees.length === 0) {
|
||||
this.$message.warning('该部门下暂无在职员工')
|
||||
return
|
||||
}
|
||||
listPerfSalary({ deptId: this.deptId, period: this.period, pageSize: 9999 }).then(salaryRes => {
|
||||
const existing = new Set((salaryRes.rows || []).map(r => String(r.employeeId)))
|
||||
const toInsert = employees.filter(e => !existing.has(String(e.employeeId) || String(e.infoId)))
|
||||
if (toInsert.length === 0) {
|
||||
this.$message.info('该部门下所有在职员工已有本月薪资记录')
|
||||
return
|
||||
}
|
||||
const emptyJson = JSON.stringify({})
|
||||
const tasks = toInsert.map(emp => addPerfSalary({
|
||||
employeeId: emp.employeeId || emp.infoId || '',
|
||||
deptId: this.deptId,
|
||||
period: this.period,
|
||||
baseSalary: emp.baseSalary || 0,
|
||||
perfBase: emp.perfBase || 0,
|
||||
posCoeff: emp.posCoeffDefault || 0,
|
||||
perfScore: 0,
|
||||
perfCoeff: 0,
|
||||
deptCoeff: 0,
|
||||
fixedCoeff: 0,
|
||||
adjCoeff: 0,
|
||||
adjCoeffManual: 0,
|
||||
totalCoeff: 0,
|
||||
perfWage: 0,
|
||||
deductionsJson: emptyJson,
|
||||
deductionsTotal: 0,
|
||||
bonusesJson: emptyJson,
|
||||
bonusesTotal: 0,
|
||||
otherAmount: 0,
|
||||
actualSalary: 0,
|
||||
fixedSalaryRef: '',
|
||||
adjSalaryRef: '',
|
||||
baoPerShiftRef: '',
|
||||
baoSalaryRef: '',
|
||||
totalSalaryRef: '',
|
||||
status: '0',
|
||||
remark: ''
|
||||
}))
|
||||
return Promise.allSettled(tasks)
|
||||
}).then(results => {
|
||||
if (!results) return
|
||||
const ok = results.filter(r => r.status === 'fulfilled').length
|
||||
const fail = results.filter(r => r.status === 'rejected').length
|
||||
this.$message.success(`生成完成:成功 ${ok} 条${fail > 0 ? `,失败 ${fail} 条` : ''}`)
|
||||
this.loadList()
|
||||
})
|
||||
}).finally(() => {
|
||||
this.generating = false
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.salary-panel {
|
||||
padding: 8px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-left,
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
::v-deep .inline-edit-table {
|
||||
border: none;
|
||||
&::before, &::after { display: none; }
|
||||
th { border: none; padding: 2px 4px; font-size: 12px; color: #909399; }
|
||||
td {
|
||||
border: none;
|
||||
border-right: 1px solid #ebeef5;
|
||||
padding: 0;
|
||||
}
|
||||
.el-table__body tr:hover td { background-color: #f5f7fa !important; }
|
||||
.cell { padding: 0; height: 100%; }
|
||||
}
|
||||
|
||||
/* 原生 input */
|
||||
.cell-input {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: right;
|
||||
padding: 0 6px;
|
||||
font-size: 12px;
|
||||
color: #303133;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
line-height: 28px;
|
||||
transition: background-color 0.15s;
|
||||
|
||||
&:focus {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 0 1px #409eff inset;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&--base { background-color: #e8f4fd; }
|
||||
&--coeff { background-color: #fff8e1; }
|
||||
&--deduct { background-color: #ffebee; }
|
||||
&--bonus { background-color: #e8f5e9; }
|
||||
&--amount { background-color: #f3e5f5; }
|
||||
&--ref { background-color: #f5f5f5; }
|
||||
&--remark { background-color: #fafafa; text-align: left; }
|
||||
}
|
||||
|
||||
.cell-readonly {
|
||||
display: block;
|
||||
text-align: right;
|
||||
padding: 0 6px;
|
||||
font-size: 12px;
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
height: 28px;
|
||||
|
||||
&--deduct { color: #e53935; }
|
||||
&--bonus { color: #43a047; }
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
color: #67c23a !important;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.el-table .el-button + .el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user