修复快捷工具无法显示
This commit is contained in:
1 parent
9898962836
commit
2e2b837a20
23 files changed
+1941
-38
No files matched your search
@@ -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,
|
||||
};
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -238,6 +238,7 @@
|
||||
<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 label="职级" width="130"><template #default="{ row }"><el-tag v-if="row.rank_name" size="small" type="primary">{{ row.rank_name }}</el-tag><span v-else>-</span></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>
|
||||
@@ -251,7 +252,13 @@
|
||||
<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="employee_id"><el-select v-model="schemeForm.employee_id" filterable style="width: 100%" @change="onSchemeEmployeeChange"><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="rank_id">
|
||||
<el-select v-model="schemeForm.rank_id" clearable placeholder="按职级带出标准薪资" style="width: 100%" :loading="rankLoading">
|
||||
<el-option v-for="item in schemeRankOptions" :key="item.id" :label="rankLabel(item)" :value="item.id" />
|
||||
</el-select>
|
||||
<div v-if="schemeRankHint" class="form-tip">{{ schemeRankHint }}</div>
|
||||
</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>
|
||||
@@ -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()]) })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -449,6 +502,7 @@ onMounted(async () => { await Promise.all([loadEmployees(), loadSchemes(), loadD
|
||||
.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; }
|
||||
.form-tip { font-size: 12px; color: #909399; line-height: 1.6; margin-top: 2px; }
|
||||
.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; }
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" :title="isEdit ? `编辑${labels.employeeLabel}` : `新增${labels.employeeLabel}`" size="520px">
|
||||
<el-drawer v-model="visible" :title="isEdit ? `编辑${labels.employeeLabel}` : isCopy ? `复制${labels.employeeLabel}` : `新增${labels.employeeLabel}`" size="520px">
|
||||
<el-alert
|
||||
v-if="isCopy"
|
||||
:title="`已按原${labels.employeeLabel}预填,账号与密码需重新填写,保存后会生成一条新记录`"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<div class="form-title">账号信息</div>
|
||||
|
||||
@@ -123,7 +131,7 @@
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleSubmit">保存</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleSubmit">{{ isEdit ? '保存' : '创建' }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
@@ -132,6 +140,7 @@
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { buildOrgTree, moduleLabels, unwrap, useOrganizationApi } from '../composables';
|
||||
import { copyName } from '@/utils/copyFields';
|
||||
|
||||
const props = defineProps({
|
||||
/** 所属模块:erp(进销存)或 oa(办公自动化) */
|
||||
@@ -146,6 +155,11 @@ const labels = computed(() => moduleLabels(props.module));
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const isEdit = ref(false);
|
||||
/**
|
||||
* 复制模式:预填原记录但走「创建」流程,保存后生成一条新记录。
|
||||
* 复制时不置 isEdit —— 账号因此保持可编辑,密码也仍然必填。
|
||||
*/
|
||||
const isCopy = ref(false);
|
||||
const editingId = ref(0);
|
||||
const formRef = ref();
|
||||
/** 编辑回填期间置 true,级联 watcher 跳过清空,避免回填值被覆盖 */
|
||||
@@ -293,16 +307,23 @@ const loadOptions = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const open = async (row, defaults = {}) => {
|
||||
/**
|
||||
* 打开抽屉。
|
||||
* @param {object|null} row 编辑的记录;传 null 表示新增
|
||||
* @param {object} options { copy: true } 复制模式;
|
||||
* { department, affiliate_unit } 新增时预选组织
|
||||
*/
|
||||
const open = async (row, options = {}) => {
|
||||
Object.assign(form, emptyForm());
|
||||
isEdit.value = Boolean(row?.id);
|
||||
isCopy.value = Boolean(options.copy);
|
||||
isEdit.value = Boolean(row?.id) && !isCopy.value;
|
||||
editingId.value = row?.id || 0;
|
||||
visible.value = true;
|
||||
formRef.value?.clearValidate();
|
||||
|
||||
await loadOptions();
|
||||
|
||||
if (isEdit.value) {
|
||||
if (row?.id) {
|
||||
try {
|
||||
const detail = unwrap(await api.value.getEmployeeDetail(row.id)) || {};
|
||||
backfilling.value = true;
|
||||
@@ -320,6 +341,13 @@ const open = async (row, defaults = {}) => {
|
||||
home_address: detail.home_address || '',
|
||||
account_status: Number(detail.account_status ?? detail.status ?? 1),
|
||||
});
|
||||
if (isCopy.value) {
|
||||
// 复制:账号是登录名且租户内唯一,必须换一个新的;密码重新设置
|
||||
form.account = '';
|
||||
form.password = '';
|
||||
form.confirmPassword = '';
|
||||
form.name = copyName(detail.name);
|
||||
}
|
||||
// watcher 为 pre-flush 异步执行,需等其执行完毕再复位标志,
|
||||
// 否则复位后 watcher 才触发,仍会清空 department / position。
|
||||
await nextTick();
|
||||
@@ -331,11 +359,11 @@ const open = async (row, defaults = {}) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (defaults.department) {
|
||||
form.department = String(defaults.department);
|
||||
if (options.department) {
|
||||
form.department = String(options.department);
|
||||
}
|
||||
if (defaults.affiliate_unit) {
|
||||
form.affiliate_unit = String(defaults.affiliate_unit);
|
||||
if (options.affiliate_unit) {
|
||||
form.affiliate_unit = String(options.affiliate_unit);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -85,10 +85,11 @@
|
||||
<el-tag :type="employeeStatusTagType(row)" size="small">{{ employeeStatusText(row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right" align="center">
|
||||
<el-table-column label="操作" width="330" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="viewDialogRef?.open(row)">查看</el-button>
|
||||
<el-button link type="primary" size="small" @click="editDialogRef?.open(row)">编辑</el-button>
|
||||
<el-button link type="warning" size="small" @click="editDialogRef?.open(row, { copy: true })">复制</el-button>
|
||||
<el-button link type="primary" size="small" @click="moveDialogRef?.open([row])">调岗</el-button>
|
||||
<el-button link type="warning" size="small" @click="handleResetPassword(row)">重置密码</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="isEdit ? '编辑组织' : '新建组织'"
|
||||
:title="isEdit ? '编辑组织' : isCopy ? '复制组织' : '新建组织'"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<el-alert
|
||||
v-if="isCopy"
|
||||
title="已按原组织预填,修改后保存会在同级下生成一条新的组织记录"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<el-form-item label="上级组织" prop="parent_id">
|
||||
<el-tree-select
|
||||
@@ -77,6 +85,7 @@
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { buildOrgTree, collectSubtreeIds, unwrap, useOrganizationApi } from '../composables';
|
||||
import { copyCode, copyName } from '@/utils/copyFields';
|
||||
|
||||
const props = defineProps({
|
||||
/** 所属模块:erp(进销存)或 oa(办公自动化) */
|
||||
@@ -90,6 +99,8 @@ const api = computed(() => useOrganizationApi(props.module));
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const isEdit = ref(false);
|
||||
/** 复制模式:预填原记录但走「创建」流程,保存后生成一条新记录 */
|
||||
const isCopy = ref(false);
|
||||
const formRef = ref();
|
||||
const orgTree = ref([]);
|
||||
const employeeOptions = ref([]);
|
||||
@@ -159,9 +170,16 @@ const loadOptions = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const open = (row, parentId) => {
|
||||
/**
|
||||
* 打开弹窗。
|
||||
* @param {object|null} row 编辑的记录;传 null 表示新建
|
||||
* @param {object} options { copy: true } 复制模式;{ parentId } 新建时预选上级组织
|
||||
*/
|
||||
const open = (row, options = {}) => {
|
||||
Object.assign(form, emptyForm());
|
||||
isEdit.value = Boolean(row);
|
||||
isCopy.value = Boolean(options.copy);
|
||||
// 复制时不能置 isEdit,否则提交会走更新、覆盖掉原记录
|
||||
isEdit.value = Boolean(row) && !isCopy.value;
|
||||
editingId.value = row?.id || 0;
|
||||
|
||||
if (row) {
|
||||
@@ -175,8 +193,13 @@ const open = (row, parentId) => {
|
||||
status: Number(row.status ?? 1),
|
||||
remark: row.remark || '',
|
||||
});
|
||||
} else if (parentId) {
|
||||
form.parent_id = Number(parentId);
|
||||
if (isCopy.value) {
|
||||
// 复制:编码在租户内唯一,必须换一个新的;名称加副本后缀便于区分
|
||||
form.org_code = copyCode(row.org_code, 20);
|
||||
form.org_name = copyName(row.org_name);
|
||||
}
|
||||
} else if (options.parentId) {
|
||||
form.parent_id = Number(options.parentId);
|
||||
form.is_company = 0;
|
||||
} else {
|
||||
form.is_company = 1;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
导入导出
|
||||
</el-button>
|
||||
<el-button :icon="Setting" @click="settingsRef?.open()">组织设置</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="orgDialogRef?.open(null, null)">新建组织</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="orgDialogRef?.open(null)">新建组织</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -75,10 +75,11 @@
|
||||
</el-tag>
|
||||
</span>
|
||||
<span class="node-actions">
|
||||
<el-button link type="primary" size="small" @click.stop="orgDialogRef?.open(null, data.id)">
|
||||
<el-button link type="primary" size="small" @click.stop="orgDialogRef?.open(null, { parentId: data.id })">
|
||||
加子级
|
||||
</el-button>
|
||||
<el-button link type="primary" size="small" @click.stop="orgDialogRef?.open(data)">编辑</el-button>
|
||||
<el-button link type="warning" size="small" @click.stop="orgDialogRef?.open(data, { copy: true })">复制</el-button>
|
||||
<el-button link type="danger" size="small" @click.stop="handleDeleteOrg(data)">删除</el-button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -168,9 +169,10 @@
|
||||
<el-tag :type="employeeStatusTagType(row)" size="small">{{ employeeStatusText(row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="170" fixed="right">
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="employeeDialogRef?.open(row)">编辑</el-button>
|
||||
<el-button link type="warning" size="small" @click="employeeDialogRef?.open(row, { copy: true })">复制</el-button>
|
||||
<el-button link type="primary" size="small" @click="moveDialogRef?.open([row])">调岗</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDeleteEmployee(row)">删除</el-button>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="isEdit ? '编辑职位' : '新增职位'"
|
||||
:title="isEdit ? '编辑职位' : isCopy ? '复制职位' : '新增职位'"
|
||||
width="560px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<el-alert
|
||||
v-if="isCopy"
|
||||
title="已按原职位预填,修改后保存会生成一条新的职位记录"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<el-form-item label="所属部门" prop="department_id">
|
||||
<el-tree-select
|
||||
@@ -35,6 +43,26 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="职级" prop="rank_id">
|
||||
<el-select
|
||||
v-model="form.rank_id"
|
||||
clearable
|
||||
placeholder="选择职级"
|
||||
style="width: 100%"
|
||||
:loading="rankLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in rankOptions"
|
||||
:key="item.id"
|
||||
:label="rankLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="form-tip">
|
||||
职级决定该职位的等级,供薪酬定薪与权限分级使用;可选项随所选部门变化(始终包含全公司通用职级)
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="form.sort" :min="0" :max="9999" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
@@ -61,9 +89,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { buildOrgTree, unwrap, useOrganizationApi } from '../composables';
|
||||
import { getJobRankList } from '@/api/jobRank';
|
||||
import { copyCode, copyName } from '@/utils/copyFields';
|
||||
|
||||
const props = defineProps({
|
||||
/** 所属模块:erp(进销存)或 oa(办公自动化) */
|
||||
@@ -77,9 +107,13 @@ const api = computed(() => useOrganizationApi(props.module));
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const isEdit = ref(false);
|
||||
/** 复制模式:预填原记录但走「创建」流程,保存后生成一条新记录 */
|
||||
const isCopy = ref(false);
|
||||
const editingId = ref(0);
|
||||
const formRef = ref();
|
||||
const departmentTree = ref([]);
|
||||
const rankOptions = ref([]);
|
||||
const rankLoading = ref(false);
|
||||
|
||||
const treeSelectProps = { value: 'id', label: 'org_name', children: 'children' };
|
||||
|
||||
@@ -88,6 +122,7 @@ const emptyForm = () => ({
|
||||
position_name: '',
|
||||
position_code: '',
|
||||
position_type: 0,
|
||||
rank_id: null,
|
||||
sort: 0,
|
||||
status: 1,
|
||||
remark: '',
|
||||
@@ -102,6 +137,34 @@ const rules = {
|
||||
],
|
||||
};
|
||||
|
||||
/** 职级下拉文案:编码 + 名称,全公司通用的带上标识 */
|
||||
const rankLabel = (item) => {
|
||||
const scope = item.department_id === 0 ? '(全公司)' : '';
|
||||
return `${item.rank_code} ${item.rank_name}${scope}`;
|
||||
};
|
||||
|
||||
/** 加载某部门可用的职级(本部门职级 + 全公司通用职级) */
|
||||
const loadRanks = async (departmentId) => {
|
||||
rankLoading.value = true;
|
||||
try {
|
||||
const params = { scope: 'assign' };
|
||||
if (departmentId) params.department_id = departmentId;
|
||||
rankOptions.value = unwrap(await getJobRankList(params)) || [];
|
||||
} catch {
|
||||
rankOptions.value = [];
|
||||
} finally {
|
||||
rankLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 切换部门后职级可选范围随之变化,重新拉取
|
||||
watch(
|
||||
() => form.department_id,
|
||||
(value) => {
|
||||
loadRanks(value);
|
||||
}
|
||||
);
|
||||
|
||||
const loadDepartments = async () => {
|
||||
try {
|
||||
departmentTree.value = buildOrgTree(unwrap(await api.value.getOrganizationList()));
|
||||
@@ -110,9 +173,16 @@ const loadDepartments = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const open = (row, departmentId) => {
|
||||
/**
|
||||
* 打开弹窗。
|
||||
* @param {object|null} row 编辑的记录;传 null 表示新增
|
||||
* @param {object} options { copy: true } 复制模式;{ departmentId } 新增时预选部门
|
||||
*/
|
||||
const open = (row, options = {}) => {
|
||||
Object.assign(form, emptyForm());
|
||||
isEdit.value = Boolean(row?.id);
|
||||
isCopy.value = Boolean(options.copy);
|
||||
// 复制时不能置 isEdit,否则提交会走更新、覆盖掉原记录
|
||||
isEdit.value = Boolean(row?.id) && !isCopy.value;
|
||||
editingId.value = row?.id || 0;
|
||||
|
||||
if (row) {
|
||||
@@ -121,16 +191,23 @@ const open = (row, departmentId) => {
|
||||
position_name: row.position_name || '',
|
||||
position_code: row.position_code || '',
|
||||
position_type: Number(row.position_type || 0),
|
||||
rank_id: row.rank_id ? Number(row.rank_id) : null,
|
||||
sort: Number(row.sort || 0),
|
||||
status: Number(row.status ?? 1),
|
||||
remark: row.remark || '',
|
||||
});
|
||||
} else if (departmentId) {
|
||||
form.department_id = Number(departmentId);
|
||||
if (isCopy.value) {
|
||||
// 复制:编码在租户内唯一,必须换一个新的;名称加副本后缀便于区分
|
||||
form.position_code = copyCode(row.position_code, 50);
|
||||
form.position_name = copyName(row.position_name);
|
||||
}
|
||||
} else if (options.departmentId) {
|
||||
form.department_id = Number(options.departmentId);
|
||||
}
|
||||
|
||||
visible.value = true;
|
||||
loadDepartments();
|
||||
loadRanks(form.department_id);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -146,6 +223,7 @@ const handleSubmit = async () => {
|
||||
position_name: form.position_name.trim(),
|
||||
position_code: form.position_code.trim(),
|
||||
position_type: form.position_type,
|
||||
rank_id: form.rank_id || 0,
|
||||
sort: form.sort,
|
||||
status: form.status,
|
||||
remark: form.remark || '',
|
||||
@@ -176,3 +254,12 @@ const handleClosed = () => {
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.6;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :icon="Refresh" @click="loadData">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="editDialogRef?.open(null, filters.department_id)">
|
||||
<el-button type="primary" :icon="Plus" @click="editDialogRef?.open(null, { departmentId: filters.department_id })">
|
||||
新增职位
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -59,6 +59,12 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="职级" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.rank_id" size="small" type="primary">{{ row.rank_name || '已设置' }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
@@ -70,9 +76,10 @@
|
||||
<el-table-column label="备注" min-width="140">
|
||||
<template #default="{ row }">{{ row.remark || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="editDialogRef?.open(row)">编辑</el-button>
|
||||
<el-button link type="warning" size="small" @click="editDialogRef?.open(row, { copy: true })">复制</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="isEdit ? '编辑职级' : isCopy ? '复制职级' : '新增职级'"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<el-alert
|
||||
v-if="isCopy"
|
||||
title="已按原职级预填,修改后保存会生成一条新的职级记录"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="职级名称" prop="rank_name">
|
||||
<el-input v-model="form.rank_name" maxlength="100" show-word-limit placeholder="例如:高级工程师" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="职级编码" prop="rank_code">
|
||||
<el-input v-model="form.rank_code" maxlength="50" show-word-limit placeholder="例如:P5 / M2" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="等级序号" prop="level_no">
|
||||
<el-input-number v-model="form.level_no" :min="0" :max="999" controls-position="right" style="width: 100%" />
|
||||
<div class="form-tip">数字越大职级越高,用于排序与「取不超过某职级的最高一级」的计算</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="适用部门" prop="department_id">
|
||||
<el-tree-select
|
||||
v-model="form.department_id"
|
||||
:data="departmentTree"
|
||||
:props="treeSelectProps"
|
||||
check-strictly
|
||||
clearable
|
||||
placeholder="不选表示全公司通用"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="form-tip">留空表示全公司通用职级,任何部门都能选用</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="标准薪资下限" prop="salary_min">
|
||||
<el-input-number v-model="form.salary_min" :min="0" :max="99999999" :precision="2" :step="1000" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="标准薪资上限" prop="salary_max">
|
||||
<el-input-number v-model="form.salary_max" :min="0" :max="99999999" :precision="2" :step="1000" controls-position="right" style="width: 100%" />
|
||||
<div class="form-tip">填 0 表示不限;薪酬方案选定该职级时会带出此区间作为参考薪资</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="form.sort" :min="0" :max="9999" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleSubmit">
|
||||
{{ isEdit ? '保存' : '创建' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import {
|
||||
checkJobRankCodeUnique,
|
||||
createJobRank,
|
||||
getJobRankDetail,
|
||||
updateJobRank,
|
||||
} from '@/api/jobRank';
|
||||
import { oaOrganizationApi } from '@/api/organization';
|
||||
import { buildOrgTree, unwrap } from '@/views/apps/organization/composables';
|
||||
import { copyCode, copyName } from '@/utils/copyFields';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const isEdit = ref(false);
|
||||
/** 复制模式:预填原记录但走「创建」流程,保存后生成一条新记录 */
|
||||
const isCopy = ref(false);
|
||||
const editingId = ref(0);
|
||||
const formRef = ref();
|
||||
const departmentTree = ref([]);
|
||||
|
||||
const treeSelectProps = { value: 'id', label: 'org_name', children: 'children' };
|
||||
|
||||
const emptyForm = () => ({
|
||||
rank_name: '',
|
||||
rank_code: '',
|
||||
level_no: 0,
|
||||
department_id: null,
|
||||
salary_min: 0,
|
||||
salary_max: 0,
|
||||
sort: 0,
|
||||
status: 1,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const form = reactive(emptyForm());
|
||||
|
||||
const rules = {
|
||||
rank_name: [{ required: true, message: '请输入职级名称', trigger: 'blur' }],
|
||||
rank_code: [
|
||||
{ required: true, message: '请输入职级编码', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z0-9_-]*$/, message: '只能包含英文、数字、下划线和横线', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
const loadDepartments = async () => {
|
||||
try {
|
||||
departmentTree.value = buildOrgTree(unwrap(await oaOrganizationApi.getOrganizationList()));
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '加载部门失败');
|
||||
}
|
||||
};
|
||||
|
||||
const open = async (row, options = {}) => {
|
||||
Object.assign(form, emptyForm());
|
||||
isCopy.value = Boolean(options.copy);
|
||||
// 复制时不能置 isEdit,否则提交会走更新、覆盖掉原记录
|
||||
isEdit.value = Boolean(row?.id) && !isCopy.value;
|
||||
editingId.value = row?.id || 0;
|
||||
|
||||
if (row) {
|
||||
try {
|
||||
const detail = unwrap(await getJobRankDetail(row.id)) || row;
|
||||
Object.assign(form, {
|
||||
rank_name: detail.rank_name || '',
|
||||
rank_code: detail.rank_code || '',
|
||||
level_no: Number(detail.level_no || 0),
|
||||
department_id: detail.department_id ? Number(detail.department_id) : null,
|
||||
salary_min: Number(detail.salary_min || 0),
|
||||
salary_max: Number(detail.salary_max || 0),
|
||||
sort: Number(detail.sort || 0),
|
||||
status: Number(detail.status ?? 1),
|
||||
remark: detail.remark || '',
|
||||
});
|
||||
if (isCopy.value) {
|
||||
// 复制:编码在租户内唯一,必须换一个新的;名称加副本后缀便于区分
|
||||
form.rank_code = copyCode(detail.rank_code, 50);
|
||||
form.rank_name = copyName(detail.rank_name);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '加载职级详情失败');
|
||||
}
|
||||
}
|
||||
|
||||
visible.value = true;
|
||||
loadDepartments();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
// 编码唯一性先在前端查一次,避免后端报错信息过长。
|
||||
// 复制模式是新记录,不排除任何已有记录。
|
||||
try {
|
||||
const excludeId = isCopy.value ? 0 : editingId.value;
|
||||
const check = unwrap(await checkJobRankCodeUnique(form.rank_code.trim(), excludeId));
|
||||
if (check && check.unique === false) {
|
||||
ElMessage.error(check.message || '职级编码已存在');
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// 校验接口异常不阻断提交,交由后端最终校验
|
||||
}
|
||||
|
||||
const payload = {
|
||||
rank_name: form.rank_name.trim(),
|
||||
rank_code: form.rank_code.trim(),
|
||||
level_no: form.level_no,
|
||||
department_id: form.department_id || 0,
|
||||
salary_min: form.salary_min,
|
||||
salary_max: form.salary_max,
|
||||
sort: form.sort,
|
||||
status: form.status,
|
||||
remark: form.remark || '',
|
||||
};
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
unwrap(await updateJobRank(editingId.value, payload));
|
||||
ElMessage.success('保存成功');
|
||||
} else {
|
||||
unwrap(await createJobRank(payload));
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '保存失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleClosed = () => {
|
||||
formRef.value?.clearValidate();
|
||||
Object.assign(form, emptyForm());
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.6;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<div class="job-rank-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>职级管理</h2>
|
||||
<p>按部门设置职位等级,供薪酬定薪与权限分级使用;未指定部门的职级为全公司通用</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :icon="Refresh" @click="loadData">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="editDialogRef?.open(null)">新增职级</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card shadow="never">
|
||||
<div class="filter-bar">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
placeholder="职级名称 / 编码"
|
||||
clearable
|
||||
class="filter-item"
|
||||
:prefix-icon="Search"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
<el-tree-select
|
||||
v-model="filters.department_id"
|
||||
:data="departmentTree"
|
||||
:props="treeSelectProps"
|
||||
check-strictly
|
||||
clearable
|
||||
placeholder="按适用部门筛选"
|
||||
class="filter-item"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
<el-select
|
||||
v-model="filters.status"
|
||||
clearable
|
||||
placeholder="状态"
|
||||
class="filter-item status-filter"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<el-table :data="pagedRows" v-loading="loading" stripe>
|
||||
<el-table-column prop="rank_code" label="职级编码" width="130" />
|
||||
<el-table-column prop="rank_name" label="职级名称" min-width="140" />
|
||||
<el-table-column prop="level_no" label="等级序号" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.level_no" size="small" type="primary">L{{ row.level_no }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="适用范围" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.department_id === 0" size="small" type="info">全公司通用</el-tag>
|
||||
<span v-else>{{ row.department_name || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标准薪资" min-width="160" align="right">
|
||||
<template #default="{ row }">{{ salaryRange(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="140">
|
||||
<template #default="{ row }">{{ row.remark || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="editDialogRef?.open(row)">编辑</el-button>
|
||||
<el-button link type="warning" size="small" @click="editDialogRef?.open(row, { copy: true })">复制</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无职级数据" :image-size="90" />
|
||||
</template>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="rows.length"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<JobRankEditDialog ref="editDialogRef" @success="loadData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Plus, Refresh, Search } from '@element-plus/icons-vue';
|
||||
import JobRankEditDialog from './components/JobRankEditDialog.vue';
|
||||
import { getJobRankList, deleteJobRank } from '@/api/jobRank';
|
||||
import { oaOrganizationApi } from '@/api/organization';
|
||||
import { buildOrgTree, unwrap } from '@/views/apps/organization/composables';
|
||||
|
||||
const editDialogRef = ref();
|
||||
const rows = ref([]);
|
||||
const loading = ref(false);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const departmentTree = ref([]);
|
||||
|
||||
const treeSelectProps = { value: 'id', label: 'org_name', children: 'children' };
|
||||
|
||||
const filters = reactive({ keyword: '', department_id: null, status: '' });
|
||||
|
||||
const pagedRows = computed(() => {
|
||||
const start = (page.value - 1) * pageSize.value;
|
||||
return rows.value.slice(start, start + pageSize.value);
|
||||
});
|
||||
|
||||
/** 标准薪资区间展示;上限为 0 表示不限 */
|
||||
const salaryRange = (row) => {
|
||||
const min = Number(row.salary_min || 0);
|
||||
const max = Number(row.salary_max || 0);
|
||||
if (min <= 0 && max <= 0) return '-';
|
||||
if (max <= 0) return `¥${min} 起`;
|
||||
return `¥${min} ~ ¥${max}`;
|
||||
};
|
||||
|
||||
const loadDepartments = async () => {
|
||||
try {
|
||||
departmentTree.value = buildOrgTree(unwrap(await oaOrganizationApi.getOrganizationList()));
|
||||
} catch {
|
||||
departmentTree.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {};
|
||||
if (filters.keyword) params.keyword = filters.keyword;
|
||||
if (filters.department_id) params.department_id = filters.department_id;
|
||||
if (filters.status !== '' && filters.status !== null) params.status = filters.status;
|
||||
|
||||
rows.value = unwrap(await getJobRankList(params)) || [];
|
||||
const maxPage = Math.max(1, Math.ceil(rows.value.length / pageSize.value));
|
||||
if (page.value > maxPage) {
|
||||
page.value = maxPage;
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '获取职级列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
page.value = 1;
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除职级「${row.rank_name}」吗?`, '删除确认', { type: 'warning' });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
unwrap(await deleteJobRank(row.id));
|
||||
ElMessage.success('删除成功');
|
||||
await loadData();
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadDepartments();
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.job-rank-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 20px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.status-filter {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.pagination-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 14px;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user