feat: 新增产线管理、批量操作与绩效考核自动化功能
1. 新增部门产线绑定字段,优化部门列表展示 2. 添加薪资、考核、维度明细的批量新增/修改接口与实现 3. 新增绩效考核得分计算引擎与WMS数据拉取服务 4. 新增一键生成薪资与考核记录的弹窗功能 5. 优化配置页面UI与新增批量导入项目功能 6. 隐藏 coil/ship.vue 页面的校正按钮
This commit is contained in:
@@ -71,8 +71,18 @@ public class PerfAppraisalController extends BaseController {
|
|||||||
@Log(title = "月度绩效考核记录", businessType = BusinessType.INSERT)
|
@Log(title = "月度绩效考核记录", businessType = BusinessType.INSERT)
|
||||||
@RepeatSubmit()
|
@RepeatSubmit()
|
||||||
@PostMapping()
|
@PostMapping()
|
||||||
public R<Void> add(@Validated(AddGroup.class) @RequestBody PerfAppraisalBo bo) {
|
public R<Long> add(@Validated(AddGroup.class) @RequestBody PerfAppraisalBo bo) {
|
||||||
return toAjax(iPerfAppraisalService.insertByBo(bo));
|
PerfAppraisalBo result = iPerfAppraisalService.insertByBo(bo);
|
||||||
|
return R.ok(result.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量新增月度绩效考核记录(含维度明细)
|
||||||
|
*/
|
||||||
|
@Log(title = "月度绩效考核记录", businessType = BusinessType.INSERT)
|
||||||
|
@PostMapping("/batch")
|
||||||
|
public R<Integer> addBatch(@RequestBody List<PerfAppraisalBo> list) {
|
||||||
|
return R.ok(iPerfAppraisalService.batchInsertByBo(list));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -85,6 +85,15 @@ public class PerfAppraisalDetailController extends BaseController {
|
|||||||
return toAjax(iPerfAppraisalDetailService.updateByBo(bo));
|
return toAjax(iPerfAppraisalDetailService.updateByBo(bo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量修改考核维度评分明细
|
||||||
|
*/
|
||||||
|
@Log(title = "考核维度评分明细", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping("/batch")
|
||||||
|
public R<Integer> editBatch(@RequestBody List<PerfAppraisalDetailBo> list) {
|
||||||
|
return R.ok(iPerfAppraisalDetailService.batchUpdateByBo(list));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除考核维度评分明细
|
* 删除考核维度评分明细
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -75,6 +75,15 @@ public class PerfSalaryController extends BaseController {
|
|||||||
return toAjax(iPerfSalaryService.insertByBo(bo));
|
return toAjax(iPerfSalaryService.insertByBo(bo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量新增月度薪资计算记录
|
||||||
|
*/
|
||||||
|
@Log(title = "月度薪资计算记录", businessType = BusinessType.INSERT)
|
||||||
|
@PostMapping("/batch")
|
||||||
|
public R<Integer> addBatch(@RequestBody List<PerfSalaryBo> list) {
|
||||||
|
return R.ok(iPerfSalaryService.batchInsertByBo(list));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改月度薪资计算记录
|
* 修改月度薪资计算记录
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ public class PerfDept extends BaseEntity {
|
|||||||
* 状态(0正常 1停用)
|
* 状态(0正常 1停用)
|
||||||
*/
|
*/
|
||||||
private String status;
|
private String status;
|
||||||
|
/**
|
||||||
|
* 产线
|
||||||
|
*/
|
||||||
|
private String productionLine;
|
||||||
/**
|
/**
|
||||||
* 删除标志(0=正常 1=已删除)
|
* 删除标志(0=正常 1=已删除)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import javax.validation.constraints.*;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,5 +96,9 @@ public class PerfAppraisalBo extends BaseEntity {
|
|||||||
*/
|
*/
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 考核维度明细(批量新增时使用)
|
||||||
|
*/
|
||||||
|
private List<PerfAppraisalDetailBo> details;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ public class PerfDeptBo extends BaseEntity {
|
|||||||
*/
|
*/
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 产线
|
||||||
|
*/
|
||||||
|
private String productionLine;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 备注
|
* 备注
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ public class PerfDeptVo {
|
|||||||
@ExcelDictFormat(readConverterExp = "0=正常,1=停用")
|
@ExcelDictFormat(readConverterExp = "0=正常,1=停用")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 产线
|
||||||
|
*/
|
||||||
|
@ExcelProperty(value = "产线")
|
||||||
|
private String productionLine;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 备注
|
* 备注
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -46,4 +46,8 @@ public interface IPerfAppraisalDetailService {
|
|||||||
* 校验并批量删除考核维度评分明细信息
|
* 校验并批量删除考核维度评分明细信息
|
||||||
*/
|
*/
|
||||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||||
|
/**
|
||||||
|
* 批量修改考核维度评分明细
|
||||||
|
*/
|
||||||
|
int batchUpdateByBo(List<PerfAppraisalDetailBo> list);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public interface IPerfAppraisalService {
|
|||||||
/**
|
/**
|
||||||
* 新增月度绩效考核记录
|
* 新增月度绩效考核记录
|
||||||
*/
|
*/
|
||||||
Boolean insertByBo(PerfAppraisalBo bo);
|
PerfAppraisalBo insertByBo(PerfAppraisalBo bo);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改月度绩效考核记录
|
* 修改月度绩效考核记录
|
||||||
@@ -46,4 +46,8 @@ public interface IPerfAppraisalService {
|
|||||||
* 校验并批量删除月度绩效考核记录信息
|
* 校验并批量删除月度绩效考核记录信息
|
||||||
*/
|
*/
|
||||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||||
|
/**
|
||||||
|
* 批量新增月度绩效考核记录(含维度明细)
|
||||||
|
*/
|
||||||
|
int batchInsertByBo(List<PerfAppraisalBo> list);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,4 +46,8 @@ public interface IPerfSalaryService {
|
|||||||
* 校验并批量删除月度薪资计算记录信息
|
* 校验并批量删除月度薪资计算记录信息
|
||||||
*/
|
*/
|
||||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||||
|
/**
|
||||||
|
* 批量新增月度薪资计算记录
|
||||||
|
*/
|
||||||
|
int batchInsertByBo(List<PerfSalaryBo> list);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,20 @@ public class PerfAppraisalDetailServiceImpl implements IPerfAppraisalDetailServi
|
|||||||
return baseMapper.updateById(update) > 0;
|
return baseMapper.updateById(update) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量修改考核维度评分明细
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int batchUpdateByBo(List<PerfAppraisalDetailBo> list) {
|
||||||
|
if (list == null || list.isEmpty()) return 0;
|
||||||
|
int count = 0;
|
||||||
|
for (PerfAppraisalDetailBo bo : list) {
|
||||||
|
PerfAppraisalDetail update = BeanUtil.toBean(bo, PerfAppraisalDetail.class);
|
||||||
|
if (baseMapper.updateById(update) > 0) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存前的数据校验
|
* 保存前的数据校验
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import com.klp.perf.domain.bo.PerfAppraisalBo;
|
|||||||
import com.klp.perf.domain.vo.PerfAppraisalVo;
|
import com.klp.perf.domain.vo.PerfAppraisalVo;
|
||||||
import com.klp.perf.domain.PerfAppraisal;
|
import com.klp.perf.domain.PerfAppraisal;
|
||||||
import com.klp.perf.mapper.PerfAppraisalMapper;
|
import com.klp.perf.mapper.PerfAppraisalMapper;
|
||||||
|
import com.klp.perf.mapper.PerfAppraisalDetailMapper;
|
||||||
|
import com.klp.perf.domain.PerfAppraisalDetail;
|
||||||
|
import com.klp.perf.domain.bo.PerfAppraisalDetailBo;
|
||||||
import com.klp.perf.service.IPerfAppraisalService;
|
import com.klp.perf.service.IPerfAppraisalService;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -30,6 +33,7 @@ import java.util.Collection;
|
|||||||
public class PerfAppraisalServiceImpl implements IPerfAppraisalService {
|
public class PerfAppraisalServiceImpl implements IPerfAppraisalService {
|
||||||
|
|
||||||
private final PerfAppraisalMapper baseMapper;
|
private final PerfAppraisalMapper baseMapper;
|
||||||
|
private final PerfAppraisalDetailMapper detailMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询月度绩效考核记录
|
* 查询月度绩效考核记录
|
||||||
@@ -81,14 +85,14 @@ public class PerfAppraisalServiceImpl implements IPerfAppraisalService {
|
|||||||
* 新增月度绩效考核记录
|
* 新增月度绩效考核记录
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Boolean insertByBo(PerfAppraisalBo bo) {
|
public PerfAppraisalBo insertByBo(PerfAppraisalBo bo) {
|
||||||
PerfAppraisal add = BeanUtil.toBean(bo, PerfAppraisal.class);
|
PerfAppraisal add = BeanUtil.toBean(bo, PerfAppraisal.class);
|
||||||
validEntityBeforeSave(add);
|
validEntityBeforeSave(add);
|
||||||
boolean flag = baseMapper.insert(add) > 0;
|
boolean flag = baseMapper.insert(add) > 0;
|
||||||
if (flag) {
|
if (flag) {
|
||||||
bo.setId(add.getId());
|
bo.setId(add.getId());
|
||||||
}
|
}
|
||||||
return flag;
|
return bo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -101,6 +105,35 @@ public class PerfAppraisalServiceImpl implements IPerfAppraisalService {
|
|||||||
return baseMapper.updateById(update) > 0;
|
return baseMapper.updateById(update) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量新增月度绩效考核记录(含维度明细)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int batchInsertByBo(List<PerfAppraisalBo> list) {
|
||||||
|
if (list == null || list.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int count = 0;
|
||||||
|
for (PerfAppraisalBo bo : list) {
|
||||||
|
PerfAppraisal entity = BeanUtil.toBean(bo, PerfAppraisal.class);
|
||||||
|
validEntityBeforeSave(entity);
|
||||||
|
if (baseMapper.insert(entity) > 0) {
|
||||||
|
bo.setId(entity.getId());
|
||||||
|
count++;
|
||||||
|
// 插入维度明细
|
||||||
|
List<PerfAppraisalDetailBo> details = bo.getDetails();
|
||||||
|
if (details != null && !details.isEmpty()) {
|
||||||
|
for (PerfAppraisalDetailBo detailBo : details) {
|
||||||
|
detailBo.setAppraisalId(entity.getId());
|
||||||
|
PerfAppraisalDetail detail = BeanUtil.toBean(detailBo, PerfAppraisalDetail.class);
|
||||||
|
detailMapper.insert(detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存前的数据校验
|
* 保存前的数据校验
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -152,6 +152,26 @@ public class PerfSalaryServiceImpl implements IPerfSalaryService {
|
|||||||
return baseMapper.updateById(update) > 0;
|
return baseMapper.updateById(update) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量新增月度薪资计算记录
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int batchInsertByBo(List<PerfSalaryBo> list) {
|
||||||
|
if (list == null || list.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int count = 0;
|
||||||
|
for (PerfSalaryBo bo : list) {
|
||||||
|
PerfSalary entity = BeanUtil.toBean(bo, PerfSalary.class);
|
||||||
|
validEntityBeforeSave(entity);
|
||||||
|
if (baseMapper.insert(entity) > 0) {
|
||||||
|
bo.setId(entity.getId());
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存前的数据校验
|
* 保存前的数据校验
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<result property="orderNum" column="order_num"/>
|
<result property="orderNum" column="order_num"/>
|
||||||
<result property="hasBaoSalary" column="has_bao_salary"/>
|
<result property="hasBaoSalary" column="has_bao_salary"/>
|
||||||
<result property="status" column="status"/>
|
<result property="status" column="status"/>
|
||||||
|
<result property="productionLine" column="production_line"/>
|
||||||
<result property="delFlag" column="del_flag"/>
|
<result property="delFlag" column="del_flag"/>
|
||||||
<result property="remark" column="remark"/>
|
<result property="remark" column="remark"/>
|
||||||
<result property="createBy" column="create_by"/>
|
<result property="createBy" column="create_by"/>
|
||||||
|
|||||||
@@ -42,3 +42,12 @@ export function delPerfAppraisal(ids) {
|
|||||||
method: 'delete'
|
method: 'delete'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量新增月度绩效考核记录(含维度明细)
|
||||||
|
export function batchAddPerfAppraisal(data) {
|
||||||
|
return request({
|
||||||
|
url: '/perf/appraisal/batch',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,3 +42,12 @@ export function delPerfAppraisalDetail(ids) {
|
|||||||
method: 'delete'
|
method: 'delete'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量修改考核维度评分明细
|
||||||
|
export function batchUpdatePerfAppraisalDetail(data) {
|
||||||
|
return request({
|
||||||
|
url: '/perf/appraisalDetail/batch',
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,3 +42,12 @@ export function delPerfSalary(ids) {
|
|||||||
method: 'delete'
|
method: 'delete'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量新增月度薪资计算记录
|
||||||
|
export function batchAddPerfSalary(data) {
|
||||||
|
return request({
|
||||||
|
url: '/perf/salary/batch',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
110
klp-ui/src/utils/perfDataService.js
Normal file
110
klp-ui/src/utils/perfDataService.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* 绩效考核数据获取服务
|
||||||
|
*
|
||||||
|
* 从 WMS 系统拉取生产、质量、成本数据,用于考核计算引擎。
|
||||||
|
*/
|
||||||
|
import { listLightPendingAction } from '@/api/wms/pendingAction'
|
||||||
|
import { getCoilStatisticsList } from '@/api/wms/coil'
|
||||||
|
import { PRODUCTION_LINES } from '@/utils/meta'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据产线 ID 串获取对应的加工工序 actionTypes(逗号分隔)
|
||||||
|
* 例如: "1,4" → "501,202,521,206,505,525"
|
||||||
|
*/
|
||||||
|
function getProcesses(productionLineIds) {
|
||||||
|
if (!productionLineIds) return ''
|
||||||
|
const ids = productionLineIds.split(',').map(s => parseInt(s.trim()))
|
||||||
|
const processes = []
|
||||||
|
ids.forEach(id => {
|
||||||
|
const pl = PRODUCTION_LINES.find(p => p.id === id)
|
||||||
|
if (pl && pl.processes) {
|
||||||
|
pl.processes.split(',').forEach(p => {
|
||||||
|
const trimmed = p.trim()
|
||||||
|
if (trimmed && !processes.includes(trimmed)) {
|
||||||
|
processes.push(trimmed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return processes.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取部门在指定周期的生产数据
|
||||||
|
* @param {String} productionLineIds - 部门绑定的产线ID串,如 "1,4"
|
||||||
|
* @param {String} period - 考核周期 "YYYY-MM"
|
||||||
|
* @returns {{ totalWeight: Number, abCount: Number, totalCount: Number, avgCostPerTon: Number }}
|
||||||
|
*/
|
||||||
|
export async function fetchProductionData(productionLineIds, period) {
|
||||||
|
const processes = getProcesses(productionLineIds)
|
||||||
|
if (!processes) {
|
||||||
|
return { totalWeight: 0, abCount: 0, totalCount: 0, avgCostPerTon: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: 查询指定工序已完成的加工记录
|
||||||
|
const pendingRes = await listLightPendingAction({
|
||||||
|
actionStatus: 2,
|
||||||
|
actionTypes: processes
|
||||||
|
})
|
||||||
|
const records = pendingRes.rows || pendingRes.data || []
|
||||||
|
const coilIds = []
|
||||||
|
records.forEach(r => {
|
||||||
|
if (r.processedCoilIds) {
|
||||||
|
const ids = typeof r.processedCoilIds === 'string'
|
||||||
|
? r.processedCoilIds.split(',').map(s => s.trim()).filter(Boolean)
|
||||||
|
: r.processedCoilIds
|
||||||
|
ids.forEach(id => { if (!coilIds.includes(id)) coilIds.push(id) })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (coilIds.length === 0) {
|
||||||
|
return { totalWeight: 0, abCount: 0, totalCount: 0, avgCostPerTon: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: 根据钢卷 ID 查询统计数据
|
||||||
|
// 构造 period 的起止时间
|
||||||
|
const [year, month] = period.split('-')
|
||||||
|
const startTime = `${year}-${month}-01 00:00:00`
|
||||||
|
const lastDay = new Date(parseInt(year), parseInt(month), 0).getDate()
|
||||||
|
const endTime = `${year}-${month}-${String(lastDay).padStart(2, '0')} 23:59:59`
|
||||||
|
|
||||||
|
const statsRes = await getCoilStatisticsList({
|
||||||
|
coilIds: coilIds.join(','),
|
||||||
|
startTime,
|
||||||
|
endTime
|
||||||
|
})
|
||||||
|
const coils = statsRes.rows || statsRes.data || []
|
||||||
|
|
||||||
|
// Step 3: 汇总产量、质量、成本
|
||||||
|
let totalWeight = 0
|
||||||
|
let abCount = 0
|
||||||
|
let totalCount = coils.length
|
||||||
|
|
||||||
|
coils.forEach(c => {
|
||||||
|
// 产量:累加钢卷重量
|
||||||
|
const weight = parseFloat(c.coilWeight) || parseFloat(c.weight) || 0
|
||||||
|
totalWeight += weight
|
||||||
|
|
||||||
|
// 质量:统计 A/B 级
|
||||||
|
const quality = (c.quality || c.qualityStatus || c.grade || '').toString().trim().toUpperCase()
|
||||||
|
if (quality === 'A' || quality === 'B') {
|
||||||
|
abCount++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 成本:从统计数据中获取吨钢成本(如果有)
|
||||||
|
let avgCostPerTon = 0
|
||||||
|
let costCount = 0
|
||||||
|
coils.forEach(c => {
|
||||||
|
const cost = parseFloat(c.costPerTon) || parseFloat(c.unitCost) || 0
|
||||||
|
if (cost > 0) {
|
||||||
|
avgCostPerTon += cost
|
||||||
|
costCount++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (costCount > 0) {
|
||||||
|
avgCostPerTon = avgCostPerTon / costCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return { totalWeight, abCount, totalCount, avgCostPerTon }
|
||||||
|
}
|
||||||
120
klp-ui/src/utils/scoreEngine.js
Normal file
120
klp-ui/src/utils/scoreEngine.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* 考核系数计算引擎
|
||||||
|
*
|
||||||
|
* DIMENSION_CALCULATORS: 维度计算器注册表,key 为 dimName,value 为计算器对象
|
||||||
|
* calculateDimScore(detail): 根据 detail 计算单维度得分
|
||||||
|
* calculateTotal(details): 遍历所有 detail,计算加权总分
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ========== 计算器注册表 ==========
|
||||||
|
// 每个计算器: { calc(actual, target, weight) => score, desc }
|
||||||
|
// actual — 实际值(数值)
|
||||||
|
// target — 目标值(数值)
|
||||||
|
// weight — 权重(如 40 表示 40%)
|
||||||
|
// 返回值: 维度得分(0 ~ 120 之间)
|
||||||
|
|
||||||
|
const DIMENSION_CALCULATORS = {
|
||||||
|
'生产效率': {
|
||||||
|
desc: '实际产量 / 目标产量 × 100 × 权重%',
|
||||||
|
calc(actual, target, weight) {
|
||||||
|
// 达成率 = 实际产量 / 目标产量,封顶 120%
|
||||||
|
const rate = Math.min(1.2, actual / target)
|
||||||
|
return Math.round(rate * 100 * (weight / 100) * 100) / 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'产品质量': {
|
||||||
|
desc: 'A+B 级卷占比 ≥ 目标值 → 满分,否则按比例得分',
|
||||||
|
calc(actual, target, weight) {
|
||||||
|
// actual: A+B级卷占比(%), target: 目标占比(%)
|
||||||
|
if (actual >= target) return weight
|
||||||
|
if (target === 0) return 0
|
||||||
|
return Math.round(weight * (actual / target) * 100) / 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'成本控制': {
|
||||||
|
desc: '平均吨钢成本 ≤ 目标值 → 满分,否则按比例扣分',
|
||||||
|
calc(actual, target, weight) {
|
||||||
|
// actual: 实际吨钢成本(元), target: 目标吨钢成本(元)
|
||||||
|
if (actual <= target) return weight
|
||||||
|
if (actual === 0) return weight
|
||||||
|
return Math.round(weight * (target / actual) * 100) / 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'安全管理': {
|
||||||
|
desc: '一票否决制:事故次数 > 目标值 → 0 分',
|
||||||
|
calc(actual, target, weight) {
|
||||||
|
// TODO: 待定义具体逻辑
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'设备维护': {
|
||||||
|
desc: '故障停机时间越少得分越高',
|
||||||
|
calc(actual, target, weight) {
|
||||||
|
// TODO: 待定义具体逻辑
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'团队管理': {
|
||||||
|
desc: '主观打分 / 100 × 权重',
|
||||||
|
calc(actual, target, weight) {
|
||||||
|
// TODO: 待定义具体逻辑
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 辅助函数 ==========
|
||||||
|
|
||||||
|
/** 安全解析为数值,解析失败返回 0 */
|
||||||
|
function parseNum(val) {
|
||||||
|
if (val === null || val === undefined || val === '') return 0
|
||||||
|
const n = parseFloat(val)
|
||||||
|
return isNaN(n) ? 0 : n
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算单个维度的得分
|
||||||
|
* @param {Object} detail - appraisal_detail 对象,需含 dimName, actualValue, targetValue, weight
|
||||||
|
* @returns {Number} 维度得分
|
||||||
|
*/
|
||||||
|
export function calculateDimScore(detail) {
|
||||||
|
const calcEntry = DIMENSION_CALCULATORS[detail.dimName]
|
||||||
|
if (!calcEntry) return 0
|
||||||
|
|
||||||
|
const actual = parseNum(detail.actualValue)
|
||||||
|
const target = parseNum(detail.targetValue)
|
||||||
|
const weight = parseNum(detail.weight)
|
||||||
|
|
||||||
|
if (target === 0) return 0
|
||||||
|
|
||||||
|
return calcEntry.calc(actual, target, weight)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量计算所有维度得分及加权总分
|
||||||
|
* @param {Array} details - appraisal_detail 对象数组
|
||||||
|
* @returns {{ finalScore: Number, details: Array }} 总分及带有计算得分的明细
|
||||||
|
*/
|
||||||
|
export function calculateTotal(details) {
|
||||||
|
if (!details || !details.length) return { finalScore: 0, details: [] }
|
||||||
|
|
||||||
|
let weightedSum = 0
|
||||||
|
let totalBonus = 0
|
||||||
|
let totalPenalty = 0
|
||||||
|
|
||||||
|
const computed = details.map(d => {
|
||||||
|
const dimScore = calculateDimScore(d)
|
||||||
|
const bonus = parseNum(d.bonus) || 0
|
||||||
|
const penalty = parseNum(d.penalty) || 0
|
||||||
|
totalBonus += bonus
|
||||||
|
totalPenalty += penalty
|
||||||
|
weightedSum += dimScore
|
||||||
|
return { ...d, _dimScore: dimScore }
|
||||||
|
})
|
||||||
|
|
||||||
|
const finalScore = Math.min(120, weightedSum + totalBonus - totalPenalty)
|
||||||
|
|
||||||
|
return { finalScore: Math.max(0, finalScore), details: computed }
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DIMENSION_CALCULATORS }
|
||||||
@@ -35,22 +35,43 @@
|
|||||||
>
|
>
|
||||||
<!-- 编辑态 -->
|
<!-- 编辑态 -->
|
||||||
<template v-if="editingDeptId === dept.id">
|
<template v-if="editingDeptId === dept.id">
|
||||||
<el-input
|
<div class="dept-edit-wrap" @click.stop>
|
||||||
v-model="editDeptName"
|
<el-input
|
||||||
size="small"
|
v-model="editDeptName"
|
||||||
ref="editInput"
|
size="small"
|
||||||
:disabled="deptSubmitting"
|
ref="editInput"
|
||||||
@keyup.enter.native="submitEditDept(dept)"
|
:disabled="deptSubmitting"
|
||||||
@keyup.esc.native="cancelEditDept"
|
>
|
||||||
@blur="submitEditDept(dept)"
|
<template v-if="deptSubmitting" slot="prefix"><i class="el-icon-loading" /></template>
|
||||||
@click.stop
|
</el-input>
|
||||||
>
|
<el-select
|
||||||
<template v-if="deptSubmitting" slot="prefix"><i class="el-icon-loading" /></template>
|
v-model="editProductionLine"
|
||||||
</el-input>
|
size="small"
|
||||||
|
multiple
|
||||||
|
collapse-tags
|
||||||
|
placeholder="选择产线"
|
||||||
|
style="width: 100%; margin-top: 4px"
|
||||||
|
:disabled="deptSubmitting"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="pl in PRODUCTION_LINES"
|
||||||
|
:key="pl.id"
|
||||||
|
:label="pl.name"
|
||||||
|
:value="pl.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<div class="dept-edit-actions">
|
||||||
|
<el-button size="mini" type="primary" :loading="deptSubmitting" @click="submitEditDept(dept)">保存</el-button>
|
||||||
|
<el-button size="mini" @click="cancelEditDept">取消</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- 展示态 -->
|
<!-- 展示态 -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="dept-name">{{ dept.deptName }}</span>
|
<div class="dept-info">
|
||||||
|
<span class="dept-name">{{ dept.deptName }}</span>
|
||||||
|
<span v-if="dept.productionLine" class="dept-pl">{{ plNames(dept.productionLine) }}</span>
|
||||||
|
</div>
|
||||||
<span class="dept-actions">
|
<span class="dept-actions">
|
||||||
<el-tooltip content="修改部门名称" placement="top" :open-delay="500">
|
<el-tooltip content="修改部门名称" placement="top" :open-delay="500">
|
||||||
<el-button type="text" size="mini" icon="el-icon-edit" @click.stop="startEditDept(dept)">编辑</el-button>
|
<el-button type="text" size="mini" icon="el-icon-edit" @click.stop="startEditDept(dept)">编辑</el-button>
|
||||||
@@ -150,6 +171,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { listPerfDept, addPerfDept, updatePerfDept, delPerfDept } from '@/api/perf/dept'
|
import { listPerfDept, addPerfDept, updatePerfDept, delPerfDept } from '@/api/perf/dept'
|
||||||
import { listEmployeeInfo, updateEmployeeInfo } from '@/api/wms/employeeInfo'
|
import { listEmployeeInfo, updateEmployeeInfo } from '@/api/wms/employeeInfo'
|
||||||
|
import { PRODUCTION_LINES } from '@/utils/meta'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PerfDept',
|
name: 'PerfDept',
|
||||||
@@ -165,6 +187,8 @@ export default {
|
|||||||
deptSubmitting: false,
|
deptSubmitting: false,
|
||||||
editingDeptId: null,
|
editingDeptId: null,
|
||||||
editDeptName: '',
|
editDeptName: '',
|
||||||
|
editProductionLine: [],
|
||||||
|
PRODUCTION_LINES,
|
||||||
|
|
||||||
// 员工
|
// 员工
|
||||||
empLoading: false,
|
empLoading: false,
|
||||||
@@ -209,6 +233,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 产线 ID 串 → 名称串
|
||||||
|
plNames(idsStr) {
|
||||||
|
if (!idsStr) return ''
|
||||||
|
const ids = idsStr.split(',').map(s => parseInt(s.trim()))
|
||||||
|
return ids.map(id => {
|
||||||
|
const pl = PRODUCTION_LINES.find(p => p.id === id)
|
||||||
|
return pl ? pl.name : ''
|
||||||
|
}).filter(Boolean).join(', ')
|
||||||
|
},
|
||||||
// ========== 部门 ==========
|
// ========== 部门 ==========
|
||||||
loadDeptList() {
|
loadDeptList() {
|
||||||
this.deptLoading = true
|
this.deptLoading = true
|
||||||
@@ -260,6 +293,7 @@ export default {
|
|||||||
startEditDept(dept) {
|
startEditDept(dept) {
|
||||||
this.editingDeptId = dept.id
|
this.editingDeptId = dept.id
|
||||||
this.editDeptName = dept.deptName
|
this.editDeptName = dept.deptName
|
||||||
|
this.editProductionLine = dept.productionLine ? dept.productionLine.split(',').map(s => parseInt(s.trim())) : []
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const input = this.$refs.editInput
|
const input = this.$refs.editInput
|
||||||
if (input && input[0]) input[0].focus()
|
if (input && input[0]) input[0].focus()
|
||||||
@@ -268,16 +302,30 @@ export default {
|
|||||||
cancelEditDept() {
|
cancelEditDept() {
|
||||||
this.editingDeptId = null
|
this.editingDeptId = null
|
||||||
this.editDeptName = ''
|
this.editDeptName = ''
|
||||||
|
this.editProductionLine = []
|
||||||
|
},
|
||||||
|
handleEditBlur(dept) {
|
||||||
|
// 延迟提交,让 el-select 的点击有时间先触发
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.editingDeptId === dept.id) {
|
||||||
|
this.submitEditDept(dept)
|
||||||
|
}
|
||||||
|
}, 200)
|
||||||
},
|
},
|
||||||
submitEditDept(dept) {
|
submitEditDept(dept) {
|
||||||
const name = this.editDeptName.trim()
|
const name = this.editDeptName.trim()
|
||||||
if (!name || name === dept.deptName) {
|
const plStr = Array.isArray(this.editProductionLine) ? this.editProductionLine.join(',') : ''
|
||||||
|
if (!name && !plStr) {
|
||||||
|
this.editingDeptId = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (name === dept.deptName && plStr === (dept.productionLine || '')) {
|
||||||
this.editingDeptId = null
|
this.editingDeptId = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.deptSubmitting) return
|
if (this.deptSubmitting) return
|
||||||
this.deptSubmitting = true
|
this.deptSubmitting = true
|
||||||
updatePerfDept({ id: dept.id, deptName: name }).then(() => {
|
updatePerfDept({ id: dept.id, deptName: name, productionLine: plStr }).then(() => {
|
||||||
this.$message.success('修改成功')
|
this.$message.success('修改成功')
|
||||||
this.editingDeptId = null
|
this.editingDeptId = null
|
||||||
this.loadDeptList()
|
this.loadDeptList()
|
||||||
@@ -470,24 +518,51 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dept-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.dept-name {
|
.dept-name {
|
||||||
|
display: block;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex: 1;
|
}
|
||||||
|
|
||||||
|
.dept-pl {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dept-actions {
|
.dept-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--input {
|
&--input {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dept-edit-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.dept-edit-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dept-empty {
|
.dept-empty {
|
||||||
|
|||||||
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>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<span class="breadcrumb">{{ deptName }} · {{ period }}</span>
|
<span class="breadcrumb">{{ deptName }}<template v-if="productionLine"> · {{ productionLine }}</template> · {{ period }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="custom-tabs">
|
<div class="custom-tabs">
|
||||||
@@ -27,6 +27,7 @@ export default {
|
|||||||
name: 'PageHeader',
|
name: 'PageHeader',
|
||||||
props: {
|
props: {
|
||||||
deptName: { type: String, default: '' },
|
deptName: { type: String, default: '' },
|
||||||
|
productionLine: { type: String, default: '' },
|
||||||
period: { type: String, default: '' },
|
period: { type: String, default: '' },
|
||||||
activeTab: { type: String, default: '' },
|
activeTab: { type: String, default: '' },
|
||||||
tabOptions: {
|
tabOptions: {
|
||||||
|
|||||||
@@ -83,7 +83,10 @@
|
|||||||
<el-card class="config-card mb8" shadow="never">
|
<el-card class="config-card mb8" shadow="never">
|
||||||
<div slot="header" class="card-header">
|
<div slot="header" class="card-header">
|
||||||
<span class="card-title">扣款/奖励项目配置</span>
|
<span class="card-title">扣款/奖励项目配置</span>
|
||||||
<el-button size="mini" type="primary" icon="el-icon-plus" @click="handleAddItem">新增</el-button>
|
<div>
|
||||||
|
<el-button size="mini" type="primary" icon="el-icon-plus" @click="handleAddItem">新增</el-button>
|
||||||
|
<el-button size="mini" icon="el-icon-upload2" @click="handleOpenImport">导入</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<el-table :data="itemList" stripe size="mini" v-loading="itemLoading" empty-text="暂无项目配置">
|
<el-table :data="itemList" stripe size="mini" v-loading="itemLoading" empty-text="暂无项目配置">
|
||||||
@@ -141,7 +144,6 @@
|
|||||||
<el-button size="mini" type="primary" icon="el-icon-plus" @click="handleAddDim">新增</el-button>
|
<el-button size="mini" type="primary" icon="el-icon-plus" @click="handleAddDim">新增</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="dimList" stripe size="mini" v-loading="dimLoading" empty-text="暂无维度配置">
|
<el-table :data="dimList" stripe size="mini" v-loading="dimLoading" empty-text="暂无维度配置">
|
||||||
<el-table-column prop="seq" label="序号" width="60" align="center" />
|
|
||||||
<el-table-column prop="dimName" label="考核维度" min-width="120" />
|
<el-table-column prop="dimName" label="考核维度" min-width="120" />
|
||||||
<el-table-column prop="weight" label="权重" width="70" align="center" />
|
<el-table-column prop="weight" label="权重" width="70" align="center" />
|
||||||
<el-table-column prop="indicator" label="量化指标" min-width="140" show-overflow-tooltip />
|
<el-table-column prop="indicator" label="量化指标" min-width="140" show-overflow-tooltip />
|
||||||
@@ -229,12 +231,7 @@
|
|||||||
<el-dialog :visible.sync="dimFormVisible" :title="dimFormTitle" width="560px" append-to-body @closed="resetDimForm">
|
<el-dialog :visible.sync="dimFormVisible" :title="dimFormTitle" width="560px" append-to-body @closed="resetDimForm">
|
||||||
<el-form ref="dimFormRef" :model="dimForm" :rules="dimRules" label-width="90px" size="small">
|
<el-form ref="dimFormRef" :model="dimForm" :rules="dimRules" label-width="90px" size="small">
|
||||||
<el-row :gutter="12">
|
<el-row :gutter="12">
|
||||||
<el-col :span="8">
|
<el-col :span="12">
|
||||||
<el-form-item label="序号" prop="seq">
|
|
||||||
<el-input v-model="dimForm.seq" placeholder="如 1.1" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="考核维度" prop="dimName">
|
<el-form-item label="考核维度" prop="dimName">
|
||||||
<el-select v-model="dimForm.dimName" placeholder="请选择考核维度" style="width: 100%">
|
<el-select v-model="dimForm.dimName" placeholder="请选择考核维度" style="width: 100%">
|
||||||
<el-option label="生产效率" value="生产效率" />
|
<el-option label="生产效率" value="生产效率" />
|
||||||
@@ -246,7 +243,7 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="12">
|
||||||
<el-form-item label="权重" prop="weight">
|
<el-form-item label="权重" prop="weight">
|
||||||
<el-input v-model="dimForm.weight" placeholder="如 40%" />
|
<el-input v-model="dimForm.weight" placeholder="如 40%" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -279,6 +276,26 @@
|
|||||||
<el-button type="primary" :loading="dimSubmitting" @click="submitDim">保存</el-button>
|
<el-button type="primary" :loading="dimSubmitting" @click="submitDim">保存</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 导入扣款/奖励项目 -->
|
||||||
|
<el-dialog :visible.sync="importDialogVisible" title="导入扣款/奖励项目" width="440px" append-to-body>
|
||||||
|
<el-form label-width="80px" size="small">
|
||||||
|
<el-form-item label="来源部门">
|
||||||
|
<el-select v-model="importDeptId" placeholder="请选择已配置的部门" style="width: 100%" filterable>
|
||||||
|
<el-option
|
||||||
|
v-for="d in importDeptList"
|
||||||
|
:key="d.id"
|
||||||
|
:label="d.deptName"
|
||||||
|
:value="d.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer">
|
||||||
|
<el-button size="small" @click="importDialogVisible = false">取消</el-button>
|
||||||
|
<el-button size="small" type="primary" :loading="importLoading" @click="handleImportItems">确认导入</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -287,6 +304,7 @@ import { listPerfDeptItemConfig, addPerfDeptItemConfig, updatePerfDeptItemConfig
|
|||||||
import { listPerfPositionTemplate, addPerfPositionTemplate, updatePerfPositionTemplate, delPerfPositionTemplate } from '@/api/perf/positionTemplate'
|
import { listPerfPositionTemplate, addPerfPositionTemplate, updatePerfPositionTemplate, delPerfPositionTemplate } from '@/api/perf/positionTemplate'
|
||||||
import { listPerfTemplateDimension, addPerfTemplateDimension, updatePerfTemplateDimension, delPerfTemplateDimension } from '@/api/perf/templateDimension'
|
import { listPerfTemplateDimension, addPerfTemplateDimension, updatePerfTemplateDimension, delPerfTemplateDimension } from '@/api/perf/templateDimension'
|
||||||
import { listPerfDeptConfig, addPerfDeptConfig, updatePerfDeptConfig } from '@/api/perf/deptConfig'
|
import { listPerfDeptConfig, addPerfDeptConfig, updatePerfDeptConfig } from '@/api/perf/deptConfig'
|
||||||
|
import { listPerfDept } from '@/api/perf/dept'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PerfConfigPanel',
|
name: 'PerfConfigPanel',
|
||||||
@@ -311,11 +329,11 @@ export default {
|
|||||||
cfgSnapshotLock: false,
|
cfgSnapshotLock: false,
|
||||||
cfgForm: { monthlyProductionTarget: '', productionUnit: '吨', perfUnitPrice: '', productionShifts: '', guaranteedShifts: '', deptCoeff: '', salaryMode: 0, baseDivisor: '', remark: '' },
|
cfgForm: { monthlyProductionTarget: '', productionUnit: '吨', perfUnitPrice: '', productionShifts: '', guaranteedShifts: '', deptCoeff: '', salaryMode: 0, baseDivisor: '', remark: '' },
|
||||||
cfgRules: {
|
cfgRules: {
|
||||||
monthlyProductionTarget: [{ required: true, message: '请输入月产量目标', trigger: 'blur' }],
|
// monthlyProductionTarget: [{ required: true, message: '请输入月产量目标', trigger: 'blur' }],
|
||||||
productionUnit: [{ required: true, message: '请选择产量单位', trigger: 'change' }],
|
// productionUnit: [{ required: true, message: '请选择产量单位', trigger: 'change' }],
|
||||||
perfUnitPrice: [{ required: true, message: '请输入绩效单价', trigger: 'blur' }],
|
// perfUnitPrice: [{ required: true, message: '请输入绩效单价', trigger: 'blur' }],
|
||||||
productionShifts: [{ required: true, message: '请输入生产班数', trigger: 'blur' }],
|
// productionShifts: [{ required: true, message: '请输入生产班数', trigger: 'blur' }],
|
||||||
deptCoeff: [{ required: true, message: '请输入车间系数', trigger: 'blur' }]
|
// deptCoeff: [{ required: true, message: '请输入车间系数', trigger: 'blur' }]
|
||||||
},
|
},
|
||||||
|
|
||||||
// Item Config
|
// Item Config
|
||||||
@@ -330,6 +348,12 @@ export default {
|
|||||||
itemName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }]
|
itemName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 导入
|
||||||
|
importDialogVisible: false,
|
||||||
|
importDeptId: null,
|
||||||
|
importDeptList: [],
|
||||||
|
importLoading: false,
|
||||||
|
|
||||||
// Template
|
// Template
|
||||||
templateLoading: false,
|
templateLoading: false,
|
||||||
templateList: [],
|
templateList: [],
|
||||||
@@ -354,7 +378,8 @@ export default {
|
|||||||
dimSubmitting: false,
|
dimSubmitting: false,
|
||||||
dimForm: {},
|
dimForm: {},
|
||||||
dimRules: {
|
dimRules: {
|
||||||
dimName: [{ required: true, message: '请选择考核维度', trigger: 'change' }]
|
dimName: [{ required: true, message: '请选择考核维度', trigger: 'change' }],
|
||||||
|
weight: [{ required: true, message: '请输入权重', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -505,6 +530,49 @@ export default {
|
|||||||
},
|
},
|
||||||
resetItemForm() { this.itemForm = {} },
|
resetItemForm() { this.itemForm = {} },
|
||||||
|
|
||||||
|
// ======== 导入扣款/奖励项目 ========
|
||||||
|
handleOpenImport() {
|
||||||
|
this.importDeptId = null
|
||||||
|
this.importDialogVisible = true
|
||||||
|
if (this.importDeptList.length === 0) {
|
||||||
|
listPerfDept({ pageSize: 9999 }).then(res => {
|
||||||
|
this.importDeptList = res.rows || []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleImportItems() {
|
||||||
|
if (!this.importDeptId) { this.$message.warning('请选择来源部门'); return }
|
||||||
|
this.importLoading = true
|
||||||
|
listPerfDeptItemConfig({ deptId: this.importDeptId, pageSize: 9999 }).then(res => {
|
||||||
|
const sourceItems = res.rows || []
|
||||||
|
if (sourceItems.length === 0) {
|
||||||
|
this.$message.warning('来源部门暂无项目配置')
|
||||||
|
return Promise.reject(new Error('empty'))
|
||||||
|
}
|
||||||
|
const tasks = sourceItems.map(item =>
|
||||||
|
addPerfDeptItemConfig({
|
||||||
|
deptId: this.deptId,
|
||||||
|
itemType: item.itemType,
|
||||||
|
itemName: item.itemName,
|
||||||
|
sortOrder: item.sortOrder || 0,
|
||||||
|
isEnabled: item.isEnabled,
|
||||||
|
month: this.period + '-01',
|
||||||
|
remark: item.remark || ''
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return Promise.all(tasks)
|
||||||
|
}).then(() => {
|
||||||
|
this.$message.success('导入成功')
|
||||||
|
this.importDialogVisible = false
|
||||||
|
this.loadItems()
|
||||||
|
}).catch((err) => {
|
||||||
|
if (err && err.message === 'empty') return
|
||||||
|
this.$message.error('导入失败')
|
||||||
|
}).finally(() => {
|
||||||
|
this.importLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
// ======== Templates ========
|
// ======== Templates ========
|
||||||
loadTemplates() {
|
loadTemplates() {
|
||||||
this.templateLoading = true
|
this.templateLoading = true
|
||||||
@@ -585,7 +653,7 @@ export default {
|
|||||||
},
|
},
|
||||||
handleAddDim() {
|
handleAddDim() {
|
||||||
this.dimFormTitle = '新增维度'
|
this.dimFormTitle = '新增维度'
|
||||||
this.dimForm = { seq: '', dimName: '', weight: '', indicator: '', targetValue: '', scoreFormula: '', scoreRule: '', isEnabled: 1, remark: '' }
|
this.dimForm = { dimName: '', weight: '', indicator: '', targetValue: '', scoreFormula: '', scoreRule: '', isEnabled: 1, remark: '' }
|
||||||
this.dimFormVisible = true
|
this.dimFormVisible = true
|
||||||
this.$nextTick(() => { this.$refs.dimFormRef?.clearValidate() })
|
this.$nextTick(() => { this.$refs.dimFormRef?.clearValidate() })
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,61 +15,85 @@
|
|||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAdd">新增</el-button>
|
<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>
|
<el-button size="small" type="success" icon="el-icon-s-operation" :loading="generating" @click="handleBatchGenerate">一键生成</el-button>
|
||||||
|
<el-button size="small" type="warning" icon="el-icon-upload" :loading="batchSaving" @click="handleBatchSave">批量保存</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 口径说明 -->
|
||||||
|
<div class="formula-bar" :class="{ expanded: formulaExpanded }" @click="formulaExpanded = !formulaExpanded">
|
||||||
|
<span class="formula-bar-title">
|
||||||
|
<i :class="formulaExpanded ? 'el-icon-arrow-down' : 'el-icon-arrow-right'" />
|
||||||
|
口径说明
|
||||||
|
</span>
|
||||||
|
<span class="formula-bar-hint" v-if="!formulaExpanded">总系数 = (岗位系数+固定系数+调整系数+手动系数+绩效系数) × 车间系数</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="formulaExpanded" class="formula-detail">
|
||||||
|
<div class="formula-row"><b>绩效系数</b> = 加权总分 ÷ 100(考核后自动同步)</div>
|
||||||
|
<div class="formula-row"><b>车间系数</b> = 车间固定系数(默认 1.0,不参与加法,作为整体乘数)</div>
|
||||||
|
<div class="formula-row"><b>总系数</b> = (岗位系数 + 固定系数 + 调整系数 + 手动系数 + 绩效系数) × 车间系数</div>
|
||||||
|
<div class="formula-row"><b>绩效工资</b> = 绩效基数 × 总系数</div>
|
||||||
|
<div class="formula-row"><b>实领薪资</b> = 底薪 + 绩效工资 + 奖励合计 − 扣款合计 + 其他金额(自动计算)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<el-table :data="list" stripe size="small" v-loading="loading" empty-text="暂无薪资记录" class="inline-edit-table" border>
|
<el-table ref="salaryTable" :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">
|
<el-table-column label="姓名" width="70" align="center">
|
||||||
<input v-model="s.row.baseSalary" class="cell-input cell-input--base" @change="markDirty(s.row)" />
|
<template slot-scope="scope">
|
||||||
|
<span class="cell-clickable" @click.stop="toggleRowExpand(scope.row)">{{ scope.row.wmsEmployeeInfo ? scope.row.wmsEmployeeInfo.name : '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="岗位" width="70" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span class="cell-clickable" @click.stop="toggleRowExpand(scope.row)">{{ scope.row.wmsEmployeeInfo ? scope.row.wmsEmployeeInfo.jobType : '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="底薪" width="60" align="right"><template slot-scope="s">
|
||||||
|
<input v-model="s.row.baseSalary" class="cell-input cell-input--base" @change="recalcSalary(s.row); markDirty(s.row)" />
|
||||||
</template></el-table-column>
|
</template></el-table-column>
|
||||||
<el-table-column label="绩效基数" width="90" align="right"><template slot-scope="s">
|
<el-table-column label="绩效基数" width="80" align="right"><template slot-scope="s">
|
||||||
<input v-model="s.row.perfBase" class="cell-input cell-input--base" @change="markDirty(s.row)" />
|
<input v-model="s.row.perfBase" class="cell-input cell-input--base" @change="markDirty(s.row)" />
|
||||||
</template></el-table-column>
|
</template></el-table-column>
|
||||||
|
|
||||||
<!-- 考核得分 -->
|
<!-- 考核得分 -->
|
||||||
<el-table-column label="考核得分" width="90" align="right"><template slot-scope="s">
|
<el-table-column label="考核得分" width="80" align="center">
|
||||||
<input v-model="s.row.perfScore" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
<template slot-scope="scope">
|
||||||
</template></el-table-column>
|
<span class="cell-clickable cell-clickable--score" @click.stop="toggleRowExpand(scope.row)">{{ scope.row.perfScore != null ? scope.row.perfScore : '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
<!-- 系数 -->
|
<!-- 系数 -->
|
||||||
<el-table-column label="绩效系数" width="90" align="right"><template slot-scope="s">
|
<el-table-column label="绩效系数" width="80" align="center">
|
||||||
<input v-model="s.row.perfCoeff" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
<template slot-scope="scope">
|
||||||
|
<span class="cell-clickable cell-clickable--coeff" @click.stop="toggleRowExpand(scope.row)">{{ scope.row.perfCoeff != null ? scope.row.perfCoeff : '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="车间系数" width="80" align="right"><template slot-scope="s">
|
||||||
|
<input v-model="s.row.deptCoeff" class="cell-input cell-input--coeff" @change="recalcCoeffs(s.row); markDirty(s.row)" />
|
||||||
</template></el-table-column>
|
</template></el-table-column>
|
||||||
<el-table-column label="车间系数" width="90" align="right"><template slot-scope="s">
|
<el-table-column label="岗位系数" width="80" align="right"><template slot-scope="s">
|
||||||
<input v-model="s.row.deptCoeff" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
<input v-model="s.row.posCoeff" class="cell-input cell-input--coeff" @change="recalcCoeffs(s.row); markDirty(s.row)" />
|
||||||
</template></el-table-column>
|
</template></el-table-column>
|
||||||
<el-table-column label="岗位系数" width="90" align="right"><template slot-scope="s">
|
<el-table-column label="固定系数" width="80" align="right"><template slot-scope="s">
|
||||||
<input v-model="s.row.posCoeff" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
<input v-model="s.row.fixedCoeff" class="cell-input cell-input--coeff" @change="recalcCoeffs(s.row); markDirty(s.row)" />
|
||||||
</template></el-table-column>
|
</template></el-table-column>
|
||||||
<el-table-column label="固定系数" width="90" align="right"><template slot-scope="s">
|
<el-table-column label="调整系数" width="80" align="right"><template slot-scope="s">
|
||||||
<input v-model="s.row.fixedCoeff" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
<input v-model="s.row.adjCoeff" class="cell-input cell-input--coeff" @change="recalcCoeffs(s.row); markDirty(s.row)" />
|
||||||
</template></el-table-column>
|
</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)" />
|
<!-- <el-table-column label="手动系数" width="80" align="right"><template slot-scope="s">
|
||||||
</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)" />
|
<input v-model="s.row.adjCoeffManual" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
||||||
</template></el-table-column>
|
</template></el-table-column> -->
|
||||||
<el-table-column label="总系数" width="80" align="right"><template slot-scope="s">
|
<el-table-column label="总系数" width="70" align="right">
|
||||||
<input v-model="s.row.totalCoeff" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
<template slot-scope="scope"><span class="cell-readonly">{{ fmt(scope.row.totalCoeff) }}</span></template>
|
||||||
</template></el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<!-- 绩效工资 -->
|
<!-- 绩效工资 -->
|
||||||
<el-table-column label="绩效工资" width="100" align="right"><template slot-scope="s">
|
<el-table-column label="绩效工资" width="80" align="right">
|
||||||
<input v-model="s.row.perfWage" class="cell-input cell-input--amount" @change="markDirty(s.row)" />
|
<template slot-scope="scope"><span class="cell-readonly">{{ fmt(scope.row.perfWage) }}</span></template>
|
||||||
</template></el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<!-- 动态扣款列 -->
|
<!-- 动态扣款列 -->
|
||||||
<el-table-column
|
<el-table-column
|
||||||
@@ -78,7 +102,7 @@
|
|||||||
><template slot-scope="s">
|
><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)" />
|
<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>
|
</template></el-table-column>
|
||||||
<el-table-column label="扣款合计" width="90" align="right">
|
<el-table-column label="扣款合计" width="80" align="right">
|
||||||
<template slot-scope="scope"><span class="cell-readonly cell-readonly--deduct">{{ scope.row._deductionsTotal }}</span></template>
|
<template slot-scope="scope"><span class="cell-readonly cell-readonly--deduct">{{ scope.row._deductionsTotal }}</span></template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
@@ -89,20 +113,20 @@
|
|||||||
><template slot-scope="s">
|
><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)" />
|
<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>
|
</template></el-table-column>
|
||||||
<el-table-column label="奖励合计" width="90" align="right">
|
<el-table-column label="奖励合计" width="80" align="right">
|
||||||
<template slot-scope="scope"><span class="cell-readonly cell-readonly--bonus">{{ scope.row._bonusesTotal }}</span></template>
|
<template slot-scope="scope"><span class="cell-readonly cell-readonly--bonus">{{ scope.row._bonusesTotal }}</span></template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<!-- 金额 -->
|
<!-- 金额 -->
|
||||||
<el-table-column label="其他金额" width="90" align="right"><template slot-scope="s">
|
<el-table-column label="其他金额" width="80" align="right"><template slot-scope="s">
|
||||||
<input v-model="s.row.otherAmount" class="cell-input cell-input--amount" @change="markDirty(s.row)" />
|
<input v-model="s.row.otherAmount" class="cell-input cell-input--amount" @change="recalcSalary(s.row); 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>
|
</template></el-table-column>
|
||||||
|
<el-table-column label="实领薪资" width="90" align="right">
|
||||||
|
<template slot-scope="scope"><span class="cell-readonly">{{ fmt(scope.row.actualSalary) }}</span></template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
<!-- 参考值 -->
|
<!-- 参考值 -->
|
||||||
<el-table-column label="固薪参考" width="90" align="right"><template slot-scope="s">
|
<!-- <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)" />
|
<input v-model="s.row.fixedSalaryRef" class="cell-input cell-input--ref" @change="markDirty(s.row)" />
|
||||||
</template></el-table-column>
|
</template></el-table-column>
|
||||||
<el-table-column label="调薪参考" width="90" align="right"><template slot-scope="s">
|
<el-table-column label="调薪参考" width="90" align="right"><template slot-scope="s">
|
||||||
@@ -116,10 +140,10 @@
|
|||||||
</template></el-table-column>
|
</template></el-table-column>
|
||||||
<el-table-column label="总薪参考" width="90" align="right"><template slot-scope="s">
|
<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)" />
|
<input v-model="s.row.totalSalaryRef" class="cell-input cell-input--ref" @change="markDirty(s.row)" />
|
||||||
</template></el-table-column>
|
</template></el-table-column> -->
|
||||||
|
|
||||||
<!-- 备注 -->
|
<!-- 备注 -->
|
||||||
<el-table-column label="备注" min-width="140"><template slot-scope="s">
|
<el-table-column label="备注" min-width="80"><template slot-scope="s">
|
||||||
<input v-model="s.row.remark" class="cell-input cell-input--remark" @change="markDirty(s.row)" />
|
<input v-model="s.row.remark" class="cell-input cell-input--remark" @change="markDirty(s.row)" />
|
||||||
</template></el-table-column>
|
</template></el-table-column>
|
||||||
|
|
||||||
@@ -133,6 +157,72 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 绩效考核明细展开面板 -->
|
||||||
|
<div v-if="expandedRow" class="expand-panel" v-loading="appLoading">
|
||||||
|
<div class="expand-panel-header">
|
||||||
|
<span class="expand-panel-title">
|
||||||
|
绩效考核评分 —
|
||||||
|
{{ expandedRow.wmsEmployeeInfo ? expandedRow.wmsEmployeeInfo.name : expandedRow.employeeId }}
|
||||||
|
({{ expandedRow.wmsEmployeeInfo ? expandedRow.wmsEmployeeInfo.jobType : '' }})
|
||||||
|
</span>
|
||||||
|
<el-tag v-if="expandedRow._appraisal" :type="expandedRow._appraisal.status === '1' ? 'warning' : 'info'" size="mini">
|
||||||
|
{{ expandedRow._appraisal.status === '1' ? '已确认' : '草稿' }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-if="expandedRow._appraisal" class="expand-panel-score">
|
||||||
|
加权总分:{{ weightedTotal }}
|
||||||
|
</span>
|
||||||
|
<el-button size="mini" type="success" :loading="saveLoading" @click="handleSaveScores">保存</el-button>
|
||||||
|
<el-button type="text" size="mini" icon="el-icon-close" @click="handleCollapseExpand" style="margin-left:auto">收起</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table v-if="expandedRow._appDetails && expandedRow._appDetails.length" :data="expandedRow._appDetails" size="mini" border class="expand-detail-table">
|
||||||
|
<el-table-column prop="dimName" label="考核维度" width="90" />
|
||||||
|
<el-table-column prop="quantitativeIndicator" label="考核指标" min-width="100" />
|
||||||
|
<el-table-column label="实际值" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<input v-model="scope.row.actualValue" class="cell-input cell-input--actual" @change="markDetailDirty(scope.row)" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="targetValue" label="目标值" width="80" align="center" />
|
||||||
|
<el-table-column label="得分" width="80" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<input v-model="scope.row.dimScore" class="cell-input cell-input--score" placeholder="0" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="加分" width="70" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<input v-model="scope.row.bonus" class="cell-input cell-input--bonus" placeholder="0" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="扣分" width="70" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<input v-model="scope.row.penalty" class="cell-input cell-input--penalty" placeholder="0" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="weight" label="权重" width="70" align="center" />
|
||||||
|
<el-table-column label="总分" width="80" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span class="cell-subtotal">{{ subtotal(scope.row) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="原始总分" width="80" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span class="cell-rawtotal">{{ rawRowTotal(scope.row) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="scoreRule" label="评分规则" min-width="120" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
<div v-else-if="expandedRow._appLoaded && !appLoading" class="expand-empty">暂无绩效考核数据</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 一键生成弹窗 -->
|
||||||
|
<GenerateDialog
|
||||||
|
:visible.sync="showGenerateDialog"
|
||||||
|
:dept-id="deptId"
|
||||||
|
:dept-name="deptName"
|
||||||
|
:period="period"
|
||||||
|
@done="onGenerateDone"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="pagination-wrapper">
|
<div class="pagination-wrapper">
|
||||||
<el-pagination
|
<el-pagination
|
||||||
:current-page="query.pageNum"
|
:current-page="query.pageNum"
|
||||||
@@ -149,15 +239,22 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { listPerfSalary, addPerfSalary, updatePerfSalary, delPerfSalary } from '@/api/perf/salary'
|
import { listPerfSalary, addPerfSalary, updatePerfSalary, delPerfSalary } from '@/api/perf/salary'
|
||||||
import { listEmployeeInfo } from '@/api/wms/employeeInfo'
|
import { listPerfAppraisal, updatePerfAppraisal } from '@/api/perf/appraisal'
|
||||||
|
import { listPerfAppraisalDetail, updatePerfAppraisalDetail, batchUpdatePerfAppraisalDetail } from '@/api/perf/appraisalDetail'
|
||||||
import { listPerfDeptItemConfig } from '@/api/perf/deptItemConfig'
|
import { listPerfDeptItemConfig } from '@/api/perf/deptItemConfig'
|
||||||
|
import { listPerfDeptConfig } from '@/api/perf/deptConfig'
|
||||||
|
import GenerateDialog from './GenerateDialog.vue'
|
||||||
|
import { calculateTotal } from '@/utils/scoreEngine'
|
||||||
|
import { fetchProductionData } from '@/utils/perfDataService'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SalaryPanel',
|
name: 'SalaryPanel',
|
||||||
|
components: { GenerateDialog },
|
||||||
props: {
|
props: {
|
||||||
deptId: { type: [Number, String], default: null },
|
deptId: { type: [Number, String], default: null },
|
||||||
deptName: { type: String, default: '' },
|
deptName: { type: String, default: '' },
|
||||||
period: { type: String, default: '' }
|
period: { type: String, default: '' },
|
||||||
|
productionLine: { type: String, default: '' }
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -172,7 +269,29 @@ export default {
|
|||||||
},
|
},
|
||||||
deductionItems: [],
|
deductionItems: [],
|
||||||
bonusItems: [],
|
bonusItems: [],
|
||||||
generating: false
|
generating: false,
|
||||||
|
batchSaving: false,
|
||||||
|
showGenerateDialog: false,
|
||||||
|
expandedRow: null,
|
||||||
|
appLoading: false,
|
||||||
|
calcLoading: false,
|
||||||
|
saveLoading: false,
|
||||||
|
_prodCache: null,
|
||||||
|
formulaExpanded: false,
|
||||||
|
_defaultDeptCoeff: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
weightedTotal() {
|
||||||
|
if (!this.expandedRow || !this.expandedRow._appDetails || !this.expandedRow._appDetails.length) return '-'
|
||||||
|
let sum = 0
|
||||||
|
this.expandedRow._appDetails.forEach(d => {
|
||||||
|
sum += (parseFloat(d.dimScore) || 0) * (parseFloat(d.weight) || 0) / 100
|
||||||
|
})
|
||||||
|
const bonus = this.expandedRow._appDetails.reduce((s, d) => s + (parseFloat(d.bonus) || 0), 0)
|
||||||
|
const penalty = this.expandedRow._appDetails.reduce((s, d) => s + (parseFloat(d.penalty) || 0), 0)
|
||||||
|
const total = Math.min(120, Math.max(0, sum + bonus - penalty))
|
||||||
|
return total.toFixed(1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -195,6 +314,7 @@ export default {
|
|||||||
},
|
},
|
||||||
loadItemConfig() {
|
loadItemConfig() {
|
||||||
if (!this.deptId || !this.period) return
|
if (!this.deptId || !this.period) return
|
||||||
|
// 加载扣款/奖励项目
|
||||||
listPerfDeptItemConfig({ deptId: this.deptId, month: this.period + '-01' }).then(res => {
|
listPerfDeptItemConfig({ deptId: this.deptId, month: this.period + '-01' }).then(res => {
|
||||||
const rows = res.rows || []
|
const rows = res.rows || []
|
||||||
this.deductionItems = rows.filter(r => r.itemType === 1 && r.isEnabled === 1)
|
this.deductionItems = rows.filter(r => r.itemType === 1 && r.isEnabled === 1)
|
||||||
@@ -203,6 +323,13 @@ export default {
|
|||||||
this.deductionItems = []
|
this.deductionItems = []
|
||||||
this.bonusItems = []
|
this.bonusItems = []
|
||||||
})
|
})
|
||||||
|
// 加载考核参数获取车间系数默认值
|
||||||
|
listPerfDeptConfig({ deptId: this.deptId, month: this.period + '-01', pageNum: 1, pageSize: 1 }).then(res => {
|
||||||
|
const rows = res.rows || []
|
||||||
|
this._defaultDeptCoeff = rows.length ? (rows[0].deptCoeff || 1) : 1
|
||||||
|
}).catch(() => {
|
||||||
|
this._defaultDeptCoeff = 1
|
||||||
|
})
|
||||||
},
|
},
|
||||||
/* 解析扣款/奖励 JSON 到行对象 */
|
/* 解析扣款/奖励 JSON 到行对象 */
|
||||||
parseRowItems(row) {
|
parseRowItems(row) {
|
||||||
@@ -226,8 +353,32 @@ export default {
|
|||||||
this.$set(row, '_deductionsTotal', sumD)
|
this.$set(row, '_deductionsTotal', sumD)
|
||||||
this.$set(row, '_bonusesTotal', sumB)
|
this.$set(row, '_bonusesTotal', sumB)
|
||||||
},
|
},
|
||||||
onDeductChange(row) { this.recalcTotals(row) },
|
/** 系数变化时自动重算总系数和绩效工资 */
|
||||||
onBonusChange(row) { this.recalcTotals(row) },
|
recalcCoeffs(row) {
|
||||||
|
const pos = parseFloat(row.posCoeff) || 0
|
||||||
|
const fixed = parseFloat(row.fixedCoeff) || 0
|
||||||
|
const adj = parseFloat(row.adjCoeff) || 0
|
||||||
|
const adjManual = parseFloat(row.adjCoeffManual) || 0
|
||||||
|
const perf = parseFloat(row.perfCoeff) || 0
|
||||||
|
const dept = parseFloat(row.deptCoeff) || 1
|
||||||
|
const total = (pos + fixed + adj + adjManual + perf) * dept
|
||||||
|
this.$set(row, 'totalCoeff', total.toFixed(2))
|
||||||
|
const perfBase = parseFloat(row.perfBase) || 0
|
||||||
|
this.$set(row, 'perfWage', (perfBase * total).toFixed(2))
|
||||||
|
this.recalcSalary(row)
|
||||||
|
},
|
||||||
|
/** 自动计算实领薪资 */
|
||||||
|
recalcSalary(row) {
|
||||||
|
const baseSalary = parseFloat(row.baseSalary) || 0
|
||||||
|
const perfWage = parseFloat(row.perfWage) || 0
|
||||||
|
const bonuses = parseFloat(row._bonusesTotal) || 0
|
||||||
|
const deductions = parseFloat(row._deductionsTotal) || 0
|
||||||
|
const other = parseFloat(row.otherAmount) || 0
|
||||||
|
const actual = baseSalary + perfWage + bonuses - deductions + other
|
||||||
|
this.$set(row, 'actualSalary', actual.toFixed(2))
|
||||||
|
},
|
||||||
|
onDeductChange(row) { this.recalcTotals(row); this.recalcSalary(row) },
|
||||||
|
onBonusChange(row) { this.recalcTotals(row); this.recalcSalary(row) },
|
||||||
markDirty(row) { this.$set(row, '_dirty', true) },
|
markDirty(row) { this.$set(row, '_dirty', true) },
|
||||||
|
|
||||||
loadList() {
|
loadList() {
|
||||||
@@ -244,7 +395,13 @@ export default {
|
|||||||
this.list = (res.rows || []).map(r => {
|
this.list = (res.rows || []).map(r => {
|
||||||
this.$set(r, '_dirty', false)
|
this.$set(r, '_dirty', false)
|
||||||
this.$set(r, '_saving', false)
|
this.$set(r, '_saving', false)
|
||||||
|
// 车间系数默认从考核参数获取,否则为 1
|
||||||
|
if (!r.deptCoeff || parseFloat(r.deptCoeff) === 0) {
|
||||||
|
this.$set(r, 'deptCoeff', (this._defaultDeptCoeff || 1).toString())
|
||||||
|
}
|
||||||
this.parseRowItems(r)
|
this.parseRowItems(r)
|
||||||
|
this.recalcCoeffs(r)
|
||||||
|
this.recalcSalary(r)
|
||||||
return r
|
return r
|
||||||
})
|
})
|
||||||
this.total = res.total || 0
|
this.total = res.total || 0
|
||||||
@@ -344,64 +501,206 @@ export default {
|
|||||||
this.$message.warning('缺少部门名称')
|
this.$message.warning('缺少部门名称')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$confirm(`确认根据"${this.deptName}"下的在职员工,一键生成本月薪资记录吗?`, '提示', { type: 'info' }).then(() => {
|
this.showGenerateDialog = true
|
||||||
this.generating = true
|
},
|
||||||
listEmployeeInfo({ dept: this.deptName, isLeave: 0, pageSize: 9999 }).then(empRes => {
|
onGenerateDone() {
|
||||||
const employees = empRes.rows || []
|
this.loadList()
|
||||||
if (employees.length === 0) {
|
this.$message.success('薪资记录生成完成')
|
||||||
this.$message.warning('该部门下暂无在职员工')
|
},
|
||||||
return
|
/** 批量保存所有已修改的行 */
|
||||||
|
handleBatchSave() {
|
||||||
|
const dirtyRows = this.list.filter(r => r._dirty)
|
||||||
|
if (dirtyRows.length === 0) {
|
||||||
|
this.$message.info('没有需要保存的修改')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.batchSaving = true
|
||||||
|
const tasks = dirtyRows.map(row => {
|
||||||
|
this.$set(row, '_saving', true)
|
||||||
|
const data = { ...row }
|
||||||
|
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
|
||||||
|
// 清除前端扩展字段
|
||||||
|
delete data.wmsEmployeeInfo
|
||||||
|
delete data._appLoaded; delete data._appraisal; delete data._appDetails
|
||||||
|
const api = data.id ? updatePerfSalary : addPerfSalary
|
||||||
|
return api(data).then(res => {
|
||||||
|
if (!data.id && res.data) {
|
||||||
|
this.$set(row, 'id', res.data.id || res.data)
|
||||||
}
|
}
|
||||||
listPerfSalary({ deptId: this.deptId, period: this.period, pageSize: 9999 }).then(salaryRes => {
|
this.$set(row, '_dirty', false)
|
||||||
const existing = new Set((salaryRes.rows || []).map(r => String(r.employeeId)))
|
return { ok: true }
|
||||||
const toInsert = employees.filter(e => !existing.has(String(e.employeeId) || String(e.infoId)))
|
}).catch(() => {
|
||||||
if (toInsert.length === 0) {
|
return { ok: false }
|
||||||
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(() => {
|
}).finally(() => {
|
||||||
this.generating = false
|
this.$set(row, '_saving', false)
|
||||||
})
|
})
|
||||||
}).catch(() => {})
|
})
|
||||||
}
|
Promise.all(tasks).then(results => {
|
||||||
|
const ok = results.filter(r => r.ok).length
|
||||||
|
const fail = results.filter(r => !r.ok).length
|
||||||
|
if (fail > 0) {
|
||||||
|
this.$message.warning(`保存完成:成功 ${ok} 条,失败 ${fail} 条`)
|
||||||
|
} else {
|
||||||
|
this.$message.success(`已保存 ${ok} 条记录`)
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.batchSaving = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/** 点击可点击列时切换展开,并加载绩效考核明细 */
|
||||||
|
toggleRowExpand(row) {
|
||||||
|
if (this.expandedRow === row) {
|
||||||
|
this.expandedRow = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.expandedRow = row
|
||||||
|
if (row._appLoaded) return
|
||||||
|
this.appLoading = true
|
||||||
|
const empId = row.employeeId
|
||||||
|
if (!empId) {
|
||||||
|
this.appLoading = false
|
||||||
|
this.$set(row, '_appLoaded', true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
listPerfAppraisal({ employeeId: empId, deptId: this.deptId, period: this.period, pageNum: 1, pageSize: 1 }).then(appRes => {
|
||||||
|
const appRows = appRes.rows || []
|
||||||
|
if (appRows.length === 0) {
|
||||||
|
this.$set(row, '_appraisal', null)
|
||||||
|
this.$set(row, '_appDetails', [])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$set(row, '_appraisal', appRows[0])
|
||||||
|
return listPerfAppraisalDetail({ appraisalId: appRows[0].id, pageSize: 9999 })
|
||||||
|
}).then(detailRes => {
|
||||||
|
if (detailRes) {
|
||||||
|
this.$set(row, '_appDetails', detailRes.rows || [])
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.$set(row, '_appraisal', null)
|
||||||
|
this.$set(row, '_appDetails', [])
|
||||||
|
}).finally(() => {
|
||||||
|
this.appLoading = false
|
||||||
|
this.$set(row, '_appLoaded', true)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/** 计算得分 */
|
||||||
|
async handleCalcScore() {
|
||||||
|
if (!this.expandedRow._appDetails || !this.expandedRow._appDetails.length) return
|
||||||
|
this.calcLoading = true
|
||||||
|
try {
|
||||||
|
// 拉取部门生产数据(缓存按 deptId+period)
|
||||||
|
if (!this._prodCache || this._prodCache.key !== `${this.deptId}_${this.period}`) {
|
||||||
|
this._prodCache = {
|
||||||
|
key: `${this.deptId}_${this.period}`,
|
||||||
|
data: await fetchProductionData(this.productionLine, this.period)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const prod = this._prodCache.data
|
||||||
|
|
||||||
|
// 将实际数据填入各维度
|
||||||
|
const details = this.expandedRow._appDetails
|
||||||
|
details.forEach(d => {
|
||||||
|
switch (d.dimName) {
|
||||||
|
case '生产效率':
|
||||||
|
if (prod.totalWeight > 0 && !d.actualValue) {
|
||||||
|
this.$set(d, 'actualValue', String(Math.round(prod.totalWeight * 100) / 100))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case '产品质量':
|
||||||
|
if (prod.totalCount > 0 && !d.actualValue) {
|
||||||
|
const ratio = Math.round((prod.abCount / prod.totalCount) * 10000) / 100
|
||||||
|
this.$set(d, 'actualValue', String(ratio))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case '成本控制':
|
||||||
|
if (prod.avgCostPerTon > 0 && !d.actualValue) {
|
||||||
|
this.$set(d, 'actualValue', String(Math.round(prod.avgCostPerTon * 100) / 100))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { finalScore, details: computed } = calculateTotal(details)
|
||||||
|
computed.forEach((d, i) => {
|
||||||
|
this.$set(details[i], 'dimScore', d._dimScore)
|
||||||
|
})
|
||||||
|
this.$message.success('计算完成,请检查后保存')
|
||||||
|
} finally {
|
||||||
|
this.calcLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** 保存得分到后端 */
|
||||||
|
async handleSaveScores() {
|
||||||
|
if (!this.expandedRow._appDetails || !this.expandedRow._appDetails.length) return
|
||||||
|
this.saveLoading = true
|
||||||
|
try {
|
||||||
|
const details = this.expandedRow._appDetails
|
||||||
|
// 构建批量更新数据
|
||||||
|
const batchData = details.map(d => ({
|
||||||
|
id: d.id,
|
||||||
|
actualValue: d.actualValue || '',
|
||||||
|
dimScore: parseFloat(d.dimScore) || 0,
|
||||||
|
bonus: parseFloat(d.bonus) || 0,
|
||||||
|
penalty: parseFloat(d.penalty) || 0
|
||||||
|
}))
|
||||||
|
// 批量更新维度明细
|
||||||
|
await batchUpdatePerfAppraisalDetail(batchData)
|
||||||
|
|
||||||
|
// 按照权重计算总分
|
||||||
|
let weightedSum = 0
|
||||||
|
let totalBonus = 0
|
||||||
|
let totalPenalty = 0
|
||||||
|
details.forEach(d => {
|
||||||
|
const score = parseFloat(d.dimScore) || 0
|
||||||
|
const weight = parseFloat(d.weight) || 0
|
||||||
|
weightedSum += score * weight / 100
|
||||||
|
totalBonus += parseFloat(d.bonus) || 0
|
||||||
|
totalPenalty += parseFloat(d.penalty) || 0
|
||||||
|
})
|
||||||
|
const finalScore = Math.min(120, Math.max(0, weightedSum + totalBonus - totalPenalty))
|
||||||
|
|
||||||
|
// 更新总考核记录
|
||||||
|
if (this.expandedRow._appraisal && this.expandedRow._appraisal.id) {
|
||||||
|
await updatePerfAppraisal({ id: this.expandedRow._appraisal.id, finalScore })
|
||||||
|
this.$set(this.expandedRow._appraisal, 'finalScore', finalScore)
|
||||||
|
}
|
||||||
|
// 同步绩效系数到薪资记录
|
||||||
|
const perfCoeff = finalScore / 100
|
||||||
|
if (this.expandedRow.id) {
|
||||||
|
await updatePerfSalary({ id: this.expandedRow.id, perfCoeff })
|
||||||
|
this.$set(this.expandedRow, 'perfCoeff', perfCoeff)
|
||||||
|
this.recalcCoeffs(this.expandedRow)
|
||||||
|
}
|
||||||
|
this.$message.success('保存成功')
|
||||||
|
} catch {
|
||||||
|
this.$message.error('保存失败')
|
||||||
|
} finally {
|
||||||
|
this.saveLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** 收起展开面板 */
|
||||||
|
handleCollapseExpand() {
|
||||||
|
this.expandedRow = null
|
||||||
|
},
|
||||||
|
/** 标记 detail 已修改 */
|
||||||
|
markDetailDirty() {},
|
||||||
|
subtotal(row) {
|
||||||
|
const score = parseFloat(row.dimScore) || 0
|
||||||
|
const weight = parseFloat(row.weight) || 0
|
||||||
|
return (score * weight / 100).toFixed(1)
|
||||||
|
},
|
||||||
|
rawRowTotal(row) {
|
||||||
|
const score = parseFloat(row.dimScore) || 0
|
||||||
|
const bonus = parseFloat(row.bonus) || 0
|
||||||
|
const penalty = parseFloat(row.penalty) || 0
|
||||||
|
return (score + bonus - penalty).toFixed(1)
|
||||||
|
},
|
||||||
|
handleExpandChange() {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -504,7 +803,202 @@ export default {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 口径说明 */
|
||||||
|
.formula-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: #f0f5ff;
|
||||||
|
border: 1px solid #d9ecff;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 0.15s;
|
||||||
|
&:hover {
|
||||||
|
background: #e6f0fc;
|
||||||
|
}
|
||||||
|
&.expanded {
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.formula-bar-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #409eff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.formula-bar-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #909399;
|
||||||
|
margin-left: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.formula-detail {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #fafbfc;
|
||||||
|
border: 1px solid #d9ecff;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.formula-row {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.8;
|
||||||
|
b {
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.el-table .el-button + .el-button {
|
.el-table .el-button + .el-button {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 展开面板 */
|
||||||
|
.expand-panel {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.expand-panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.expand-panel-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
.expand-panel-score {
|
||||||
|
margin-left: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.expand-detail-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.expand-empty {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 实际值输入 */
|
||||||
|
.cell-input--actual {
|
||||||
|
width: 100%;
|
||||||
|
height: 26px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
outline: none;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #303133;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
&:focus {
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加分扣分输入 */
|
||||||
|
.cell-input--bonus,
|
||||||
|
.cell-input--penalty {
|
||||||
|
width: 100%;
|
||||||
|
height: 26px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
outline: none;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
&:focus {
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cell-input--bonus {
|
||||||
|
color: #67c23a;
|
||||||
|
background: #f0f9eb;
|
||||||
|
}
|
||||||
|
.cell-input--penalty {
|
||||||
|
color: #f56c6c;
|
||||||
|
background: #fef0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 得分手动输入 */
|
||||||
|
.cell-input--score {
|
||||||
|
width: 100%;
|
||||||
|
height: 26px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
outline: none;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 600;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
&:focus {
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 总分列 */
|
||||||
|
.cell-subtotal {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e6a23c;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-rawtotal {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 得分显示 */
|
||||||
|
.cell-score {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #409eff;
|
||||||
|
&--empty {
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 可点击列样式 */
|
||||||
|
.cell-clickable {
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #409eff;
|
||||||
|
border-bottom: 1px dashed #409eff;
|
||||||
|
padding: 0 2px;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
&:hover {
|
||||||
|
color: #337ecc;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
}
|
||||||
|
&--score {
|
||||||
|
color: #e6a23c;
|
||||||
|
border-bottom-color: #e6a23c;
|
||||||
|
&:hover { color: #cf9236; }
|
||||||
|
}
|
||||||
|
&--coeff {
|
||||||
|
color: #67c23a;
|
||||||
|
border-bottom-color: #67c23a;
|
||||||
|
&:hover { color: #529b2e; }
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -18,7 +18,10 @@
|
|||||||
:class="{ active: currentDept && currentDept.id === dept.id }"
|
:class="{ active: currentDept && currentDept.id === dept.id }"
|
||||||
@click="handleDeptSelect(dept)"
|
@click="handleDeptSelect(dept)"
|
||||||
>
|
>
|
||||||
<span class="dept-name">{{ dept.deptName }}</span>
|
<div class="dept-info">
|
||||||
|
<span class="dept-name">{{ dept.deptName }}</span>
|
||||||
|
<span v-if="dept.productionLine" class="dept-pl">{{ plNames(dept.productionLine) }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!deptLoading && deptList.length === 0" class="dept-empty">暂无部门数据</div>
|
<div v-if="!deptLoading && deptList.length === 0" class="dept-empty">暂无部门数据</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,6 +32,7 @@
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
v-if="currentDept"
|
v-if="currentDept"
|
||||||
:dept-name="currentDept.deptName"
|
:dept-name="currentDept.deptName"
|
||||||
|
:production-line="plNames(currentDept.productionLine)"
|
||||||
:period="periodStr"
|
:period="periodStr"
|
||||||
:active-tab.sync="activeTab"
|
:active-tab.sync="activeTab"
|
||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
@@ -39,7 +43,7 @@
|
|||||||
|
|
||||||
<div class="panel-container" v-if="currentDept">
|
<div class="panel-container" v-if="currentDept">
|
||||||
<PerfConfigPanel v-if="activeTab === 'perfConfig'" :dept-id="currentDept.id" :period="periodStr" />
|
<PerfConfigPanel v-if="activeTab === 'perfConfig'" :dept-id="currentDept.id" :period="periodStr" />
|
||||||
<SalaryPanel v-if="activeTab === 'salary'" :dept-id="currentDept.id" :dept-name="currentDept.deptName" :period="periodStr" />
|
<SalaryPanel v-if="activeTab === 'salary'" :dept-id="currentDept.id" :dept-name="currentDept.deptName" :period="periodStr" :production-line="currentDept.productionLine" />
|
||||||
<SummaryPanel v-if="activeTab === 'summary'" :dept-id="currentDept.id" :dept-name="currentDept.deptName" :period="periodStr" />
|
<SummaryPanel v-if="activeTab === 'summary'" :dept-id="currentDept.id" :dept-name="currentDept.deptName" :period="periodStr" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,6 +58,7 @@ import PerfConfigPanel from './PerfConfigPanel'
|
|||||||
import SalaryPanel from './SalaryPanel'
|
import SalaryPanel from './SalaryPanel'
|
||||||
import SummaryPanel from './SummaryPanel'
|
import SummaryPanel from './SummaryPanel'
|
||||||
import { listPerfDept } from '@/api/perf/dept'
|
import { listPerfDept } from '@/api/perf/dept'
|
||||||
|
import { PRODUCTION_LINES } from '@/utils/meta'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PerfIndex',
|
name: 'PerfIndex',
|
||||||
@@ -81,6 +86,14 @@ export default {
|
|||||||
this.loadDeptList()
|
this.loadDeptList()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
plNames(idsStr) {
|
||||||
|
if (!idsStr) return ''
|
||||||
|
const ids = idsStr.split(',').map(s => parseInt(s.trim()))
|
||||||
|
return ids.map(id => {
|
||||||
|
const pl = PRODUCTION_LINES.find(p => p.id === id)
|
||||||
|
return pl ? pl.name : ''
|
||||||
|
}).filter(Boolean).join(', ')
|
||||||
|
},
|
||||||
loadDeptList() {
|
loadDeptList() {
|
||||||
this.deptLoading = true
|
this.deptLoading = true
|
||||||
listPerfDept({}).then(res => {
|
listPerfDept({}).then(res => {
|
||||||
@@ -163,13 +176,28 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dept-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.dept-name {
|
.dept-name {
|
||||||
|
display: block;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex: 1;
|
}
|
||||||
|
|
||||||
|
.dept-pl {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dept-empty {
|
.dept-empty {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default {
|
|||||||
showStatus: false,
|
showStatus: false,
|
||||||
hideType: false,
|
hideType: false,
|
||||||
showExportTime: true,
|
showExportTime: true,
|
||||||
correctButton: true,
|
correctButton: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user