| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258 |
- <template>
- <div class="ele-body" v-loading="loading">
- <el-card shadow="never">
- <el-form label-width="80px" class="ele-form-search" @keyup.enter.native="applyFilters" @submit.native.prevent>
- <el-row :gutter="15">
- <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
- <el-form-item label="姓名">
- <el-input v-model.trim="filters.name" clearable placeholder="请输入" />
- </el-form-item>
- </el-col>
- <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
- <el-form-item label="所属机构">
- <el-select v-model="filters.organizationId" clearable filterable placeholder="请选择" style="width: 100%">
- <el-option v-for="item in legalEntities" :key="String(item.id)" :label="item.name" :value="item.id" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
- <el-form-item label="岗位性质">
- <el-select v-model="filters.positionType" clearable placeholder="请选择" style="width: 100%">
- <el-option v-for="item in statusOptions" :key="String(item.value)" :label="item.label"
- :value="item.value" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
- <div class="ele-form-actions">
- <el-button type="primary" icon="el-icon-search" class="ele-btn-icon" @click="applyFilters">
- 查询
- </el-button>
- <el-button type="primary" icon="el-icon-refresh" class="ele-btn-icon" @click="resetFilters">
- 重置
- </el-button>
- </div>
- </el-col>
- </el-row>
- </el-form>
- <ele-pro-table ref="table" row-key="userId" :columns="columns" :datasource="fetchPage" :need-page="true"
- :page-size="20" :cache-key="$tableCacheKey('hr-onboarding-handling')" class="onboarding-table" @selection-change="selectedRows = $event">
- <template v-slot:toolbar>
- <el-button v-if="$hasPermission('main:hrOnboarding:save')" type="primary" icon="el-icon-plus"
- class="ele-btn-icon" @click="openCreate">
- 新建
- </el-button>
- </template>
- <!-- 点员工姓名开详情,替代原操作列「查看」入口 -->
- <template v-slot:name="{ row }">
- <el-link v-if="row.name" type="primary" :underline="false" @click.stop="openDetail(row)">
- {{ row.name }}
- </el-link>
- <span v-else>-</span>
- </template>
- <!-- 组织信息拆两列(2026-10-29 用户要求):所属机构 = organizationName、
- 所属部门 = targetDepartmentName,直接取接口字段,没返回就显示 -,不做兜底 -->
- <!-- 流程进度:文案直接用 onboardingProgressValue,配色按 onboardingProgress 枚举判断 -->
- <template v-slot:flow="{ row }">
- <el-tag v-if="row.onboardingProgressValue" size="mini" effect="plain"
- :type="progressTagType(row.onboardingProgress)">
- {{ row.onboardingProgressValue }}
- </el-tag>
- </template>
- <!-- 岗位性质:只认接口的岗位性质名称快照(positionTypeName),取不到就留空 -->
- <template v-slot:positionType="{ row }">
- <el-tag v-if="row.positionTypeName" size="mini" :type="statusTagType(row.positionTypeName)">
- {{ row.positionTypeName }}
- </el-tag>
- </template>
- <!-- 操作栏:能否办理/删除一律由 onboardingProgress 枚举判断 -->
- <template v-slot:action="{ row }">
- <el-link v-if="!isProgressCompleted(row) && $hasPermission('main:hrOnboarding:update')" type="primary"
- :underline="false" icon="el-icon-edit" @click.stop="openEdit(row)">
- 办理
- </el-link>
- <el-link v-if="canDelete(row) && $hasPermission('main:hrOnboarding:delete')" type="danger"
- :underline="false" icon="el-icon-delete" @click.stop="deleteRecord(row)">
- 删除
- </el-link>
- </template>
- <template slot="empty">
- <el-empty description="暂无数据" />
- </template>
- </ele-pro-table>
- </el-card>
- <ele-modal :title="dialogTitle" :visible.sync="dialogVisible" width="1280px" custom-class="onboarding-modal"
- :close-on-click-modal="false" append-to-body>
- <el-form ref="form" :model="editing" :rules="rules" :disabled="currentStep === 'profile' && !profileEditable"
- label-width="100px" class="onboarding-form">
- <div class="dialog-summary">
- <span class="avatar">{{ (editing.name || "新").slice(0, 1) }}</span>
- <div>
- <h3>{{ editing.name || "新建待入职人员" }}</h3>
- <p>
- {{
- editing.employeeNo ||
- editing.invitationNo ||
- "完善资料并按顺序推进办理步骤"
- }}
- </p>
- </div>
- <el-tag :type="statusTag(editing.status)" effect="plain">{{
- editing.status || "待办理"
- }}</el-tag>
- </div>
- <el-divider>办理步骤</el-divider>
- <div class="step-grid">
- <button v-for="step in steps" :key="step.key" type="button" class="step-card" :class="{
- done: stepDone(step.key),
- active: currentStep === step.key,
- locked: isStepLocked(step.key),
- }" :disabled="isStepDisabled(step.key)" @click="handleStep(step)">
- <span><i :class="step.icon"></i></span>
- <div>
- <strong>{{ step.label }}</strong>
- <small>{{ stepDesc(step.key) }}</small>
- </div>
- <i class="el-icon-arrow-right"></i>
- </button>
- </div>
- <!-- 第二步:录入 -->
- <template v-if="currentStep === 'profile'">
- <el-divider>基本档案</el-divider>
- <div class="form-grid">
- <el-form-item label="办理方式" prop="createMode">
- <el-radio-group v-model="editing.createMode" :disabled="!isCreate && !!editing.userId">
- <el-radio label="manual">手动添加</el-radio>
- <el-radio label="invite">二维码邀请</el-radio>
- </el-radio-group>
- </el-form-item>
- </div>
- <!-- 二维码邀请:不填入职资料,只展示邀请二维码(二维码图待后端就绪后接入,暂留空位) -->
- <div v-if="editing.createMode === 'invite'" class="invite-qr-panel">
- <div v-if="qrImageUrl" class="invite-qr-card">
- <img :src="qrImageUrl" alt="入职邀请二维码" />
- <p>请候选人扫码填写入职资料</p>
- <el-input v-if="inviteLink" :value="inviteLink" readonly size="small" class="invite-qr-link">
- <el-button slot="append" icon="el-icon-document-copy" @click="copyInviteLink">复制</el-button>
- </el-input>
- </div>
- </div>
- <div v-else class="form-grid">
- <el-form-item label="姓名" prop="name">
- <el-input v-model.trim="editing.name" placeholder="输入姓名后自动带出录用信息" @blur="onNameBlur"
- @keyup.enter.native="searchHiringByName">
- <i v-if="hiringSearching" slot="suffix" class="el-icon-loading" />
- </el-input>
- </el-form-item>
- <el-form-item label="入职日期" prop="hireDate">
- <el-date-picker v-model="editing.hireDate" value-format="yyyy-MM-dd" placeholder="计划入职日期" />
- </el-form-item>
- <el-form-item label="所属机构" prop="legalId">
- <el-select v-model="editing.legalId" filterable clearable placeholder="请选择所属机构" @change="onLegalChange">
- <el-option v-for="item in legalEntities" :key="item.id" :label="item.name" :value="item.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="目标部门" prop="deptId">
- <organization-select v-model="editing.deptId" :data="departmentTree" :disabled="!editing.legalId"
- :placeholder="editing.legalId ? '请选择部门' : '请先选择所属机构'" @input="onDeptChange" />
- </el-form-item>
- <el-form-item label="岗位序列" prop="positionSequenceId">
- <el-select v-model="editing.positionSequenceId" filterable clearable :loading="sequenceLoading"
- :disabled="!editing.deptId" :placeholder="editing.deptId ? '请选择岗位序列' : '请先选择目标部门'
- " @change="onPositionSequenceChange">
- <el-option v-for="item in sequenceOptions" :key="item.id" :label="item.name" :value="item.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="岗位" prop="positionId">
- <el-select v-model="editing.positionId" filterable clearable :loading="positionLoading"
- :disabled="!editing.positionSequenceId" :placeholder="editing.positionSequenceId ? '请选择岗位' : '请先选择岗位序列'
- " @change="onPositionChange">
- <el-option v-for="item in positionOptions" :key="item.id" :label="item.name" :value="item.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="岗位性质" prop="positionType">
- <el-input v-model="editing.positionTypeName" readonly placeholder="选择岗位后自动带出" />
- </el-form-item>
- <el-form-item label="岗位层级" prop="positionLevelId">
- <el-select v-model="editing.positionLevelId" clearable filterable :loading="levelLoading"
- :disabled="!editing.positionId" :placeholder="editing.positionId ? '请选择岗位层级' : '请先选择岗位'
- " style="width: 100%" @change="onPositionLevelChange">
- <el-option v-for="item in levelOptions" :key="item.id || item.name" :label="item.name"
- :value="item.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="手机号" prop="phone">
- <el-input v-model.trim="editing.phone" maxlength="11" placeholder="请输入 11 位手机号码" />
- </el-form-item>
- <el-form-item label="身份证号" prop="idNumber">
- <el-input v-model.trim="editing.idNumber" maxlength="18" placeholder="请输入 18 位身份证号" />
- </el-form-item>
- <el-form-item label="附件" class="form-grid-full">
- <fileMain v-model="editing.fileIds" :type="readOnly ? 'view' : ''" size="small" />
- <p class="attach-hint">支持上传简历、证件等入职材料,可点击查看已传附件</p>
- </el-form-item>
- </div>
- </template>
- <!-- 第三步:签署合同 → 合同详情视图(复用合同管理 adaptContract) -->
- <template v-else-if="currentStep === 'contract'">
- <el-divider>合同信息</el-divider>
- <!-- 有完整合同详情:按合同管理详情样式展示 -->
- <div v-if="contractDetailRow" class="cm-create-page cm-detail-page">
- <!-- 员工/合同状态头 -->
- <div class="cm-create-employee-card cm-detail-employee">
- <span class="cm-employee-avatar">{{ contractDetailRow.employeeName ?
- contractDetailRow.employeeName.slice(0, 1) : '?' }}</span>
- <div class="cm-employee-info">
- <b>员工姓名:{{ contractDetailRow.employeeName }}<span class="cm-employee-no">员工工号:{{
- contractDetailRow.employeeNo }}</span></b>
- </div>
- <div class="cm-detail-status">
- <small>合同状态</small>
- <span class="cm-status" :class="contractStatusClass(contractDetailRow.status)">{{
- contractDetailRow.status }}</span>
- </div>
- </div>
- <!-- 合同基本信息 -->
- <div class="cm-create-section">
- <div class="cm-create-section-title">合同基本信息</div>
- <div class="cm-create-grid">
- <div class="cm-compare-item"><span class="cm-compare-label">合同编号:</span><span
- class="cm-compare-value">{{ contractDetailRow.contractNo }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">合同类型:</span><span
- class="cm-compare-value">{{ contractDetailRow.contractType }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">所属机构:</span><span
- class="cm-compare-value">{{ contractDetailRow.legalEntity }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">所属部门:</span><span
- class="cm-compare-value">{{ contractDetailRow.department }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">岗位名称:</span><span
- class="cm-compare-value">{{ contractDetailRow.positionName || editing.position || '-' }}</span>
- </div>
- <div class="cm-compare-item"><span class="cm-compare-label">岗位层级:</span><span
- class="cm-compare-value">{{ contractDetailRow.level || '-' }}</span></div>
- <!-- <div class="cm-compare-item"><span class="cm-compare-label">直属上级:</span><span class="cm-compare-value">{{ contractDetailRow.manager || '-' }}</span></div> -->
- </div>
- </div>
- <!-- 合同期限与薪资 -->
- <div class="cm-create-section">
- <div class="cm-create-section-title">合同期限与薪资</div>
- <div class="cm-create-grid">
- <div class="cm-compare-item"><span class="cm-compare-label">合同起始:</span><span
- class="cm-compare-value">{{ contractDetailRow.startDate || '-' }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">合同终止:</span><span
- class="cm-compare-value">{{ contractDetailRow.endDate || '长期' }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">月薪资:</span><span
- class="cm-compare-value is-highlight">{{ contractDetailRow.salary ? ('¥ ' +
- Number(contractDetailRow.salary).toLocaleString() + ' 元/月') : '待定' }}</span></div>
- <!-- 试用期:接口回显 probationMonths(月数),0/空 = 无试用期 -->
- <div class="cm-compare-item"><span class="cm-compare-label">试用期:</span><span
- class="cm-compare-value">{{ contractDetailRow.probationText }}</span></div>
- </div>
- </div>
- <!-- 签署信息 -->
- <div class="cm-create-section">
- <div class="cm-create-section-title">签署信息</div>
- <div class="cm-create-grid">
- <div class="cm-compare-item"><span class="cm-compare-label">签署方式:</span><span
- class="cm-compare-value">{{ contractDetailRow.signMethod || '-' }}</span></div>
- <!-- <div class="cm-compare-item"><span class="cm-compare-label">签署状态:</span><span class="cm-compare-value">{{ contractDetailRow.signingStatusLabel || '-' }}</span></div> -->
- <div v-if="contractDetailRow.flowSignStatus" class="cm-compare-item"><span
- class="cm-compare-label">流程节点电子签章:</span><span class="cm-compare-value">{{
- contractDetailRow.flowSignStatusLabel || '-' }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">合同附件:</span><span class="cm-compare-value">
- <fileMain v-if="contractDetailRow.attachmentFileIds && contractDetailRow.attachmentFileIds.length"
- :value="contractDetailRow.attachmentFileIds" type="view" size="small" />
- <span v-else class="cm-detail-muted">暂无附件</span>
- </span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">签订日期:</span><span
- class="cm-compare-value">{{ contractDetailRow.signDate || contractDetailRow.createdAt || '-'
- }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">创建时间:</span><span
- class="cm-compare-value">{{ contractDetailRow.createdAt || '-' }}</span></div>
- </div>
- </div>
- </div>
- <!-- 合同详情加载中/为空:用入职信息做简版展示 -->
- <div v-else class="info-display-grid">
- <div class="info-item">
- <span class="info-label">姓名</span>
- <strong>{{ editing.name || "-" }}</strong>
- </div>
- <div class="info-item">
- <span class="info-label">入职日期</span>
- <strong>{{ editing.hireDate || "-" }}</strong>
- </div>
- <div class="info-item">
- <span class="info-label">所属机构</span>
- <strong>{{ editing.company || "-" }}</strong>
- </div>
- <div class="info-item">
- <span class="info-label">目标部门</span>
- <strong>{{ editing.department || deptNameOf(editing.deptId) || "-" }}</strong>
- </div>
- <div class="info-item">
- <span class="info-label">岗位</span>
- <strong>{{ editing.position || editing.positionName || "-" }}</strong>
- </div>
- <div class="info-item">
- <span class="info-label">手机号</span>
- <strong>{{ editing.phone || "-" }}</strong>
- </div>
- </div>
- </template>
- <!-- 第四步:物品领用 -->
- <template v-else-if="currentStep === 'assets'">
- <el-divider>物品领用</el-divider>
- <ItemReceiptPanel ref="receiptPanel" :employee-id="editing.userId || editing.id"
- :init-receipt-id="receiptData && receiptData.id ? receiptData.id : ''"
- :onboarding-progress="editing.onboardingProgress || ''" :read-only="readOnly || receiptApproving"
- :contract-signed="contractSigned" @loaded="onReceiptLoaded" @completed="onReceiptCompleted"
- @request-approval="openReceiptApproval" />
- </template>
- <template v-else-if="currentStep === 'complete'">
- <div class="onboarding-complete">
- <i class="el-icon-circle-check success-icon"></i>
- <div class="complete-text">
- <div class="complete-title">已入职</div>
- <div class="complete-sub">入职流程已完成</div>
- </div>
- </div>
- </template>
- <!-- 其他步骤提示 -->
- <template v-else>
- <el-alert :title="stepHint" type="info" :closable="false" show-icon />
- </template>
- </el-form>
- <span slot="footer">
- <el-button @click="dialogVisible = false">{{
- readOnly ? "关闭" : "取消"
- }}</el-button>
- <!-- 详情模式(openDetail):不显示业务操作按钮 -->
- <template v-if="readOnly">
- <el-button v-if="currentStep === 'contract' && contractAlreadySaved" type="primary" icon="el-icon-document"
- @click="goViewContract">去查看合同</el-button>
- <el-button v-else-if="currentStep === 'assets' && (receiptApproving || editing.assets === '已领用')"
- type="primary" plain icon="el-icon-tickets" @click="openReceiptApprovalRecord">审批记录</el-button>
- </template>
- <!-- 办理模式(openEdit):按步骤+进度判定 -->
- <template v-else>
- <!-- profile 节点:无审批,始终显示保存/下一步(仅 ONBOARDING 进度可编辑;其他进度档案已完成,按钮不出现) -->
- <template v-if="currentStep === 'profile' && profileEditable">
- <el-button :loading="saving" @click="saveDraft">保存</el-button>
- <el-button type="primary" :loading="saving" @click="goNextStep">下一步</el-button>
- </template>
- <!-- contract 节点 -->
- <template v-else-if="currentStep === 'contract'">
- <el-button v-if="contractAlreadySaved" type="primary" icon="el-icon-document"
- @click="goViewContract">去查看合同</el-button>
- <el-button v-if="platformAccount && !contractAlreadySaved && $hasPermission('main:hrOnboarding:sign')"
- type="primary" @click="goSignContract">去签署合同</el-button>
- </template>
- <!-- assets 节点 -->
- <template v-else-if="currentStep === 'assets'">
- <el-button v-if="!receiptApproving && editing.assets !== '已领用'" :loading="receiptSaving"
- @click="saveReceipt">保存</el-button>
- <el-button v-if="!receiptApproving && editing.assets !== '已领用' && $hasPermission('main:hrOnboarding:submit')"
- type="primary" :loading="receiptSubmitting" @click="submitReceipt">提交</el-button>
- <el-button v-if="receiptApproving || editing.assets === '已领用'" type="primary" plain icon="el-icon-tickets"
- @click="openReceiptApprovalRecord">审批记录</el-button>
- </template>
- </template>
- </span>
- </ele-modal>
- <processSubmitDialog :processSubmitDialogFlag.sync="processSubmitDialogFlag" v-if="processSubmitDialogFlag"
- ref="processSubmitDialogRef" @reload="onProcessSubmitReload" />
- <BpmProcessDetailDialog ref="approvalRecordDialog" />
- </div>
- </template>
- <script>
- import {
- buildInviteQrImage,
- createOnboarding,
- deleteOnboarding,
- getOnboardingByUserId,
- getOnboardingPage,
- ONBOARDING_APPROVAL_BUSINESS_KEY,
- updateOnboarding,
- } from "@/api/hr/onboarding";
- import {
- adaptHiringApproval,
- getHiringApprovalPage,
- getHiringApprovalById,
- } from "@/api/hr/hiringApproval";
- import {
- getDepartmentPositionSequences,
- getDepartmentPositions,
- getDepartmentPositionType,
- getDepartmentPositionLevels,
- getEmployeeContractStatus,
- getContractById,
- adaptContract,
- } from "@/api/hr";
- import {
- createPersonalDirectory,
- hasJobNumber,
- listOrganizations,
- searchUsersByKeyword,
- } from "@/api/organization";
- /* 机构选项取登录缓存(loginChangeGroupVOList) */
- import { getOrgOptions, getCurrentOrgId, getOrgName } from "@/utils/current-org";
- import { getDictByCode } from "@/api/sys";
- import { buildOrganizationTree } from "@/utils/organization-tree";
- import { showRequestError } from "@/utils/message";
- import { resolveContractManagementPath } from "@/utils/menu-rewrite";
- import { getCurrentUser } from "@/utils/token-util";
- import OrganizationSelect from "@/views/organization/components/OrganizationSelect.vue";
- import processSubmitDialog from "@/BIZComponents/processSubmitDialog/processSubmitDialog.vue";
- import ItemReceiptPanel from "./ItemReceiptPanel.vue";
- import BpmProcessDetailDialog from "@/views/bpm/processInstance/detailDialog.vue";
- import {
- RECEIPT_APPROVAL_BUSINESS_KEY,
- } from "@/api/hr/onboardingItemReceipt";
- const emptyFilters = () => ({
- name: "",
- /* 所属机构默认登录机构列表第一个(用户所属多个机构时默认第一个) */
- organizationId: getCurrentOrgId() || null,
- /* 岗位性质(列表「状态」筛选,与列「岗位性质」同口径):取岗位性质字典项值 */
- positionType: "",
- });
- const empty = () => ({
- id: "",
- onboardingId: "",
- invitationId: "",
- invitationNo: "",
- employeeNo: "",
- jobNumber: "",
- name: "",
- department: "",
- deptId: null,
- position: "",
- positionName: "",
- positionId: null,
- positionSequence: "",
- // 岗位序列/层级/性质字段(提交给后端)
- positionSequenceId: "",
- positionLevelId: "",
- positionLevel: "",
- positionType: "",
- positionTypeName: "",
- hireDate: "",
- company: "",
- legalId: null,
- phone: "",
- email: "",
- idNumber: "",
- gender: "",
- age: "",
- probationFlag: 0,
- probationMonths: undefined,
- createMode: "manual",
- status: "待办理",
- flow: "待补充档案",
- offer: "已发起",
- contract: "未发起",
- assets: "未领用",
- remark: "",
- publicPath: "",
- publicToken: "",
- invitationStatus: "",
- onboardingStatus: "待补充档案",
- approvalStatus: "未发起",
- /** 入职流程进度枚举(如 ITEM_RECEIPT_APPROVING:物品领用审批中),后端返回 */
- onboardingProgress: "",
- createUserId: "",
- hiringApprovalId: "",
- candidateId: "",
- userId: "",
- fileIds: [],
- rowType: "record",
- });
- /**
- * 个人文档区(POST /fm/directory/createPersonal/{userId})去重缓存。
- * 接口文档未说明是否幂等,而组件内存名单刷新后会丢失,
- * 因此把「已成功创建」的 userId 持久化,避免对同一员工重复创建。
- * 只记录创建成功的,失败不落盘,保证下次仍可重试。
- */
- const PERSONAL_DIRECTORY_CACHE_KEY = "hr:onboarding:createdPersonalDirectory";
- const PERSONAL_DIRECTORY_CACHE_LIMIT = 500;
- function readCreatedDirectoryUsers() {
- try {
- const raw = window.localStorage.getItem(PERSONAL_DIRECTORY_CACHE_KEY);
- const list = raw ? JSON.parse(raw) : [];
- return Array.isArray(list) ? list.map((id) => String(id)) : [];
- } catch (error) {
- return [];
- }
- }
- function writeCreatedDirectoryUsers(list = []) {
- try {
- window.localStorage.setItem(
- PERSONAL_DIRECTORY_CACHE_KEY,
- JSON.stringify(list.slice(-PERSONAL_DIRECTORY_CACHE_LIMIT)),
- );
- } catch (error) {
- /* 隐私模式或存储不可用时忽略:去重退化为本次会话的内存名单 */
- }
- }
- export default {
- name: "OnboardingHandling",
- components: { OrganizationSelect, processSubmitDialog, ItemReceiptPanel, BpmProcessDetailDialog },
- data() {
- return {
- loading: false,
- saving: false,
- receiptSaving: false,
- receiptSubmitting: false,
- /** 公共审批弹框来源:onboarding=入职审批,receipt=物品领用审批 */
- approvalSource: null,
- filters: emptyFilters(),
- rows: [],
- totalCount: 0,
- orgList: [],
- legalEntities: [],
- departmentTree: [],
- positionOptions: [],
- positionLoading: false,
- sequenceOptions: [],
- sequenceLoading: false,
- levelOptions: [],
- levelLoading: false,
- selectedRows: [],
- dialogVisible: false,
- dialogTitle: "办理入职",
- readOnly: false,
- isCreate: false,
- /** 合同已保存(草稿或已提交审批):锁定"员工档案录入"步骤,直接进入"签署合同" */
- contractAlreadySaved: false,
- /** 合同流程接口返回的合同ID,用于跳转合同管理查看详情 */
- contractIdFromStatus: "",
- /** 合同流程接口返回的合同状态码,EFFECTIVE 时进入"物品领用"步骤 */
- contractStatusCode: "",
- /** 合同详情(签署合同节点展示) */
- contractDetailRow: null,
- editing: empty(),
- currentStep: "profile",
- processSubmitDialogFlag: false,
- currentLaunchRow: null,
- accountChecking: false,
- accountChecked: false,
- platformAccount: null,
- hiringSearching: false,
- lastHiringSearchName: "",
- /** 本次会话已处理(已创建或确认无需创建)的 userId,避免同一次办理重复调用 */
- personalDirectoryUserIds: [],
- /** 已成功创建过个人文档区的 userId(localStorage 持久化,跨刷新去重) */
- createdDirectoryUsers: readCreatedDirectoryUsers(),
- /** 物品领用单最新加载结果(含审批流程实例 id,用于查看审批记录) */
- receiptData: null,
- /** 本次会话物品领用审批已发起:提交成功后立即隐藏保存/提交,不等接口刷新 */
- receiptApprovalStarted: false,
- positionNatureOptions: [],
- steps: [
- { key: "offer", label: "Offer发起", icon: "el-icon-message" },
- { key: "profile", label: "员工档案录入", icon: "el-icon-edit-outline" },
- {
- key: "contract",
- label: "签署合同",
- icon: "el-icon-document-checked",
- },
- { key: "assets", label: "物品领用", icon: "el-icon-box" },
- { key: "complete", label: "完成", icon: "el-icon-circle-check" },
- ],
- };
- },
- computed: {
- styleResponsive() {
- return this.$store.state.theme?.styleResponsive !== false;
- },
- rules() {
- if (this.currentStep !== "profile") return {};
- /* 二维码邀请:入职资料表单整体隐藏,隐藏字段不参与校验,否则「保存」永远过不去 */
- if (this.editing.createMode === "invite") return {};
- return {
- name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
- hireDate: [
- { required: true, message: "请选择入职日期", trigger: "change" },
- ],
- legalId: [
- { required: true, message: "请选择所属机构", trigger: "change" },
- ],
- deptId: [
- { required: true, message: "请选择目标部门", trigger: "change" },
- ],
- positionId: [
- { required: true, message: "请选择岗位", trigger: "change" },
- ],
- positionSequenceId: [
- { required: true, message: "请选择岗位序列", trigger: "change" },
- ],
- positionLevelId: [
- { required: true, message: "请选择岗位层级", trigger: "change" },
- ],
- phone: [
- { required: true, message: "请输入手机号", trigger: "blur" },
- {
- pattern: /^1[3-9]\d{9}$/,
- message: "请输入正确的 11 位手机号码",
- trigger: "blur",
- },
- ],
- idNumber: [
- { required: true, message: "请输入身份证号", trigger: "blur" },
- {
- pattern: /(^\d{15}$)|(^\d{17}[\dXx]$)/,
- message: "请输入正确的身份证号码",
- trigger: "blur",
- },
- ],
- positionType: [
- { required: true, message: "请选择岗位性质", trigger: "change" },
- ],
- };
- },
- columns() {
- return [
- { type: "selection", width: 56, align: "center" },
- { type: "index", label: "序号", width: 60, align: "center" },
- {
- prop: "name",
- label: "员工",
- minWidth: 90,
- align: "left",
- showOverflowTooltip: true,
- slot: "name",
- },
- {
- /* 原「组织信息」一列拆成所属机构 / 所属部门两列(2026-10-29 用户要求) */
- prop: "organizationName",
- label: "所属机构",
- minWidth: 200,
- align: "left",
- showOverflowTooltip: true,
- formatter: (_r, _c, value) => value || "-",
- },
- {
- prop: "targetDepartmentName",
- label: "所属部门",
- minWidth: 160,
- align: "left",
- showOverflowTooltip: true,
- formatter: (_r, _c, value) => value || "-",
- },
- {
- prop: "position",
- label: "岗位",
- minWidth: 110,
- align: "left",
- showOverflowTooltip: true,
- formatter: (_r, _c, value) => value || "-",
- },
- {
- prop: "hireDate",
- label: "入职日期",
- width: 112,
- align: "left",
- formatter: (_r, _c, value) => value || "-",
- },
- {
- prop: "flow",
- label: "流程进度",
- minWidth: 120,
- align: "left",
- slot: "flow",
- },
- {
- /* 岗位性质只展示接口的 positionTypeName 快照(与编辑弹框、岗位档案同口径);
- columnKey 沿用旧值 status:列设置的本地缓存按 columnKey||prop 匹配,
- 改 prop 后缓存匹配不到会把该列排到最前 */
- prop: "positionTypeName",
- columnKey: "status",
- label: "岗位性质",
- width: 100,
- align: "left",
- slot: "positionType",
- },
- {
- prop: "createUserName",
- label: "创建人",
- width: 100,
- align: "left",
- },
- {
- prop: "createTime",
- label: "创建时间",
- width: 100,
- align: "left",
- },
- {
- prop: "updateUserName",
- label: "修改人",
- width: 100,
- align: "left",
- },
- {
- prop: "updateTime",
- label: "修改时间",
- width: 100,
- align: "left",
- },
- {
- columnKey: "action",
- label: "操作",
- width: 170,
- slot: "action",
- fixed: "right",
- align: "left",
- resizable: false,
- },
- ];
- },
- stepHint() {
- if (this.currentStep === "assets") {
- if (this.contractSigned) {
- return "合同已签署,请登记物品领用并提交。";
- }
- return "合同签署完成后才能提交物品领用,当前仅可保存草稿。";
- }
- if (this.currentStep === "complete") {
- return this.editing.status === "已完成"
- ? "入职流程已完成。"
- : "完成前序步骤后即可结束办理。";
- }
- return "请按步骤完成入职办理。";
- },
- statusOptions() {
- return this.positionNatureOptions;
- },
- inviteLink() {
- if (this.editing.publicPath) return this.editing.publicPath;
- if (this.editing.publicToken)
- return `/onboarding/public/${this.editing.publicToken}`;
- return "";
- },
- qrImageUrl() {
- if (this.editing.qrCodeUrl) return this.editing.qrCodeUrl;
- return buildInviteQrImage(this.inviteLink);
- },
- /** 录入已保存(M02 返回 userId)后才可进入后续步骤 */
- isProfileSaved() {
- return Boolean(this.editing.userId);
- },
- /** 办理入职进度枚举(后端上报,唯一真值源) */
- onboardingProgressNormalized() {
- return String(this.editing.onboardingProgress || "").toUpperCase();
- },
- /** 进度 → 当前应高亮的步骤 */
- progressToActiveStep() {
- const p = this.onboardingProgressNormalized;
- const map = {
- ONBOARDING: "profile",
- CONTRACT_DRAFT: "contract",
- CONTRACT_APPROVING: "contract",
- CONTRACT_APPROVED: "assets",
- ITEM_RECEIPT_DRAFT: "assets",
- ITEM_RECEIPT_APPROVING: "assets",
- COMPLETED: "complete",
- };
- return map[p] || null;
- },
- /** 档案录入节点是否允许编辑:新建态、或后端进度为 ONBOARDING 时允许 */
- profileEditable() {
- if (this.isCreate) return true;
- return this.onboardingProgressNormalized === "ONBOARDING";
- },
- /** 根据进度枚举,返回哪些节点已完成(单调累积,一旦 done 不会变灰) */
- doneStepSet() {
- const p = this.onboardingProgressNormalized;
- const completedFromProfile = [
- "CONTRACT_DRAFT",
- "CONTRACT_APPROVING",
- "CONTRACT_APPROVED",
- "ITEM_RECEIPT_DRAFT",
- "ITEM_RECEIPT_APPROVING",
- "COMPLETED",
- ];
- const completedFromContract = [
- "CONTRACT_APPROVED",
- "ITEM_RECEIPT_DRAFT",
- "ITEM_RECEIPT_APPROVING",
- "COMPLETED",
- ];
- return {
- offer: true,
- profile: completedFromProfile.includes(p) || this.isProfileSaved,
- contract: completedFromContract.includes(p),
- assets: p === "COMPLETED" || this.editing.assets === "已领用",
- complete: p === "COMPLETED" || this.editing.status === "已完成",
- };
- },
- /** 合同已签署/生效,物品领用单才允许提交 */
- contractSigned() {
- return Boolean(this.stepDone("contract"));
- },
- /** 物品领用审批中/已完成:表单只读、隐藏保存/提交
- * 唯一真值源:后端返回的入职进度枚举 onboardingProgress
- * 只有 ITEM_RECEIPT_APPROVING(审批中)和 COMPLETED(完成)才锁,
- * ITEM_RECEIPT_DRAFT(草稿态)始终允许编辑。
- */
- receiptApproving() {
- const progress = String(this.editing.onboardingProgress || "").toUpperCase();
- return (
- progress === "ITEM_RECEIPT_APPROVING" ||
- progress === "COMPLETED" ||
- this.receiptApprovalStarted
- );
- },
- /** 物品领用单已存在审批流程实例,可查看审批记录 */
- receiptHasRecord() {
- const row = { ...this.editing, ...(this.receiptData || {}) };
- // 有流程实例 id → 确定可查
- if (row.processInstanceId || row.approvalInstanceId || row.processInstance) {
- return true;
- }
- // 领用单状态已非草稿 / 审批流程已发起 → 一定走过审批,展示按钮
- const status = String(row.receiptStatus || "").toUpperCase();
- if (status && status !== "DRAFT") return true;
- const aps = String(
- row.approvalStatus || row.approvalStatusName || "",
- )
- .trim()
- .toUpperCase();
- return Boolean(
- aps &&
- !["", "未发起", "NOT_SUBMITTED", "DRAFT", "UNSUBMITTED", "NONE"].includes(
- aps,
- ),
- );
- },
- },
- created() {
- this.loadOrganizations();
- this.loadPositionNatureOptions();
- },
- methods: {
- positionTypeLabel(value) {
- if (value == null || value === "") return "-";
- const text = String(value);
- const hit = this.positionNatureOptions.find(
- (item) =>
- String(item.value) === text ||
- String(item.key) === text ||
- String(item.label) === text ||
- String(item.id) === text,
- );
- return hit?.label || text;
- },
- resolvePositionType(raw) {
- if (raw == null || raw === "") return "";
- const text = String(raw);
- const hit = this.positionNatureOptions.find(
- (item) =>
- String(item.value) === text ||
- String(item.key) === text ||
- String(item.label) === text ||
- String(item.id) === text,
- );
- return hit ? hit.value : raw;
- },
- /** 根据岗位性质 id/名称反查字典 label,用于提交 positionTypeName */
- matchPositionTypeName(raw) {
- if (raw == null || raw === "") return "";
- const text = String(raw);
- const hit = this.positionNatureOptions.find(
- (item) =>
- String(item.value) === text ||
- String(item.key) === text ||
- String(item.label) === text ||
- String(item.id) === text,
- );
- return hit ? hit.label : raw;
- },
- async loadPositionNatureOptions() {
- try {
- const items = await getDictByCode("position_nature");
- this.positionNatureOptions = (Array.isArray(items) ? items : [])
- .map((item) => {
- const label = String(item.label ?? item.value ?? "").trim();
- const id = item.id != null && item.id !== "" ? item.id : item.key;
- if (!label || id == null || id === "") return null;
- return {
- label,
- value: id,
- key: item.key != null ? String(item.key) : String(id),
- id,
- };
- })
- .filter(Boolean);
- } catch (error) {
- this.positionNatureOptions = [];
- }
- },
- deptNameOf(deptId) {
- if (!deptId) return "";
- return (
- this.orgList.find((item) => String(item.id) === String(deptId))?.name ||
- ""
- );
- },
- missingApi(action, extra) {
- this.$message.warning(
- `${action}:当前「M02人员入职」文档未提供对应接口${extra ? `,${extra}` : ""
- },页面功能已保留。`,
- );
- },
- async refresh() {
- this.loading = true;
- this.selectedRows = [];
- try {
- await Promise.all([
- this.loadOrganizations(),
- this.loadPositionNatureOptions(),
- ]);
- this.reloadTable();
- } finally {
- this.loading = false;
- }
- },
- applyFilters() {
- this.reloadTable();
- },
- resetFilters() {
- this.filters = emptyFilters();
- this.reloadTable();
- },
- reloadTable() {
- this.$nextTick(() => this.$refs.table?.reload({ page: 1 }));
- },
- async fetchPage({ page, limit }) {
- try {
- const data = await getOnboardingPage({
- pageNum: page,
- size: limit,
- name: this.filters.name,
- organizationId: this.filters.organizationId,
- positionType: this.filters.positionType,
- });
- /* 列表所属机构 / 所属部门直接用接口原始字段 organizationName、targetDepartmentName,
- 不再按 deptId 到组织树里往上反推机构名(会把部门名/上级名当机构名显示) */
- const list = data.list || [];
- this.rows = list;
- this.totalCount = Number(data.count || 0);
- return { list, count: this.totalCount };
- } catch (error) {
- this.rows = [];
- this.totalCount = 0;
- showRequestError(error, "入职列表加载失败");
- return { list: [], count: 0 };
- }
- },
- async loadOrganizations() {
- try {
- /* 机构选项:登录缓存 loginChangeGroupVOList(不再按组织类型筛选) */
- this.legalEntities = getOrgOptions();
- /* 组织列表:仅用于部门级联 */
- const list = await listOrganizations().catch(() => []);
- this.orgList = Array.isArray(list) ? list : [];
- } catch (error) {
- this.orgList = [];
- this.legalEntities = [];
- }
- },
- buildDeptTree(legalId) {
- if (!legalId) {
- this.departmentTree = buildOrganizationTree(this.orgList);
- return;
- }
- const legalKey = String(legalId);
- const underLegal = [];
- const walk = (parentId) => {
- this.orgList.forEach((item) => {
- if (String(item.parentId) === String(parentId)) {
- underLegal.push(item);
- walk(item.id);
- }
- });
- };
- const root = this.orgList.find((item) => String(item.id) === legalKey);
- if (root) underLegal.push(root);
- walk(legalId);
- this.departmentTree = buildOrganizationTree(
- underLegal.length ? underLegal : this.orgList,
- );
- },
- onLegalChange(legalId) {
- this.editing.deptId = null;
- this.editing.department = "";
- this.clearPositionCascade();
- this.sequenceOptions = [];
- const legal = this.legalEntities.find(
- (item) => String(item.id) === String(legalId),
- );
- this.editing.company = legal?.name || "";
- this.buildDeptTree(legalId);
- },
- onDeptChange(deptId) {
- if (this._suppressDeptPositionReset) return;
- this.editing.department = this.deptNameOf(deptId);
- this.clearPositionCascade();
- this.sequenceOptions = [];
- // 手动选部门:该部门没有维护岗位序列时给出指引
- this.loadPositionSequences(deptId, { notifyEmpty: true });
- },
- /** 接口返回可能是数组 / {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) : [];
- },
- /** 清空「岗位序列」及以下的级联字段与下拉选项(与录用管理一致) */
- clearPositionCascade() {
- this.editing.positionSequenceId = "";
- this.editing.positionSequence = "";
- this.editing.positionId = null;
- this.editing.position = "";
- this.editing.positionName = "";
- this.editing.positionType = "";
- this.editing.positionTypeName = "";
- this.editing.positionLevelId = "";
- this.editing.positionLevel = "";
- this.positionOptions = [];
- this.levelOptions = [];
- },
- /**
- * 入职部门 → 岗位序列下拉(GET /fm/tmaindepartmentposition/sequence/list)
- * notifyEmpty:用户主动切换部门时,该部门下没有任何岗位序列则提示去组织机构维护
- */
- async loadPositionSequences(deptId, { notifyEmpty = false } = {}) {
- if (deptId == null || deptId === "") {
- this.sequenceOptions = [];
- this.sequenceLoading = false;
- return;
- }
- this.sequenceLoading = true;
- let requestFailed = false;
- try {
- const data = await getDepartmentPositionSequences(deptId).catch(() => {
- requestFailed = true;
- return [];
- });
- this.sequenceOptions = this.toOptionList(data)
- .filter((item) => item && item.positionSequenceId != null)
- .map((item) => ({
- id: item.positionSequenceId,
- name: item.positionSequence || "",
- }));
- // 接口异常时不误报,只有确实没有配置才提示
- if (notifyEmpty && !requestFailed && !this.sequenceOptions.length) {
- this.$message.warning(
- "该部门下暂无配置岗位信息,请在组织机构的该部门补充岗位信息",
- );
- }
- } catch (error) {
- this.sequenceOptions = [];
- } finally {
- this.sequenceLoading = false;
- }
- },
- /** 入职部门 + 岗位序列 → 岗位下拉 */
- async loadDepartmentPositions() {
- const { deptId, positionSequenceId } = this.editing;
- if (!deptId || !positionSequenceId) {
- this.positionOptions = [];
- return;
- }
- this.positionLoading = true;
- try {
- const data = await getDepartmentPositions({
- deptId,
- positionSequenceId,
- });
- this.positionOptions = this.toOptionList(data)
- .filter((item) => item && item.positionId != null)
- .map((item) => ({
- id: item.positionId,
- name: item.positionName || "",
- }));
- } catch (error) {
- this.positionOptions = [];
- } finally {
- this.positionLoading = false;
- }
- },
- /** 入职部门 + 岗位 + 序列 → 岗位性质(接口返回数组,取第一条自动带出) */
- async loadPositionType() {
- const { deptId, positionId, positionSequenceId } = this.editing;
- if (!deptId || !positionId || !positionSequenceId) return;
- try {
- const data = await getDepartmentPositionType({
- deptId,
- positionId,
- positionSequenceId,
- });
- const first = this.toOptionList(data)[0];
- this.editing.positionType = first?.positionType || "";
- this.editing.positionTypeName = first?.positionTypeName || "";
- } catch (error) {
- this.editing.positionType = "";
- this.editing.positionTypeName = "";
- }
- },
- /** 入职部门 + 岗位 + 序列 + 岗位性质 → 岗位层级下拉 */
- async loadPositionLevels() {
- const { deptId, positionId, positionSequenceId, positionType } =
- this.editing;
- if (!deptId || !positionId || !positionSequenceId || !positionType) {
- this.levelOptions = [];
- return;
- }
- this.levelLoading = true;
- try {
- const data = await getDepartmentPositionLevels({
- deptId,
- positionId,
- positionSequenceId,
- positionType,
- });
- this.levelOptions = this.toOptionList(data)
- .filter((item) => item && item.positionLevelId != null)
- .map((item) => ({
- id: item.positionLevelId,
- name: item.positionLevel || "",
- }));
- } catch (error) {
- this.levelOptions = [];
- } finally {
- this.levelLoading = false;
- }
- },
- /** 选择岗位序列:清空岗位及以下级联,加载该序列下的岗位 */
- async onPositionSequenceChange(positionSequenceId) {
- const hit = this.sequenceOptions.find(
- (item) => String(item.id) === String(positionSequenceId),
- );
- this.clearPositionCascade();
- if (positionSequenceId == null || positionSequenceId === "") return;
- this.editing.positionSequenceId = positionSequenceId;
- this.editing.positionSequence = hit?.name || "";
- await this.loadDepartmentPositions();
- },
- /** 选择岗位:自动带出岗位性质,再加载岗位层级下拉 */
- async onPositionChange(id) {
- const hit = this.positionOptions.find(
- (item) => String(item.id) === String(id),
- );
- this.editing.positionType = "";
- this.editing.positionTypeName = "";
- this.editing.positionLevelId = "";
- this.editing.positionLevel = "";
- this.levelOptions = [];
- if (id == null || id === "") {
- this.editing.positionId = null;
- this.editing.position = "";
- this.editing.positionName = "";
- return;
- }
- this.editing.positionId = id;
- this.editing.position = hit?.name || "";
- this.editing.positionName = hit?.name || "";
- await this.loadPositionType();
- await this.loadPositionLevels();
- },
- /** 岗位层级下拉切换:同步 id 与名称 */
- onPositionLevelChange(value) {
- const hit = this.levelOptions.find(
- (item) => String(item.id) === String(value),
- );
- this.editing.positionLevel = hit ? hit.name : "";
- },
- /** 回显:按 部门 → 序列 → 岗位 → 层级 逐级预热下拉选项(与录用管理一致) */
- async primePositionCascade() {
- const {
- deptId,
- positionSequenceId,
- positionId,
- positionType,
- positionLevelId,
- } = this.editing;
- if (!deptId) {
- this.sequenceOptions = [];
- this.positionOptions = [];
- this.levelOptions = [];
- return;
- }
- await this.loadPositionSequences(deptId);
- if (
- positionSequenceId &&
- !this.sequenceOptions.some(
- (item) => String(item.id) === String(positionSequenceId),
- )
- ) {
- this.sequenceOptions.unshift({
- id: positionSequenceId,
- name: this.editing.positionSequence || "",
- });
- }
- if (!positionSequenceId) {
- this.positionOptions = [];
- this.levelOptions = [];
- return;
- }
- await this.loadDepartmentPositions();
- if (
- positionId &&
- !this.positionOptions.some(
- (item) => String(item.id) === String(positionId),
- )
- ) {
- this.positionOptions.unshift({
- id: positionId,
- name: this.editing.positionName || this.editing.position || "",
- });
- }
- if (!positionId || !positionType) {
- this.levelOptions = [];
- return;
- }
- await this.loadPositionLevels();
- if (
- positionLevelId &&
- !this.levelOptions.some(
- (item) => String(item.id) === String(positionLevelId),
- )
- ) {
- this.levelOptions.unshift({
- id: positionLevelId,
- name: this.editing.positionLevel || "",
- });
- }
- },
- onNameBlur() {
- this.searchHiringByName();
- },
- mapPositionType(raw) {
- return this.resolvePositionType(raw);
- },
- pickHiringMatch(list = [], name = "") {
- const target = String(name || "").trim();
- const rows = (Array.isArray(list) ? list : []).filter((item) => {
- const candidate = String(item.candidateName || item.name || "").trim();
- return !target || candidate === target || candidate.includes(target);
- });
- if (!rows.length) return null;
- const exact = rows.filter(
- (item) =>
- String(item.candidateName || item.name || "").trim() === target,
- );
- const pool = exact.length ? exact : rows;
- return (
- pool.find((item) => item.statusCode === "APPROVED") ||
- pool.find((item) => item.statusCode === "PENDING") ||
- pool[0]
- );
- },
- /** 清空由录用信息带出的字段(保留当前输入的姓名与已选所属机构) */
- clearHiringAutofill() {
- const keepName = this.editing.name;
- const keepLegalId = this.editing.legalId;
- const keepCompany = this.editing.company;
- this.editing.hiringApprovalId = "";
- this.editing.candidateId = "";
- this.editing.userId = "";
- this.editing.hireDate = "";
- this.editing.phone = "";
- this.editing.idNumber = "";
- this.editing.deptId = null;
- this.editing.department = "";
- this.clearPositionCascade();
- this.sequenceOptions = [];
- this.editing.name = keepName;
- /* 所属机构不随姓名检索变化:只有用户手动改机构(onLegalChange)才重置 */
- this.editing.legalId = keepLegalId;
- this.editing.company = keepCompany;
- this.buildDeptTree(keepLegalId || null);
- this.$nextTick(() => this.$refs.form?.clearValidate());
- },
- async searchHiringByName() {
- if (this.readOnly || this.editing.createMode === "invite") return;
- const name = String(this.editing.name || "").trim();
- if (!name || name.length < 2) {
- if (this.editing.hiringApprovalId || this.lastHiringSearchName) {
- this.clearHiringAutofill();
- this.lastHiringSearchName = "";
- }
- return;
- }
- // 姓名未变且刚检索过,避免失焦重复请求;换名则必定重新检索填充
- if (name === this.lastHiringSearchName) return;
- this.hiringSearching = true;
- try {
- const data = await getHiringApprovalPage({
- pageNum: 1,
- size: 20,
- offset: 0,
- keyword: name,
- });
- const toList = (payload) => {
- const raw = Array.isArray(payload?.list)
- ? payload.list
- : Array.isArray(payload?.records)
- ? payload.records
- : Array.isArray(payload)
- ? payload
- : [];
- return raw.map((item) => adaptHiringApproval(item));
- };
- let list = toList(data);
- let hit = this.pickHiringMatch(list, name);
- if (!hit) {
- const approved = await getHiringApprovalPage({
- pageNum: 1,
- size: 50,
- offset: 0,
- approvalStatus: "APPROVED",
- });
- list = toList(approved);
- hit = this.pickHiringMatch(list, name);
- }
- if (hit?.id) {
- try {
- hit = await getHiringApprovalById(hit.id);
- } catch (e) {
- /* 列表数据足够时直接使用 */
- }
- }
- this.lastHiringSearchName = name;
- if (!hit) {
- this.clearHiringAutofill();
- return;
- }
- await this.applyHiringToForm(hit);
- this.$message.success(
- `已带出录用信息:${hit.candidateName || hit.name || name}`,
- );
- } catch (error) {
- showRequestError(error, "检索录用信息失败");
- } finally {
- this.hiringSearching = false;
- }
- },
- async applyHiringToForm(hit = {}) {
- const name = hit.candidateName || hit.name || this.editing.name;
- const hireDate = hit.hireDate || hit.expectedEntryDate || "";
- const phone = hit.phone || hit.mobile || hit.contactMobile || "";
- const idNumber = hit.idNumber || hit.idCard || "";
- const deptId = hit.hiringDeptId || hit.departmentId || hit.deptId || null;
- const positionId = hit.positionId || null;
- const positionName = hit.positionName || "";
- const positionType = this.resolvePositionType(
- hit.positionType || hit.employmentType || hit.staffingType,
- );
- const positionTypeName = this.matchPositionTypeName(
- hit.positionTypeName ||
- hit.employmentType ||
- hit.staffingType,
- );
- this._suppressDeptPositionReset = true;
- try {
- // 先清空再写入,保证「没有的字段」不会残留上一次的值
- this.clearHiringAutofill();
- this.editing.name = name;
- this.editing.hiringApprovalId = hit.id || "";
- this.editing.candidateId = hit.candidateId || hit.userId || "";
- this.editing.hireDate = hireDate;
- this.editing.phone = phone;
- this.editing.idNumber = idNumber;
- this.editing.positionType = positionType;
- this.editing.positionTypeName = positionTypeName;
- // 回显岗位序列/层级(adaptHiringApproval 已保留这些字段)
- this.editing.positionSequence = hit.positionSequence || "";
- this.editing.positionSequenceId = hit.positionSequenceId || "";
- this.editing.positionLevelId = hit.positionLevelId || "";
- this.editing.positionLevel = hit.positionLevel || "";
- if (deptId) {
- this.editing.deptId = deptId;
- this.editing.department =
- hit.hiringDeptName || hit.departmentName || this.deptNameOf(deptId);
- const parents = new Map(
- this.orgList.map((item) => [String(item.id), item]),
- );
- let cursor = this.orgList.find(
- (item) => String(item.id) === String(deptId),
- );
- let root = cursor;
- while (cursor && parents.has(String(cursor.parentId || 0))) {
- cursor = parents.get(String(cursor.parentId));
- if (cursor) root = cursor;
- }
- /* 只有反推到的根节点确实是机构选项时才覆盖所属机构,否则沿用用户已选值 */
- const rootLegal = root?.id
- ? this.legalEntities.find((item) => String(item.id) === String(root.id))
- : null;
- if (rootLegal) {
- this.editing.legalId = root.id;
- this.editing.company = root.name || rootLegal.name || "";
- }
- this.buildDeptTree(this.editing.legalId || null);
- } else {
- /* 录用信息没有目标部门:只清部门相关字段,已选所属机构保持不动 */
- this.editing.deptId = null;
- this.editing.department = "";
- this.buildDeptTree(this.editing.legalId || null);
- }
- this.editing.positionId = positionId;
- this.editing.position = positionName;
- this.editing.positionName = positionName;
- // 与录用管理一致:按 部门 → 序列 → 岗位 → 层级 逐级预热下拉选项
- await this.primePositionCascade();
- const matched = this.positionOptions.find(
- (item) => String(item.id) === String(this.editing.positionId),
- );
- if (matched) {
- this.editing.position = matched.name;
- this.editing.positionName = matched.name;
- }
- this.$nextTick(() => this.$refs.form?.clearValidate());
- } finally {
- this.$nextTick(() => {
- this._suppressDeptPositionReset = false;
- });
- }
- },
- resetAccountState() {
- this.accountChecking = false;
- this.accountChecked = false;
- this.platformAccount = null;
- this.hiringSearching = false;
- this.lastHiringSearchName = "";
- },
- openCreate() {
- this.readOnly = false;
- this.isCreate = true;
- this.dialogTitle = "办理入职";
- this.contractAlreadySaved = false;
- this.contractIdFromStatus = "";
- this.contractStatusCode = "";
- this.editing = empty();
- /* 默认带出当前登录用户所属机构(登录缓存) */
- const currentOrgId = getCurrentOrgId();
- if (currentOrgId) {
- this.editing.legalId = currentOrgId;
- this.editing.company = getOrgName(currentOrgId);
- }
- this.currentStep = "profile";
- this.receiptData = null;
- this.receiptApprovalStarted = false;
- this.resetAccountState();
- this.positionOptions = [];
- this.buildDeptTree(this.editing.legalId);
- this.dialogVisible = true;
- this.$nextTick(() => this.$refs.form?.clearValidate());
- },
- async hydrateEditing(row = {}, { readOnly = false } = {}) {
- const userId = row.userId || row.id;
- this.receiptData = null;
- this.receiptApprovalStarted = false;
- this.editing = {
- ...empty(),
- ...JSON.parse(JSON.stringify(row)),
- createMode: "manual",
- fileIds: this.normalizeFileIds(row),
- };
- this.editing.positionType = this.resolvePositionType(
- row.positionType || row.employmentType || row.staffStatus || row.status,
- );
- this.editing.positionTypeName = this.matchPositionTypeName(
- row.positionTypeName ||
- row.employmentType ||
- row.staffStatus ||
- row.status,
- );
- this.resetAccountState();
- this.buildDeptTree(this.editing.legalId);
- this.dialogVisible = true;
- // 无 userId(如新建)直接用当前 deptId 预热级联下拉,避免重复请求
- if (!userId) {
- this.primePositionCascade();
- return;
- }
- try {
- const detail = await getOnboardingByUserId(userId);
- this.editing = {
- ...this.editing,
- ...detail,
- createMode: "manual",
- fileIds: this.normalizeFileIds(detail),
- };
- this.editing.positionType = this.resolvePositionType(
- detail.positionType || detail.employmentType || detail.staffStatus,
- );
- this.editing.positionTypeName = this.matchPositionTypeName(
- detail.positionTypeName || detail.employmentType || detail.staffStatus,
- );
- this.buildDeptTree(this.editing.legalId);
- // 详情加载完成后按最终 deptId 预热级联下拉(部门 → 序列 → 岗位 → 层级)
- this.primePositionCascade();
- this.markPersonalDirectoryIfArchived(this.editing);
- } catch (error) {
- showRequestError(error, "入职详情加载失败");
- }
- },
- async openEdit(row) {
- this.readOnly = false;
- this.isCreate = false;
- this.dialogTitle = "办理入职";
- this.contractAlreadySaved = false;
- this.contractIdFromStatus = "";
- this.contractStatusCode = "";
- this.contractDetailRow = null;
- // 查询合同流程状态:接口 data 有返回(非空)则锁定到"签署合同"步骤
- const employeeId = row.userId || row.id;
- if (employeeId) {
- try {
- const res = await getEmployeeContractStatus(employeeId);
- // 响应结构:{ hasContract, contracts: [{ contractId, contractStatus, ... }] }
- const hasContract = res?.hasContract || (Array.isArray(res?.contracts) && res.contracts.length > 0);
- if (hasContract) {
- this.contractAlreadySaved = true;
- const firstContract = (res.contracts && res.contracts[0]) || {};
- this.contractIdFromStatus = firstContract.contractId || firstContract.id || "";
- this.contractStatusCode = String(
- firstContract.contractStatus || firstContract.statusCode || "",
- ).toUpperCase();
- // 拉合同完整详情(签署合同节点用)
- this.loadContractDetail(this.contractIdFromStatus);
- }
- } catch (error) {
- // 接口不可用或无合同记录,继续走原有入职办理流程
- }
- }
- // 先 hydrateEditing(把 onboardingProgress 写入 editing),再用 progressToActiveStep 统一算当前步骤
- this.hydrateEditing(row);
- const activeStep = this.progressToActiveStep;
- this.currentStep = activeStep || (this.contractStatusCode === "EFFECTIVE"
- ? "assets"
- : this.contractAlreadySaved
- ? "contract"
- : "profile");
- },
- async openDetail(row) {
- this.readOnly = true;
- this.isCreate = false;
- this.dialogTitle = "入职详情";
- this.contractAlreadySaved = false;
- this.contractIdFromStatus = "";
- this.contractStatusCode = "";
- this.contractDetailRow = null;
- // 详情同样查询合同状态:有合同数据则锁定到"签署合同"步骤
- const employeeId = row.userId || row.id;
- if (employeeId) {
- try {
- const res = await getEmployeeContractStatus(employeeId);
- const hasContract = res?.hasContract || (Array.isArray(res?.contracts) && res.contracts.length > 0);
- if (hasContract) {
- this.contractAlreadySaved = true;
- const firstContract = (res.contracts && res.contracts[0]) || {};
- this.contractIdFromStatus = firstContract.contractId || firstContract.id || "";
- this.contractStatusCode = String(
- firstContract.contractStatus || firstContract.statusCode || "",
- ).toUpperCase();
- this.loadContractDetail(this.contractIdFromStatus);
- }
- } catch (error) {
- // 接口不可用或无合同记录
- }
- }
- this.hydrateEditing(row, { readOnly: true });
- const activeStep = this.progressToActiveStep;
- this.currentStep = activeStep || (this.contractStatusCode === "EFFECTIVE"
- ? "assets"
- : this.contractAlreadySaved
- ? "contract"
- : "profile");
- },
- normalizeFileIds(row = {}) {
- if (Array.isArray(row.fileIds) && row.fileIds.length) return row.fileIds.slice();
- const raw = row.attachmentFileIds || row.fileIds || "";
- if (Array.isArray(raw)) return raw.slice();
- if (!raw) return [];
- return String(raw)
- .split(",")
- .map((item) => item.trim())
- .filter(Boolean);
- },
- nextStep(r) {
- if (r.flow !== "档案已完善" && r.status === "待办理") return "profile";
- if (
- !r.contract ||
- r.contract === "未发起" ||
- r.contract === "未签署" ||
- r.contract === "待签署"
- )
- return "contract";
- if (r.assets !== "已领用") return "assets";
- return "complete";
- },
- stepDone(k) {
- // 当前进度节点也算 done(这样用户切走后它不会变灰,保持已完成的蓝边白底样式)
- if (k === this.progressToActiveStep) return true;
- return this.doneStepSet[k];
- },
- stepPrerequisite(key) {
- const order = ["offer", "profile", "contract", "assets", "complete"];
- const idx = order.indexOf(key);
- if (idx <= 0) return null;
- return order[idx - 1];
- },
- /** 当前步骤是否可点击进入:done 的 + 当前高亮的可进;offer 单独禁用 */
- canEnterStep(key) {
- if (key === "offer") return false;
- if (this.stepDone(key)) return true; // 已完成的节点,点击查看
- if (key === this.currentStep) return true; // 当前高亮节点
- return false;
- },
- /** 灰色禁用样式:既没 done 又不是当前高亮节点(以 UI 的 currentStep 为准) */
- isStepLocked(key) {
- return !this.stepDone(key) && key !== this.currentStep;
- },
- /** 按钮 disabled:offer 和未解锁的 */
- isStepDisabled(key) {
- if (key === "offer") return true;
- return !this.canEnterStep(key);
- },
- stepLockTip(key) {
- const prev = this.stepPrerequisite(key);
- const labels = {
- offer: "Offer发起",
- profile: "员工档案录入",
- contract: "签署合同",
- assets: "物品领用",
- complete: "完成",
- };
- if (key === "profile" && this.contractAlreadySaved) {
- return "合同已保存,员工档案不可再修改";
- }
- if (key === "contract" && !this.isProfileSaved) {
- return "请先保存员工档案录入信息,再进入签署合同";
- }
- if (prev) return `请先完成「${labels[prev] || prev}」`;
- return "请先完成前置步骤";
- },
- stepDesc(k) {
- if (k === "contract" && !this.isProfileSaved && !this.contractAlreadySaved) return "需先保存员工档案";
- if (k === "assets" && !this.stepDone("contract")) return "需先签署合同";
- if (k === "complete" && !this.stepDone("assets")) return "需先物品领用";
- return {
- offer: "已发起",
- profile: this.contractAlreadySaved
- ? "已保存"
- : this.isProfileSaved
- ? this.editing.flow || "已保存"
- : "待保存",
- contract:
- this.contractStatusCode === "EFFECTIVE"
- ? "已生效"
- : this.editing.contract || "未签署",
- // 物品领用审批中:步骤副标题显示"待审批"(已完成则仍显示已领用)
- assets:
- this.editing.assets === "已领用"
- ? "已领用"
- : this.receiptApproving
- ? "待审批"
- : this.editing.assets || "未领用",
- complete: this.editing.status === "已完成" ? "已完成" : "待确认",
- }[k];
- },
- handleStep(step) {
- // Offer 默认已点亮,不可点击切换
- if (step.key === "offer") return;
- if (!this.canEnterStep(step.key)) {
- return this.$message.warning(this.stepLockTip(step.key));
- }
- if (this.readOnly) {
- this.currentStep = step.key;
- return;
- }
- if (step.key === "profile") {
- this.currentStep = "profile";
- return;
- }
- if (step.key === "contract") {
- this.currentStep = "contract";
- return;
- }
- if (step.key === "assets") {
- this.currentStep = "assets";
- return;
- }
- if (step.key === "complete") {
- this.currentStep = "complete";
- }
- },
- /** 物品领用单加载完成:已提交的领用单回写为已领用 */
- onReceiptLoaded(receipt) {
- this.receiptData = receipt || null;
- if (receipt && String(receipt.receiptStatus || "").toUpperCase() === "COMPLETED") {
- this.editing.assets = "已领用";
- }
- },
- /** 查看物品领用审批记录(复用 BPM 流程详情弹框) */
- openReceiptApprovalRecord() {
- const row = { ...this.editing, ...(this.receiptData || {}) };
- this.$refs.approvalRecordDialog?.open(row);
- },
- /** 物品领用单提交成功:入职流程完成 */
- onReceiptCompleted() {
- this.editing.assets = "已领用";
- this.editing.status = "已完成";
- this.reloadTable();
- },
- async saveReceipt() {
- const panel = this.$refs.receiptPanel;
- if (!panel) return;
- this.receiptSaving = true;
- try {
- await panel.saveDraft();
- } finally {
- this.receiptSaving = false;
- }
- },
- async submitReceipt() {
- const panel = this.$refs.receiptPanel;
- if (!panel) return;
- this.receiptSubmitting = true;
- try {
- await panel.submit();
- } finally {
- this.receiptSubmitting = false;
- }
- },
- /** 物品领用提交:领用单已先保存/修改完成,这里弹出公共的提交审批弹框 */
- openReceiptApproval(receipt) {
- const id = receipt?.id;
- if (!id) {
- this.$message.warning("领用单保存失败,无法提交审批");
- return;
- }
- this.approvalSource = "receipt";
- // 提交成功后立即锁定:隐藏保存/提交,仅保留审批记录
- this.receiptApprovalStarted = true;
- this.processSubmitDialogFlag = true;
- this.$nextTick(() => {
- const currentUser = getCurrentUser() || {};
- this.$refs.processSubmitDialogRef?.init({
- businessId: id,
- businessKey: RECEIPT_APPROVAL_BUSINESS_KEY,
- formCreateUserId:
- receipt.createUserId || currentUser.id || currentUser.userId || "",
- variables: {
- businessCode: String(id),
- businessName: "入职物品领用",
- businessType: "入职物品领用审批",
- },
- });
- });
- },
- mergeSavedResult(saved) {
- const userId = saved.userId || this.editing.userId;
- const next = {
- ...this.editing,
- ...saved,
- userId,
- id: userId || saved.id || this.editing.id,
- onboardingId: saved.onboardingId || userId || this.editing.onboardingId,
- jobNumber: saved.jobNumber || this.editing.jobNumber || "",
- name: saved.name || this.editing.name,
- company: saved.company || this.editing.company,
- /* 只认接口返回的部门名,其次用刚在弹框里选的那个(同一份数据);
- 不再按 deptId 去组织树反查名字顶替 */
- department: saved.department || this.editing.department,
- position:
- saved.position ||
- saved.positionName ||
- this.editing.position,
- createMode: this.editing.createMode,
- positionType: this.resolvePositionType(
- saved.positionType ||
- saved.employmentType ||
- saved.staffStatus ||
- this.editing.positionType,
- ),
- positionTypeName: this.matchPositionTypeName(
- saved.positionTypeName ||
- saved.employmentType ||
- saved.staffStatus ||
- this.editing.positionTypeName ||
- this.editing.positionType,
- ),
- fileIds: Array.isArray(saved.fileIds)
- ? saved.fileIds
- : Array.isArray(this.editing.fileIds)
- ? this.editing.fileIds
- : [],
- };
- this.editing = next;
- this.isCreate = false;
- return next;
- },
- persistOnboarding() {
- return new Promise((resolve, reject) => {
- this.$refs.form.validate(async (valid) => {
- if (!valid) {
- reject(new Error("表单校验未通过"));
- return;
- }
- this.saving = true;
- try {
- const payload = {
- ...this.editing,
- employmentTypeLabel:
- this.editing.positionTypeName ||
- this.positionTypeLabel(this.editing.positionType),
- };
- const userIdBefore = this.editing.userId;
- let alreadyArchived = hasJobNumber(this.editing);
- if (userIdBefore && !alreadyArchived) {
- try {
- const detail = await getOnboardingByUserId(userIdBefore);
- alreadyArchived = hasJobNumber(detail);
- if (detail.jobNumber) {
- this.editing.jobNumber = detail.jobNumber;
- }
- this.markPersonalDirectoryIfArchived({
- ...detail,
- userId: userIdBefore,
- });
- } catch (error) {
- /* 以保存前表单中的工号为准 */
- }
- }
- const saved = userIdBefore
- ? await updateOnboarding(payload)
- : await createOnboarding(payload);
- const merged = this.mergeSavedResult(saved);
- await this.maybeCreatePersonalDirectory(merged, alreadyArchived);
- this.reloadTable();
- resolve(merged);
- } catch (error) {
- showRequestError(error, "保存入职办理失败");
- reject(error);
- } finally {
- this.saving = false;
- }
- });
- });
- },
- rememberPersonalDirectoryUser(userId) {
- const key = String(userId || "").trim();
- if (!key) return;
- if (!this.personalDirectoryUserIds.includes(key)) {
- this.personalDirectoryUserIds.push(key);
- }
- },
- markPersonalDirectoryIfArchived(record = {}) {
- if (!hasJobNumber(record)) return;
- this.rememberPersonalDirectoryUser(record.userId || record.id);
- },
- /** 创建成功才持久化;失败不落盘,保证下次仍可重试 */
- rememberCreatedDirectoryUser(userId) {
- const key = String(userId || "").trim();
- if (!key || this.createdDirectoryUsers.includes(key)) return;
- this.createdDirectoryUsers.push(key);
- writeCreatedDirectoryUsers(this.createdDirectoryUsers);
- },
- async maybeCreatePersonalDirectory(record = {}, existedJobNumber = false) {
- const userId = record.userId || record.id;
- if (!userId) return;
- const key = String(userId);
- // 打开详情时已有工号:员工已入库归档,禁止再创建个人文档区
- if (existedJobNumber) {
- this.rememberPersonalDirectoryUser(key);
- return;
- }
- if (this.personalDirectoryUserIds.includes(key)) return;
- // 已成功创建过(含刷新页面、重开弹框后再次保存的场景)不再重复创建
- if (this.createdDirectoryUsers.includes(key)) return;
- try {
- await createPersonalDirectory(userId);
- this.rememberPersonalDirectoryUser(key);
- this.rememberCreatedDirectoryUser(key);
- } catch (error) {
- showRequestError(error, "创建个人文档区失败");
- }
- },
- async saveDraft() {
- try {
- await this.persistOnboarding();
- this.$message.success("入职资料已保存");
- } catch (error) {
- /* 已提示 */
- }
- },
- async goNextStep() {
- try {
- await this.persistOnboarding();
- try {
- await this.$confirm(
- "录用信息已保存,是否跳转到合同签署页面?",
- "提示",
- {
- confirmButtonText: "确定",
- cancelButtonText: "取消",
- type: "info",
- },
- );
- } catch (error) {
- this.currentStep = "profile";
- return;
- }
- this.jumpToContractCreate();
- } catch (error) {
- /* 保存失败已提示 */
- }
- },
- jumpToContractCreate() {
- const empId =
- this.platformAccount?.id ||
- this.editing.candidateId ||
- this.editing.userId ||
- "";
- this.navigateToContractCreate({
- employeeId: empId ? String(empId) : "",
- employeeName: this.editing.name || "",
- employeeNo: this.editing.employeeNo || "",
- phone: this.editing.phone || "",
- departmentId: this.editing.deptId ? String(this.editing.deptId) : "",
- department:
- this.editing.department ||
- this.deptNameOf(this.editing.deptId) ||
- "",
- positionId: this.editing.positionId
- ? String(this.editing.positionId)
- : "",
- positionName:
- this.editing.position || this.editing.positionName || "",
- legalEntityId: this.editing.legalId
- ? String(this.editing.legalId)
- : "",
- legalEntity: this.editing.company || "",
- });
- },
- navigateToContractCreate(query = {}) {
- this.dialogVisible = false;
- const path = resolveContractManagementPath(this);
- const go = () => {
- const location = {
- path,
- query: {
- action: "create",
- ...query,
- },
- };
- const open = this.$route.path === path ? "replace" : "push";
- this.$router[open](location).catch((err) => {
- if (err && err.name === "NavigationDuplicated") return;
- if (path !== "/contract-management") {
- return this.$router
- .push({ ...location, path: "/contract-management" })
- .catch(() => {
- this.$message.error("无法打开合同管理,请确认该菜单已配置");
- });
- }
- this.$message.error("无法打开合同管理,请确认该菜单已配置");
- });
- };
- this.$nextTick(go);
- },
- /** 根据合同ID拉取完整详情(入职签署合同节点展示) */
- async loadContractDetail(contractId) {
- if (!contractId) {
- this.contractDetailRow = null;
- return;
- }
- try {
- const detail = await getContractById(contractId);
- this.contractDetailRow = adaptContract(detail || {});
- } catch (error) {
- this.contractDetailRow = null;
- }
- },
- /** 跳转合同管理 - 查看合同详情 */
- goViewContract() {
- const contractId = this.contractIdFromStatus;
- if (!contractId) {
- this.$message.warning("未获取到合同ID,无法查看合同详情");
- return;
- }
- this.dialogVisible = false;
- const path = resolveContractManagementPath(this);
- const location = {
- path,
- query: {
- action: "view",
- contractId: String(contractId),
- employeeId: String(this.editing.userId || this.editing.id || ""),
- employeeName: this.editing.name || "",
- },
- };
- const open = this.$route.path === path ? "replace" : "push";
- this.$router[open](location).catch((err) => {
- if (err && err.name === "NavigationDuplicated") return;
- if (path !== "/contract-management") {
- return this.$router
- .push({ ...location, path: "/contract-management" })
- .catch(() => {
- this.$message.error("无法打开合同管理,请确认该菜单已配置");
- });
- }
- this.$message.error("无法打开合同管理,请确认该菜单已配置");
- });
- },
- launchApproval(row) {
- const id = row?.userId || row?.id;
- if (!id) {
- this.$message.warning("缺少员工主键,无法提交审批");
- return;
- }
- this.currentLaunchRow = row;
- this.processSubmitDialogFlag = true;
- this.$nextTick(() => {
- const currentUser = getCurrentUser() || {};
- const params = {
- businessId: id,
- businessKey: ONBOARDING_APPROVAL_BUSINESS_KEY,
- formCreateUserId:
- row.createUserId || currentUser.id || currentUser.userId || "",
- variables: {
- businessCode: row.employeeNo || row.invitationNo || "",
- businessName: row.name || row.employeeName || "",
- businessType: "入职审批",
- departmentName: row.department || "",
- positionName: row.position || row.positionName || "",
- },
- };
- this.$refs.processSubmitDialogRef?.init(params);
- });
- },
- onProcessSubmitReload() {
- if (this.approvalSource === "receipt") {
- this.approvalSource = null;
- // 审批提交后刷新领用单(状态以后端为准:审批中 / 已完成)
- this.$refs.receiptPanel?.load();
- this.refresh();
- return;
- }
- this.currentLaunchRow = null;
- this.refresh();
- },
- async checkPlatformAccount() {
- const keyword =
- this.editing.phone || this.editing.name || this.editing.idNumber;
- if (!keyword) {
- this.$message.warning("请先完善候选人手机号、姓名或身份证号");
- return;
- }
- this.accountChecking = true;
- this.accountChecked = false;
- this.platformAccount = null;
- try {
- const data = await searchUsersByKeyword(keyword, {
- pageNum: 1,
- size: 30,
- });
- const list = Array.isArray(data?.list) ? data.list : [];
- const phone = String(this.editing.phone || "").trim();
- const idNumber = String(this.editing.idNumber || "").trim().toUpperCase();
- const name = String(this.editing.name || "").trim();
- const hit =
- list.find((u) => phone && String(u.phone || "") === phone) ||
- list.find((u) => {
- const card = String(u.idCard || u.idNumber || "")
- .trim()
- .toUpperCase();
- return idNumber && card === idNumber;
- }) ||
- list.find((u) => name && String(u.name || "").trim() === name) ||
- null;
- if (!hit) {
- this.accountChecked = true;
- this.$message.warning("未检索到平台账号");
- return;
- }
- const deptId =
- (hit.deptIds && hit.deptIds[0]) || hit.deptId || hit.groupId || null;
- const org = this.orgList.find(
- (item) => String(item.id) === String(deptId || ""),
- );
- this.platformAccount = {
- id: hit.id,
- name: hit.name || this.editing.name,
- employeeNo: hit.jobNumber || "",
- phone: hit.phone || this.editing.phone,
- department: hit.deptName || org?.name || this.editing.department,
- departmentId: deptId,
- position: hit.postName || this.editing.position,
- positionId: hit.postId || this.editing.positionId,
- legalEntity: this.editing.company,
- legalEntityId: this.editing.legalId,
- };
- this.accountChecked = true;
- this.$message.success("已找到平台账号");
- } catch (error) {
- showRequestError(error, "检索平台账号失败");
- } finally {
- this.accountChecking = false;
- }
- },
- goSignContract() {
- if (!this.platformAccount?.id) {
- this.$message.warning("请先检索并确认候选人平台账号");
- return;
- }
- const emp = this.platformAccount;
- this.navigateToContractCreate({
- employeeId: String(emp.id),
- employeeName: emp.name || "",
- employeeNo: emp.employeeNo || "",
- phone: emp.phone || "",
- departmentId: emp.departmentId ? String(emp.departmentId) : "",
- department: emp.department || "",
- positionId: emp.positionId ? String(emp.positionId) : "",
- positionName: emp.position || "",
- legalEntityId: emp.legalEntityId ? String(emp.legalEntityId) : "",
- legalEntity: emp.legalEntity || "",
- });
- },
- /** 入职进度已完成(终态):操作栏是否还提供「办理」的唯一判据(onboardingProgress) */
- isProgressCompleted(row) {
- return String(row?.onboardingProgress || "").toUpperCase() === "COMPLETED";
- },
- /** 只有草稿才能删除:未进入审批流程的入职单(办理入职 / 合同草稿) */
- canDelete(row) {
- const progress = String(row?.onboardingProgress || "").toUpperCase();
- return progress === "ONBOARDING" || progress === "CONTRACT_DRAFT";
- },
- async deleteRecord(row) {
- if (!this.canDelete(row)) {
- this.$message.warning("只有草稿状态的入职单可以删除");
- return;
- }
- const userId = row?.userId || row?.id;
- if (!userId) {
- this.$message.warning("缺少员工主键,无法删除");
- return;
- }
- try {
- await this.$confirm("确认删除该入职记录?", "提示", { type: "warning" });
- await deleteOnboarding(userId);
- this.$message.success("已删除");
- this.reloadTable();
- } catch (error) {
- if (error === "cancel" || error === "close") return;
- showRequestError(error, "删除入职记录失败");
- }
- },
- copyInviteLink() {
- const text = this.inviteLink;
- if (!text) return;
- if (navigator.clipboard?.writeText) {
- navigator.clipboard.writeText(text).then(
- () => this.$message.success("链接已复制"),
- () => this.$message.warning("复制失败,请手动复制"),
- );
- return;
- }
- this.$message.info(text);
- },
- statusClass(s) {
- if (s === "离职" || s === "已完成") return "completed";
- if (["全职", "兼职", "实习", "正式", "试用", "已登记"].includes(s)) {
- return "processing";
- }
- return { 待办理: "pending", 办理中: "processing", 已完成: "completed" }[s];
- },
- /** 合同状态 → CSS 类(复用合同管理的配色) */
- contractStatusClass(s) {
- const map = {
- 草稿: "draft", 审批中: "pending", 待签署: "pending", 待签: "pending", 签署中: "signing",
- 已签待生效: "pending", 待生效: "pending", 生效: "active", 生效中: "active",
- 已变更: "changed", 已续签: "active", 已解除: "terminated", 已终止: "terminated",
- 已作废: "expired", 已取消: "expired", 已归档: "archived", 试用期: "trial", 已到期: "expired", 已过期: "expired",
- };
- return map[s] || "pending";
- },
- badgeClass(v) {
- return v === "档案已完善" || v === "已登记" ? "success" : "warning";
- },
- statusTag(s) {
- if (s === "已完成" || s === "正式" || s === "全职") return "success";
- if (s === "办理中" || s === "试用" || s === "实习" || s === "兼职") {
- return "warning";
- }
- return "info";
- },
- statusTagType(s) {
- return this.statusTag(s);
- },
- /** 办理进度枚举(onboardingProgress)→ 流程进度标签配色:
- * 草稿类(ONBOARDING / CONTRACT_DRAFT / ITEM_RECEIPT_DRAFT)灰、
- * 审批中(CONTRACT_APPROVING / ITEM_RECEIPT_APPROVING)橙、
- * 合同审批通过(CONTRACT_APPROVED)主色蓝(流程未走完)、
- * 完成(COMPLETED)绿;与「岗位性质」标签(实底)区分,这里用 effect="plain" 描边样式 */
- progressTagType(progress) {
- const map = {
- ONBOARDING: "info",
- CONTRACT_DRAFT: "info",
- ITEM_RECEIPT_DRAFT: "info",
- CONTRACT_APPROVING: "warning",
- ITEM_RECEIPT_APPROVING: "warning",
- CONTRACT_APPROVED: "",
- COMPLETED: "success",
- };
- const code = String(progress || "").toUpperCase();
- return map[code] != null ? map[code] : "info";
- },
- },
- };
- </script>
- <style src="../../../styles/views/onboardingHandling/index.scss" scoped lang="scss"></style>
- <!-- 复用合同管理的详情样式(非 scoped,否则 cm-create-grid 等穿透不过) -->
- <style lang="scss" src="../../../styles/views/contractManagement/index.scss"></style>
|