feat: 新增绩效系统核心模块并修复菜单路径问题
本次提交完成多项核心功能开发与优化: 1. 修复菜单路径计算逻辑,目录节点路径拼接祖先路径 2. 为绩效相关实体类添加日期格式化注解,统一参数绑定格式 3. 新增10+个绩效系统API接口,覆盖部门、薪资、考核等模块 4. 新增绩效系统首页、部门管理、薪资明细等多个页面组件
This commit is contained in:
@@ -7,6 +7,8 @@ import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
/**
|
||||
* 车间考核参数配置对象 perf_dept_config
|
||||
@@ -65,6 +67,8 @@ public class PerfDeptConfig extends BaseEntity {
|
||||
/**
|
||||
* 月份
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private Date month;
|
||||
/**
|
||||
* 备注
|
||||
|
||||
@@ -7,6 +7,7 @@ import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
/**
|
||||
* 车间扣款/奖励项目配置对象 perf_dept_item_config
|
||||
@@ -45,6 +46,8 @@ public class PerfDeptItemConfig extends BaseEntity {
|
||||
/**
|
||||
* 月份
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private Date month;
|
||||
/**
|
||||
*
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.klp.perf.domain.bo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.klp.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import javax.validation.constraints.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -72,6 +75,8 @@ public class PerfDeptConfigBo extends BaseEntity {
|
||||
/**
|
||||
* 月份
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private Date month;
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ import javax.validation.constraints.*;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
/**
|
||||
* 车间扣款/奖励项目配置业务对象 perf_dept_item_config
|
||||
@@ -47,6 +48,8 @@ public class PerfDeptItemConfigBo extends BaseEntity {
|
||||
/**
|
||||
* 月份
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private Date month;
|
||||
|
||||
/**
|
||||
|
||||
44
klp-ui/src/api/perf/appraisal.js
Normal file
44
klp-ui/src/api/perf/appraisal.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询月度绩效考核记录列表
|
||||
export function listPerfAppraisal(query) {
|
||||
return request({
|
||||
url: '/perf/appraisal/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询月度绩效考核记录详细
|
||||
export function getPerfAppraisal(id) {
|
||||
return request({
|
||||
url: '/perf/appraisal/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增月度绩效考核记录
|
||||
export function addPerfAppraisal(data) {
|
||||
return request({
|
||||
url: '/perf/appraisal',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改月度绩效考核记录
|
||||
export function updatePerfAppraisal(data) {
|
||||
return request({
|
||||
url: '/perf/appraisal',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除月度绩效考核记录
|
||||
export function delPerfAppraisal(ids) {
|
||||
return request({
|
||||
url: '/perf/appraisal/' + ids,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
44
klp-ui/src/api/perf/appraisalDetail.js
Normal file
44
klp-ui/src/api/perf/appraisalDetail.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询考核维度评分明细列表
|
||||
export function listPerfAppraisalDetail(query) {
|
||||
return request({
|
||||
url: '/perf/appraisalDetail/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询考核维度评分明细详细
|
||||
export function getPerfAppraisalDetail(id) {
|
||||
return request({
|
||||
url: '/perf/appraisalDetail/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增考核维度评分明细
|
||||
export function addPerfAppraisalDetail(data) {
|
||||
return request({
|
||||
url: '/perf/appraisalDetail',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改考核维度评分明细
|
||||
export function updatePerfAppraisalDetail(data) {
|
||||
return request({
|
||||
url: '/perf/appraisalDetail',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除考核维度评分明细
|
||||
export function delPerfAppraisalDetail(ids) {
|
||||
return request({
|
||||
url: '/perf/appraisalDetail/' + ids,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
44
klp-ui/src/api/perf/dept.js
Normal file
44
klp-ui/src/api/perf/dept.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询绩效系统部门列表
|
||||
export function listPerfDept(query) {
|
||||
return request({
|
||||
url: '/perf/dept/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询绩效系统部门详细
|
||||
export function getPerfDept(id) {
|
||||
return request({
|
||||
url: '/perf/dept/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增绩效系统部门
|
||||
export function addPerfDept(data) {
|
||||
return request({
|
||||
url: '/perf/dept',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改绩效系统部门
|
||||
export function updatePerfDept(data) {
|
||||
return request({
|
||||
url: '/perf/dept',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除绩效系统部门
|
||||
export function delPerfDept(ids) {
|
||||
return request({
|
||||
url: '/perf/dept/' + ids,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
44
klp-ui/src/api/perf/deptConfig.js
Normal file
44
klp-ui/src/api/perf/deptConfig.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询车间考核参数配置列表
|
||||
export function listPerfDeptConfig(query) {
|
||||
return request({
|
||||
url: '/perf/deptConfig/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询车间考核参数配置详细
|
||||
export function getPerfDeptConfig(id) {
|
||||
return request({
|
||||
url: '/perf/deptConfig/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增车间考核参数配置
|
||||
export function addPerfDeptConfig(data) {
|
||||
return request({
|
||||
url: '/perf/deptConfig',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改车间考核参数配置
|
||||
export function updatePerfDeptConfig(data) {
|
||||
return request({
|
||||
url: '/perf/deptConfig',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除车间考核参数配置
|
||||
export function delPerfDeptConfig(ids) {
|
||||
return request({
|
||||
url: '/perf/deptConfig/' + ids,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
44
klp-ui/src/api/perf/deptItemConfig.js
Normal file
44
klp-ui/src/api/perf/deptItemConfig.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询车间扣款/奖励项目配置列表
|
||||
export function listPerfDeptItemConfig(query) {
|
||||
return request({
|
||||
url: '/perf/deptItemConfig/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询车间扣款/奖励项目配置详细
|
||||
export function getPerfDeptItemConfig(id) {
|
||||
return request({
|
||||
url: '/perf/deptItemConfig/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增车间扣款/奖励项目配置
|
||||
export function addPerfDeptItemConfig(data) {
|
||||
return request({
|
||||
url: '/perf/deptItemConfig',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改车间扣款/奖励项目配置
|
||||
export function updatePerfDeptItemConfig(data) {
|
||||
return request({
|
||||
url: '/perf/deptItemConfig',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除车间扣款/奖励项目配置
|
||||
export function delPerfDeptItemConfig(ids) {
|
||||
return request({
|
||||
url: '/perf/deptItemConfig/' + ids,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
44
klp-ui/src/api/perf/deptSummary.js
Normal file
44
klp-ui/src/api/perf/deptSummary.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询车间月度汇总列表
|
||||
export function listPerfDeptSummary(query) {
|
||||
return request({
|
||||
url: '/perf/deptSummary/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询车间月度汇总详细
|
||||
export function getPerfDeptSummary(id) {
|
||||
return request({
|
||||
url: '/perf/deptSummary/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增车间月度汇总
|
||||
export function addPerfDeptSummary(data) {
|
||||
return request({
|
||||
url: '/perf/deptSummary',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改车间月度汇总
|
||||
export function updatePerfDeptSummary(data) {
|
||||
return request({
|
||||
url: '/perf/deptSummary',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除车间月度汇总
|
||||
export function delPerfDeptSummary(ids) {
|
||||
return request({
|
||||
url: '/perf/deptSummary/' + ids,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
44
klp-ui/src/api/perf/employeeInfo.js
Normal file
44
klp-ui/src/api/perf/employeeInfo.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询员工绩效薪资基本信息列表
|
||||
export function listPerfEmployeeInfo(query) {
|
||||
return request({
|
||||
url: '/perf/employeeInfo/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询员工绩效薪资基本信息详细
|
||||
export function getPerfEmployeeInfo(id) {
|
||||
return request({
|
||||
url: '/perf/employeeInfo/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增员工绩效薪资基本信息
|
||||
export function addPerfEmployeeInfo(data) {
|
||||
return request({
|
||||
url: '/perf/employeeInfo',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改员工绩效薪资基本信息
|
||||
export function updatePerfEmployeeInfo(data) {
|
||||
return request({
|
||||
url: '/perf/employeeInfo',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除员工绩效薪资基本信息
|
||||
export function delPerfEmployeeInfo(ids) {
|
||||
return request({
|
||||
url: '/perf/employeeInfo/' + ids,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
44
klp-ui/src/api/perf/positionTemplate.js
Normal file
44
klp-ui/src/api/perf/positionTemplate.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询岗位绩效考核模板列表
|
||||
export function listPerfPositionTemplate(query) {
|
||||
return request({
|
||||
url: '/perf/positionTemplate/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询岗位绩效考核模板详细
|
||||
export function getPerfPositionTemplate(id) {
|
||||
return request({
|
||||
url: '/perf/positionTemplate/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增岗位绩效考核模板
|
||||
export function addPerfPositionTemplate(data) {
|
||||
return request({
|
||||
url: '/perf/positionTemplate',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改岗位绩效考核模板
|
||||
export function updatePerfPositionTemplate(data) {
|
||||
return request({
|
||||
url: '/perf/positionTemplate',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除岗位绩效考核模板
|
||||
export function delPerfPositionTemplate(ids) {
|
||||
return request({
|
||||
url: '/perf/positionTemplate/' + ids,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
44
klp-ui/src/api/perf/salary.js
Normal file
44
klp-ui/src/api/perf/salary.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询月度薪资计算记录列表
|
||||
export function listPerfSalary(query) {
|
||||
return request({
|
||||
url: '/perf/salary/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询月度薪资计算记录详细
|
||||
export function getPerfSalary(id) {
|
||||
return request({
|
||||
url: '/perf/salary/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增月度薪资计算记录
|
||||
export function addPerfSalary(data) {
|
||||
return request({
|
||||
url: '/perf/salary',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改月度薪资计算记录
|
||||
export function updatePerfSalary(data) {
|
||||
return request({
|
||||
url: '/perf/salary',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除月度薪资计算记录
|
||||
export function delPerfSalary(ids) {
|
||||
return request({
|
||||
url: '/perf/salary/' + ids,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
44
klp-ui/src/api/perf/templateDimension.js
Normal file
44
klp-ui/src/api/perf/templateDimension.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询考核维度明细列表
|
||||
export function listPerfTemplateDimension(query) {
|
||||
return request({
|
||||
url: '/perf/templateDimension/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询考核维度明细详细
|
||||
export function getPerfTemplateDimension(id) {
|
||||
return request({
|
||||
url: '/perf/templateDimension/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增考核维度明细
|
||||
export function addPerfTemplateDimension(data) {
|
||||
return request({
|
||||
url: '/perf/templateDimension',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改考核维度明细
|
||||
export function updatePerfTemplateDimension(data) {
|
||||
return request({
|
||||
url: '/perf/templateDimension',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除考核维度明细
|
||||
export function delPerfTemplateDimension(ids) {
|
||||
return request({
|
||||
url: '/perf/templateDimension/' + ids,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
580
klp-ui/src/views/perf/dept/index.vue
Normal file
580
klp-ui/src/views/perf/dept/index.vue
Normal file
@@ -0,0 +1,580 @@
|
||||
<template>
|
||||
<div class="perf-dept-page">
|
||||
<div class="main-layout">
|
||||
<!-- Left: Department List -->
|
||||
<el-card class="dept-list-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span class="header-title">部门列表</span>
|
||||
<el-tooltip content="点击后输入部门名称,按回车保存" placement="top" :open-delay="500">
|
||||
<el-button size="mini" type="primary" icon="el-icon-plus" :disabled="editingDeptId !== null" @click="startAddDept">新增</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="dept-tip">左侧选择部门,右侧显示该部门的员工</div>
|
||||
<div v-loading="deptLoading" class="dept-custom-list">
|
||||
<!-- 新增行 -->
|
||||
<div v-if="showAddInput" class="dept-item dept-item--input">
|
||||
<el-input
|
||||
v-model="newDeptName"
|
||||
size="small"
|
||||
placeholder="请输入部门名称,按回车保存"
|
||||
ref="addInput"
|
||||
:disabled="deptSubmitting"
|
||||
@keyup.enter.native="submitAddDept"
|
||||
@keyup.esc.native="cancelAddDept"
|
||||
@blur="submitAddDept"
|
||||
>
|
||||
<template v-if="deptSubmitting" slot="prefix"><i class="el-icon-loading" /></template>
|
||||
</el-input>
|
||||
</div>
|
||||
<div
|
||||
v-for="dept in deptList"
|
||||
:key="dept.id"
|
||||
class="dept-item"
|
||||
:class="{ active: currentDeptId === dept.id }"
|
||||
@click="handleDeptSelect(dept)"
|
||||
>
|
||||
<!-- 编辑态 -->
|
||||
<template v-if="editingDeptId === dept.id">
|
||||
<el-input
|
||||
v-model="editDeptName"
|
||||
size="small"
|
||||
ref="editInput"
|
||||
:disabled="deptSubmitting"
|
||||
@keyup.enter.native="submitEditDept(dept)"
|
||||
@keyup.esc.native="cancelEditDept"
|
||||
@blur="submitEditDept(dept)"
|
||||
@click.stop
|
||||
>
|
||||
<template v-if="deptSubmitting" slot="prefix"><i class="el-icon-loading" /></template>
|
||||
</el-input>
|
||||
</template>
|
||||
<!-- 展示态 -->
|
||||
<template v-else>
|
||||
<span class="dept-name">{{ dept.deptName }}</span>
|
||||
<span class="dept-actions">
|
||||
<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-tooltip>
|
||||
<el-tooltip content="删除此部门及其员工" placement="top" :open-delay="500">
|
||||
<el-button type="text" size="mini" icon="el-icon-delete" @click.stop="handleDelDept(dept)">删除</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="!deptLoading && deptList.length === 0 && !showAddInput" class="dept-empty">暂无部门数据</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- Right: Employee List -->
|
||||
<el-card class="employee-list-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span class="header-title">员工列表</span>
|
||||
<div class="header-actions">
|
||||
<el-input v-model="empQuery.name" size="small" placeholder="姓名" clearable style="width: 140px" @keyup.enter.native="loadEmpList" />
|
||||
<el-select v-model="empQuery.isLeave" size="small" placeholder="在职状态" clearable style="width: 110px" @change="loadEmpList">
|
||||
<el-option label="在职" :value="0" />
|
||||
<el-option label="离职" :value="1" />
|
||||
</el-select>
|
||||
<el-button size="small" type="primary" icon="el-icon-search" @click="loadEmpList">查询</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="empLoading" :data="empList" stripe :empty-text="currentDeptId ? '暂无员工数据' : '请先在左侧点击选择一个部门'">
|
||||
<el-table-column prop="name" label="姓名" min-width="100" />
|
||||
<el-table-column prop="dept" label="部门" min-width="120" />
|
||||
<el-table-column prop="jobType" label="岗位工种" min-width="120" />
|
||||
<el-table-column prop="isLeave" label="在职状态" min-width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.isLeave === 0 ? 'success' : 'info'" size="small">{{ scope.row.isLeave === 0 ? '在职' : '离职' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="baseSalary" label="底薪(元)" min-width="110" align="right" />
|
||||
<el-table-column prop="perfBase" label="绩效基数(元)" min-width="120" align="right" />
|
||||
<el-table-column prop="posCoeffDefault" label="岗位系数" min-width="100" align="center" />
|
||||
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-tooltip content="修改该员工的绩效薪资信息" placement="top" :open-delay="500">
|
||||
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEditEmp(scope.row)">编辑</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
:current-page="empQuery.pageNum"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size="empQuery.pageSize"
|
||||
:total="empTotal"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleEmpSizeChange"
|
||||
@current-change="handleEmpPageChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 底薪编辑弹窗 -->
|
||||
<el-dialog :visible.sync="empDialogVisible" title="修改薪资信息" width="480px" append-to-body @closed="resetEmpForm">
|
||||
<!-- 员工信息卡片 -->
|
||||
<div class="emp-info-card">
|
||||
<div class="emp-info-row" v-for="(item, idx) in empInfoRows" :key="idx" :class="{ 'emp-info-row--alt': idx % 2 === 1 }">
|
||||
<span class="emp-info-label">{{ item.label }}</span>
|
||||
<span class="emp-info-value" :class="{ 'emp-info-value--dim': !item.value }">{{ item.value || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 薪资编辑区 -->
|
||||
<div class="emp-salary-section">
|
||||
<div class="emp-salary-title">薪资信息</div>
|
||||
<el-form ref="empFormRef" :model="empForm" :rules="empRules" label-width="96px" size="small">
|
||||
<el-form-item label="底薪(元)" prop="baseSalary">
|
||||
<el-input v-model="empForm.baseSalary" placeholder="请输入底薪" />
|
||||
</el-form-item>
|
||||
<el-form-item label="绩效基数(元)" prop="perfBase">
|
||||
<el-input v-model="empForm.perfBase" placeholder="请输入绩效基数" />
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位系数" prop="posCoeffDefault">
|
||||
<el-input v-model="empForm.posCoeffDefault" placeholder="请输入岗位系数" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<el-button @click="empDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="empSubmitting" @click="submitEmp">保存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listPerfDept, addPerfDept, updatePerfDept, delPerfDept } from '@/api/perf/dept'
|
||||
import { listEmployeeInfo, updateEmployeeInfo } from '@/api/wms/employeeInfo'
|
||||
|
||||
export default {
|
||||
name: 'PerfDept',
|
||||
data() {
|
||||
return {
|
||||
// 部门
|
||||
deptLoading: false,
|
||||
deptList: [],
|
||||
currentDeptId: null,
|
||||
currentDeptName: '',
|
||||
showAddInput: false,
|
||||
newDeptName: '',
|
||||
deptSubmitting: false,
|
||||
editingDeptId: null,
|
||||
editDeptName: '',
|
||||
|
||||
// 员工
|
||||
empLoading: false,
|
||||
empList: [],
|
||||
empTotal: 0,
|
||||
empQuery: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
name: '',
|
||||
isLeave: null
|
||||
},
|
||||
empDialogVisible: false,
|
||||
empSubmitting: false,
|
||||
empForm: {},
|
||||
empRules: {
|
||||
baseSalary: [
|
||||
{ required: true, message: '请输入底薪', trigger: 'blur' },
|
||||
{ pattern: /^\d+(\.\d{1,2})?$/, message: '请输入有效的金额', trigger: 'blur' }
|
||||
],
|
||||
perfBase: [
|
||||
{ required: true, message: '请输入绩效基数', trigger: 'blur' },
|
||||
{ pattern: /^\d+(\.\d{1,2})?$/, message: '请输入有效的金额', trigger: 'blur' }
|
||||
],
|
||||
posCoeffDefault: [
|
||||
{ pattern: /^\d+(\.\d{1,2})?$/, message: '请输入有效的系数', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadDeptList()
|
||||
},
|
||||
computed: {
|
||||
empInfoRows() {
|
||||
return [
|
||||
{ label: '姓名', value: this.empForm.name },
|
||||
{ label: '部门', value: this.empForm.dept },
|
||||
{ label: '岗位工种', value: this.empForm.jobType },
|
||||
{ label: '在职状态', value: this.empForm.isLeave === 0 ? '在职' : this.empForm.isLeave === 1 ? '离职' : '' },
|
||||
{ label: '备注', value: this.empForm.remark }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// ========== 部门 ==========
|
||||
loadDeptList() {
|
||||
this.deptLoading = true
|
||||
listPerfDept({}).then(res => {
|
||||
this.deptList = res.rows || []
|
||||
}).finally(() => {
|
||||
this.deptLoading = false
|
||||
})
|
||||
},
|
||||
handleDeptSelect(row) {
|
||||
if (row) {
|
||||
this.currentDeptId = row.id
|
||||
this.currentDeptName = row.deptName
|
||||
this.empQuery.pageNum = 1
|
||||
this.loadEmpList()
|
||||
} else {
|
||||
this.currentDeptId = null
|
||||
this.currentDeptName = ''
|
||||
this.empList = []
|
||||
this.empTotal = 0
|
||||
}
|
||||
},
|
||||
startAddDept() {
|
||||
this.showAddInput = true
|
||||
this.newDeptName = ''
|
||||
this.$nextTick(() => { this.$refs.addInput && this.$refs.addInput.focus() })
|
||||
},
|
||||
cancelAddDept() {
|
||||
this.showAddInput = false
|
||||
this.newDeptName = ''
|
||||
},
|
||||
submitAddDept() {
|
||||
const name = this.newDeptName.trim()
|
||||
if (!name) {
|
||||
this.showAddInput = false
|
||||
return
|
||||
}
|
||||
if (this.deptSubmitting) return
|
||||
this.deptSubmitting = true
|
||||
addPerfDept({ deptName: name, orderNum: 0, hasBaoSalary: 0, status: '0' }).then(() => {
|
||||
this.$message.success('新增成功')
|
||||
this.showAddInput = false
|
||||
this.newDeptName = ''
|
||||
this.loadDeptList()
|
||||
}).finally(() => {
|
||||
this.deptSubmitting = false
|
||||
})
|
||||
},
|
||||
startEditDept(dept) {
|
||||
this.editingDeptId = dept.id
|
||||
this.editDeptName = dept.deptName
|
||||
this.$nextTick(() => {
|
||||
const input = this.$refs.editInput
|
||||
if (input && input[0]) input[0].focus()
|
||||
})
|
||||
},
|
||||
cancelEditDept() {
|
||||
this.editingDeptId = null
|
||||
this.editDeptName = ''
|
||||
},
|
||||
submitEditDept(dept) {
|
||||
const name = this.editDeptName.trim()
|
||||
if (!name || name === dept.deptName) {
|
||||
this.editingDeptId = null
|
||||
return
|
||||
}
|
||||
if (this.deptSubmitting) return
|
||||
this.deptSubmitting = true
|
||||
updatePerfDept({ id: dept.id, deptName: name }).then(() => {
|
||||
this.$message.success('修改成功')
|
||||
this.editingDeptId = null
|
||||
this.loadDeptList()
|
||||
}).finally(() => {
|
||||
this.deptSubmitting = false
|
||||
})
|
||||
},
|
||||
handleDelDept(row) {
|
||||
this.$confirm(`确认删除部门"${row.deptName}"吗?删除后该部门下的员工数据也将一并移除。`, '提示', { type: 'warning' }).then(() => {
|
||||
delPerfDept(row.id).then(() => {
|
||||
this.$message.success('删除成功')
|
||||
if (this.currentDeptId === row.id) {
|
||||
this.currentDeptId = null
|
||||
this.currentDeptName = ''
|
||||
this.empList = []
|
||||
this.empTotal = 0
|
||||
}
|
||||
this.loadDeptList()
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
|
||||
// ========== 员工 ==========
|
||||
loadEmpList() {
|
||||
if (!this.currentDeptId) return
|
||||
this.empLoading = true
|
||||
const params = {
|
||||
...this.empQuery,
|
||||
dept: this.currentDeptName
|
||||
}
|
||||
// 处理 isLeave 空字符串,不传无效值
|
||||
if (params.isLeave === '' || params.isLeave === null || params.isLeave === undefined) {
|
||||
delete params.isLeave
|
||||
}
|
||||
listEmployeeInfo(params).then(res => {
|
||||
this.empList = res.rows || []
|
||||
this.empTotal = res.total || 0
|
||||
}).finally(() => {
|
||||
this.empLoading = false
|
||||
})
|
||||
},
|
||||
handleEmpSizeChange(val) {
|
||||
this.empQuery.pageSize = val
|
||||
this.loadEmpList()
|
||||
},
|
||||
handleEmpPageChange(val) {
|
||||
this.empQuery.pageNum = val
|
||||
this.loadEmpList()
|
||||
},
|
||||
handleEditEmp(row) {
|
||||
this.empForm = { ...row }
|
||||
this.empDialogVisible = true
|
||||
this.$nextTick(() => { this.$refs.empFormRef && this.$refs.empFormRef.clearValidate() })
|
||||
},
|
||||
submitEmp() {
|
||||
this.$refs.empFormRef.validate(valid => {
|
||||
if (!valid) return
|
||||
this.empSubmitting = true
|
||||
updateEmployeeInfo({
|
||||
infoId: this.empForm.infoId,
|
||||
baseSalary: this.empForm.baseSalary,
|
||||
perfBase: this.empForm.perfBase,
|
||||
posCoeffDefault: this.empForm.posCoeffDefault
|
||||
}).then(() => {
|
||||
this.$message.success('修改成功')
|
||||
this.empDialogVisible = false
|
||||
this.loadEmpList()
|
||||
}).finally(() => {
|
||||
this.empSubmitting = false
|
||||
})
|
||||
})
|
||||
},
|
||||
resetEmpForm() {
|
||||
this.empForm = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.perf-dept-page {
|
||||
height: calc(100vh - 84px);
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.dept-list-card,
|
||||
.employee-list-card {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
::v-deep .el-card__header {
|
||||
border: none;
|
||||
padding: 10px 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
::v-deep .el-card__body {
|
||||
border: none;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.dept-list-card ::v-deep .el-card__body {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.employee-list-card {
|
||||
::v-deep .el-card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.employee-list-card ::v-deep .el-table {
|
||||
border: none;
|
||||
flex: 1;
|
||||
&::before, &::after {
|
||||
display: none;
|
||||
}
|
||||
th {
|
||||
border: none;
|
||||
}
|
||||
td {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.dept-custom-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dept-tip {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
background-color: #f4f4f5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dept-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #ecf5ff;
|
||||
|
||||
.dept-name {
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.dept-name {
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dept-actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&--input {
|
||||
padding: 4px 8px;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.dept-empty {
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
color: #c0c4cc;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.emp-field-readonly {
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
/* 员工信息卡片 */
|
||||
.emp-info-card {
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.emp-info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
min-height: 36px;
|
||||
transition: background-color 0.12s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
&--alt {
|
||||
background-color: #fafbfc;
|
||||
}
|
||||
}
|
||||
|
||||
.emp-info-label {
|
||||
flex: 0 0 80px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
text-align: right;
|
||||
letter-spacing: 1px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.emp-info-value {
|
||||
flex: 1;
|
||||
padding: 6px 12px 6px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
&--dim {
|
||||
color: #c0c4cc;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
/* 薪资编辑区 */
|
||||
.emp-salary-section {
|
||||
.emp-salary-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
padding-left: 2px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
::v-deep .el-form-item {
|
||||
margin-bottom: 14px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
226
klp-ui/src/views/perf/index/DeptConfigPanel.vue
Normal file
226
klp-ui/src/views/perf/index/DeptConfigPanel.vue
Normal file
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div class="dept-config-panel" v-loading="loading">
|
||||
<div class="panel-body">
|
||||
<div v-if="!hasConfig && !loading" class="empty-state">
|
||||
<el-empty description="暂无考核参数配置">
|
||||
<el-button type="primary" size="small" @click="handleAdd">新增配置</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
|
||||
<el-form v-else ref="formRef" :model="form" :rules="rules" label-width="130px" size="small" class="config-form">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="月产量目标" prop="monthlyProductionTarget">
|
||||
<el-input-number v-model="form.monthlyProductionTarget" :min="0" :precision="2" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="产量单位" prop="productionUnit">
|
||||
<el-select v-model="form.productionUnit" placeholder="请选择" style="width: 100%">
|
||||
<el-option label="吨" value="吨" />
|
||||
<el-option label="m" value="m" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="绩效单价(元)" prop="perfUnitPrice">
|
||||
<el-input-number v-model="form.perfUnitPrice" :min="0" :precision="2" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="生产班数" prop="productionShifts">
|
||||
<el-input-number v-model="form.productionShifts" :min="0" :step="1" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="停机保底班数" prop="guaranteedShifts">
|
||||
<el-input-number v-model="form.guaranteedShifts" :min="0" :step="1" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="车间系数" prop="deptCoeff">
|
||||
<el-input-number v-model="form.deptCoeff" :min="0" :precision="2" :step="0.1" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="薪资模式" prop="salaryMode">
|
||||
<el-radio-group v-model="form.salaryMode">
|
||||
<el-radio :label="0">普通模式</el-radio>
|
||||
<el-radio :label="1">保底模式</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8" v-if="form.salaryMode === 1">
|
||||
<el-form-item label="保底薪资除数" prop="baseDivisor">
|
||||
<el-input-number v-model="form.baseDivisor" :min="1" :step="1" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="16">
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="200" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="24" style="text-align: center; padding-top: 12px;">
|
||||
<el-button type="primary" size="small" :loading="submitting" @click="handleSave">保存</el-button>
|
||||
<el-button size="small" v-if="!isNew" @click="handleReset">重置</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listPerfDeptConfig, addPerfDeptConfig, updatePerfDeptConfig } from '@/api/perf/deptConfig'
|
||||
|
||||
export default {
|
||||
name: 'DeptConfigPanel',
|
||||
props: {
|
||||
deptId: { type: [Number, String], default: null },
|
||||
period: { type: String, default: '' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
submitting: false,
|
||||
hasConfig: false,
|
||||
isNew: false,
|
||||
configId: null,
|
||||
form: this.getEmptyForm(),
|
||||
rules: {
|
||||
monthlyProductionTarget: [{ required: true, message: '请输入月产量目标', trigger: 'blur' }],
|
||||
productionUnit: [{ required: true, message: '请选择产量单位', trigger: 'change' }],
|
||||
perfUnitPrice: [{ required: true, message: '请输入绩效单价', trigger: 'blur' }],
|
||||
productionShifts: [{ required: true, message: '请输入生产班数', trigger: 'blur' }],
|
||||
deptCoeff: [{ required: true, message: '请输入车间系数', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
deptId: { immediate: true, handler: 'loadData' },
|
||||
period: 'loadData'
|
||||
},
|
||||
methods: {
|
||||
getEmptyForm() {
|
||||
return {
|
||||
monthlyProductionTarget: null,
|
||||
productionUnit: '吨',
|
||||
perfUnitPrice: null,
|
||||
productionShifts: null,
|
||||
guaranteedShifts: 0,
|
||||
deptCoeff: 1.0,
|
||||
salaryMode: 0,
|
||||
baseDivisor: 60,
|
||||
remark: ''
|
||||
}
|
||||
},
|
||||
loadData() {
|
||||
if (!this.deptId || !this.period) return
|
||||
this.loading = true
|
||||
this.hasConfig = false
|
||||
this.isNew = false
|
||||
this.configId = null
|
||||
listPerfDeptConfig({ deptId: this.deptId, month: this.period + '-01' }).then(res => {
|
||||
const rows = res.rows || []
|
||||
if (rows.length > 0) {
|
||||
const item = rows[0]
|
||||
this.hasConfig = true
|
||||
this.isNew = false
|
||||
this.configId = item.id
|
||||
this.form = {
|
||||
monthlyProductionTarget: item.monthlyProductionTarget,
|
||||
productionUnit: item.productionUnit || '吨',
|
||||
perfUnitPrice: item.perfUnitPrice,
|
||||
productionShifts: item.productionShifts,
|
||||
guaranteedShifts: item.guaranteedShifts || 0,
|
||||
deptCoeff: item.deptCoeff ?? 1.0,
|
||||
salaryMode: item.salaryMode ?? 0,
|
||||
baseDivisor: item.baseDivisor ?? 60,
|
||||
remark: item.remark || ''
|
||||
}
|
||||
} else {
|
||||
this.hasConfig = false
|
||||
this.form = this.getEmptyForm()
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleAdd() {
|
||||
this.hasConfig = true
|
||||
this.isNew = true
|
||||
this.configId = null
|
||||
this.form = this.getEmptyForm()
|
||||
this.$nextTick(() => {
|
||||
this.$refs.formRef && this.$refs.formRef.clearValidate()
|
||||
})
|
||||
},
|
||||
handleSave() {
|
||||
this.$refs.formRef.validate(valid => {
|
||||
if (!valid) return
|
||||
this.submitting = true
|
||||
const data = {
|
||||
...this.form,
|
||||
deptId: this.deptId,
|
||||
month: this.period + '-01'
|
||||
}
|
||||
if (this.isNew) {
|
||||
addPerfDeptConfig(data).then(() => {
|
||||
this.$message.success('新增成功')
|
||||
this.loadData()
|
||||
}).finally(() => {
|
||||
this.submitting = false
|
||||
})
|
||||
} else {
|
||||
data.id = this.configId
|
||||
updatePerfDeptConfig(data).then(() => {
|
||||
this.$message.success('保存成功')
|
||||
this.loadData()
|
||||
}).finally(() => {
|
||||
this.submitting = false
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
handleReset() {
|
||||
this.loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dept-config-panel {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 24px 24px 40px;
|
||||
}
|
||||
|
||||
.config-form {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
}
|
||||
</style>
|
||||
142
klp-ui/src/views/perf/index/PageHeader.vue
Normal file
142
klp-ui/src/views/perf/index/PageHeader.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<span class="breadcrumb">{{ deptName }} · {{ period }}</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="custom-tabs">
|
||||
<div
|
||||
v-for="tab in tabOptions"
|
||||
:key="tab.key"
|
||||
class="custom-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="$emit('update:activeTab', tab.key)"
|
||||
>{{ tab.label }}</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button class="refresh-btn" size="mini" icon="el-icon-refresh" circle @click="$emit('refresh')" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PageHeader',
|
||||
props: {
|
||||
deptName: { type: String, default: '' },
|
||||
period: { type: String, default: '' },
|
||||
activeTab: { type: String, default: '' },
|
||||
tabOptions: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{ key: 'perfConfig', label: '考核配置' },
|
||||
{ key: 'salary', label: '薪资明细' },
|
||||
{ key: 'summary', label: '统计' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
background-color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
background-color: #409eff;
|
||||
border-radius: 2px;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.custom-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.custom-tab {
|
||||
position: relative;
|
||||
padding: 0 16px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
height: 2px;
|
||||
background-color: #409eff;
|
||||
border-radius: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
background-color: #f0f5ff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
669
klp-ui/src/views/perf/index/PerfConfigPanel.vue
Normal file
669
klp-ui/src/views/perf/index/PerfConfigPanel.vue
Normal file
@@ -0,0 +1,669 @@
|
||||
<template>
|
||||
<div class="perf-config-panel">
|
||||
<!-- Section 0: 考核参数 -->
|
||||
<div class="config-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">考核参数</span>
|
||||
<el-button size="small" type="primary" :loading="cfgSubmitting" @click="handleSaveCfg">{{ cfgId ? '保存' : '新增' }}</el-button>
|
||||
</div>
|
||||
<div class="cfg-form" v-loading="cfgLoading">
|
||||
<el-form v-if="cfgReady" ref="cfgFormRef" :model="cfgForm" :rules="cfgRules" label-width="110px" size="small" inline>
|
||||
<el-form-item label="月产量目标" prop="monthlyProductionTarget">
|
||||
<el-input v-model="cfgForm.monthlyProductionTarget" placeholder="月产量目标" style="width: 120px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="产量单位" prop="productionUnit">
|
||||
<el-select v-model="cfgForm.productionUnit" style="width: 80px">
|
||||
<el-option label="吨" value="吨" />
|
||||
<el-option label="m" value="m" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="绩效单价(元)" prop="perfUnitPrice">
|
||||
<el-input v-model="cfgForm.perfUnitPrice" placeholder="绩效单价" style="width: 110px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="生产班数" prop="productionShifts">
|
||||
<el-input v-model="cfgForm.productionShifts" placeholder="班数" style="width: 80px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="保底班数" prop="guaranteedShifts">
|
||||
<el-input v-model="cfgForm.guaranteedShifts" placeholder="保底班数" style="width: 80px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="车间系数" prop="deptCoeff">
|
||||
<el-input v-model="cfgForm.deptCoeff" placeholder="系数" style="width: 80px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="薪资模式" prop="salaryMode">
|
||||
<el-radio-group v-model="cfgForm.salaryMode" size="small">
|
||||
<el-radio :label="0">普通</el-radio>
|
||||
<el-radio :label="1">保底</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="保底薪资除数" prop="baseDivisor" v-if="cfgForm.salaryMode === 1">
|
||||
<el-input v-model="cfgForm.baseDivisor" placeholder="除数" style="width: 80px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="cfgForm.remark" placeholder="备注" style="width: 200px" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 1: 扣款/奖励项目配置 -->
|
||||
<div class="config-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">扣款/奖励项目配置</span>
|
||||
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAddItem">新增项目</el-button>
|
||||
</div>
|
||||
<el-table :data="itemList" stripe size="small" v-loading="itemLoading" empty-text="暂无项目配置">
|
||||
<el-table-column label="类型" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.itemType === 2 ? 'success' : 'danger'" size="small">
|
||||
{{ scope.row.itemType === 2 ? '奖励' : '扣款' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="itemName" label="项目名称" min-width="160" />
|
||||
<el-table-column label="月份" width="100" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.month ? (scope.row.month).substring(0, 7) : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sortOrder" label="排序" width="60" align="center" />
|
||||
<el-table-column label="启用" width="60" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-switch v-model="scope.row.isEnabled" :active-value="1" :inactive-value="0" size="small" @change="handleToggleItemEnabled(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="140" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEditItem(scope.row)">编辑</el-button>
|
||||
<el-button type="text" size="mini" icon="el-icon-delete" style="color: #f56c6c" @click="handleDelItem(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- Section 2: 岗位绩效考核模板 -->
|
||||
<div class="config-section" v-loading="ctxLoading">
|
||||
<div class="section-header">
|
||||
<span class="section-title">岗位绩效考核模板 <span class="section-hint">右键 tab 可编辑或删除</span></span>
|
||||
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAddTemplate">新增岗位</el-button>
|
||||
</div>
|
||||
<div v-if="templateList.length" class="template-tabs">
|
||||
<span
|
||||
v-for="tpl in templateList"
|
||||
:key="tpl.id"
|
||||
class="template-tab"
|
||||
:class="{ 'template-tab--active': activeTemplateId === String(tpl.id) }"
|
||||
@click="activeTemplateId = String(tpl.id); loadDims()"
|
||||
@contextmenu.prevent.stop="onTabContextMenu($event, tpl)"
|
||||
>{{ tpl.positionName }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 维度表格 -->
|
||||
<div v-if="activeTpl" class="dim-section">
|
||||
<div class="dim-toolbar">
|
||||
<span class="dim-subtitle">{{ activeTpl.positionName }} — 考核维度</span>
|
||||
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAddDim">新增维度</el-button>
|
||||
</div>
|
||||
<el-table :data="dimList" stripe size="small" 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="weight" label="权重" width="70" align="center" />
|
||||
<el-table-column prop="indicator" label="量化指标" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="targetValue" label="目标值" min-width="100" />
|
||||
<el-table-column prop="scoreRule" label="评分规则" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="启用" width="60" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-switch v-model="scope.row.isEnabled" :active-value="1" :inactive-value="0" size="small" @change="handleToggleDimEnabled(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEditDim(scope.row)">编辑</el-button>
|
||||
<el-button type="text" size="mini" icon="el-icon-delete" style="color: #f56c6c" @click="handleDelDim(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-else class="dim-empty">点击 tab 查看维度,右键 tab 可编辑或删除模板</div>
|
||||
</div>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<div
|
||||
v-show="ctxMenu.visible"
|
||||
class="ctx-menu"
|
||||
:style="{ left: ctxMenu.x + 'px', top: ctxMenu.y + 'px' }"
|
||||
>
|
||||
<div class="ctx-menu-item" @click="ctxEditTemplate">编辑模板</div>
|
||||
<div class="ctx-menu-item ctx-menu-item--danger" @click="ctxDelTemplate">删除模板</div>
|
||||
</div>
|
||||
|
||||
<!-- Item Dialog -->
|
||||
<el-dialog :visible.sync="itemDialogVisible" :title="itemDialogTitle" width="480px" append-to-body @closed="resetItemForm">
|
||||
<el-form ref="itemFormRef" :model="itemForm" :rules="itemRules" label-width="90px" size="small">
|
||||
<el-form-item label="类型" prop="itemType">
|
||||
<el-radio-group v-model="itemForm.itemType">
|
||||
<el-radio :label="1">扣款项目</el-radio>
|
||||
<el-radio :label="2">奖励项目</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="月份" prop="month">
|
||||
<el-date-picker v-model="itemForm.month" type="month" placeholder="选择月份" value-format="yyyy-MM-dd" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目名称" prop="itemName">
|
||||
<el-input v-model="itemForm.itemName" placeholder="请输入项目名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input v-model="itemForm.sortOrder" placeholder="排序号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否启用">
|
||||
<el-switch v-model="itemForm.isEnabled" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="itemForm.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="itemDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="itemSubmitting" @click="submitItem">保存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Template Dialog -->
|
||||
<el-dialog :visible.sync="templateDialogVisible" :title="templateDialogTitle" width="480px" append-to-body @closed="resetTemplateForm">
|
||||
<el-form ref="templateFormRef" :model="templateForm" :rules="templateRules" label-width="90px" size="small">
|
||||
<el-form-item label="岗位名称" prop="positionName">
|
||||
<el-input v-model="templateForm.positionName" placeholder="与员工表岗位字段匹配,同时作为模板标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否启用">
|
||||
<el-switch v-model="templateForm.isEnabled" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="templateForm.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="templateDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="templateSubmitting" @click="submitTemplate">保存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Dimension Form Dialog -->
|
||||
<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-row :gutter="12">
|
||||
<el-col :span="8">
|
||||
<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-select v-model="dimForm.dimName" placeholder="请选择考核维度" style="width: 100%">
|
||||
<el-option label="生产效率" value="生产效率" />
|
||||
<el-option label="产品质量" value="产品质量" />
|
||||
<el-option label="成本控制" value="成本控制" />
|
||||
<el-option label="安全管理" value="安全管理" />
|
||||
<el-option label="设备维护" value="设备维护" />
|
||||
<el-option label="团队管理" value="团队管理" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="权重" prop="weight">
|
||||
<el-input v-model="dimForm.weight" placeholder="如 40%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="量化指标" prop="indicator">
|
||||
<el-input v-model="dimForm.indicator" placeholder="如:月产量达标率" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="目标值" prop="targetValue">
|
||||
<el-input v-model="dimForm.targetValue" placeholder="如:≥98%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="评分公式" prop="scoreFormula">
|
||||
<el-input v-model="dimForm.scoreFormula" placeholder="如: =IF(E4>0,25*(F4/E4),0)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="评分规则" prop="scoreRule">
|
||||
<el-input v-model="dimForm.scoreRule" type="textarea" :rows="2" placeholder="评分规则文字描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="dimForm.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="dimFormVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="dimSubmitting" @click="submitDim">保存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listPerfDeptItemConfig, addPerfDeptItemConfig, updatePerfDeptItemConfig, delPerfDeptItemConfig } from '@/api/perf/deptItemConfig'
|
||||
import { listPerfPositionTemplate, addPerfPositionTemplate, updatePerfPositionTemplate, delPerfPositionTemplate } from '@/api/perf/positionTemplate'
|
||||
import { listPerfTemplateDimension, addPerfTemplateDimension, updatePerfTemplateDimension, delPerfTemplateDimension } from '@/api/perf/templateDimension'
|
||||
import { listPerfDeptConfig, addPerfDeptConfig, updatePerfDeptConfig } from '@/api/perf/deptConfig'
|
||||
|
||||
export default {
|
||||
name: 'PerfConfigPanel',
|
||||
props: {
|
||||
deptId: { type: [Number, String], default: null },
|
||||
period: { type: String, default: '' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageLoading: false,
|
||||
|
||||
// Dept Config (考核参数)
|
||||
cfgLoading: false,
|
||||
cfgSubmitting: false,
|
||||
cfgReady: false,
|
||||
cfgId: null,
|
||||
cfgForm: { monthlyProductionTarget: '', productionUnit: '吨', perfUnitPrice: '', productionShifts: '', guaranteedShifts: '', deptCoeff: '', salaryMode: 0, baseDivisor: '', remark: '' },
|
||||
cfgRules: {
|
||||
monthlyProductionTarget: [{ required: true, message: '请输入月产量目标', trigger: 'blur' }],
|
||||
productionUnit: [{ required: true, message: '请选择产量单位', trigger: 'change' }],
|
||||
perfUnitPrice: [{ required: true, message: '请输入绩效单价', trigger: 'blur' }],
|
||||
productionShifts: [{ required: true, message: '请输入生产班数', trigger: 'blur' }],
|
||||
deptCoeff: [{ required: true, message: '请输入车间系数', trigger: 'blur' }]
|
||||
},
|
||||
|
||||
// Item Config
|
||||
itemLoading: false,
|
||||
itemList: [],
|
||||
itemDialogVisible: false,
|
||||
itemDialogTitle: '新增项目',
|
||||
itemSubmitting: false,
|
||||
itemForm: {},
|
||||
itemRules: {
|
||||
itemType: [{ required: true, message: '请选择类型', trigger: 'change' }],
|
||||
itemName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }]
|
||||
},
|
||||
|
||||
// Template
|
||||
templateLoading: false,
|
||||
templateList: [],
|
||||
activeTemplateId: '',
|
||||
templateDialogVisible: false,
|
||||
templateDialogTitle: '新增模板',
|
||||
templateSubmitting: false,
|
||||
templateForm: {},
|
||||
templateRules: {
|
||||
positionName: [{ required: true, message: '请输入岗位名称', trigger: 'blur' }]
|
||||
},
|
||||
|
||||
// Context menu
|
||||
ctxMenu: { visible: false, x: 0, y: 0, tpl: null },
|
||||
ctxLoading: false,
|
||||
|
||||
// Dimension
|
||||
dimLoading: false,
|
||||
dimList: [],
|
||||
dimFormVisible: false,
|
||||
dimFormTitle: '新增维度',
|
||||
dimSubmitting: false,
|
||||
dimForm: {},
|
||||
dimRules: {
|
||||
dimName: [{ required: true, message: '请选择考核维度', trigger: 'change' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
activeTpl() {
|
||||
return this.templateList.find(t => String(t.id) === this.activeTemplateId) || null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
deptId: { immediate: true, handler: 'loadAll' },
|
||||
period: 'loadAll'
|
||||
},
|
||||
methods: {
|
||||
loadAll() {
|
||||
if (!this.deptId) return
|
||||
this.loadDeptConfig()
|
||||
this.loadItems()
|
||||
this.loadTemplates()
|
||||
},
|
||||
// ======== Dept Config ========
|
||||
loadDeptConfig() {
|
||||
this.cfgLoading = true
|
||||
this.cfgReady = false
|
||||
listPerfDeptConfig({ deptId: this.deptId, month: this.period + '-01' }).then(res => {
|
||||
const rows = res.rows || []
|
||||
if (rows.length > 0) {
|
||||
const item = rows[0]
|
||||
this.cfgId = item.id
|
||||
this.cfgForm = {
|
||||
monthlyProductionTarget: item.monthlyProductionTarget ?? '',
|
||||
productionUnit: item.productionUnit || '吨',
|
||||
perfUnitPrice: item.perfUnitPrice ?? '',
|
||||
productionShifts: item.productionShifts ?? '',
|
||||
guaranteedShifts: item.guaranteedShifts ?? '',
|
||||
deptCoeff: item.deptCoeff ?? '',
|
||||
salaryMode: item.salaryMode ?? 0,
|
||||
baseDivisor: item.baseDivisor ?? '',
|
||||
remark: item.remark || ''
|
||||
}
|
||||
} else {
|
||||
this.cfgId = null
|
||||
this.cfgForm = { monthlyProductionTarget: '', productionUnit: '吨', perfUnitPrice: '', productionShifts: '', guaranteedShifts: '', deptCoeff: '', salaryMode: 0, baseDivisor: '', remark: '' }
|
||||
}
|
||||
this.cfgReady = true
|
||||
}).finally(() => { this.cfgLoading = false })
|
||||
},
|
||||
handleSaveCfg() {
|
||||
this.$refs.cfgFormRef.validate(valid => {
|
||||
if (!valid) return
|
||||
this.cfgSubmitting = true
|
||||
const data = { ...this.cfgForm, deptId: this.deptId, month: this.period + '-01' }
|
||||
const api = this.cfgId ? updatePerfDeptConfig : addPerfDeptConfig
|
||||
if (this.cfgId) data.id = this.cfgId
|
||||
api(data).then(() => {
|
||||
this.$message.success('保存成功')
|
||||
this.loadDeptConfig()
|
||||
}).finally(() => { this.cfgSubmitting = false })
|
||||
})
|
||||
},
|
||||
// ======== Items ========
|
||||
loadItems() {
|
||||
this.itemLoading = true
|
||||
listPerfDeptItemConfig({ deptId: this.deptId, month: this.period + '-01' }).then(res => {
|
||||
this.itemList = res.rows || []
|
||||
}).finally(() => { this.itemLoading = false })
|
||||
},
|
||||
handleAddItem() {
|
||||
this.itemDialogTitle = '新增项目'
|
||||
this.itemForm = { itemType: 1, itemName: '', sortOrder: 0, isEnabled: 1, remark: '', month: this.period + '-01' }
|
||||
this.itemDialogVisible = true
|
||||
this.$nextTick(() => { this.$refs.itemFormRef?.clearValidate() })
|
||||
},
|
||||
handleEditItem(row) {
|
||||
this.itemDialogTitle = '编辑项目'
|
||||
this.itemForm = { ...row }
|
||||
this.itemDialogVisible = true
|
||||
this.$nextTick(() => { this.$refs.itemFormRef?.clearValidate() })
|
||||
},
|
||||
handleDelItem(row) {
|
||||
this.$confirm(`确认删除项目"${row.itemName}"吗?`, '提示', { type: 'warning' }).then(() => {
|
||||
delPerfDeptItemConfig(row.id).then(() => {
|
||||
this.$message.success('删除成功')
|
||||
this.loadItems()
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleToggleItemEnabled(row) {
|
||||
updatePerfDeptItemConfig({ id: row.id, isEnabled: row.isEnabled })
|
||||
},
|
||||
submitItem() {
|
||||
this.$refs.itemFormRef.validate(valid => {
|
||||
if (!valid) return
|
||||
this.itemSubmitting = true
|
||||
const data = { ...this.itemForm, deptId: this.deptId }
|
||||
const api = data.id ? updatePerfDeptItemConfig : addPerfDeptItemConfig
|
||||
api(data).then(() => {
|
||||
this.$message.success(data.id ? '修改成功' : '新增成功')
|
||||
this.itemDialogVisible = false
|
||||
this.loadItems()
|
||||
}).finally(() => { this.itemSubmitting = false })
|
||||
})
|
||||
},
|
||||
resetItemForm() { this.itemForm = {} },
|
||||
|
||||
// ======== Templates ========
|
||||
loadTemplates() {
|
||||
this.templateLoading = true
|
||||
listPerfPositionTemplate({ deptId: this.deptId }).then(res => {
|
||||
this.templateList = res.rows || []
|
||||
if (this.templateList.length && !this.templateList.find(t => String(t.id) === this.activeTemplateId)) {
|
||||
this.activeTemplateId = String(this.templateList[0].id)
|
||||
this.loadDims()
|
||||
}
|
||||
}).finally(() => { this.templateLoading = false })
|
||||
},
|
||||
handleAddTemplate() {
|
||||
this.templateDialogTitle = '新增模板'
|
||||
this.templateForm = { positionName: '', isEnabled: 1, remark: '' }
|
||||
this.templateDialogVisible = true
|
||||
this.$nextTick(() => { this.$refs.templateFormRef?.clearValidate() })
|
||||
},
|
||||
|
||||
// 右键菜单
|
||||
onTabContextMenu(event, tpl) {
|
||||
this.$set(this.ctxMenu, 'tpl', tpl)
|
||||
this.$set(this.ctxMenu, 'x', event.pageX)
|
||||
this.$set(this.ctxMenu, 'y', event.pageY)
|
||||
this.$set(this.ctxMenu, 'visible', true)
|
||||
this.$nextTick(() => {
|
||||
const close = () => this.closeCtxMenu()
|
||||
document.addEventListener('click', close, { once: true })
|
||||
document.addEventListener('contextmenu', close, { once: true })
|
||||
})
|
||||
},
|
||||
closeCtxMenu() {
|
||||
this.ctxMenu.visible = false
|
||||
},
|
||||
ctxEditTemplate() {
|
||||
if (this.ctxLoading) return
|
||||
const tpl = this.ctxMenu.tpl
|
||||
this.ctxMenu.visible = false
|
||||
this.templateDialogTitle = '编辑模板'
|
||||
this.templateForm = { ...tpl }
|
||||
this.templateDialogVisible = true
|
||||
this.$nextTick(() => { this.$refs.templateFormRef?.clearValidate() })
|
||||
},
|
||||
ctxDelTemplate() {
|
||||
if (this.ctxLoading) return
|
||||
const tpl = this.ctxMenu.tpl
|
||||
this.ctxMenu.visible = false
|
||||
this.$confirm(`确认删除模板"${tpl.positionName}"吗?关联的维度也将一并删除。`, '提示', { type: 'warning' }).then(() => {
|
||||
this.ctxLoading = true
|
||||
delPerfPositionTemplate(tpl.id).then(() => {
|
||||
this.$message.success('删除成功')
|
||||
if (String(tpl.id) === this.activeTemplateId) this.activeTemplateId = ''
|
||||
this.loadTemplates()
|
||||
}).finally(() => { this.ctxLoading = false })
|
||||
}).catch(() => {})
|
||||
},
|
||||
submitTemplate() {
|
||||
this.$refs.templateFormRef.validate(valid => {
|
||||
if (!valid) return
|
||||
this.templateSubmitting = true
|
||||
const data = { ...this.templateForm, deptId: this.deptId, title: this.templateForm.positionName }
|
||||
const api = data.id ? updatePerfPositionTemplate : addPerfPositionTemplate
|
||||
api(data).then(() => {
|
||||
this.$message.success(data.id ? '修改成功' : '新增成功')
|
||||
this.templateDialogVisible = false
|
||||
this.loadTemplates()
|
||||
}).finally(() => { this.templateSubmitting = false })
|
||||
})
|
||||
},
|
||||
resetTemplateForm() { this.templateForm = {} },
|
||||
|
||||
// ======== Dimensions ========
|
||||
loadDims() {
|
||||
if (!this.activeTpl) { this.dimList = []; return }
|
||||
this.dimLoading = true
|
||||
listPerfTemplateDimension({ templateId: this.activeTpl.id }).then(res => {
|
||||
this.dimList = res.rows || []
|
||||
}).finally(() => { this.dimLoading = false })
|
||||
},
|
||||
handleAddDim() {
|
||||
this.dimFormTitle = '新增维度'
|
||||
this.dimForm = { seq: '', dimName: '', weight: '', indicator: '', targetValue: '', scoreFormula: '', scoreRule: '', isEnabled: 1, remark: '' }
|
||||
this.dimFormVisible = true
|
||||
this.$nextTick(() => { this.$refs.dimFormRef?.clearValidate() })
|
||||
},
|
||||
handleEditDim(row) {
|
||||
this.dimFormTitle = '编辑维度'
|
||||
this.dimForm = { ...row }
|
||||
this.dimFormVisible = true
|
||||
this.$nextTick(() => { this.$refs.dimFormRef?.clearValidate() })
|
||||
},
|
||||
handleDelDim(row) {
|
||||
this.$confirm(`确认删除维度"${row.dimName}"吗?`, '提示', { type: 'warning' }).then(() => {
|
||||
delPerfTemplateDimension(row.id).then(() => {
|
||||
this.$message.success('删除成功')
|
||||
this.loadDims()
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleToggleDimEnabled(row) {
|
||||
updatePerfTemplateDimension({ id: row.id, isEnabled: row.isEnabled })
|
||||
},
|
||||
submitDim() {
|
||||
this.$refs.dimFormRef.validate(valid => {
|
||||
if (!valid) return
|
||||
this.dimSubmitting = true
|
||||
const data = { ...this.dimForm, templateId: this.activeTpl.id }
|
||||
const api = data.id ? updatePerfTemplateDimension : addPerfTemplateDimension
|
||||
api(data).then(() => {
|
||||
this.$message.success(data.id ? '修改成功' : '新增成功')
|
||||
this.dimFormVisible = false
|
||||
this.loadDims()
|
||||
}).finally(() => { this.dimSubmitting = false })
|
||||
})
|
||||
},
|
||||
resetDimForm() { this.dimForm = {} }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.perf-config-panel {
|
||||
padding: 8px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
background-color: #fff;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.section-hint {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.cfg-form {
|
||||
padding: 12px;
|
||||
|
||||
::v-deep .el-form-item {
|
||||
margin-bottom: 8px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .el-table {
|
||||
border: none;
|
||||
&::before, &::after { display: none; }
|
||||
}
|
||||
|
||||
/* 自定义模板 tabs */
|
||||
.template-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.template-tab {
|
||||
display: inline-block;
|
||||
padding: 4px 14px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
background: #f4f4f5;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
background: #ecf5ff;
|
||||
}
|
||||
|
||||
&--active {
|
||||
color: #fff;
|
||||
background: #409eff;
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: #66b1ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 维度区域 */
|
||||
.dim-section {
|
||||
padding: 8px 0 0;
|
||||
}
|
||||
|
||||
.dim-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 12px 8px;
|
||||
}
|
||||
|
||||
.dim-subtitle {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.dim-empty {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: #c0c4cc;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 右键菜单 */
|
||||
.ctx-menu {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,.12);
|
||||
padding: 4px 0;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.ctx-menu-item {
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
|
||||
&:hover { background-color: #f5f7fa; }
|
||||
|
||||
&--danger {
|
||||
color: #f56c6c;
|
||||
&:hover { background-color: #fef0f0; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
509
klp-ui/src/views/perf/index/SalaryPanel.vue
Normal file
509
klp-ui/src/views/perf/index/SalaryPanel.vue
Normal file
@@ -0,0 +1,509 @@
|
||||
<template>
|
||||
<div class="salary-panel">
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-input v-model="query.employeeId" size="small" placeholder="员工ID" clearable style="width: 140px" @keyup.enter.native="loadList" />
|
||||
<el-select v-model="query.status" size="small" placeholder="状态" clearable style="width: 110px" @change="loadList">
|
||||
<el-option label="草稿" :value="'0'" />
|
||||
<el-option label="已确认" :value="'1'" />
|
||||
<el-option label="已归档" :value="'2'" />
|
||||
</el-select>
|
||||
<el-button size="small" type="primary" icon="el-icon-search" @click="loadList">查询</el-button>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAdd">新增</el-button>
|
||||
<el-button size="small" type="success" icon="el-icon-s-operation" :loading="generating" @click="handleBatchGenerate">一键生成</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<div class="table-wrap">
|
||||
<el-table :data="list" stripe size="small" v-loading="loading" empty-text="暂无薪资记录" class="inline-edit-table" border>
|
||||
<!-- 只读列 -->
|
||||
<!-- <el-table-column prop="employeeId" label="员工ID" width="90" /> -->
|
||||
<el-table-column prop="status" label="状态" width="70" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="statusType(scope.row.status)" size="small">{{ statusLabel(scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 薪资基数 -->
|
||||
<el-table-column label="底薪" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.baseSalary" class="cell-input cell-input--base" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="绩效基数" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.perfBase" class="cell-input cell-input--base" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
|
||||
<!-- 考核得分 -->
|
||||
<el-table-column label="考核得分" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.perfScore" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
|
||||
<!-- 系数 -->
|
||||
<el-table-column label="绩效系数" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.perfCoeff" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="车间系数" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.deptCoeff" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="岗位系数" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.posCoeff" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="固定系数" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.fixedCoeff" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="调整系数" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.adjCoeff" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="手动调整系数" width="100" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.adjCoeffManual" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="总系数" width="80" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.totalCoeff" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
|
||||
<!-- 绩效工资 -->
|
||||
<el-table-column label="绩效工资" width="100" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.perfWage" class="cell-input cell-input--amount" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
|
||||
<!-- 动态扣款列 -->
|
||||
<el-table-column
|
||||
v-for="item in deductionItems" :key="'ded-'+item.itemName"
|
||||
:label="item.itemName" width="100" align="right"
|
||||
><template slot-scope="s">
|
||||
<input v-model="s.row._d[item.itemName]" class="cell-input cell-input--deduct" @change="onDeductChange(s.row); markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="扣款合计" width="90" align="right">
|
||||
<template slot-scope="scope"><span class="cell-readonly cell-readonly--deduct">{{ scope.row._deductionsTotal }}</span></template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 动态奖励列 -->
|
||||
<el-table-column
|
||||
v-for="item in bonusItems" :key="'bon-'+item.itemName"
|
||||
:label="item.itemName" width="100" align="right"
|
||||
><template slot-scope="s">
|
||||
<input v-model="s.row._b[item.itemName]" class="cell-input cell-input--bonus" @change="onBonusChange(s.row); markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="奖励合计" width="90" align="right">
|
||||
<template slot-scope="scope"><span class="cell-readonly cell-readonly--bonus">{{ scope.row._bonusesTotal }}</span></template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 金额 -->
|
||||
<el-table-column label="其他金额" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.otherAmount" class="cell-input cell-input--amount" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="实领薪资" width="100" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.actualSalary" class="cell-input cell-input--amount" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
|
||||
<!-- 参考值 -->
|
||||
<el-table-column label="固薪参考" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.fixedSalaryRef" class="cell-input cell-input--ref" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="调薪参考" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.adjSalaryRef" class="cell-input cell-input--ref" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="包薪每班参考" width="100" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.baoPerShiftRef" class="cell-input cell-input--ref" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="包薪参考" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.baoSalaryRef" class="cell-input cell-input--ref" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
<el-table-column label="总薪参考" width="90" align="right"><template slot-scope="s">
|
||||
<input v-model="s.row.totalSalaryRef" class="cell-input cell-input--ref" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
|
||||
<!-- 备注 -->
|
||||
<el-table-column label="备注" min-width="140"><template slot-scope="s">
|
||||
<input v-model="s.row.remark" class="cell-input cell-input--remark" @change="markDirty(s.row)" />
|
||||
</template></el-table-column>
|
||||
|
||||
<!-- 操作 -->
|
||||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button v-if="scope.row._dirty" type="text" size="mini" icon="el-icon-check" :loading="scope.row._saving" class="btn-save" @click="saveRow(scope.row)">保存</el-button>
|
||||
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDel(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
:current-page="query.pageNum"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size="query.pageSize"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listPerfSalary, addPerfSalary, updatePerfSalary, delPerfSalary } from '@/api/perf/salary'
|
||||
import { listEmployeeInfo } from '@/api/wms/employeeInfo'
|
||||
import { listPerfDeptItemConfig } from '@/api/perf/deptItemConfig'
|
||||
|
||||
export default {
|
||||
name: 'SalaryPanel',
|
||||
props: {
|
||||
deptId: { type: [Number, String], default: null },
|
||||
deptName: { type: String, default: '' },
|
||||
period: { type: String, default: '' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
list: [],
|
||||
total: 0,
|
||||
query: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
employeeId: '',
|
||||
status: ''
|
||||
},
|
||||
deductionItems: [],
|
||||
bonusItems: [],
|
||||
generating: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
deptId: { immediate: true, handler: 'onDeptChange' },
|
||||
period: 'onDeptChange'
|
||||
},
|
||||
methods: {
|
||||
fmt(val) {
|
||||
return val != null ? val : '-'
|
||||
},
|
||||
statusType(status) {
|
||||
return { '0': 'info', '1': 'warning', '2': 'success' }[status] || 'info'
|
||||
},
|
||||
statusLabel(status) {
|
||||
return { '0': '草稿', '1': '已确认', '2': '已归档' }[status] || '草稿'
|
||||
},
|
||||
onDeptChange() {
|
||||
this.loadItemConfig()
|
||||
this.loadList()
|
||||
},
|
||||
loadItemConfig() {
|
||||
if (!this.deptId || !this.period) return
|
||||
listPerfDeptItemConfig({ deptId: this.deptId, month: this.period + '-01' }).then(res => {
|
||||
const rows = res.rows || []
|
||||
this.deductionItems = rows.filter(r => r.itemType === 1 && r.isEnabled === 1)
|
||||
this.bonusItems = rows.filter(r => r.itemType === 2 && r.isEnabled === 1)
|
||||
}).catch(() => {
|
||||
this.deductionItems = []
|
||||
this.bonusItems = []
|
||||
})
|
||||
},
|
||||
/* 解析扣款/奖励 JSON 到行对象 */
|
||||
parseRowItems(row) {
|
||||
if (!row._d) {
|
||||
const d = {}
|
||||
try { Object.assign(d, JSON.parse(row.deductionsJson || '{}')) } catch {}
|
||||
this.deductionItems.forEach(item => { if (!(item.itemName in d)) d[item.itemName] = '' })
|
||||
this.$set(row, '_d', d)
|
||||
}
|
||||
if (!row._b) {
|
||||
const b = {}
|
||||
try { Object.assign(b, JSON.parse(row.bonusesJson || '{}')) } catch {}
|
||||
this.bonusItems.forEach(item => { if (!(item.itemName in b)) b[item.itemName] = '' })
|
||||
this.$set(row, '_b', b)
|
||||
}
|
||||
this.recalcTotals(row)
|
||||
},
|
||||
recalcTotals(row) {
|
||||
const sumD = Object.values(row._d || {}).reduce((s, v) => s + (parseFloat(v) || 0), 0)
|
||||
const sumB = Object.values(row._b || {}).reduce((s, v) => s + (parseFloat(v) || 0), 0)
|
||||
this.$set(row, '_deductionsTotal', sumD)
|
||||
this.$set(row, '_bonusesTotal', sumB)
|
||||
},
|
||||
onDeductChange(row) { this.recalcTotals(row) },
|
||||
onBonusChange(row) { this.recalcTotals(row) },
|
||||
markDirty(row) { this.$set(row, '_dirty', true) },
|
||||
|
||||
loadList() {
|
||||
if (!this.deptId || !this.period) return
|
||||
this.loading = true
|
||||
const params = {
|
||||
pageNum: this.query.pageNum,
|
||||
pageSize: this.query.pageSize,
|
||||
deptId: this.deptId,
|
||||
period: this.period,
|
||||
employeeId: this.query.employeeId || undefined,
|
||||
status: this.query.status || undefined
|
||||
}
|
||||
listPerfSalary(params).then(res => {
|
||||
this.list = (res.rows || []).map(r => {
|
||||
this.$set(r, '_dirty', false)
|
||||
this.$set(r, '_saving', false)
|
||||
this.parseRowItems(r)
|
||||
return r
|
||||
})
|
||||
this.total = res.total || 0
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.query.pageSize = val
|
||||
this.loadList()
|
||||
},
|
||||
handlePageChange(val) {
|
||||
this.query.pageNum = val
|
||||
this.loadList()
|
||||
},
|
||||
handleAdd() {
|
||||
const row = {
|
||||
employeeId: '',
|
||||
deptId: this.deptId,
|
||||
period: this.period,
|
||||
baseSalary: '',
|
||||
perfBase: '',
|
||||
perfScore: '',
|
||||
perfCoeff: '',
|
||||
deptCoeff: '',
|
||||
posCoeff: '',
|
||||
fixedCoeff: '',
|
||||
adjCoeff: '',
|
||||
adjCoeffManual: '',
|
||||
totalCoeff: '',
|
||||
perfWage: '',
|
||||
deductionsJson: '{}',
|
||||
bonusesJson: '{}',
|
||||
otherAmount: '',
|
||||
actualSalary: '',
|
||||
fixedSalaryRef: '',
|
||||
adjSalaryRef: '',
|
||||
baoPerShiftRef: '',
|
||||
baoSalaryRef: '',
|
||||
totalSalaryRef: '',
|
||||
status: '0',
|
||||
remark: '',
|
||||
_dirty: true,
|
||||
_saving: false,
|
||||
_d: {},
|
||||
_b: {},
|
||||
_deductionsTotal: 0,
|
||||
_bonusesTotal: 0
|
||||
}
|
||||
this.deductionItems.forEach(item => { row._d[item.itemName] = '' })
|
||||
this.bonusItems.forEach(item => { row._b[item.itemName] = '' })
|
||||
this.list.unshift(row)
|
||||
},
|
||||
saveRow(row) {
|
||||
this.$set(row, '_saving', true)
|
||||
const data = { ...row }
|
||||
// 序列化扣款/奖励 JSON
|
||||
data.deductionsJson = JSON.stringify(row._d || {})
|
||||
data.bonusesJson = JSON.stringify(row._b || {})
|
||||
data.deductionsTotal = row._deductionsTotal
|
||||
data.bonusesTotal = row._bonusesTotal
|
||||
// 清理内部字段
|
||||
delete data._d; delete data._b; delete data._dirty; delete data._saving
|
||||
delete data._deductionsTotal; delete data._bonusesTotal
|
||||
|
||||
const api = data.id ? updatePerfSalary : addPerfSalary
|
||||
api(data).then(res => {
|
||||
// 乐观更新:如果是新增,用返回值更新 id
|
||||
if (!data.id && res.data) {
|
||||
this.$set(row, 'id', res.data.id || res.data)
|
||||
}
|
||||
this.$set(row, '_dirty', false)
|
||||
this.$message.success('保存成功')
|
||||
}).catch(() => {
|
||||
this.$message.error('保存失败')
|
||||
this.loadList() // 失败则重新加载以回滚
|
||||
}).finally(() => {
|
||||
this.$set(row, '_saving', false)
|
||||
})
|
||||
},
|
||||
handleDel(row) {
|
||||
if (!row.id) {
|
||||
// 未保存的新行直接移除
|
||||
const idx = this.list.indexOf(row)
|
||||
if (idx >= 0) this.list.splice(idx, 1)
|
||||
return
|
||||
}
|
||||
this.$confirm(`确认删除该薪资记录吗?`, '提示', { type: 'warning' }).then(() => {
|
||||
delPerfSalary(row.id).then(() => {
|
||||
this.$message.success('删除成功')
|
||||
this.loadList()
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleBatchGenerate() {
|
||||
if (!this.deptName) {
|
||||
this.$message.warning('缺少部门名称')
|
||||
return
|
||||
}
|
||||
this.$confirm(`确认根据"${this.deptName}"下的在职员工,一键生成本月薪资记录吗?`, '提示', { type: 'info' }).then(() => {
|
||||
this.generating = true
|
||||
listEmployeeInfo({ dept: this.deptName, isLeave: 0, pageSize: 9999 }).then(empRes => {
|
||||
const employees = empRes.rows || []
|
||||
if (employees.length === 0) {
|
||||
this.$message.warning('该部门下暂无在职员工')
|
||||
return
|
||||
}
|
||||
listPerfSalary({ deptId: this.deptId, period: this.period, pageSize: 9999 }).then(salaryRes => {
|
||||
const existing = new Set((salaryRes.rows || []).map(r => String(r.employeeId)))
|
||||
const toInsert = employees.filter(e => !existing.has(String(e.employeeId) || String(e.infoId)))
|
||||
if (toInsert.length === 0) {
|
||||
this.$message.info('该部门下所有在职员工已有本月薪资记录')
|
||||
return
|
||||
}
|
||||
const emptyJson = JSON.stringify({})
|
||||
const tasks = toInsert.map(emp => addPerfSalary({
|
||||
employeeId: emp.employeeId || emp.infoId || '',
|
||||
deptId: this.deptId,
|
||||
period: this.period,
|
||||
baseSalary: emp.baseSalary || 0,
|
||||
perfBase: emp.perfBase || 0,
|
||||
posCoeff: emp.posCoeffDefault || 0,
|
||||
perfScore: 0,
|
||||
perfCoeff: 0,
|
||||
deptCoeff: 0,
|
||||
fixedCoeff: 0,
|
||||
adjCoeff: 0,
|
||||
adjCoeffManual: 0,
|
||||
totalCoeff: 0,
|
||||
perfWage: 0,
|
||||
deductionsJson: emptyJson,
|
||||
deductionsTotal: 0,
|
||||
bonusesJson: emptyJson,
|
||||
bonusesTotal: 0,
|
||||
otherAmount: 0,
|
||||
actualSalary: 0,
|
||||
fixedSalaryRef: '',
|
||||
adjSalaryRef: '',
|
||||
baoPerShiftRef: '',
|
||||
baoSalaryRef: '',
|
||||
totalSalaryRef: '',
|
||||
status: '0',
|
||||
remark: ''
|
||||
}))
|
||||
return Promise.allSettled(tasks)
|
||||
}).then(results => {
|
||||
if (!results) return
|
||||
const ok = results.filter(r => r.status === 'fulfilled').length
|
||||
const fail = results.filter(r => r.status === 'rejected').length
|
||||
this.$message.success(`生成完成:成功 ${ok} 条${fail > 0 ? `,失败 ${fail} 条` : ''}`)
|
||||
this.loadList()
|
||||
})
|
||||
}).finally(() => {
|
||||
this.generating = false
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.salary-panel {
|
||||
padding: 8px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-left,
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
::v-deep .inline-edit-table {
|
||||
border: none;
|
||||
&::before, &::after { display: none; }
|
||||
th { border: none; padding: 2px 4px; font-size: 12px; color: #909399; }
|
||||
td {
|
||||
border: none;
|
||||
border-right: 1px solid #ebeef5;
|
||||
padding: 0;
|
||||
}
|
||||
.el-table__body tr:hover td { background-color: #f5f7fa !important; }
|
||||
.cell { padding: 0; height: 100%; }
|
||||
}
|
||||
|
||||
/* 原生 input */
|
||||
.cell-input {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: right;
|
||||
padding: 0 6px;
|
||||
font-size: 12px;
|
||||
color: #303133;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
line-height: 28px;
|
||||
transition: background-color 0.15s;
|
||||
|
||||
&:focus {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 0 1px #409eff inset;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&--base { background-color: #e8f4fd; }
|
||||
&--coeff { background-color: #fff8e1; }
|
||||
&--deduct { background-color: #ffebee; }
|
||||
&--bonus { background-color: #e8f5e9; }
|
||||
&--amount { background-color: #f3e5f5; }
|
||||
&--ref { background-color: #f5f5f5; }
|
||||
&--remark { background-color: #fafafa; text-align: left; }
|
||||
}
|
||||
|
||||
.cell-readonly {
|
||||
display: block;
|
||||
text-align: right;
|
||||
padding: 0 6px;
|
||||
font-size: 12px;
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
height: 28px;
|
||||
|
||||
&--deduct { color: #e53935; }
|
||||
&--bonus { color: #43a047; }
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
color: #67c23a !important;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.el-table .el-button + .el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
</style>
|
||||
240
klp-ui/src/views/perf/index/SummaryPanel.vue
Normal file
240
klp-ui/src/views/perf/index/SummaryPanel.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<div class="summary-panel" v-loading="loading">
|
||||
<div v-if="!summary && !loading" class="empty-state">
|
||||
<el-empty description="暂无统计数据">
|
||||
<span slot="extra" style="color: #909399; font-size: 13px;">请确认已配置考核参数并生成薪资记录</span>
|
||||
</el-empty>
|
||||
</div>
|
||||
|
||||
<template v-if="summary">
|
||||
<!-- Overview Cards -->
|
||||
<div class="stat-cards">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">总人数</div>
|
||||
<div class="stat-value">{{ fmt(summary.totalEmployees) }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">系数总计</div>
|
||||
<div class="stat-value">{{ fmt(summary.totalCoeffSum) }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">平均考核得分</div>
|
||||
<div class="stat-value highlight">{{ fmt(summary.avgPerfScore) }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">平均绩效系数</div>
|
||||
<div class="stat-value">{{ fmt(summary.avgPerfCoeff) }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">月产量</div>
|
||||
<div class="stat-value">{{ fmt(summary.monthlyProduction) }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">系数单价</div>
|
||||
<div class="stat-value highlight">{{ fmt(summary.unitPrice) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Salary Breakdown Card -->
|
||||
<div class="detail-card">
|
||||
<div class="card-title">薪资构成</div>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">底薪合计</span>
|
||||
<span class="detail-value">{{ fmt(summary.totalBaseSalary) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">绩效工资合计</span>
|
||||
<span class="detail-value highlight">{{ fmt(summary.totalPerfWage) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">扣款合计</span>
|
||||
<span class="detail-value danger">{{ fmt(summary.totalDeductions) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">奖励合计</span>
|
||||
<span class="detail-value success">{{ fmt(summary.totalBonuses) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">其他合计</span>
|
||||
<span class="detail-value">{{ fmt(summary.totalOther) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">实领薪资合计</span>
|
||||
<span class="detail-value total">{{ fmt(summary.totalActualSalary) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">人均实领薪资</span>
|
||||
<span class="detail-value highlight">{{ fmt(summary.avgActualSalary) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="status-bar">
|
||||
<span class="status-label">状态</span>
|
||||
<el-tag :type="summary.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ summary.status === 1 ? '已确认' : '草稿' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listPerfDeptSummary } from '@/api/perf/deptSummary'
|
||||
|
||||
export default {
|
||||
name: 'SummaryPanel',
|
||||
props: {
|
||||
deptId: { type: [Number, String], default: null },
|
||||
period: { type: String, default: '' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
summary: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
deptId: { immediate: true, handler: 'loadSummary' },
|
||||
period: 'loadSummary'
|
||||
},
|
||||
methods: {
|
||||
fmt(val) {
|
||||
return val != null ? val : '-'
|
||||
},
|
||||
loadSummary() {
|
||||
if (!this.deptId || !this.period) return
|
||||
this.loading = true
|
||||
this.summary = null
|
||||
listPerfDeptSummary({ deptId: this.deptId, period: this.period }).then(res => {
|
||||
const rows = res.rows || []
|
||||
this.summary = rows.length > 0 ? rows[0] : null
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.summary-panel {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background-color: #f5f7fa;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stat-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 20px 16px;
|
||||
text-align: center;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #303133;
|
||||
|
||||
&.highlight {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
|
||||
&.highlight {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.total {
|
||||
color: #e6a23c;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
207
klp-ui/src/views/perf/index/YearMonthTabs.vue
Normal file
207
klp-ui/src/views/perf/index/YearMonthTabs.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div class="year-month-tabs">
|
||||
<!-- Year Tabs -->
|
||||
<div class="year-row">
|
||||
<div
|
||||
v-for="y in years"
|
||||
:key="y"
|
||||
class="year-tab"
|
||||
:class="{ active: currentYear === y }"
|
||||
@click="handleYearClick(y)"
|
||||
>{{ y }}年</div>
|
||||
</div>
|
||||
<!-- Month Tabs -->
|
||||
<div class="month-row" v-if="currentYear">
|
||||
<div
|
||||
v-for="m in 12"
|
||||
:key="m"
|
||||
class="month-tab"
|
||||
:class="{ active: currentYear === selectedYear && currentMonth === m }"
|
||||
:disabled="!isMonthAvailable(currentYear, m)"
|
||||
@click="handleMonthClick(m)"
|
||||
>{{ String(m).padStart(2, '0') }}月</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'YearMonthTabs',
|
||||
props: {
|
||||
/** 当前选中的年月,格式 { year, month } */
|
||||
value: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
/** 可选的年份范围偏移:往前推 N 年 */
|
||||
pastYears: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
/** 可选的年份范围偏移:往后推 N 年 */
|
||||
futureYears: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
/** 月份可用性判断函数 (year, month) => boolean,默认全部可用 */
|
||||
monthValidator: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const now = new Date()
|
||||
const currentYear = now.getFullYear()
|
||||
const currentMonth = now.getMonth() + 1
|
||||
return {
|
||||
selectedYear: this.value?.year || currentYear,
|
||||
currentMonth: this.value?.month || currentMonth
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentYear: {
|
||||
get() { return this.selectedYear },
|
||||
set(v) { this.selectedYear = v }
|
||||
},
|
||||
years() {
|
||||
const now = new Date().getFullYear()
|
||||
const start = now - this.pastYears
|
||||
const end = now + this.futureYears
|
||||
const list = []
|
||||
for (let y = start; y <= end; y++) {
|
||||
list.push(y)
|
||||
}
|
||||
return list
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
handler(v) {
|
||||
if (v && v.year && v.month) {
|
||||
this.selectedYear = v.year
|
||||
this.currentMonth = v.month
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isMonthAvailable(year, month) {
|
||||
if (this.monthValidator) {
|
||||
return this.monthValidator(year, month)
|
||||
}
|
||||
// 默认不限制未来月份
|
||||
return true
|
||||
},
|
||||
handleYearClick(year) {
|
||||
this.selectedYear = year
|
||||
// 切换年份时,如果当前月份不可用则重置为1月
|
||||
if (!this.isMonthAvailable(year, this.currentMonth)) {
|
||||
this.currentMonth = 1
|
||||
}
|
||||
this.emitChange()
|
||||
},
|
||||
handleMonthClick(month) {
|
||||
if (!this.isMonthAvailable(this.selectedYear, month)) return
|
||||
this.currentMonth = month
|
||||
this.emitChange()
|
||||
},
|
||||
emitChange() {
|
||||
this.$emit('input', { year: this.selectedYear, month: this.currentMonth })
|
||||
this.$emit('change', { year: this.selectedYear, month: this.currentMonth })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.year-month-tabs {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.year-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
height: 36px;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.year-tab {
|
||||
position: relative;
|
||||
padding: 0 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
height: 2px;
|
||||
background-color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.month-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
height: 34px;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.month-tab {
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
position: relative;
|
||||
|
||||
&:hover:not([disabled]) {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 6px;
|
||||
right: 6px;
|
||||
height: 2px;
|
||||
background-color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
color: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
206
klp-ui/src/views/perf/index/index.vue
Normal file
206
klp-ui/src/views/perf/index/index.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div class="perf-index-page">
|
||||
<!-- Top: Year-Month Tabs -->
|
||||
<YearMonthTabs v-model="selectedDate" @change="handleDateChange" />
|
||||
|
||||
<!-- Main: Left Dept List + Right Content -->
|
||||
<div class="main-layout">
|
||||
<!-- Left: Department List -->
|
||||
<div class="dept-panel">
|
||||
<div class="dept-panel-header">
|
||||
<span class="dept-panel-title">部门列表</span>
|
||||
</div>
|
||||
<div class="dept-list" v-loading="deptLoading">
|
||||
<div
|
||||
v-for="dept in deptList"
|
||||
:key="dept.id"
|
||||
class="dept-item"
|
||||
:class="{ active: currentDept && currentDept.id === dept.id }"
|
||||
@click="handleDeptSelect(dept)"
|
||||
>
|
||||
<span class="dept-name">{{ dept.deptName }}</span>
|
||||
</div>
|
||||
<div v-if="!deptLoading && deptList.length === 0" class="dept-empty">暂无部门数据</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Content -->
|
||||
<div class="content-panel">
|
||||
<PageHeader
|
||||
v-if="currentDept"
|
||||
:dept-name="currentDept.deptName"
|
||||
:period="periodStr"
|
||||
:active-tab.sync="activeTab"
|
||||
@refresh="handleRefresh"
|
||||
/>
|
||||
<div v-else class="no-dept-tip">
|
||||
<span class="placeholder-text">请先在左侧选择一个部门</span>
|
||||
</div>
|
||||
|
||||
<div class="panel-container" v-if="currentDept">
|
||||
<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" />
|
||||
<SummaryPanel v-if="activeTab === 'summary'" :dept-id="currentDept.id" :period="periodStr" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import YearMonthTabs from './YearMonthTabs'
|
||||
import PageHeader from './PageHeader'
|
||||
import PerfConfigPanel from './PerfConfigPanel'
|
||||
import SalaryPanel from './SalaryPanel'
|
||||
import SummaryPanel from './SummaryPanel'
|
||||
import { listPerfDept } from '@/api/perf/dept'
|
||||
|
||||
export default {
|
||||
name: 'PerfIndex',
|
||||
components: { YearMonthTabs, PageHeader, PerfConfigPanel, SalaryPanel, SummaryPanel },
|
||||
data() {
|
||||
const now = new Date()
|
||||
return {
|
||||
deptLoading: false,
|
||||
deptList: [],
|
||||
currentDept: null,
|
||||
selectedDate: {
|
||||
year: now.getFullYear(),
|
||||
month: now.getMonth() + 1
|
||||
},
|
||||
activeTab: 'perfConfig'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
periodStr() {
|
||||
const { year, month } = this.selectedDate
|
||||
return `${year}-${String(month).padStart(2, '0')}`
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadDeptList()
|
||||
},
|
||||
methods: {
|
||||
loadDeptList() {
|
||||
this.deptLoading = true
|
||||
listPerfDept({}).then(res => {
|
||||
this.deptList = res.rows || []
|
||||
}).finally(() => {
|
||||
this.deptLoading = false
|
||||
})
|
||||
},
|
||||
handleDeptSelect(dept) {
|
||||
this.currentDept = dept
|
||||
},
|
||||
handleDateChange(date) {
|
||||
this.selectedDate = date
|
||||
},
|
||||
handleRefresh() {
|
||||
// Force child components to reload by toggling through the tab
|
||||
// (Each panel watches deptId/period and will reload on change)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.perf-index-page {
|
||||
height: calc(100vh - 84px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// ======== Left: Department Panel ========
|
||||
.dept-panel {
|
||||
background-color: #fff;
|
||||
border-right: 1px solid #e4e7ed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dept-panel-header {
|
||||
padding: 14px 16px 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.dept-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.dept-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #ecf5ff;
|
||||
|
||||
.dept-name {
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dept-name {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dept-empty {
|
||||
padding: 32px 0;
|
||||
text-align: center;
|
||||
color: #c0c4cc;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
// ======== Right: Content Panel ========
|
||||
.content-panel {
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.no-dept-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 14px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.panel-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -633,8 +633,8 @@ export default {
|
||||
fullPath = '/' + parent + ownPath;
|
||||
}
|
||||
}
|
||||
// 目录(M)的路径作为子节点的父路径
|
||||
const currentParentPath = node.menuType === 'M' ? ownPath : parentPath;
|
||||
// 目录(M)的路径拼接祖先路径后作为子节点的父路径
|
||||
const currentParentPath = node.menuType === 'M' ? (parentPath ? parentPath + '/' + ownPath : ownPath) : parentPath;
|
||||
|
||||
result.push({
|
||||
menuName: prefix + node.menuName,
|
||||
|
||||
Reference in New Issue
Block a user