2025-12-22 10:57:47 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="hrm-page">
|
2025-12-22 16:53:48 +08:00
|
|
|
|
<section class="stats-grid">
|
|
|
|
|
|
<el-card class="metal-panel kpi-card" shadow="hover">
|
|
|
|
|
|
<div class="kpi-title">薪酬方案数</div>
|
|
|
|
|
|
<div class="kpi-value">{{ payPlanList.length }}</div>
|
|
|
|
|
|
<div class="kpi-sub">启用 {{ payPlanList.filter(i => i.status === 'active').length }} 个</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
<el-card class="metal-panel kpi-card" shadow="hover">
|
|
|
|
|
|
<div class="kpi-title">薪酬批次数</div>
|
|
|
|
|
|
<div class="kpi-value">{{ payRunList.length }}</div>
|
|
|
|
|
|
<div class="kpi-sub">审批中 {{ payRunList.filter(i => i.status === 'pending').length }} 个</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
<el-card class="metal-panel kpi-card" shadow="hover">
|
|
|
|
|
|
<div class="kpi-title">工资条总额(应发)</div>
|
|
|
|
|
|
<div class="kpi-value">¥{{ formatNumber(payslipList.reduce((s, i) => s + (Number(i.amountGross) || 0), 0)) }}</div>
|
|
|
|
|
|
<div class="kpi-sub">实发 ¥{{ formatNumber(payslipList.reduce((s, i) => s + (Number(i.amountNet) || 0), 0)) }}</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
<el-card class="metal-panel kpi-card" shadow="hover">
|
|
|
|
|
|
<div class="kpi-title">指标快照</div>
|
|
|
|
|
|
<div class="kpi-value">{{ statList.length }}</div>
|
|
|
|
|
|
<div class="kpi-sub">最新日期:{{ latestStatDate || '-' }}</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
2025-12-22 10:57:47 +08:00
|
|
|
|
<section class="panel-grid triple">
|
|
|
|
|
|
<el-card class="metal-panel" shadow="hover">
|
|
|
|
|
|
<div slot="header" class="panel-header">
|
|
|
|
|
|
<span>薪酬方案</span>
|
2025-12-22 16:53:48 +08:00
|
|
|
|
<div class="actions-inline">
|
|
|
|
|
|
<el-select v-model="payPlanQuery.status" size="mini" placeholder="状态" clearable style="width:120px" @change="loadPayPlan">
|
|
|
|
|
|
<el-option label="启用" value="active" />
|
|
|
|
|
|
<el-option label="草稿/停用" value="inactive" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openPlanDialog()">新增</el-button>
|
|
|
|
|
|
<el-button size="mini" icon="el-icon-refresh" @click="loadPayPlan">刷新</el-button>
|
|
|
|
|
|
</div>
|
2025-12-22 10:57:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<el-table :data="payPlanList" v-loading="payPlanLoading" height="320" stripe>
|
|
|
|
|
|
<el-table-column label="方案" prop="planName" min-width="140" />
|
|
|
|
|
|
<el-table-column label="币种" prop="currency" min-width="90" />
|
|
|
|
|
|
<el-table-column label="状态" prop="status" min-width="100">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-tag :type="statusType(scope.row.status)">{{ scope.row.status || '-' }}</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="描述" prop="remark" min-width="160" show-overflow-tooltip />
|
2025-12-22 16:53:48 +08:00
|
|
|
|
<el-table-column label="操作" width="140" fixed="right">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-button size="mini" type="text" @click="openPlanDialog(scope.row)">编辑</el-button>
|
|
|
|
|
|
<el-button size="mini" type="text" @click="delPlan(scope.row)">删除</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2025-12-22 10:57:47 +08:00
|
|
|
|
</el-table>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
|
|
<el-card class="metal-panel" shadow="hover">
|
|
|
|
|
|
<div slot="header" class="panel-header">
|
|
|
|
|
|
<span>薪酬批次</span>
|
2025-12-22 16:53:48 +08:00
|
|
|
|
<div class="actions-inline">
|
|
|
|
|
|
<el-select v-model="payRunQuery.status" size="mini" placeholder="状态" clearable style="width:120px" @change="loadPayRun">
|
|
|
|
|
|
<el-option label="审批中" value="pending" />
|
|
|
|
|
|
<el-option label="已完成" value="approved" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openRunDialog()">新增</el-button>
|
|
|
|
|
|
<el-button size="mini" icon="el-icon-refresh" @click="loadPayRun">刷新</el-button>
|
|
|
|
|
|
</div>
|
2025-12-22 10:57:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<el-table :data="payRunList" v-loading="payRunLoading" height="320" stripe>
|
|
|
|
|
|
<el-table-column label="批次" prop="runName" min-width="140" />
|
|
|
|
|
|
<el-table-column label="周期" prop="period" min-width="120" />
|
|
|
|
|
|
<el-table-column label="状态" prop="status" min-width="100">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-tag :type="statusType(scope.row.status)">{{ scope.row.status || '-' }}</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="备注" prop="remark" min-width="160" show-overflow-tooltip />
|
2025-12-22 16:53:48 +08:00
|
|
|
|
<el-table-column label="操作" width="140" fixed="right">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-button size="mini" type="text" @click="openRunDialog(scope.row)">编辑</el-button>
|
|
|
|
|
|
<el-button size="mini" type="text" @click="delRun(scope.row)">删除</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2025-12-22 10:57:47 +08:00
|
|
|
|
</el-table>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
|
|
<el-card class="metal-panel" shadow="hover">
|
|
|
|
|
|
<div slot="header" class="panel-header">
|
|
|
|
|
|
<span>工资条 & 指标</span>
|
|
|
|
|
|
<div class="actions-inline">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="payslipQuery.batchNo"
|
|
|
|
|
|
placeholder="批次编号"
|
|
|
|
|
|
size="mini"
|
|
|
|
|
|
style="width: 150px"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
@keyup.enter.native="loadPayslip"
|
|
|
|
|
|
/>
|
2025-12-22 16:53:48 +08:00
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="payslipQuery.empName"
|
|
|
|
|
|
placeholder="员工姓名/工号"
|
|
|
|
|
|
size="mini"
|
|
|
|
|
|
style="width: 150px"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
@keyup.enter.native="loadPayslip"
|
|
|
|
|
|
/>
|
2025-12-22 10:57:47 +08:00
|
|
|
|
<el-button size="mini" type="primary" @click="loadPayslip">查询</el-button>
|
2025-12-22 16:53:48 +08:00
|
|
|
|
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openPayslipDialog()">新增</el-button>
|
|
|
|
|
|
<el-button size="mini" icon="el-icon-download" :loading="exportLoading" @click="exportPayslip">导出</el-button>
|
2025-12-22 10:57:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="dual-tables">
|
|
|
|
|
|
<div class="table-half">
|
|
|
|
|
|
<div class="table-title">工资条</div>
|
2025-12-22 16:53:48 +08:00
|
|
|
|
<el-table :data="payslipList" v-loading="payslipLoading" height="230" stripe @row-click="openPayslipDetail">
|
|
|
|
|
|
<el-table-column label="员工" prop="empName" min-width="120" />
|
2025-12-22 10:57:47 +08:00
|
|
|
|
<el-table-column label="应发" prop="amountGross" min-width="100">
|
|
|
|
|
|
<template slot-scope="scope">¥{{ formatNumber(scope.row.amountGross) }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="实发" prop="amountNet" min-width="100">
|
|
|
|
|
|
<template slot-scope="scope">¥{{ formatNumber(scope.row.amountNet) }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="状态" prop="status" min-width="90">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-tag :type="statusType(scope.row.status)" size="mini">{{ scope.row.status || '-' }}</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2025-12-22 16:53:48 +08:00
|
|
|
|
<el-table-column label="操作" width="140" fixed="right">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-button size="mini" type="text" @click.stop="openPayslipDialog(scope.row)">编辑</el-button>
|
|
|
|
|
|
<el-button size="mini" type="text" @click.stop="delPayslipRow(scope.row)">删除</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2025-12-22 10:57:47 +08:00
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="table-half">
|
|
|
|
|
|
<div class="table-title">指标快照</div>
|
2025-12-22 16:53:48 +08:00
|
|
|
|
<div class="actions-inline" style="margin-bottom:6px">
|
|
|
|
|
|
<el-select v-model="statQuery.statType" size="mini" placeholder="类型" clearable style="width:140px" @change="loadStatSnapshot">
|
|
|
|
|
|
<el-option label="工资成本" value="cost" />
|
|
|
|
|
|
<el-option label="人均成本" value="per_capita_cost" />
|
|
|
|
|
|
<el-option label="奖金" value="bonus" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-table :data="statList" v-loading="statLoading" height="200" stripe>
|
2025-12-22 10:57:47 +08:00
|
|
|
|
<el-table-column label="类型" prop="statType" min-width="120" />
|
|
|
|
|
|
<el-table-column label="维度" prop="dimension" min-width="120" show-overflow-tooltip />
|
|
|
|
|
|
<el-table-column label="数值" prop="value" min-width="100" />
|
|
|
|
|
|
<el-table-column label="日期" prop="statDate" min-width="120" />
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</section>
|
2025-12-22 16:53:48 +08:00
|
|
|
|
|
|
|
|
|
|
<el-dialog title="工资条详情" :visible.sync="payslipDetailVisible" width="520px" append-to-body>
|
|
|
|
|
|
<el-descriptions :column="2" size="small" border>
|
|
|
|
|
|
<el-descriptions-item label="员工">{{ payslipDetail.empName || payslipDetail.empId }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="批次">{{ payslipDetail.batchNo || '-' }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="应发">¥{{ formatNumber(payslipDetail.amountGross) }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="实发">¥{{ formatNumber(payslipDetail.amountNet) }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="状态">{{ payslipDetail.status || '-' }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="周期">{{ payslipDetail.period || '-' }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="发放日期">{{ payslipDetail.payDate || '-' }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="备注">{{ payslipDetail.remark || '-' }}</el-descriptions-item>
|
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
|
<div slot="footer" class="dialog-footer">
|
|
|
|
|
|
<el-button @click="payslipDetailVisible = false">关闭</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 方案弹窗 -->
|
|
|
|
|
|
<el-dialog title="薪酬方案" :visible.sync="planDialogVisible" width="480px" append-to-body>
|
|
|
|
|
|
<el-form ref="planFormRef" :model="planForm" :rules="planRules" label-width="100px" size="small">
|
|
|
|
|
|
<el-form-item label="方案名称" prop="planName">
|
|
|
|
|
|
<el-input v-model="planForm.planName" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="币种">
|
|
|
|
|
|
<el-input v-model="planForm.currency" placeholder="如:CNY" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="状态">
|
|
|
|
|
|
<el-select v-model="planForm.status" placeholder="选择状态" style="width: 100%">
|
|
|
|
|
|
<el-option label="启用" value="active" />
|
|
|
|
|
|
<el-option label="停用/草稿" value="inactive" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="备注">
|
|
|
|
|
|
<el-input v-model="planForm.remark" type="textarea" :rows="2" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
<div slot="footer">
|
|
|
|
|
|
<el-button @click="planDialogVisible = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" :loading="planSubmitting" @click="submitPlan">保存</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 批次弹窗 -->
|
|
|
|
|
|
<el-dialog title="薪酬批次" :visible.sync="runDialogVisible" width="480px" append-to-body>
|
|
|
|
|
|
<el-form ref="runFormRef" :model="runForm" :rules="runRules" label-width="100px" size="small">
|
|
|
|
|
|
<el-form-item label="批次名称" prop="runName">
|
|
|
|
|
|
<el-input v-model="runForm.runName" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="核算周期" prop="period">
|
|
|
|
|
|
<el-input v-model="runForm.period" placeholder="如 2024-08" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="关联方案" prop="planId">
|
|
|
|
|
|
<el-select v-model="runForm.planId" placeholder="选择方案" filterable style="width: 100%">
|
|
|
|
|
|
<el-option v-for="plan in payPlanList" :key="plan.planId" :label="plan.planName" :value="plan.planId" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="状态">
|
|
|
|
|
|
<el-select v-model="runForm.status" placeholder="选择状态" style="width: 100%">
|
|
|
|
|
|
<el-option label="审批中" value="pending" />
|
|
|
|
|
|
<el-option label="已完成" value="approved" />
|
|
|
|
|
|
<el-option label="草稿" value="draft" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="备注">
|
|
|
|
|
|
<el-input v-model="runForm.remark" type="textarea" :rows="2" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
<div slot="footer">
|
|
|
|
|
|
<el-button @click="runDialogVisible = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" :loading="runSubmitting" @click="submitRun">保存</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 工资条弹窗 -->
|
|
|
|
|
|
<el-dialog title="工资条" :visible.sync="payslipDialogVisible" width="520px" append-to-body>
|
|
|
|
|
|
<el-form ref="payslipFormRef" :model="payslipForm" :rules="payslipRules" label-width="110px" size="small">
|
|
|
|
|
|
<el-form-item label="员工" prop="empId">
|
|
|
|
|
|
<el-select v-model="payslipForm.empId" filterable placeholder="选择员工" style="width: 100%" :loading="employeeLoading">
|
|
|
|
|
|
<el-option v-for="emp in employeeOptions" :key="emp.empId" :label="`${emp.empName || emp.empCode || emp.empId}`" :value="emp.empId" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="批次号" prop="batchNo">
|
|
|
|
|
|
<el-input v-model="payslipForm.batchNo" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="应发金额" prop="amountGross">
|
|
|
|
|
|
<el-input-number v-model="payslipForm.amountGross" :min="0" :step="100" style="width:100%" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="实发金额" prop="amountNet">
|
|
|
|
|
|
<el-input-number v-model="payslipForm.amountNet" :min="0" :step="100" style="width:100%" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="周期" prop="period">
|
|
|
|
|
|
<el-input v-model="payslipForm.period" placeholder="如 2024-08" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="状态">
|
|
|
|
|
|
<el-select v-model="payslipForm.status" placeholder="选择状态" style="width: 100%">
|
|
|
|
|
|
<el-option label="待发" value="pending" />
|
|
|
|
|
|
<el-option label="已发" value="paid" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="备注">
|
|
|
|
|
|
<el-input v-model="payslipForm.remark" type="textarea" :rows="2" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
<div slot="footer">
|
|
|
|
|
|
<el-button @click="payslipDialogVisible = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" :loading="payslipSubmitting" @click="submitPayslip">保存</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-dialog>
|
2025-12-22 10:57:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2025-12-22 16:53:48 +08:00
|
|
|
|
import {
|
|
|
|
|
|
listPayPlan,
|
|
|
|
|
|
getPayPlan,
|
|
|
|
|
|
addPayPlan,
|
|
|
|
|
|
updatePayPlan,
|
|
|
|
|
|
delPayPlan,
|
|
|
|
|
|
listPayRun,
|
|
|
|
|
|
getPayRun,
|
|
|
|
|
|
addPayRun,
|
|
|
|
|
|
updatePayRun,
|
|
|
|
|
|
delPayRun,
|
|
|
|
|
|
listPayslip,
|
|
|
|
|
|
getPayslip,
|
|
|
|
|
|
addPayslip,
|
|
|
|
|
|
updatePayslip,
|
|
|
|
|
|
delPayslip,
|
|
|
|
|
|
listEmployee,
|
|
|
|
|
|
listStatSnapshot
|
|
|
|
|
|
} from '@/api/hrm'
|
2025-12-22 10:57:47 +08:00
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'HrmPayroll',
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
payPlanList: [],
|
|
|
|
|
|
payPlanLoading: false,
|
2025-12-22 16:53:48 +08:00
|
|
|
|
payPlanQuery: { status: undefined },
|
2025-12-22 10:57:47 +08:00
|
|
|
|
payRunList: [],
|
|
|
|
|
|
payRunLoading: false,
|
2025-12-22 16:53:48 +08:00
|
|
|
|
payRunQuery: { status: undefined },
|
2025-12-22 10:57:47 +08:00
|
|
|
|
payslipList: [],
|
|
|
|
|
|
payslipLoading: false,
|
2025-12-22 16:53:48 +08:00
|
|
|
|
payslipQuery: { batchNo: '', empName: '' },
|
2025-12-22 10:57:47 +08:00
|
|
|
|
statList: [],
|
2025-12-22 16:53:48 +08:00
|
|
|
|
statLoading: false,
|
|
|
|
|
|
statQuery: { statType: undefined },
|
|
|
|
|
|
payslipDetailVisible: false,
|
|
|
|
|
|
payslipDetail: {},
|
|
|
|
|
|
employeeOptions: [],
|
|
|
|
|
|
employeeLoading: false,
|
|
|
|
|
|
exportLoading: false,
|
|
|
|
|
|
planDialogVisible: false,
|
|
|
|
|
|
planSubmitting: false,
|
|
|
|
|
|
planForm: {},
|
|
|
|
|
|
planRules: { planName: [{ required: true, message: '请输入方案名称', trigger: 'blur' }] },
|
|
|
|
|
|
runDialogVisible: false,
|
|
|
|
|
|
runSubmitting: false,
|
|
|
|
|
|
runForm: {},
|
|
|
|
|
|
runRules: {
|
|
|
|
|
|
runName: [{ required: true, message: '请输入批次名称', trigger: 'blur' }],
|
|
|
|
|
|
period: [{ required: true, message: '请输入核算周期', trigger: 'blur' }],
|
|
|
|
|
|
planId: [{ required: true, message: '请选择方案', trigger: 'change' }]
|
|
|
|
|
|
},
|
|
|
|
|
|
payslipDialogVisible: false,
|
|
|
|
|
|
payslipSubmitting: false,
|
|
|
|
|
|
payslipForm: {},
|
|
|
|
|
|
payslipRules: {
|
|
|
|
|
|
empId: [{ required: true, message: '请输入员工ID', trigger: 'blur' }],
|
|
|
|
|
|
amountGross: [{ required: true, message: '请输入应发金额', trigger: 'blur' }],
|
|
|
|
|
|
amountNet: [{ required: true, message: '请输入实发金额', trigger: 'blur' }],
|
|
|
|
|
|
batchNo: [{ required: true, message: '请输入批次号', trigger: 'blur' }]
|
|
|
|
|
|
}
|
2025-12-22 10:57:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-12-23 15:56:15 +08:00
|
|
|
|
computed: {
|
|
|
|
|
|
latestStatDate() {
|
|
|
|
|
|
const dates = (this.statList || []).map(i => i.statDate).filter(Boolean)
|
|
|
|
|
|
if (!dates.length) return ''
|
|
|
|
|
|
return dates.sort().pop()
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-12-22 10:57:47 +08:00
|
|
|
|
created() {
|
|
|
|
|
|
this.loadPayPlan()
|
|
|
|
|
|
this.loadPayRun()
|
|
|
|
|
|
this.loadPayslip()
|
|
|
|
|
|
this.loadStatSnapshot()
|
2025-12-22 16:53:48 +08:00
|
|
|
|
this.loadEmployees()
|
2025-12-22 10:57:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
statusType(status) {
|
|
|
|
|
|
if (!status) return 'info'
|
|
|
|
|
|
const map = { pending: 'warning', draft: 'info', approved: 'success', rejected: 'danger', active: 'success', inactive: 'info' }
|
|
|
|
|
|
return map[status] || 'info'
|
|
|
|
|
|
},
|
|
|
|
|
|
formatNumber(num) {
|
|
|
|
|
|
if (num === null || num === undefined || isNaN(num)) return '0'
|
|
|
|
|
|
return Number(num).toLocaleString(undefined, { maximumFractionDigits: 2 })
|
|
|
|
|
|
},
|
|
|
|
|
|
loadPayPlan() {
|
|
|
|
|
|
this.payPlanLoading = true
|
2025-12-22 16:53:48 +08:00
|
|
|
|
listPayPlan({ pageNum: 1, pageSize: 50, status: this.payPlanQuery.status })
|
2025-12-22 10:57:47 +08:00
|
|
|
|
.then(res => {
|
|
|
|
|
|
this.payPlanList = res.rows || []
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
this.payPlanLoading = false
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
loadPayRun() {
|
|
|
|
|
|
this.payRunLoading = true
|
2025-12-22 16:53:48 +08:00
|
|
|
|
listPayRun({ pageNum: 1, pageSize: 50, status: this.payRunQuery.status })
|
2025-12-22 10:57:47 +08:00
|
|
|
|
.then(res => {
|
|
|
|
|
|
this.payRunList = res.rows || []
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
this.payRunLoading = false
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
loadPayslip() {
|
|
|
|
|
|
this.payslipLoading = true
|
2025-12-22 16:53:48 +08:00
|
|
|
|
listPayslip({ pageNum: 1, pageSize: 50, batchNo: this.payslipQuery.batchNo, empName: this.payslipQuery.empName })
|
2025-12-22 10:57:47 +08:00
|
|
|
|
.then(res => {
|
|
|
|
|
|
this.payslipList = res.rows || []
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
this.payslipLoading = false
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
loadStatSnapshot() {
|
|
|
|
|
|
this.statLoading = true
|
2025-12-22 16:53:48 +08:00
|
|
|
|
listStatSnapshot({ pageNum: 1, pageSize: 50, statType: this.statQuery.statType })
|
2025-12-22 10:57:47 +08:00
|
|
|
|
.then(res => {
|
|
|
|
|
|
this.statList = res.rows || []
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
this.statLoading = false
|
|
|
|
|
|
})
|
2025-12-22 16:53:48 +08:00
|
|
|
|
},
|
|
|
|
|
|
loadEmployees() {
|
|
|
|
|
|
this.employeeLoading = true
|
|
|
|
|
|
listEmployee({ pageNum: 1, pageSize: 500 })
|
|
|
|
|
|
.then(res => {
|
|
|
|
|
|
this.employeeOptions = res.rows || []
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
this.employeeLoading = false
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
openPayslipDetail(row) {
|
|
|
|
|
|
this.payslipDetail = { ...row }
|
|
|
|
|
|
this.payslipDetailVisible = true
|
|
|
|
|
|
},
|
|
|
|
|
|
// 方案 CRUD
|
|
|
|
|
|
openPlanDialog(row) {
|
|
|
|
|
|
this.planForm = row ? { ...row } : { planName: '', currency: 'CNY', status: 'active', remark: '' }
|
|
|
|
|
|
this.planDialogVisible = true
|
|
|
|
|
|
this.$nextTick(() => this.$refs.planFormRef && this.$refs.planFormRef.clearValidate())
|
|
|
|
|
|
},
|
|
|
|
|
|
submitPlan() {
|
|
|
|
|
|
this.$refs.planFormRef.validate(valid => {
|
|
|
|
|
|
if (!valid) return
|
|
|
|
|
|
this.planSubmitting = true
|
|
|
|
|
|
const api = this.planForm.planId ? updatePayPlan : addPayPlan
|
|
|
|
|
|
api(this.planForm)
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
this.$message.success('已保存')
|
|
|
|
|
|
this.planDialogVisible = false
|
|
|
|
|
|
this.loadPayPlan()
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
this.planSubmitting = false
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
delPlan(row) {
|
|
|
|
|
|
this.$confirm('确认删除该方案吗?', '提示', { type: 'warning' }).then(() => {
|
|
|
|
|
|
delPayPlan(row.planId).then(() => {
|
|
|
|
|
|
this.$message.success('已删除')
|
|
|
|
|
|
this.loadPayPlan()
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
// 批次 CRUD
|
|
|
|
|
|
openRunDialog(row) {
|
|
|
|
|
|
this.runForm = row
|
|
|
|
|
|
? { ...row }
|
|
|
|
|
|
: { runName: '', period: '', planId: this.payPlanList[0]?.planId, status: 'pending', remark: '' }
|
|
|
|
|
|
this.runDialogVisible = true
|
|
|
|
|
|
this.$nextTick(() => this.$refs.runFormRef && this.$refs.runFormRef.clearValidate())
|
|
|
|
|
|
},
|
|
|
|
|
|
submitRun() {
|
|
|
|
|
|
this.$refs.runFormRef.validate(valid => {
|
|
|
|
|
|
if (!valid) return
|
|
|
|
|
|
this.runSubmitting = true
|
|
|
|
|
|
const api = this.runForm.runId ? updatePayRun : addPayRun
|
|
|
|
|
|
api(this.runForm)
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
this.$message.success('已保存')
|
|
|
|
|
|
this.runDialogVisible = false
|
|
|
|
|
|
this.loadPayRun()
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
this.runSubmitting = false
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
delRun(row) {
|
|
|
|
|
|
this.$confirm('确认删除该批次吗?', '提示', { type: 'warning' }).then(() => {
|
|
|
|
|
|
delPayRun(row.runId).then(() => {
|
|
|
|
|
|
this.$message.success('已删除')
|
|
|
|
|
|
this.loadPayRun()
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
// 工资条 CRUD
|
|
|
|
|
|
openPayslipDialog(row) {
|
|
|
|
|
|
this.payslipForm = row
|
|
|
|
|
|
? { ...row }
|
|
|
|
|
|
: {
|
|
|
|
|
|
empId: '',
|
|
|
|
|
|
batchNo: '',
|
|
|
|
|
|
amountGross: 0,
|
|
|
|
|
|
amountNet: 0,
|
|
|
|
|
|
period: '',
|
|
|
|
|
|
status: 'pending',
|
|
|
|
|
|
remark: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
this.payslipDialogVisible = true
|
|
|
|
|
|
this.$nextTick(() => this.$refs.payslipFormRef && this.$refs.payslipFormRef.clearValidate())
|
|
|
|
|
|
},
|
|
|
|
|
|
submitPayslip() {
|
|
|
|
|
|
this.$refs.payslipFormRef.validate(valid => {
|
|
|
|
|
|
if (!valid) return
|
|
|
|
|
|
this.payslipSubmitting = true
|
|
|
|
|
|
const api = this.payslipForm.slipId ? updatePayslip : addPayslip
|
|
|
|
|
|
api(this.payslipForm)
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
this.$message.success('已保存')
|
|
|
|
|
|
this.payslipDialogVisible = false
|
|
|
|
|
|
this.loadPayslip()
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
this.payslipSubmitting = false
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
delPayslipRow(row) {
|
|
|
|
|
|
this.$confirm('确认删除该工资条吗?', '提示', { type: 'warning' }).then(() => {
|
|
|
|
|
|
delPayslip(row.slipId).then(() => {
|
|
|
|
|
|
this.$message.success('已删除')
|
|
|
|
|
|
this.loadPayslip()
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
exportPayslip() {
|
|
|
|
|
|
if (!this.payslipList.length) {
|
|
|
|
|
|
this.$message.info('暂无可导出的数据')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.exportLoading = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const headers = ['员工', '批次号', '应发', '实发', '周期', '状态', '发放日期', '备注']
|
|
|
|
|
|
const rows = this.payslipList.map(i => [
|
|
|
|
|
|
i.empName || i.empId || '',
|
|
|
|
|
|
i.batchNo || '',
|
|
|
|
|
|
i.amountGross || '',
|
|
|
|
|
|
i.amountNet || '',
|
|
|
|
|
|
i.period || '',
|
|
|
|
|
|
i.status || '',
|
|
|
|
|
|
i.payDate || '',
|
|
|
|
|
|
(i.remark || '').replace(/\r?\n/g, ' ')
|
|
|
|
|
|
])
|
|
|
|
|
|
const tableRows = [headers, ...rows]
|
|
|
|
|
|
.map(
|
|
|
|
|
|
r =>
|
|
|
|
|
|
'<tr>' +
|
|
|
|
|
|
r
|
|
|
|
|
|
.map(v => `<td style="mso-number-format:'\\@';">${String(v).replace(/</g, '<').replace(/>/g, '>')}</td>`)
|
|
|
|
|
|
.join('') +
|
|
|
|
|
|
'</tr>'
|
|
|
|
|
|
)
|
|
|
|
|
|
.join('')
|
|
|
|
|
|
const html = `<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><meta charset="UTF-8"></head><body><table>${tableRows}</table></body></html>`
|
|
|
|
|
|
const blob = new Blob([html], { type: 'application/vnd.ms-excel;charset=utf-8;' })
|
|
|
|
|
|
const url = URL.createObjectURL(blob)
|
|
|
|
|
|
const a = document.createElement('a')
|
|
|
|
|
|
a.href = url
|
|
|
|
|
|
a.download = `payslips_${Date.now()}.xls`
|
|
|
|
|
|
a.click()
|
|
|
|
|
|
URL.revokeObjectURL(url)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.exportLoading = false
|
|
|
|
|
|
}
|
2025-12-22 10:57:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.hrm-page {
|
|
|
|
|
|
padding: 16px 20px 32px;
|
|
|
|
|
|
background: #f8f9fb;
|
|
|
|
|
|
}
|
2025-12-22 16:53:48 +08:00
|
|
|
|
.stats-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(4, 1fr);
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
2025-12-22 10:57:47 +08:00
|
|
|
|
.panel-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.metal-panel {
|
|
|
|
|
|
border: 1px solid #d7d9df;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
.panel-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
}
|
|
|
|
|
|
.actions-inline {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
2025-12-22 16:53:48 +08:00
|
|
|
|
.kpi-card {
|
|
|
|
|
|
.kpi-title {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #7a7d85;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.kpi-value {
|
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
.kpi-sub {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-22 10:57:47 +08:00
|
|
|
|
.dual-tables {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.table-half {
|
|
|
|
|
|
border: 1px dashed #e6e8ed;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.table-title {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (max-width: 1200px) {
|
2025-12-22 16:53:48 +08:00
|
|
|
|
.stats-grid {
|
|
|
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
|
}
|
2025-12-22 10:57:47 +08:00
|
|
|
|
.panel-grid {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
.dual-tables {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|