Files
yunzerwebsiteallinone/backend/src/views/apps/oa/compensation/index.vue
T

521 lines
33 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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">
<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">
<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">
<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">
<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="120px">
<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_name ? `(${employee.department_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 lang="less" 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; }
.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; } }
// ===== 暗色主题适配 =====
html.dark & {
.page-header p {
color: var(--el-text-color-secondary);
}
.dashboard-toolbar {
color: var(--el-text-color-regular);
}
.stat-card {
background: var(--el-bg-color);
border-color: var(--el-border-color-lighter);
.stat-label,
.stat-sub {
color: var(--el-text-color-secondary);
}
.stat-value {
color: var(--el-text-color-primary);
}
}
.table-card {
background: var(--el-bg-color);
border-color: var(--el-border-color-lighter);
}
.section-title {
color: var(--el-text-color-primary);
}
.salary-section {
border-top-color: var(--el-border-color-lighter);
}
.scheme-form {
border-top-color: var(--el-border-color-lighter);
}
.detail-head {
border-bottom-color: var(--el-border-color-lighter);
span {
color: var(--el-text-color-secondary);
}
}
.detail-summary {
span {
color: var(--el-text-color-secondary);
}
strong {
color: var(--el-text-color-primary);
}
}
.detail-remark {
color: var(--el-text-color-regular);
background: var(--el-fill-color-light);
}
.amount-summary {
background: var(--el-fill-color-light);
div {
border-right-color: var(--el-border-color-lighter);
}
}
}
</style>