| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833 |
- <template>
- <div>
- <el-dialog :visible.sync="dialogVisible" :width="fullscreen ? '100%' : '1450px'" :top="fullscreen ? '0' : '3vh'"
- append-to-body :close-on-click-modal="false" :close-on-press-escape="!showWorkTypeSelect"
- :before-close="handleBeforeClose" :custom-class="'employee-archive-dialog' + (fullscreen ? ' is-fullscreen' : '')"
- @closed="resetForm">
- <div slot="title" class="archive-dialog-title">
- <span>{{ isEdit ? '编辑员工档案' : '新增员工档案' }}</span>
- <button type="button" class="archive-dialog-fullscreen" :title="fullscreen ? '退出全屏' : '全屏'"
- @click.stop="toggleFullscreen">
- <i :class="fullscreen ? 'el-icon-_compress' : 'el-icon-full-screen'"></i>
- </button>
- </div>
- <div class="archive-form-summary">
- <span class="summary-icon"><i class="el-icon-user"></i></span>
- <div class="summary-copy">
- <strong>{{ form.name || '新员工档案' }}</strong>
- <p>{{ form.employeeNo || '待生成工号' }}</p>
- </div>
- <div class="summary-completion">
- <div><span>资料完整度</span><strong>{{ completionRate }}%</strong></div>
- <span class="completion-track"><i :style="{ width: `${completionRate}%` }"></i></span>
- </div>
- </div>
- <el-form ref="form" :model="form" :rules="rules" label-position="top" size="small" class="archive-form-shell">
- <el-tabs v-model="activeTab" tab-position="left" class="archive-form-tabs">
- <el-tab-pane v-for="section in sections" :key="section.name" :name="section.name">
- <span slot="label" class="section-tab-label">
- <i :class="section.icon"></i>
- <span><strong>{{ section.label }}</strong><small>{{ section.subtitle }}</small></span>
- <em>{{ sectionProgress[section.name] }}</em>
- </span>
- <div v-if="section.name === 'base'" class="section-content">
- <header class="section-title">
- <div>
- <h3>基本信息</h3>
- <p>员工身份、组织任职与联系方式,字段与档案模型一一对应</p>
- </div><span>必填 {{ requiredProgress.base }}</span>
- </header>
- <div class="subsection-title"><strong>身份信息</strong></div>
- <el-row :gutter="16">
- <el-col :xs="24" :sm="12"><el-form-item label="姓名" prop="name"><el-input v-model.trim="form.name"
- maxlength="20" placeholder="请输入员工真实姓名" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="性别" prop="gender"><el-select v-model="form.gender"
- class="full-width"><el-option v-for="item in genderOptions" :key="item.value"
- :label="item.label" :value="item.value" /></el-select></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="身份证" prop="idCard"><el-input v-model.trim="form.idCard"
- maxlength="18" placeholder="请输入18位身份证号" @input="handleIdCardInput"
- @blur="parseIdCard" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="出生日期" prop="birthDate"><el-date-picker
- v-model="form.birthDate" value-format="yyyy-MM-dd" type="date" placeholder="可由身份证自动生成"
- class="full-width" @change="syncAge" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="年龄"><el-input :value="form.age || ''" readonly
- placeholder="根据出生日期自动计算" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="民族"><el-select v-model="form.ethnic" filterable
- allow-create default-first-option class="full-width"><el-option v-for="item in ethnicOptions"
- :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="政治面貌"><el-input v-model.trim="form.politicalStatus"
- maxlength="20" placeholder="党员、预备党员、群众等" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="婚姻状态"><el-select v-model="form.maritalStatus"
- class="full-width"><el-option v-for="item in maritalOptions" :key="item" :label="item"
- :value="item" /></el-select></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="籍贯"><el-input v-model.trim="form.nativePlace"
- maxlength="30" placeholder="省 / 市" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="最高学历"><el-select v-model="form.education"
- class="full-width"><el-option v-for="item in educationOptions" :key="item" :label="item"
- :value="item" /></el-select></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="毕业院校"><el-input v-model.trim="form.school" maxlength="40"
- placeholder="请输入毕业院校" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="户籍地址"><el-input v-model.trim="form.registeredAddress"
- maxlength="100" placeholder="请输入户籍地址" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="现居住地址"><el-input v-model.trim="form.currentAddress"
- maxlength="100" placeholder="请输入现居住地址" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="工作地址"><el-input v-model.trim="form.workAddress"
- maxlength="60" placeholder="实际办公地点" /></el-form-item></el-col>
- </el-row>
- <div class="subsection-title"><strong>组织与任职</strong></div>
- <el-row :gutter="16">
- <!-- 左列:组织归属 + 岗位链路(机构 → 主所属部门 → 隶属部门 → 岗位序列 → 岗位 → 性质 → 层级)。
- 用「列容器」而不是 el-col 平铺:主所属部门只在编辑态渲染,平铺会让它之后的字段
- 整体错位一格,新增态「工号」就会挤回左列。 -->
- <el-col :xs="24" :sm="12">
- <el-form-item label="所属机构" prop="groupId">
- <el-select v-model="form.groupId" filterable class="full-width" placeholder="请选择法人组织"
- :disabled="isEdit" @change="handleLegalEntityChange">
- <el-option v-for="item in legalEntities" :key="String(item.id)" :label="item.name"
- :value="String(item.id)" />
- </el-select>
- </el-form-item>
- <!-- 主所属部门:与所属机构级联,树状单选(与 kd-aiot-frontend 的 org-user-edit 同款交互)。
- 新增态:选项 = 机构节点(置灰不可选)+ 机构下所有部门;选完从隶属部门里剔除,避免重复。
- ★ 2026-10-28 编辑态回显改成接口 deptName 文本:原来按 deptId 去部门树里找节点显示名称,
- 节点匹配不到或撞上机构根节点时就会显示成机构名字(用户实拍:主所属部门显示成机构名)。
- 名称只认详情接口 deptName,取不到就留空提示,不拿机构名 / 隶属部门拼串顶替。 -->
- <!-- required:编辑态 prop 置空不参与校验,但星号要一直展示(2026-10-29 用户要求) -->
- <el-form-item label="主所属部门" required :prop="isEdit ? undefined : 'primaryDeptId'">
- <el-input v-if="isEdit" :value="primaryDeptName" disabled
- :placeholder="primaryDeptName ? '' : '接口未返回部门名称(deptName),请后端补充'" />
- <el-cascader v-else :key="`primary-dept-${form.groupId || 'none'}`" v-model="form.primaryDeptId"
- :options="mainDeptOptions" :props="organizationProps" filterable clearable
- :show-all-levels="false" :append-to-body="true" popper-class="employee-archive-dept-cascader"
- class="full-width" :disabled="!form.groupId"
- :placeholder="form.groupId ? '请选择主所属部门' : '请先选择所属机构'"
- @change="handlePrimaryDeptChange" />
- </el-form-item>
- <el-form-item label="隶属部门" prop="deptIds"><el-cascader
- :key="`dept-cascader-${form.groupId || 'none'}-${deptCascaderKey}`" v-model="form.deptIds"
- :options="affiDeptOptions" :props="deptCascaderProps" filterable clearable :show-all-levels="false"
- :append-to-body="true" popper-class="employee-archive-dept-cascader" ref="deptCascader"
- class="full-width dept-multi-cascader" :disabled="isEdit || !form.groupId"
- :placeholder="deptCascaderPlaceholder"
- @change="handleOrganizationChange" /></el-form-item>
- <!-- 所属工厂:与所属机构级联(2026-10-29 用户要求),只能选当前机构下的工厂;
- 未选机构时禁用。编辑态仍整体只读(与其它字段口径一致)。 -->
- <el-form-item label="所属工厂">
- <el-select v-model="form.factoryId" filterable clearable class="full-width"
- :loading="factoryLoading" :disabled="isEdit || !form.groupId"
- :placeholder="form.groupId ? '请选择所属工厂' : '请先选择所属机构'" @change="handleFactoryChange">
- <el-option v-for="item in factoryOptions" :key="String(item.id)" :label="item.name"
- :value="item.id" />
- </el-select>
- </el-form-item>
- <!-- 岗位链路级联来源 = 主所属部门(2026-10-29 用户确认:主所属部门 → 岗位序列 → 岗位 → 性质/层级)。
- 编辑态:已有值保持只读;空白时放开编辑,允许补选(2026-10-29 用户要求)。 -->
- <el-form-item label="岗位序列" prop="positionSequenceId">
- <el-select v-model="form.positionSequenceId" filterable clearable class="full-width"
- :loading="sequenceLoading" :disabled="(isEdit && hasPositionSequence) || !hasPrimaryDept"
- :placeholder="hasPrimaryDept ? '请选择岗位序列' : '请先选择主所属部门'"
- @change="handlePositionSequenceChange">
- <el-option v-for="item in sequenceOptions" :key="`seq-${String(item.id)}`" :label="item.name"
- :value="String(item.id)" />
- </el-select>
- </el-form-item>
- <el-form-item label="岗位" prop="positionId">
- <el-select :key="`position-select-${positionSelectKey}`" v-model="form.positionId" filterable
- clearable class="full-width" :loading="positionLoading"
- :disabled="(isEdit && Boolean(form.positionId)) || !hasPositionSequence"
- :placeholder="hasPositionSequence ? '请选择岗位' : '请先选择岗位序列'" @change="handlePositionChange">
- <el-option v-for="item in positionOptions" :key="`pos-${String(item.id)}`"
- :label="positionOptionLabel(item)" :value="String(item.id)" />
- </el-select>
- </el-form-item>
- <el-form-item label="岗位性质" prop="employmentType"><el-select
- v-model="form.employmentType" filterable
- clearable class="full-width" :disabled="isEdit && Boolean(form.employmentType)"
- :placeholder="form.positionId ? '请选择岗位性质' : '请先选择岗位'"
- @change="syncEmploymentTypeName">
- <el-option v-for="item in employmentOptions" :key="item.key || item.value" :label="item.label"
- :value="item.value" />
- </el-select></el-form-item>
- <el-form-item label="岗位层级" prop="staffingLevel"><el-select
- v-model="form.staffingLevel"
- class="full-width"
- :disabled="(isEdit && Boolean(form.staffingLevel)) || !staffingLevelOptions.length"
- :placeholder="form.positionId ? '请选择岗位层级' : '请先选择岗位'" @change="handleStaffingLevelChange">
- <el-option v-for="item in staffingLevelOptions" :key="item" :label="item"
- :value="item" /></el-select></el-form-item>
- </el-col>
- <!-- 右列:账号、直属上级与时间状态(所属工厂 2026-10-29 移到左列隶属部门之下) -->
- <el-col :xs="24" :sm="12">
- <el-form-item label="用户账号"><el-input v-model.trim="form.account"
- maxlength="30" placeholder="系统登录账号,支持自动生成、手动修改"><template
- slot="prepend">@</template></el-input></el-form-item>
- <el-form-item label="工号" prop="employeeNo"><el-input
- v-model.trim="form.employeeNo" maxlength="20" placeholder="集团全局唯一" disabled><el-button
- v-if="!isEdit" slot="append"
- @click="generateEmployeeNo">生成</el-button></el-input></el-form-item>
- <el-form-item label="直属上级">
- <el-select v-model="form.managerId" filterable remote clearable class="full-width"
- :loading="managerLoading" :disabled="isEdit" :remote-method="searchManagers" placeholder="输入姓名/工号搜索全员"
- @change="handleManagerChange" @focus="ensureManagerOptions">
- <el-option v-for="item in managerOptions" :key="item.id" :label="managerOptionLabel(item)"
- :value="item.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="入职时间" prop="hireDate"><el-date-picker
- v-model="form.hireDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择入职日期"
- class="full-width" :disabled="isEdit" /></el-form-item>
- <el-form-item label="转正日期"><el-date-picker v-model="form.regularDate"
- value-format="yyyy-MM-dd" type="date" placeholder="转正生效日期"
- class="full-width" :disabled="isEdit" /></el-form-item>
- <el-form-item label="员工状态" prop="status"><el-select v-model="form.status"
- class="full-width" :disabled="isEdit"><el-option v-for="item in statusOptions" :key="item.value" :label="item.label"
- :value="item.value" /></el-select></el-form-item>
- <!-- 是否启用:2026-10-29 用户要求放到右列末尾(员工状态之下) -->
- <el-form-item label="是否启用">
- <div class="enable-field"><el-switch v-model="form.enabled" active-color="#18a875" /><span>{{
- form.enabled ? '启用' :
- '待岗' }}</span></div>
- </el-form-item>
- </el-col>
- </el-row>
- <div class="subsection-title"><strong>联系方式</strong></div>
- <el-row :gutter="16">
- <el-col :xs="24" :sm="12"><el-form-item label="手机号" prop="phone"><el-input v-model.trim="form.phone"
- maxlength="11" placeholder="请输入 11 位手机号码"
- prefix-icon="el-icon-mobile-phone" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="邮箱"><el-input v-model.trim="form.email" maxlength="50"
- placeholder="name@company.com" prefix-icon="el-icon-message" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="微信号"><el-input v-model.trim="form.wechat" maxlength="30"
- placeholder="请输入微信号" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="紧急联系人"><el-input v-model.trim="form.emergencyContact"
- maxlength="20" placeholder="请输入联系人姓名" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="紧急联系人关系"><el-input v-model.trim="form.emergencyRelation"
- maxlength="20" placeholder="如配偶、父母、子女" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="紧急联系电话"><el-input v-model.trim="form.emergencyPhone"
- maxlength="11" placeholder="请输入联系电话" /></el-form-item></el-col>
- </el-row>
- </div>
- <div v-else-if="section.name === 'experience'" class="section-content">
- <header class="section-title">
- <div>
- <h3>教育与工作履历</h3>
- <p>最高教育经历和入职前工作经历</p>
- </div><span>{{ form.previousJobs.length }} 段履历</span>
- </header>
- <div class="subsection-title"><strong>教育经历</strong><el-button type="primary" size="mini"
- @click="addEducation">添加教育</el-button></div>
- <el-alert v-if="form.educations.length > 1" type="warning" :closable="false" show-icon
- title="多条教育经历保存时只会提交第一条。" style="margin-bottom: 10px" />
- <el-table :data="form.educations" border size="small" class="education-table" empty-text="暂无教育经历">
- <el-table-column type="index" label="序号" width="55" align="center" />
- <el-table-column label="学历" min-width="120">
- <template slot-scope="{ row }">
- <el-select v-model="row.education" placeholder="请选择" class="full-width">
- <el-option v-for="opt in educationOptions" :key="opt" :label="opt" :value="opt" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column label="学位" min-width="110">
- <template slot-scope="{ row }">
- <el-select v-model="row.degree" clearable placeholder="请选择" class="full-width">
- <el-option v-for="opt in ['无', '学士', '硕士', '博士']" :key="opt" :label="opt" :value="opt" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column label="毕业日期" min-width="150">
- <template slot-scope="{ row }">
- <el-date-picker v-model="row.graduationDate" value-format="yyyy-MM-dd" type="date"
- placeholder="选择日期" class="full-width" />
- </template>
- </el-table-column>
- <el-table-column label="毕业院校" min-width="160">
- <template slot-scope="{ row }">
- <el-input v-model.trim="row.school" maxlength="40" placeholder="请输入毕业院校" />
- </template>
- </el-table-column>
- <el-table-column label="所学专业" min-width="150">
- <template slot-scope="{ row }">
- <el-input v-model.trim="row.major" maxlength="40" placeholder="请输入所学专业" />
- </template>
- </el-table-column>
- <el-table-column label="操作" width="90" align="center">
- <template slot-scope="{ $index }">
- <el-button type="text" icon="el-icon-delete" class="education-delete-btn"
- @click="removeItem('educations', $index)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- <div class="subsection-title"><strong>工作履历</strong><el-button type="primary" size="mini"
- @click="addPreviousJob">添加履历</el-button></div>
- <el-table :data="form.previousJobs" border size="small" class="job-table" empty-text="暂无入职前工作履历">
- <el-table-column type="index" label="序号" width="55" align="center" />
- <el-table-column min-width="150">
- <template slot="header"><span class="cell-required">工作开始日期</span></template>
- <template slot-scope="{ row }">
- <el-date-picker v-model="row.startDate" value-format="yyyy-MM-dd" type="date" placeholder="开始日期"
- class="full-width" />
- </template>
- </el-table-column>
- <el-table-column min-width="150">
- <template slot="header"><span class="cell-required">工作结束日期</span></template>
- <template slot-scope="{ row }">
- <el-date-picker v-model="row.endDate" value-format="yyyy-MM-dd" type="date" placeholder="结束日期"
- class="full-width" />
- </template>
- </el-table-column>
- <el-table-column min-width="150">
- <template slot="header"><span class="cell-required">前公司名称</span></template>
- <template slot-scope="{ row }">
- <el-input v-model.trim="row.company" maxlength="50" placeholder="请输入公司名称" />
- </template>
- </el-table-column>
- <el-table-column min-width="130">
- <template slot="header"><span class="cell-required">前公司岗位</span></template>
- <template slot-scope="{ row }">
- <el-input v-model.trim="row.position" maxlength="30" placeholder="请输入岗位" />
- </template>
- </el-table-column>
- <el-table-column label="前公司部门" min-width="130">
- <template slot-scope="{ row }">
- <el-input v-model.trim="row.department" maxlength="30" placeholder="请输入部门" />
- </template>
- </el-table-column>
- <el-table-column label="离职原因" min-width="130">
- <template slot-scope="{ row }">
- <el-input v-model.trim="row.leaveReason" maxlength="40" placeholder="请输入" />
- </template>
- </el-table-column>
- <el-table-column label="工作描述" min-width="180">
- <template slot-scope="{ row }">
- <el-input v-model.trim="row.description" maxlength="300" placeholder="请输入工作描述" />
- </template>
- </el-table-column>
- <el-table-column label="操作" width="90" align="center">
- <template slot-scope="{ $index }">
- <el-button type="text" icon="el-icon-delete" class="job-delete-btn"
- @click="removeItem('previousJobs', $index)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <div v-else-if="section.name === 'contract'" class="section-content">
- <header class="section-title">
- <div>
- <h3>劳动合同</h3>
- <p>劳动合同只读同步自合同管理</p>
- </div><span>{{ contractState }}</span>
- </header>
- <div class="subsection-title"><strong>劳动合同(只读)</strong></div>
- <el-row :gutter="16">
- <el-col :xs="24" :sm="12"><el-form-item label="分类"><el-input :value="form.contract.category" readonly
- placeholder="—" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="合同编号/名称"><el-input
- :value="form.contract.name || form.contract.number" readonly
- placeholder="—" /></el-form-item></el-col>
- <el-col :xs="24" :sm="8"><el-form-item label="签订时间"><el-input :value="form.contract.signDate" readonly
- placeholder="—" /></el-form-item></el-col>
- <el-col :xs="24" :sm="8"><el-form-item label="开始时间"><el-input :value="form.contract.startDate" readonly
- placeholder="—" /></el-form-item></el-col>
- <el-col :xs="24" :sm="8"><el-form-item label="结束时间"><el-input :value="form.contract.endDate" readonly
- placeholder="无固定期限/未同步" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="生效状态"><el-input :value="form.contract.status" readonly
- placeholder="—" /></el-form-item></el-col>
- <el-col :span="24"><el-form-item label="备注"><el-input :value="form.contract.remark" type="textarea"
- :rows="2" readonly placeholder="—" /></el-form-item></el-col>
- </el-row>
- </div>
- <div v-else-if="section.name === 'payroll'" class="section-content">
- <header class="section-title">
- <div>
- <h3>薪酬与社保</h3>
- <p>工资卡银行账户与社保公积金参保记录</p>
- </div><span>{{ form.socialSecurityRecords.length }} 条社保记录</span>
- </header>
- <!--
- 职责边界:本页只记「个人参保事实 + 银行账户」。
- 缴费比例 / 基数上下限是公司级参数(五险一金方案);
- 定岗薪资 / 绩效基数 / 参保方案覆盖按人按版本在「员工薪资档案」维护。
- -->
- <el-alert class="payroll-scope-alert" type="info" :closable="false" show-icon
- title="这里只记个人的参保事实与银行账户">
- <template slot="default">
- 社保 / 公积金的<b>缴费比例与基数上下限</b>是公司级参数,统一维护在「薪资核算 → 五险一金方案」,本页无需填写;
- <b>定岗薪资 / 绩效基数 / 参保方案覆盖</b>按人按版本维护在「薪资核算 → 员工薪资档案」。
- 本页「基数」只记录该员工的实际缴费基数。
- </template>
- </el-alert>
- <div class="subsection-title"><strong>薪酬银行</strong><el-button type="primary" size="mini"
- @click="addBankAccount">添加银行</el-button></div>
- <el-table :data="form.bankAccounts" border size="small" class="bank-table" empty-text="暂无银行账户">
- <el-table-column type="index" label="序号" width="55" align="center" />
- <el-table-column label="开户行" min-width="180">
- <template slot-scope="{ row }">
- <el-input v-model.trim="row.bankName" maxlength="50" placeholder="请输入开户行" />
- </template>
- </el-table-column>
- <el-table-column label="银行网点" min-width="160">
- <template slot-scope="{ row }">
- <el-input v-model.trim="row.bankBranch" maxlength="100" placeholder="请输入银行网点" />
- </template>
- </el-table-column>
- <el-table-column label="银行卡号" min-width="180">
- <template slot-scope="{ row }">
- <el-input v-model.trim="row.bankAccount" maxlength="24" placeholder="请输入银行卡号" />
- </template>
- </el-table-column>
- <el-table-column min-width="140">
- <template slot="header"><span class="cell-required">姓名</span></template>
- <template slot-scope="{ row }">
- <el-input v-model.trim="row.bankAccountName" maxlength="20" placeholder="请输入开户人" />
- </template>
- </el-table-column>
- <el-table-column label="操作" width="90" align="center">
- <template slot-scope="{ $index }">
- <el-button type="text" icon="el-icon-delete" class="bank-delete-btn"
- @click="removeItem('bankAccounts', $index)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- <div class="subsection-title"><strong>社保公积金</strong><el-button type="primary" size="mini"
- @click="addSocialSecurity">添加记录</el-button></div>
- <el-table :data="form.socialSecurityRecords" border size="small" class="social-table" empty-text="暂无社保记录">
- <el-table-column type="index" label="序号" width="55" align="center" />
- <el-table-column label="社保类型" min-width="140">
- <template slot-scope="{ row }">
- <el-select v-model="row.type" clearable placeholder="请选择" class="full-width">
- <el-option v-for="opt in socialSecurityOptions" :key="opt" :label="opt" :value="opt" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column label="是否启用" min-width="120">
- <template slot-scope="{ row }">
- <el-select v-model="row.isEnabled" placeholder="请选择" class="full-width"
- @change="handleSocialEnabledChange(row)">
- <el-option label="是" :value="1" />
- <el-option label="否" :value="0" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column label="基数" min-width="120">
- <template slot-scope="{ row }">
- <el-input-number v-model="row.base" :min="0" :precision="2" :controls="false" class="full-width" />
- </template>
- </el-table-column>
- <el-table-column label="参保日期" min-width="160">
- <template slot-scope="{ row }">
- <el-date-picker v-model="row.insuredDate" value-format="yyyy-MM-dd" type="date" placeholder="选择日期"
- class="full-width" />
- </template>
- </el-table-column>
- <el-table-column label="停保日期" min-width="150">
- <template slot-scope="{ row }">
- <el-date-picker v-model="row.stopDate" value-format="yyyy-MM-dd" type="date" placeholder="在职可为空"
- class="full-width" />
- </template>
- </el-table-column>
- <el-table-column label="停缴记录" min-width="130">
- <template slot-scope="{ row }">
- <el-select v-model="row.stopReason" clearable placeholder="请选择" class="full-width">
- <el-option v-for="opt in ['离职', '调转', '其他']" :key="opt" :label="opt" :value="opt" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="90" align="center">
- <template slot-scope="{ $index }">
- <!-- 已保存(接口回显带 id)的社保记录不允许删除,只能通过「停缴」处理 -->
- <el-button type="text" icon="el-icon-delete" class="social-delete-btn"
- @click="handleSocialDelete($index)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <div v-else-if="section.name === 'qualification'" class="section-content">
- <header class="section-title">
- <div>
- <h3>资质与技能</h3>
- <p>工种、证照、技能与档案附件</p>
- </div><span>{{ form.workTypes.length + form.certificates.length + form.skills.length }} 条资质记录</span>
- </header>
- <div class="subsection-title"><strong>工种</strong><el-button type="primary" size="mini"
- @click="openWorkTypeSelect">添加工种</el-button></div>
- <el-table :data="form.workTypes" border size="small" class="work-type-table" empty-text="暂无工种记录">
- <el-table-column type="index" label="序号" width="55" align="center" />
- <el-table-column label="类型" min-width="110" show-overflow-tooltip :formatter="workTypeTypeLabel" />
- <el-table-column prop="code" label="编码" min-width="110" show-overflow-tooltip />
- <el-table-column prop="name" label="名称" min-width="110" show-overflow-tooltip />
- <el-table-column label="等级" min-width="110" show-overflow-tooltip :formatter="workTypeLevelLabel" />
- <el-table-column label="标准工时费" min-width="110" show-overflow-tooltip
- :formatter="workTypeHourCostLabel" />
- <el-table-column label="工时费单位" min-width="110" show-overflow-tooltip :formatter="workTypeUnitLabel" />
- <el-table-column label="操作" width="90" align="left">
- <template slot-scope="{ $index }">
- <el-popconfirm class="ele-action" title="确定要删除当前工种吗?" @confirm="removeItem('workTypes', $index)">
- <el-link slot="reference" type="danger" :underline="false" icon="el-icon-delete">删除</el-link>
- </el-popconfirm>
- </template>
- </el-table-column>
- </el-table>
- <div class="subsection-title"><strong>资质证照列表</strong><el-button type="primary" size="mini"
- @click="addCertificate">添加资质证照</el-button></div>
- <el-table :data="form.certificates" border size="small" class="certificate-table" empty-text="暂无资质证照记录">
- <el-table-column type="index" label="序号" width="55" align="center" />
- <el-table-column min-width="200">
- <template slot="header"><span class="cell-required">证照类型</span></template>
- <template slot-scope="{ row }">
- <el-select v-model="row.type" placeholder="请选择" class="full-width" @change="handleCertTypeChange(row)">
- <el-option v-for="opt in certificateTypeOptions" :key="opt" :label="opt" :value="opt" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column min-width="130">
- <template slot="header"><span class="cell-required">证照编号</span></template>
- <template slot-scope="{ row }">
- <el-input v-model.trim="row.number" maxlength="30" placeholder="请输入" />
- </template>
- </el-table-column>
- <el-table-column label="等级" min-width="200">
- <template slot-scope="{ row }">
- <el-select v-model="row.level" clearable placeholder="请选择" class="full-width"
- :disabled="isCertLevelDisabled(row.type)">
- <el-option v-for="opt in certLevelOptionsFor(row.type)" :key="opt.value" :label="opt.label"
- :value="opt.value" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column min-width="170">
- <template slot="header"><span class="cell-required">有效期开始日期</span></template>
- <template slot-scope="{ row }">
- <el-date-picker v-model="row.startDate" value-format="yyyy-MM-dd" type="date" placeholder="选择日期"
- class="full-width" />
- </template>
- </el-table-column>
- <el-table-column min-width="170">
- <template slot="header"><span class="cell-required">有效期结束日期</span></template>
- <template slot-scope="{ row }">
- <el-date-picker v-model="row.endDate" value-format="yyyy-MM-dd" type="date" placeholder="选择日期"
- class="full-width" />
- </template>
- </el-table-column>
- <el-table-column label="颁布日期" min-width="170">
- <template slot-scope="{ row }">
- <el-date-picker v-model="row.issueDate" value-format="yyyy-MM-dd" type="date" placeholder="选择日期"
- class="full-width" />
- </template>
- </el-table-column>
- <el-table-column label="证照附件" min-width="140">
- <template slot-scope="{ row }">
- <fileMain v-model="row.attachment" size="mini" />
- </template>
- </el-table-column>
- <el-table-column min-width="110">
- <template slot="header"><span class="cell-required">状态</span></template>
- <template slot-scope="{ row }">
- <el-select v-model="row.status" placeholder="请选择" class="full-width">
- <el-option label="有效" value="有效" />
- <el-option label="失效" value="失效" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column label="是否有岗位津贴" min-width="140">
- <template slot-scope="{ row }">
- <el-select v-model="row.hasPositionAllowance" placeholder="请选择" class="full-width"
- @change="handlePositionAllowanceChange(row)">
- <el-option label="是" :value="1" />
- <el-option label="否" :value="0" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column label="津贴金额" min-width="130">
- <template slot-scope="{ row }">
- <!-- 不设 precision:整数就显示整数(100),不要 100.00;小数按输入保留 -->
- <el-input-number v-model="row.positionAllowanceAmount" :min="0" :controls="false"
- placeholder="请输入" class="full-width" />
- </template>
- </el-table-column>
- <el-table-column label="津贴单位" min-width="140">
- <template slot-scope="{ row }">
- <el-select v-model="row.positionAllowanceUnit" placeholder="请选择" class="full-width">
- <el-option v-for="item in allowanceUnitOptions" :key="item.value" :label="item.label"
- :value="item.value" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column label="备注" min-width="140">
- <template slot-scope="{ row }">
- <el-input v-model.trim="row.remark" maxlength="200" placeholder="请输入" />
- </template>
- </el-table-column>
- <el-table-column label="操作" width="90" align="center">
- <template slot-scope="{ $index }">
- <el-button type="text" icon="el-icon-delete" class="certificate-delete-btn"
- @click="removeItem('certificates', $index)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- <div class="subsection-title"><strong>技能列表</strong><el-button type="primary" size="mini"
- @click="addSkill">添加技能</el-button></div>
- <el-table :data="form.skills" border size="small" class="skill-table" empty-text="暂无技能记录">
- <el-table-column type="index" label="序号" width="55" align="center" />
- <el-table-column label="分类" min-width="130">
- <template slot-scope="{ row }">
- <el-select v-model="row.category" clearable placeholder="请选择" class="full-width">
- <el-option v-for="opt in skillCategoryOptions" :key="opt" :label="opt" :value="opt" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column min-width="140">
- <template slot="header"><span class="cell-required">技能名称</span></template>
- <template slot-scope="{ row }">
- <el-input v-model.trim="row.name" maxlength="30" placeholder="请输入" />
- </template>
- </el-table-column>
- <el-table-column label="技能等级" min-width="120">
- <template slot-scope="{ row }">
- <el-select v-model="row.level" clearable placeholder="请选择" class="full-width">
- <el-option v-for="opt in skillLevelOptions" :key="opt" :label="opt" :value="opt" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column label="技能经验(年限)" min-width="130">
- <template slot-scope="{ row }">
- <el-input-number v-model="row.years" :min="0" :precision="1" :controls="false" class="full-width" />
- </template>
- </el-table-column>
- <el-table-column label="证照附件" min-width="140">
- <template slot-scope="{ row }">
- <fileMain v-model="row.attachment" size="mini" />
- </template>
- </el-table-column>
- <el-table-column label="备注" min-width="160">
- <template slot-scope="{ row }">
- <el-input v-model.trim="row.remark" maxlength="200" placeholder="培训经历、考核情况" />
- </template>
- </el-table-column>
- <el-table-column label="操作" width="90" align="center">
- <template slot-scope="{ $index }">
- <el-button type="text" icon="el-icon-delete" class="skill-delete-btn"
- @click="removeItem('skills', $index)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- <div class="subsection-title attachment-title"><strong>档案附件</strong>
- <fileMain v-model="form.files" size="mini" />
- </div>
- <el-form-item label="档案备注" class="remarks-item"><el-input v-model.trim="form.remarks" type="textarea"
- :rows="3" maxlength="500" show-word-limit placeholder="补充需要说明的员工档案信息" /></el-form-item>
- </div>
- <div v-else class="section-content record-section-content">
- <employee-archive-records ref="records" :archive="form" />
- </div>
- </el-tab-pane>
- </el-tabs>
- </el-form>
- <span slot="footer" class="dialog-footer">
- <span></span>
- <span>
- <el-button @click="closeDialog">取消</el-button>
- <el-button type="primary" :loading="saving" @click="submit">保存</el-button>
- </span>
- </span>
- </el-dialog>
- <aptitude-dialog ref="workTypeSelect" :disabled-ids="selectedWorkTypeIds" @success="handleWorkTypeSelect"
- @update:visible="showWorkTypeSelect = $event" />
- </div>
- </template>
- <script>
- import EmployeeArchiveRecords from './EmployeeArchiveRecords.vue';
- import AptitudeDialog from '@/views/hrManagement/jobManagement/components/aptitudeDialog.vue';
- import dictMixins from '@/mixins/dictMixins';
- import {
- getPositionById,
- getPositionSequenceList,
- getPositionSequencePage,
- getPositionSequenceLevels,
- getDepartmentPositionSequences,
- getDepartmentPositions,
- getDepartmentPositionType,
- getDepartmentPositionLevels,
- } from '@/api/hr';
- import { listOrganizations, getFactoryAreaPage, searchUsersByKeyword, loadEmployeeBriefs, EMPLOYEE_STATUS, EMPLOYEE_STATUS_CODE, CERT_TYPE } from '@/api/organization';
- import { getDictByCode } from '@/api/sys';
- import { loadEmployeeStatusOptions } from '@/utils/employee-status-dict';
- /* 性别:接口口径 1 男 / 2 女(下拉 value 即接口码);没有值就留空,不提交该字段 */
- import { GENDER_OPTIONS, normalizeGender } from '@/utils/gender';
- import { buildOrganizationTree, flattenOrganizationTree } from '@/utils/organization-tree';
- import { toDocIds } from '@/components/addDoc/util';
- import { positionAllowanceUnitOptions } from '@/enum/dict.js';
- /* 证照等级:按证照类型联动(证件类型 → 可选等级对照表) */
- import { getCertLevelOptions, isCertLevelDisabled, NO_LEVEL_CERT_TYPES, NO_LEVEL_VALUE } from '@/enum/certLevel';
- /* 所属机构默认值取登录缓存(loginChangeGroupVOList 第一条) */
- import { getCurrentOrgId, getOrgOptions } from '@/utils/current-org';
- /* 明确报错:岗位性质字典失败时提示,不静默留空 */
- import { showRequestError } from '@/utils/message';
- const emptyContract = () => ({ category: '', name: '', signDate: '', startDate: '', endDate: '', files: [], status: '生效', remark: '' });
- const emptyForm = () => ({
- id: null, avatar: '', name: '', employeeNo: '', gender: '', birthDate: '', age: null, idCard: '', idCardExpiry: '', ethnic: '汉族',
- maritalStatus: '未婚', politicalStatus: '群众', nativePlace: '', householdType: '', healthStatus: '优',
- /* 所属机构只认 groupId + 登录缓存里的机构名称:不再有写死的公司名 / legalEntity 缓存字段 */
- groupId: null, factory: '', factoryId: null, deptId: null, deptIds: [], deptName: '',
- /* 主所属部门:树状单选,回显详情接口的 deptId(deptId/deptName 与 kd-aiot-frontend 同源)。
- 只在前端做选择与去重,**不参与提交**(saveNew 入参没有 deptId)。 */
- primaryDeptId: '',
- position: '', positionId: null, positionIds: [], positionBindings: [],
- // 岗位序列(部门 → 序列 → 岗位 → 性质 → 层级,与办理入职一致)
- positionSequenceId: '', positionSequence: '', positionSequenceName: '',
- positionType: '', level: '', staffingLevel: '', dutyLevel: '', manager: '', managerId: null,
- // 新建默认「正式在职」:下拉 value 是 hr_user_status 字典的数字状态码,'1' 即「正式在职」
- employmentType: '', employmentTypeName: '', status: '1', hireDate: '', regularDate: '', account: '', enabled: true,
- phone: '', email: '', wechat: '', workAddress: '', currentAddress: '', registeredAddress: '', emergencyContact: '', emergencyRelation: '', emergencyPhone: '', familyMembers: [],
- education: '本科', school: '', major: '', graduationDate: '',
- educations: [],
- previousJobs: [],
- contract: emptyContract(), bankAccounts: [],
- socialSecurityRecords: [],
- workTypes: [],
- certificates: [],
- skills: [],
- files: [], remarks: '',
- attendanceRecords: [], performanceRecords: [], trainingRecords: [], rewardPunishmentRecords: [], awards: [], positionTransfers: [], contractChanges: [],
- leaveInfo: { enabled: false, position: '', department: '', leaveDate: '', leaveType: '', leaveReason: '', serviceYears: '', handoverStatus: '' }
- });
- export default {
- name: 'EmployeeArchiveForm',
- components: { EmployeeArchiveRecords, AptitudeDialog },
- mixins: [dictMixins],
- props: {
- visible: Boolean,
- employee: { type: Object, default: null },
- organizations: { type: Array, default: () => [] },
- existingNumbers: { type: Array, default: () => [] }
- },
- data() {
- const validatePhone = (rule, value, callback) => {
- if (!value || /^1\d{10}$/.test(value) || /^1\d{2}\*{4}\d{4}$/.test(value)) callback();
- else callback(new Error('请输入正确的 11 位手机号码'));
- };
- /**
- * 身份证:必填(2026-10-28 用户要求),只校验位数(15 位纯数字 / 18 位且末位可为 X),
- * 与「办理入职」口径一致;不再校验出生日期合法性、不再算校验位——避免真实/历史数据被前端拦下。
- */
- const validateIdCard = (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- const text = String(value || '').trim();
- if (!text) return callback(new Error('请输入身份证号码'));
- // 脱敏展示(如 4301********1234)允许通过
- if (/^\d{6}\*{8}[\dXx]{4}$/.test(text)) return callback();
- if (!/^\d{15}$|^\d{17}[\dXx]$/.test(text)) {
- return callback(new Error('请输入正确的身份证号码'));
- }
- callback();
- };
- const validateNumber = (rule, value, callback) => {
- const duplicate = this.existingNumbers.some((item) => item === value && item !== this.employee?.employeeNo);
- if (duplicate) callback(new Error('员工编号已存在'));
- else callback();
- };
- return {
- activeTab: 'base', saving: false, fullscreen: false, form: emptyForm(), organizationTree: [],
- showWorkTypeSelect: false,
- workTypeLevelOptions: [
- { label: '初级', value: '1' },
- { label: '中级', value: '2' },
- { label: '高级', value: '3' }
- ],
- sections: [
- { name: 'base', label: '基本信息', subtitle: '身份/任职/联系', icon: 'el-icon-user' },
- { name: 'experience', label: '教育与履历', subtitle: '学习与工作经历', icon: 'el-icon-notebook-2' },
- { name: 'contract', label: '合同', subtitle: '劳动合同信息', icon: 'el-icon-document-checked' },
- { name: 'payroll', label: '薪酬与社保', subtitle: '银行/社保公积金', icon: 'el-icon-bank-card' },
- { name: 'qualification', label: '资质证照', subtitle: '工种/证照/技能', icon: 'el-icon-medal' },
- { name: 'records', label: '业务记录', subtitle: '考勤绩效与异动', icon: 'el-icon-tickets' }
- ],
- // 岗位设定列表(岗位下拉数据源)
- positionOptions: [],
- // 岗位序列下拉(多部门并集,来源 /fm/tmaindepartmentposition/sequence/list)
- sequenceOptions: [],
- sequenceLoading: false,
- // 岗位层级名 → 层级主键(部门岗位接口返回,提交 positionLevelId 用)
- levelIdByName: {},
- // HR 岗位分页真实主键集合(主数据 postId 如 1,4 不在此集合,禁止对其调 getById)
- hrPositionIdSet: {},
- // 岗位序列 → 层级主键,用于提交 positionLevelId / positionSequenceId
- sequenceLevelMap: {},
- // 强制重建隶属部门级联,避免 options 清空后内部 activePath 残留导致 node.level 报错
- deptCascaderKey: 0,
- // 岗位单选回显:用 key 重建,避免 options 后到时只显示 ID
- positionSelectKey: 0,
- positionLoading: false,
- // 直属上级:按隶属部门联动人员
- managerOptions: [],
- managerLoading: false,
- ethnicOptions: ['汉族', '土家族', '苗族', '侗族', '回族', '瑶族', '其他'],
- genderOptions: [...GENDER_OPTIONS],
- // 员工状态下拉:由 hr_user_status 字典填充(见 loadStatusOptions)
- statusOptions: [],
- maritalOptions: ['未婚', '已婚', '离异', '丧偶'],
- educationOptions: ['小学', '初中', '高中', '大专', '本科', '硕士', '博士'],
- employmentOptions: [],
- staffingLevelOptions: [],
- factoryOptions: [],
- factoryLoading: false,
- socialSecurityOptions: ['四险', '四险一金', '五险', '五险一金', '六险一金'],
- contractTypeOptions: ['固定期限', '无固定期限', '以完成一定工作任务为期限', '劳务协议', '实习协议'],
- // 证照类型:取自 CERT_TYPE 键(与提交编码/回显反查同一数据源,新增类型只需改 CERT_TYPE)
- certificateTypeOptions: Object.keys(CERT_TYPE),
- skillCategoryOptions: ['专业技能', '办公技能', '语言技能', '特种操作技能', '其他'],
- skillLevelOptions: ['了解', '熟练', '精通', '初级', '中级', '高级'],
- organizationProps: { value: 'id', label: 'name', children: 'children', emitPath: false, checkStrictly: true },
- rules: {
- name: [{ required: true, message: '请输入员工姓名', trigger: 'blur' }],
- employeeNo: [{ required: true, message: '请输入工号', trigger: 'blur' }, { pattern: /^[A-Za-z0-9_-]{2,20}$/, message: '仅支持字母、数字、下划线和短横线', trigger: 'blur' }, { validator: validateNumber, trigger: 'blur' }],
- /* 性别:必填(2026-10-28 用户要求)。required 只负责显示必填星号,中文提示由 validator 出 */
- gender: [{
- required: true,
- validator: (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- if (!value) callback(new Error('请选择性别'));
- else callback();
- },
- trigger: 'change',
- }],
- groupId: [{
- required: true,
- validator: (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- if (value == null || value === '') callback(new Error('请选择所属机构'));
- else callback();
- },
- trigger: 'change',
- }],
- /* 隶属部门:非必填(2026-10-29 用户要求),保留校验器仅为占位口径统一,空值直接放行 */
- deptIds: [{
- validator: (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- callback();
- },
- trigger: 'change',
- }],
- /* 主所属部门(form.primaryDeptId):必填,只在新增态校验——编辑态该字段是只读回显文本,
- 用户改不了,若历史数据没主部门会永远卡住保存(编辑态 form-item 的 prop 已置空)。 */
- primaryDeptId: [{
- required: true,
- validator: (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- if (value == null || value === '') {
- callback(new Error('请选择主所属部门'));
- return;
- }
- callback();
- },
- trigger: 'change',
- }],
- /* 员工状态:必填。required 只负责显示必填星号,实际校验走 validator(_skipFormValidate 时跳过) */
- status: [{
- required: true,
- validator: (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- if (!value) callback(new Error('请选择员工状态'));
- else callback();
- },
- trigger: 'change',
- }],
- hireDate: [{
- required: true,
- validator: (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- if (!value) callback(new Error('请选择入职时间'));
- else callback();
- },
- trigger: 'change',
- }],
- positionSequenceId: [{
- required: true,
- validator: (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- if (value == null || value === '') {
- callback(new Error('请选择岗位序列'));
- return;
- }
- callback();
- },
- trigger: 'change',
- }],
- positionId: [{
- required: true,
- validator: (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- if (value == null || value === '') {
- callback(new Error('请选择岗位'));
- return;
- }
- callback();
- },
- trigger: 'change',
- }],
- /* 岗位性质(form.employmentType,提交字典 id):必填 */
- employmentType: [{
- required: true,
- validator: (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- if (value == null || value === '') {
- callback(new Error('请选择岗位性质'));
- return;
- }
- callback();
- },
- trigger: 'change',
- }],
- /* 岗位层级(form.staffingLevel):必填。岗位未配置层级时下拉无候选,给出可操作的提示 */
- staffingLevel: [{
- required: true,
- validator: (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- if (value == null || value === '') {
- let message = '请选择岗位层级';
- if (!this.staffingLevelOptions.length) {
- message = this.form.positionId
- ? '当前岗位未配置岗位层级,请在岗位档案中维护后再保存'
- : '请先选择岗位';
- }
- callback(new Error(message));
- return;
- }
- callback();
- },
- trigger: 'change',
- }],
- phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }, { validator: validatePhone, trigger: 'blur' }],
- email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }],
- /* 身份证:必填(required 只为星号,空值/格式提示都在 validateIdCard 里) */
- idCard: [{ required: true, validator: validateIdCard, trigger: ['blur', 'change'] }],
- }
- };
- },
- computed: {
- dialogVisible: {
- get() { return this.visible; },
- set(value) {
- if (!value) this.saving = false;
- this.$emit('update:visible', value);
- }
- },
- isEdit() { return Boolean(this.employee?.id); },
- /* 编辑态「主所属部门」回显文本:只认详情接口的 deptName(adaptEmployee 直读 raw.deptName)。
- ★ 不能用 form.deptName:打开弹窗后 normalizeDeptAgainstOptions → applyDeptIds 会把它重写成
- 「隶属部门拼接串」,主部门就显示成隶属部门了(历史 bug,用户实拍物资部/机构名)。 */
- primaryDeptName() {
- if (!this.isEdit) return '';
- return this.employee?.deptName || '';
- },
- /* 主所属部门选项:机构节点本身置灰(公司级不可选)+ 其下部门(工厂层已剥掉,与隶属部门同一棵部门树)。
- 与所属机构级联:换机构后 form.groupId 变化 → 这里重算,页面靠 :key 重建级联组件。 */
- mainDeptOptions() {
- if (!this.form.groupId) return [];
- const orgNode = this.findOrgNodeById(this.form.groupId);
- if (!orgNode) return [];
- const company = {
- id: String(orgNode.id),
- // 根节点名与「所属机构」保持同一来源(编辑态优先接口 groupName)
- name: this.employee?.company || orgNode.name || '',
- disabled: true,
- children: this.deptOptions,
- };
- return [company];
- },
- nameInitial() { return (this.form.name || '新').slice(-1); },
- avatarStyle() { const tones = ['#1890ff', '#13a471', '#7c5ce7', '#d08716', '#d25567']; const code = [...(this.form.name || '新')].reduce((sum, char) => sum + char.charCodeAt(0), 0); return { '--avatar-color': tones[code % tones.length] }; },
- currentSectionLabel() { return this.sections.find((item) => item.name === this.activeTab)?.label || ''; },
- // 所属机构:名称只认登录缓存「所属机构」(sessionStorage.loginChangeGroupVOList)里的 name。
- // ★ 2026-10-28 清掉三处编造:名称缺失的机构不进下拉(原来会退化成显示 id),
- // 编辑态补出的选项也不再用 form.legalEntity / form.company / 裸 id 顶替名称,
- // 组织树里查不到就如实标注「机构名称未返回」,暴露接口缺口。
- legalEntities() {
- let list = getOrgOptions()
- .map((item) => ({ id: String(item.id), name: item.name || '' }))
- .filter((item) => item.name);
- if (!list.length) {
- const tree = this.organizationTree.length ? this.organizationTree : this.organizations;
- list = flattenOrganizationTree(tree || [])
- .filter((item) => ['10', '20'].includes(String(item.type)) && item.name)
- .map((item) => ({ id: String(item.id), name: item.name }));
- }
- const currentId = this.form.groupId != null && this.form.groupId !== '' ? String(this.form.groupId) : '';
- /* 编辑态:所属机构的展示名一律以详情接口返回的 groupName(adaptEmployee → company)为准。
- 原来只按 id 去组织树反查名称,命中的节点可能不是机构(实例:机构位被显示成「财务部」),
- 所以接口给了 groupName 就直接用它覆盖同名 id 的选项名称。 */
- const apiName = this.employee?.company || '';
- if (currentId) {
- const idx = list.findIndex((item) => String(item.id) === currentId);
- if (idx >= 0) {
- if (apiName) list[idx] = { ...list[idx], name: apiName };
- } else {
- const tree = this.organizationTree.length ? this.organizationTree : this.organizations;
- const hit = flattenOrganizationTree(tree || []).find((item) => String(item.id) === currentId);
- list.unshift({
- id: currentId,
- // 接口 groupName 优先,其次组织树真实名称,都没有就如实标注缺口(不拿 id 冒充)
- name: apiName || hit?.name || `机构名称未返回(id: ${currentId})`
- });
- }
- }
- return list;
- },
- // 隶属部门:只跟所属机构关联(与工厂相互独立),选完机构即可选部门。
- // 组织树里部门挂在工厂下,这里把工厂层剥掉,把各部门提级展示
- deptOptions() {
- const tree = this.organizationTree.length ? this.organizationTree : this.organizations;
- if (!this.form.groupId) return [];
- const find = (nodes, id) => {
- for (const node of nodes || []) {
- if (!node) continue;
- if (String(node.id) === String(id)) return node;
- const matched = find(node.children || [], id);
- if (matched) return matched;
- }
- return null;
- };
- const orgNode = find(tree || [], this.form.groupId);
- if (!orgNode) return [];
- const isFactoryNode = (node) => ['40', 'factory'].includes(String(node.type));
- // 工厂只作为归属信息:其下的部门直接提到机构层,选任一工厂的部门都能直接选
- const collectDeptRoots = (nodes) => {
- const list = [];
- (nodes || []).forEach((node) => {
- if (!node) return;
- if (isFactoryNode(node)) {
- list.push(...collectDeptRoots(node.children || []));
- return;
- }
- list.push(node);
- });
- return list;
- };
- const sanitize = (nodes) =>
- (nodes || [])
- .filter(Boolean)
- .map((node) => {
- const children = sanitize(node.children || []);
- const next = {
- ...node,
- id: node.id != null ? String(node.id) : node.id,
- name: node.name || '',
- };
- if (children.length) next.children = children;
- else delete next.children;
- return next;
- });
- return sanitize(collectDeptRoots(orgNode.children || []));
- },
- deptCascaderProps() {
- return { ...this.organizationProps, multiple: true };
- },
- /* 隶属部门选项:部门树里把「主所属部门」那个节点置灰(同一部门不能既是主部门又是隶属部门,
- 与 kd-aiot-frontend 的 affiDeptTree 一致)。只做浅拷贝标记,不动 deptOptions 本身 */
- affiDeptOptions() {
- const primaryId = this.form.primaryDeptId == null ? '' : String(this.form.primaryDeptId);
- const mark = (nodes) =>
- (nodes || []).map((node) => {
- const next = { ...node, disabled: Boolean(primaryId) && String(node.id) === primaryId };
- if (node.children) next.children = mark(node.children);
- return next;
- });
- return mark(this.deptOptions);
- },
- /* 岗位链路级联开关:只看主所属部门(2026-10-29 用户确认级联来源改为主所属部门) */
- hasPrimaryDept() {
- return this.form.primaryDeptId != null && this.form.primaryDeptId !== '';
- },
- hasPositionSequence() {
- return Boolean(this.form.positionSequenceId);
- },
- requiredProgress() {
- // 与 form rules 一一对应(含主所属部门 / 身份证 / 岗位序列 / 岗位性质 / 岗位层级),保证「必填 x/y」口径准确
- // 隶属部门 deptIds 已改非必填(2026-10-29),不进必填计数(仍留在 sectionProgress 完整度统计里)
- const keys = ['name', 'employeeNo', 'gender', 'idCard', 'groupId', 'primaryDeptId', 'status', 'hireDate', 'positionSequenceId', 'positionId', 'employmentType', 'staffingLevel', 'phone'];
- return { base: `${keys.filter((key) => { const value = this.form[key]; return Array.isArray(value) ? value.length : Boolean(value); }).length}/${keys.length}` };
- },
- sectionProgress() {
- const count = (keys) => `${keys.filter((key) => { const value = this.form[key]; return Array.isArray(value) ? value.length : Boolean(value); }).length}/${keys.length}`;
- return {
- base: count(['name', 'employeeNo', 'groupId', 'factory', 'deptIds', 'status', 'hireDate', 'positionId', 'employmentType', 'staffingLevel', 'gender', 'birthDate', 'idCard', 'ethnic', 'politicalStatus', 'maritalStatus', 'nativePlace', 'education', 'school', 'phone', 'email', 'wechat', 'currentAddress', 'registeredAddress', 'workAddress', 'emergencyContact', 'emergencyPhone']),
- experience: count(['educations', 'previousJobs']),
- contract: `${[this.form.contract.number || this.form.contract.name, this.form.contract.startDate, this.form.contract.endDate].filter(Boolean).length}/3`,
- payroll: count(['bankAccounts', 'socialSecurityRecords']),
- qualification: count(['workTypes', 'certificates', 'skills', 'files']),
- records: `${['attendanceRecords', 'performanceRecords', 'trainingRecords', 'rewardPunishmentRecords', 'awards', 'positionTransfers', 'contractChanges'].reduce((sum, key) => sum + this.form[key].length, 0) + (this.form.leaveInfo.enabled ? 1 : 0)}`
- };
- },
- completionRate() {
- /* ★ 2026-10-28 修正:原来取 form.bankAccount / form.insuredDate,这两个字段在 form 上根本不存在
- (它们是 bankAccounts[] / socialSecurityRecords[] 的行内字段),恒为 false → 完整度最多 16/18、
- 永远填不到 100%。改成按数组是否有行来判断。 */
- const values = [this.form.name, this.form.employeeNo, this.form.groupId, this.form.positionId, this.form.status, this.form.hireDate, this.form.phone, this.form.gender, this.form.email, this.form.birthDate, this.form.education, this.form.school, this.form.currentAddress, this.form.contract.number, this.form.bankAccounts?.length, this.form.socialSecurityRecords?.length, this.form.skills?.length, this.form.files?.length];
- return Math.round(values.filter(Boolean).length / values.length * 100);
- },
- contractState() { return this.form.contract.name ? (this.form.contract.endDate ? '合同资料已填写' : '待补结束日期') : '待录入合同'; },
- selectedWorkTypeIds() {
- return (this.form.workTypes || [])
- .map((item) => item.id)
- .filter((id) => id != null && id !== '');
- },
- /** 隶属部门级联的占位符文案(启用后由内层搜索框渲染,见 syncDeptCascaderPlaceholder) */
- deptCascaderPlaceholder() {
- return this.form.groupId ? '请选择隶属部门(可多选)' : '请先选择所属机构';
- },
- /** 证照「津贴单位」选项:前端写死(元/月、元/季、元/年),不走接口字典 */
- allowanceUnitOptions() {
- return positionAllowanceUnitOptions;
- }
- },
- created() {
- this.requestDict('工种类型');
- this.loadStatusOptions();
- },
- mounted() {
- this.syncDeptCascaderPlaceholder();
- },
- updated() {
- this.syncDeptCascaderPlaceholder();
- },
- watch: {
- visible(value) { if (value) this.loadForm(); }
- },
- methods: {
- /**
- * 员工状态下拉选项:直接取 hr_user_status 字典(value = 字典值,即后端 status),
- * 新增/编辑与列表「状态」筛选共用同一个加载器(utils/employee-status-dict)。
- * 字典拿不到就明确报错并留空,不再用内置码表静默顶替。
- */
- async loadStatusOptions() {
- try {
- this.statusOptions = await loadEmployeeStatusOptions();
- } catch (error) {
- this.statusOptions = [];
- console.error('[员工档案] 员工状态字典加载失败:', error);
- this.$message.error(error.message || '员工状态字典加载失败');
- }
- // 字典可能在弹框打开之后才回来:用字典值把已回填的值对齐一次(只对齐字典里存在的值)
- if (this.visible && this.employee) {
- const next = this.resolveStatusForForm(this.employee);
- if (this.statusOptions.some((opt) => String(opt.value) === String(next))) {
- this.form.status = next;
- }
- }
- },
- /**
- * 回显员工状态:下拉 value 一律是**字典里的数字状态码**,所以按数字码匹配。
- * ★ 字典实测码空间:1=正式在职、6=试用期、7=待岗、8=已离职(swagger 注释 1-6 与实际不符)。
- * ① 字典里有该状态码 → 直接用(正常路径,如 status=1 → 「正式在职」);
- * ② 字典里没有 → 用 EMPLOYEE_STATUS_CODE 把码转成中文再按字典文案匹配,命中即用字典值,并 warn;
- * ③ 仍命中不了 → 原码原样回显(会显示成数字,一眼能看出字典缺项),不编造文案。
- * ④ 历史数据没有状态码时才按中文文案反查。
- * ⚠ 取码只认 `statusCode`(adaptEmployee 里 = 后端原始 status),其次是 `raw.status`:
- * 千万别读 `source.status`,它在 adaptEmployee 里是列表态
- * (active / probation / inactive / disabled)。
- * ⚠ 原实现 `return this.statusOptions[0]` 已删除——两个条件都失配时它会直接回落成
- * 字典第一项,这正是「列表已离职、编辑里变待入职」的来源。
- */
- resolveStatusForForm(source = {}) {
- const rawCode = source.statusCode ?? source.raw?.status;
- const code = rawCode == null || rawCode === '' ? null : Number(rawCode);
- if (code != null && Number.isFinite(code) && code > 0) {
- const value = String(code);
- if (this.statusOptions.some((opt) => String(opt.value) === value)) return value;
- const backendLabel = EMPLOYEE_STATUS_CODE[code] || '';
- const byDictLabel = backendLabel
- ? this.statusOptions.find(
- (opt) => opt.label === backendLabel || opt.label.includes(backendLabel),
- )
- : null;
- if (byDictLabel) {
- console.warn(
- `[员工档案] 员工状态字典缺 ${value}(后端语义「${backendLabel}」),已按文案回显为「${byDictLabel.label}」`,
- );
- return byDictLabel.value;
- }
- console.warn(`[员工档案] 员工状态字典缺 ${value}(后端语义「${backendLabel}」),该状态下拉按原码显示`);
- return value;
- }
- // 历史数据没有状态码时才按中文反查;仍匹配不到就保留原值(不编造一个状态出来)
- const byLabel = this.statusOptions.find(
- (opt) => opt.label === source.formStatus || opt.label === source.status,
- );
- if (byLabel) return byLabel.value;
- return source.formStatus || source.status || '';
- },
- /** 离职页签强制写状态时用的值:优先取字典里的「离职」项,字典缺项才退回后端码 */
- resolveLeaveStatusValue() {
- const hit = this.statusOptions.find((opt) => opt.label.includes('离职'));
- if (hit) return hit.value;
- console.warn('[员工档案] 员工状态字典里没有「离职」项,离职保存将退回后端码 8');
- return String(EMPLOYEE_STATUS['离职']);
- },
- /**
- * 回显性别。
- * ★ 接口码优先:adaptEmployee 里 `raw.sex` 是后端原始码(1 男 / 2 女),
- * `gender` 只是它转好的中文文案;两者都过 normalizeGender 落到下拉 value('1'/'2')。
- * 取不到值 → 留空(下拉显示 placeholder),不编造「男」,也不显示「未知」选项。
- */
- resolveGenderForForm(source = {}) {
- const rawSex = source.raw?.sex ?? source.sex;
- if (rawSex != null && rawSex !== '') return normalizeGender(rawSex);
- return normalizeGender(source.gender);
- },
- loadForm() {
- this._skipFormValidate = true;
- this.activeTab = 'base';
- const source = this.employee ? JSON.parse(JSON.stringify(this.employee)) : {};
- const empty = emptyForm();
- this.form = {
- ...empty, ...source,
- groupId: source.groupId != null && source.groupId !== ''
- ? String(source.groupId)
- : (source.legalEntityId != null && source.legalEntityId !== '' ? String(source.legalEntityId) : null),
- // 所属机构名称不再单独缓存到表单(legalEntities 直接读登录缓存 / 组织树),不编造名称
- status: this.resolveStatusForForm(source),
- /* 性别:下拉 value 就是接口码(1 男 / 2 女),回填统一走 normalizeGender;
- 接口没给值(或值无法识别)就留空,不编造「男」也不显示「未知」 */
- gender: this.resolveGenderForForm(source),
- hireDate: source.hireDate || source.joinDate || '',
- factoryId: source.factoryId != null && source.factoryId !== '' ? String(source.factoryId) : null,
- factory: source.factory || source.factoryName || '',
- deptIds: this.normalizeDeptIds(source.deptIds?.length ? source.deptIds : source.deptId),
- deptName: source.deptName || '',
- /* 主所属部门:回显只认详情接口的 deptId(名称由级联组件按树上的节点显示);
- 取不到就留空,不从 deptIds 反查、不拿隶属部门拼串顶替 */
- primaryDeptId: source.deptId != null && source.deptId !== '' ? String(source.deptId) : '',
- managerId: source.managerId != null && source.managerId !== '' ? String(source.managerId) : null,
- manager: source.manager || '',
- positionIds: this.normalizePositionIds(source).slice(0, 1),
- positionBindings: Array.isArray(source.positionBindings)
- ? source.positionBindings.slice(0, 1)
- : [],
- positionId: this.normalizePositionIds(source)[0] || null,
- position: source.position || source.postName || '',
- // 岗位序列:优先员工主数据字段,其次岗位绑定项
- positionSequenceId: this.pickSequenceId(source),
- positionSequence: source.positionSequence || source.positionSequenceName || '',
- positionSequenceName: source.positionSequenceName || source.positionSequence || '',
- contract: { ...emptyContract(), ...(source.contract || {}) },
- leaveInfo: { ...empty.leaveInfo, ...(source.leaveInfo || {}) },
- familyMembers: source.familyMembers || [], files: toDocIds(source.files),
- // 教育经历:兼容旧的单条 education/degree/school 字段
- educations: source.educations?.length
- ? source.educations
- : (source.education
- ? [{ education: source.education || '本科', degree: source.degree || '', graduationDate: source.graduationDate || '', school: source.school || '', major: source.major || '' }]
- : []),
- // 薪酬银行:兼容旧的单条 bankName/bankAccount 字段
- bankAccounts: source.bankAccounts?.length
- ? source.bankAccounts
- : (source.bankName || source.bankAccount
- ? [{ bankName: source.bankName || '', bankBranch: source.bankBranch || '', bankAccount: source.bankAccount || '', bankAccountName: source.bankAccountName || '' }]
- : []),
- // 工作履历:兼容旧的 dateRange
- previousJobs: (source.previousJobs || []).map((item) => ({
- ...item,
- startDate: item.startDate || item.dateRange?.[0] || '',
- endDate: item.endDate || item.dateRange?.[1] || '',
- })),
- // 技能:兼容旧字符串数组
- skills: (source.skills || []).map((item) => (typeof item === 'string' ? { name: item, category: '', level: '', years: 0, attachment: [], remark: '' } : { ...item, attachment: toDocIds(item.attachment) })),
- // 证照:岗位津贴 0 否 / 1 是,金额转数字,兼容接口返回字符串
- certificates: (source.certificates || []).map((item) => ({
- ...item,
- hasPositionAllowance: Number(item.hasPositionAllowance) === 1 ? 1 : 0,
- positionAllowanceAmount: item.positionAllowanceAmount === '' || item.positionAllowanceAmount == null
- ? null
- : Number(item.positionAllowanceAmount),
- // 津贴单位是写死的 month/quarter/year,接口若返回数字要先转字符串,否则下拉匹配不上;
- // 有津贴但接口没给单位时补默认「元/月」
- positionAllowanceUnit: item.positionAllowanceUnit === '' || item.positionAllowanceUnit == null
- ? (Number(item.hasPositionAllowance) === 1 ? this.defaultAllowanceUnit() : '')
- : String(item.positionAllowanceUnit),
- attachment: toDocIds(item.attachment),
- })),
- // 社保公积金:是否启用 0 否 / 1 是(接口可能返回字符串,统一成数字)
- socialSecurityRecords: (source.socialSecurityRecords || []).map((item) => ({
- ...item,
- isEnabled: Number(item.isEnabled) === 1 ? 1 : 0,
- })),
- workTypes: (source.workTypes || []).map((item) => this.mapProfessionToWorkType(item)),
- attendanceRecords: source.attendanceRecords || [], performanceRecords: source.performanceRecords || [],
- trainingRecords: (source.trainingRecords || []).map((item) => ({ ...item, attachment: toDocIds(item.attachment) })),
- rewardPunishmentRecords: (source.rewardPunishmentRecords || []).map((item) => ({ ...item, attachment: toDocIds(item.attachment) })), awards: source.awards || [], positionTransfers: source.positionTransfers || [], contractChanges: source.contractChanges || []
- };
- this.loadEmploymentTypeOptions();
- this.loadSequences()
- .then(() => this.loadPositionSequences())
- .then(() => this.resolveSequenceForEdit())
- .then(() => (this.form.positionSequenceId ? this.loadDepartmentPositions() : null))
- .then(() => (this.form.positionId ? this.loadPositionNatureAndLevels({ prime: true }) : null))
- .then(() => {
- this.remapLegacyPositionIds();
- this.mergeBoundPositionOptions();
- // 部门岗位接口已给出层级下拉时,不再用岗位自带层级覆盖
- if (!this.staffingLevelOptions.length) this.syncStaffingLevelsFromPosition();
- });
- /* 直属上级与部门/机构解耦(全员搜索):表单填充后预拉候选,
- 确保**新增**时点开下拉立刻有选项(下拉 focus 时还有 ensureManagerOptions 兜一次)。
- 编辑态下拉是 disabled(模板 :disabled="isEdit"),候选没有任何用处;
- 回显姓名走 syncManagerField 内部「候选 → 按 id 单查(loadEmployeeBriefs)」的路径即可,
- 不再为一个点不开的下拉白拉一次全员列表(2026-10-28)。 */
- if (this.isEdit) {
- this.syncManagerField();
- } else {
- this.searchManagers('').then(() => this.syncManagerField());
- }
- this.loadOrganizations().then(() => {
- /* 新增员工默认带出所属机构(登录机构列表第一个) */
- this.applyDefaultLegalEntity();
- this.normalizeDeptAgainstOptions();
- /* 工厂与机构级联(2026-10-29 用户要求):等机构(含新增默认带出)就绪后再按机构拉工厂 */
- this.loadFactories().then(() => this.syncFactoryDisplay());
- });
- this.syncAge();
- if (!this.employee) this.generateEmployeeNo();
- this.$nextTick(() => {
- this.$refs.form?.clearValidate();
- this.$nextTick(() => {
- this._skipFormValidate = false;
- });
- });
- },
- normalizePositionIds(source = {}) {
- let ids = [];
- if (Array.isArray(source.positionBindings) && source.positionBindings.length) {
- // 主岗优先,避免回显顺序错乱
- ids = [...source.positionBindings]
- .sort((a, b) => Number(b.primaryFlag || 0) - Number(a.primaryFlag || 0))
- .map((item) => (item.positionId != null ? String(item.positionId) : ''))
- .filter(Boolean);
- } else if (Array.isArray(source.positionIds) && source.positionIds.length) {
- ids = source.positionIds.map(String);
- } else if (source.positionId != null && source.positionId !== '') {
- ids = [String(source.positionId)];
- } else if (source.postId != null && source.postId !== '') {
- ids = [String(source.postId)];
- }
- // 兼容 "id1,id2" 整串
- return Array.from(
- new Set(
- ids
- .flatMap((id) => String(id).split(/[,,]/))
- .map((item) => item.trim())
- .filter(Boolean),
- ),
- );
- },
- positionFallbackNames() {
- return String(this.form.position || '')
- .split(/[,,、]/)
- .map((item) => item.trim())
- .filter(Boolean);
- },
- positionOptionLabel(item) {
- return item.positionName || item.name || (item.id ? `岗位 ${item.id}` : '未命名岗位');
- },
- dedupePositionOptions(list = []) {
- const map = new Map();
- list.forEach((item) => {
- if (item == null || item.id == null || item.id === '') return;
- const id = String(item.id);
- if (!map.has(id)) {
- map.set(id, { ...item, id });
- return;
- }
- // 后写入的若带名称则补全
- const prev = map.get(id);
- map.set(id, {
- ...prev,
- ...item,
- id,
- positionName: item.positionName || item.name || prev.positionName || prev.name,
- name: item.name || item.positionName || prev.name || prev.positionName
- });
- });
- return Array.from(map.values());
- },
- mergeBoundPositionOptions() {
- const fallbackNames = this.positionFallbackNames();
- const bindings = this.form.positionBindings || [];
- const selectedIds = (this.form.positionIds || []).map(String).filter(Boolean);
- const extras = [];
- const pushExtra = (positionId, positionName, binding = {}) => {
- if (!positionId) return;
- extras.push({
- id: String(positionId),
- positionName: positionName || binding.positionName || '',
- name: positionName || binding.positionName || '',
- positionSequence: binding.positionSequenceName || '',
- positionSequenceName: binding.positionSequenceName || '',
- positionSequenceId: binding.positionSequenceId || '',
- level: binding.positionLevelName || '',
- positionLevel: binding.positionLevelName || '',
- positionLevelId: binding.positionLevelId || '',
- sequenceId: binding.positionSequenceId || '',
- });
- };
- selectedIds.forEach((id, index) => {
- const binding = bindings.find((item) => String(item.positionId) === id) || {};
- const name =
- binding.positionName ||
- fallbackNames[index] ||
- '';
- pushExtra(id, name, binding);
- });
- bindings.forEach((item, index) => {
- if (item.positionId == null || item.positionId === '') return;
- if (selectedIds.includes(String(item.positionId))) return;
- pushExtra(
- item.positionId,
- item.positionName || fallbackNames[index] || '',
- item,
- );
- });
- // 已选岗位放到前面,保证 el-select 标签优先匹配到名称
- this.positionOptions = this.dedupePositionOptions([
- ...extras,
- ...(this.positionOptions || []),
- ]);
- this.syncPositionDisplay();
- this.refreshPositionSelectEcho();
- },
- /**
- * 主数据短 postId(如 1,4)按名称映射;长整型视为 HR 岗位主键,缺失时保留并补名称,禁止误删导致回显成裸 ID
- */
- remapLegacyPositionIds() {
- const ids = (this.form.positionIds || []).map(String).filter(Boolean);
- if (!ids.length) return;
- const fallbackNames = this.positionFallbackNames();
- const hrOptions = (this.positionOptions || []).filter(
- (item) => item && this.hrPositionIdSet[String(item.id)],
- );
- const findHrByName = (name) => {
- const text = String(name || '').trim();
- if (!text) return null;
- return (
- hrOptions.find((item) => {
- const label = String(item.positionName || item.name || '').trim();
- return label === text;
- }) || null
- );
- };
- const isLikelyHrPositionId = (id) => /^\d{15,}$/.test(String(id).trim());
- const nextIds = [];
- const nextBindings = [];
- let changed = false;
- ids.forEach((key, index) => {
- const binding =
- (this.form.positionBindings || []).find((item) => String(item.positionId) === key) || {};
- const opt = this.positionOptions.find((item) => String(item.id) === key) || {};
- let name =
- binding.positionName ||
- opt.positionName ||
- opt.name ||
- fallbackNames[index] ||
- '';
- if (this.hrPositionIdSet[key] || isLikelyHrPositionId(key)) {
- if (!name && fallbackNames[index]) {
- name = fallbackNames[index];
- changed = true;
- }
- nextIds.push(key);
- nextBindings.push({
- ...binding,
- positionId: key,
- positionName: name || binding.positionName || '',
- primaryFlag: nextIds.length === 1 ? 1 : 0,
- });
- return;
- }
- const matched = findHrByName(name);
- if (matched) {
- const realId = String(matched.id);
- if (!nextIds.includes(realId)) {
- nextIds.push(realId);
- nextBindings.push({
- ...binding,
- positionId: realId,
- positionName: matched.positionName || matched.name || name,
- primaryFlag: nextIds.length === 1 ? 1 : 0,
- });
- }
- changed = true;
- return;
- }
- // 仅丢弃无法识别的短旧 postId
- changed = true;
- });
- if (!changed && nextIds.join(',') === ids.join(',')) {
- // 名称可能仍需回填
- const needNameFill = nextBindings.some((item, index) => !item.positionName && fallbackNames[index]);
- if (!needNameFill) return;
- }
- this.form.positionIds = nextIds.slice(0, 1);
- this.form.positionBindings = nextBindings.slice(0, 1).map((item) => ({
- ...item,
- positionName: item.positionName || fallbackNames[0] || '',
- primaryFlag: 1,
- }));
- this.form.positionId = this.form.positionIds[0] || null;
- this.form.position =
- this.form.positionBindings[0]?.positionName || this.form.position;
- this.syncPositionDisplay();
- },
- /** Element 多选在 options 异步到达后需重建一次,否则标签可能只剩裸 ID */
- refreshPositionSelectEcho() {
- const id = this.form.positionId != null && this.form.positionId !== ''
- ? String(this.form.positionId)
- : (this.form.positionIds || []).map(String).filter(Boolean)[0] || null;
- this.form.positionId = id;
- this.form.positionIds = id ? [id] : [];
- this.positionSelectKey += 1;
- this.$nextTick(() => {
- this.syncPositionDisplay();
- this.$refs.form?.clearValidate('positionId');
- });
- },
- syncPositionDisplay() {
- const fallbackNames = this.positionFallbackNames();
- const key = this.form.positionId != null && this.form.positionId !== ''
- ? String(this.form.positionId)
- : '';
- const selected = key
- ? (() => {
- const opt = this.positionOptions.find((item) => String(item.id) === key) || {};
- const binding =
- (this.form.positionBindings || []).find((item) => String(item.positionId) === key) || {};
- const meta = this.resolvePositionLevelMeta(opt, {
- ...binding,
- positionLevelName: this.form.staffingLevel || binding.positionLevelName,
- });
- return [{
- positionId: key,
- positionName:
- opt.positionName ||
- opt.name ||
- binding.positionName ||
- fallbackNames[0] ||
- '',
- positionLevelId: meta.positionLevelId,
- positionLevelName: meta.positionLevelName,
- positionSequenceId: meta.positionSequenceId,
- positionSequenceName: meta.positionSequenceName,
- primaryFlag: 1,
- }];
- })()
- : [];
- this.form.positionBindings = selected;
- this.form.positionIds = key ? [key] : [];
- this.form.position = selected[0]?.positionName || '';
- this.form.positionId = selected[0]?.positionId || null;
- this.form.level = selected[0]?.positionLevelName || this.form.level;
- if (selected[0]?.positionLevelName) {
- this.form.staffingLevel = selected[0].positionLevelName;
- }
- },
- isNumericId(value) {
- return value != null && value !== '' && /^-?\d+$/.test(String(value).trim());
- },
- findSequenceMeta(sequenceId = '', sequenceName = '') {
- const map = this.sequenceLevelMap || {};
- const name = String(sequenceName || '').trim();
- if (name && map[name]) return map[name];
- const sid = this.isNumericId(sequenceId) ? String(sequenceId).trim() : '';
- if (sid && map[`id:${sid}`]) return map[`id:${sid}`];
- if (sid) {
- return Object.values(map).find((item) => String(item.sequenceId) === sid) || null;
- }
- if (name) {
- return (
- Object.values(map).find(
- (item) => String(item.sequenceName || '').trim() === name,
- ) || null
- );
- }
- return null;
- },
- /**
- * 人员岗位关联必填:positionLevelId / positionSequenceId
- * 岗位分页通常只有序列名与层级文案,需用岗位序列接口映射主键
- */
- resolvePositionLevelMeta(opt = {}, binding = {}) {
- let positionSequenceId = binding.positionSequenceId || opt.sequenceId || opt.positionSequenceId || '';
- let positionSequenceName = String(
- binding.positionSequenceName || opt.positionSequence || opt.positionSequenceName || '',
- ).trim();
- let positionLevelId = binding.positionLevelId || opt.positionLevelId || '';
- let positionLevelName =
- binding.positionLevelName || opt.positionLevel || opt.level || '';
- // 部门岗位接口带回的「层级名 → 主键」优先,避免只认序列层级表
- if (!this.isNumericId(positionLevelId) && positionLevelName) {
- const mapped = this.levelIdByName[String(positionLevelName).trim()];
- if (this.isNumericId(mapped)) positionLevelId = String(mapped).trim();
- }
- const levelNames = [];
- const pushName = (value) => {
- const text = String(value || '').trim();
- if (text && !levelNames.includes(text)) levelNames.push(text);
- };
- (opt.positionLevels || []).forEach(pushName);
- pushName(positionLevelName);
- pushName(opt.positionLevel);
- pushName(opt.level);
- const seqMeta = this.findSequenceMeta(positionSequenceId, positionSequenceName);
- if (seqMeta) {
- if (!positionSequenceName) positionSequenceName = seqMeta.sequenceName || '';
- if (!this.isNumericId(positionSequenceId) && this.isNumericId(seqMeta.sequenceId)) {
- positionSequenceId = String(seqMeta.sequenceId).trim();
- }
- }
- const levels = seqMeta?.levels || [];
- if (!this.isNumericId(positionLevelId) && levels.length) {
- const matched =
- levels.find((item) =>
- levelNames.some(
- (name) =>
- name === item.label ||
- name === item.value ||
- String(item.id) === name,
- ),
- ) || levels[0];
- if (matched && this.isNumericId(matched.id)) {
- positionLevelId = String(matched.id).trim();
- positionLevelName = matched.label || matched.value || positionLevelName;
- }
- } else if (this.isNumericId(positionLevelId) && !positionLevelName && levels.length) {
- const matched = levels.find((item) => String(item.id) === String(positionLevelId));
- if (matched) positionLevelName = matched.label || matched.value || '';
- }
- return {
- positionSequenceId: this.isNumericId(positionSequenceId) ? String(positionSequenceId).trim() : '',
- positionSequenceName: positionSequenceName || '',
- positionLevelId: this.isNumericId(positionLevelId) ? String(positionLevelId).trim() : '',
- positionLevelName: positionLevelName || '',
- };
- },
- /** 写入 sequenceLevelMap:按序列名 + id:主键 双索引 */
- putSequenceMeta(map, item = {}) {
- const name = String(item.sequence || item.label || item.name || '').trim();
- const sequenceId = this.isNumericId(item.id) ? String(item.id).trim() : '';
- if (!name && !sequenceId) return;
- const levels = (item.levelList || item.children || [])
- .map((level) => {
- const id = this.isNumericId(level.id) ? String(level.id).trim() : '';
- const label = level.label || level.value || level.level || level.positionLevel || '';
- return { id, label, value: label };
- })
- .filter((level) => level.id);
- const entry = { sequenceId, sequenceName: name, levels };
- if (name) {
- const prev = map[name];
- map[name] = {
- ...(prev || {}),
- ...entry,
- levels: levels.length ? levels : (prev?.levels || []),
- sequenceId: sequenceId || prev?.sequenceId || '',
- };
- }
- if (sequenceId) map[`id:${sequenceId}`] = entry;
- },
- async loadSequences() {
- const map = {};
- try {
- // 与岗位档案编辑一致:page 接口 levelList 带层级主键
- const page = await getPositionSequencePage({ pageNum: 1, size: 1000, status: '1' });
- (page?.list || []).forEach((item) => this.putSequenceMeta(map, item));
- } catch (error) {
- // page 失败时再走列表接口
- }
- if (!Object.keys(map).length) {
- try {
- const list = await getPositionSequenceList();
- (Array.isArray(list) ? list : []).forEach((item) => this.putSequenceMeta(map, item));
- } catch (error) {
- // ignore
- }
- }
- this.sequenceLevelMap = map;
- },
- enrichSelectedPositions() {
- this.remapLegacyPositionIds();
- this.mergeBoundPositionOptions();
- },
- /** 保存前确保每岗都有序列/层级主键:本地映射 → 缺则拉 getById 再映射 */
- async ensurePositionMetaForSave() {
- this.enrichSelectedPositions();
- const missing = (this.form.positionBindings || []).filter(
- (item) => !item.positionLevelId || !item.positionSequenceId,
- );
- if (!missing.length) return true;
- await Promise.all(
- missing.map(async (item) => {
- const key = String(item.positionId || '');
- if (!key) return;
- try {
- const detail = await Promise.race([
- getPositionById(key),
- new Promise((_, reject) =>
- setTimeout(() => reject(new Error('岗位详情超时')), 8000),
- ),
- ]);
- if (!detail) return;
- // 若序列层级表里还没有该序列,按序列名补拉层级
- const seqName = detail.positionSequence || '';
- if (seqName && !this.findSequenceMeta('', seqName)?.levels?.length) {
- try {
- const levels = await getPositionSequenceLevels(seqName);
- const seqMeta = this.findSequenceMeta('', seqName);
- const sequenceId = seqMeta?.sequenceId || '';
- const mapped = (Array.isArray(levels) ? levels : []).map((level) => ({
- id: level.id != null ? String(level.id) : '',
- label: level.level || level.levelName || level.label || '',
- value: level.level || level.levelName || level.value || '',
- })).filter((level) => this.isNumericId(level.id));
- if (seqName) {
- this.$set(this.sequenceLevelMap, seqName, {
- sequenceId,
- sequenceName: seqName,
- levels: mapped,
- });
- }
- } catch (error) {
- // ignore
- }
- }
- const merged = {
- id: key,
- positionName: detail.positionName || detail.name || item.positionName || '',
- name: detail.name || detail.positionName || item.positionName || '',
- positionSequence: detail.positionSequence || '',
- positionSequenceName: detail.positionSequence || '',
- sequenceId: detail.sequenceId != null ? String(detail.sequenceId) : '',
- positionSequenceId: detail.sequenceId != null ? String(detail.sequenceId) : '',
- positionLevel: detail.positionLevel || '',
- positionLevels: detail.positionLevels || [],
- };
- // 用序列名反查序列主键
- if (!merged.sequenceId && merged.positionSequence) {
- const seqMeta = this.findSequenceMeta('', merged.positionSequence);
- if (seqMeta?.sequenceId) {
- merged.sequenceId = seqMeta.sequenceId;
- merged.positionSequenceId = seqMeta.sequenceId;
- }
- }
- this.positionOptions = this.dedupePositionOptions([
- merged,
- ...(this.positionOptions || []),
- ]);
- } catch (error) {
- // 单岗失败不阻断其余岗
- }
- }),
- );
- this.syncPositionDisplay();
- const stillMissing = (this.form.positionBindings || []).find(
- (item) => !item.positionLevelId || !item.positionSequenceId,
- );
- if (stillMissing) {
- this.activeTab = 'base';
- this.$message.warning(
- `岗位「${stillMissing.positionName || stillMissing.positionId}」缺少岗位序列或层级配置,请在岗位档案中维护后再保存`,
- );
- return false;
- }
- return true;
- },
- /** 强制关闭:复位 loading,并清掉可能残留的 Element 遮罩 */
- closeDialog() {
- this.saving = false;
- this.dialogVisible = false;
- this.clearOrphanOverlays();
- },
- handleBeforeClose(done) {
- this.saving = false;
- if (typeof done === 'function') done();
- this.$nextTick(() => this.clearOrphanOverlays());
- },
- clearOrphanOverlays() {
- this.$nextTick(() => {
- try {
- const openDialog = Array.from(document.querySelectorAll('.el-dialog__wrapper')).some((el) => {
- if (!el || el.style.display === 'none') return false;
- const style = window.getComputedStyle(el);
- return style.display !== 'none' && style.visibility !== 'hidden';
- });
- if (openDialog) return;
- document.querySelectorAll('body > .v-modal').forEach((el) => el.remove());
- document.body.classList.remove('el-popup-parent--hidden');
- if (document.body.style.overflow === 'hidden') {
- document.body.style.removeProperty('overflow');
- }
- } catch (error) {
- // ignore
- }
- });
- },
- toggleFullscreen() {
- this.fullscreen = !this.fullscreen;
- },
- resetForm() {
- this._skipFormValidate = true;
- this.saving = false;
- this.fullscreen = false;
- // 先抬高 key,避免空 options 触发 Cascader 内部 null.level
- this.deptCascaderKey += 1;
- this.positionSelectKey += 1;
- this.activeTab = 'base';
- this.managerOptions = [];
- this.managerLoading = false;
- this.factoryOptions = [];
- this.factoryLoading = false;
- this.staffingLevelOptions = [];
- this.positionOptions = [];
- this.sequenceOptions = [];
- this.sequenceLoading = false;
- this.levelIdByName = {};
- this.showWorkTypeSelect = false;
- this._managerRequestKey = '';
- this.$nextTick(() => {
- this.form = emptyForm();
- this.$refs.form?.clearValidate();
- this.clearOrphanOverlays();
- });
- },
- generateEmployeeNo() { const maxNo = this.existingNumbers.reduce((max, item) => Math.max(max, Number(String(item).match(/\d+/)?.[0] || 0)), 0); this.form.employeeNo = `ZY${String(maxNo + 1).padStart(4, '0')}`; this.$nextTick(() => this.$refs.form?.clearValidate('employeeNo')); },
- normalizeDeptIds(value) {
- const list = Array.isArray(value) ? value : value != null && value !== '' ? [value] : [];
- return Array.from(
- new Set(
- list
- .flat(2)
- .map((id) => String(id == null ? '' : id).trim())
- .filter((id) => id && id !== 'null' && id !== 'undefined'),
- ),
- );
- },
- findDeptNode(id, nodes = this.deptOptions) {
- for (const node of nodes || []) {
- if (!node) continue;
- if (String(node.id) === String(id)) return node;
- const matched = this.findDeptNode(id, node.children || []);
- if (matched) return matched;
- }
- return null;
- },
- /* 2026-10-29 岗位链路级联来源改为主所属部门,隶属部门变化不再影响岗位链路,
- 因此不再有 resetDependents 重置逻辑 */
- applyDeptIds(ids) {
- const list = this.normalizeDeptIds(ids).filter(
- (id) => String(id) !== String(this.form.groupId || ''),
- );
- this.form.deptIds = list;
- this.form.deptId = list[0] || null;
- /* ★ 2026-10-28 编辑态不重写 deptName:deptName 在详情接口里是「主所属部门」,
- 这里重写成隶属部门拼接串会(a)让主所属部门回显成隶属部门(b)保存时把后端的主部门名覆盖掉。
- 新增态沿用原来「隶属部门名拼接」的提交口径(saveNew 只认 deptName 一个字符串)。 */
- if (!this.isEdit) {
- this.form.deptName = list
- .map((id) => this.findDeptNode(id)?.name)
- .filter(Boolean)
- .join('、');
- }
- },
- /** 新增员工默认带出所属机构:登录机构列表第一个(命中法人组织选项时才填充) */
- applyDefaultLegalEntity() {
- if (this.isEdit || (this.form.groupId != null && this.form.groupId !== '')) return;
- const list = this.legalEntities || [];
- if (!list.length) return;
- const wantId = getCurrentOrgId();
- const hit =
- (wantId != null && wantId !== ''
- ? list.find((item) => String(item.id) === String(wantId))
- : null) || list[0];
- if (!hit) return;
- this.form.groupId = String(hit.id);
- },
- handleOrganizationChange(ids) {
- this.applyDeptIds(ids);
- this.$nextTick(() => this.$refs.form?.validateField('deptIds'));
- },
- /**
- * Element 多选级联会同时渲染两个占位符:外层 el-input__inner 与内层 el-cascader__search-input,
- * 两层叠在一起就是用户看到的“好几层重影”。仅靠 CSS 伪元素隐藏外层在部分主题/热更新下不生效,
- * 这里直接改 DOM 属性兜底:已选机构(启用)时清空外层占位符,占位符只由内层搜索框渲染;
- * 未选机构(禁用)时内层不渲染,恢复外层提示文案。
- */
- syncDeptCascaderPlaceholder() {
- const input = this.$refs.deptCascader?.$el?.querySelector?.('.el-input__inner');
- if (!input) return;
- const want = this.form.groupId ? '' : this.deptCascaderPlaceholder;
- if ((input.getAttribute('placeholder') || '') !== want) {
- input.setAttribute('placeholder', want);
- }
- },
- /* 所属机构切换:重置依赖机构的部门 / 岗位级联,并按新机构重拉工厂(清空已选工厂);
- 直属上级已改为全员搜索,与机构/部门解耦,保持已选直属上级不动 */
- handleLegalEntityChange() {
- this.deptCascaderKey += 1;
- /* 机构变了,机构下的部门树整棵换掉 → 已选主所属部门 / 隶属部门都失效,一并清空 */
- this.form.primaryDeptId = '';
- this.applyDeptIds([]);
- this.resetPositionsByDepartment();
- /* 工厂与机构级联(2026-10-29 用户要求):换机构后原工厂不再属于新机构,清空并按新机构重拉 */
- this.form.factoryId = null;
- this.form.factory = '';
- this.loadFactories();
- },
- /* 主所属部门选中:把该部门从隶属部门里剔除(两者不允许重复,与参考项目 checkedKeys 一致);
- 岗位链路(序列/岗位/性质/层级)级联来源 = 主所属部门 → 主部门变了整段重置重拉 */
- async handlePrimaryDeptChange(id) {
- const picked = id == null || id === '' ? '' : String(id);
- if (!picked) return;
- const current = this.normalizeDeptIds(this.form.deptIds);
- const rest = current.filter((item) => String(item) !== picked);
- if (rest.length !== current.length) {
- this.applyDeptIds(rest);
- }
- await this.resetPositionsByDepartment();
- },
- /* 在组织树里按 id 找节点(主所属部门要拿机构节点做置灰根节点) */
- findOrgNodeById(id, nodes = this.organizationTree.length ? this.organizationTree : this.organizations) {
- for (const node of nodes || []) {
- if (!node) continue;
- if (String(node.id) === String(id)) return node;
- const matched = this.findOrgNodeById(id, node.children || []);
- if (matched) return matched;
- }
- return null;
- },
- normalizeDeptAgainstOptions() {
- const ids = this.normalizeDeptIds(
- this.form.deptIds?.length ? this.form.deptIds : this.form.deptId,
- ).filter((id) => String(id) !== String(this.form.groupId || ''));
- const valid = ids.filter((id) => this.findDeptNode(id));
- this.applyDeptIds(valid.length ? valid : ids);
- },
- /** 直属上级下拉选项文案:名字(工号),无工号时只显示名字(姓名必有,不编占位文案) */
- managerOptionLabel(item = {}) {
- const name = item.name || '';
- const jobNumber = item.jobNumber || item.employeeNo || '';
- return jobNumber ? `${name}(${jobNumber})` : name;
- },
- handleManagerChange(id) {
- const matched = this.managerOptions.find((item) => String(item.id) === String(id));
- this.form.managerId = id != null && id !== '' ? String(id) : null;
- this.form.manager = matched?.name || '';
- },
- /**
- * 把「已选直属上级」补进下拉候选,保证 el-select 能显示「名字(工号)」而不是裸 id。
- * ★ 只有已知真实姓名时才注入——查不到姓名不再编「未命名人员」占位(见 syncManagerField)。
- */
- mergeCurrentManagerOption() {
- const id = this.form.managerId;
- if (id == null || id === '') return;
- if (!this.form.manager) return;
- const exists = this.managerOptions.some((item) => String(item.id) === String(id));
- if (exists) return;
- this.managerOptions.unshift({
- id: String(id),
- name: this.form.manager,
- jobNumber: ''
- });
- },
- /**
- * 直属上级回显:接口只给 managerId、不给 managerUserName 时(常见于该上级用户没挂员工主档),
- * ① 下拉候选里已有 → 直接用候选姓名;
- * ② 按 id 批量反查员工简要信息(POST /main/user/batch)→ 用真名并补进候选;
- * ③ 两者都取不到(该上级已不存在 / 无员工主档)→ <b>清空该字段</b>,不显示任何占位文案。
- * 反查接口报错时保持原值不动(只提示),避免把有效引用误清成空。
- */
- async syncManagerField() {
- const id = this.form.managerId;
- if (id == null || id === '') return;
- const hit = this.managerOptions.find((item) => String(item.id) === String(id));
- if (hit && hit.name) {
- this.form.manager = hit.name;
- return;
- }
- try {
- const briefs = await loadEmployeeBriefs([id]);
- const brief = briefs.get(String(id));
- if (brief && brief.name) {
- this.form.manager = brief.name;
- this.managerOptions.unshift({
- id: String(id),
- name: brief.name,
- jobNumber: brief.employeeNo || ''
- });
- return;
- }
- /* 查不到该上级:按「无直属上级」处理,清空字段(后端清空能力缺失已在需求清单中) */
- this.form.managerId = null;
- this.form.manager = '';
- } catch (error) {
- showRequestError(error, '直属上级姓名加载失败');
- }
- },
- /**
- * 直属上级:远程搜索全员(姓名/工号/手机号,searchUsersByKeyword 内部并行检索),
- * 不再按隶属部门过滤——新员工尚未落部门或部门下无人时也能直接选人;
- * 登录人组织范围由 getUserPage 自身权限控制,isQueryLZ=0 不带离职员工。
- */
- async searchManagers(keyword) {
- const requestKey = String(keyword || '');
- this._managerRequestKey = requestKey;
- this.managerLoading = true;
- try {
- const data = await searchUsersByKeyword(keyword || '', {
- pageNum: 1,
- size: 30,
- isQueryLZ: 0
- });
- if (this._managerRequestKey !== requestKey) return;
- const selfId = this.form.id != null ? String(this.form.id) : '';
- this.managerOptions = (data?.list || [])
- .map((item) => ({
- id: item.id == null ? item.id : String(item.id),
- name: item.name || '',
- jobNumber: item.jobNumber || item.employeeNo || ''
- }))
- .filter((item) => item.id != null && item.id !== '' && item.name && String(item.id) !== selfId);
- this.mergeCurrentManagerOption();
- } catch (error) {
- if (this._managerRequestKey !== requestKey) return;
- this.managerOptions = [];
- this.mergeCurrentManagerOption();
- } finally {
- if (this._managerRequestKey === requestKey) {
- this.managerLoading = false;
- }
- }
- },
- /** 首次点开下拉时候选还是空的,补拉一次默认列表 */
- ensureManagerOptions() {
- if (!this.managerOptions.length && !this.managerLoading) {
- this.searchManagers('');
- }
- },
- async loadOrganizations() {
- try {
- const list = await listOrganizations();
- this.organizationTree = buildOrganizationTree(Array.isArray(list) ? list : []);
- } catch (error) {
- this.organizationTree = [];
- }
- },
- /**
- * 所属工厂:取工厂列表接口 /main/factoryarea/page(type=1 工厂)。
- * ★ 2026-10-29 用户要求:工厂与所属机构级联——只能选当前机构下的工厂。
- * 服务端按 groupId 过滤(swagger /main/factoryarea/page 支持 groupId 参数);
- * 未选机构时不发请求,下拉为空。
- */
- async loadFactories() {
- const groupId = this.form.groupId;
- if (groupId == null || groupId === '') {
- this.factoryOptions = [];
- return;
- }
- this.factoryLoading = true;
- try {
- const data = await getFactoryAreaPage({ groupId });
- const list = Array.isArray(data?.list) ? data.list : Array.isArray(data) ? data : [];
- this.factoryOptions = list
- .map((item) => ({
- id: String(item.id),
- name: item.name || item.factoryName || item.pathName || `工厂 ${item.id}`,
- code: item.code || '',
- enabled: item.enabled,
- /* 行自带 groupId 时用于客户端复核(防止服务端过滤参数失效);没带就不过滤 */
- groupId: item.groupId == null ? null : String(item.groupId),
- }))
- .filter((item) => item.id && item.name)
- .filter((item) => item.groupId == null || item.groupId === String(groupId));
- } catch (error) {
- this.factoryOptions = [];
- this.$message.error(error.message || '工厂列表加载失败');
- } finally {
- this.factoryLoading = false;
- }
- },
- syncFactoryDisplay() {
- if (this.form.factoryId == null || this.form.factoryId === '') {
- this.form.factoryId = null;
- return;
- }
- this.form.factoryId = String(this.form.factoryId);
- const matched = this.factoryOptions.find((item) => String(item.id) === String(this.form.factoryId));
- if (matched) {
- this.form.factory = matched.name;
- return;
- }
- // 详情带回的工厂不在分页结果里时,补一条选项避免只显示 ID
- if (this.form.factory) {
- this.factoryOptions.unshift({
- id: String(this.form.factoryId),
- name: this.form.factory,
- code: '',
- enabled: 1
- });
- }
- },
- /* 工厂与隶属部门相互独立(都只挂所属机构):换工厂只更新工厂本身,不动部门与岗位级联 */
- handleFactoryChange(id) {
- const matched = this.factoryOptions.find((item) => String(item.id) === String(id));
- this.form.factory = matched?.name || '';
- this.form.factoryId = id != null && id !== '' ? String(id) : null;
- },
- async handlePositionChange() {
- this.form.positionId =
- this.form.positionId != null && this.form.positionId !== ''
- ? String(this.form.positionId)
- : null;
- this.form.positionIds = this.form.positionId ? [this.form.positionId] : [];
- if (!this.form.positionId) {
- this.levelIdByName = {};
- this.staffingLevelOptions = [];
- this.form.staffingLevel = '';
- this.form.level = '';
- } else {
- // 选完岗位自动带出岗位性质与层级(仍可手动修改)
- await this.loadPositionNatureAndLevels();
- if (!this.staffingLevelOptions.length) this.syncStaffingLevelsFromPosition();
- }
- this.syncPositionDisplay();
- this.enrichSelectedPositions();
- this.$nextTick(() => this.$refs.form?.validateField('positionId'));
- },
- handleAvatarUpload(file) { if (file.size > 2 * 1024 * 1024) { this.$message.warning('员工照片不能超过 2MB'); return false; } const reader = new FileReader(); reader.onload = (event) => { this.form.avatar = event.target.result; }; reader.readAsDataURL(file); return false; },
- addEducation() { this.form.educations.push({ education: '本科', degree: '学士', graduationDate: '', school: '', major: '' }); },
- addBankAccount() { this.form.bankAccounts.push({ bankName: '', bankBranch: '', bankAccount: '', bankAccountName: '' }); },
- addPreviousJob() { this.form.previousJobs.push({ startDate: '', endDate: '', company: '', position: '', department: '', leaveReason: '', description: '' }); },
- addSocialSecurity() { this.form.socialSecurityRecords.push({ type: '五险一金', base: 0, insuredDate: '', stopDate: '', stopReason: '', isEnabled: 0 }); },
- /** 社保公积金只能有一条启用:启用当前行时,其余行自动置为「否」 */
- handleSocialEnabledChange(currentRow) {
- if (currentRow.isEnabled !== 1) return;
- this.form.socialSecurityRecords.forEach((row) => {
- if (row !== currentRow) row.isEnabled = 0;
- });
- },
- /**
- * 社保公积金删除拦截:
- * 1. 已保存的记录(接口回显带 id)不允许删除,提示通过「停缴日期 + 停缴记录」处理;
- * 2. 本次新增、尚未保存的行可以删,但需二次确认。
- */
- handleSocialDelete(index) {
- const row = this.form.socialSecurityRecords[index];
- if (!row) return;
- const saved = row.id != null && row.id !== '';
- if (saved) {
- this.$message.warning('已保存的社保记录不支持删除,如需终止参保请填写「停缴日期」和「停缴记录」');
- return;
- }
- this.$confirm('确认删除该社保记录?', '提示', {
- type: 'warning',
- confirmButtonText: '确定',
- cancelButtonText: '取消'
- })
- .then(() => {
- this.form.socialSecurityRecords.splice(index, 1);
- })
- .catch(() => {});
- },
- workTypeTypeLabel(_row, _column, cellValue) {
- const type = _row?.type ?? cellValue;
- return this.getDictValue('工种类型', type) || type || '-';
- },
- workTypeLevelLabel(_row, _column, cellValue) {
- const level = _row?.level ?? cellValue;
- const hit = this.workTypeLevelOptions.find(
- (item) => String(item.value) === String(level) || item.label === level,
- );
- return hit?.label || level || '-';
- },
- workTypeHourCostLabel(row) {
- const wage = row.hourCost ?? row.hourlyWage;
- return wage || wage === 0 ? wage : '-';
- },
- workTypeUnitLabel(row) {
- return row.unit || row.wageUnit || '-';
- },
- mapProfessionToWorkType(item = {}) {
- const unit = item.unit || (item.money && item.date ? `${item.money}/${item.date}` : '') || item.wageUnit || '';
- return {
- id: item.id,
- type: item.type || '',
- code: item.code || '',
- name: item.name || '',
- level: item.level || '',
- hourCost: item.hourCost ?? item.hourlyWage ?? '',
- unit,
- hourlyWage: item.hourCost ?? item.hourlyWage ?? 0,
- wageUnit: unit,
- userId: item.userId || '',
- userName: item.userName || '',
- };
- },
- openWorkTypeSelect() {
- this.showWorkTypeSelect = true;
- this.$refs.workTypeSelect?.open();
- },
- handleWorkTypeSelect(selection) {
- const existIds = new Set(
- (this.form.workTypes || []).map((item) => String(item.id)).filter((id) => id && id !== 'undefined'),
- );
- (selection || []).forEach((item) => {
- const mapped = this.mapProfessionToWorkType(item);
- const id = mapped.id == null || mapped.id === '' ? '' : String(mapped.id);
- if (id && existIds.has(id)) return;
- this.form.workTypes.push(mapped);
- if (id) existIds.add(id);
- });
- },
- addCertificate() { this.form.certificates.push({ type: '', number: '', level: '', status: '有效', startDate: '', endDate: '', issueDate: '', attachment: [], remark: '', hasPositionAllowance: 0, positionAllowanceAmount: null, positionAllowanceUnit: '' }); },
- /** 等级选项:按证照类型从 certLevel.js 取;表单类型名与字典键差一个「证」字时(特种作业证/特种作业)做兼容 */
- certLevelOptionsFor(type) {
- if (!type) return [];
- const direct = getCertLevelOptions(type);
- if (direct.length) return direct;
- return getCertLevelOptions(type.replace(/证$/, ''));
- },
- /** 等级下拉是否禁用(包装 certLevel.js 的导出函数,供模板调用) */
- isCertLevelDisabled(type) {
- return isCertLevelDisabled(type);
- },
- /** 证照类型变化:无等级类型自动填「无」并禁用;已选等级不在新类型选项里则清空 */
- handleCertTypeChange(row) {
- if (!row.type) { row.level = ''; return; }
- if (NO_LEVEL_CERT_TYPES.includes(row.type)) { row.level = NO_LEVEL_VALUE; return; }
- const opts = this.certLevelOptionsFor(row.type);
- if (!opts.some((opt) => opt.value === row.level)) row.level = '';
- },
- /** 津贴单位默认值:写死选项里优先取 month(元/月),取不到才用第一项 */
- defaultAllowanceUnit(options = this.allowanceUnitOptions) {
- if (!options || !options.length) return '';
- const hit = options.find((item) => String(item.value) === 'month');
- return hit ? hit.value : options[0].value;
- },
- /** 岗位津贴:选「否」时清空金额和单位;选「是」时补上单位默认值(元/月) */
- handlePositionAllowanceChange(row) {
- if (row.hasPositionAllowance !== 1) {
- row.positionAllowanceAmount = null;
- row.positionAllowanceUnit = '';
- return;
- }
- if (!row.positionAllowanceUnit) row.positionAllowanceUnit = this.defaultAllowanceUnit();
- },
- addSkill() { this.form.skills.push({ category: '', name: '', level: '', years: 0, attachment: [], remark: '' }); },
- removeItem(key, index) { this.form[key].splice(index, 1); },
- calculateAge(date) {
- if (!date) return null;
- const birthday = new Date(date);
- if (Number.isNaN(birthday.getTime())) return null;
- const today = new Date();
- let age = today.getFullYear() - birthday.getFullYear();
- if (
- today.getMonth() < birthday.getMonth() ||
- (today.getMonth() === birthday.getMonth() && today.getDate() < birthday.getDate())
- ) {
- age -= 1;
- }
- return age >= 0 ? age : null;
- },
- syncAge() {
- this.form.age = this.calculateAge(this.form.birthDate);
- },
- /** 从 18 位身份证解析出生日期并回填年龄(格式合法时覆盖生成) */
- parseIdCard() {
- const value = String(this.form.idCard || '').trim().toUpperCase();
- this.form.idCard = value;
- if (!/^\d{17}[\dXx]$/.test(value)) return false;
- const y = value.slice(6, 10);
- const m = value.slice(10, 12);
- const d = value.slice(12, 14);
- const birth = `${y}-${m}-${d}`;
- const date = new Date(Number(y), Number(m) - 1, Number(d));
- if (
- date.getFullYear() !== Number(y) ||
- date.getMonth() !== Number(m) - 1 ||
- date.getDate() !== Number(d)
- ) {
- return false;
- }
- this.form.birthDate = birth;
- this.form.age = this.calculateAge(birth);
- return true;
- },
- handleIdCardInput(value) {
- const text = String(value || '').trim();
- if (text.length === 18) this.parseIdCard();
- },
- extractPositionLevelNames(opt = {}) {
- const names = [];
- const add = (value) => {
- if (value == null || value === '') return;
- const text =
- typeof value === 'object'
- ? value.level ||
- value.levelName ||
- value.label ||
- value.positionLevel ||
- value.value ||
- ''
- : value;
- const name = String(text || '').trim();
- if (name && !names.includes(name)) names.push(name);
- };
- (opt.positionLevels || []).forEach(add);
- add(opt.positionLevel);
- add(opt.level);
- return names;
- },
- syncStaffingLevelsFromPosition() {
- const primaryId = this.form.positionId || (this.form.positionIds || [])[0];
- if (!primaryId) {
- this.staffingLevelOptions = [];
- this.form.staffingLevel = '';
- this.form.level = '';
- return;
- }
- const opt =
- this.positionOptions.find((item) => String(item.id) === String(primaryId)) || {};
- const levels = this.extractPositionLevelNames(opt);
- this.staffingLevelOptions = levels;
- if (!levels.length) {
- this.form.staffingLevel = '';
- this.form.level = '';
- return;
- }
- if (!levels.includes(this.form.staffingLevel)) {
- this.form.staffingLevel = levels[0];
- }
- this.form.level = this.form.staffingLevel;
- },
- handleStaffingLevelChange(value) {
- this.form.staffingLevel = value || '';
- this.form.level = value || '';
- this.syncPositionDisplay();
- },
- clearSelectedPositions() {
- this.form.positionIds = [];
- this.form.positionBindings = [];
- this.form.positionId = null;
- this.form.position = '';
- this.form.staffingLevel = '';
- this.form.level = '';
- this.staffingLevelOptions = [];
- this.levelIdByName = {};
- },
- /** 清空「岗位」及以下的级联字段与下拉选项(与办理入职一致) */
- clearPositionCascade() {
- this.clearSelectedPositions();
- this.positionOptions = [];
- this.hrPositionIdSet = {};
- this.positionLoading = false;
- },
- /** 主所属部门(或所属机构)变化:清空岗位序列及以下级联,并按主所属部门重新拉序列 */
- async resetPositionsByDepartment() {
- this.clearPositionCascade();
- this.form.positionSequenceId = '';
- this.form.positionSequence = '';
- this.form.positionSequenceName = '';
- this.sequenceOptions = [];
- this.sequenceLoading = false;
- await this.loadPositionSequences();
- },
- // 后端返回值(字典 id / 历史文案 / 编码)统一转成选项 value(字典 id),便于下拉回显
- resolveEmploymentType(raw) {
- if (raw == null || raw === '') return '';
- const text = String(raw);
- const hit = this.employmentOptions.find(
- (item) =>
- String(item.value) === text ||
- String(item.label) === text ||
- String(item.key) === text,
- );
- return hit ? hit.value : text;
- },
- /** 岗位性质名称快照:按字典 id 反查 label,随 employmentType 一起提交(positionTypeName) */
- syncEmploymentTypeName(value) {
- const hit = this.employmentOptions.find((item) => String(item.value) === String(value));
- this.form.employmentTypeName = hit ? hit.label : '';
- },
- // 岗位性质(员工档案字段 employmentType):字典 id 作为选项 value,提交后端即字典 id
- async loadEmploymentTypeOptions() {
- try {
- const items = await getDictByCode('position_nature');
- this.employmentOptions = (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);
- if (this.form.employmentType) {
- this.form.employmentType = this.resolveEmploymentType(this.form.employmentType);
- this.syncEmploymentTypeName(this.form.employmentType);
- }
- } catch (error) {
- // 岗位性质已改为必填:字典取不到就明确报错,不要静默留空导致下拉无选项、用户不知道原因
- this.employmentOptions = [];
- showRequestError(error, '岗位性质字典加载失败,请刷新重试');
- }
- },
- /** 接口返回可能是数组 / {list|records|rows|data},统一取数组(与办理入职一致) */
- toOptionList(payload) {
- if (Array.isArray(payload)) return payload.filter(Boolean);
- const list =
- payload?.list || payload?.records || payload?.rows || payload?.data;
- return Array.isArray(list) ? list.filter(Boolean) : [];
- },
- /** 岗位序列主键回显:员工主数据字段优先,其次岗位绑定项 */
- pickSequenceId(source = {}) {
- const binding =
- (Array.isArray(source.positionBindings) ? source.positionBindings[0] : null) || {};
- const raw = source.positionSequenceId || binding.positionSequenceId || '';
- return this.isNumericId(raw) ? String(raw).trim() : '';
- },
- /**
- * 主所属部门 → 岗位序列下拉(2026-10-29 用户确认:级联来源从隶属部门改为主所属部门)
- * GET /fm/tmaindepartmentposition/sequence/list
- */
- async loadPositionSequences() {
- const deptId =
- this.form.primaryDeptId == null || this.form.primaryDeptId === ''
- ? ''
- : String(this.form.primaryDeptId);
- if (!deptId) {
- this.sequenceOptions = [];
- this.sequenceLoading = false;
- return;
- }
- this.sequenceLoading = true;
- let requestFailed = false;
- try {
- const data = await getDepartmentPositionSequences(deptId).catch(() => {
- requestFailed = true;
- return [];
- });
- const map = new Map();
- this.toOptionList(data).forEach((item) => {
- if (!item || item.positionSequenceId == null || item.positionSequenceId === '') return;
- const id = String(item.positionSequenceId);
- if (!map.has(id)) map.set(id, { id, name: item.positionSequence || '' });
- });
- const list = Array.from(map.values());
- const currentId = this.form.positionSequenceId ? String(this.form.positionSequenceId) : '';
- if (currentId && !list.some((item) => item.id === currentId)) {
- list.unshift({
- id: currentId,
- name: this.form.positionSequence || this.form.positionSequenceName || '',
- });
- }
- this.sequenceOptions = list;
- if (!list.length && !requestFailed) {
- // 主所属部门下没有维护任何岗位序列(接口异常时不误报):引导去组织机构的岗位信息里维护
- this.warnEmptySequences([deptId]);
- }
- } catch (error) {
- this.sequenceOptions = [];
- } finally {
- this.sequenceLoading = false;
- }
- },
- /** 部门下没有岗位序列时提示:每次选中无岗位部门都弹,不做去重(用户要求) */
- warnEmptySequences(deptIds = []) {
- if (!deptIds.length) return;
- this.$message.warning('该部门下暂无配置岗位信息,请在组织机构的该部门补充岗位信息');
- },
- /** 切换岗位序列:清空岗位及以下的级联字段与选项,再按该序列加载岗位 */
- async handlePositionSequenceChange(id) {
- const hit = this.sequenceOptions.find((item) => String(item.id) === String(id));
- const keep = id != null && id !== '';
- this.clearPositionCascade();
- this.form.positionSequenceId = keep ? String(id) : '';
- this.form.positionSequence = keep ? (hit?.name || '') : '';
- this.form.positionSequenceName = this.form.positionSequence;
- if (keep) await this.loadDepartmentPositions();
- this.$nextTick(() => {
- this.$refs.form?.validateField('positionSequenceId');
- this.$refs.form?.validateField('positionId');
- });
- },
- /**
- * 主所属部门 + 岗位序列 → 岗位下拉(2026-10-29 级联来源改为主所属部门)
- * GET /fm/tmaindepartmentposition/position/list
- */
- async loadDepartmentPositions() {
- const deptId =
- this.form.primaryDeptId == null || this.form.primaryDeptId === ''
- ? ''
- : String(this.form.primaryDeptId);
- const positionSequenceId = this.form.positionSequenceId;
- if (!deptId || !positionSequenceId) {
- this.positionOptions = [];
- this.hrPositionIdSet = {};
- this.positionLoading = false;
- return;
- }
- this.positionLoading = true;
- try {
- const data = await getDepartmentPositions({ deptId, positionSequenceId }).catch(() => []);
- const sequenceName = this.form.positionSequence || this.form.positionSequenceName || '';
- const list = this.toOptionList(data)
- .filter((item) => item && item.positionId != null && String(item.positionName || '').trim())
- .map((item) => {
- const name = String(item.positionName || '').trim();
- return {
- id: String(item.positionId),
- positionId: String(item.positionId),
- positionName: name,
- name,
- deptId,
- sequenceId: String(positionSequenceId),
- positionSequenceId: String(positionSequenceId),
- positionSequence: sequenceName,
- positionLevels: [],
- };
- });
- const idSet = {};
- list.forEach((item) => {
- idSet[item.id] = true;
- });
- this.hrPositionIdSet = idSet;
- this.positionOptions = this.dedupePositionOptions(list);
- } catch (error) {
- this.positionOptions = [];
- this.hrPositionIdSet = {};
- } finally {
- this.positionLoading = false;
- }
- },
- /** 编辑回显:序列主键缺失时按绑定岗位反查补全,并保证当前序列在下拉里可见 */
- resolveSequenceForEdit() {
- const binding = (this.form.positionBindings || [])[0] || {};
- let seqId = this.isNumericId(this.form.positionSequenceId)
- ? String(this.form.positionSequenceId).trim()
- : '';
- let seqName = String(this.form.positionSequence || this.form.positionSequenceName || '').trim();
- if (!seqId && this.isNumericId(binding.positionSequenceId)) {
- seqId = String(binding.positionSequenceId).trim();
- }
- if (!seqName) {
- seqName = String(binding.positionSequenceName || binding.positionSequence || '').trim();
- }
- if (!seqId || !seqName) {
- const meta = this.findSequenceMeta(seqId, seqName);
- if (meta) {
- if (!seqId && this.isNumericId(meta.sequenceId)) seqId = String(meta.sequenceId).trim();
- if (!seqName) seqName = String(meta.sequenceName || '').trim();
- }
- }
- this.form.positionSequenceId = seqId || '';
- this.form.positionSequence = seqName;
- this.form.positionSequenceName = seqName;
- if (seqId && !this.sequenceOptions.some((item) => String(item.id) === seqId)) {
- this.sequenceOptions = [{ id: seqId, name: seqName || '岗位序列' }].concat(this.sequenceOptions);
- }
- return null;
- },
- /**
- * 岗位 → 岗位性质(自动带出默认值,可手改)→ 岗位层级下拉
- * prime=true 用于编辑回显:只预热下拉与主键映射,不覆盖已存值
- */
- async loadPositionNatureAndLevels({ prime = false } = {}) {
- const positionId = this.form.positionId;
- const positionSequenceId = this.form.positionSequenceId;
- if (!positionId || !positionSequenceId) return;
- const opt = this.positionOptions.find((item) => String(item.id) === String(positionId)) || {};
- const deptId = opt.deptId || this.normalizeDeptIds(this.form.deptIds)[0];
- if (!deptId) return;
- let natureText = '';
- try {
- const data = await getDepartmentPositionType({
- deptId,
- positionId,
- positionSequenceId,
- }).catch(() => []);
- const first = this.toOptionList(data)[0];
- if (first) {
- this.form.positionType = first.positionType || this.form.positionType || '';
- natureText = String(first.positionTypeName || first.positionType || '').trim();
- }
- } catch (error) {
- natureText = '';
- }
- if (natureText && (!prime || !this.form.employmentType)) {
- this.form.employmentType = this.resolveEmploymentType(natureText);
- this.syncEmploymentTypeName(this.form.employmentType);
- }
- const names = [];
- const idMap = {};
- try {
- /* positionType(岗位性质编码快照)是该接口必填参数:
- 岗位性质没取到时直接不发请求(否则后端回「岗位性质编码不能为空」),走下方岗位自带层级兜底 */
- const levels = this.form.positionType
- ? await getDepartmentPositionLevels({
- deptId,
- positionId,
- positionSequenceId,
- positionType: this.form.positionType,
- })
- : [];
- this.toOptionList(levels).forEach((item) => {
- const name = String(item.positionLevel || item.positionLevelName || '').trim();
- if (!name) return;
- if (!names.includes(name)) names.push(name);
- if (item.positionLevelId != null && item.positionLevelId !== '') {
- idMap[name] = String(item.positionLevelId);
- }
- });
- } catch (error) {
- // 层级接口异常:明确报错后退回岗位自带层级配置(不静默吞掉)
- showRequestError(error, '岗位层级加载失败');
- }
- if (names.length) {
- this.levelIdByName = idMap;
- this.staffingLevelOptions = names;
- if (names.includes(this.form.staffingLevel)) {
- this.form.level = this.form.staffingLevel;
- } else if (!prime || !this.form.staffingLevel) {
- this.form.staffingLevel = names[0];
- this.form.level = this.form.staffingLevel;
- }
- } else if (!prime) {
- this.levelIdByName = {};
- this.syncStaffingLevelsFromPosition();
- }
- },
- // 保存前整体校验:基础必填 + 各分区必填 + 日期逻辑 + 记录分区,校验不过跳转到对应 tab
- async validateSections() {
- // 1. 基础 el-form(base tab 必填)
- if (!this.$refs.form) {
- this.$message.warning('表单未就绪,请稍后重试');
- return false;
- }
- this._skipFormValidate = false;
- const formValid = await new Promise((resolve) => {
- try {
- this.$refs.form.validate((valid) => resolve(Boolean(valid)));
- } catch (error) {
- resolve(false);
- }
- });
- if (!formValid) {
- this.activeTab = 'base';
- this.$message.warning('请完善基本信息中标记的必填项');
- return false;
- }
- // 同步岗位绑定,并解析 positionList 必填的序列/层级主键(界面仍只展示岗位名)
- const positionMetaOk = await this.ensurePositionMetaForSave();
- if (!positionMetaOk) return false;
- // 2. 各分区重复列表必填/日期逻辑
- const sectionInvalid = this.checkRepeatLists();
- if (sectionInvalid) {
- this.activeTab = sectionInvalid.tab;
- this.$message.warning(sectionInvalid.message);
- return false;
- }
- // 3. 记录分区(考勤/绩效/培训/奖惩)
- const recordsResult = this.$refs.records?.validateRecords?.();
- if (recordsResult && !recordsResult.valid) {
- this.activeTab = 'records';
- this.$message.warning(recordsResult.message);
- return false;
- }
- return true;
- },
- // 校验各分区重复列表必填与日期逻辑,返回首个不满足项 { tab, message } 或 null
- checkRepeatLists() {
- const gt = (start, end) => start && end && start > end;
- // 日期逻辑(基本信息)
- if (gt(this.form.birthDate, this.form.hireDate)) return { tab: 'base', message: '入职日期不能早于出生日期' };
- if (gt(this.form.hireDate, this.form.regularDate)) return { tab: 'base', message: '转正日期不能早于入职日期' };
- // 工作履历
- for (const item of this.form.previousJobs || []) {
- if (!item.startDate || !item.endDate || !item.company || !item.position) {
- return { tab: 'experience', message: '工作履历的必填项未完善(工作开始/结束日期、前公司名称、前公司岗位)' };
- }
- if (gt(item.startDate, item.endDate)) return { tab: 'experience', message: '工作结束日期必须晚于开始日期' };
- }
- // 劳动合同已改为只读,不再在档案页校验必填/日期
- // 薪酬银行:姓名必填
- for (const item of this.form.bankAccounts || []) {
- if (!item.bankAccountName) return { tab: 'payroll', message: '薪酬银行的姓名(开户人)必填' };
- }
- // 社保记录:停保≥参保
- for (const item of this.form.socialSecurityRecords || []) {
- if (gt(item.insuredDate, item.stopDate)) return { tab: 'payroll', message: '社保停保日期不能早于参保日期' };
- }
- // 证照:类型/编号/状态/有效期必填,有效期结束≥开始
- for (const item of this.form.certificates || []) {
- if (!item.type || !item.number || !item.status || !item.startDate || !item.endDate) {
- return { tab: 'qualification', message: '证照的必填项未完善(证照类型、证照编号、状态、有效期起止)' };
- }
- if (gt(item.startDate, item.endDate)) return { tab: 'qualification', message: '证照有效期结束时间不能早于开始时间' };
- if (item.hasPositionAllowance === 1 && (item.positionAllowanceAmount === '' || item.positionAllowanceAmount == null)) {
- return { tab: 'qualification', message: '证照选择了有岗位津贴,请填写津贴金额' };
- }
- if (item.hasPositionAllowance === 1 && !item.positionAllowanceUnit) {
- return { tab: 'qualification', message: '证照选择了有岗位津贴,请选择津贴单位' };
- }
- }
- // 技能:技能名称必填
- for (const item of this.form.skills || []) {
- if (!item.name) return { tab: 'qualification', message: '技能名称必填' };
- }
- return null;
- },
- async submit() {
- if (this.saving) return;
- this.saving = true;
- // 兜底:无论父级是否回调,都要结束 loading,避免异常后整页卡死
- const stopSaving = () => {
- this.saving = false;
- };
- const safetyTimer = setTimeout(stopSaving, 45000);
- try {
- const ok = await this.validateSections();
- if (!ok) return;
- const today = new Date().toISOString().slice(0, 10);
- const certificates = (this.form.certificates || [])
- .filter((item) => item.type)
- .map((item) => ({
- ...item,
- hasPositionAllowance: item.hasPositionAllowance === 1 ? 1 : 0,
- // 无岗位津贴时不给金额,有津贴时按输入值提交
- positionAllowanceAmount: item.hasPositionAllowance === 1 && item.positionAllowanceAmount !== '' && item.positionAllowanceAmount != null
- ? Number(item.positionAllowanceAmount)
- : null,
- // 无岗位津贴时不传单位,字典码统一按字符串提交
- positionAllowanceUnit: item.hasPositionAllowance === 1 && item.positionAllowanceUnit
- ? String(item.positionAllowanceUnit)
- : null,
- status: item.endDate && item.endDate < today ? '失效' : (item.status || '有效'),
- }));
- const contract = { ...(this.form.contract || {}) };
- const leaveEnabled = this.form.leaveInfo?.enabled && Boolean(this.form.leaveInfo?.leaveDate);
- const awards = (this.form.awards || [])
- .filter((item) => item.name)
- .map((item) => ({ ...item, level: item.level || '公司级' }));
- const bankAccounts = (this.form.bankAccounts || []).map((item, i) => ({
- ...item,
- bankBranch: item.bankBranch || '',
- bankAccountName: item.bankAccountName || (i === 0 ? this.form.name : ''),
- }));
- // 社保公积金:isEnabled 只允许一条为 1,历史数据若有多条启用则只保留第一条
- let socialEnabledAssigned = false;
- const socialSecurityRecords = (this.form.socialSecurityRecords || []).map((item) => {
- const enabled = item.isEnabled === 1 && !socialEnabledAssigned;
- if (enabled) socialEnabledAssigned = true;
- return { ...item, isEnabled: enabled ? 1 : 0 };
- });
- if ((this.form.educations || []).length > 1) {
- this.$message.info('多条教育经历仅第一条会写入员工主数据字段');
- }
- const firstEducation = (this.form.educations || [])[0] || {};
- const payload = {
- ...this.form,
- certificates,
- contract,
- awards,
- bankAccounts,
- socialSecurityRecords,
- education: firstEducation.education || this.form.education || '',
- degree: firstEducation.degree || this.form.degree || '',
- school: firstEducation.school || this.form.school || '',
- major: firstEducation.major || this.form.major || '',
- graduationDate: firstEducation.graduationDate || this.form.graduationDate || '',
- // 离职页签一旦启用,状态固定为「离职」;取字典里的离职项值(与下拉同一套值空间)
- status: leaveEnabled ? this.resolveLeaveStatusValue() : this.form.status,
- age: this.calculateAge(this.form.birthDate),
- };
- // done 必须被调用;用回调而不是依赖 resolve/reject,避免漏调导致永久 await
- const saveError = await new Promise((resolve) => {
- let settled = false;
- const done = (error) => {
- if (settled) return;
- settled = true;
- resolve(error || null);
- };
- try {
- this.$emit('save', payload, done);
- } catch (error) {
- done(error);
- }
- setTimeout(() => done(new Error('保存超时,请重试')), 40000);
- });
- if (saveError) {
- if (saveError.message && !saveError.__toastShown) {
- // 父级未提示时再兜底(例如超时)
- if (saveError.message === '保存超时,请重试') {
- this.$message.error(saveError.message);
- }
- }
- return;
- }
- this.dialogVisible = false;
- } catch (error) {
- this.$message.error(error?.message || '保存失败,请重试');
- } finally {
- clearTimeout(safetyTimer);
- stopSaving();
- }
- }
- }
- };
- </script>
- <style src="../../../../styles/views/employeeArchive/components/EmployeeArchiveForm.scss" lang="scss" scoped></style>
- <style src="../../../../styles/views/employeeArchive/components/EmployeeArchiveForm.global.scss" lang="scss"></style>
- <!-- 薪酬与社保页签的职责边界提示条 -->
- <style lang="scss" scoped>
- .payroll-scope-alert {
- margin-bottom: 16px;
- ::v-deep .el-alert__content {
- line-height: 1.8;
- }
- }
- </style>
|