<template>
|
<el-dialog
|
v-model="dialogVisible"
|
:title="operationType === 'add' ? '新建工资表' : '编辑工资表'"
|
width="90%"
|
:close-on-click-modal="false"
|
destroy-on-close
|
@close="closeDia"
|
>
|
<div class="form-dia-body">
|
<!-- 基础资料 -->
|
<el-card class="form-card" shadow="never">
|
<template #header>
|
<span class="card-title"><span class="card-title-line">|</span> 基础资料</span>
|
<el-icon class="card-collapse"><ArrowUp /></el-icon>
|
</template>
|
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
|
<el-row :gutter="24">
|
<el-col :span="6">
|
<el-form-item label="工资主题" prop="title">
|
<el-input
|
v-model="form.title"
|
placeholder="请输入"
|
clearable
|
maxlength="20"
|
show-word-limit
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="选择部门" prop="deptId">
|
<el-select
|
v-model="form.deptId"
|
placeholder="请选择"
|
clearable
|
style="width: 100%"
|
>
|
<el-option
|
v-for="item in deptOptions"
|
:key="item.deptId"
|
:label="item.deptName"
|
:value="item.deptId"
|
/>
|
</el-select>
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="选择工资月份" prop="payMonth">
|
<el-date-picker
|
v-model="form.payMonth"
|
type="month"
|
value-format="YYYY-MM"
|
format="YYYY-MM"
|
placeholder="请选择工资月份"
|
style="width: 100%"
|
clearable
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="备注" prop="remark">
|
<el-input
|
v-model="form.remark"
|
placeholder="请输入"
|
clearable
|
/>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
</el-form>
|
</el-card>
|
|
<!-- 操作按钮 -->
|
<div class="toolbar">
|
<el-button type="primary" @click="handleGenerate">生成工资表</el-button>
|
<el-button @click="handleExport">导出</el-button>
|
<el-button @click="handleImport">导入</el-button>
|
<el-button @click="handleClear">清空</el-button>
|
<el-button @click="openAddPerson">新增人员</el-button>
|
<el-button @click="handleBatchDelete">删除</el-button>
|
<el-button @click="handleTaxForm">个税表</el-button>
|
</div>
|
|
<!-- 员工工资详情表格 -->
|
<div class="employee-table-wrap">
|
<el-table
|
ref="employeeTableRef"
|
:data="employeeList"
|
border
|
max-height="400"
|
@selection-change="onEmployeeSelectionChange"
|
>
|
<el-table-column type="selection" width="55" align="center" />
|
<el-table-column label="员工姓名" prop="staffName" minWidth="100" />
|
<el-table-column label="角色" prop="roleName" minWidth="100" />
|
<el-table-column label="部门" prop="deptName" minWidth="100" />
|
<el-table-column label="基本工资" minWidth="110">
|
<template #default="{ row }">
|
<el-input
|
v-model.number="row.basicSalary"
|
type="number"
|
placeholder="0"
|
size="small"
|
@input="row.basicSalary = parseNum(row.basicSalary)"
|
/>
|
</template>
|
</el-table-column>
|
<el-table-column label="计件工资" minWidth="110">
|
<template #default="{ row }">
|
<el-input
|
v-model.number="row.pieceworkSalary"
|
type="number"
|
placeholder="0"
|
size="small"
|
@input="row.pieceworkSalary = parseNum(row.pieceworkSalary)"
|
/>
|
</template>
|
</el-table-column>
|
<el-table-column label="计时工资" minWidth="110">
|
<template #default="{ row }">
|
<el-input
|
v-model.number="row.hourlySalary"
|
type="number"
|
placeholder="0"
|
size="small"
|
@input="row.hourlySalary = parseNum(row.hourlySalary)"
|
/>
|
</template>
|
</el-table-column>
|
<el-table-column label="其他收入" minWidth="110">
|
<template #default="{ row }">
|
<el-input
|
v-model.number="row.otherIncome"
|
type="number"
|
placeholder="0"
|
size="small"
|
@input="row.otherIncome = parseNum(row.otherIncome)"
|
/>
|
</template>
|
</el-table-column>
|
<el-table-column label="社保个人" minWidth="110">
|
<template #default="{ row }">
|
<el-input
|
v-model.number="row.socialSecurityIndividuals"
|
type="number"
|
placeholder="0"
|
size="small"
|
@input="row.socialSecurityIndividuals = parseNum(row.socialSecurityIndividuals)"
|
/>
|
</template>
|
</el-table-column>
|
<el-table-column label="公积金个人" minWidth="120">
|
<template #default="{ row }">
|
<el-input
|
v-model.number="row.providentFundIndividuals"
|
type="number"
|
placeholder="0"
|
size="small"
|
@input="row.providentFundIndividuals = parseNum(row.providentFundIndividuals)"
|
/>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="80" align="center" fixed="right">
|
<template #default="{ row }">
|
<el-button type="primary" link @click="removeEmployee(row)">删除</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
<div v-if="!employeeList.length" class="table-empty">暂无数据</div>
|
</div>
|
</div>
|
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button @click="closeDia">取消</el-button>
|
<el-button type="primary" @click="submitForm">确定</el-button>
|
</div>
|
</template>
|
|
<!-- 新增人员弹窗 -->
|
<el-dialog
|
v-model="addPersonVisible"
|
title="新增人员"
|
width="400px"
|
append-to-body
|
@close="addPersonClose"
|
>
|
<div class="add-person-tree">
|
<el-tree
|
ref="personTreeRef"
|
:data="deptStaffTree"
|
show-checkbox
|
node-key="id"
|
:props="{ label: 'label', children: 'children' }"
|
default-expand-all
|
/>
|
</div>
|
<template #footer>
|
<el-button @click="addPersonVisible = false">取消</el-button>
|
<el-button type="primary" @click="confirmAddPerson">确定</el-button>
|
</template>
|
</el-dialog>
|
|
<!-- 个税表弹窗 -->
|
<el-dialog
|
v-model="taxDialogVisible"
|
title="个税表"
|
width="700px"
|
append-to-body
|
>
|
<div class="tax-desc">个人所得税免征额:5000元</div>
|
<el-table :data="taxTableData" border style="width: 100%;margin-bottom: 20px;">
|
<el-table-column prop="level" label="级数" width="80" align="center" />
|
<el-table-column
|
prop="range"
|
label="全年应纳税所得额/元"
|
min-width="220"
|
/>
|
<el-table-column
|
prop="rate"
|
label="税率(%)"
|
width="100"
|
align="center"
|
/>
|
<el-table-column
|
prop="quickDeduction"
|
label="速算扣除数/元"
|
width="160"
|
align="center"
|
/>
|
</el-table>
|
</el-dialog>
|
</el-dialog>
|
</template>
|
|
<script setup>
|
import { ref, reactive, toRefs, computed, getCurrentInstance, nextTick } from "vue";
|
import { ArrowUp } from "@element-plus/icons-vue";
|
import { listDept } from "@/api/system/dept.js";
|
import { staffOnJobList } from "@/api/personnelManagement/monthlyStatistics.js";
|
import {
|
monthlyStatisticsAdd,
|
monthlyStatisticsUpdate,
|
monthlyStatisticsGet,
|
} from "@/api/personnelManagement/monthlyStatistics.js";
|
|
const emit = defineEmits(["update:modelValue", "close"]);
|
const props = defineProps({
|
modelValue: { type: Boolean, default: false },
|
operationType: { type: String, default: "add" },
|
row: { type: Object, default: () => ({}) },
|
});
|
|
const { proxy } = getCurrentInstance();
|
|
const dialogVisible = computed({
|
get: () => props.modelValue,
|
set: (val) => emit("update:modelValue", val),
|
});
|
|
const formRef = ref(null);
|
const employeeTableRef = ref(null);
|
const personTreeRef = ref(null);
|
const addPersonVisible = ref(false);
|
const taxDialogVisible = ref(false);
|
const deptOptions = ref([]);
|
const deptStaffTree = ref([]);
|
const employeeList = ref([]);
|
const selectedEmployees = ref([]);
|
const taxTableData = ref([
|
{ level: 1, range: "不超过36000元", rate: 3, quickDeduction: 0 },
|
{ level: 2, range: "超过36000-144000元", rate: 10, quickDeduction: 2520 },
|
{ level: 3, range: "超过144000-300000元", rate: 20, quickDeduction: 16920 },
|
{ level: 4, range: "超过300000-420000元", rate: 25, quickDeduction: 31920 },
|
{ level: 5, range: "超过420000-660000元", rate: 30, quickDeduction: 52920 },
|
{ level: 6, range: "超过660000-960000元", rate: 35, quickDeduction: 85920 },
|
{ level: 7, range: "超过960000元", rate: 45, quickDeduction: 181920 },
|
]);
|
|
function parseNum(v) {
|
if (v === "" || v == null) return 0;
|
const n = Number(v);
|
return isNaN(n) ? 0 : n;
|
}
|
|
// 基础资料表单
|
const data = reactive({
|
form: {
|
id: undefined,
|
title: "",
|
deptId: undefined,
|
payMonth: "",
|
remark: "",
|
},
|
rules: {
|
title: [{ required: true, message: "请输入工资主题", trigger: "blur" }],
|
deptId: [{ required: true, message: "请选择部门", trigger: "change" }],
|
payMonth: [{ required: true, message: "请选择工资月份", trigger: "change" }],
|
},
|
});
|
const { form, rules } = toRefs(data);
|
|
// 扁平化部门树供下拉使用
|
function flattenDept(tree, list = []) {
|
if (!tree?.length) return list;
|
tree.forEach((node) => {
|
list.push({ deptId: node.deptId, deptName: node.deptName });
|
if (node.children?.length) flattenDept(node.children, list);
|
});
|
return list;
|
}
|
|
const loadDeptOptions = () => {
|
listDept().then((res) => {
|
const tree = res.data ?? [];
|
deptOptions.value = flattenDept(tree);
|
});
|
};
|
|
// 构建 部门-人员 树(用于新增人员弹窗)
|
const loadDeptStaffTree = () => {
|
Promise.all([listDept(), staffOnJobList()]).then(([deptRes, staffRes]) => {
|
const tree = deptRes.data ?? [];
|
const staffList = staffRes.data ?? [];
|
const deptMap = new Map();
|
function walk(nodes) {
|
nodes.forEach((node) => {
|
deptMap.set(node.deptId, {
|
id: "dept_" + node.deptId,
|
deptId: node.deptId,
|
label: node.deptName,
|
type: "dept",
|
children: [],
|
});
|
if (node.children?.length) walk(node.children);
|
});
|
}
|
walk(tree);
|
staffList.forEach((s) => {
|
const deptId = s.deptId ?? s.dept_id;
|
const node = deptMap.get(deptId);
|
if (node) {
|
node.children.push({
|
id: s.id ?? s.staffId,
|
staffId: s.id ?? s.staffId,
|
label: s.staffName ?? s.name,
|
type: "staff",
|
...s,
|
});
|
}
|
});
|
deptStaffTree.value = Array.from(deptMap.values()).filter(
|
(n) => n.children && n.children.length > 0
|
);
|
});
|
};
|
|
const openDialog = (type, row) => {
|
nextTick(() => {
|
loadDeptOptions();
|
employeeList.value = [];
|
Object.assign(form.value, {
|
id: undefined,
|
title: "",
|
deptId: undefined,
|
payMonth: "",
|
remark: "",
|
});
|
if (type === "edit" && row?.id) {
|
monthlyStatisticsGet(row.id).then((res) => {
|
const d = res.data || {};
|
form.value.id = d.id;
|
form.value.title = d.title ?? d.payDateStr ?? "";
|
form.value.deptId = d.deptId;
|
form.value.payMonth = d.payMonth ?? d.payDate ?? d.payDateStr ?? "";
|
form.value.remark = d.remark ?? "";
|
employeeList.value = (d.detailList || d.employeeList || []).map((e) => ({
|
...e,
|
basicSalary: parseNum(e.basicSalary),
|
pieceworkSalary: parseNum(e.pieceworkSalary),
|
hourlySalary: parseNum(e.hourlySalary),
|
otherIncome: parseNum(e.otherIncome),
|
socialSecurityIndividuals: parseNum(e.socialSecurityIndividuals),
|
providentFundIndividuals: parseNum(e.providentFundIndividuals),
|
}));
|
});
|
}
|
});
|
};
|
|
const openAddPerson = () => {
|
loadDeptStaffTree();
|
addPersonVisible.value = true;
|
nextTick(() => {
|
personTreeRef.value?.setCheckedKeys([]);
|
});
|
};
|
|
const addPersonClose = () => {};
|
|
const confirmAddPerson = () => {
|
const tree = personTreeRef.value;
|
if (!tree) {
|
addPersonVisible.value = false;
|
return;
|
}
|
const checked = tree.getCheckedNodes();
|
const staffNodes = checked.filter((n) => n.type === "staff");
|
const existIds = new Set(employeeList.value.map((e) => e.staffId || e.id));
|
staffNodes.forEach((node) => {
|
const id = node.staffId ?? node.id;
|
if (existIds.has(id)) return;
|
existIds.add(id);
|
employeeList.value.push({
|
staffId: id,
|
id: id,
|
staffName: node.label,
|
roleName: node.roleName ?? node.role ?? "",
|
deptName: node.deptName ?? "",
|
basicSalary: 0,
|
pieceworkSalary: 0,
|
hourlySalary: 0,
|
otherIncome: 0,
|
socialSecurityIndividuals: 0,
|
providentFundIndividuals: 0,
|
});
|
});
|
addPersonVisible.value = false;
|
};
|
|
const removeEmployee = (row) => {
|
employeeList.value = employeeList.value.filter(
|
(e) => (e.staffId || e.id) !== (row.staffId || row.id)
|
);
|
};
|
|
const onEmployeeSelectionChange = (selection) => {
|
selectedEmployees.value = selection;
|
};
|
|
const handleBatchDelete = () => {
|
if (!selectedEmployees.value?.length) {
|
proxy.$modal.msgWarning("请先勾选要删除的员工");
|
return;
|
}
|
const ids = new Set(selectedEmployees.value.map((e) => e.staffId || e.id));
|
employeeList.value = employeeList.value.filter(
|
(e) => !ids.has(e.staffId || e.id)
|
);
|
};
|
|
const handleGenerate = () => {
|
proxy.$modal.msgInfo("生成工资表功能需对接后端");
|
};
|
|
const handleExport = () => {
|
proxy.$modal.msgInfo("导出功能需对接后端");
|
};
|
|
const handleImport = () => {
|
proxy.$modal.msgInfo("导入功能需对接后端");
|
};
|
|
const handleClear = () => {
|
proxy.$modal.confirm("确定清空当前员工列表吗?").then(() => {
|
employeeList.value = [];
|
}).catch(() => {});
|
};
|
|
const handleTaxForm = () => {
|
taxDialogVisible.value = true;
|
};
|
|
const submitForm = () => {
|
formRef.value?.validate((valid) => {
|
if (!valid) return;
|
const payload = {
|
...form.value,
|
detailList: employeeList.value.map((e) => ({
|
staffId: e.staffId ?? e.id,
|
staffName: e.staffName,
|
basicSalary: parseNum(e.basicSalary),
|
pieceworkSalary: parseNum(e.pieceworkSalary),
|
hourlySalary: parseNum(e.hourlySalary),
|
otherIncome: parseNum(e.otherIncome),
|
socialSecurityIndividuals: parseNum(e.socialSecurityIndividuals),
|
providentFundIndividuals: parseNum(e.providentFundIndividuals),
|
})),
|
};
|
if (props.operationType === "add") {
|
monthlyStatisticsAdd(payload).then(() => {
|
proxy.$modal.msgSuccess("新增成功");
|
closeDia();
|
});
|
} else {
|
monthlyStatisticsUpdate(payload).then(() => {
|
proxy.$modal.msgSuccess("修改成功");
|
closeDia();
|
});
|
}
|
});
|
};
|
|
const closeDia = () => {
|
dialogVisible.value = false;
|
emit("close");
|
};
|
|
defineExpose({ openDialog });
|
</script>
|
|
<style scoped>
|
.form-dia-body {
|
padding: 0;
|
}
|
.card-title-line {
|
color: #f56c6c;
|
margin-right: 4px;
|
}
|
.form-card {
|
margin-bottom: 16px;
|
}
|
.form-card :deep(.el-card__header) {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 12px 16px;
|
}
|
.card-title {
|
font-weight: 500;
|
}
|
.card-collapse {
|
color: #999;
|
cursor: pointer;
|
}
|
.toolbar {
|
margin-bottom: 16px;
|
display: flex;
|
flex-wrap: wrap;
|
gap: 10px;
|
}
|
.employee-table-wrap {
|
position: relative;
|
min-height: 120px;
|
}
|
.table-empty {
|
text-align: center;
|
padding: 24px;
|
color: #999;
|
font-size: 14px;
|
}
|
.add-person-tree {
|
max-height: 360px;
|
overflow-y: auto;
|
padding: 8px 0;
|
}
|
.tax-desc {
|
margin-bottom: 12px;
|
font-size: 14px;
|
color: #606266;
|
}
|
.dialog-footer {
|
text-align: right;
|
}
|
</style>
|