增加档案功能
This commit is contained in:
@@ -0,0 +1,460 @@
|
||||
<template>
|
||||
<div class="compensation-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>薪酬管理</h2>
|
||||
<p>维护员工薪酬方案,核算、确认并发放月度工资</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :icon="Setting" @click="schemeVisible = true">薪酬方案</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreate">新增薪资单</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-toolbar">
|
||||
<span>统计月份</span>
|
||||
<el-date-picker
|
||||
v-model="dashboardMonth"
|
||||
type="month"
|
||||
value-format="YYYY-MM"
|
||||
:clearable="false"
|
||||
style="width: 150px"
|
||||
@change="loadDashboard"
|
||||
/>
|
||||
</div>
|
||||
<div class="stats-row">
|
||||
<div class="stat-card blue">
|
||||
<div class="stat-label">核算员工</div>
|
||||
<div class="stat-value">{{ dashboard.employee_count || 0 }}</div>
|
||||
<div class="stat-sub">{{ dashboard.payroll_month || dashboardMonth }} 薪资单</div>
|
||||
</div>
|
||||
<div class="stat-card purple">
|
||||
<div class="stat-label">应发工资</div>
|
||||
<div class="stat-value">¥{{ money(dashboard.total_gross_salary) }}</div>
|
||||
<div class="stat-sub">税前及各类补贴合计</div>
|
||||
</div>
|
||||
<div class="stat-card green">
|
||||
<div class="stat-label">实发工资</div>
|
||||
<div class="stat-value">¥{{ money(dashboard.total_net_salary) }}</div>
|
||||
<div class="stat-sub">扣除五险一金及个税后</div>
|
||||
</div>
|
||||
<div class="stat-card orange">
|
||||
<div class="stat-label">待处理</div>
|
||||
<div class="stat-value">{{ (dashboard.draft_count || 0) + (dashboard.confirmed_count || 0) }}</div>
|
||||
<div class="stat-sub">草稿 {{ dashboard.draft_count || 0 }} / 待发放 {{ dashboard.confirmed_count || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<div class="filter-bar">
|
||||
<el-date-picker
|
||||
v-model="filters.payroll_month"
|
||||
type="month"
|
||||
value-format="YYYY-MM"
|
||||
placeholder="工资月份"
|
||||
clearable
|
||||
style="width: 145px"
|
||||
/>
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
placeholder="搜索员工姓名"
|
||||
clearable
|
||||
style="width: 190px"
|
||||
@keyup.enter="loadPayrolls"
|
||||
/>
|
||||
<el-select v-model="filters.status" placeholder="全部状态" clearable style="width: 125px">
|
||||
<el-option label="草稿" value="0" />
|
||||
<el-option label="已确认" value="1" />
|
||||
<el-option label="已发放" value="2" />
|
||||
<el-option label="已作废" value="3" />
|
||||
</el-select>
|
||||
<el-button type="primary" :icon="Search" @click="search">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="listLoading" :data="payrolls" stripe>
|
||||
<el-table-column prop="payroll_month" label="工资月份" width="110" />
|
||||
<el-table-column prop="employee_name" label="员工" min-width="110" />
|
||||
<el-table-column prop="department" label="部门" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="position" label="职位" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="应发工资" width="130" align="right">
|
||||
<template #default="{ row }">¥{{ money(row.gross_salary) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="扣款合计" width="125" align="right">
|
||||
<template #default="{ row }">¥{{ money(row.total_deduction) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实发工资" width="135" align="right">
|
||||
<template #default="{ row }"><strong class="net-money">¥{{ money(row.net_salary) }}</strong></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="update_time" label="更新时间" width="170">
|
||||
<template #default="{ row }">{{ dateTime(row.update_time || row.create_time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="250">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
|
||||
<el-button v-if="row.status === 0" link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-dropdown v-if="row.status < 2" trigger="click" @command="status => changeStatus(row, status)">
|
||||
<el-button link type="primary">更多<el-icon class="more-icon"><ArrowDown /></el-icon></el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-if="row.status === 0" :command="1">确认薪资单</el-dropdown-item>
|
||||
<el-dropdown-item v-if="row.status === 1" :command="2">标记为已发放</el-dropdown-item>
|
||||
<el-dropdown-item :command="3" divided>作废薪资单</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button v-if="row.status === 0" link type="danger" @click="removePayroll(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="暂无薪资单,请先新增月度薪资单" /></template>
|
||||
</el-table>
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadPayrolls"
|
||||
@size-change="loadPayrolls"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="payrollVisible" :title="editingID ? '编辑薪资单' : '新增薪资单'" width="940px" top="4vh" destroy-on-close @closed="resetPayrollForm">
|
||||
<el-form ref="payrollFormRef" :model="payrollForm" :rules="payrollRules" label-width="94px">
|
||||
<div class="form-grid main-fields">
|
||||
<el-form-item label="工资月份" prop="payroll_month">
|
||||
<el-date-picker v-model="payrollForm.payroll_month" type="month" value-format="YYYY-MM" :clearable="false" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="员工" prop="employee_id">
|
||||
<el-select v-model="payrollForm.employee_id" filterable placeholder="选择员工" style="width: 100%" @change="onEmployeeChange">
|
||||
<el-option v-for="employee in employees" :key="employee.id" :label="employeeLabel(employee)" :value="employee.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="应用方案">
|
||||
<el-select v-model="selectedSchemeID" clearable placeholder="可选:套用已有薪酬方案" style="width: 100%" @change="applyScheme">
|
||||
<el-option v-for="scheme in matchingSchemes" :key="scheme.id" :label="scheme.scheme_name" :value="scheme.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="payrollForm.remark" placeholder="选填" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="salary-section">
|
||||
<div class="section-title"><span class="dot addition" />应发项目</div>
|
||||
<div class="salary-grid">
|
||||
<MoneyInput v-model="payrollForm.base_salary" label="基本工资" />
|
||||
<MoneyInput v-model="payrollForm.post_allowance" label="岗位津贴" />
|
||||
<MoneyInput v-model="payrollForm.performance_salary" label="绩效工资" />
|
||||
<MoneyInput v-model="payrollForm.transport_allowance" label="交通补贴" />
|
||||
<MoneyInput v-model="payrollForm.meal_allowance" label="餐补" />
|
||||
<MoneyInput v-model="payrollForm.communication_allowance" label="通讯补贴" />
|
||||
<MoneyInput v-model="payrollForm.overtime_pay" label="加班工资" />
|
||||
<MoneyInput v-model="payrollForm.bonus" label="奖金" />
|
||||
<MoneyInput v-model="payrollForm.other_addition" label="其他应发" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="salary-section">
|
||||
<div class="section-title"><span class="dot deduction" />扣款项目</div>
|
||||
<div class="salary-grid">
|
||||
<MoneyInput v-model="payrollForm.leave_deduction" label="请假扣款" />
|
||||
<MoneyInput v-model="payrollForm.late_deduction" label="迟到早退扣款" />
|
||||
<MoneyInput v-model="payrollForm.other_deduction" label="其他扣款" />
|
||||
<MoneyInput v-model="payrollForm.social_insurance" label="个人社保" />
|
||||
<MoneyInput v-model="payrollForm.housing_fund" label="个人公积金" />
|
||||
<MoneyInput v-model="payrollForm.personal_income_tax" label="个人所得税" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="salary-section custom-items">
|
||||
<div class="section-title">
|
||||
<span>自定义薪资项</span>
|
||||
<el-button type="primary" link :icon="Plus" @click="addItem">添加项目</el-button>
|
||||
</div>
|
||||
<div v-for="(item, index) in payrollForm.items" :key="item.key" class="custom-item">
|
||||
<el-select v-model="item.item_type" style="width: 105px">
|
||||
<el-option label="增项" :value="1" />
|
||||
<el-option label="扣项" :value="2" />
|
||||
</el-select>
|
||||
<el-input v-model="item.item_name" placeholder="项目名称,如:补发工资" style="width: 230px" />
|
||||
<el-input-number v-model="item.amount" :min="0" :precision="2" :controls="false" placeholder="金额" style="width: 140px" />
|
||||
<el-input v-model="item.remark" placeholder="备注(选填)" />
|
||||
<el-button text type="danger" :icon="Delete" @click="payrollForm.items.splice(index, 1)" />
|
||||
</div>
|
||||
<el-empty v-if="!payrollForm.items.length" :image-size="45" description="暂无自定义项目" />
|
||||
</div>
|
||||
|
||||
<div class="amount-summary">
|
||||
<div><span>应发合计</span><strong class="addition-text">¥{{ money(grossTotal) }}</strong></div>
|
||||
<div><span>扣款合计</span><strong class="deduction-text">¥{{ money(deductionTotal) }}</strong></div>
|
||||
<div class="net"><span>实发工资</span><strong>¥{{ money(netTotal) }}</strong></div>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="payrollVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saveLoading" @click="savePayroll">保存薪资单</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="detailVisible" title="薪资单详情" width="820px">
|
||||
<template v-if="detail">
|
||||
<div class="detail-head">
|
||||
<div><strong>{{ detail.employee_name }}</strong><span>{{ detail.department || "未设置部门" }} · {{ detail.position || "未设置职位" }}</span></div>
|
||||
<div><span>{{ detail.payroll_month }} 薪资</span><el-tag :type="statusType(detail.status)">{{ statusText(detail.status) }}</el-tag></div>
|
||||
</div>
|
||||
<div class="detail-summary">
|
||||
<div><span>应发工资</span><strong>¥{{ money(detail.gross_salary) }}</strong></div>
|
||||
<div><span>扣款合计</span><strong>¥{{ money(detail.total_deduction) }}</strong></div>
|
||||
<div class="net"><span>实发工资</span><strong>¥{{ money(detail.net_salary) }}</strong></div>
|
||||
</div>
|
||||
<el-descriptions :column="3" border class="detail-descriptions">
|
||||
<el-descriptions-item v-for="entry in detailEntries" :key="entry.label" :label="entry.label">¥{{ money(entry.value) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div v-if="detail.items?.length" class="detail-items">
|
||||
<h4>自定义薪资项</h4>
|
||||
<el-table :data="detail.items" size="small">
|
||||
<el-table-column prop="item_name" label="项目名称" />
|
||||
<el-table-column label="类型" width="100"><template #default="{ row }"><el-tag :type="row.item_type === 1 ? 'success' : 'danger'" size="small">{{ row.item_type === 1 ? "增项" : "扣项" }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="金额" width="150" align="right"><template #default="{ row }">¥{{ money(row.amount) }}</template></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-if="detail.remark" class="detail-remark">备注:{{ detail.remark }}</div>
|
||||
</template>
|
||||
<template #footer><el-button @click="detailVisible = false">关闭</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="schemeVisible" title="薪酬方案管理" width="980px" destroy-on-close>
|
||||
<div class="scheme-toolbar">
|
||||
<el-button type="primary" :icon="Plus" @click="openSchemeCreate">新增方案</el-button>
|
||||
</div>
|
||||
<el-table :data="schemes" max-height="420">
|
||||
<el-table-column prop="scheme_name" label="方案名称" min-width="150" />
|
||||
<el-table-column label="员工" width="130"><template #default="{ row }">{{ employeeName(row.employee_id) }}</template></el-table-column>
|
||||
<el-table-column prop="effective_date" label="生效日期" width="115" />
|
||||
<el-table-column prop="expiry_date" label="失效日期" width="115"><template #default="{ row }">{{ row.expiry_date || "长期" }}</template></el-table-column>
|
||||
<el-table-column label="固定月薪" width="130" align="right"><template #default="{ row }">¥{{ money(fixedSchemeAmount(row)) }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="85"><template #default="{ row }"><el-tag :type="row.status === 0 ? 'success' : 'info'" size="small">{{ row.status === 0 ? "生效中" : "已停用" }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }"><el-button link type="primary" @click="openSchemeEdit(row)">编辑</el-button><el-button link type="danger" @click="removeScheme(row)">删除</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="scheme-form" v-if="schemeFormVisible">
|
||||
<div class="section-title">{{ schemeEditingID ? "编辑薪酬方案" : "新增薪酬方案" }}</div>
|
||||
<el-form ref="schemeFormRef" :model="schemeForm" :rules="schemeRules" label-width="100px">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="方案名称" prop="scheme_name"><el-input v-model="schemeForm.scheme_name" /></el-form-item>
|
||||
<el-form-item label="适用员工" prop="employee_id"><el-select v-model="schemeForm.employee_id" filterable style="width: 100%"><el-option v-for="employee in employees" :key="employee.id" :label="employeeLabel(employee)" :value="employee.id" /></el-select></el-form-item>
|
||||
<el-form-item label="生效日期" prop="effective_date"><el-date-picker v-model="schemeForm.effective_date" value-format="YYYY-MM-DD" style="width: 100%" /></el-form-item>
|
||||
<el-form-item label="失效日期"><el-date-picker v-model="schemeForm.expiry_date" value-format="YYYY-MM-DD" clearable style="width: 100%" /></el-form-item>
|
||||
</div>
|
||||
<div class="salary-grid">
|
||||
<MoneyInput v-model="schemeForm.base_salary" label="基本工资" /><MoneyInput v-model="schemeForm.post_allowance" label="岗位津贴" /><MoneyInput v-model="schemeForm.performance_salary" label="绩效工资" /><MoneyInput v-model="schemeForm.transport_allowance" label="交通补贴" /><MoneyInput v-model="schemeForm.meal_allowance" label="餐补" /><MoneyInput v-model="schemeForm.communication_allowance" label="通讯补贴" /><MoneyInput v-model="schemeForm.social_insurance_base" label="社保缴费基数" /><MoneyInput v-model="schemeForm.housing_fund_base" label="公积金缴存基数" />
|
||||
<el-form-item label="社保个人比例"><el-input-number v-model="schemeForm.social_insurance_rate" :min="0" :max="1" :step="0.01" :precision="4" :controls="false" style="width: 100%" /></el-form-item>
|
||||
<el-form-item label="公积金个人比例"><el-input-number v-model="schemeForm.housing_fund_rate" :min="0" :max="1" :step="0.01" :precision="4" :controls="false" style="width: 100%" /></el-form-item>
|
||||
<el-form-item label="方案状态"><el-radio-group v-model="schemeForm.status"><el-radio :value="0">生效</el-radio><el-radio :value="1">停用</el-radio></el-radio-group></el-form-item>
|
||||
</div>
|
||||
<el-form-item label="备注"><el-input v-model="schemeForm.remark" type="textarea" :rows="2" /></el-form-item>
|
||||
<div class="scheme-actions"><el-button @click="schemeFormVisible = false">取消</el-button><el-button type="primary" :loading="schemeSaving" @click="saveScheme">保存方案</el-button></div>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, defineComponent, h, onMounted, reactive, ref } from "vue"
|
||||
import { ArrowDown, Delete, Plus, Refresh, Search, Setting } from "@element-plus/icons-vue"
|
||||
import { ElInputNumber, ElFormItem, ElMessage, ElMessageBox } from "element-plus"
|
||||
import {
|
||||
createCompensationScheme, createPayroll, deleteCompensationScheme, deletePayroll,
|
||||
getCompensationDashboard, getCompensationEmployees, getCompensationSchemes,
|
||||
getPayrollDetail, getPayrollList, updateCompensationScheme, updatePayroll, updatePayrollStatus
|
||||
} from "@/api/oaCompensation"
|
||||
|
||||
const MoneyInput = defineComponent({
|
||||
props: { modelValue: Number, label: String },
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, { emit }) {
|
||||
return () => h(ElFormItem, { label: props.label }, {
|
||||
default: () => h(ElInputNumber, { modelValue: props.modelValue, "onUpdate:modelValue": value => emit("update:modelValue", value || 0), min: 0, precision: 2, controls: false, style: "width: 100%" })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const currentMonth = () => new Date().toISOString().slice(0, 7)
|
||||
const dashboardMonth = ref(currentMonth())
|
||||
const dashboard = ref({})
|
||||
const listLoading = ref(false)
|
||||
const payrolls = ref([])
|
||||
const employees = ref([])
|
||||
const schemes = ref([])
|
||||
const filters = reactive({ payroll_month: "", keyword: "", status: "" })
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
const payrollVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const schemeVisible = ref(false)
|
||||
const schemeFormVisible = ref(false)
|
||||
const saveLoading = ref(false)
|
||||
const schemeSaving = ref(false)
|
||||
const editingID = ref(0)
|
||||
const schemeEditingID = ref(0)
|
||||
const selectedSchemeID = ref(null)
|
||||
const detail = ref(null)
|
||||
const payrollFormRef = ref()
|
||||
const schemeFormRef = ref()
|
||||
|
||||
function emptyPayroll() {
|
||||
return { employee_id: null, scheme_id: null, payroll_month: currentMonth(), base_salary: 0, post_allowance: 0, performance_salary: 0, transport_allowance: 0, meal_allowance: 0, communication_allowance: 0, overtime_pay: 0, bonus: 0, other_addition: 0, leave_deduction: 0, late_deduction: 0, other_deduction: 0, social_insurance: 0, housing_fund: 0, personal_income_tax: 0, remark: "", items: [] }
|
||||
}
|
||||
function emptyScheme() {
|
||||
return { employee_id: null, scheme_name: "", effective_date: new Date().toISOString().slice(0, 10), expiry_date: "", base_salary: 0, post_allowance: 0, performance_salary: 0, transport_allowance: 0, meal_allowance: 0, communication_allowance: 0, social_insurance_base: 0, housing_fund_base: 0, social_insurance_rate: 0, housing_fund_rate: 0, status: 0, remark: "" }
|
||||
}
|
||||
const payrollForm = reactive(emptyPayroll())
|
||||
const schemeForm = reactive(emptyScheme())
|
||||
const payrollRules = { employee_id: [{ required: true, message: "请选择员工", trigger: "change" }], payroll_month: [{ required: true, message: "请选择工资月份", trigger: "change" }] }
|
||||
const schemeRules = { employee_id: [{ required: true, message: "请选择员工", trigger: "change" }], scheme_name: [{ required: true, message: "请输入方案名称", trigger: "blur" }], effective_date: [{ required: true, message: "请选择生效日期", trigger: "change" }] }
|
||||
|
||||
const matchingSchemes = computed(() => schemes.value.filter(item => item.employee_id === payrollForm.employee_id && item.status === 0))
|
||||
const grossTotal = computed(() => sum(payrollForm, ["base_salary", "post_allowance", "performance_salary", "transport_allowance", "meal_allowance", "communication_allowance", "overtime_pay", "bonus", "other_addition"]) + customTotal(1))
|
||||
const deductionTotal = computed(() => sum(payrollForm, ["leave_deduction", "late_deduction", "other_deduction", "social_insurance", "housing_fund", "personal_income_tax"]) + customTotal(2))
|
||||
const netTotal = computed(() => grossTotal.value - deductionTotal.value)
|
||||
const detailEntries = computed(() => detail.value ? [
|
||||
{ label: "基本工资", value: detail.value.base_salary }, { label: "岗位津贴", value: detail.value.post_allowance }, { label: "绩效工资", value: detail.value.performance_salary },
|
||||
{ label: "交通补贴", value: detail.value.transport_allowance }, { label: "餐补", value: detail.value.meal_allowance }, { label: "通讯补贴", value: detail.value.communication_allowance },
|
||||
{ label: "加班工资", value: detail.value.overtime_pay }, { label: "奖金", value: detail.value.bonus }, { label: "其他应发", value: detail.value.other_addition },
|
||||
{ label: "请假扣款", value: detail.value.leave_deduction }, { label: "迟到早退扣款", value: detail.value.late_deduction }, { label: "其他扣款", value: detail.value.other_deduction },
|
||||
{ label: "个人社保", value: detail.value.social_insurance }, { label: "个人公积金", value: detail.value.housing_fund }, { label: "个人所得税", value: detail.value.personal_income_tax }
|
||||
] : [])
|
||||
|
||||
function sum(data, keys) { return keys.reduce((total, key) => total + Number(data[key] || 0), 0) }
|
||||
function customTotal(type) { return payrollForm.items.filter(item => item.item_type === type).reduce((total, item) => total + Number(item.amount || 0), 0) }
|
||||
function money(value) { return Number(value || 0).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }
|
||||
function dateTime(value) { return value ? String(value).replace("T", " ").slice(0, 19) : "-" }
|
||||
function statusText(status) { return ["草稿", "已确认", "已发放", "已作废"][status] || "未知" }
|
||||
function statusType(status) { return ["info", "warning", "success", "danger"][status] || "info" }
|
||||
function employeeLabel(employee) { return `${employee.name}${employee.department ? `(${employee.department})` : ""}` }
|
||||
function employeeName(id) { return employees.value.find(item => item.id === id)?.name || "-" }
|
||||
function fixedSchemeAmount(item) { return sum(item, ["base_salary", "post_allowance", "performance_salary", "transport_allowance", "meal_allowance", "communication_allowance"]) }
|
||||
|
||||
async function loadEmployees() {
|
||||
const res = await getCompensationEmployees()
|
||||
if (res?.code === 200) employees.value = res.data || []
|
||||
}
|
||||
async function loadSchemes() {
|
||||
const res = await getCompensationSchemes()
|
||||
if (res?.code === 200) schemes.value = res.data || []
|
||||
}
|
||||
async function loadDashboard() {
|
||||
const res = await getCompensationDashboard({ payroll_month: dashboardMonth.value })
|
||||
if (res?.code === 200) dashboard.value = res.data || {}
|
||||
}
|
||||
async function loadPayrolls() {
|
||||
listLoading.value = true
|
||||
try {
|
||||
const res = await getPayrollList({ ...filters, page: pagination.page, pageSize: pagination.pageSize })
|
||||
if (res?.code === 200) {
|
||||
payrolls.value = res.data?.list || []
|
||||
pagination.total = res.data?.total || 0
|
||||
}
|
||||
} finally { listLoading.value = false }
|
||||
}
|
||||
function search() { pagination.page = 1; loadPayrolls() }
|
||||
function resetFilters() { Object.assign(filters, { payroll_month: "", keyword: "", status: "" }); search() }
|
||||
function assignForm(target, values) { Object.assign(target, emptyPayroll(), values) }
|
||||
|
||||
function openCreate() { editingID.value = 0; assignForm(payrollForm, emptyPayroll()); selectedSchemeID.value = null; payrollVisible.value = true }
|
||||
async function openEdit(row) {
|
||||
const res = await getPayrollDetail(row.id)
|
||||
if (res?.code !== 200) return ElMessage.error(res?.msg || "获取薪资单失败")
|
||||
editingID.value = row.id
|
||||
assignForm(payrollForm, { ...res.data, items: (res.data.items || []).map(item => ({ ...item, key: crypto.randomUUID?.() || `${Date.now()}-${item.id}` })) })
|
||||
selectedSchemeID.value = res.data.scheme_id || null
|
||||
payrollVisible.value = true
|
||||
}
|
||||
function resetPayrollForm() { payrollFormRef.value?.resetFields() }
|
||||
function onEmployeeChange() { selectedSchemeID.value = null; payrollForm.scheme_id = null }
|
||||
function applyScheme(id) {
|
||||
const scheme = schemes.value.find(item => item.id === id)
|
||||
payrollForm.scheme_id = id || null
|
||||
if (!scheme) return
|
||||
for (const key of ["base_salary", "post_allowance", "performance_salary", "transport_allowance", "meal_allowance", "communication_allowance"]) payrollForm[key] = Number(scheme[key] || 0)
|
||||
payrollForm.social_insurance = Number(scheme.social_insurance_base || 0) * Number(scheme.social_insurance_rate || 0)
|
||||
payrollForm.housing_fund = Number(scheme.housing_fund_base || 0) * Number(scheme.housing_fund_rate || 0)
|
||||
}
|
||||
function addItem() { payrollForm.items.push({ key: crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`, item_name: "", item_type: 1, amount: 0, remark: "", sort_order: payrollForm.items.length }) }
|
||||
async function savePayroll() {
|
||||
await payrollFormRef.value?.validate()
|
||||
saveLoading.value = true
|
||||
try {
|
||||
const payload = { ...payrollForm, items: payrollForm.items.map((item, index) => ({ ...item, sort_order: index })) }
|
||||
const res = editingID.value ? await updatePayroll(editingID.value, payload) : await createPayroll(payload)
|
||||
if (res?.code === 200) { ElMessage.success(editingID.value ? "薪资单已更新" : "薪资单已创建"); payrollVisible.value = false; loadPayrolls(); loadDashboard() } else ElMessage.error(res?.msg || "保存失败")
|
||||
} finally { saveLoading.value = false }
|
||||
}
|
||||
async function openDetail(row) {
|
||||
const res = await getPayrollDetail(row.id)
|
||||
if (res?.code === 200) { detail.value = res.data; detailVisible.value = true } else ElMessage.error(res?.msg || "获取详情失败")
|
||||
}
|
||||
async function changeStatus(row, status) {
|
||||
const action = { 1: "确认", 2: "标记为已发放", 3: "作废" }[status]
|
||||
try { await ElMessageBox.confirm(`确定${action}「${row.employee_name}」的 ${row.payroll_month} 薪资单吗?`, "操作确认", { type: status === 3 ? "warning" : "info" }) } catch { return }
|
||||
const res = await updatePayrollStatus(row.id, status)
|
||||
if (res?.code === 200) { ElMessage.success("操作成功"); loadPayrolls(); loadDashboard() } else ElMessage.error(res?.msg || "操作失败")
|
||||
}
|
||||
async function removePayroll(row) {
|
||||
try { await ElMessageBox.confirm(`确定删除「${row.employee_name}」的薪资单吗?`, "删除确认", { type: "warning" }) } catch { return }
|
||||
const res = await deletePayroll(row.id)
|
||||
if (res?.code === 200) { ElMessage.success("删除成功"); loadPayrolls(); loadDashboard() } else ElMessage.error(res?.msg || "删除失败")
|
||||
}
|
||||
function resetSchemeForm() { Object.assign(schemeForm, emptyScheme()) }
|
||||
function openSchemeCreate() { schemeEditingID.value = 0; resetSchemeForm(); schemeFormVisible.value = true }
|
||||
function openSchemeEdit(row) { schemeEditingID.value = row.id; Object.assign(schemeForm, emptyScheme(), row); schemeFormVisible.value = true }
|
||||
async function saveScheme() {
|
||||
await schemeFormRef.value?.validate()
|
||||
schemeSaving.value = true
|
||||
try {
|
||||
const res = schemeEditingID.value ? await updateCompensationScheme(schemeEditingID.value, schemeForm) : await createCompensationScheme(schemeForm)
|
||||
if (res?.code === 200) { ElMessage.success("方案已保存"); schemeFormVisible.value = false; loadSchemes() } else ElMessage.error(res?.msg || "保存失败")
|
||||
} finally { schemeSaving.value = false }
|
||||
}
|
||||
async function removeScheme(row) {
|
||||
try { await ElMessageBox.confirm(`确定删除薪酬方案「${row.scheme_name}」吗?`, "删除确认", { type: "warning" }) } catch { return }
|
||||
const res = await deleteCompensationScheme(row.id)
|
||||
if (res?.code === 200) { ElMessage.success("删除成功"); loadSchemes() } else ElMessage.error(res?.msg || "删除失败")
|
||||
}
|
||||
|
||||
onMounted(async () => { await Promise.all([loadEmployees(), loadSchemes(), loadDashboard(), loadPayrolls()]) })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.compensation-page { padding: 16px 20px 28px; }
|
||||
.page-header, .header-actions, .filter-bar, .dashboard-toolbar, .scheme-toolbar, .section-title, .detail-head, .detail-head > div, .custom-item { display: flex; align-items: center; }
|
||||
.page-header { justify-content: space-between; margin-bottom: 16px; }
|
||||
.page-header h2 { font-size: 20px; margin: 0 0 4px; }
|
||||
.page-header p { margin: 0; color: #909399; font-size: 13px; }
|
||||
.header-actions, .filter-bar { gap: 12px; }
|
||||
.dashboard-toolbar { justify-content: flex-end; gap: 10px; margin: -2px 0 10px; color: #606266; font-size: 13px; }
|
||||
.stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px; }
|
||||
.stat-card { padding: 15px 18px; background: #fff; border: 1px solid #ebeef5; border-radius: 8px; border-top: 3px solid #409eff; }
|
||||
.stat-card.purple { border-top-color: #8b5cf6; }.stat-card.green { border-top-color: #67c23a; }.stat-card.orange { border-top-color: #e6a23c; }
|
||||
.stat-label, .stat-sub { color: #909399; font-size: 13px; }.stat-value { margin: 7px 0 4px; font-size: 25px; font-weight: 600; color: #303133; }.stat-sub { font-size: 12px; }
|
||||
.table-card { padding: 16px; background: #fff; border: 1px solid #ebeef5; border-radius: 8px; }.filter-bar { flex-wrap: wrap; margin-bottom: 14px; }.pagination { display: flex; justify-content: flex-end; margin-top: 16px; }
|
||||
.net-money, .addition-text { color: #e6a23c; }.deduction-text { color: #f56c6c; }.more-icon { margin-left: 3px; vertical-align: -2px; }
|
||||
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 24px; }.main-fields { margin-bottom: 4px; }
|
||||
.salary-section { border-top: 1px solid #ebeef5; padding: 14px 0 2px; margin-top: 4px; }.section-title { justify-content: space-between; gap: 8px; margin-bottom: 13px; font-weight: 600; color: #303133; }.dot { width: 8px; height: 8px; border-radius: 50%; }.dot.addition { background: #67c23a; }.dot.deduction { background: #f56c6c; }.section-title:has(.dot) { justify-content: flex-start; }
|
||||
.salary-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: 20px; }
|
||||
.custom-items { padding-bottom: 10px; }.custom-item { gap: 10px; margin-bottom: 10px; }.custom-item :deep(.el-input) { flex: 1; }
|
||||
.amount-summary { display: grid; grid-template-columns: repeat(3, 1fr); background: #f5f7fa; border-radius: 6px; padding: 13px 18px; text-align: center; }.amount-summary div { border-right: 1px solid #dcdfe6; }.amount-summary div:last-child { border: none; }.amount-summary span { color: #909399; font-size: 13px; margin-right: 8px; }.amount-summary strong { font-size: 17px; }.amount-summary .net strong { color: #409eff; font-size: 20px; }
|
||||
.detail-head { justify-content: space-between; padding-bottom: 14px; border-bottom: 1px solid #ebeef5; }.detail-head > div { gap: 10px; }.detail-head strong { font-size: 18px; }.detail-head span { color: #909399; font-size: 13px; }.detail-summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; padding: 18px 0; }.detail-summary div { display: flex; flex-direction: column; gap: 5px; }.detail-summary span { font-size: 13px; color: #909399; }.detail-summary strong { font-size: 19px; color: #303133; }.detail-summary .net strong { color: #409eff; }.detail-descriptions { margin-bottom: 16px; }.detail-items h4 { margin: 12px 0 8px; }.detail-remark { margin-top: 14px; padding: 10px; color: #606266; background: #f5f7fa; font-size: 13px; }
|
||||
.scheme-toolbar { justify-content: flex-end; margin-bottom: 12px; }.scheme-form { margin-top: 18px; padding-top: 18px; border-top: 1px solid #ebeef5; }.scheme-actions { display: flex; justify-content: flex-end; gap: 10px; }
|
||||
@media (max-width: 1000px) { .stats-row { grid-template-columns: repeat(2, 1fr); }.salary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 700px) { .page-header { align-items: flex-start; gap: 12px; flex-direction: column; }.stats-row, .form-grid, .salary-grid { grid-template-columns: 1fr; }.custom-item { flex-wrap: wrap; }.amount-summary { grid-template-columns: 1fr; gap: 10px; }.amount-summary div { border: none; }.detail-summary { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user