diff --git a/backend/src/api/jobRank.js b/backend/src/api/jobRank.js new file mode 100644 index 0000000..fc11213 --- /dev/null +++ b/backend/src/api/jobRank.js @@ -0,0 +1,67 @@ +import request from '@/utils/request'; + +/** + * 职级管理接口 + * + * 职级(Job Rank)定义「职位等级」:每个部门可以各设一套职级阶梯, + * department_id = 0 表示全公司通用职级,任何部门都能选用。 + * + * 数据按租户隔离:后端一律从 JWT 里取 tid,前端传的 tenant_id 一律忽略。 + */ + +/** 职级列表。scope=assign 时返回「该部门可用」的职级(部门自身 + 全公司通用) */ +export function getJobRankList(params = {}) { + return request({ + url: '/backend/jobrank/list', + method: 'get', + params, + }); +} + +export function getJobRankDetail(id) { + return request({ + url: `/backend/jobrank/detail/${id}`, + method: 'get', + }); +} + +export function createJobRank(data) { + return request({ + url: '/backend/jobrank/create', + method: 'post', + data, + }); +} + +export function updateJobRank(id, data) { + return request({ + url: `/backend/jobrank/update/${id}`, + method: 'post', + data, + }); +} + +export function deleteJobRank(id) { + return request({ + url: `/backend/jobrank/delete/${id}`, + method: 'delete', + }); +} + +/** 职级编码唯一性校验(excludeId 为编辑时要排除的自身) */ +export function checkJobRankCodeUnique(rankCode, excludeId) { + return request({ + url: '/backend/jobrank/checkCodeUnique', + method: 'get', + params: { rank_code: rankCode, exclude_id: excludeId || '' }, + }); +} + +export default { + getJobRankList, + getJobRankDetail, + createJobRank, + updateJobRank, + deleteJobRank, + checkJobRankCodeUnique, +}; diff --git a/backend/src/utils/copyFields.js b/backend/src/utils/copyFields.js new file mode 100644 index 0000000..a7b777e --- /dev/null +++ b/backend/src/utils/copyFields.js @@ -0,0 +1,31 @@ +/** + * 「复制修改」通用字段处理。 + * + * 复制 = 用已有记录把表单预填好,把「同租户内不能重复」的字段改掉, + * 让用户只改几项就能保存成一条新记录。 + * + * 各模块的复制差异都体现在调用处,这里只提供字段级的小工具。 + */ + +/** 名称加「- 副本」后缀,一眼能看出这是复制出来的那条 */ +export function copyName(name) { + const value = String(name || '').trim(); + return value ? `${value} - 副本` : '- 副本'; +} + +/** + * 编码加「-COPY」后缀。 + * + * 编码在同租户内唯一,复制时必须换一个新的。后缀只用字母与横线, + * 以符合各处的编码校验 /^[a-zA-Z0-9_-]*$/。 + * + * maxLength 传该字段的长度上限:加上后缀后超长会被前端的 + * maxlength 直接截断,所以这里先按上限裁剪原值再拼后缀。 + */ +export function copyCode(code, maxLength = 50) { + const value = String(code || '').trim(); + if (!value) return ''; + const suffix = '-COPY'; + const keep = Math.max(1, maxLength - suffix.length); + return `${value.slice(0, keep)}${suffix}`; +} diff --git a/backend/src/views/apps/oa/compensation/index.vue b/backend/src/views/apps/oa/compensation/index.vue index 5b98918..39fb43a 100644 --- a/backend/src/views/apps/oa/compensation/index.vue +++ b/backend/src/views/apps/oa/compensation/index.vue @@ -238,6 +238,7 @@ + @@ -251,7 +252,13 @@
- + + + + + +
{{ schemeRankHint }}
+
@@ -278,6 +285,7 @@ import { getCompensationDashboard, getCompensationEmployees, getCompensationSchemes, getPayrollDetail, getPayrollList, updateCompensationScheme, updatePayroll, updatePayrollStatus } from "@/api/oaCompensation" +import { getJobRankList } from "@/api/jobRank" const MoneyInput = defineComponent({ props: { modelValue: Number, label: String }, @@ -315,13 +323,58 @@ 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: "" } + return { employee_id: null, rank_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 jobRanks = ref([]) +const rankLoading = ref(false) + +/** 当前方案可选的职级:员工所在部门的职级 + 全公司通用职级 */ +const schemeRankOptions = computed(() => { + const employee = employees.value.find(item => item.id === schemeForm.employee_id) + const departmentId = Number(employee?.department || 0) + return jobRanks.value.filter(item => !item.department_id || !departmentId || Number(item.department_id) === departmentId) +}) + +/** 职级下拉文案:编码 + 名称,全公司通用的带标识 */ +function rankLabel(item) { + const scope = item.department_id === 0 ? "(全公司)" : "" + return `${item.rank_code} ${item.rank_name}${scope}` +} + +/** 已选职级的标准薪资提示 */ +const schemeRankHint = computed(() => { + const rank = jobRanks.value.find(item => item.id === schemeForm.rank_id) + if (!rank) return "" + const min = Number(rank.salary_min || 0) + const max = Number(rank.salary_max || 0) + if (min <= 0 && max <= 0) return "该职级未配置标准薪资区间" + const range = max > 0 ? `¥${money(min)} ~ ¥${money(max)}` : `¥${money(min)} 起` + return `该职级标准薪资区间:${range}(基本工资留空时自动取下限)` +}) + +async function loadJobRanks() { + rankLoading.value = true + try { + const res = await getJobRankList({}) + if (res?.code === 200) jobRanks.value = res.data || [] + } finally { rankLoading.value = false } +} + +/** 切换员工:职级可选范围随部门变化,已选职级若不再适用则清空 */ +function onSchemeEmployeeChange() { + const options = schemeRankOptions.value + if (schemeForm.rank_id && !options.some(item => item.id === schemeForm.rank_id)) { + schemeForm.rank_id = null + } +} + 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)) @@ -416,7 +469,7 @@ async function removePayroll(row) { } 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 } +function openSchemeEdit(row) { schemeEditingID.value = row.id; Object.assign(schemeForm, emptyScheme(), row); schemeForm.rank_id = row.rank_id ? Number(row.rank_id) : null; schemeFormVisible.value = true } async function saveScheme() { await schemeFormRef.value?.validate() schemeSaving.value = true @@ -431,7 +484,7 @@ async function removeScheme(row) { if (res?.code === 200) { ElMessage.success("删除成功"); loadSchemes() } else ElMessage.error(res?.msg || "删除失败") } -onMounted(async () => { await Promise.all([loadEmployees(), loadSchemes(), loadDashboard(), loadPayrolls()]) }) +onMounted(async () => { await Promise.all([loadEmployees(), loadSchemes(), loadDashboard(), loadPayrolls(), loadJobRanks()]) }) diff --git a/backend/src/views/apps/organization/components/PositionPage.vue b/backend/src/views/apps/organization/components/PositionPage.vue index 0c19bae..10ae7a0 100644 --- a/backend/src/views/apps/organization/components/PositionPage.vue +++ b/backend/src/views/apps/organization/components/PositionPage.vue @@ -7,7 +7,7 @@
刷新 - + 新增职位
@@ -59,6 +59,12 @@
+ + +