feat: 新增产线管理、批量操作与绩效考核自动化功能
1. 新增部门产线绑定字段,优化部门列表展示 2. 添加薪资、考核、维度明细的批量新增/修改接口与实现 3. 新增绩效考核得分计算引擎与WMS数据拉取服务 4. 新增一键生成薪资与考核记录的弹窗功能 5. 优化配置页面UI与新增批量导入项目功能 6. 隐藏 coil/ship.vue 页面的校正按钮
This commit is contained in:
347
klp-ui/src/views/perf/index/GenerateDialog.vue
Normal file
347
klp-ui/src/views/perf/index/GenerateDialog.vue
Normal file
@@ -0,0 +1,347 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
title="一键生成薪资记录"
|
||||
:visible="visible"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="!running"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="generate-progress">
|
||||
<!-- 总体进度条 -->
|
||||
<el-progress
|
||||
:percentage="overallPercent"
|
||||
:status="overallStatus"
|
||||
:show-text="false"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
|
||||
<!-- 步骤列表 -->
|
||||
<div
|
||||
v-for="(step, idx) in steps"
|
||||
:key="idx"
|
||||
class="step-row"
|
||||
:class="'step--' + step.status"
|
||||
>
|
||||
<div class="step-icon">
|
||||
<i v-if="step.status === 'pending'" class="el-icon-more-outline"></i>
|
||||
<i v-else-if="step.status === 'in_progress'" class="el-icon-loading"></i>
|
||||
<i v-else-if="step.status === 'completed'" class="el-icon-circle-check"></i>
|
||||
<i v-else-if="step.status === 'error'" class="el-icon-circle-close"></i>
|
||||
</div>
|
||||
<div class="step-body">
|
||||
<div class="step-label">{{ step.label }}</div>
|
||||
<div v-if="step.detail" class="step-detail">{{ step.detail }}</div>
|
||||
<div v-if="step.status === 'error' && step.errorMsg" class="step-error">{{ step.errorMsg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span slot="footer">
|
||||
<el-button size="small" @click="handleClose" :disabled="running">关闭</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listPerfSalary, delPerfSalary, batchAddPerfSalary } from '@/api/perf/salary'
|
||||
import { listPerfAppraisal, delPerfAppraisal, batchAddPerfAppraisal } from '@/api/perf/appraisal'
|
||||
import { listPerfPositionTemplate } from '@/api/perf/positionTemplate'
|
||||
import { listPerfTemplateDimension } from '@/api/perf/templateDimension'
|
||||
import { listPerfDeptConfig } from '@/api/perf/deptConfig'
|
||||
import { listEmployeeInfo } from '@/api/wms/employeeInfo'
|
||||
|
||||
export default {
|
||||
name: 'GenerateDialog',
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
deptId: { type: [Number, String], default: null },
|
||||
deptName: { type: String, default: '' },
|
||||
period: { type: String, default: '' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
running: false,
|
||||
steps: [
|
||||
{ label: '查询已有记录', status: 'pending', detail: '' },
|
||||
{ label: '清除旧记录', status: 'pending', detail: '' },
|
||||
{ label: '生成薪资基础数据', status: 'pending', detail: '' },
|
||||
{ label: '生成绩效考核评分', status: 'pending', detail: '' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
overallPercent() {
|
||||
const done = this.steps.filter(s => s.status === 'completed').length
|
||||
return Math.round((done / this.steps.length) * 100)
|
||||
},
|
||||
overallStatus() {
|
||||
const hasError = this.steps.some(s => s.status === 'error')
|
||||
if (hasError) return 'exception'
|
||||
if (this.overallPercent === 100) return 'success'
|
||||
return ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
if (val) {
|
||||
if (!this.deptId || !this.period) {
|
||||
this.$message.warning('缺少部门或周期参数')
|
||||
this.$emit('update:visible', false)
|
||||
return
|
||||
}
|
||||
this.startGenerate()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
if (this.running) return
|
||||
this.$emit('update:visible', false)
|
||||
},
|
||||
|
||||
resetSteps() {
|
||||
this.steps.forEach(s => {
|
||||
s.status = 'pending'
|
||||
s.detail = ''
|
||||
s.errorMsg = ''
|
||||
})
|
||||
},
|
||||
|
||||
setStep(idx, status, detail, errorMsg) {
|
||||
const s = this.steps[idx]
|
||||
s.status = status
|
||||
if (detail !== undefined) s.detail = detail
|
||||
if (errorMsg !== undefined) s.errorMsg = errorMsg
|
||||
},
|
||||
|
||||
async startGenerate() {
|
||||
this.running = true
|
||||
this.resetSteps()
|
||||
|
||||
try {
|
||||
// ====== 前置检查:是否已设定考核参数 ======
|
||||
const cfgRes = await listPerfDeptConfig({ deptId: this.deptId, pageNum: 1, pageSize: 1 })
|
||||
if (!cfgRes.rows || cfgRes.rows.length === 0) {
|
||||
this.$message.warning('请先在"考核配置"页中设定考核参数,再一键生成')
|
||||
this.running = false
|
||||
// 重置所有步骤为 pending
|
||||
this.resetSteps()
|
||||
return
|
||||
}
|
||||
const deptCoeff = cfgRes.rows[0].deptCoeff || 1
|
||||
|
||||
// ====== Step 0: 查询已有记录 ======
|
||||
const [salaryRes, appRes] = await Promise.all([
|
||||
listPerfSalary({ deptId: this.deptId, period: this.period, pageNum: 1, pageSize: 9999 }),
|
||||
listPerfAppraisal({ deptId: this.deptId, period: this.period, pageNum: 1, pageSize: 9999 })
|
||||
])
|
||||
const salaryIds = (salaryRes.rows || []).map(r => r.id)
|
||||
const appIds = (appRes.rows || []).map(r => r.id)
|
||||
const totalExisting = salaryIds.length + appIds.length
|
||||
this.setStep(0, 'completed', totalExisting > 0
|
||||
? `查询到 ${salaryIds.length} 条薪资记录、${appIds.length} 条考核记录`
|
||||
: '暂无已有记录')
|
||||
|
||||
// ====== Step 1: 清除旧记录 ======
|
||||
this.setStep(1, 'in_progress')
|
||||
if (totalExisting === 0) {
|
||||
this.setStep(1, 'completed', '无需清除')
|
||||
} else {
|
||||
const delTasks = []
|
||||
if (salaryIds.length) delTasks.push(delPerfSalary(salaryIds.join(',')))
|
||||
if (appIds.length) delTasks.push(delPerfAppraisal(appIds.join(',')))
|
||||
await Promise.all(delTasks)
|
||||
const parts = []
|
||||
if (salaryIds.length) parts.push(`${salaryIds.length} 条薪资记录`)
|
||||
if (appIds.length) parts.push(`${appIds.length} 条考核记录`)
|
||||
this.setStep(1, 'completed', `已删除 ${parts.join('、')}`)
|
||||
}
|
||||
|
||||
// ====== Step 2: 生成薪资基础数据 ======
|
||||
this.setStep(2, 'in_progress')
|
||||
const empRes = await listEmployeeInfo({ dept: this.deptName, isLeave: 0, pageSize: 9999 })
|
||||
const employees = empRes.rows || []
|
||||
if (employees.length === 0) {
|
||||
this.setStep(2, 'completed', '该部门下暂无在职员工')
|
||||
this.setStep(3, 'completed', '无需生成')
|
||||
this.running = false
|
||||
return
|
||||
}
|
||||
|
||||
const emptyJson = JSON.stringify({})
|
||||
const salaryList = employees.map(emp => ({
|
||||
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: deptCoeff,
|
||||
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: ''
|
||||
}))
|
||||
const batchRes = await batchAddPerfSalary(salaryList)
|
||||
this.setStep(2, 'completed', `已为 ${batchRes.data || 0} 名员工生成薪资记录`)
|
||||
|
||||
// ====== Step 3: 生成绩效考核评分 ======
|
||||
this.setStep(3, 'in_progress')
|
||||
|
||||
// 加载岗位模板
|
||||
const tplRes = await listPerfPositionTemplate({ deptId: this.deptId })
|
||||
const templates = tplRes.rows || []
|
||||
const tplMap = {}
|
||||
templates.forEach(t => { tplMap[t.positionName] = t })
|
||||
|
||||
// 按模板分组员工,并预加载维度
|
||||
const tplDimensions = {} // templateId → dimensions[]
|
||||
const appraisalList = [] // 最终批量插入数组
|
||||
|
||||
for (const emp of employees) {
|
||||
const position = emp.jobType || emp.position || emp.posName || emp.postName || ''
|
||||
const template = tplMap[position]
|
||||
if (!template) continue
|
||||
|
||||
// 懒加载维度
|
||||
if (!tplDimensions[template.id]) {
|
||||
const dimRes = await listPerfTemplateDimension({ templateId: template.id })
|
||||
tplDimensions[template.id] = (dimRes.rows || []).filter(d => d.isEnabled !== 0)
|
||||
}
|
||||
const dimensions = tplDimensions[template.id]
|
||||
if (dimensions.length === 0) continue
|
||||
|
||||
// 构建嵌套明细
|
||||
const details = dimensions.map(dim => ({
|
||||
dimensionId: dim.id,
|
||||
seq: dim.seq || '',
|
||||
dimName: dim.dimName,
|
||||
dimScore: 0,
|
||||
targetValue: dim.targetValue || '100',
|
||||
weight: dim.weight || '0',
|
||||
quantitativeIndicator: dim.indicator || '',
|
||||
scoreFormula: dim.scoreFormula || '',
|
||||
scoreRule: dim.scoreRule || ''
|
||||
}))
|
||||
|
||||
appraisalList.push({
|
||||
employeeId: emp.employeeId || emp.infoId || '',
|
||||
deptId: this.deptId,
|
||||
period: this.period,
|
||||
templateId: template.id,
|
||||
finalScore: 0,
|
||||
status: 0,
|
||||
details: details
|
||||
})
|
||||
}
|
||||
|
||||
if (appraisalList.length > 0) {
|
||||
await batchAddPerfAppraisal(appraisalList)
|
||||
const totalDims = appraisalList.reduce((sum, a) => sum + (a.details ? a.details.length : 0), 0)
|
||||
this.setStep(3, 'completed',
|
||||
`已为 ${appraisalList.length} 名员工生成绩效考核(共 ${totalDims} 个维度)`)
|
||||
} else {
|
||||
this.setStep(3, 'completed', '无匹配的岗位模板,跳过考核生成')
|
||||
}
|
||||
|
||||
// 完成
|
||||
this.running = false
|
||||
this.$emit('done')
|
||||
} catch (err) {
|
||||
// 找出当前进行中的步骤标记为错误
|
||||
const activeIdx = this.steps.findIndex(s => s.status === 'in_progress')
|
||||
if (activeIdx >= 0) {
|
||||
this.setStep(activeIdx, 'error', '', (err && err.message) || '操作失败')
|
||||
}
|
||||
this.running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.generate-progress {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.step-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 10px 4px;
|
||||
border-left: 2px solid #dcdfe6;
|
||||
margin-left: 12px;
|
||||
|
||||
&.step--in_progress {
|
||||
border-left-color: #409eff;
|
||||
.step-icon { color: #409eff; }
|
||||
}
|
||||
&.step--completed {
|
||||
border-left-color: #67c23a;
|
||||
.step-icon { color: #67c23a; }
|
||||
}
|
||||
&.step--error {
|
||||
border-left-color: #f56c6c;
|
||||
.step-icon { color: #f56c6c; }
|
||||
}
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
margin-left: -14px;
|
||||
margin-right: 10px;
|
||||
font-size: 16px;
|
||||
color: #c0c4cc;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
line-height: 24px;
|
||||
.step--pending & { color: #909399; }
|
||||
}
|
||||
|
||||
.step-detail {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 2px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.step-error {
|
||||
font-size: 12px;
|
||||
color: #f56c6c;
|
||||
margin-top: 2px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user