| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618 |
- <template>
- <el-drawer :visible="visible" direction="rtl" :size="drawerSize" custom-class="position-edit-drawer" append-to-body
- :before-close="closeDrawer" @update:visible="updateVisible" @closed="reset">
- <template slot="title">
- <div class="drawer-title">
- <span><i class="el-icon-collection-tag"></i></span>
- <div>
- <h2>{{
- readonly ? "查看岗位档案" : isUpdate ? "编辑岗位档案" : "新增岗位档案"
- }}</h2>
- </div>
- </div>
- </template>
- <div class="drawer-body">
- <el-form ref="form" :model="form" :rules="rules" :disabled="readonly" label-position="top">
- <!-- ===== 基本信息 置顶 ===== -->
- <section class="form-section basic-section">
- <div class="section-heading">
- <div>
- <i class="el-icon-collection-tag"></i><span><strong>基本信息</strong></span>
- </div>
- </div>
- <el-row :gutter="18">
- <el-col :xs="24" :sm="12" :md="8">
- <el-form-item label="岗位编码" prop="positionCode">
- <el-input v-model.trim="form.positionCode" maxlength="32" disabled placeholder="自动生成">
- <i slot="prefix" class="el-icon-cpu"></i>
- </el-input>
- </el-form-item>
- </el-col>
- <el-col :xs="24" :sm="12" :md="8">
- <el-form-item label="岗位名称" prop="name" data-field-anchor="field-name">
- <el-input v-model.trim="form.name" maxlength="64" placeholder="如:数控车床操作工">
- <i slot="prefix" class="el-icon-suitcase"></i>
- </el-input>
- </el-form-item>
- </el-col>
- <el-col :xs="24" :sm="12" :md="8">
- <el-form-item label="所属机构" prop="legalId" data-field-anchor="field-legalId">
- <el-select v-model="form.legalId" class="full-width" placeholder="请选择法人" @change="handleLegalChange">
- <el-option v-for="item in legalEntities" :key="item.id" :label="item.name" :value="item.id" />
- </el-select>
- </el-form-item>
- </el-col>
- <!-- <el-col :xs="24" :sm="12" :md="8">-->
- <!-- <el-form-item label="所属部门" prop="deptId">-->
- <!-- <el-select v-model="form.deptId" filterable class="full-width" placeholder="请选择部门">-->
- <!-- <el-option v-for="item in departmentOptions" :key="item.id" :label="item.name" :value="item.id" />-->
- <!-- </el-select>-->
- <!-- </el-form-item>-->
- <!-- </el-col>-->
- <el-col :xs="24" :sm="12" :md="8">
- <el-form-item label="岗位序列" prop="positionSequence" data-field-anchor="field-positionSequence">
- <el-select v-model="form.positionSequence" class="full-width" placeholder="请选择岗位序列"
- :disabled="sequenceLocked" @change="handleSequenceChange">
- <el-option v-for="item in sequenceList" :key="item.key || item.id" :label="item.label"
- :value="item.label" />
- </el-select>
- </el-form-item>
- </el-col>
- <!-- <el-col :xs="24" :sm="12" :md="8">-->
- <!-- <el-form-item label="岗位层级" prop="level">-->
- <!-- <el-select v-if="configuredLevelOptions.length" v-model="form.level" class="full-width" placeholder="请选择岗位层级" @change="handleLevelChange">-->
- <!-- <el-option v-for="item in configuredLevelOptions" :key="item.value" :label="item.label" :value="item.value" />-->
- <!-- </el-select>-->
- <!-- <el-input-number v-else v-model="form.level" :min="1" :max="20" controls-position="right" class="full-width" @change="handleLevelChange" />-->
- <!-- </el-form-item>-->
- <!-- </el-col>-->
- <el-col :xs="24" :sm="12" :md="8">
- <el-form-item label="岗位性质" prop="type" data-field-anchor="field-type">
- <el-select v-model="form.type" class="full-width" clearable placeholder="请选择岗位性质">
- <el-option v-for="item in typeOptions" :key="String(item.value)" :label="item.label"
- :value="item.value" />
- </el-select>
- </el-form-item>
- </el-col>
- <!-- <el-col :xs="24" :sm="12" :md="8">-->
- <!-- <el-form-item label="岗位编制" prop="staffing">-->
- <!-- <el-input-number v-model="form.staffing" :min="0" :max="999" controls-position="right" class="full-width" />-->
- <!-- </el-form-item>-->
- <!-- </el-col>-->
- <el-col :xs="24" :sm="12" :md="8">
- <el-form-item label="岗位状态">
- <el-select v-model="form.status" class="full-width">
- <el-option label="启用" :value="1" />
- <el-option label="停用" :value="2" />
- <el-option label="待撤销" :value="3" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col v-if="form.status === 3" :xs="24" :sm="12" :md="8">
- <el-form-item label="计划撤销日期">
- <el-date-picker v-model="form.revokeDate" type="date" value-format="yyyy-MM-dd"
- :picker-options="revokePickerOptions" class="full-width" />
- </el-form-item>
- </el-col>
- </el-row>
- </section>
- <!-- ===== 岗位层级配置:左层级清单 / 右配置面板 ===== -->
- <div class="level-config-layout" :class="{ 'is-empty': !form.levels.length }">
- <template v-if="form.levels.length">
- <!-- 左:当前序列下的启用层级清单(红/黄/绿状态点) -->
- <aside class="level-rail">
- <div class="rail-title">
- 岗位层级
- <span class="rail-count">{{ form.levels.length }} 级</span>
- </div>
- <div v-for="item in form.levels" :key="item.level" class="rail-item"
- :class="{ active: item.level === activeLevel }" @click="applyActiveLevel(item.level)">
- <div class="rail-item-head">
- <span class="rail-code">{{ item.level }}</span>
- <span class="status-dot" :class="levelStatus(item).cls"></span>
- </div>
- <div class="rail-item-foot">
- <span class="status-text">{{
- levelStatus(item).text
- }}</span>
- <el-tag v-if="isApexLevel(item)" size="mini" type="warning" effect="plain">最高层级</el-tag>
- </div>
- </div>
- </aside>
- <!-- 右:当前层级的配置面板 -->
- <div class="level-panel">
- <div class="panel-header">
- <div class="panel-title">
- <strong>{{ activeLevel }}</strong>
- <span class="panel-subtitle">层级配置</span>
- </div>
- <div class="panel-meta">
- <span v-if="pendingModules.length" class="pending-text">
- 待配置:{{ pendingModules.join("、") }}
- </span>
- <span v-else class="pending-done">
- <i class="el-icon-circle-check"></i> 已完整配置
- </span>
- </div>
- </div>
- <div class="status-tabs module-tabs" role="tablist">
- <button v-for="m in moduleTabs" :key="m.key" type="button" :class="{ active: activeModule === m.key }"
- :disabled="m.disabled" @click="!m.disabled && (activeModule = m.key)">
- {{ m.label }}
- <span v-if="m.moduleKey">{{
- m.disabled ? "免配" : moduleStatusText(m.moduleKey)
- }}</span>
- </button>
- </div>
- <div v-if="isApexLevel(currentLevelMeta)" class="apex-tip">
- <i class="el-icon-top"></i>
- 「{{ activeLevel }}」是本岗位序列的最高层级,上面已经没有更高的层级了。
- </div>
- <!-- 定薪定责 -->
- <div v-show="activeModule === 'salary'" class="module-panel">
- <div class="duty-layout">
- <section class="form-section salary-section">
- <div class="section-heading">
- <div>
- <i class="el-icon-coin"></i><span><strong>定薪标准</strong></span>
- </div>
- <span class="salary-summary">{{ salarySummary }}</span>
- </div>
- <el-row :gutter="16">
- <el-col :xs="24" :sm="8"><el-form-item label="薪资下限" prop="salaryMin"
- data-field-anchor="field-salaryMin"><el-input-number v-model="form.salaryMin" :min="0"
- :step="500" controls-position="right" class="full-width"
- @change="syncSalaryMid" /></el-form-item></el-col>
- <el-col :xs="24" :sm="8"><el-form-item label="薪资中位值"><el-input-number v-model="form.salaryMid"
- :min="0" :step="500" controls-position="right" class="full-width" /></el-form-item></el-col>
- <el-col :xs="24" :sm="8"><el-form-item label="薪资上限" prop="salaryMax"
- data-field-anchor="field-salaryMax"><el-input-number v-model="form.salaryMax" :min="0"
- :step="500" controls-position="right" class="full-width"
- @change="syncSalaryMid" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="薪资模板"><el-select v-model="form.salaryTemplateId"
- filterable allow-create default-first-option class="full-width"
- placeholder="关联薪资模板名称或编码"><el-option v-for="item in availableSalaryTemplates" :key="item.id"
- :label="item.name" :value="item.name" /></el-select></el-form-item></el-col>
- </el-row>
- </section>
- <section class="form-section qualification-section">
- <div class="section-heading">
- <div>
- <i class="el-icon-user"></i><span><strong>任职条件</strong></span>
- </div>
- </div>
- <el-row :gutter="16" class="qualification-basics">
- <el-col :xs="24" :sm="8"><el-form-item label="学历要求"><el-select v-model="form.eduReq"
- class="full-width"><el-option label="初中及以上" value="初中及以上" /><el-option label="中专及以上"
- value="中专及以上" /><el-option label="大专及以上" value="大专及以上" /><el-option label="本科及以上"
- value="本科及以上" /></el-select></el-form-item></el-col>
- <el-col :xs="24" :sm="8"><el-form-item label="性别要求"><el-select v-model="form.genderReq"
- class="full-width"><el-option label="不限" value="不限" /><el-option label="男"
- value="男" /><el-option label="女" value="女" /></el-select></el-form-item></el-col>
- <el-col :xs="24" :sm="8"><el-form-item label="经验要求(年)"><el-input-number v-model="form.expReq"
- :min="0" :max="30" controls-position="right" class="full-width" /></el-form-item></el-col>
- </el-row>
- <el-row :gutter="16" class="qualification-timing">
- <el-col :xs="24" :sm="12">
- <div class="age-panel">
- <el-form-item label="年龄范围" class="age-field">
- <div class="age-value">
- <i class="el-icon-time"></i>{{ form.ageRange[0] }} -
- {{ form.ageRange[1] }} 岁
- </div>
- <el-slider v-model="form.ageRange" range :min="18" :max="65" />
- <div class="age-scale">
- <span>18 岁</span><span>65 岁</span>
- </div>
- </el-form-item>
- </div>
- </el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="创建日期" class="date-panel"><el-date-picker
- v-model="form.effectDate" value-format="yyyy-MM-dd" type="date"
- class="full-width" /></el-form-item></el-col>
- </el-row>
- <el-form-item label="工作环境" class="work-environment-field"><el-input v-model.trim="form.workEnv"
- placeholder="如:车间、办公室、高温或粉尘环境等"><i slot="prefix"
- class="el-icon-office-building"></i></el-input></el-form-item>
- </section>
- <section class="form-section responsibility-section">
- <div class="section-heading">
- <div>
- <i class="el-icon-document-checked"></i><span><strong>岗位职责</strong></span>
- </div>
- <span class="duty-counter">已填写 {{ dutyCount }} 条</span>
- </div>
- <el-form-item label="岗位职责" required data-field-anchor="field-dutyText"><el-input v-model="dutyText"
- type="textarea" :rows="6" placeholder="1. 负责…… 2. 落实…… 3. 协同……" /></el-form-item>
- </section>
- </div>
- </div>
- <!-- 技能资质 -->
- <div v-show="activeModule === 'skill'" class="module-panel">
- <section class="form-section skill-section">
- <div class="section-heading">
- <div>
- <i class="el-icon-s-check"></i><span><strong>技能资质</strong></span>
- </div>
- </div>
- <div class="form-subtitle-row" data-field-anchor="field-skills">
- <div class="form-subtitle">必备技能</div>
- <el-button v-if="!readonly" type="primary" size="mini" @click="addSkill">新增技能</el-button>
- </div>
- <el-table :data="form.skills" border size="small" class="skill-cert-table" empty-text="暂无必备技能">
- <el-table-column type="index" label="序号" width="55" align="center" />
- <el-table-column label="等级" width="160">
- <template slot-scope="{ row }">
- <el-select v-model="row.level" placeholder="等级" class="full-width">
- <el-option v-for="(item, optIdx) in skillLevelOptions"
- :key="'skill-level-' + optIdx + '-' + item.value" :label="item.label" :value="item.value" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column label="技能名称" min-width="320">
- <template slot-scope="{ row }">
- <el-input v-model.trim="row.name" placeholder="技能名称" />
- </template>
- </el-table-column>
- <el-table-column label="必备" width="110" align="center">
- <template slot-scope="{ row }">
- <el-switch v-model="row.required" />
- </template>
- </el-table-column>
- <el-table-column v-if="!readonly" label="操作" width="90" align="center">
- <template slot-scope="{ $index }">
- <el-button type="text" class="skill-cert-delete" @click="removeSkill($index)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- <div class="form-subtitle-row">
- <div class="form-subtitle">资质证书</div>
- <el-button v-if="!readonly" type="primary" size="mini" @click="addCert">新增证书</el-button>
- </div>
- <el-table :data="form.certificates" border size="small" class="skill-cert-table" empty-text="暂无资质证书">
- <el-table-column type="index" label="序号" width="55" align="center" />
- <el-table-column label="证书分类" width="160">
- <template slot-scope="{ row }">
- <el-select v-model="row.category" placeholder="证书分类" class="full-width">
- <el-option v-for="(item, optIdx) in idTypeOptions"
- :key="'id-type-' + optIdx + '-' + item.value" :label="item.label" :value="item.value" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column label="证书名称" min-width="320">
- <template slot-scope="{ row }">
- <el-input v-model.trim="row.name" placeholder="证书名称" />
- </template>
- </el-table-column>
- <el-table-column v-if="!readonly" label="操作" width="90" align="center">
- <template slot-scope="{ $index }">
- <el-button type="text" class="skill-cert-delete" @click="removeCert($index)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- <div class="form-subtitle">推荐培训</div>
- <!-- 数据源:EHS 线上培训计划(按所属机构过滤);提交 trainingId,选项 value 为培训计划 id -->
- <el-select v-model="form.trainings" multiple filterable :loading="trainingLoading" class="full-width"
- placeholder="请选择关联培训课程">
- <el-option v-for="(item, optIdx) in trainingOptions" :key="'training-' + optIdx + '-' + item.value"
- :label="item.label" :value="item.value" />
- </el-select>
- </section>
- </div>
- <!-- 暂时下线「晋升路径」配置(2026-09-29):页签与表单均隐藏,接口参数同步去除,
- 恢复时取消下方注释并同步恢复 moduleTabs / pendingModules / levelStatus 中的 promote 相关代码
- <div v-show="activeModule === 'promote'" class="module-panel">
- <section class="form-section promote-section">
- <div class="section-heading">
- <div>
- <i class="el-icon-s-opportunity"></i>
- <div>
- <strong>晋升路径</strong>
- </div>
- </div>
- <el-button v-if="!readonly" type="primary" plain size="small" icon="el-icon-plus"
- @click="addPromotePath">
- 新增路径
- </el-button>
- </div>
- <div v-for="(path, index) in form.promote.paths" :key="index" class="promote-path-card">
- <div class="promote-path-head">
- <span class="promote-path-index">路径 {{ index + 1 }}</span>
- <el-button v-if="!readonly" type="danger" plain size="mini" icon="el-icon-delete"
- :disabled="form.promote.paths.length <= 1" @click="removePromotePath(index)">
- 删除
- </el-button>
- </div>
- <el-row :gutter="18">
- <el-col :xs="24" :md="8">
- <el-form-item label="向上流动层级" :data-field-anchor="'field-promote-up-' + index">
- <el-select v-model="path.up" filterable clearable class="full-width"
- placeholder="请选择向上流动岗位序列层级" :loading="promoteTargetsLoading">
- <el-option v-for="item in upwardOptions" :key="'up-' + item.id"
- :label="promoteOptionLabel(item)" :value="item.name" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :xs="24" :md="8">
- <el-form-item label="向下流动层级" :data-field-anchor="'field-promote-down-' + index">
- <el-select v-model="path.down" filterable clearable class="full-width"
- placeholder="请选择向下流动岗位序列层级" :loading="promoteTargetsLoading">
- <el-option v-for="item in downwardOptions" :key="'down-' + item.id"
- :label="promoteOptionLabel(item)" :value="item.name" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :xs="24" :md="8">
- <el-form-item label="平级流动层级" :data-field-anchor="'field-promote-lateral-' + index">
- <el-select v-model="path.lateral" filterable clearable class="full-width"
- placeholder="请选择平级流动岗位序列层级" :loading="promoteTargetsLoading">
- <el-option v-for="item in lateralOptions" :key="'lateral-' + item.id"
- :label="promoteOptionLabel(item)" :value="item.name" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- </div>
- <el-form-item label="晋升条件" class="promote-conditions">
- <el-select v-model="form.promote.conditions" multiple filterable allow-create default-first-option
- class="full-width" placeholder="公共条件,输入条件后回车,如:绩效≥B">
- <el-option v-for="(item, optIdx) in conditionOptions"
- :key="'condition-' + optIdx + '-' + item.value" :label="item.label" :value="item.value" />
- </el-select>
- </el-form-item>
- </section>
- </div>
- 晋升路径暂时下线结束 -->
- </div>
- </template>
- <div v-else class="level-empty-tip">
- <i class="el-icon-info"></i>
- {{ levelMessage }}
- </div>
- </div>
- </el-form>
- </div>
- <div class="drawer-footer">
- <div v-if="readonly">
- <el-button @click="closeDrawer()">关闭</el-button>
- <el-button
- v-if="$hasPermission(isUpdate ? 'hr:hrPosition:update' : 'hr:hrPosition:save')"
- type="primary"
- @click="$emit('edit')"
- >编辑</el-button>
- </div>
- <div v-else>
- <el-button @click="closeDrawer()">取消</el-button>
- <el-button
- v-if="$hasPermission(isUpdate ? 'hr:hrPosition:update' : 'hr:hrPosition:save')"
- type="primary"
- :loading="saving"
- @click="save"
- >
- {{ saving ? "正在保存" : "保存岗位" }}
- </el-button>
- </div>
- </div>
- </el-drawer>
- </template>
- <script>
- import { getDict, getDictByCode } from "@/api/sys";
- // 薪资模板下拉数据源:复用薪资核算「薪资模板设定」列表接口(按法人 companyId 返回摘要)
- import { getSalaryTemplateList } from "@/api/salary/template";
- /* 推荐培训数据源:EHS 线上培训计划分页(参数 deptId 实为机构 id) */
- import { getOnlineTrainingPlanPage } from "@/api/ehs";
- import {
- getPositionSequencePage,
- getPromotionTargets,
- adaptPromotionLevelOptions,
- } from "@/api/hr";
- import { formatDate } from "@/utils/dateUtils";
- /* 所属机构默认值取登录缓存(loginChangeGroupVOList) */
- import { getCurrentOrgId } from "@/utils/current-org";
- /** 本地当天日期 yyyy-MM-dd(避免 toISOString 用 UTC 错日) */
- function todayLocalDate() {
- return formatDate(new Date(), "yyyy-MM-dd");
- }
- // 每个层级 Tab 独立维护的配置字段(定薪/任职/职责/技能/晋升)
- const LEVEL_FIELDS = [
- "salaryMin",
- "salaryMid",
- "salaryMax",
- "salaryTemplateId",
- "perfSchemeId",
- "eduReq",
- "genderReq",
- "expReq",
- "ageRange",
- "effectDate",
- "workEnv",
- "skills",
- "certificates",
- "trainings",
- /* 推荐培训 id→名称映射:随层级配置一起保存(提交时要带 trainingName) */
- "trainingNameById",
- "promote",
- ];
- const emptyForm = () => ({
- id: null,
- positionCode: "",
- name: "",
- /* 所属机构:新增时由 initForm 按登录缓存机构填充 */
- legalId: null,
- deptId: null,
- level: 1,
- positionName: "",
- positionSequence: "",
- positionLevel: "",
- sequenceId: null,
- type: "",
- staffing: 0,
- actual: 0,
- status: 1,
- revokeDate: "",
- version: 1,
- salaryMin: 0,
- salaryMid: 0,
- salaryMax: 0,
- salaryTemplateId: "",
- perfSchemeId: "",
- workEnv: "",
- ageRange: [18, 60],
- genderReq: "不限",
- eduReq: "中专及以上",
- expReq: 0,
- effectDate: todayLocalDate(),
- duties: [],
- skills: [],
- certificates: [],
- trainings: [],
- promote: { paths: [{ up: "", down: "", lateral: "" }], conditions: [] },
- employees: [],
- history: [],
- updatedAt: "",
- levels: [],
- });
- export default {
- name: "PositionEditDialog",
- props: {
- visible: Boolean,
- data: Object,
- // 只读模式:详情查看用,底部显示「编辑档案」按钮
- readonly: {
- type: Boolean,
- default: false,
- },
- nextIndex: {
- type: Number,
- default: 1,
- },
- positionList: {
- type: Array,
- default: () => [],
- },
- legalEntities: {
- type: Array,
- default: () => [],
- },
- departments: {
- type: Array,
- default: () => [],
- },
- sequenceOptions: {
- type: Array,
- default: () => [],
- },
- },
- data() {
- return {
- activeLevel: "",
- // 当前层级下的子模块:salary 定薪定责 / skill 技能资质 / promote 晋升路径
- activeModule: "salary",
- // 岗位序列下拉:每次打开弹窗从接口刷新最新数据
- sequenceList: [],
- form: emptyForm(),
- dutyText: "",
- saving: false,
- // 薪资模板下拉选项(接口加载):{ id, name },name 取 templateName
- salaryTemplateOptions: [],
- // 晋升条件:/system/dict/getByCode/Promotion_Conditions
- conditionOptions: [],
- // 必备技能等级:/system/dict/getByCode/Required_Skills
- skillLevelOptions: [],
- // 推荐培训:EHS 线上培训计划分页(按所属机构过滤),值为培训计划 id
- trainingOptions: [],
- trainingLoading: false,
- // 岗位性质:/system/dict/getByCode/position_nature,无静态兜底
- typeOptions: [],
- // 资质证书类别:/system/dict/id_type
- idTypeOptions: [],
- // 晋升路径目标:分别来自 upward / downward / lateral 流动岗位序列层级接口
- upwardOptions: [],
- downwardOptions: [],
- lateralOptions: [],
- promoteTargetsLoading: false,
- revokePickerOptions: {
- disabledDate: (date) =>
- date.getTime() < new Date().setHours(0, 0, 0, 0),
- },
- rules: {
- name: [{ required: true, message: "请输入岗位名称", trigger: "blur" }],
- legalId: [{ required: true, message: "请选择法人", trigger: "change" }],
- deptId: [{ required: true, message: "请选择部门", trigger: "change" }],
- positionSequence: [
- { required: true, message: "请选择岗位序列", trigger: "change" },
- ],
- type: [
- { required: true, message: "请选择岗位性质", trigger: "change" },
- ],
- level: [
- { required: true, message: "请输入岗位层级", trigger: "change" },
- ],
- staffing: [
- { required: true, message: "请输入编制人数", trigger: "change" },
- ],
- },
- };
- },
- computed: {
- isUpdate() {
- return Boolean(this.data?.id);
- },
- // 新增时岗位序列与左侧所选序列绑定,不可在表单中改选
- sequenceLocked() {
- return !this.isUpdate && Boolean(this.data?.positionSequence);
- },
- // 当前激活层级的配置条目(form.levels 中的一项)
- currentLevelMeta() {
- if (!this.activeLevel) return null;
- return (
- this.form.levels.find((item) => item.level === this.activeLevel) ||
- null
- );
- },
- // 当前层级尚未配置完成的模块名(面板头部「待配置:xxx」)
- pendingModules() {
- const item = this.currentLevelMeta;
- if (!item) return [];
- const names = { salary: "定薪标准", skill: "技能资质" };
- const list = Object.keys(names)
- .filter((key) => !this.moduleConfigured(item, key))
- .map((key) => names[key]);
- // 晋升路径暂时下线,不参与「待配置」提示;恢复时取消注释
- // if (!this.isApexLevel(item) && !this.moduleConfigured(item, "promote")) {
- // list.push("晋升路径");
- // }
- return list;
- },
- // 每个层级下的子模块 Tab(定薪/技能);晋升路径暂时下线,恢复时加回 promote 项
- moduleTabs() {
- const apex = this.isApexLevel(this.currentLevelMeta);
- return [
- { key: "salary", label: "定薪标准", moduleKey: "salary" },
- { key: "skill", label: "技能资质", moduleKey: "skill" },
- // 晋升路径暂时下线(2026-09-29),恢复时取消注释
- // {
- // key: "promote",
- // label: "晋升路径",
- // moduleKey: "promote",
- // disabled: apex,
- // },
- ];
- },
- // 表单 prop → 定位信息(提交失败时切 Tab 并滚动)
- formFieldAnchors() {
- return {
- name: { module: "basic", anchor: "field-name" },
- legalId: { module: "basic", anchor: "field-legalId" },
- deptId: { module: "basic", anchor: "field-deptId" },
- positionSequence: { module: "basic", anchor: "field-positionSequence" },
- type: { module: "basic", anchor: "field-type" },
- level: { module: "basic", anchor: "field-level" },
- staffing: { module: "basic", anchor: "field-staffing" },
- salaryMin: { module: "salary", anchor: "field-salaryMin" },
- salaryMax: { module: "salary", anchor: "field-salaryMax" },
- dutyText: { module: "salary", anchor: "field-dutyText" },
- };
- },
- // 层级配置区占位提示:区分「未选序列」与「所选序列无启用层级」
- levelMessage() {
- if (!this.form.positionSequence) {
- return "请先选择岗位序列,以加载该序列的启用层级配置";
- }
- if (!this.form.levels.length) {
- return "当前岗位序列暂无启用的岗位层级,请先在「岗位序列 · 维护层级」中配置";
- }
- return "";
- },
- departmentOptions() {
- return this.departments.filter(
- (item) => String(item.legalId) === String(this.form.legalId),
- );
- },
- availableSalaryTemplates() {
- // 已按当前法人 companyId 拉取,无需本地再过滤 legalIds
- return this.salaryTemplateOptions;
- },
- configuredLevelOptions() {
- const sequence = this.sequenceList.find(
- (item) =>
- item.label === this.form.positionSequence ||
- item.name === this.form.positionSequence,
- );
- return (sequence?.children || [])
- .map((item) => ({
- value:
- item.value ??
- Number(String(item.id || item.label).match(/\d+/)?.[0] || 0),
- label: item.label,
- ...item,
- }))
- .filter((item) => item.label);
- },
- salarySummary() {
- const min = Number(this.form.salaryMin || 0).toLocaleString("zh-CN");
- const max = Number(this.form.salaryMax || 0).toLocaleString("zh-CN");
- return `¥${min} - ¥${max} / 月`;
- },
- dutyCount() {
- return this.dutyText
- .split("\n")
- .map((item) => item.trim())
- .filter(Boolean).length;
- },
- drawerSize() {
- return window.innerWidth < 1000 ? "96%" : "900px";
- },
- },
- created() {
- this.loadDictOptions();
- this.loadTypeOptions();
- this.loadIdTypeOptions();
- this.loadSequences();
- },
- watch: {
- visible(value) {
- if (value) {
- this.initialize();
- }
- },
- // 切换层级 Tab:保存当前层级配置,加载目标层级配置(晋升目标查询随晋升路径一起下线)
- activeLevel(newVal, oldVal) {
- if (!newVal) return;
- if (oldVal) this.saveLevelConfig(oldVal);
- this.loadLevelConfig(newVal);
- this.form.positionLevel = newVal;
- // this.loadPromoteTargetOptions();
- },
- },
- methods: {
- // 岗位序列下拉:每次打开从接口拉取最新启用序列及其层级
- async loadSequences() {
- this.sequenceList = this.sequenceOptions.slice();
- try {
- const page = await getPositionSequencePage({
- pageNum: 1,
- size: 1000,
- status: "1",
- });
- const list = (page?.list || page?.records || [])
- .map((item) => {
- const name =
- item.sequence || item.positionSequence || item.name || "";
- return {
- id: item.id,
- label: name,
- name,
- children: (item.levelList || []).map((level, index) => ({
- id: level.id ?? `lvl-${index}`,
- label: level.level || level.label || "",
- value: level.level || level.positionLevel || level.label || "",
- levelGrade: Number(level.levelGrade) || 0,
- })),
- };
- })
- .filter((item) => item.label);
- if (list.length) this.sequenceList = list;
- } catch (error) {
- // 接口失败时保留 prop 传入的序列
- }
- },
- // 晋升条件 / 必备技能等级 / 推荐培训:按字典编码拉取
- async loadDictOptions() {
- const load = async (code, field) => {
- try {
- const items = await getDictByCode(code);
- this[field] = items.filter((item) => item.value);
- } catch (error) {
- this[field] = [];
- this.$message.error(error.message || `${code} 字典加载失败`);
- }
- };
- await Promise.all([
- // 晋升路径暂时下线,晋升条件字典不再拉取;恢复时取消注释
- // load("Promotion_Conditions", "conditionOptions"),
- load("Required_Skills", "skillLevelOptions"),
- ]);
- },
- /**
- * 推荐培训名称映射同步:接口提交 trainings 要求 trainingId + trainingName 都必填,
- * 把下拉选项(id → trainingName)合并进每个层级的 trainingNameById;
- * 已从接口回显的名称优先(那是岗位档案保存时的名称快照)。
- */
- syncTrainingNameMap() {
- const fromOptions = {};
- this.trainingOptions.forEach((option) => {
- const id = String(option?.value ?? "").trim();
- const name = String(option?.label ?? "").trim();
- if (id && name) fromOptions[id] = name;
- });
- (this.form.levels || []).forEach((level) => {
- this.$set(level, "trainingNameById", {
- ...fromOptions,
- ...(level.trainingNameById || {}),
- });
- });
- },
- /**
- * 推荐培训:数据源为 EHS「线上培训计划」分页接口(非字典)。
- * ★ 接口参数 deptId 实际传本项目「机构 id」(= 表单所属机构 legalId / organizationId)。
- * 选项 value 用培训计划主键 id(提交 trainingId),label 用 trainingName。
- */
- async loadTrainingOptions() {
- const orgId = this.form.legalId;
- if (orgId == null || orgId === "") {
- this.trainingOptions = [];
- return;
- }
- this.trainingLoading = true;
- try {
- const page = await getOnlineTrainingPlanPage({
- deptId: orgId,
- pageNum: 1,
- size: 200,
- });
- this.trainingOptions = (page.list || [])
- .map((item) => {
- const id = String(item.id ?? "").trim();
- const label = String(item.trainingName ?? "").trim();
- if (!id || !label) return null;
- return { label, value: id };
- })
- .filter(Boolean);
- /* 选项(id + 名称)同步进各层级的 trainingNameById,提交时接口要求带 trainingName */
- this.syncTrainingNameMap();
- } catch (error) {
- this.trainingOptions = [];
- this.$message.error(error.message || "线上培训计划加载失败");
- } finally {
- this.trainingLoading = false;
- }
- },
- // 资质证书类别:/system/dict/id_type
- async loadIdTypeOptions() {
- try {
- const items = await getDict("id_type");
- this.idTypeOptions = items.filter((item) => item.value);
- } catch (error) {
- this.idTypeOptions = [];
- this.$message.error(error.message || "证书类别字典加载失败");
- }
- },
- // 岗位性质只走字典接口 /system/dict/getByCode/position_nature,无静态兜底
- // 选项/回填/提交(positionType)统一使用「字典 id」,label 用字典文案
- async loadTypeOptions() {
- try {
- const items = await getDictByCode("position_nature");
- this.typeOptions = items
- .map((item) => {
- const label = String(item.label ?? item.value ?? "").trim();
- const id = String(item.id ?? item.key ?? "").trim();
- if (!label || !id) return null;
- return {
- label,
- value: id,
- key: item.key != null ? String(item.key) : id,
- };
- })
- .filter(Boolean);
- this.normalizeFormType();
- } catch (error) {
- this.typeOptions = [];
- this.$message.error(error.message || "岗位性质字典加载失败");
- }
- },
- // 后端返回值(字典 id / 历史文案 / 编码)统一转成选项 value(字典 id),避免下拉显示空
- resolveTypeValue(raw) {
- if (raw === null || raw === undefined || raw === "") return "";
- const text = String(raw);
- const hit = this.typeOptions.find(
- (item) =>
- String(item.value) === text ||
- String(item.label) === text ||
- String(item.key) === text,
- );
- return hit ? hit.value : text;
- },
- normalizeFormType() {
- if (!this.form) return;
- this.form.type = this.resolveTypeValue(this.form.type);
- },
- /**
- * 所属部门归一(dept_id 为 NOT NULL 的历史列,界面已无该表单项)。
- * 历史数据里这列存的是「机构 id」,也有部分脏数据指向已删除组织;
- * 这里只把「既不是现存部门、也不是任何机构 id」的野值回落到当前法人,正常值原样保留。
- */
- normalizeDeptId() {
- if (!this.form) return;
- const deptKey = String(this.form.deptId ?? "").trim();
- if (!deptKey) {
- this.form.deptId = this.form.legalId ?? null;
- return;
- }
- const inDept = this.departments.some(
- (item) => String(item.id) === deptKey,
- );
- const inOrg = this.legalEntities.some(
- (item) => String(item.id) === deptKey,
- );
- if (!inDept && !inOrg) this.form.deptId = this.form.legalId ?? null;
- },
- // 岗位序列的启用层级:直接从 sequenceList 的 children 取,不调接口(按序列名称)
- // 返回元数据数组 [{ label, id, levelGrade }],label 即层级文本(C1/P1…)
- fetchEnabledLevels(seqName) {
- if (!seqName) return [];
- const option = this.sequenceList.find(
- (item) => item.label === seqName || item.name === seqName,
- );
- const levels = (option?.children || [])
- .filter((item) => {
- // 若子项含启用状态字段则只取启用,否则默认全部启用
- if (item.enable !== undefined && item.enable !== null) {
- return Boolean(item.enable);
- }
- if (item.status !== undefined && item.status !== null) {
- return Number(item.status) === 1;
- }
- return true;
- })
- .map((item) => ({
- label: item.label || item.level || "",
- id: item.id ?? null,
- levelGrade: Number(item.levelGrade) || 0,
- }))
- .filter((item) => item.label);
- // 测试:打印当前所选岗位序列及其层级
- console.log(
- "[岗位档案] 当前岗位序列:",
- seqName,
- "| 层级:",
- JSON.stringify(levels),
- );
- return levels;
- },
- // 新建一个层级配置(默认值 + 可选沿用来源配置)
- // meta:{ label, id, levelGrade }——id/levelGrade 用于提交 positionLevelId 与「最高层级」判定
- emptyLevelConfig(meta, source = null) {
- const m =
- typeof meta === "string" ? { label: meta } : meta || { label: "" };
- const cfg = {
- level: m.label || "",
- levelId: m.id ?? null,
- levelGrade: Number(m.levelGrade) || 0,
- salaryMin: 0,
- salaryMid: 0,
- salaryMax: 0,
- salaryTemplateId: this.availableSalaryTemplates[0]?.name || "",
- perfSchemeId: "",
- eduReq: "中专及以上",
- genderReq: "不限",
- expReq: 0,
- ageRange: [18, 60],
- effectDate: todayLocalDate(),
- workEnv: "",
- skills: [],
- certificates: [],
- trainings: [],
- /* 推荐培训「id → 名称」映射:接口提交 trainings 时 trainingId 与 trainingName 都必填,
- 名称快照在此维护(下拉选项 + 接口回显写入),见 syncTrainingNameMap() */
- trainingNameById: {},
- promote: { paths: [{ up: "", down: "", lateral: "" }], conditions: [] },
- dutyText: "",
- };
- if (source) {
- LEVEL_FIELDS.forEach((field) => {
- cfg[field] = JSON.parse(JSON.stringify(source[field] ?? cfg[field]));
- });
- cfg.dutyText =
- source.dutyText ||
- (Array.isArray(source.duties) ? source.duties.join("\n") : "") ||
- "";
- cfg.duties = Array.isArray(source.duties)
- ? JSON.parse(JSON.stringify(source.duties))
- : String(cfg.dutyText || "")
- .split("\n")
- .map((item) => item.trim())
- .filter(Boolean);
- }
- return cfg;
- },
- // 保存当前表单配置到指定层级
- saveLevelConfig(levelName) {
- const target = this.form.levels.find((item) => item.level === levelName);
- if (!target) return;
- LEVEL_FIELDS.forEach((field) => {
- target[field] = JSON.parse(JSON.stringify(this.form[field]));
- });
- const duties = this.dutyText
- .split("\n")
- .map((item) => item.trim())
- .filter(Boolean);
- target.dutyText = this.dutyText;
- target.duties = duties;
- },
- // 加载指定层级配置到表单(保留当前子页签;顶层级不可停在晋升路径)
- loadLevelConfig(levelName) {
- const source = this.form.levels.find((item) => item.level === levelName);
- if (!source) return;
- LEVEL_FIELDS.forEach((field) => {
- this.form[field] = JSON.parse(JSON.stringify(source[field]));
- });
- this.dutyText =
- source.dutyText ||
- (Array.isArray(source.duties) ? source.duties.join("\n") : "");
- if (this.isApexLevel(source) && this.activeModule === "promote") {
- this.activeModule = "salary";
- }
- this.$nextTick(() => this.$refs.form?.clearValidate());
- },
- // ===== 层级配置状态(红/黄/绿状态点 + 待配置提示) =====
- // 「最高层级」=本序列 levelGrade 最大的那一级(数据缺失时回落到清单最后一项)。
- // 后端层级表没有最高层级标记字段,这里按级别自动判定,无需后端支持。
- isApexLevel(item) {
- if (!item || !this.form.levels.length) return false;
- const grades = this.form.levels.map((l) => Number(l.levelGrade) || 0);
- const max = Math.max(...grades);
- if (max > 0) return Number(item.levelGrade) === max;
- return item.level === this.form.levels[this.form.levels.length - 1].level;
- },
- // 单模块是否已配置:定薪=薪资区间或模板;技能=技能/证书;晋升=路径或条件(最高层级免配)
- moduleConfigured(item, key) {
- if (!item) return false;
- if (key === "salary") {
- return (
- (Number(item.salaryMin) > 0 && Number(item.salaryMax) > 0) ||
- Boolean(item.salaryTemplateId)
- );
- }
- if (key === "skill") {
- return (
- (item.skills || []).some((s) => s.name) ||
- (item.certificates || []).some((c) => c.name)
- );
- }
- if (key === "promote") {
- if (this.isApexLevel(item)) return true;
- const paths = item.promote?.paths || [];
- return (
- paths.some((p) => p.up || p.down || p.lateral) ||
- (item.promote?.conditions || []).length > 0
- );
- }
- return false;
- },
- // 层级整体状态:绿=全部已配置,黄=部分,红=全未配置(晋升路径暂时下线,不参与统计)
- levelStatus(item) {
- const modules = ["salary", "skill"];
- // 晋升路径暂时下线,恢复时取消下一行注释
- // if (!this.isApexLevel(item)) modules.push("promote");
- const done = modules.filter((key) => this.moduleConfigured(item, key))
- .length;
- if (!done) return { cls: "is-red", text: "未配置" };
- if (done === modules.length) return { cls: "is-green", text: "已配置" };
- return { cls: "is-yellow", text: "部分配置" };
- },
- // 子页签右侧的状态小字
- moduleStatusText(key) {
- const item = this.currentLevelMeta;
- if (!item) return "";
- return this.moduleConfigured(item, key) ? "已配" : "待配";
- },
- async initialize() {
- // 每次打开弹窗都拉取最新岗位序列;清空请求键以便同岗位可重新加载晋升目标
- this._promoteTargetsRequestKey = "";
- await this.loadSequences();
- const source = this.data
- ? JSON.parse(JSON.stringify(this.data))
- : emptyForm();
- this.form = {
- ...emptyForm(),
- ...source,
- promote: this.normalizePromote(source.promote),
- levels: [],
- };
- this.dutyText = (this.form.duties || []).join("\n");
- if (this.isUpdate) {
- // 编辑:优先用接口返回的每层级配置,再按序列启用层级补齐缺失项
- const seqLevels = this.fetchEnabledLevels(this.form.positionSequence);
- const existingLevels = Array.isArray(source.levels) ? source.levels : [];
- const existingMap = new Map(
- existingLevels.map((item) => [item.level, item]),
- );
- this.form.levels = seqLevels.map((meta) => {
- const cfg = existingMap.get(meta.label);
- if (!cfg) return this.emptyLevelConfig(meta, this.form);
- return {
- ...this.emptyLevelConfig(meta),
- ...cfg,
- // 层级元数据以序列接口为准,防止旧数据残留脏值
- level: meta.label,
- levelId: meta.id,
- levelGrade: meta.levelGrade,
- };
- });
- await this.applyActiveLevel(this.form.levels[0]?.level || "");
- await this.loadSalaryTemplates(this.form.legalId);
- if (!this.form.effectDate) {
- this.form.effectDate = todayLocalDate();
- }
- } else {
- // 新增岗位档案:默认选中当前登录用户所属机构(登录缓存)
- this.form.legalId =
- getCurrentOrgId() || this.legalEntities[0]?.id || null;
- await this.loadSalaryTemplates(this.form.legalId);
- this.ensureSequenceOption(source);
- // 左侧已选序列则带入;未选则留空,由弹框内自行选择
- this.form.positionSequence = this.resolveCreateSequence(source);
- this.form.level = this.configuredLevelOptions[0]?.value || 1;
- this.syncSequenceFields();
- this.refreshCode();
- this.form.deptId = this.departmentOptions[0]?.id || null;
- this.form.salaryTemplateId =
- this.availableSalaryTemplates[0]?.name || "";
- // 创建日期默认当前操作时间
- this.form.effectDate = todayLocalDate();
- // 加载序列启用层级;若左侧已选层级则落到对应项
- const levelMetas = await this.fetchEnabledLevels(
- this.form.positionSequence,
- );
- this.form.levels = levelMetas.map((meta) =>
- this.emptyLevelConfig(meta),
- );
- const presetLevel =
- source.positionLevel && levelMetas.some((m) => m.label === source.positionLevel)
- ? source.positionLevel
- : levelMetas[0]?.label || "";
- await this.applyActiveLevel(presetLevel);
- }
- this.normalizeFormType();
- // 所属部门历史脏值归一(必须早于校验,否则野值会锁死保存)
- this.normalizeDeptId();
- // 推荐培训选项依赖所属机构(机构 id = 接口 deptId),机构确定后再拉
- await this.loadTrainingOptions();
- },
- // 统一切换当前层级:避免与 activeLevel watcher 各请求一遍晋升目标
- async applyActiveLevel(nextLevel) {
- const level = nextLevel || "";
- if (this.activeLevel === level) {
- if (!level) {
- this.upwardOptions = [];
- this.downwardOptions = [];
- this.lateralOptions = [];
- return;
- }
- this.loadLevelConfig(level);
- this.form.positionLevel = level;
- // await this.loadPromoteTargetOptions();
- return;
- }
- this.activeLevel = level;
- },
- normalizePromote(source) {
- const promote = { ...emptyForm().promote, ...(source || {}) };
- const hasPath = (list) =>
- Array.isArray(list) &&
- list.some((path) => path?.up || path?.down || path?.lateral);
- if (!Array.isArray(promote.paths) || !hasPath(promote.paths)) {
- promote.paths =
- promote.up?.length || promote.down?.length || promote.lateral?.length
- ? [
- {
- up: promote.up?.[0] || "",
- down: promote.down?.[0] || "",
- lateral: promote.lateral?.[0] || "",
- },
- ]
- : [{ up: "", down: "", lateral: "" }];
- }
- return promote;
- },
- addPromotePath() {
- this.form.promote.paths.push({ up: "", down: "", lateral: "" });
- },
- removePromotePath(index) {
- if (this.form.promote.paths.length <= 1) return;
- this.form.promote.paths.splice(index, 1);
- },
- promoteOptionLabel(item) {
- if (item.positionSequence && item.positionLevel) {
- return `${item.positionSequence} · ${item.positionLevel}`;
- }
- return item.name || item.positionName || "";
- },
- // 当前层级对应的序列主键 / 层级文本 / 层级级别,供流动层级查询使用
- resolvePromoteQuery() {
- const sequence = this.sequenceList.find(
- (item) =>
- item.label === this.form.positionSequence ||
- item.name === this.form.positionSequence ||
- String(item.id) === String(this.form.sequenceId),
- );
- const levelName =
- this.activeLevel || this.form.positionLevel || "";
- const levelMeta = (sequence?.children || []).find(
- (item) =>
- item.label === levelName ||
- String(item.value) === String(levelName),
- );
- return {
- positionSequenceId: sequence?.id ?? this.form.sequenceId ?? null,
- positionSequenceLevel: levelName,
- levelGrade: Number(levelMeta?.levelGrade) || 0,
- };
- },
- // 查询向上 / 向下 / 平级流动岗位序列层级(同参去重,避免 watcher + 手动调用打两遍)
- async loadPromoteTargetOptions() {
- const { positionSequenceId, positionSequenceLevel, levelGrade } =
- this.resolvePromoteQuery();
- if (!positionSequenceId || !positionSequenceLevel || !levelGrade) {
- this._promoteTargetsRequestKey = "";
- this.upwardOptions = [];
- this.downwardOptions = [];
- this.lateralOptions = [];
- return;
- }
- const params = {
- levelGrade,
- positionSequenceId,
- positionSequenceLevel,
- };
- const requestKey = [
- params.levelGrade,
- params.positionSequenceId,
- params.positionSequenceLevel,
- ].join("|");
- if (this._promoteTargetsRequestKey === requestKey) return;
- this._promoteTargetsRequestKey = requestKey;
- this.promoteTargetsLoading = true;
- try {
- const [upward, downward, lateral] = await Promise.all([
- getPromotionTargets("upward", params),
- getPromotionTargets("downward", params),
- getPromotionTargets("lateral", params),
- ]);
- if (this._promoteTargetsRequestKey !== requestKey) return;
- this.upwardOptions = adaptPromotionLevelOptions(upward);
- this.downwardOptions = adaptPromotionLevelOptions(downward);
- this.lateralOptions = adaptPromotionLevelOptions(lateral);
- this.ensureSelectedPromoteOptions();
- } catch (error) {
- if (this._promoteTargetsRequestKey !== requestKey) return;
- this.upwardOptions = [];
- this.downwardOptions = [];
- this.lateralOptions = [];
- this.$message.error(error.message || "流动岗位序列层级加载失败");
- } finally {
- if (this._promoteTargetsRequestKey === requestKey) {
- this.promoteTargetsLoading = false;
- }
- }
- },
- // 已保存但不在本次查询结果中的层级,补进选项以免下拉空白
- // id 置空:buildLevelConfig 会用 name 去 positionList / promoteTargetOptions 里重新解析岗位主键
- ensureSelectedPromoteOptions() {
- const paths = this.form.promote?.paths || [];
- const cleanPathValue = (v) => String(v || "").replace(/^kept-/, "") || "";
- const append = (list, rawName) => {
- const name = cleanPathValue(rawName);
- if (!name) return list;
- if (list.some((item) => item.name === name)) return list;
- const [sequence, level] = name.split(/\s*[·\-]\s*/);
- return [
- ...list,
- {
- id: null, // 关键:不造假 id,让后端/序列化层用 name 解析
- name,
- positionName: name,
- positionSequence: level ? sequence : "",
- positionLevel: level || name,
- },
- ];
- };
- paths.forEach((path) => {
- // 同步清理 path 对象里的脏值,避免 el-select v-model 带 kept- 前缀
- if (path.up && path.up.startsWith("kept-")) path.up = cleanPathValue(path.up);
- if (path.down && path.down.startsWith("kept-")) path.down = cleanPathValue(path.down);
- if (path.lateral && path.lateral.startsWith("kept-")) path.lateral = cleanPathValue(path.lateral);
- this.upwardOptions = append(this.upwardOptions, path.up);
- this.downwardOptions = append(this.downwardOptions, path.down);
- this.lateralOptions = append(this.lateralOptions, path.lateral);
- });
- },
- handleLegalChange() {
- this.form.deptId = this.departmentOptions[0]?.id || null;
- this.loadSalaryTemplates(this.form.legalId);
- if (
- !this.availableSalaryTemplates.some(
- (item) => item.id === this.form.salaryTemplateId,
- )
- ) {
- this.form.salaryTemplateId =
- this.availableSalaryTemplates[0]?.name || "";
- }
- this.refreshCode();
- // 推荐培训按机构(= 接口 deptId)过滤,切换机构后重新拉取并清掉不属于新机构的已选值
- this.form.trainings = [];
- this.loadTrainingOptions();
- },
- // 按当前法人(companyId = legalId)拉取薪资核算「薪资模板设定」列表,作为薪资模板下拉数据源
- async loadSalaryTemplates(companyId) {
- if (companyId == null || companyId === "") {
- this.salaryTemplateOptions = [];
- return;
- }
- try {
- const list = await getSalaryTemplateList(companyId);
- this.salaryTemplateOptions = (Array.isArray(list) ? list : []).map(
- (item) => ({ id: item.id, name: item.templateName || item.name || "" }),
- );
- } catch (error) {
- this.salaryTemplateOptions = [];
- this.$message.error(error.message || "薪资模板列表加载失败");
- }
- },
- // 选择岗位序列 → 加载启用层级清单(左侧 rail),并刷新晋升路径可选岗位
- async handleSequenceChange() {
- this.syncSequenceFields();
- this.refreshCode();
- const levelMetas = await this.fetchEnabledLevels(
- this.form.positionSequence,
- );
- this.form.levels = levelMetas.map((meta) =>
- this.emptyLevelConfig(meta),
- );
- // 序列变化后允许按新序列重新请求(即使首层文案相同)
- this._promoteTargetsRequestKey = "";
- await this.applyActiveLevel(levelMetas[0]?.label || "");
- },
- handleLevelChange() {
- this.syncSequenceFields();
- this.refreshCode();
- this._promoteTargetsRequestKey = "";
- // this.loadPromoteTargetOptions();
- },
- // 新增时优先用左侧所选序列(名称 / 主键 / 序列编码)匹配下拉项
- resolveCreateSequence(source = {}) {
- const name = String(source.positionSequence || "").trim();
- const seqId = source.sequenceId;
- const seqCode = source.seq;
- const found = this.sequenceList.find(
- (item) =>
- (name && (item.label === name || item.name === name)) ||
- (seqId != null && String(item.id) === String(seqId)) ||
- (seqCode &&
- (String(item.id) === String(seqCode) || item.label === seqCode)),
- );
- return found?.label || name || "";
- },
- // 左侧选中的序列若不在启用下拉里,补进选项以免无法回显
- ensureSequenceOption(source = {}) {
- const name = String(source.positionSequence || "").trim();
- if (!name) return;
- const exists = this.sequenceList.some(
- (item) => item.label === name || item.name === name,
- );
- if (exists) return;
- const fromTree = (this.sequenceOptions || []).find(
- (item) => item.label === name || item.sequence === name,
- );
- this.sequenceList = [
- {
- id: source.sequenceId || fromTree?.sequenceId || fromTree?.id || name,
- label: name,
- name,
- children: fromTree?.children || [],
- },
- ...this.sequenceList,
- ];
- },
- syncSequenceFields() {
- const sequence = this.sequenceList.find(
- (item) =>
- item.label === this.form.positionSequence ||
- item.name === this.form.positionSequence,
- );
- if (sequence?.label) this.form.positionSequence = sequence.label;
- if (sequence?.id != null) this.form.sequenceId = sequence.id;
- if (this.activeLevel) this.form.positionLevel = this.activeLevel;
- },
- refreshCode() {
- // 岗位编码由后端生成,前端不再自动生成;新增时保持为空
- },
- syncSalaryMid() {
- if (this.form.salaryMax >= this.form.salaryMin) {
- this.form.salaryMid = Math.round(
- (Number(this.form.salaryMax) + Number(this.form.salaryMin)) / 2,
- );
- }
- },
- addSkill() {
- this.form.skills.push({
- name: "",
- level: this.skillLevelOptions[0]?.value || "",
- required: true,
- matchRate: 0,
- });
- },
- removeSkill(index) {
- this.form.skills.splice(index, 1);
- },
- addCert() {
- this.form.certificates.push({
- name: "",
- category: this.idTypeOptions[0]?.value || "",
- });
- },
- removeCert(index) {
- this.form.certificates.splice(index, 1);
- },
- save() {
- // 保存请求进行中直接拦截,避免重复点击并发触发多次保存接口
- if (this.saving) return;
- // 保存当前层级配置,并同步岗位序列名称/主键
- if (this.activeLevel) this.saveLevelConfig(this.activeLevel);
- this.syncSequenceFields();
- if (!String(this.form.positionSequence || "").trim()) {
- this.$message.warning("请选择岗位序列");
- this.$refs.form?.validateField("positionSequence");
- this.focusFieldError({
- message: "请选择岗位序列",
- module: "basic",
- anchor: "field-positionSequence",
- });
- return;
- }
- this.$refs.form.validate(async (valid, invalidFields) => {
- if (this.saving) return;
- if (!valid) {
- const firstProp = Object.keys(invalidFields || {})[0];
- const detail = invalidFields?.[firstProp]?.[0];
- const meta = this.formFieldAnchors[firstProp] || {
- module: "basic",
- anchor: `field-${firstProp}`,
- };
- this.$message.warning(detail?.message || "请完善必填项");
- await this.focusFieldError({
- message: detail?.message || "请完善必填项",
- level: meta.module === "basic" ? "" : this.activeLevel,
- ...meta,
- });
- return;
- }
- const errors = this.collectRuleErrors();
- if (errors.length) {
- this.$message.warning(errors[0].message);
- await this.focusFieldError(errors[0]);
- return;
- }
- this.saving = true;
- const duties = this.dutyText
- .split("\n")
- .map((item) => item.trim())
- .filter(Boolean);
- // 顶层与当前层级职责保持一致,确保 levelConfigs[].salary.dutyContent 带上最新输入
- if (this.activeLevel) {
- const current = this.form.levels.find(
- (item) => item.level === this.activeLevel,
- );
- if (current) {
- current.dutyText = this.dutyText;
- current.duties = duties;
- }
- }
- // 提交前再刷一遍当前 Tab 到 levels,避免漏掉未切换层级时的定薪/技能等变更
- if (this.activeLevel) this.saveLevelConfig(this.activeLevel);
- const payload = {
- ...this.form,
- positionSequence: String(this.form.positionSequence || "").trim(),
- version: this.isUpdate ? Number(this.form.version || 1) + 1 : 1,
- duties,
- dutyText: this.dutyText,
- // 供序列化时把晋升目标名称解析为岗位主键
- promoteTargetOptions: [
- ...this.upwardOptions,
- ...this.downwardOptions,
- ...this.lateralOptions,
- ],
- updatedAt: new Date().toISOString().slice(0, 10),
- history: [
- {
- time: new Date().toLocaleString("zh-CN", { hour12: false }),
- title: this.isUpdate ? "编辑岗位档案" : "新增岗位档案",
- content: "岗位档案已提交至人力资源服务。",
- },
- ...(this.form.history || []),
- ],
- };
- // loading 交给父级接口请求结束后回调释放,避免请求还没回来按钮就恢复可点
- this.$emit("save", payload, () => {
- this.saving = false;
- });
- });
- },
- /** 定位到未填项:切层级 / 子模块后滚动并短暂高亮 */
- async focusFieldError(error = {}) {
- if (error.level && error.level !== this.activeLevel) {
- this.activeLevel = error.level;
- await this.$nextTick();
- await new Promise((resolve) => setTimeout(resolve, 50));
- }
- if (
- error.module &&
- error.module !== "basic" &&
- error.module !== this.activeModule
- ) {
- this.activeModule = error.module;
- await this.$nextTick();
- }
- if (
- error.anchor === "field-skills" &&
- !(this.form.skills || []).length &&
- !this.readonly
- ) {
- this.addSkill();
- await this.$nextTick();
- }
- await this.$nextTick();
- const root = this.$el;
- if (!root) return;
- const selector = error.anchor
- ? `[data-field-anchor="${error.anchor}"]`
- : ".el-form-item.is-error";
- const target =
- root.querySelector(selector) ||
- root.querySelector(".el-form-item.is-error");
- if (!target) return;
- target.scrollIntoView({ behavior: "smooth", block: "center" });
- target.classList.add("field-anchor-flash");
- window.setTimeout(() => target.classList.remove("field-anchor-flash"), 1600);
- const focusable = target.querySelector(
- "input:not([disabled]), textarea:not([disabled]), .el-input__inner, .el-textarea__inner",
- );
- if (focusable && typeof focusable.focus === "function") {
- try {
- focusable.focus({ preventScroll: true });
- } catch (e) {
- focusable.focus();
- }
- }
- },
- /** 基础信息必填外的自定义校验(定薪/技能/晋升不再强制必填;岗位职责必填) */
- findFirstFieldError() {
- if (this.form.type === "" || this.form.type == null) {
- return {
- message: "请选择岗位性质",
- module: "basic",
- anchor: "field-type",
- };
- }
- if (!String(this.dutyText || "").trim()) {
- return {
- message: "请输入岗位职责",
- level: this.activeLevel,
- module: "salary",
- anchor: "field-dutyText",
- };
- }
- return null;
- },
- reset() {
- this.form = emptyForm();
- this.dutyText = "";
- this.activeLevel = "";
- // 关闭弹窗时复位按钮 loading,避免下次打开残留「正在保存」
- this.saving = false;
- this.$refs.form?.clearValidate();
- },
- updateVisible(value) {
- this.$emit("update:visible", value);
- },
- closeDrawer(done) {
- this.updateVisible(false);
- if (typeof done === "function") done();
- },
- collectRuleErrors() {
- const fieldError = this.findFirstFieldError();
- const errors = fieldError ? [fieldError] : [];
- const duplicate = this.positionList.some(
- (item) =>
- item.positionCode === this.form.positionCode &&
- String(item.id) !== String(this.form.id),
- );
- /* 所属部门:界面已下线该字段,历史数据这一列存的是「机构 id」(dept_id NOT NULL)。
- departments 由 index.vue 构建时主动剔除了机构节点,所以机构 id 在 departments 里必然找不到,
- 原逻辑会把「合法历史数据」判成非法并锁死保存。此处把机构节点本身也视为合法归属,
- 只有既不是部门、也不是任何机构 id 的野值才拦截。 */
- const deptKey = String(this.form.deptId ?? "").trim();
- const department = this.departments.find(
- (item) => String(item.id) === deptKey,
- );
- const isOrgNode =
- deptKey !== "" &&
- (deptKey === String(this.form.legalId ?? "").trim() ||
- this.legalEntities.some((item) => String(item.id) === deptKey));
- const deptNotInLegal =
- Boolean(department) &&
- String(department.legalId ?? "").trim() !==
- String(this.form.legalId ?? "").trim();
- if (deptKey && !isOrgNode && (!department || deptNotInLegal)) {
- errors.push({
- message: "所属部门必须存在且属于当前法人",
- module: "basic",
- anchor: "field-deptId",
- });
- }
- if (this.form.level < 1) {
- errors.push({
- message: "岗位层级必须大于 0",
- module: "basic",
- anchor: "field-level",
- });
- }
- if (this.form.status === 3 && !this.form.revokeDate) {
- errors.push({
- message: "待撤销岗位必须填写计划撤销日期",
- module: "basic",
- anchor: "field-type",
- });
- }
- if (duplicate) {
- errors.push({
- message: "岗位编码已存在",
- module: "basic",
- anchor: "field-name",
- });
- }
- const certificates = this.form.certificates || [];
- if (certificates.some((item) => !String(item?.name || "").trim())) {
- errors.push({
- message: "证书名称不能为空",
- level: this.activeLevel,
- module: "skill",
- anchor: "field-skills",
- });
- }
- return errors;
- },
- },
- };
- </script>
- <style src="../../../../styles/views/positionManagement/components/PositionEditDialog.scss" lang="scss" scoped></style>
|