2026-07-10 10:43:16 +08:00
|
|
|
|
<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" />
|
2026-07-10 17:25:23 +08:00
|
|
|
|
<!-- 状态筛选:当前阶段为保证统计页能直接聚合展示,临时停用 status 参数筛选 -->
|
|
|
|
|
|
<!-- <el-select v-model="query.status" size="small" placeholder="状态" clearable style="width: 110px" @change="loadList">
|
2026-07-10 10:43:16 +08:00
|
|
|
|
<el-option label="草稿" :value="'0'" />
|
|
|
|
|
|
<el-option label="已确认" :value="'1'" />
|
|
|
|
|
|
<el-option label="已归档" :value="'2'" />
|
2026-07-10 17:25:23 +08:00
|
|
|
|
</el-select> -->
|
2026-07-10 10:43:16 +08:00
|
|
|
|
<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>
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<el-button size="small" type="warning" icon="el-icon-upload" :loading="batchSaving" @click="handleBatchSave">批量保存</el-button>
|
2026-07-10 10:43:16 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<!-- 口径说明 -->
|
|
|
|
|
|
<div class="formula-bar" :class="{ expanded: formulaExpanded }" @click="formulaExpanded = !formulaExpanded">
|
|
|
|
|
|
<span class="formula-bar-title">
|
|
|
|
|
|
<i :class="formulaExpanded ? 'el-icon-arrow-down' : 'el-icon-arrow-right'" />
|
|
|
|
|
|
口径说明
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="formula-bar-hint" v-if="!formulaExpanded">总系数 = (岗位系数+固定系数+调整系数+手动系数+绩效系数) × 车间系数</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="formulaExpanded" class="formula-detail">
|
|
|
|
|
|
<div class="formula-row"><b>绩效系数</b> = 加权总分 ÷ 100(考核后自动同步)</div>
|
|
|
|
|
|
<div class="formula-row"><b>车间系数</b> = 车间固定系数(默认 1.0,不参与加法,作为整体乘数)</div>
|
|
|
|
|
|
<div class="formula-row"><b>总系数</b> = (岗位系数 + 固定系数 + 调整系数 + 手动系数 + 绩效系数) × 车间系数</div>
|
|
|
|
|
|
<div class="formula-row"><b>绩效工资</b> = 绩效基数 × 总系数</div>
|
|
|
|
|
|
<div class="formula-row"><b>实领薪资</b> = 底薪 + 绩效工资 + 奖励合计 − 扣款合计 + 其他金额(自动计算)</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-10 10:43:16 +08:00
|
|
|
|
<!-- 表格 -->
|
|
|
|
|
|
<div class="table-wrap">
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<el-table ref="salaryTable" :data="list" stripe size="small" v-loading="loading" empty-text="暂无薪资记录" class="inline-edit-table" border>
|
|
|
|
|
|
<!-- 薪资基数 -->
|
|
|
|
|
|
<el-table-column label="姓名" width="70" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<span class="cell-clickable" @click.stop="toggleRowExpand(scope.row)">{{ scope.row.wmsEmployeeInfo ? scope.row.wmsEmployeeInfo.name : '-' }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="岗位" width="70" align="center">
|
2026-07-10 10:43:16 +08:00
|
|
|
|
<template slot-scope="scope">
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<span class="cell-clickable" @click.stop="toggleRowExpand(scope.row)">{{ scope.row.wmsEmployeeInfo ? scope.row.wmsEmployeeInfo.jobType : '-' }}</span>
|
2026-07-10 10:43:16 +08:00
|
|
|
|
</template>
|
2026-07-11 14:53:42 +08:00
|
|
|
|
</el-table-column>
|
2026-07-10 10:43:16 +08:00
|
|
|
|
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<el-table-column label="底薪" width="60" align="right"><template slot-scope="s">
|
|
|
|
|
|
<input v-model="s.row.baseSalary" class="cell-input cell-input--base" @change="recalcSalary(s.row); markDirty(s.row)" />
|
2026-07-10 10:43:16 +08:00
|
|
|
|
</template></el-table-column>
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<el-table-column label="绩效基数" width="80" align="right"><template slot-scope="s">
|
2026-07-10 10:43:16 +08:00
|
|
|
|
<input v-model="s.row.perfBase" class="cell-input cell-input--base" @change="markDirty(s.row)" />
|
|
|
|
|
|
</template></el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 考核得分 -->
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<el-table-column label="考核得分" width="80" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<span class="cell-clickable cell-clickable--score" @click.stop="toggleRowExpand(scope.row)">{{ scope.row.perfScore != null ? scope.row.perfScore : '-' }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-10 10:43:16 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 系数 -->
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<el-table-column label="绩效系数" width="80" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<span class="cell-clickable cell-clickable--coeff" @click.stop="toggleRowExpand(scope.row)">{{ scope.row.perfCoeff != null ? scope.row.perfCoeff : '-' }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="车间系数" width="80" align="right"><template slot-scope="s">
|
|
|
|
|
|
<input v-model="s.row.deptCoeff" class="cell-input cell-input--coeff" @change="recalcCoeffs(s.row); markDirty(s.row)" />
|
2026-07-10 10:43:16 +08:00
|
|
|
|
</template></el-table-column>
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<el-table-column label="岗位系数" width="80" align="right"><template slot-scope="s">
|
|
|
|
|
|
<input v-model="s.row.posCoeff" class="cell-input cell-input--coeff" @change="recalcCoeffs(s.row); markDirty(s.row)" />
|
2026-07-10 10:43:16 +08:00
|
|
|
|
</template></el-table-column>
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<el-table-column label="固定系数" width="80" align="right"><template slot-scope="s">
|
|
|
|
|
|
<input v-model="s.row.fixedCoeff" class="cell-input cell-input--coeff" @change="recalcCoeffs(s.row); markDirty(s.row)" />
|
2026-07-10 10:43:16 +08:00
|
|
|
|
</template></el-table-column>
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<el-table-column label="调整系数" width="80" align="right"><template slot-scope="s">
|
|
|
|
|
|
<input v-model="s.row.adjCoeff" class="cell-input cell-input--coeff" @change="recalcCoeffs(s.row); markDirty(s.row)" />
|
2026-07-10 10:43:16 +08:00
|
|
|
|
</template></el-table-column>
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<!-- 手动系数 隐藏,由考核得分自动覆盖 -->
|
|
|
|
|
|
<!-- <el-table-column label="手动系数" width="80" align="right"><template slot-scope="s">
|
2026-07-10 10:43:16 +08:00
|
|
|
|
<input v-model="s.row.adjCoeffManual" class="cell-input cell-input--coeff" @change="markDirty(s.row)" />
|
2026-07-11 14:53:42 +08:00
|
|
|
|
</template></el-table-column> -->
|
|
|
|
|
|
<el-table-column label="总系数" width="70" align="right">
|
|
|
|
|
|
<template slot-scope="scope"><span class="cell-readonly">{{ fmt(scope.row.totalCoeff) }}</span></template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-10 10:43:16 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 绩效工资 -->
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<el-table-column label="绩效工资" width="80" align="right">
|
|
|
|
|
|
<template slot-scope="scope"><span class="cell-readonly">{{ fmt(scope.row.perfWage) }}</span></template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-10 10:43:16 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 动态扣款列 -->
|
|
|
|
|
|
<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>
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<el-table-column label="扣款合计" width="80" align="right">
|
2026-07-10 10:43:16 +08:00
|
|
|
|
<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>
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<el-table-column label="奖励合计" width="80" align="right">
|
2026-07-10 10:43:16 +08:00
|
|
|
|
<template slot-scope="scope"><span class="cell-readonly cell-readonly--bonus">{{ scope.row._bonusesTotal }}</span></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 金额 -->
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<el-table-column label="其他金额" width="80" align="right"><template slot-scope="s">
|
|
|
|
|
|
<input v-model="s.row.otherAmount" class="cell-input cell-input--amount" @change="recalcSalary(s.row); markDirty(s.row)" />
|
2026-07-10 10:43:16 +08:00
|
|
|
|
</template></el-table-column>
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<el-table-column label="实领薪资" width="90" align="right">
|
|
|
|
|
|
<template slot-scope="scope"><span class="cell-readonly">{{ fmt(scope.row.actualSalary) }}</span></template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-10 10:43:16 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 参考值 -->
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<!-- <el-table-column label="固薪参考" width="90" align="right"><template slot-scope="s">
|
2026-07-10 10:43:16 +08:00
|
|
|
|
<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)" />
|
2026-07-11 14:53:42 +08:00
|
|
|
|
</template></el-table-column> -->
|
2026-07-10 10:43:16 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 备注 -->
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<el-table-column label="备注" min-width="80"><template slot-scope="s">
|
2026-07-10 10:43:16 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-07-11 14:53:42 +08:00
|
|
|
|
<!-- 绩效考核明细展开面板 -->
|
|
|
|
|
|
<div v-if="expandedRow" class="expand-panel" v-loading="appLoading">
|
|
|
|
|
|
<div class="expand-panel-header">
|
|
|
|
|
|
<span class="expand-panel-title">
|
|
|
|
|
|
绩效考核评分 —
|
|
|
|
|
|
{{ expandedRow.wmsEmployeeInfo ? expandedRow.wmsEmployeeInfo.name : expandedRow.employeeId }}
|
|
|
|
|
|
({{ expandedRow.wmsEmployeeInfo ? expandedRow.wmsEmployeeInfo.jobType : '' }})
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<el-tag v-if="expandedRow._appraisal" :type="expandedRow._appraisal.status === '1' ? 'warning' : 'info'" size="mini">
|
|
|
|
|
|
{{ expandedRow._appraisal.status === '1' ? '已确认' : '草稿' }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
<span v-if="expandedRow._appraisal" class="expand-panel-score">
|
|
|
|
|
|
加权总分:{{ weightedTotal }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<el-button size="mini" type="success" :loading="saveLoading" @click="handleSaveScores">保存</el-button>
|
|
|
|
|
|
<el-button type="text" size="mini" icon="el-icon-close" @click="handleCollapseExpand" style="margin-left:auto">收起</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-table v-if="expandedRow._appDetails && expandedRow._appDetails.length" :data="expandedRow._appDetails" size="mini" border class="expand-detail-table">
|
|
|
|
|
|
<el-table-column prop="dimName" label="考核维度" width="90" />
|
|
|
|
|
|
<el-table-column prop="quantitativeIndicator" label="考核指标" min-width="100" />
|
|
|
|
|
|
<el-table-column label="实际值" width="100" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<input v-model="scope.row.actualValue" class="cell-input cell-input--actual" @change="markDetailDirty(scope.row)" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="targetValue" label="目标值" width="80" align="center" />
|
|
|
|
|
|
<el-table-column label="得分" width="80" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<input v-model="scope.row.dimScore" class="cell-input cell-input--score" placeholder="0" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="加分" width="70" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<input v-model="scope.row.bonus" class="cell-input cell-input--bonus" placeholder="0" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="扣分" width="70" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<input v-model="scope.row.penalty" class="cell-input cell-input--penalty" placeholder="0" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="weight" label="权重" width="70" align="center" />
|
|
|
|
|
|
<el-table-column label="总分" width="80" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<span class="cell-subtotal">{{ subtotal(scope.row) }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="原始总分" width="80" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<span class="cell-rawtotal">{{ rawRowTotal(scope.row) }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="scoreRule" label="评分规则" min-width="120" show-overflow-tooltip />
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<div v-else-if="expandedRow._appLoaded && !appLoading" class="expand-empty">暂无绩效考核数据</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 一键生成弹窗 -->
|
|
|
|
|
|
<GenerateDialog
|
|
|
|
|
|
:visible.sync="showGenerateDialog"
|
|
|
|
|
|
:dept-id="deptId"
|
|
|
|
|
|
:dept-name="deptName"
|
|
|
|
|
|
:period="period"
|
|
|
|
|
|
@done="onGenerateDone"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-07-10 10:43:16 +08:00
|
|
|
|
<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'
|
2026-07-11 14:53:42 +08:00
|
|
|
|
import { listPerfAppraisal, updatePerfAppraisal } from '@/api/perf/appraisal'
|
|
|
|
|
|
import { listPerfAppraisalDetail, updatePerfAppraisalDetail, batchUpdatePerfAppraisalDetail } from '@/api/perf/appraisalDetail'
|
2026-07-10 10:43:16 +08:00
|
|
|
|
import { listPerfDeptItemConfig } from '@/api/perf/deptItemConfig'
|
2026-07-11 14:53:42 +08:00
|
|
|
|
import { listPerfDeptConfig } from '@/api/perf/deptConfig'
|
|
|
|
|
|
import GenerateDialog from './GenerateDialog.vue'
|
|
|
|
|
|
import { calculateTotal } from '@/utils/scoreEngine'
|
|
|
|
|
|
import { fetchProductionData } from '@/utils/perfDataService'
|
2026-07-10 10:43:16 +08:00
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'SalaryPanel',
|
2026-07-11 14:53:42 +08:00
|
|
|
|
components: { GenerateDialog },
|
2026-07-10 10:43:16 +08:00
|
|
|
|
props: {
|
|
|
|
|
|
deptId: { type: [Number, String], default: null },
|
|
|
|
|
|
deptName: { type: String, default: '' },
|
2026-07-11 14:53:42 +08:00
|
|
|
|
period: { type: String, default: '' },
|
|
|
|
|
|
productionLine: { type: String, default: '' }
|
2026-07-10 10:43:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
list: [],
|
|
|
|
|
|
total: 0,
|
|
|
|
|
|
query: {
|
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
|
employeeId: '',
|
2026-07-10 17:25:23 +08:00
|
|
|
|
// status: ''
|
2026-07-10 10:43:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
deductionItems: [],
|
|
|
|
|
|
bonusItems: [],
|
2026-07-11 14:53:42 +08:00
|
|
|
|
generating: false,
|
|
|
|
|
|
batchSaving: false,
|
|
|
|
|
|
showGenerateDialog: false,
|
|
|
|
|
|
expandedRow: null,
|
|
|
|
|
|
appLoading: false,
|
|
|
|
|
|
calcLoading: false,
|
|
|
|
|
|
saveLoading: false,
|
|
|
|
|
|
_prodCache: null,
|
|
|
|
|
|
formulaExpanded: false,
|
|
|
|
|
|
_defaultDeptCoeff: 1
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
weightedTotal() {
|
|
|
|
|
|
if (!this.expandedRow || !this.expandedRow._appDetails || !this.expandedRow._appDetails.length) return '-'
|
|
|
|
|
|
let sum = 0
|
|
|
|
|
|
this.expandedRow._appDetails.forEach(d => {
|
|
|
|
|
|
sum += (parseFloat(d.dimScore) || 0) * (parseFloat(d.weight) || 0) / 100
|
|
|
|
|
|
})
|
|
|
|
|
|
const bonus = this.expandedRow._appDetails.reduce((s, d) => s + (parseFloat(d.bonus) || 0), 0)
|
|
|
|
|
|
const penalty = this.expandedRow._appDetails.reduce((s, d) => s + (parseFloat(d.penalty) || 0), 0)
|
|
|
|
|
|
const total = Math.min(120, Math.max(0, sum + bonus - penalty))
|
|
|
|
|
|
return total.toFixed(1)
|
2026-07-10 10:43:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
watch: {
|
|
|
|
|
|
deptId: { immediate: true, handler: 'onDeptChange' },
|
|
|
|
|
|
period: 'onDeptChange'
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
fmt(val) {
|
|
|
|
|
|
return val != null ? val : '-'
|
|
|
|
|
|
},
|
2026-07-10 17:25:23 +08:00
|
|
|
|
// statusType(status) {
|
|
|
|
|
|
// return { '0': 'info', '1': 'warning', '2': 'success' }[status] || 'info'
|
|
|
|
|
|
// },
|
|
|
|
|
|
// statusLabel(status) {
|
|
|
|
|
|
// return { '0': '草稿', '1': '已确认', '2': '已归档' }[status] || '草稿'
|
|
|
|
|
|
// },
|
2026-07-10 10:43:16 +08:00
|
|
|
|
onDeptChange() {
|
|
|
|
|
|
this.loadItemConfig()
|
|
|
|
|
|
this.loadList()
|
|
|
|
|
|
},
|
|
|
|
|
|
loadItemConfig() {
|
|
|
|
|
|
if (!this.deptId || !this.period) return
|
2026-07-11 14:53:42 +08:00
|
|
|
|
// 加载扣款/奖励项目
|
2026-07-10 10:43:16 +08:00
|
|
|
|
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 = []
|
|
|
|
|
|
})
|
2026-07-11 14:53:42 +08:00
|
|
|
|
// 加载考核参数获取车间系数默认值
|
|
|
|
|
|
listPerfDeptConfig({ deptId: this.deptId, month: this.period + '-01', pageNum: 1, pageSize: 1 }).then(res => {
|
|
|
|
|
|
const rows = res.rows || []
|
|
|
|
|
|
this._defaultDeptCoeff = rows.length ? (rows[0].deptCoeff || 1) : 1
|
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
|
this._defaultDeptCoeff = 1
|
|
|
|
|
|
})
|
2026-07-10 10:43:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
/* 解析扣款/奖励 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)
|
|
|
|
|
|
},
|
2026-07-11 14:53:42 +08:00
|
|
|
|
/** 系数变化时自动重算总系数和绩效工资 */
|
|
|
|
|
|
recalcCoeffs(row) {
|
|
|
|
|
|
const pos = parseFloat(row.posCoeff) || 0
|
|
|
|
|
|
const fixed = parseFloat(row.fixedCoeff) || 0
|
|
|
|
|
|
const adj = parseFloat(row.adjCoeff) || 0
|
|
|
|
|
|
const adjManual = parseFloat(row.adjCoeffManual) || 0
|
|
|
|
|
|
const perf = parseFloat(row.perfCoeff) || 0
|
|
|
|
|
|
const dept = parseFloat(row.deptCoeff) || 1
|
|
|
|
|
|
const total = (pos + fixed + adj + adjManual + perf) * dept
|
|
|
|
|
|
this.$set(row, 'totalCoeff', total.toFixed(2))
|
|
|
|
|
|
const perfBase = parseFloat(row.perfBase) || 0
|
|
|
|
|
|
this.$set(row, 'perfWage', (perfBase * total).toFixed(2))
|
|
|
|
|
|
this.recalcSalary(row)
|
|
|
|
|
|
},
|
|
|
|
|
|
/** 自动计算实领薪资 */
|
|
|
|
|
|
recalcSalary(row) {
|
|
|
|
|
|
const baseSalary = parseFloat(row.baseSalary) || 0
|
|
|
|
|
|
const perfWage = parseFloat(row.perfWage) || 0
|
|
|
|
|
|
const bonuses = parseFloat(row._bonusesTotal) || 0
|
|
|
|
|
|
const deductions = parseFloat(row._deductionsTotal) || 0
|
|
|
|
|
|
const other = parseFloat(row.otherAmount) || 0
|
|
|
|
|
|
const actual = baseSalary + perfWage + bonuses - deductions + other
|
|
|
|
|
|
this.$set(row, 'actualSalary', actual.toFixed(2))
|
|
|
|
|
|
},
|
|
|
|
|
|
onDeductChange(row) { this.recalcTotals(row); this.recalcSalary(row) },
|
|
|
|
|
|
onBonusChange(row) { this.recalcTotals(row); this.recalcSalary(row) },
|
2026-07-10 10:43:16 +08:00
|
|
|
|
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,
|
2026-07-10 17:25:23 +08:00
|
|
|
|
employeeId: this.query.employeeId || undefined
|
2026-07-10 10:43:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
listPerfSalary(params).then(res => {
|
|
|
|
|
|
this.list = (res.rows || []).map(r => {
|
|
|
|
|
|
this.$set(r, '_dirty', false)
|
|
|
|
|
|
this.$set(r, '_saving', false)
|
2026-07-11 14:53:42 +08:00
|
|
|
|
// 车间系数默认从考核参数获取,否则为 1
|
|
|
|
|
|
if (!r.deptCoeff || parseFloat(r.deptCoeff) === 0) {
|
|
|
|
|
|
this.$set(r, 'deptCoeff', (this._defaultDeptCoeff || 1).toString())
|
|
|
|
|
|
}
|
2026-07-10 10:43:16 +08:00
|
|
|
|
this.parseRowItems(r)
|
2026-07-11 14:53:42 +08:00
|
|
|
|
this.recalcCoeffs(r)
|
|
|
|
|
|
this.recalcSalary(r)
|
2026-07-10 10:43:16 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-07-11 14:53:42 +08:00
|
|
|
|
this.showGenerateDialog = true
|
|
|
|
|
|
},
|
|
|
|
|
|
onGenerateDone() {
|
|
|
|
|
|
this.loadList()
|
|
|
|
|
|
this.$message.success('薪资记录生成完成')
|
|
|
|
|
|
},
|
|
|
|
|
|
/** 批量保存所有已修改的行 */
|
|
|
|
|
|
handleBatchSave() {
|
|
|
|
|
|
const dirtyRows = this.list.filter(r => r._dirty)
|
|
|
|
|
|
if (dirtyRows.length === 0) {
|
|
|
|
|
|
this.$message.info('没有需要保存的修改')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.batchSaving = true
|
|
|
|
|
|
const tasks = dirtyRows.map(row => {
|
|
|
|
|
|
this.$set(row, '_saving', true)
|
|
|
|
|
|
const data = { ...row }
|
|
|
|
|
|
data.deductionsJson = JSON.stringify(row._d || {})
|
|
|
|
|
|
data.bonusesJson = JSON.stringify(row._b || {})
|
|
|
|
|
|
data.deductionsTotal = row._deductionsTotal
|
|
|
|
|
|
data.bonusesTotal = row._bonusesTotal
|
|
|
|
|
|
delete data._d; delete data._b; delete data._dirty; delete data._saving
|
|
|
|
|
|
delete data._deductionsTotal; delete data._bonusesTotal
|
|
|
|
|
|
// 清除前端扩展字段
|
|
|
|
|
|
delete data.wmsEmployeeInfo
|
|
|
|
|
|
delete data._appLoaded; delete data._appraisal; delete data._appDetails
|
|
|
|
|
|
const api = data.id ? updatePerfSalary : addPerfSalary
|
|
|
|
|
|
return api(data).then(res => {
|
|
|
|
|
|
if (!data.id && res.data) {
|
|
|
|
|
|
this.$set(row, 'id', res.data.id || res.data)
|
2026-07-10 10:43:16 +08:00
|
|
|
|
}
|
2026-07-11 14:53:42 +08:00
|
|
|
|
this.$set(row, '_dirty', false)
|
|
|
|
|
|
return { ok: true }
|
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
|
return { ok: false }
|
2026-07-10 10:43:16 +08:00
|
|
|
|
}).finally(() => {
|
2026-07-11 14:53:42 +08:00
|
|
|
|
this.$set(row, '_saving', false)
|
2026-07-10 10:43:16 +08:00
|
|
|
|
})
|
2026-07-11 14:53:42 +08:00
|
|
|
|
})
|
|
|
|
|
|
Promise.all(tasks).then(results => {
|
|
|
|
|
|
const ok = results.filter(r => r.ok).length
|
|
|
|
|
|
const fail = results.filter(r => !r.ok).length
|
|
|
|
|
|
if (fail > 0) {
|
|
|
|
|
|
this.$message.warning(`保存完成:成功 ${ok} 条,失败 ${fail} 条`)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.$message.success(`已保存 ${ok} 条记录`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}).finally(() => {
|
|
|
|
|
|
this.batchSaving = false
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
/** 点击可点击列时切换展开,并加载绩效考核明细 */
|
|
|
|
|
|
toggleRowExpand(row) {
|
|
|
|
|
|
if (this.expandedRow === row) {
|
|
|
|
|
|
this.expandedRow = null
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.expandedRow = row
|
|
|
|
|
|
if (row._appLoaded) return
|
|
|
|
|
|
this.appLoading = true
|
|
|
|
|
|
const empId = row.employeeId
|
|
|
|
|
|
if (!empId) {
|
|
|
|
|
|
this.appLoading = false
|
|
|
|
|
|
this.$set(row, '_appLoaded', true)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
listPerfAppraisal({ employeeId: empId, deptId: this.deptId, period: this.period, pageNum: 1, pageSize: 1 }).then(appRes => {
|
|
|
|
|
|
const appRows = appRes.rows || []
|
|
|
|
|
|
if (appRows.length === 0) {
|
|
|
|
|
|
this.$set(row, '_appraisal', null)
|
|
|
|
|
|
this.$set(row, '_appDetails', [])
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.$set(row, '_appraisal', appRows[0])
|
|
|
|
|
|
return listPerfAppraisalDetail({ appraisalId: appRows[0].id, pageSize: 9999 })
|
|
|
|
|
|
}).then(detailRes => {
|
|
|
|
|
|
if (detailRes) {
|
|
|
|
|
|
this.$set(row, '_appDetails', detailRes.rows || [])
|
|
|
|
|
|
}
|
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
|
this.$set(row, '_appraisal', null)
|
|
|
|
|
|
this.$set(row, '_appDetails', [])
|
|
|
|
|
|
}).finally(() => {
|
|
|
|
|
|
this.appLoading = false
|
|
|
|
|
|
this.$set(row, '_appLoaded', true)
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
/** 计算得分 */
|
|
|
|
|
|
async handleCalcScore() {
|
|
|
|
|
|
if (!this.expandedRow._appDetails || !this.expandedRow._appDetails.length) return
|
|
|
|
|
|
this.calcLoading = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 拉取部门生产数据(缓存按 deptId+period)
|
|
|
|
|
|
if (!this._prodCache || this._prodCache.key !== `${this.deptId}_${this.period}`) {
|
|
|
|
|
|
this._prodCache = {
|
|
|
|
|
|
key: `${this.deptId}_${this.period}`,
|
|
|
|
|
|
data: await fetchProductionData(this.productionLine, this.period)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const prod = this._prodCache.data
|
|
|
|
|
|
|
|
|
|
|
|
// 将实际数据填入各维度
|
|
|
|
|
|
const details = this.expandedRow._appDetails
|
|
|
|
|
|
details.forEach(d => {
|
|
|
|
|
|
switch (d.dimName) {
|
|
|
|
|
|
case '生产效率':
|
|
|
|
|
|
if (prod.totalWeight > 0 && !d.actualValue) {
|
|
|
|
|
|
this.$set(d, 'actualValue', String(Math.round(prod.totalWeight * 100) / 100))
|
|
|
|
|
|
}
|
|
|
|
|
|
break
|
|
|
|
|
|
case '产品质量':
|
|
|
|
|
|
if (prod.totalCount > 0 && !d.actualValue) {
|
|
|
|
|
|
const ratio = Math.round((prod.abCount / prod.totalCount) * 10000) / 100
|
|
|
|
|
|
this.$set(d, 'actualValue', String(ratio))
|
|
|
|
|
|
}
|
|
|
|
|
|
break
|
|
|
|
|
|
case '成本控制':
|
|
|
|
|
|
if (prod.avgCostPerTon > 0 && !d.actualValue) {
|
|
|
|
|
|
this.$set(d, 'actualValue', String(Math.round(prod.avgCostPerTon * 100) / 100))
|
|
|
|
|
|
}
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const { finalScore, details: computed } = calculateTotal(details)
|
|
|
|
|
|
computed.forEach((d, i) => {
|
|
|
|
|
|
this.$set(details[i], 'dimScore', d._dimScore)
|
|
|
|
|
|
})
|
|
|
|
|
|
this.$message.success('计算完成,请检查后保存')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.calcLoading = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
/** 保存得分到后端 */
|
|
|
|
|
|
async handleSaveScores() {
|
|
|
|
|
|
if (!this.expandedRow._appDetails || !this.expandedRow._appDetails.length) return
|
|
|
|
|
|
this.saveLoading = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const details = this.expandedRow._appDetails
|
|
|
|
|
|
// 构建批量更新数据
|
|
|
|
|
|
const batchData = details.map(d => ({
|
|
|
|
|
|
id: d.id,
|
|
|
|
|
|
actualValue: d.actualValue || '',
|
|
|
|
|
|
dimScore: parseFloat(d.dimScore) || 0,
|
|
|
|
|
|
bonus: parseFloat(d.bonus) || 0,
|
|
|
|
|
|
penalty: parseFloat(d.penalty) || 0
|
|
|
|
|
|
}))
|
|
|
|
|
|
// 批量更新维度明细
|
|
|
|
|
|
await batchUpdatePerfAppraisalDetail(batchData)
|
|
|
|
|
|
|
|
|
|
|
|
// 按照权重计算总分
|
|
|
|
|
|
let weightedSum = 0
|
|
|
|
|
|
let totalBonus = 0
|
|
|
|
|
|
let totalPenalty = 0
|
|
|
|
|
|
details.forEach(d => {
|
|
|
|
|
|
const score = parseFloat(d.dimScore) || 0
|
|
|
|
|
|
const weight = parseFloat(d.weight) || 0
|
|
|
|
|
|
weightedSum += score * weight / 100
|
|
|
|
|
|
totalBonus += parseFloat(d.bonus) || 0
|
|
|
|
|
|
totalPenalty += parseFloat(d.penalty) || 0
|
|
|
|
|
|
})
|
|
|
|
|
|
const finalScore = Math.min(120, Math.max(0, weightedSum + totalBonus - totalPenalty))
|
|
|
|
|
|
|
|
|
|
|
|
// 更新总考核记录
|
|
|
|
|
|
if (this.expandedRow._appraisal && this.expandedRow._appraisal.id) {
|
|
|
|
|
|
await updatePerfAppraisal({ id: this.expandedRow._appraisal.id, finalScore })
|
|
|
|
|
|
this.$set(this.expandedRow._appraisal, 'finalScore', finalScore)
|
|
|
|
|
|
}
|
|
|
|
|
|
// 同步绩效系数到薪资记录
|
|
|
|
|
|
const perfCoeff = finalScore / 100
|
|
|
|
|
|
if (this.expandedRow.id) {
|
|
|
|
|
|
await updatePerfSalary({ id: this.expandedRow.id, perfCoeff })
|
|
|
|
|
|
this.$set(this.expandedRow, 'perfCoeff', perfCoeff)
|
|
|
|
|
|
this.recalcCoeffs(this.expandedRow)
|
|
|
|
|
|
}
|
|
|
|
|
|
this.$message.success('保存成功')
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
this.$message.error('保存失败')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.saveLoading = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
/** 收起展开面板 */
|
|
|
|
|
|
handleCollapseExpand() {
|
|
|
|
|
|
this.expandedRow = null
|
|
|
|
|
|
},
|
|
|
|
|
|
/** 标记 detail 已修改 */
|
|
|
|
|
|
markDetailDirty() {},
|
|
|
|
|
|
subtotal(row) {
|
|
|
|
|
|
const score = parseFloat(row.dimScore) || 0
|
|
|
|
|
|
const weight = parseFloat(row.weight) || 0
|
|
|
|
|
|
return (score * weight / 100).toFixed(1)
|
|
|
|
|
|
},
|
|
|
|
|
|
rawRowTotal(row) {
|
|
|
|
|
|
const score = parseFloat(row.dimScore) || 0
|
|
|
|
|
|
const bonus = parseFloat(row.bonus) || 0
|
|
|
|
|
|
const penalty = parseFloat(row.penalty) || 0
|
|
|
|
|
|
return (score + bonus - penalty).toFixed(1)
|
|
|
|
|
|
},
|
|
|
|
|
|
handleExpandChange() {}
|
2026-07-10 10:43:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 14:53:42 +08:00
|
|
|
|
/* 口径说明 */
|
|
|
|
|
|
.formula-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
background: #f0f5ff;
|
|
|
|
|
|
border: 1px solid #d9ecff;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
transition: background 0.15s;
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background: #e6f0fc;
|
|
|
|
|
|
}
|
|
|
|
|
|
&.expanded {
|
|
|
|
|
|
border-radius: 4px 4px 0 0;
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.formula-bar-title {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.formula-bar-hint {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
margin-left: 12px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.formula-detail {
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
background: #fafbfc;
|
|
|
|
|
|
border: 1px solid #d9ecff;
|
|
|
|
|
|
border-top: none;
|
|
|
|
|
|
border-radius: 0 0 4px 4px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.formula-row {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
|
b {
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 10:43:16 +08:00
|
|
|
|
.el-table .el-button + .el-button {
|
|
|
|
|
|
margin-left: 0;
|
|
|
|
|
|
}
|
2026-07-11 14:53:42 +08:00
|
|
|
|
|
|
|
|
|
|
/* 展开面板 */
|
|
|
|
|
|
.expand-panel {
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
|
border: 1px solid #ebeef5;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.expand-panel-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.expand-panel-title {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
}
|
|
|
|
|
|
.expand-panel-score {
|
|
|
|
|
|
margin-left: 12px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
.expand-detail-table {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
.expand-empty {
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 12px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 实际值输入 */
|
|
|
|
|
|
.cell-input--actual {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 26px;
|
|
|
|
|
|
border: 1px solid #dcdfe6;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
transition: border-color 0.15s;
|
|
|
|
|
|
&:focus {
|
|
|
|
|
|
border-color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 加分扣分输入 */
|
|
|
|
|
|
.cell-input--bonus,
|
|
|
|
|
|
.cell-input--penalty {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 26px;
|
|
|
|
|
|
border: 1px solid #dcdfe6;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
transition: border-color 0.15s;
|
|
|
|
|
|
&:focus {
|
|
|
|
|
|
border-color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.cell-input--bonus {
|
|
|
|
|
|
color: #67c23a;
|
|
|
|
|
|
background: #f0f9eb;
|
|
|
|
|
|
}
|
|
|
|
|
|
.cell-input--penalty {
|
|
|
|
|
|
color: #f56c6c;
|
|
|
|
|
|
background: #fef0f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 得分手动输入 */
|
|
|
|
|
|
.cell-input--score {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 26px;
|
|
|
|
|
|
border: 1px solid #dcdfe6;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
transition: border-color 0.15s;
|
|
|
|
|
|
&:focus {
|
|
|
|
|
|
border-color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 总分列 */
|
|
|
|
|
|
.cell-subtotal {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #e6a23c;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cell-rawtotal {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 得分显示 */
|
|
|
|
|
|
.cell-score {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
&--empty {
|
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 可点击列样式 */
|
|
|
|
|
|
.cell-clickable {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
border-bottom: 1px dashed #409eff;
|
|
|
|
|
|
padding: 0 2px;
|
|
|
|
|
|
transition: color 0.15s, border-color 0.15s;
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
color: #337ecc;
|
|
|
|
|
|
border-bottom-style: solid;
|
|
|
|
|
|
}
|
|
|
|
|
|
&--score {
|
|
|
|
|
|
color: #e6a23c;
|
|
|
|
|
|
border-bottom-color: #e6a23c;
|
|
|
|
|
|
&:hover { color: #cf9236; }
|
|
|
|
|
|
}
|
|
|
|
|
|
&--coeff {
|
|
|
|
|
|
color: #67c23a;
|
|
|
|
|
|
border-bottom-color: #67c23a;
|
|
|
|
|
|
&:hover { color: #529b2e; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-10 10:43:16 +08:00
|
|
|
|
</style>
|