| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648 |
- <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.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="contractTemplateId">
- <el-select
- v-model="form.contractTemplateId"
- filterable
- class="full-width"
- placeholder="请选择岗位合同模板"
- >
- <el-option
- v-for="item in contractTemplates"
- :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="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>
- <!-- ===== 按岗位序列层级动态 Tab 配置 ===== -->
- <div class="level-tabs-wrap" :class="{ 'is-empty': !levelTabs.length }">
- <template v-if="levelTabs.length">
- <el-tabs v-model="activeLevel" class="level-tabs">
- <el-tab-pane
- v-for="lvl in levelTabs"
- :key="lvl"
- :label="lvl"
- :name="lvl"
- />
- </el-tabs>
- <div class="level-config-body">
- <div class="status-tabs module-tabs" role="tablist">
- <button
- v-for="m in moduleTabs"
- :key="m.key"
- type="button"
- :class="{ active: activeModule === m.key }"
- @click="activeModule = m.key"
- >
- {{ m.label }}
- </button>
- </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-col :xs="24" :sm="12"
- ><el-form-item label="绩效方案"
- ><el-select
- v-model="form.perfSchemeId"
- filterable
- allow-create
- default-first-option
- class="full-width"
- placeholder="关联绩效方案名称或编码"
- ><el-option
- v-for="item in performanceSchemes"
- :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>
- <el-select
- v-model="form.trainings"
- multiple
- filterable
- allow-create
- default-first-option
- 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>
- <!-- 晋升路径 -->
- <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
- type="primary"
- icon="el-icon-edit"
- @click="$emit('edit')"
- >编辑档案</el-button
- >
- </div>
- <div v-else>
- <el-button @click="closeDrawer()">取消</el-button>
- <el-button type="primary" :loading="saving" @click="save">
- {{ saving ? "正在保存" : "保存岗位" }}
- </el-button>
- </div>
- </div>
- </el-drawer>
- </template>
- <script>
- import { performanceSchemes, salaryTemplates } from "../mock";
- import { getDict, getDictByCode } from "@/api/sys";
- import {
- getContractTemplatePage,
- getPositionSequencePage,
- getPromotionTargets,
- adaptPromotionLevelOptions,
- } from "@/api/hr";
- import { formatDate } from "@/utils/dateUtils";
- /** 本地当天日期 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",
- "promote",
- ];
- const emptyForm = () => ({
- id: null,
- positionCode: "",
- name: "",
- legalId: 1,
- deptId: null,
- level: 1,
- positionName: "",
- positionSequence: "",
- positionLevel: "",
- sequenceId: null,
- type: "",
- staffing: 0,
- actual: 0,
- status: 1,
- revokeDate: "",
- contractTemplateId: null,
- 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,
- salaryTemplates,
- performanceSchemes,
- // 岗位合同模板:接口加载,无静态兜底
- contractTemplates: [],
- // 晋升条件:/system/dict/getByCode/Promotion_Conditions
- conditionOptions: [],
- // 必备技能等级:/system/dict/getByCode/Required_Skills
- skillLevelOptions: [],
- // 推荐培训:/system/dict/getByCode/Recommended_Training
- trainingOptions: [],
- // 岗位性质:/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);
- },
- // 启用层级 Tab 列表(来自当前岗位序列的启用层级)
- levelTabs() {
- return this.form.levels.map((item) => item.level);
- },
- // 每个层级下的子模块 Tab(定薪/技能/晋升)
- moduleTabs() {
- return [
- { key: "salary", label: "定薪定责" },
- { key: "skill", label: "技能资质" },
- { key: "promote", label: "晋升路径" },
- ];
- },
- // 表单 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" },
- };
- },
- // 层级 Tab 区占位提示:区分「未选序列」与「所选序列无启用层级」
- levelMessage() {
- if (!this.form.positionSequence) {
- return "请先选择岗位序列,以加载该序列的启用层级配置";
- }
- if (!this.levelTabs.length) {
- return "当前岗位序列暂无启用的岗位层级,请先在「岗位序列 · 维护层级」中配置";
- }
- return "";
- },
- departmentOptions() {
- return this.departments.filter(
- (item) => String(item.legalId) === String(this.form.legalId),
- );
- },
- availableSalaryTemplates() {
- return this.salaryTemplates.filter((item) =>
- item.legalIds.includes(this.form.legalId),
- );
- },
- 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.loadContractTemplates();
- 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 loadContractTemplates() {
- try {
- const data = await getContractTemplatePage({
- pageNum: 1,
- size: 1000,
- templateStatus: "ENABLED",
- });
- this.contractTemplates = (data?.list || [])
- .map((item) => ({ id: item.id, name: item.templateName || "" }))
- .filter((item) => item.name);
- } catch (error) {
- this.contractTemplates = [];
- this.$message.error(error.message || "岗位合同模板加载失败");
- }
- },
- // 岗位序列下拉:每次打开从接口拉取最新启用序列及其层级
- 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"),
- load("Recommended_Training", "trainingOptions"),
- ]);
- },
- // 资质证书类别:/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,无静态兜底
- // 选项展示与回填使用字典 value(文案),不用 key(如 general_positions)
- async loadTypeOptions() {
- try {
- const items = await getDictByCode("position_nature");
- this.typeOptions = items
- .map((item) => {
- const text = String(item.value ?? item.label ?? "");
- return {
- label: text,
- value: text,
- key: item.key != null ? String(item.key) : text,
- };
- })
- .filter((item) => item.value);
- this.normalizeFormType();
- } catch (error) {
- this.typeOptions = [];
- this.$message.error(error.message || "岗位性质字典加载失败");
- }
- },
- // 把历史编码(key)转成字典 value,避免下拉显示 general_positions
- 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);
- },
- // 岗位序列的启用层级:直接从 sequenceList 的 children 取,不调接口(按序列名称)
- 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) => item.label || item.level || "")
- .filter(Boolean);
- // 测试:打印当前所选岗位序列及其层级
- console.log("[岗位档案] 当前岗位序列:", seqName, "| 层级:", JSON.stringify(levels));
- return levels;
- },
- // 新建一个层级配置(默认值 + 可选沿用来源配置)
- emptyLevelConfig(level, source = null) {
- const cfg = {
- level,
- salaryMin: 0,
- salaryMid: 0,
- salaryMax: 0,
- salaryTemplateId: this.availableSalaryTemplates[0]?.name || "",
- perfSchemeId: this.performanceSchemes[0]?.name || "",
- eduReq: "中专及以上",
- genderReq: "不限",
- expReq: 0,
- ageRange: [18, 60],
- effectDate: todayLocalDate(),
- workEnv: "",
- skills: [],
- certificates: [],
- trainings: [],
- 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") : "");
- this.activeModule = "salary";
- this.$nextTick(() => this.$refs.form?.clearValidate());
- },
- 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((level) => {
- const cfg = existingMap.get(level);
- return cfg
- ? { ...this.emptyLevelConfig(level), ...cfg }
- : this.emptyLevelConfig(level, this.form);
- });
- await this.applyActiveLevel(this.form.levels[0]?.level || "");
- if (!this.form.effectDate) {
- this.form.effectDate = todayLocalDate();
- }
- } else {
- this.form.legalId = this.legalEntities[0]?.id || null;
- 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();
- // 加载序列启用层级;若左侧已选层级则落到对应 Tab
- const levels = await this.fetchEnabledLevels(this.form.positionSequence);
- this.form.levels = levels.map((level) => this.emptyLevelConfig(level));
- const presetLevel =
- source.positionLevel && levels.includes(source.positionLevel)
- ? source.positionLevel
- : levels[0] || "";
- await this.applyActiveLevel(presetLevel);
- }
- this.normalizeFormType();
- },
- // 统一切换当前层级:避免与 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;
- }
- }
- },
- // 已保存但不在本次查询结果中的层级,补进选项以免下拉空白
- ensureSelectedPromoteOptions() {
- const paths = this.form.promote?.paths || [];
- const append = (list, name) => {
- if (!name || list.some((item) => item.name === name)) return list;
- const [sequence, level] = String(name).split(/\s*·\s*/);
- return [
- ...list,
- {
- id: `kept-${name}`,
- name,
- positionName: name,
- positionSequence: level ? sequence : "",
- positionLevel: level || name,
- },
- ];
- };
- paths.forEach((path) => {
- 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;
- if (
- !this.availableSalaryTemplates.some(
- (item) => item.id === this.form.salaryTemplateId,
- )
- ) {
- this.form.salaryTemplateId =
- this.availableSalaryTemplates[0]?.name || "";
- }
- this.refreshCode();
- },
- // 选择岗位序列 → 加载启用层级 Tab,并刷新晋升路径可选岗位
- async handleSequenceChange() {
- this.syncSequenceFields();
- this.refreshCode();
- const levels = await this.fetchEnabledLevels(this.form.positionSequence);
- this.form.levels = levels.map((level) => this.emptyLevelConfig(level));
- // 序列变化后允许按新序列重新请求(即使首层文案相同)
- this._promoteTargetsRequestKey = "";
- await this.applyActiveLevel(levels[0] || "");
- },
- 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.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 || []),
- ],
- };
- 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 = "";
- 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 &&
- item.id !== this.form.id,
- );
- const department = this.departments.find(
- (item) => String(item.id) === String(this.form.deptId),
- );
- if (!department || String(department.legalId) !== String(this.form.legalId))) {
- 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>
|