| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651 |
- <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" :label="item"
- :value="item" /></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 :xs="24" :sm="12"><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-col>
- <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-col>
- <el-col :xs="24" :sm="12">
- <el-form-item label="所属机构" prop="groupId">
- <el-select v-model="form.groupId" filterable class="full-width" placeholder="请选择法人组织"
- @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>
- </el-col>
- <el-col :xs="24" :sm="12">
- <el-form-item label="所属工厂">
- <el-select v-model="form.factoryId" filterable clearable class="full-width"
- :loading="factoryLoading" :disabled="!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>
- </el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="隶属部门" prop="deptIds"><el-cascader
- :key="`dept-cascader-${form.groupId || 'none'}-${deptCascaderKey}`" v-model="form.deptIds"
- :options="deptOptions" :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="!form.groupId"
- :placeholder="deptCascaderPlaceholder"
- @change="handleOrganizationChange" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12">
- <el-form-item label="岗位序列" prop="positionSequenceId">
- <el-select v-model="form.positionSequenceId" filterable clearable class="full-width"
- :loading="sequenceLoading" :disabled="!hasDept" :placeholder="hasDept ? '请选择岗位序列' : '请先选择隶属部门'"
- @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-col>
- <el-col :xs="24" :sm="12">
- <el-form-item label="岗位" prop="positionId">
- <el-select :key="`position-select-${positionSelectKey}`" v-model="form.positionId" filterable
- clearable class="full-width" :loading="positionLoading" :disabled="!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-col>
- <el-col :xs="24" :sm="12"><el-form-item label="岗位性质"><el-select v-model="form.employmentType" filterable
- clearable class="full-width" :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-col>
- <el-col :xs="24" :sm="12"><el-form-item label="岗位层级"><el-select v-model="form.staffingLevel"
- class="full-width" :disabled="!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>
- <el-col :xs="24" :sm="12">
- <el-form-item label="直属上级">
- <el-select v-model="form.managerId" filterable clearable class="full-width"
- :loading="managerLoading" :disabled="!hasDept" :placeholder="hasDept ? '请选择直属上级' : '请先选择隶属部门'"
- @change="handleManagerChange">
- <el-option v-for="item in managerOptions" :key="item.id" :label="item.name" :value="item.id" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="入职时间" prop="hireDate"><el-date-picker
- v-model="form.hireDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择入职日期"
- class="full-width" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="转正日期"><el-date-picker v-model="form.regularDate"
- value-format="yyyy-MM-dd" type="date" placeholder="转正生效日期"
- class="full-width" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="员工状态" prop="status"><el-select v-model="form.status"
- class="full-width"><el-option v-for="item in statusOptions" :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="关键工序"><el-input v-model.trim="form.keyProcess"
- maxlength="30" placeholder="标记特种工序岗位人员" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><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, getUserPage, EMPLOYEE_STATUS, CERT_TYPE } from '@/api/organization';
- import { getDictByCode } from '@/api/sys';
- 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';
- 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: '优',
- company: '中盈产业集团有限公司', legalEntity: '', groupId: null, factory: '', factoryId: null, deptId: null, deptIds: [], deptName: '',
- position: '', positionId: null, positionIds: [], positionBindings: [],
- // 岗位序列(部门 → 序列 → 岗位 → 性质 → 层级,与办理入职一致)
- positionSequenceId: '', positionSequence: '', positionSequenceName: '',
- positionType: '', level: '', staffingLevel: '', dutyLevel: '', manager: '', managerId: null,
- employmentType: '', employmentTypeName: '', status: '全职', hireDate: '', regularDate: '', account: '', enabled: true,
- keyProcess: '',
- 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 位手机号码'));
- };
- const validateIdCard = (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- const text = String(value || '').trim();
- if (!text) return callback();
- // 脱敏展示(如 4301********1234)允许通过
- if (/^\d{6}\*{8}[\dXx]{4}$/.test(text)) return callback();
- if (!/^\d{17}[\dXx]$/.test(text)) {
- return callback(new Error('请输入正确的身份证号码'));
- }
- const birth = text.slice(6, 14);
- const y = Number(birth.slice(0, 4));
- const m = Number(birth.slice(4, 6));
- const d = Number(birth.slice(6, 8));
- const date = new Date(y, m - 1, d);
- if (
- date.getFullYear() !== y ||
- date.getMonth() !== m - 1 ||
- date.getDate() !== d ||
- y < 1900 ||
- date.getTime() > Date.now()
- ) {
- return callback(new Error('请输入正确的身份证号码'));
- }
- const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
- const codes = '10X98765432';
- let sum = 0;
- for (let i = 0; i < 17; i += 1) sum += Number(text[i]) * weights[i];
- if (codes[sum % 11] !== text[17].toUpperCase()) {
- 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: ['男', '女', '未知'],
- 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' }],
- gender: [{
- 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',
- }],
- deptIds: [{
- required: true,
- validator: (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- if (!this.normalizeDeptIds(value).length) {
- callback(new Error('请选择隶属部门'));
- return;
- }
- callback();
- },
- trigger: 'change',
- }],
- status: [{
- 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',
- }],
- phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }, { validator: validatePhone, trigger: 'blur' }],
- email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }],
- idCard: [{ 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); },
- 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);
- // 登录缓存缺失时(兜底选项只有 id 没有名称,会显示成 "1"),回落到组织树里的
- // 集团/公司节点(type 10/20),保证下拉可读、可选(如「中赢合力」);
- // 编辑老数据时原机构不在选项里,则补一条兜底选项,避免下拉只显示 ID
- legalEntities() {
- let list = getOrgOptions().map((item) => ({
- id: String(item.id),
- name: item.name
- }));
- if (!list.some((item) => item.name)) {
- const tree = this.organizationTree.length ? this.organizationTree : this.organizations;
- const companies = flattenOrganizationTree(tree || [])
- .filter((item) => ['10', '20'].includes(String(item.type)))
- .map((item) => ({ id: String(item.id), name: item.name, type: item.type }));
- if (companies.length) list = companies;
- }
- const currentId = this.form.groupId != null && this.form.groupId !== '' ? String(this.form.groupId) : '';
- if (currentId && !list.some((item) => String(item.id) === currentId)) {
- const tree = this.organizationTree.length ? this.organizationTree : this.organizations;
- const hit = flattenOrganizationTree(tree || []).find((item) => String(item.id) === currentId);
- list.unshift({
- id: currentId,
- name: hit?.name || this.form.legalEntity || this.form.company || currentId,
- type: hit?.type || '20'
- });
- }
- 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 };
- },
- hasDept() {
- return this.normalizeDeptIds(this.form.deptIds).length > 0;
- },
- hasPositionSequence() {
- return Boolean(this.form.positionSequenceId);
- },
- requiredProgress() {
- const keys = ['name', 'employeeNo', 'gender', 'groupId', 'status', 'hireDate', 'positionId', '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', 'keyProcess']),
- 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() {
- 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.bankAccount, this.form.insuredDate, 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(label=中文名,value=状态编码)。
- * 下拉 value 用字典编码,回显按状态码匹配、提交直接传编码;
- * 字典为空或加载失败时,用 EMPLOYEE_STATUS(中文→数字码)兜底,保证一定能选。 */
- async loadStatusOptions() {
- const fallback = Object.entries(EMPLOYEE_STATUS).map(([label, code]) => ({ label, value: String(code) }));
- try {
- const items = await getDictByCode('hr_user_status');
- const options = (Array.isArray(items) ? items : [])
- .map((item) => {
- const label = String(item.label ?? item.name ?? '').trim();
- const value = item.value ?? item.id ?? item.key;
- if (!label || value == null || value === '') return null;
- return { label, value: String(value) };
- })
- .filter(Boolean);
- this.statusOptions = options.length ? options : fallback;
- } catch (error) {
- this.statusOptions = fallback;
- }
- // 字典晚于表单加载时,把已回填的中文/旧值重新对齐成字典 value
- if (this.form.status) {
- const hit = this.statusOptions.find(
- (opt) => String(opt.value) === String(this.form.status) || opt.label === this.form.status,
- );
- if (hit) this.form.status = hit.value;
- }
- },
- /** 回显员工状态:优先用后端状态码匹配字典 value,其次按中文 label 匹配 */
- resolveStatusForForm(source) {
- const code = source.statusCode ?? source.raw?.status;
- const byCode = code != null && code !== ''
- ? this.statusOptions.find((opt) => String(opt.value) === String(code))
- : null;
- if (byCode) return byCode.value;
- const byLabel = this.statusOptions.find(
- (opt) => opt.label === source.formStatus || opt.label === source.status,
- );
- if (byLabel) return byLabel.value;
- return this.statusOptions.length ? this.statusOptions[0].value : (source.formStatus || '');
- },
- 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),
- legalEntity: source.legalEntity || source.company || '',
- company: source.company || source.legalEntity || '',
- status: this.resolveStatusForForm(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 || '',
- 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();
- });
- this.loadOrganizations().then(() => {
- /* 新增员工默认带出所属机构(登录机构列表第一个) */
- this.applyDefaultLegalEntity();
- this.normalizeDeptAgainstOptions();
- this.loadFactories().then(() => this.syncFactoryDisplay());
- this.loadManagerOptions({ keepSelection: true });
- });
- 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;
- },
- applyDeptIds(ids, { resetDependents = false } = {}) {
- const list = this.normalizeDeptIds(ids).filter(
- (id) => String(id) !== String(this.form.groupId || ''),
- );
- this.form.deptIds = list;
- this.form.deptId = list[0] || null;
- this.form.deptName = list
- .map((id) => this.findDeptNode(id)?.name)
- .filter(Boolean)
- .join('、');
- if (resetDependents) {
- this.loadManagerOptions({ keepSelection: false });
- this.resetPositionsByDepartment();
- }
- },
- /** 新增员工默认带出所属机构:登录机构列表第一个(命中法人组织选项时才填充) */
- 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);
- this.form.legalEntity = hit.name || '';
- this.form.company = hit.name || this.form.company;
- },
- handleOrganizationChange(ids) {
- this.applyDeptIds(ids, { resetDependents: true });
- 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(id) {
- const selected = this.legalEntities.find((item) => String(item.id) === String(id));
- this.form.legalEntity = selected?.name || '';
- this.form.company = selected?.name || this.form.company;
- // 工厂与隶属部门都只挂所属机构:换机构时两者一起重置(彼此之间互不影响)
- this.deptCascaderKey += 1;
- this.form.factoryId = null;
- this.form.factory = '';
- this.factoryOptions = [];
- this.applyDeptIds([]);
- this.form.managerId = null;
- this.form.manager = '';
- this.managerOptions = [];
- this.resetPositionsByDepartment();
- this.loadFactories();
- },
- 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);
- },
- 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 || '';
- },
- mergeCurrentManagerOption() {
- const id = this.form.managerId;
- if (id == null || id === '') 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 || '未命名人员'
- });
- },
- async loadManagerOptions({ keepSelection = false } = {}) {
- const deptId = this.form.deptId;
- if (!keepSelection) {
- this.form.managerId = null;
- this.form.manager = '';
- }
- if (!deptId) {
- this.managerOptions = [];
- return;
- }
- const requestKey = String(deptId);
- this._managerRequestKey = requestKey;
- this.managerLoading = true;
- try {
- const data = await getUserPage({
- pageNum: 1,
- size: 99999,
- groupId: deptId,
- 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 || ''
- }))
- .filter((item) => item.id != null && item.id !== '' && item.name && String(item.id) !== selfId);
- if (this.form.managerId != null && this.form.managerId !== '') {
- this.form.managerId = String(this.form.managerId);
- }
- this.mergeCurrentManagerOption();
- } catch (error) {
- if (this._managerRequestKey !== requestKey) return;
- this.managerOptions = [];
- this.mergeCurrentManagerOption();
- } finally {
- if (this._managerRequestKey === requestKey) {
- this.managerLoading = false;
- }
- }
- },
- async loadOrganizations() {
- try {
- const list = await listOrganizations();
- this.organizationTree = buildOrganizationTree(Array.isArray(list) ? list : []);
- } catch (error) {
- this.organizationTree = [];
- }
- },
- collectFactoryOrgs(groupId) {
- if (!groupId) return [];
- const tree = this.organizationTree.length ? this.organizationTree : this.organizations;
- const find = (nodes) => {
- for (const node of nodes || []) {
- if (!node) continue;
- if (String(node.id) === String(groupId)) return node;
- const matched = find(node.children || []);
- if (matched) return matched;
- }
- return null;
- };
- const list = [];
- const walk = (nodes) => {
- (nodes || []).forEach((node) => {
- if (!node) return;
- const type = String(node.type);
- if (type === '40' || type === 'factory') {
- list.push({
- id: String(node.id),
- name: node.name || '',
- code: node.groupCode || '',
- groupId: String(groupId),
- });
- }
- walk(node.children || []);
- });
- };
- walk(find(tree || [])?.children || []);
- return list.filter((item) => item.id && item.name);
- },
- factoryBelongsToOrg(item = {}, groupId) {
- if (!groupId) return false;
- const orgId = String(groupId);
- const ids = [
- item.groupId,
- item.organizationId,
- item.companyId,
- item.parentId,
- item.legalId,
- ]
- .filter((value) => value != null && value !== '')
- .map((value) => String(value));
- if (!ids.length) return true;
- return ids.includes(orgId);
- },
- async loadFactories() {
- const groupId = this.form.groupId;
- if (!groupId) {
- this.factoryOptions = [];
- this.factoryLoading = false;
- return;
- }
- this.factoryLoading = true;
- try {
- const fromTree = this.collectFactoryOrgs(groupId);
- if (fromTree.length) {
- this.factoryOptions = fromTree;
- return;
- }
- const data = await getFactoryAreaPage({
- pageNum: 1,
- size: 999,
- type: 1,
- groupId,
- organizationId: groupId,
- });
- this.factoryOptions = (Array.isArray(data?.list) ? data.list : (Array.isArray(data) ? data : []))
- .map((item) => ({
- id: String(item.id),
- name: item.name || item.factoryName || item.pathName || `工厂 ${item.id}`,
- code: item.code || '',
- enabled: item.enabled,
- groupId: item.groupId || item.organizationId || item.companyId || item.parentId || '',
- }))
- .filter((item) => item.id && item.name && this.factoryBelongsToOrg(item, groupId));
- } catch (error) {
- this.factoryOptions = this.collectFactoryOrgs(groupId);
- if (!this.factoryOptions.length) {
- 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 = [];
- }
- },
- /** 接口返回可能是数组 / {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() : '';
- },
- /**
- * 隶属部门(可多选)→ 岗位序列下拉:逐部门取序列后并集去重
- * GET /fm/tmaindepartmentposition/sequence/list
- */
- async loadPositionSequences() {
- const deptIds = this.normalizeDeptIds(this.form.deptIds);
- if (!deptIds.length) {
- this.sequenceOptions = [];
- this.sequenceLoading = false;
- return;
- }
- this.sequenceLoading = true;
- let requestFailed = false;
- try {
- const groups = await Promise.all(
- deptIds.map((deptId) =>
- getDepartmentPositionSequences(deptId).catch(() => {
- requestFailed = true;
- return [];
- }),
- ),
- );
- const map = new Map();
- groups
- .flatMap((data) => 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(deptIds);
- }
- } 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');
- });
- },
- /**
- * 隶属部门 + 岗位序列 → 岗位下拉(多部门并集,逐项记录来源部门用于带出性质)
- * GET /fm/tmaindepartmentposition/position/list
- */
- async loadDepartmentPositions() {
- const deptIds = this.normalizeDeptIds(this.form.deptIds);
- const positionSequenceId = this.form.positionSequenceId;
- if (!deptIds.length || !positionSequenceId) {
- this.positionOptions = [];
- this.hrPositionIdSet = {};
- this.positionLoading = false;
- return;
- }
- this.positionLoading = true;
- try {
- const groups = await Promise.all(
- deptIds.map(async (deptId) => {
- const data = await getDepartmentPositions({ deptId, positionSequenceId }).catch(() => []);
- return this.toOptionList(data).map((item) => ({ ...item, deptId }));
- }),
- );
- const sequenceName = this.form.positionSequence || this.form.positionSequenceName || '';
- const list = groups
- .flat()
- .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: item.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 {
- const levels = await getDepartmentPositionLevels({
- deptId,
- positionId,
- positionSequenceId,
- positionType: this.form.positionType || '',
- }).catch(() => []);
- 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) {
- // 层级接口异常时退回岗位自带层级配置
- }
- 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.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>
|