绩效统计页面开发
This commit is contained in:
@@ -85,6 +85,17 @@ public class PerfDeptSummaryController extends BaseController {
|
|||||||
return toAjax(iPerfDeptSummaryService.updateByBo(bo));
|
return toAjax(iPerfDeptSummaryService.updateByBo(bo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存车间月度汇总(按 deptId + period 自动新增或更新)
|
||||||
|
* 前端统计页使用:将当前页面计算/展示的汇总数据落库到 perf_dept_summary 表。
|
||||||
|
*/
|
||||||
|
@Log(title = "车间月度汇总", businessType = BusinessType.OTHER)
|
||||||
|
@RepeatSubmit()
|
||||||
|
@PostMapping("/saveByPeriod")
|
||||||
|
public R<Void> saveByPeriod(@RequestBody PerfDeptSummaryBo bo) {
|
||||||
|
return toAjax(iPerfDeptSummaryService.saveByDeptPeriod(bo));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除车间月度汇总
|
* 删除车间月度汇总
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -46,4 +46,9 @@ public interface IPerfDeptSummaryService {
|
|||||||
* 校验并批量删除车间月度汇总信息
|
* 校验并批量删除车间月度汇总信息
|
||||||
*/
|
*/
|
||||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 deptId + period 保存汇总(存在则更新,不存在则新增)
|
||||||
|
*/
|
||||||
|
Boolean saveByDeptPeriod(PerfDeptSummaryBo bo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import com.klp.perf.service.IPerfDeptSummaryService;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 车间月度汇总Service业务层处理
|
* 车间月度汇总Service业务层处理
|
||||||
@@ -121,4 +122,27 @@ public class PerfDeptSummaryServiceImpl implements IPerfDeptSummaryService {
|
|||||||
}
|
}
|
||||||
return baseMapper.deleteBatchIds(ids) > 0;
|
return baseMapper.deleteBatchIds(ids) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 deptId + period 保存汇总(存在则更新,不存在则新增)
|
||||||
|
* 用于前端“统计页保存汇总”按钮:将页面计算出的数据落库到 perf_dept_summary 表。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Boolean saveByDeptPeriod(PerfDeptSummaryBo bo) {
|
||||||
|
if (bo == null || bo.getDeptId() == null || StringUtils.isBlank(bo.getPeriod())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (bo.getId() != null) {
|
||||||
|
return updateByBo(bo);
|
||||||
|
}
|
||||||
|
PerfDeptSummary existed = baseMapper.selectOne(Wrappers.<PerfDeptSummary>lambdaQuery()
|
||||||
|
.eq(PerfDeptSummary::getDeptId, bo.getDeptId())
|
||||||
|
.eq(PerfDeptSummary::getPeriod, bo.getPeriod())
|
||||||
|
.last("limit 1"));
|
||||||
|
if (Objects.nonNull(existed)) {
|
||||||
|
bo.setId(existed.getId());
|
||||||
|
return updateByBo(bo);
|
||||||
|
}
|
||||||
|
return insertByBo(bo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,15 @@ export function updatePerfDeptSummary(data) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存车间月度汇总(按 deptId + period 自动新增或更新)
|
||||||
|
export function savePerfDeptSummaryByPeriod(data) {
|
||||||
|
return request({
|
||||||
|
url: '/perf/deptSummary/saveByPeriod',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 删除车间月度汇总
|
// 删除车间月度汇总
|
||||||
export function delPerfDeptSummary(ids) {
|
export function delPerfDeptSummary(ids) {
|
||||||
return request({
|
return request({
|
||||||
|
|||||||
@@ -1,57 +1,92 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="perf-config-panel">
|
<div class="perf-config-panel">
|
||||||
<!-- Section 0: 考核参数 -->
|
<!-- Section 0: 考核参数 -->
|
||||||
<div class="config-section">
|
<el-card class="config-card mb8" shadow="never">
|
||||||
<div class="section-header">
|
<div slot="header" class="card-header">
|
||||||
<span class="section-title">考核参数</span>
|
<div class="card-left">
|
||||||
<el-button size="small" type="primary" :loading="cfgSubmitting" @click="handleSaveCfg">{{ cfgId ? '保存' : '新增' }}</el-button>
|
<span class="card-title">考核参数</span>
|
||||||
|
<el-tag size="mini" :type="cfgStatus.type" class="cfg-status">{{ cfgStatus.text }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<el-button size="mini" :type="cfgBtnType" :loading="cfgSubmitting" @click="handleSaveCfg">{{ cfgBtnText }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="cfg-form" v-loading="cfgLoading">
|
<div v-loading="cfgLoading" class="card-body">
|
||||||
<el-form v-if="cfgReady" ref="cfgFormRef" :model="cfgForm" :rules="cfgRules" label-width="110px" size="small" inline>
|
<el-form v-if="cfgReady" ref="cfgFormRef" :model="cfgForm" :rules="cfgRules" label-width="96px" size="small" class="cfg-form">
|
||||||
<el-form-item label="月产量目标" prop="monthlyProductionTarget">
|
<el-row :gutter="12">
|
||||||
<el-input v-model="cfgForm.monthlyProductionTarget" placeholder="月产量目标" style="width: 120px" />
|
<el-col :span="6">
|
||||||
</el-form-item>
|
<el-form-item label="月产量目标" prop="monthlyProductionTarget">
|
||||||
<el-form-item label="产量单位" prop="productionUnit">
|
<el-input v-model="cfgForm.monthlyProductionTarget" placeholder="请输入" />
|
||||||
<el-select v-model="cfgForm.productionUnit" style="width: 80px">
|
</el-form-item>
|
||||||
<el-option label="吨" value="吨" />
|
</el-col>
|
||||||
<el-option label="m" value="m" />
|
<el-col :span="6">
|
||||||
</el-select>
|
<el-form-item label="产量单位" prop="productionUnit">
|
||||||
</el-form-item>
|
<el-select v-model="cfgForm.productionUnit" placeholder="请选择" style="width:100%">
|
||||||
<el-form-item label="绩效单价(元)" prop="perfUnitPrice">
|
<el-option label="吨" value="吨" />
|
||||||
<el-input v-model="cfgForm.perfUnitPrice" placeholder="绩效单价" style="width: 110px" />
|
<el-option label="m" value="m" />
|
||||||
</el-form-item>
|
</el-select>
|
||||||
<el-form-item label="生产班数" prop="productionShifts">
|
</el-form-item>
|
||||||
<el-input v-model="cfgForm.productionShifts" placeholder="班数" style="width: 80px" />
|
</el-col>
|
||||||
</el-form-item>
|
<el-col :span="6">
|
||||||
<el-form-item label="保底班数" prop="guaranteedShifts">
|
<el-form-item label="绩效单价" prop="perfUnitPrice">
|
||||||
<el-input v-model="cfgForm.guaranteedShifts" placeholder="保底班数" style="width: 80px" />
|
<el-input v-model="cfgForm.perfUnitPrice" placeholder="元" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="车间系数" prop="deptCoeff">
|
</el-col>
|
||||||
<el-input v-model="cfgForm.deptCoeff" placeholder="系数" style="width: 80px" />
|
<el-col :span="6">
|
||||||
</el-form-item>
|
<el-form-item label="生产班数" prop="productionShifts">
|
||||||
<el-form-item label="薪资模式" prop="salaryMode">
|
<el-input v-model="cfgForm.productionShifts" placeholder="请输入" />
|
||||||
<el-radio-group v-model="cfgForm.salaryMode" size="small">
|
</el-form-item>
|
||||||
<el-radio :label="0">普通</el-radio>
|
</el-col>
|
||||||
<el-radio :label="1">保底</el-radio>
|
</el-row>
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
<el-row :gutter="12">
|
||||||
<el-form-item label="保底薪资除数" prop="baseDivisor" v-if="cfgForm.salaryMode === 1">
|
<el-col :span="6">
|
||||||
<el-input v-model="cfgForm.baseDivisor" placeholder="除数" style="width: 80px" />
|
<el-form-item label="保底班数" prop="guaranteedShifts">
|
||||||
</el-form-item>
|
<el-input v-model="cfgForm.guaranteedShifts" placeholder="请输入" />
|
||||||
<el-form-item label="备注">
|
</el-form-item>
|
||||||
<el-input v-model="cfgForm.remark" placeholder="备注" style="width: 200px" />
|
</el-col>
|
||||||
</el-form-item>
|
<el-col :span="6">
|
||||||
|
<el-form-item label="车间系数" prop="deptCoeff">
|
||||||
|
<el-input v-model="cfgForm.deptCoeff" placeholder="请输入" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<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-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="保底除数" prop="baseDivisor">
|
||||||
|
<el-input
|
||||||
|
:value="cfgForm.salaryMode === 1 ? cfgForm.baseDivisor : '普通模式不可编辑'"
|
||||||
|
placeholder="请输入"
|
||||||
|
:disabled="cfgForm.salaryMode !== 1"
|
||||||
|
@input="val => cfgForm.baseDivisor = val"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="12">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="cfgForm.remark" placeholder="请输入" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
<!-- Section 1: 扣款/奖励项目配置 -->
|
<!-- Section 1: 扣款/奖励项目配置 -->
|
||||||
<div class="config-section">
|
<el-card class="config-card mb8" shadow="never">
|
||||||
<div class="section-header">
|
<div slot="header" class="card-header">
|
||||||
<span class="section-title">扣款/奖励项目配置</span>
|
<span class="card-title">扣款/奖励项目配置</span>
|
||||||
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAddItem">新增项目</el-button>
|
<el-button size="mini" type="primary" icon="el-icon-plus" @click="handleAddItem">新增</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="itemList" stripe size="small" v-loading="itemLoading" empty-text="暂无项目配置">
|
<div class="card-body">
|
||||||
|
<el-table :data="itemList" stripe size="mini" v-loading="itemLoading" empty-text="暂无项目配置">
|
||||||
<el-table-column label="类型" width="80" align="center">
|
<el-table-column label="类型" width="80" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-tag :type="scope.row.itemType === 2 ? 'success' : 'danger'" size="small">
|
<el-tag :type="scope.row.itemType === 2 ? 'success' : 'danger'" size="small">
|
||||||
@@ -70,20 +105,23 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
|
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" width="140" align="center" fixed="right">
|
<el-table-column label="操作" width="160" align="center" fixed="right">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEditItem(scope.row)">编辑</el-button>
|
<div class="op-actions">
|
||||||
<el-button type="text" size="mini" icon="el-icon-delete" style="color: #f56c6c" @click="handleDelItem(scope.row)">删除</el-button>
|
<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>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<!-- Section 2: 岗位绩效考核模板 -->
|
<!-- Section 2: 岗位绩效考核模板 -->
|
||||||
<div class="config-section" v-loading="ctxLoading">
|
<el-card class="config-card" shadow="never" v-loading="ctxLoading">
|
||||||
<div class="section-header">
|
<div slot="header" class="card-header">
|
||||||
<span class="section-title">岗位绩效考核模板 <span class="section-hint">右键 tab 可编辑或删除</span></span>
|
<span class="card-title">岗位绩效考核模板 <span class="card-hint">右键岗位可编辑或删除</span></span>
|
||||||
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAddTemplate">新增岗位</el-button>
|
<el-button size="mini" type="primary" icon="el-icon-plus" @click="handleAddTemplate">新增</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="templateList.length" class="template-tabs">
|
<div v-if="templateList.length" class="template-tabs">
|
||||||
<span
|
<span
|
||||||
@@ -100,9 +138,9 @@
|
|||||||
<div v-if="activeTpl" class="dim-section">
|
<div v-if="activeTpl" class="dim-section">
|
||||||
<div class="dim-toolbar">
|
<div class="dim-toolbar">
|
||||||
<span class="dim-subtitle">{{ activeTpl.positionName }} — 考核维度</span>
|
<span class="dim-subtitle">{{ activeTpl.positionName }} — 考核维度</span>
|
||||||
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAddDim">新增维度</el-button>
|
<el-button size="mini" type="primary" icon="el-icon-plus" @click="handleAddDim">新增</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="dimList" stripe size="small" v-loading="dimLoading" empty-text="暂无维度配置">
|
<el-table :data="dimList" stripe size="mini" v-loading="dimLoading" empty-text="暂无维度配置">
|
||||||
<el-table-column prop="seq" label="序号" width="60" align="center" />
|
<el-table-column prop="seq" label="序号" width="60" align="center" />
|
||||||
<el-table-column prop="dimName" label="考核维度" min-width="120" />
|
<el-table-column prop="dimName" label="考核维度" min-width="120" />
|
||||||
<el-table-column prop="weight" label="权重" width="70" align="center" />
|
<el-table-column prop="weight" label="权重" width="70" align="center" />
|
||||||
@@ -114,16 +152,18 @@
|
|||||||
<el-switch v-model="scope.row.isEnabled" :active-value="1" :inactive-value="0" size="small" @change="handleToggleDimEnabled(scope.row)" />
|
<el-switch v-model="scope.row.isEnabled" :active-value="1" :inactive-value="0" size="small" @change="handleToggleDimEnabled(scope.row)" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="100" align="center">
|
<el-table-column label="操作" width="140" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEditDim(scope.row)">编辑</el-button>
|
<div class="op-actions">
|
||||||
<el-button type="text" size="mini" icon="el-icon-delete" style="color: #f56c6c" @click="handleDelDim(scope.row)">删除</el-button>
|
<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>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="dim-empty">点击 tab 查看维度,右键 tab 可编辑或删除模板</div>
|
<div v-else class="dim-empty">点击 tab 查看维度,右键 tab 可编辑或删除模板</div>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
<!-- 右键菜单 -->
|
<!-- 右键菜单 -->
|
||||||
<div
|
<div
|
||||||
@@ -160,7 +200,7 @@
|
|||||||
<el-input v-model="itemForm.remark" type="textarea" :rows="2" />
|
<el-input v-model="itemForm.remark" type="textarea" :rows="2" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div slot="footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
<el-button @click="itemDialogVisible = false">取消</el-button>
|
<el-button @click="itemDialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="itemSubmitting" @click="submitItem">保存</el-button>
|
<el-button type="primary" :loading="itemSubmitting" @click="submitItem">保存</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -179,7 +219,7 @@
|
|||||||
<el-input v-model="templateForm.remark" type="textarea" :rows="2" />
|
<el-input v-model="templateForm.remark" type="textarea" :rows="2" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div slot="footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
<el-button @click="templateDialogVisible = false">取消</el-button>
|
<el-button @click="templateDialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="templateSubmitting" @click="submitTemplate">保存</el-button>
|
<el-button type="primary" :loading="templateSubmitting" @click="submitTemplate">保存</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -234,7 +274,7 @@
|
|||||||
<el-input v-model="dimForm.remark" type="textarea" :rows="2" />
|
<el-input v-model="dimForm.remark" type="textarea" :rows="2" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div slot="footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
<el-button @click="dimFormVisible = false">取消</el-button>
|
<el-button @click="dimFormVisible = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="dimSubmitting" @click="submitDim">保存</el-button>
|
<el-button type="primary" :loading="dimSubmitting" @click="submitDim">保存</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -262,7 +302,13 @@ export default {
|
|||||||
cfgLoading: false,
|
cfgLoading: false,
|
||||||
cfgSubmitting: false,
|
cfgSubmitting: false,
|
||||||
cfgReady: false,
|
cfgReady: false,
|
||||||
|
// cfgId 存在表示该部门该月份已存在配置记录;为空表示首次配置(未落库)
|
||||||
cfgId: null,
|
cfgId: null,
|
||||||
|
// 方案C:通过“快照对比”识别是否改动未保存
|
||||||
|
cfgSnapshot: '',
|
||||||
|
cfgDirty: false,
|
||||||
|
// 后端回填表单时锁定快照对比,避免回填过程误判为“已修改未保存”
|
||||||
|
cfgSnapshotLock: false,
|
||||||
cfgForm: { monthlyProductionTarget: '', productionUnit: '吨', perfUnitPrice: '', productionShifts: '', guaranteedShifts: '', deptCoeff: '', salaryMode: 0, baseDivisor: '', remark: '' },
|
cfgForm: { monthlyProductionTarget: '', productionUnit: '吨', perfUnitPrice: '', productionShifts: '', guaranteedShifts: '', deptCoeff: '', salaryMode: 0, baseDivisor: '', remark: '' },
|
||||||
cfgRules: {
|
cfgRules: {
|
||||||
monthlyProductionTarget: [{ required: true, message: '请输入月产量目标', trigger: 'blur' }],
|
monthlyProductionTarget: [{ required: true, message: '请输入月产量目标', trigger: 'blur' }],
|
||||||
@@ -315,13 +361,48 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
activeTpl() {
|
activeTpl() {
|
||||||
return this.templateList.find(t => String(t.id) === this.activeTemplateId) || null
|
return this.templateList.find(t => String(t.id) === this.activeTemplateId) || null
|
||||||
|
},
|
||||||
|
cfgStatus() {
|
||||||
|
if (this.cfgDirty) return { type: 'danger', text: '已修改未保存' }
|
||||||
|
if (this.cfgId) return { type: 'success', text: '已保存' }
|
||||||
|
return { type: 'warning', text: '未保存(首次配置)' }
|
||||||
|
},
|
||||||
|
cfgBtnText() {
|
||||||
|
return this.cfgId ? '保存修改' : '新增并保存'
|
||||||
|
},
|
||||||
|
cfgBtnType() {
|
||||||
|
return this.cfgDirty ? 'danger' : 'primary'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
deptId: { immediate: true, handler: 'loadAll' },
|
deptId: { immediate: true, handler: 'loadAll' },
|
||||||
period: 'loadAll'
|
period: 'loadAll',
|
||||||
|
cfgForm: {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
// 表单未就绪或正在回填时,不做“脏状态”判断
|
||||||
|
if (!this.cfgReady) return
|
||||||
|
if (this.cfgSnapshotLock) return
|
||||||
|
this.cfgDirty = this.calcCfgSnapshot() !== this.cfgSnapshot
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 将考核参数表单按字段归一化,用于快照比对(避免 undefined/null 造成误判)
|
||||||
|
calcCfgSnapshot() {
|
||||||
|
const f = this.cfgForm || {}
|
||||||
|
return JSON.stringify({
|
||||||
|
monthlyProductionTarget: f.monthlyProductionTarget ?? '',
|
||||||
|
productionUnit: f.productionUnit ?? '',
|
||||||
|
perfUnitPrice: f.perfUnitPrice ?? '',
|
||||||
|
productionShifts: f.productionShifts ?? '',
|
||||||
|
guaranteedShifts: f.guaranteedShifts ?? '',
|
||||||
|
deptCoeff: f.deptCoeff ?? '',
|
||||||
|
salaryMode: f.salaryMode ?? 0,
|
||||||
|
baseDivisor: f.baseDivisor ?? '',
|
||||||
|
remark: f.remark ?? ''
|
||||||
|
})
|
||||||
|
},
|
||||||
loadAll() {
|
loadAll() {
|
||||||
if (!this.deptId) return
|
if (!this.deptId) return
|
||||||
this.loadDeptConfig()
|
this.loadDeptConfig()
|
||||||
@@ -332,6 +413,8 @@ export default {
|
|||||||
loadDeptConfig() {
|
loadDeptConfig() {
|
||||||
this.cfgLoading = true
|
this.cfgLoading = true
|
||||||
this.cfgReady = false
|
this.cfgReady = false
|
||||||
|
this.cfgSnapshotLock = true
|
||||||
|
// 约定:后端按 month=YYYY-MM-01 查询,前端 period 为 YYYY-MM
|
||||||
listPerfDeptConfig({ deptId: this.deptId, month: this.period + '-01' }).then(res => {
|
listPerfDeptConfig({ deptId: this.deptId, month: this.period + '-01' }).then(res => {
|
||||||
const rows = res.rows || []
|
const rows = res.rows || []
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
@@ -353,13 +436,22 @@ export default {
|
|||||||
this.cfgForm = { monthlyProductionTarget: '', productionUnit: '吨', perfUnitPrice: '', productionShifts: '', guaranteedShifts: '', deptCoeff: '', salaryMode: 0, baseDivisor: '', remark: '' }
|
this.cfgForm = { monthlyProductionTarget: '', productionUnit: '吨', perfUnitPrice: '', productionShifts: '', guaranteedShifts: '', deptCoeff: '', salaryMode: 0, baseDivisor: '', remark: '' }
|
||||||
}
|
}
|
||||||
this.cfgReady = true
|
this.cfgReady = true
|
||||||
}).finally(() => { this.cfgLoading = false })
|
this.$nextTick(() => {
|
||||||
|
// 表单回填完成后建立快照,并重置“脏状态”
|
||||||
|
this.cfgSnapshot = this.calcCfgSnapshot()
|
||||||
|
this.cfgDirty = false
|
||||||
|
this.cfgSnapshotLock = false
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
this.cfgLoading = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
handleSaveCfg() {
|
handleSaveCfg() {
|
||||||
this.$refs.cfgFormRef.validate(valid => {
|
this.$refs.cfgFormRef.validate(valid => {
|
||||||
if (!valid) return
|
if (!valid) return
|
||||||
this.cfgSubmitting = true
|
this.cfgSubmitting = true
|
||||||
const data = { ...this.cfgForm, deptId: this.deptId, month: this.period + '-01' }
|
const data = { ...this.cfgForm, deptId: this.deptId, month: this.period + '-01' }
|
||||||
|
// cfgId 存在走更新;不存在走新增(首次保存)
|
||||||
const api = this.cfgId ? updatePerfDeptConfig : addPerfDeptConfig
|
const api = this.cfgId ? updatePerfDeptConfig : addPerfDeptConfig
|
||||||
if (this.cfgId) data.id = this.cfgId
|
if (this.cfgId) data.id = this.cfgId
|
||||||
api(data).then(() => {
|
api(data).then(() => {
|
||||||
@@ -539,44 +631,59 @@ export default {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-section {
|
.config-card {
|
||||||
background-color: #fff;
|
border-radius: 0;
|
||||||
margin-bottom: 12px;
|
box-shadow: none;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
::v-deep .el-card {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
::v-deep .el-card__header {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
::v-deep .el-card__body {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 12px;
|
justify-content: space-between;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.card-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfg-status {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-hint {
|
.card-hint {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #c0c4cc;
|
color: #c0c4cc;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cfg-form {
|
.card-body {
|
||||||
padding: 12px;
|
padding: 0;
|
||||||
|
|
||||||
::v-deep .el-form-item {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-table {
|
.cfg-form {
|
||||||
border: none;
|
::v-deep .el-form-item {
|
||||||
&::before, &::after { display: none; }
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 自定义模板 tabs */
|
/* 自定义模板 tabs */
|
||||||
@@ -584,17 +691,17 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 8px 12px;
|
padding: 0 0 10px;
|
||||||
border-bottom: 1px solid #ebeef5;
|
border-bottom: 1px solid #ebeef5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-tab {
|
.template-tab {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 4px 14px;
|
padding: 4px 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
background: #f4f4f5;
|
background: #f4f4f5;
|
||||||
border-radius: 12px;
|
border-radius: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
@@ -617,14 +724,14 @@ export default {
|
|||||||
|
|
||||||
/* 维度区域 */
|
/* 维度区域 */
|
||||||
.dim-section {
|
.dim-section {
|
||||||
padding: 8px 0 0;
|
padding: 10px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dim-toolbar {
|
.dim-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 12px 8px;
|
padding: 0 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dim-subtitle {
|
.dim-subtitle {
|
||||||
@@ -645,8 +752,8 @@ export default {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #e4e7ed;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 0;
|
||||||
box-shadow: 0 2px 12px rgba(0,0,0,.12);
|
box-shadow: 0 2px 12px rgba(0,0,0,.12);
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
@@ -666,4 +773,16 @@ export default {
|
|||||||
&:hover { background-color: #fef0f0; }
|
&:hover { background-color: #fef0f0; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.op-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-actions ::v-deep .el-button--text {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
<el-input v-model="query.employeeId" size="small" placeholder="员工ID" clearable style="width: 140px" @keyup.enter.native="loadList" />
|
<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">
|
<!-- 状态筛选:当前阶段为保证统计页能直接聚合展示,临时停用 status 参数筛选 -->
|
||||||
|
<!-- <el-select v-model="query.status" size="small" placeholder="状态" clearable style="width: 110px" @change="loadList">
|
||||||
<el-option label="草稿" :value="'0'" />
|
<el-option label="草稿" :value="'0'" />
|
||||||
<el-option label="已确认" :value="'1'" />
|
<el-option label="已确认" :value="'1'" />
|
||||||
<el-option label="已归档" :value="'2'" />
|
<el-option label="已归档" :value="'2'" />
|
||||||
</el-select>
|
</el-select> -->
|
||||||
<el-button size="small" type="primary" icon="el-icon-search" @click="loadList">查询</el-button>
|
<el-button size="small" type="primary" icon="el-icon-search" @click="loadList">查询</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
@@ -22,11 +23,12 @@
|
|||||||
<el-table :data="list" stripe size="small" v-loading="loading" empty-text="暂无薪资记录" class="inline-edit-table" border>
|
<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="employeeId" label="员工ID" width="90" /> -->
|
||||||
<el-table-column prop="status" label="状态" width="70" align="center">
|
<!-- 状态列:同上,临时停用 -->
|
||||||
|
<!-- <el-table-column prop="status" label="状态" width="70" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-tag :type="statusType(scope.row.status)" size="small">{{ statusLabel(scope.row.status) }}</el-tag>
|
<el-tag :type="statusType(scope.row.status)" size="small">{{ statusLabel(scope.row.status) }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column> -->
|
||||||
|
|
||||||
<!-- 薪资基数 -->
|
<!-- 薪资基数 -->
|
||||||
<el-table-column label="底薪" width="90" align="right"><template slot-scope="s">
|
<el-table-column label="底薪" width="90" align="right"><template slot-scope="s">
|
||||||
@@ -166,7 +168,7 @@ export default {
|
|||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
employeeId: '',
|
employeeId: '',
|
||||||
status: ''
|
// status: ''
|
||||||
},
|
},
|
||||||
deductionItems: [],
|
deductionItems: [],
|
||||||
bonusItems: [],
|
bonusItems: [],
|
||||||
@@ -181,12 +183,12 @@ export default {
|
|||||||
fmt(val) {
|
fmt(val) {
|
||||||
return val != null ? val : '-'
|
return val != null ? val : '-'
|
||||||
},
|
},
|
||||||
statusType(status) {
|
// statusType(status) {
|
||||||
return { '0': 'info', '1': 'warning', '2': 'success' }[status] || 'info'
|
// return { '0': 'info', '1': 'warning', '2': 'success' }[status] || 'info'
|
||||||
},
|
// },
|
||||||
statusLabel(status) {
|
// statusLabel(status) {
|
||||||
return { '0': '草稿', '1': '已确认', '2': '已归档' }[status] || '草稿'
|
// return { '0': '草稿', '1': '已确认', '2': '已归档' }[status] || '草稿'
|
||||||
},
|
// },
|
||||||
onDeptChange() {
|
onDeptChange() {
|
||||||
this.loadItemConfig()
|
this.loadItemConfig()
|
||||||
this.loadList()
|
this.loadList()
|
||||||
@@ -236,8 +238,7 @@ export default {
|
|||||||
pageSize: this.query.pageSize,
|
pageSize: this.query.pageSize,
|
||||||
deptId: this.deptId,
|
deptId: this.deptId,
|
||||||
period: this.period,
|
period: this.period,
|
||||||
employeeId: this.query.employeeId || undefined,
|
employeeId: this.query.employeeId || undefined
|
||||||
status: this.query.status || undefined
|
|
||||||
}
|
}
|
||||||
listPerfSalary(params).then(res => {
|
listPerfSalary(params).then(res => {
|
||||||
this.list = (res.rows || []).map(r => {
|
this.list = (res.rows || []).map(r => {
|
||||||
|
|||||||
@@ -7,113 +7,373 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="summary">
|
<template v-if="summary">
|
||||||
<!-- Overview Cards -->
|
<div class="page-title">
|
||||||
<div class="stat-cards">
|
<span>{{ titleText }}</span>
|
||||||
<div class="stat-card">
|
<el-button size="mini" type="primary" :loading="saving" @click="saveSummary">保存汇总</el-button>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Salary Breakdown Card -->
|
<el-row :gutter="10" class="mb8">
|
||||||
<div class="detail-card">
|
<el-col :span="4">
|
||||||
<div class="card-title">薪资构成</div>
|
<el-card shadow="never" class="stat-card">
|
||||||
<div class="detail-grid">
|
<div class="stat-item">
|
||||||
<div class="detail-item">
|
<div class="stat-value">{{ fmtInt(summary.totalEmployees) }}</div>
|
||||||
<span class="detail-label">底薪合计</span>
|
<div class="stat-label">总人数</div>
|
||||||
<span class="detail-value">{{ fmt(summary.totalBaseSalary) }}</span>
|
</div>
|
||||||
</div>
|
</el-card>
|
||||||
<div class="detail-item">
|
</el-col>
|
||||||
<span class="detail-label">绩效工资合计</span>
|
<el-col :span="4">
|
||||||
<span class="detail-value highlight">{{ fmt(summary.totalPerfWage) }}</span>
|
<el-card shadow="never" class="stat-card">
|
||||||
</div>
|
<div class="stat-item">
|
||||||
<div class="detail-item">
|
<div class="stat-value stat-value--primary">{{ fmtMoney(payableSalary) }}</div>
|
||||||
<span class="detail-label">扣款合计</span>
|
<div class="stat-label">应发总薪</div>
|
||||||
<span class="detail-value danger">{{ fmt(summary.totalDeductions) }}</span>
|
</div>
|
||||||
</div>
|
</el-card>
|
||||||
<div class="detail-item">
|
</el-col>
|
||||||
<span class="detail-label">奖励合计</span>
|
<el-col :span="4">
|
||||||
<span class="detail-value success">{{ fmt(summary.totalBonuses) }}</span>
|
<el-card shadow="never" class="stat-card">
|
||||||
</div>
|
<div class="stat-item">
|
||||||
<div class="detail-item">
|
<div class="stat-value stat-value--primary">{{ fmtMoney(summary.totalActualSalary) }}</div>
|
||||||
<span class="detail-label">其他合计</span>
|
<div class="stat-label">实发总薪</div>
|
||||||
<span class="detail-value">{{ fmt(summary.totalOther) }}</span>
|
</div>
|
||||||
</div>
|
</el-card>
|
||||||
<div class="detail-item">
|
</el-col>
|
||||||
<span class="detail-label">实领薪资合计</span>
|
<el-col :span="4">
|
||||||
<span class="detail-value total">{{ fmt(summary.totalActualSalary) }}</span>
|
<el-card shadow="never" class="stat-card">
|
||||||
</div>
|
<div class="stat-item">
|
||||||
<div class="detail-item">
|
<div class="stat-value">{{ fmtMoney(summary.avgActualSalary) }}</div>
|
||||||
<span class="detail-label">人均实领薪资</span>
|
<div class="stat-label">人均薪资</div>
|
||||||
<span class="detail-value highlight">{{ fmt(summary.avgActualSalary) }}</span>
|
</div>
|
||||||
</div>
|
</el-card>
|
||||||
</div>
|
</el-col>
|
||||||
</div>
|
<el-col :span="4">
|
||||||
|
<el-card shadow="never" class="stat-card">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value stat-value--danger">{{ fmtMoney(summary.totalDeductions) }}</div>
|
||||||
|
<div class="stat-label">扣款总额</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-card shadow="never" class="stat-card">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value stat-value--success">{{ fmtMoney(summary.totalBonuses) }}</div>
|
||||||
|
<div class="stat-label">奖励总额</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
<!-- Status -->
|
<el-row :gutter="10" class="mb8">
|
||||||
<div class="status-bar">
|
<el-col :span="8">
|
||||||
<span class="status-label">状态</span>
|
<el-card shadow="never" class="mini-card">
|
||||||
<el-tag :type="summary.status === 1 ? 'success' : 'info'" size="small">
|
<div class="mini-row">
|
||||||
{{ summary.status === 1 ? '已确认' : '草稿' }}
|
<span class="mini-label">薪资记录数</span>
|
||||||
</el-tag>
|
<span class="mini-value">{{ fmtInt(salaryStats.total) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="never" class="mini-card">
|
||||||
|
<div class="mini-row">
|
||||||
|
<span class="mini-label">当月确认数</span>
|
||||||
|
<span class="mini-value">{{ fmtInt(salaryStats.confirmed) }}</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="never" class="mini-card">
|
||||||
|
<div class="mini-row">
|
||||||
|
<span class="mini-label">状态</span>
|
||||||
|
<span class="mini-value">{{ summary.status === 1 ? '已确认' : '草稿' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-card shadow="never" class="chart-card">
|
||||||
|
<div slot="header" class="chart-header">
|
||||||
|
<span class="chart-title">薪资分布</span>
|
||||||
|
</div>
|
||||||
|
<div ref="salaryChart" class="salary-chart" />
|
||||||
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { listPerfDeptSummary } from '@/api/perf/deptSummary'
|
import { listPerfDeptSummary } from '@/api/perf/deptSummary'
|
||||||
|
import { listPerfSalary } from '@/api/perf/salary'
|
||||||
|
import { listEmployeeInfo } from '@/api/wms/employeeInfo'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import { savePerfDeptSummaryByPeriod } from '@/api/perf/deptSummary'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SummaryPanel',
|
name: 'SummaryPanel',
|
||||||
props: {
|
props: {
|
||||||
deptId: { type: [Number, String], default: null },
|
deptId: { type: [Number, String], default: null },
|
||||||
|
deptName: { type: String, default: '' },
|
||||||
period: { type: String, default: '' }
|
period: { type: String, default: '' }
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
summary: null
|
saving: false,
|
||||||
|
// 汇总数据来源:
|
||||||
|
// 1) 优先后端汇总表 perf_dept_summary
|
||||||
|
// 2) 若汇总表为空但薪资明细有数据,则在前端基于 perf_salary 聚合生成 summary
|
||||||
|
summary: null,
|
||||||
|
// 薪资明细列表(用于图表展示、以及汇总表缺失时的前端聚合兜底)
|
||||||
|
salaryList: [],
|
||||||
|
// 员工ID -> 员工姓名 映射(用于图表显示更友好)
|
||||||
|
employeeNameMap: {},
|
||||||
|
chart: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
titleText() {
|
||||||
|
const name = this.deptName || '车间'
|
||||||
|
return `${name} · ${this.period} 车间汇总`
|
||||||
|
},
|
||||||
|
// 应发总薪(展示层计算口径)
|
||||||
|
payableSalary() {
|
||||||
|
if (!this.summary) return null
|
||||||
|
const base = this.toNumber(this.summary.totalBaseSalary)
|
||||||
|
const perf = this.toNumber(this.summary.totalPerfWage)
|
||||||
|
const ded = this.toNumber(this.summary.totalDeductions)
|
||||||
|
const bon = this.toNumber(this.summary.totalBonuses)
|
||||||
|
const oth = this.toNumber(this.summary.totalOther)
|
||||||
|
return base + perf + bon + oth - ded
|
||||||
|
},
|
||||||
|
salaryStats() {
|
||||||
|
const total = this.salaryList.length
|
||||||
|
// 当前阶段已临时停用“状态”筛选与状态列,因此确认数直接按总数展示
|
||||||
|
const confirmed = total
|
||||||
|
return { total, confirmed }
|
||||||
|
},
|
||||||
|
salaryChartData() {
|
||||||
|
// 取实领薪资 Top12(从低到高展示),用于横向柱状图
|
||||||
|
const list = (this.salaryList || []).map(r => {
|
||||||
|
const id = r.employeeId != null ? String(r.employeeId) : ''
|
||||||
|
const name = this.employeeNameMap[id] || id || '-'
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
value: this.toNumber(r.actualSalary)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const top = list.sort((a, b) => b.value - a.value).slice(0, 12)
|
||||||
|
top.reverse()
|
||||||
|
return top
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
deptId: { immediate: true, handler: 'loadSummary' },
|
deptId: { immediate: true, handler: 'loadAll' },
|
||||||
period: 'loadSummary'
|
period: 'loadAll',
|
||||||
|
deptName: 'loadAll',
|
||||||
|
salaryChartData() {
|
||||||
|
this.$nextTick(() => this.renderChart())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
window.addEventListener('resize', this.handleResize)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.handleResize)
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.dispose()
|
||||||
|
this.chart = null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fmt(val) {
|
toNumber(val) {
|
||||||
return val != null ? val : '-'
|
if (val == null || val === '') return 0
|
||||||
|
const n = typeof val === 'number' ? val : parseFloat(val)
|
||||||
|
return isNaN(n) ? 0 : n
|
||||||
},
|
},
|
||||||
loadSummary() {
|
fmtInt(val) {
|
||||||
|
if (val == null || val === '') return '-'
|
||||||
|
const n = typeof val === 'number' ? val : parseFloat(val)
|
||||||
|
if (isNaN(n)) return '-'
|
||||||
|
return String(Math.round(n))
|
||||||
|
},
|
||||||
|
fmtMoney(val) {
|
||||||
|
if (val == null || val === '') return '-'
|
||||||
|
const n = this.toNumber(val)
|
||||||
|
return new Intl.NumberFormat('zh-CN', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'CNY',
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
}).format(n)
|
||||||
|
},
|
||||||
|
handleResize() {
|
||||||
|
if (this.chart) this.chart.resize()
|
||||||
|
},
|
||||||
|
// 将当前页面展示的汇总数据落库到 perf_dept_summary
|
||||||
|
// 后端按 deptId + period 做新增或更新(无须前端关心是否已有记录)
|
||||||
|
saveSummary() {
|
||||||
|
if (!this.summary) return
|
||||||
|
this.saving = true
|
||||||
|
const payload = {
|
||||||
|
id: this.summary.id,
|
||||||
|
deptId: this.deptId,
|
||||||
|
period: this.period,
|
||||||
|
totalEmployees: this.summary.totalEmployees,
|
||||||
|
totalCoeffSum: this.summary.totalCoeffSum,
|
||||||
|
avgPerfScore: this.summary.avgPerfScore,
|
||||||
|
avgPerfCoeff: this.summary.avgPerfCoeff,
|
||||||
|
totalBaseSalary: this.summary.totalBaseSalary,
|
||||||
|
totalPerfWage: this.summary.totalPerfWage,
|
||||||
|
totalDeductions: this.summary.totalDeductions,
|
||||||
|
totalBonuses: this.summary.totalBonuses,
|
||||||
|
totalOther: this.summary.totalOther,
|
||||||
|
totalActualSalary: this.summary.totalActualSalary,
|
||||||
|
avgActualSalary: this.summary.avgActualSalary,
|
||||||
|
monthlyProduction: this.summary.monthlyProduction,
|
||||||
|
unitPrice: this.summary.unitPrice,
|
||||||
|
status: this.summary.status,
|
||||||
|
remark: this.summary.remark
|
||||||
|
}
|
||||||
|
savePerfDeptSummaryByPeriod(payload).then(() => {
|
||||||
|
this.$message.success('保存成功')
|
||||||
|
this.loadAll()
|
||||||
|
}).finally(() => {
|
||||||
|
this.saving = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loadAll() {
|
||||||
if (!this.deptId || !this.period) return
|
if (!this.deptId || !this.period) return
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.summary = null
|
this.summary = null
|
||||||
listPerfDeptSummary({ deptId: this.deptId, period: this.period }).then(res => {
|
this.salaryList = []
|
||||||
const rows = res.rows || []
|
this.employeeNameMap = {}
|
||||||
|
|
||||||
|
// p1: 汇总表(可能为空)
|
||||||
|
// p2: 薪资明细(用于图表,也用于汇总表缺失时的前端聚合)
|
||||||
|
// p3: 员工列表(用于 employeeId -> name 的映射,缺 deptName 时跳过)
|
||||||
|
const p1 = listPerfDeptSummary({ deptId: this.deptId, period: this.period })
|
||||||
|
const p2 = listPerfSalary({ deptId: this.deptId, period: this.period, pageSize: 9999 })
|
||||||
|
const p3 = this.deptName
|
||||||
|
? listEmployeeInfo({ dept: this.deptName, pageSize: 9999 })
|
||||||
|
: Promise.resolve({ rows: [] })
|
||||||
|
|
||||||
|
Promise.all([p1, p2, p3]).then(([summaryRes, salaryRes, empRes]) => {
|
||||||
|
const rows = summaryRes.rows || []
|
||||||
this.summary = rows.length > 0 ? rows[0] : null
|
this.summary = rows.length > 0 ? rows[0] : null
|
||||||
|
|
||||||
|
this.salaryList = salaryRes.rows || []
|
||||||
|
|
||||||
|
const map = {}
|
||||||
|
const empRows = empRes.rows || []
|
||||||
|
empRows.forEach(e => {
|
||||||
|
const id = e.employeeId != null ? String(e.employeeId) : (e.infoId != null ? String(e.infoId) : '')
|
||||||
|
if (id) map[id] = e.name || e.nickName || e.userName || id
|
||||||
|
})
|
||||||
|
this.employeeNameMap = map
|
||||||
|
|
||||||
|
// 后端汇总表未生成时,直接从薪资明细聚合出汇总,保证统计页可用
|
||||||
|
if (!this.summary && this.salaryList.length) {
|
||||||
|
this.summary = this.buildSummaryFromSalary()
|
||||||
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
|
this.$nextTick(() => this.renderChart())
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
// 前端聚合汇总(仅在 perf_dept_summary 为空时触发)
|
||||||
|
buildSummaryFromSalary() {
|
||||||
|
const salaryList = this.salaryList || []
|
||||||
|
const empSet = new Set()
|
||||||
|
let totalBaseSalary = 0
|
||||||
|
let totalPerfWage = 0
|
||||||
|
let totalDeductions = 0
|
||||||
|
let totalBonuses = 0
|
||||||
|
let totalOther = 0
|
||||||
|
let totalActualSalary = 0
|
||||||
|
|
||||||
|
salaryList.forEach(r => {
|
||||||
|
if (r.employeeId != null) empSet.add(String(r.employeeId))
|
||||||
|
totalBaseSalary += this.toNumber(r.baseSalary)
|
||||||
|
totalPerfWage += this.toNumber(r.perfWage)
|
||||||
|
totalDeductions += this.toNumber(r.deductionsTotal)
|
||||||
|
totalBonuses += this.toNumber(r.bonusesTotal)
|
||||||
|
totalOther += this.toNumber(r.otherAmount)
|
||||||
|
totalActualSalary += this.toNumber(r.actualSalary)
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalEmployees = empSet.size || this.toNumber(this.salaryList.length)
|
||||||
|
const avgActualSalary = totalEmployees ? totalActualSalary / totalEmployees : 0
|
||||||
|
return {
|
||||||
|
deptId: this.deptId,
|
||||||
|
period: this.period,
|
||||||
|
totalEmployees,
|
||||||
|
totalCoeffSum: 0,
|
||||||
|
avgPerfScore: 0,
|
||||||
|
avgPerfCoeff: 0,
|
||||||
|
totalBaseSalary,
|
||||||
|
totalPerfWage,
|
||||||
|
totalDeductions,
|
||||||
|
totalBonuses,
|
||||||
|
totalOther,
|
||||||
|
totalActualSalary,
|
||||||
|
avgActualSalary,
|
||||||
|
monthlyProduction: 0,
|
||||||
|
unitPrice: 0,
|
||||||
|
status: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderChart() {
|
||||||
|
if (!this.summary) return
|
||||||
|
const el = this.$refs.salaryChart
|
||||||
|
if (!el) return
|
||||||
|
if (!this.chart) {
|
||||||
|
this.chart = echarts.init(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = this.salaryChartData
|
||||||
|
if (!data.length) {
|
||||||
|
this.chart.clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chart.setOption({
|
||||||
|
grid: { left: 120, right: 24, top: 10, bottom: 10, containLabel: true },
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: { type: 'shadow' },
|
||||||
|
formatter: params => {
|
||||||
|
const p = Array.isArray(params) ? params[0] : params
|
||||||
|
return `${p.name}<br/>${this.fmtMoney(p.value)}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLabel: {
|
||||||
|
formatter: v => {
|
||||||
|
const n = this.toNumber(v)
|
||||||
|
return n >= 10000 ? (n / 10000).toFixed(1) + 'w' : String(n)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
splitLine: { lineStyle: { color: '#f0f2f5' } }
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: data.map(d => d.name),
|
||||||
|
axisTick: { show: false },
|
||||||
|
axisLine: { show: false },
|
||||||
|
axisLabel: { color: '#606266', fontSize: 12 }
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'bar',
|
||||||
|
data: data.map(d => d.value),
|
||||||
|
barWidth: 14,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#409eff',
|
||||||
|
borderRadius: [2, 2, 2, 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,8 +383,7 @@ export default {
|
|||||||
.summary-panel {
|
.summary-panel {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: #f5f7fa;
|
padding: 12px;
|
||||||
padding: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
@@ -136,105 +395,109 @@ export default {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-cards {
|
.page-title {
|
||||||
display: grid;
|
font-size: 14px;
|
||||||
grid-template-columns: repeat(6, 1fr);
|
font-weight: 600;
|
||||||
gap: 12px;
|
color: #303133;
|
||||||
margin-bottom: 16px;
|
margin: 4px 0 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 20px 16px;
|
box-shadow: none;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
::v-deep .el-card__header {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
::v-deep .el-card__body {
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
margin-bottom: 8px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 22px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
&.highlight {
|
.stat-value--primary {
|
||||||
color: #409eff;
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value--danger {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value--success {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-card {
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: none;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
::v-deep .el-card__header {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
::v-deep .el-card__body {
|
||||||
|
padding: 10px 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-card {
|
.mini-row {
|
||||||
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;
|
display: flex;
|
||||||
flex-direction: column;
|
justify-content: space-between;
|
||||||
gap: 4px;
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #303133;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-label {
|
.mini-label {
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
color: #909399;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-value {
|
.chart-card {
|
||||||
font-size: 16px;
|
border-radius: 4px;
|
||||||
font-weight: 600;
|
box-shadow: none;
|
||||||
color: #303133;
|
::v-deep .el-card__header {
|
||||||
|
padding: 10px 12px;
|
||||||
&.highlight {
|
border-bottom: 1px solid #ebeef5;
|
||||||
color: #409eff;
|
|
||||||
}
|
}
|
||||||
|
::v-deep .el-card__body {
|
||||||
&.danger {
|
padding: 12px;
|
||||||
color: #f56c6c;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.success {
|
|
||||||
color: #67c23a;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.total {
|
|
||||||
color: #e6a23c;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
}
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-bar {
|
.chart-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
justify-content: space-between;
|
||||||
padding: 12px 20px;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-label {
|
.chart-title {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #606266;
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.salary-chart {
|
||||||
|
height: 320px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
<div class="panel-container" v-if="currentDept">
|
<div class="panel-container" v-if="currentDept">
|
||||||
<PerfConfigPanel v-if="activeTab === 'perfConfig'" :dept-id="currentDept.id" :period="periodStr" />
|
<PerfConfigPanel v-if="activeTab === 'perfConfig'" :dept-id="currentDept.id" :period="periodStr" />
|
||||||
<SalaryPanel v-if="activeTab === 'salary'" :dept-id="currentDept.id" :dept-name="currentDept.deptName" :period="periodStr" />
|
<SalaryPanel v-if="activeTab === 'salary'" :dept-id="currentDept.id" :dept-name="currentDept.deptName" :period="periodStr" />
|
||||||
<SummaryPanel v-if="activeTab === 'summary'" :dept-id="currentDept.id" :period="periodStr" />
|
<SummaryPanel v-if="activeTab === 'summary'" :dept-id="currentDept.id" :dept-name="currentDept.deptName" :period="periodStr" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user