index.vue 88 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258
  1. <template>
  2. <div class="ele-body" v-loading="loading">
  3. <el-card shadow="never">
  4. <el-form label-width="80px" class="ele-form-search" @keyup.enter.native="applyFilters" @submit.native.prevent>
  5. <el-row :gutter="15">
  6. <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
  7. <el-form-item label="姓名">
  8. <el-input v-model.trim="filters.name" clearable placeholder="请输入" />
  9. </el-form-item>
  10. </el-col>
  11. <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
  12. <el-form-item label="所属机构">
  13. <el-select v-model="filters.organizationId" clearable filterable placeholder="请选择" style="width: 100%">
  14. <el-option v-for="item in legalEntities" :key="String(item.id)" :label="item.name" :value="item.id" />
  15. </el-select>
  16. </el-form-item>
  17. </el-col>
  18. <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
  19. <el-form-item label="岗位性质">
  20. <el-select v-model="filters.positionType" clearable placeholder="请选择" style="width: 100%">
  21. <el-option v-for="item in statusOptions" :key="String(item.value)" :label="item.label"
  22. :value="item.value" />
  23. </el-select>
  24. </el-form-item>
  25. </el-col>
  26. <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
  27. <div class="ele-form-actions">
  28. <el-button type="primary" icon="el-icon-search" class="ele-btn-icon" @click="applyFilters">
  29. 查询
  30. </el-button>
  31. <el-button type="primary" icon="el-icon-refresh" class="ele-btn-icon" @click="resetFilters">
  32. 重置
  33. </el-button>
  34. </div>
  35. </el-col>
  36. </el-row>
  37. </el-form>
  38. <ele-pro-table ref="table" row-key="userId" :columns="columns" :datasource="fetchPage" :need-page="true"
  39. :page-size="20" :cache-key="$tableCacheKey('hr-onboarding-handling')" class="onboarding-table" @selection-change="selectedRows = $event">
  40. <template v-slot:toolbar>
  41. <el-button v-if="$hasPermission('main:hrOnboarding:save')" type="primary" icon="el-icon-plus"
  42. class="ele-btn-icon" @click="openCreate">
  43. 新建
  44. </el-button>
  45. </template>
  46. <!-- 点员工姓名开详情,替代原操作列「查看」入口 -->
  47. <template v-slot:name="{ row }">
  48. <el-link v-if="row.name" type="primary" :underline="false" @click.stop="openDetail(row)">
  49. {{ row.name }}
  50. </el-link>
  51. <span v-else>-</span>
  52. </template>
  53. <!-- 组织信息拆两列(2026-10-29 用户要求):所属机构 = organizationName、
  54. 所属部门 = targetDepartmentName,直接取接口字段,没返回就显示 -,不做兜底 -->
  55. <!-- 流程进度:文案直接用 onboardingProgressValue,配色按 onboardingProgress 枚举判断 -->
  56. <template v-slot:flow="{ row }">
  57. <el-tag v-if="row.onboardingProgressValue" size="mini" effect="plain"
  58. :type="progressTagType(row.onboardingProgress)">
  59. {{ row.onboardingProgressValue }}
  60. </el-tag>
  61. </template>
  62. <!-- 岗位性质:只认接口的岗位性质名称快照(positionTypeName),取不到就留空 -->
  63. <template v-slot:positionType="{ row }">
  64. <el-tag v-if="row.positionTypeName" size="mini" :type="statusTagType(row.positionTypeName)">
  65. {{ row.positionTypeName }}
  66. </el-tag>
  67. </template>
  68. <!-- 操作栏:能否办理/删除一律由 onboardingProgress 枚举判断 -->
  69. <template v-slot:action="{ row }">
  70. <el-link v-if="!isProgressCompleted(row) && $hasPermission('main:hrOnboarding:update')" type="primary"
  71. :underline="false" icon="el-icon-edit" @click.stop="openEdit(row)">
  72. 办理
  73. </el-link>
  74. <el-link v-if="canDelete(row) && $hasPermission('main:hrOnboarding:delete')" type="danger"
  75. :underline="false" icon="el-icon-delete" @click.stop="deleteRecord(row)">
  76. 删除
  77. </el-link>
  78. </template>
  79. <template slot="empty">
  80. <el-empty description="暂无数据" />
  81. </template>
  82. </ele-pro-table>
  83. </el-card>
  84. <ele-modal :title="dialogTitle" :visible.sync="dialogVisible" width="1280px" custom-class="onboarding-modal"
  85. :close-on-click-modal="false" append-to-body>
  86. <el-form ref="form" :model="editing" :rules="rules" :disabled="currentStep === 'profile' && !profileEditable"
  87. label-width="100px" class="onboarding-form">
  88. <div class="dialog-summary">
  89. <span class="avatar">{{ (editing.name || "新").slice(0, 1) }}</span>
  90. <div>
  91. <h3>{{ editing.name || "新建待入职人员" }}</h3>
  92. <p>
  93. {{
  94. editing.employeeNo ||
  95. editing.invitationNo ||
  96. "完善资料并按顺序推进办理步骤"
  97. }}
  98. </p>
  99. </div>
  100. <el-tag :type="statusTag(editing.status)" effect="plain">{{
  101. editing.status || "待办理"
  102. }}</el-tag>
  103. </div>
  104. <el-divider>办理步骤</el-divider>
  105. <div class="step-grid">
  106. <button v-for="step in steps" :key="step.key" type="button" class="step-card" :class="{
  107. done: stepDone(step.key),
  108. active: currentStep === step.key,
  109. locked: isStepLocked(step.key),
  110. }" :disabled="isStepDisabled(step.key)" @click="handleStep(step)">
  111. <span><i :class="step.icon"></i></span>
  112. <div>
  113. <strong>{{ step.label }}</strong>
  114. <small>{{ stepDesc(step.key) }}</small>
  115. </div>
  116. <i class="el-icon-arrow-right"></i>
  117. </button>
  118. </div>
  119. <!-- 第二步:录入 -->
  120. <template v-if="currentStep === 'profile'">
  121. <el-divider>基本档案</el-divider>
  122. <div class="form-grid">
  123. <el-form-item label="办理方式" prop="createMode">
  124. <el-radio-group v-model="editing.createMode" :disabled="!isCreate && !!editing.userId">
  125. <el-radio label="manual">手动添加</el-radio>
  126. <el-radio label="invite">二维码邀请</el-radio>
  127. </el-radio-group>
  128. </el-form-item>
  129. </div>
  130. <!-- 二维码邀请:不填入职资料,只展示邀请二维码(二维码图待后端就绪后接入,暂留空位) -->
  131. <div v-if="editing.createMode === 'invite'" class="invite-qr-panel">
  132. <div v-if="qrImageUrl" class="invite-qr-card">
  133. <img :src="qrImageUrl" alt="入职邀请二维码" />
  134. <p>请候选人扫码填写入职资料</p>
  135. <el-input v-if="inviteLink" :value="inviteLink" readonly size="small" class="invite-qr-link">
  136. <el-button slot="append" icon="el-icon-document-copy" @click="copyInviteLink">复制</el-button>
  137. </el-input>
  138. </div>
  139. </div>
  140. <div v-else class="form-grid">
  141. <el-form-item label="姓名" prop="name">
  142. <el-input v-model.trim="editing.name" placeholder="输入姓名后自动带出录用信息" @blur="onNameBlur"
  143. @keyup.enter.native="searchHiringByName">
  144. <i v-if="hiringSearching" slot="suffix" class="el-icon-loading" />
  145. </el-input>
  146. </el-form-item>
  147. <el-form-item label="入职日期" prop="hireDate">
  148. <el-date-picker v-model="editing.hireDate" value-format="yyyy-MM-dd" placeholder="计划入职日期" />
  149. </el-form-item>
  150. <el-form-item label="所属机构" prop="legalId">
  151. <el-select v-model="editing.legalId" filterable clearable placeholder="请选择所属机构" @change="onLegalChange">
  152. <el-option v-for="item in legalEntities" :key="item.id" :label="item.name" :value="item.id" />
  153. </el-select>
  154. </el-form-item>
  155. <el-form-item label="目标部门" prop="deptId">
  156. <organization-select v-model="editing.deptId" :data="departmentTree" :disabled="!editing.legalId"
  157. :placeholder="editing.legalId ? '请选择部门' : '请先选择所属机构'" @input="onDeptChange" />
  158. </el-form-item>
  159. <el-form-item label="岗位序列" prop="positionSequenceId">
  160. <el-select v-model="editing.positionSequenceId" filterable clearable :loading="sequenceLoading"
  161. :disabled="!editing.deptId" :placeholder="editing.deptId ? '请选择岗位序列' : '请先选择目标部门'
  162. " @change="onPositionSequenceChange">
  163. <el-option v-for="item in sequenceOptions" :key="item.id" :label="item.name" :value="item.id" />
  164. </el-select>
  165. </el-form-item>
  166. <el-form-item label="岗位" prop="positionId">
  167. <el-select v-model="editing.positionId" filterable clearable :loading="positionLoading"
  168. :disabled="!editing.positionSequenceId" :placeholder="editing.positionSequenceId ? '请选择岗位' : '请先选择岗位序列'
  169. " @change="onPositionChange">
  170. <el-option v-for="item in positionOptions" :key="item.id" :label="item.name" :value="item.id" />
  171. </el-select>
  172. </el-form-item>
  173. <el-form-item label="岗位性质" prop="positionType">
  174. <el-input v-model="editing.positionTypeName" readonly placeholder="选择岗位后自动带出" />
  175. </el-form-item>
  176. <el-form-item label="岗位层级" prop="positionLevelId">
  177. <el-select v-model="editing.positionLevelId" clearable filterable :loading="levelLoading"
  178. :disabled="!editing.positionId" :placeholder="editing.positionId ? '请选择岗位层级' : '请先选择岗位'
  179. " style="width: 100%" @change="onPositionLevelChange">
  180. <el-option v-for="item in levelOptions" :key="item.id || item.name" :label="item.name"
  181. :value="item.id" />
  182. </el-select>
  183. </el-form-item>
  184. <el-form-item label="手机号" prop="phone">
  185. <el-input v-model.trim="editing.phone" maxlength="11" placeholder="请输入 11 位手机号码" />
  186. </el-form-item>
  187. <el-form-item label="身份证号" prop="idNumber">
  188. <el-input v-model.trim="editing.idNumber" maxlength="18" placeholder="请输入 18 位身份证号" />
  189. </el-form-item>
  190. <el-form-item label="附件" class="form-grid-full">
  191. <fileMain v-model="editing.fileIds" :type="readOnly ? 'view' : ''" size="small" />
  192. <p class="attach-hint">支持上传简历、证件等入职材料,可点击查看已传附件</p>
  193. </el-form-item>
  194. </div>
  195. </template>
  196. <!-- 第三步:签署合同 → 合同详情视图(复用合同管理 adaptContract) -->
  197. <template v-else-if="currentStep === 'contract'">
  198. <el-divider>合同信息</el-divider>
  199. <!-- 有完整合同详情:按合同管理详情样式展示 -->
  200. <div v-if="contractDetailRow" class="cm-create-page cm-detail-page">
  201. <!-- 员工/合同状态头 -->
  202. <div class="cm-create-employee-card cm-detail-employee">
  203. <span class="cm-employee-avatar">{{ contractDetailRow.employeeName ?
  204. contractDetailRow.employeeName.slice(0, 1) : '?' }}</span>
  205. <div class="cm-employee-info">
  206. <b>员工姓名:{{ contractDetailRow.employeeName }}<span class="cm-employee-no">员工工号:{{
  207. contractDetailRow.employeeNo }}</span></b>
  208. </div>
  209. <div class="cm-detail-status">
  210. <small>合同状态</small>
  211. <span class="cm-status" :class="contractStatusClass(contractDetailRow.status)">{{
  212. contractDetailRow.status }}</span>
  213. </div>
  214. </div>
  215. <!-- 合同基本信息 -->
  216. <div class="cm-create-section">
  217. <div class="cm-create-section-title">合同基本信息</div>
  218. <div class="cm-create-grid">
  219. <div class="cm-compare-item"><span class="cm-compare-label">合同编号:</span><span
  220. class="cm-compare-value">{{ contractDetailRow.contractNo }}</span></div>
  221. <div class="cm-compare-item"><span class="cm-compare-label">合同类型:</span><span
  222. class="cm-compare-value">{{ contractDetailRow.contractType }}</span></div>
  223. <div class="cm-compare-item"><span class="cm-compare-label">所属机构:</span><span
  224. class="cm-compare-value">{{ contractDetailRow.legalEntity }}</span></div>
  225. <div class="cm-compare-item"><span class="cm-compare-label">所属部门:</span><span
  226. class="cm-compare-value">{{ contractDetailRow.department }}</span></div>
  227. <div class="cm-compare-item"><span class="cm-compare-label">岗位名称:</span><span
  228. class="cm-compare-value">{{ contractDetailRow.positionName || editing.position || '-' }}</span>
  229. </div>
  230. <div class="cm-compare-item"><span class="cm-compare-label">岗位层级:</span><span
  231. class="cm-compare-value">{{ contractDetailRow.level || '-' }}</span></div>
  232. <!-- <div class="cm-compare-item"><span class="cm-compare-label">直属上级:</span><span class="cm-compare-value">{{ contractDetailRow.manager || '-' }}</span></div> -->
  233. </div>
  234. </div>
  235. <!-- 合同期限与薪资 -->
  236. <div class="cm-create-section">
  237. <div class="cm-create-section-title">合同期限与薪资</div>
  238. <div class="cm-create-grid">
  239. <div class="cm-compare-item"><span class="cm-compare-label">合同起始:</span><span
  240. class="cm-compare-value">{{ contractDetailRow.startDate || '-' }}</span></div>
  241. <div class="cm-compare-item"><span class="cm-compare-label">合同终止:</span><span
  242. class="cm-compare-value">{{ contractDetailRow.endDate || '长期' }}</span></div>
  243. <div class="cm-compare-item"><span class="cm-compare-label">月薪资:</span><span
  244. class="cm-compare-value is-highlight">{{ contractDetailRow.salary ? ('¥ ' +
  245. Number(contractDetailRow.salary).toLocaleString() + ' 元/月') : '待定' }}</span></div>
  246. <!-- 试用期:接口回显 probationMonths(月数),0/空 = 无试用期 -->
  247. <div class="cm-compare-item"><span class="cm-compare-label">试用期:</span><span
  248. class="cm-compare-value">{{ contractDetailRow.probationText }}</span></div>
  249. </div>
  250. </div>
  251. <!-- 签署信息 -->
  252. <div class="cm-create-section">
  253. <div class="cm-create-section-title">签署信息</div>
  254. <div class="cm-create-grid">
  255. <div class="cm-compare-item"><span class="cm-compare-label">签署方式:</span><span
  256. class="cm-compare-value">{{ contractDetailRow.signMethod || '-' }}</span></div>
  257. <!-- <div class="cm-compare-item"><span class="cm-compare-label">签署状态:</span><span class="cm-compare-value">{{ contractDetailRow.signingStatusLabel || '-' }}</span></div> -->
  258. <div v-if="contractDetailRow.flowSignStatus" class="cm-compare-item"><span
  259. class="cm-compare-label">流程节点电子签章:</span><span class="cm-compare-value">{{
  260. contractDetailRow.flowSignStatusLabel || '-' }}</span></div>
  261. <div class="cm-compare-item"><span class="cm-compare-label">合同附件:</span><span class="cm-compare-value">
  262. <fileMain v-if="contractDetailRow.attachmentFileIds && contractDetailRow.attachmentFileIds.length"
  263. :value="contractDetailRow.attachmentFileIds" type="view" size="small" />
  264. <span v-else class="cm-detail-muted">暂无附件</span>
  265. </span></div>
  266. <div class="cm-compare-item"><span class="cm-compare-label">签订日期:</span><span
  267. class="cm-compare-value">{{ contractDetailRow.signDate || contractDetailRow.createdAt || '-'
  268. }}</span></div>
  269. <div class="cm-compare-item"><span class="cm-compare-label">创建时间:</span><span
  270. class="cm-compare-value">{{ contractDetailRow.createdAt || '-' }}</span></div>
  271. </div>
  272. </div>
  273. </div>
  274. <!-- 合同详情加载中/为空:用入职信息做简版展示 -->
  275. <div v-else class="info-display-grid">
  276. <div class="info-item">
  277. <span class="info-label">姓名</span>
  278. <strong>{{ editing.name || "-" }}</strong>
  279. </div>
  280. <div class="info-item">
  281. <span class="info-label">入职日期</span>
  282. <strong>{{ editing.hireDate || "-" }}</strong>
  283. </div>
  284. <div class="info-item">
  285. <span class="info-label">所属机构</span>
  286. <strong>{{ editing.company || "-" }}</strong>
  287. </div>
  288. <div class="info-item">
  289. <span class="info-label">目标部门</span>
  290. <strong>{{ editing.department || deptNameOf(editing.deptId) || "-" }}</strong>
  291. </div>
  292. <div class="info-item">
  293. <span class="info-label">岗位</span>
  294. <strong>{{ editing.position || editing.positionName || "-" }}</strong>
  295. </div>
  296. <div class="info-item">
  297. <span class="info-label">手机号</span>
  298. <strong>{{ editing.phone || "-" }}</strong>
  299. </div>
  300. </div>
  301. </template>
  302. <!-- 第四步:物品领用 -->
  303. <template v-else-if="currentStep === 'assets'">
  304. <el-divider>物品领用</el-divider>
  305. <ItemReceiptPanel ref="receiptPanel" :employee-id="editing.userId || editing.id"
  306. :init-receipt-id="receiptData && receiptData.id ? receiptData.id : ''"
  307. :onboarding-progress="editing.onboardingProgress || ''" :read-only="readOnly || receiptApproving"
  308. :contract-signed="contractSigned" @loaded="onReceiptLoaded" @completed="onReceiptCompleted"
  309. @request-approval="openReceiptApproval" />
  310. </template>
  311. <template v-else-if="currentStep === 'complete'">
  312. <div class="onboarding-complete">
  313. <i class="el-icon-circle-check success-icon"></i>
  314. <div class="complete-text">
  315. <div class="complete-title">已入职</div>
  316. <div class="complete-sub">入职流程已完成</div>
  317. </div>
  318. </div>
  319. </template>
  320. <!-- 其他步骤提示 -->
  321. <template v-else>
  322. <el-alert :title="stepHint" type="info" :closable="false" show-icon />
  323. </template>
  324. </el-form>
  325. <span slot="footer">
  326. <el-button @click="dialogVisible = false">{{
  327. readOnly ? "关闭" : "取消"
  328. }}</el-button>
  329. <!-- 详情模式(openDetail):不显示业务操作按钮 -->
  330. <template v-if="readOnly">
  331. <el-button v-if="currentStep === 'contract' && contractAlreadySaved" type="primary" icon="el-icon-document"
  332. @click="goViewContract">去查看合同</el-button>
  333. <el-button v-else-if="currentStep === 'assets' && (receiptApproving || editing.assets === '已领用')"
  334. type="primary" plain icon="el-icon-tickets" @click="openReceiptApprovalRecord">审批记录</el-button>
  335. </template>
  336. <!-- 办理模式(openEdit):按步骤+进度判定 -->
  337. <template v-else>
  338. <!-- profile 节点:无审批,始终显示保存/下一步(仅 ONBOARDING 进度可编辑;其他进度档案已完成,按钮不出现) -->
  339. <template v-if="currentStep === 'profile' && profileEditable">
  340. <el-button :loading="saving" @click="saveDraft">保存</el-button>
  341. <el-button type="primary" :loading="saving" @click="goNextStep">下一步</el-button>
  342. </template>
  343. <!-- contract 节点 -->
  344. <template v-else-if="currentStep === 'contract'">
  345. <el-button v-if="contractAlreadySaved" type="primary" icon="el-icon-document"
  346. @click="goViewContract">去查看合同</el-button>
  347. <el-button v-if="platformAccount && !contractAlreadySaved && $hasPermission('main:hrOnboarding:sign')"
  348. type="primary" @click="goSignContract">去签署合同</el-button>
  349. </template>
  350. <!-- assets 节点 -->
  351. <template v-else-if="currentStep === 'assets'">
  352. <el-button v-if="!receiptApproving && editing.assets !== '已领用'" :loading="receiptSaving"
  353. @click="saveReceipt">保存</el-button>
  354. <el-button v-if="!receiptApproving && editing.assets !== '已领用' && $hasPermission('main:hrOnboarding:submit')"
  355. type="primary" :loading="receiptSubmitting" @click="submitReceipt">提交</el-button>
  356. <el-button v-if="receiptApproving || editing.assets === '已领用'" type="primary" plain icon="el-icon-tickets"
  357. @click="openReceiptApprovalRecord">审批记录</el-button>
  358. </template>
  359. </template>
  360. </span>
  361. </ele-modal>
  362. <processSubmitDialog :processSubmitDialogFlag.sync="processSubmitDialogFlag" v-if="processSubmitDialogFlag"
  363. ref="processSubmitDialogRef" @reload="onProcessSubmitReload" />
  364. <BpmProcessDetailDialog ref="approvalRecordDialog" />
  365. </div>
  366. </template>
  367. <script>
  368. import {
  369. buildInviteQrImage,
  370. createOnboarding,
  371. deleteOnboarding,
  372. getOnboardingByUserId,
  373. getOnboardingPage,
  374. ONBOARDING_APPROVAL_BUSINESS_KEY,
  375. updateOnboarding,
  376. } from "@/api/hr/onboarding";
  377. import {
  378. adaptHiringApproval,
  379. getHiringApprovalPage,
  380. getHiringApprovalById,
  381. } from "@/api/hr/hiringApproval";
  382. import {
  383. getDepartmentPositionSequences,
  384. getDepartmentPositions,
  385. getDepartmentPositionType,
  386. getDepartmentPositionLevels,
  387. getEmployeeContractStatus,
  388. getContractById,
  389. adaptContract,
  390. } from "@/api/hr";
  391. import {
  392. createPersonalDirectory,
  393. hasJobNumber,
  394. listOrganizations,
  395. searchUsersByKeyword,
  396. } from "@/api/organization";
  397. /* 机构选项取登录缓存(loginChangeGroupVOList) */
  398. import { getOrgOptions, getCurrentOrgId, getOrgName } from "@/utils/current-org";
  399. import { getDictByCode } from "@/api/sys";
  400. import { buildOrganizationTree } from "@/utils/organization-tree";
  401. import { showRequestError } from "@/utils/message";
  402. import { resolveContractManagementPath } from "@/utils/menu-rewrite";
  403. import { getCurrentUser } from "@/utils/token-util";
  404. import OrganizationSelect from "@/views/organization/components/OrganizationSelect.vue";
  405. import processSubmitDialog from "@/BIZComponents/processSubmitDialog/processSubmitDialog.vue";
  406. import ItemReceiptPanel from "./ItemReceiptPanel.vue";
  407. import BpmProcessDetailDialog from "@/views/bpm/processInstance/detailDialog.vue";
  408. import {
  409. RECEIPT_APPROVAL_BUSINESS_KEY,
  410. } from "@/api/hr/onboardingItemReceipt";
  411. const emptyFilters = () => ({
  412. name: "",
  413. /* 所属机构默认登录机构列表第一个(用户所属多个机构时默认第一个) */
  414. organizationId: getCurrentOrgId() || null,
  415. /* 岗位性质(列表「状态」筛选,与列「岗位性质」同口径):取岗位性质字典项值 */
  416. positionType: "",
  417. });
  418. const empty = () => ({
  419. id: "",
  420. onboardingId: "",
  421. invitationId: "",
  422. invitationNo: "",
  423. employeeNo: "",
  424. jobNumber: "",
  425. name: "",
  426. department: "",
  427. deptId: null,
  428. position: "",
  429. positionName: "",
  430. positionId: null,
  431. positionSequence: "",
  432. // 岗位序列/层级/性质字段(提交给后端)
  433. positionSequenceId: "",
  434. positionLevelId: "",
  435. positionLevel: "",
  436. positionType: "",
  437. positionTypeName: "",
  438. hireDate: "",
  439. company: "",
  440. legalId: null,
  441. phone: "",
  442. email: "",
  443. idNumber: "",
  444. gender: "",
  445. age: "",
  446. probationFlag: 0,
  447. probationMonths: undefined,
  448. createMode: "manual",
  449. status: "待办理",
  450. flow: "待补充档案",
  451. offer: "已发起",
  452. contract: "未发起",
  453. assets: "未领用",
  454. remark: "",
  455. publicPath: "",
  456. publicToken: "",
  457. invitationStatus: "",
  458. onboardingStatus: "待补充档案",
  459. approvalStatus: "未发起",
  460. /** 入职流程进度枚举(如 ITEM_RECEIPT_APPROVING:物品领用审批中),后端返回 */
  461. onboardingProgress: "",
  462. createUserId: "",
  463. hiringApprovalId: "",
  464. candidateId: "",
  465. userId: "",
  466. fileIds: [],
  467. rowType: "record",
  468. });
  469. /**
  470. * 个人文档区(POST /fm/directory/createPersonal/{userId})去重缓存。
  471. * 接口文档未说明是否幂等,而组件内存名单刷新后会丢失,
  472. * 因此把「已成功创建」的 userId 持久化,避免对同一员工重复创建。
  473. * 只记录创建成功的,失败不落盘,保证下次仍可重试。
  474. */
  475. const PERSONAL_DIRECTORY_CACHE_KEY = "hr:onboarding:createdPersonalDirectory";
  476. const PERSONAL_DIRECTORY_CACHE_LIMIT = 500;
  477. function readCreatedDirectoryUsers() {
  478. try {
  479. const raw = window.localStorage.getItem(PERSONAL_DIRECTORY_CACHE_KEY);
  480. const list = raw ? JSON.parse(raw) : [];
  481. return Array.isArray(list) ? list.map((id) => String(id)) : [];
  482. } catch (error) {
  483. return [];
  484. }
  485. }
  486. function writeCreatedDirectoryUsers(list = []) {
  487. try {
  488. window.localStorage.setItem(
  489. PERSONAL_DIRECTORY_CACHE_KEY,
  490. JSON.stringify(list.slice(-PERSONAL_DIRECTORY_CACHE_LIMIT)),
  491. );
  492. } catch (error) {
  493. /* 隐私模式或存储不可用时忽略:去重退化为本次会话的内存名单 */
  494. }
  495. }
  496. export default {
  497. name: "OnboardingHandling",
  498. components: { OrganizationSelect, processSubmitDialog, ItemReceiptPanel, BpmProcessDetailDialog },
  499. data() {
  500. return {
  501. loading: false,
  502. saving: false,
  503. receiptSaving: false,
  504. receiptSubmitting: false,
  505. /** 公共审批弹框来源:onboarding=入职审批,receipt=物品领用审批 */
  506. approvalSource: null,
  507. filters: emptyFilters(),
  508. rows: [],
  509. totalCount: 0,
  510. orgList: [],
  511. legalEntities: [],
  512. departmentTree: [],
  513. positionOptions: [],
  514. positionLoading: false,
  515. sequenceOptions: [],
  516. sequenceLoading: false,
  517. levelOptions: [],
  518. levelLoading: false,
  519. selectedRows: [],
  520. dialogVisible: false,
  521. dialogTitle: "办理入职",
  522. readOnly: false,
  523. isCreate: false,
  524. /** 合同已保存(草稿或已提交审批):锁定"员工档案录入"步骤,直接进入"签署合同" */
  525. contractAlreadySaved: false,
  526. /** 合同流程接口返回的合同ID,用于跳转合同管理查看详情 */
  527. contractIdFromStatus: "",
  528. /** 合同流程接口返回的合同状态码,EFFECTIVE 时进入"物品领用"步骤 */
  529. contractStatusCode: "",
  530. /** 合同详情(签署合同节点展示) */
  531. contractDetailRow: null,
  532. editing: empty(),
  533. currentStep: "profile",
  534. processSubmitDialogFlag: false,
  535. currentLaunchRow: null,
  536. accountChecking: false,
  537. accountChecked: false,
  538. platformAccount: null,
  539. hiringSearching: false,
  540. lastHiringSearchName: "",
  541. /** 本次会话已处理(已创建或确认无需创建)的 userId,避免同一次办理重复调用 */
  542. personalDirectoryUserIds: [],
  543. /** 已成功创建过个人文档区的 userId(localStorage 持久化,跨刷新去重) */
  544. createdDirectoryUsers: readCreatedDirectoryUsers(),
  545. /** 物品领用单最新加载结果(含审批流程实例 id,用于查看审批记录) */
  546. receiptData: null,
  547. /** 本次会话物品领用审批已发起:提交成功后立即隐藏保存/提交,不等接口刷新 */
  548. receiptApprovalStarted: false,
  549. positionNatureOptions: [],
  550. steps: [
  551. { key: "offer", label: "Offer发起", icon: "el-icon-message" },
  552. { key: "profile", label: "员工档案录入", icon: "el-icon-edit-outline" },
  553. {
  554. key: "contract",
  555. label: "签署合同",
  556. icon: "el-icon-document-checked",
  557. },
  558. { key: "assets", label: "物品领用", icon: "el-icon-box" },
  559. { key: "complete", label: "完成", icon: "el-icon-circle-check" },
  560. ],
  561. };
  562. },
  563. computed: {
  564. styleResponsive() {
  565. return this.$store.state.theme?.styleResponsive !== false;
  566. },
  567. rules() {
  568. if (this.currentStep !== "profile") return {};
  569. /* 二维码邀请:入职资料表单整体隐藏,隐藏字段不参与校验,否则「保存」永远过不去 */
  570. if (this.editing.createMode === "invite") return {};
  571. return {
  572. name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
  573. hireDate: [
  574. { required: true, message: "请选择入职日期", trigger: "change" },
  575. ],
  576. legalId: [
  577. { required: true, message: "请选择所属机构", trigger: "change" },
  578. ],
  579. deptId: [
  580. { required: true, message: "请选择目标部门", trigger: "change" },
  581. ],
  582. positionId: [
  583. { required: true, message: "请选择岗位", trigger: "change" },
  584. ],
  585. positionSequenceId: [
  586. { required: true, message: "请选择岗位序列", trigger: "change" },
  587. ],
  588. positionLevelId: [
  589. { required: true, message: "请选择岗位层级", trigger: "change" },
  590. ],
  591. phone: [
  592. { required: true, message: "请输入手机号", trigger: "blur" },
  593. {
  594. pattern: /^1[3-9]\d{9}$/,
  595. message: "请输入正确的 11 位手机号码",
  596. trigger: "blur",
  597. },
  598. ],
  599. idNumber: [
  600. { required: true, message: "请输入身份证号", trigger: "blur" },
  601. {
  602. pattern: /(^\d{15}$)|(^\d{17}[\dXx]$)/,
  603. message: "请输入正确的身份证号码",
  604. trigger: "blur",
  605. },
  606. ],
  607. positionType: [
  608. { required: true, message: "请选择岗位性质", trigger: "change" },
  609. ],
  610. };
  611. },
  612. columns() {
  613. return [
  614. { type: "selection", width: 56, align: "center" },
  615. { type: "index", label: "序号", width: 60, align: "center" },
  616. {
  617. prop: "name",
  618. label: "员工",
  619. minWidth: 90,
  620. align: "left",
  621. showOverflowTooltip: true,
  622. slot: "name",
  623. },
  624. {
  625. /* 原「组织信息」一列拆成所属机构 / 所属部门两列(2026-10-29 用户要求) */
  626. prop: "organizationName",
  627. label: "所属机构",
  628. minWidth: 200,
  629. align: "left",
  630. showOverflowTooltip: true,
  631. formatter: (_r, _c, value) => value || "-",
  632. },
  633. {
  634. prop: "targetDepartmentName",
  635. label: "所属部门",
  636. minWidth: 160,
  637. align: "left",
  638. showOverflowTooltip: true,
  639. formatter: (_r, _c, value) => value || "-",
  640. },
  641. {
  642. prop: "position",
  643. label: "岗位",
  644. minWidth: 110,
  645. align: "left",
  646. showOverflowTooltip: true,
  647. formatter: (_r, _c, value) => value || "-",
  648. },
  649. {
  650. prop: "hireDate",
  651. label: "入职日期",
  652. width: 112,
  653. align: "left",
  654. formatter: (_r, _c, value) => value || "-",
  655. },
  656. {
  657. prop: "flow",
  658. label: "流程进度",
  659. minWidth: 120,
  660. align: "left",
  661. slot: "flow",
  662. },
  663. {
  664. /* 岗位性质只展示接口的 positionTypeName 快照(与编辑弹框、岗位档案同口径);
  665. columnKey 沿用旧值 status:列设置的本地缓存按 columnKey||prop 匹配,
  666. 改 prop 后缓存匹配不到会把该列排到最前 */
  667. prop: "positionTypeName",
  668. columnKey: "status",
  669. label: "岗位性质",
  670. width: 100,
  671. align: "left",
  672. slot: "positionType",
  673. },
  674. {
  675. prop: "createUserName",
  676. label: "创建人",
  677. width: 100,
  678. align: "left",
  679. },
  680. {
  681. prop: "createTime",
  682. label: "创建时间",
  683. width: 100,
  684. align: "left",
  685. },
  686. {
  687. prop: "updateUserName",
  688. label: "修改人",
  689. width: 100,
  690. align: "left",
  691. },
  692. {
  693. prop: "updateTime",
  694. label: "修改时间",
  695. width: 100,
  696. align: "left",
  697. },
  698. {
  699. columnKey: "action",
  700. label: "操作",
  701. width: 170,
  702. slot: "action",
  703. fixed: "right",
  704. align: "left",
  705. resizable: false,
  706. },
  707. ];
  708. },
  709. stepHint() {
  710. if (this.currentStep === "assets") {
  711. if (this.contractSigned) {
  712. return "合同已签署,请登记物品领用并提交。";
  713. }
  714. return "合同签署完成后才能提交物品领用,当前仅可保存草稿。";
  715. }
  716. if (this.currentStep === "complete") {
  717. return this.editing.status === "已完成"
  718. ? "入职流程已完成。"
  719. : "完成前序步骤后即可结束办理。";
  720. }
  721. return "请按步骤完成入职办理。";
  722. },
  723. statusOptions() {
  724. return this.positionNatureOptions;
  725. },
  726. inviteLink() {
  727. if (this.editing.publicPath) return this.editing.publicPath;
  728. if (this.editing.publicToken)
  729. return `/onboarding/public/${this.editing.publicToken}`;
  730. return "";
  731. },
  732. qrImageUrl() {
  733. if (this.editing.qrCodeUrl) return this.editing.qrCodeUrl;
  734. return buildInviteQrImage(this.inviteLink);
  735. },
  736. /** 录入已保存(M02 返回 userId)后才可进入后续步骤 */
  737. isProfileSaved() {
  738. return Boolean(this.editing.userId);
  739. },
  740. /** 办理入职进度枚举(后端上报,唯一真值源) */
  741. onboardingProgressNormalized() {
  742. return String(this.editing.onboardingProgress || "").toUpperCase();
  743. },
  744. /** 进度 → 当前应高亮的步骤 */
  745. progressToActiveStep() {
  746. const p = this.onboardingProgressNormalized;
  747. const map = {
  748. ONBOARDING: "profile",
  749. CONTRACT_DRAFT: "contract",
  750. CONTRACT_APPROVING: "contract",
  751. CONTRACT_APPROVED: "assets",
  752. ITEM_RECEIPT_DRAFT: "assets",
  753. ITEM_RECEIPT_APPROVING: "assets",
  754. COMPLETED: "complete",
  755. };
  756. return map[p] || null;
  757. },
  758. /** 档案录入节点是否允许编辑:新建态、或后端进度为 ONBOARDING 时允许 */
  759. profileEditable() {
  760. if (this.isCreate) return true;
  761. return this.onboardingProgressNormalized === "ONBOARDING";
  762. },
  763. /** 根据进度枚举,返回哪些节点已完成(单调累积,一旦 done 不会变灰) */
  764. doneStepSet() {
  765. const p = this.onboardingProgressNormalized;
  766. const completedFromProfile = [
  767. "CONTRACT_DRAFT",
  768. "CONTRACT_APPROVING",
  769. "CONTRACT_APPROVED",
  770. "ITEM_RECEIPT_DRAFT",
  771. "ITEM_RECEIPT_APPROVING",
  772. "COMPLETED",
  773. ];
  774. const completedFromContract = [
  775. "CONTRACT_APPROVED",
  776. "ITEM_RECEIPT_DRAFT",
  777. "ITEM_RECEIPT_APPROVING",
  778. "COMPLETED",
  779. ];
  780. return {
  781. offer: true,
  782. profile: completedFromProfile.includes(p) || this.isProfileSaved,
  783. contract: completedFromContract.includes(p),
  784. assets: p === "COMPLETED" || this.editing.assets === "已领用",
  785. complete: p === "COMPLETED" || this.editing.status === "已完成",
  786. };
  787. },
  788. /** 合同已签署/生效,物品领用单才允许提交 */
  789. contractSigned() {
  790. return Boolean(this.stepDone("contract"));
  791. },
  792. /** 物品领用审批中/已完成:表单只读、隐藏保存/提交
  793. * 唯一真值源:后端返回的入职进度枚举 onboardingProgress
  794. * 只有 ITEM_RECEIPT_APPROVING(审批中)和 COMPLETED(完成)才锁,
  795. * ITEM_RECEIPT_DRAFT(草稿态)始终允许编辑。
  796. */
  797. receiptApproving() {
  798. const progress = String(this.editing.onboardingProgress || "").toUpperCase();
  799. return (
  800. progress === "ITEM_RECEIPT_APPROVING" ||
  801. progress === "COMPLETED" ||
  802. this.receiptApprovalStarted
  803. );
  804. },
  805. /** 物品领用单已存在审批流程实例,可查看审批记录 */
  806. receiptHasRecord() {
  807. const row = { ...this.editing, ...(this.receiptData || {}) };
  808. // 有流程实例 id → 确定可查
  809. if (row.processInstanceId || row.approvalInstanceId || row.processInstance) {
  810. return true;
  811. }
  812. // 领用单状态已非草稿 / 审批流程已发起 → 一定走过审批,展示按钮
  813. const status = String(row.receiptStatus || "").toUpperCase();
  814. if (status && status !== "DRAFT") return true;
  815. const aps = String(
  816. row.approvalStatus || row.approvalStatusName || "",
  817. )
  818. .trim()
  819. .toUpperCase();
  820. return Boolean(
  821. aps &&
  822. !["", "未发起", "NOT_SUBMITTED", "DRAFT", "UNSUBMITTED", "NONE"].includes(
  823. aps,
  824. ),
  825. );
  826. },
  827. },
  828. created() {
  829. this.loadOrganizations();
  830. this.loadPositionNatureOptions();
  831. },
  832. methods: {
  833. positionTypeLabel(value) {
  834. if (value == null || value === "") return "-";
  835. const text = String(value);
  836. const hit = this.positionNatureOptions.find(
  837. (item) =>
  838. String(item.value) === text ||
  839. String(item.key) === text ||
  840. String(item.label) === text ||
  841. String(item.id) === text,
  842. );
  843. return hit?.label || text;
  844. },
  845. resolvePositionType(raw) {
  846. if (raw == null || raw === "") return "";
  847. const text = String(raw);
  848. const hit = this.positionNatureOptions.find(
  849. (item) =>
  850. String(item.value) === text ||
  851. String(item.key) === text ||
  852. String(item.label) === text ||
  853. String(item.id) === text,
  854. );
  855. return hit ? hit.value : raw;
  856. },
  857. /** 根据岗位性质 id/名称反查字典 label,用于提交 positionTypeName */
  858. matchPositionTypeName(raw) {
  859. if (raw == null || raw === "") return "";
  860. const text = String(raw);
  861. const hit = this.positionNatureOptions.find(
  862. (item) =>
  863. String(item.value) === text ||
  864. String(item.key) === text ||
  865. String(item.label) === text ||
  866. String(item.id) === text,
  867. );
  868. return hit ? hit.label : raw;
  869. },
  870. async loadPositionNatureOptions() {
  871. try {
  872. const items = await getDictByCode("position_nature");
  873. this.positionNatureOptions = (Array.isArray(items) ? items : [])
  874. .map((item) => {
  875. const label = String(item.label ?? item.value ?? "").trim();
  876. const id = item.id != null && item.id !== "" ? item.id : item.key;
  877. if (!label || id == null || id === "") return null;
  878. return {
  879. label,
  880. value: id,
  881. key: item.key != null ? String(item.key) : String(id),
  882. id,
  883. };
  884. })
  885. .filter(Boolean);
  886. } catch (error) {
  887. this.positionNatureOptions = [];
  888. }
  889. },
  890. deptNameOf(deptId) {
  891. if (!deptId) return "";
  892. return (
  893. this.orgList.find((item) => String(item.id) === String(deptId))?.name ||
  894. ""
  895. );
  896. },
  897. missingApi(action, extra) {
  898. this.$message.warning(
  899. `${action}:当前「M02人员入职」文档未提供对应接口${extra ? `,${extra}` : ""
  900. },页面功能已保留。`,
  901. );
  902. },
  903. async refresh() {
  904. this.loading = true;
  905. this.selectedRows = [];
  906. try {
  907. await Promise.all([
  908. this.loadOrganizations(),
  909. this.loadPositionNatureOptions(),
  910. ]);
  911. this.reloadTable();
  912. } finally {
  913. this.loading = false;
  914. }
  915. },
  916. applyFilters() {
  917. this.reloadTable();
  918. },
  919. resetFilters() {
  920. this.filters = emptyFilters();
  921. this.reloadTable();
  922. },
  923. reloadTable() {
  924. this.$nextTick(() => this.$refs.table?.reload({ page: 1 }));
  925. },
  926. async fetchPage({ page, limit }) {
  927. try {
  928. const data = await getOnboardingPage({
  929. pageNum: page,
  930. size: limit,
  931. name: this.filters.name,
  932. organizationId: this.filters.organizationId,
  933. positionType: this.filters.positionType,
  934. });
  935. /* 列表所属机构 / 所属部门直接用接口原始字段 organizationName、targetDepartmentName,
  936. 不再按 deptId 到组织树里往上反推机构名(会把部门名/上级名当机构名显示) */
  937. const list = data.list || [];
  938. this.rows = list;
  939. this.totalCount = Number(data.count || 0);
  940. return { list, count: this.totalCount };
  941. } catch (error) {
  942. this.rows = [];
  943. this.totalCount = 0;
  944. showRequestError(error, "入职列表加载失败");
  945. return { list: [], count: 0 };
  946. }
  947. },
  948. async loadOrganizations() {
  949. try {
  950. /* 机构选项:登录缓存 loginChangeGroupVOList(不再按组织类型筛选) */
  951. this.legalEntities = getOrgOptions();
  952. /* 组织列表:仅用于部门级联 */
  953. const list = await listOrganizations().catch(() => []);
  954. this.orgList = Array.isArray(list) ? list : [];
  955. } catch (error) {
  956. this.orgList = [];
  957. this.legalEntities = [];
  958. }
  959. },
  960. buildDeptTree(legalId) {
  961. if (!legalId) {
  962. this.departmentTree = buildOrganizationTree(this.orgList);
  963. return;
  964. }
  965. const legalKey = String(legalId);
  966. const underLegal = [];
  967. const walk = (parentId) => {
  968. this.orgList.forEach((item) => {
  969. if (String(item.parentId) === String(parentId)) {
  970. underLegal.push(item);
  971. walk(item.id);
  972. }
  973. });
  974. };
  975. const root = this.orgList.find((item) => String(item.id) === legalKey);
  976. if (root) underLegal.push(root);
  977. walk(legalId);
  978. this.departmentTree = buildOrganizationTree(
  979. underLegal.length ? underLegal : this.orgList,
  980. );
  981. },
  982. onLegalChange(legalId) {
  983. this.editing.deptId = null;
  984. this.editing.department = "";
  985. this.clearPositionCascade();
  986. this.sequenceOptions = [];
  987. const legal = this.legalEntities.find(
  988. (item) => String(item.id) === String(legalId),
  989. );
  990. this.editing.company = legal?.name || "";
  991. this.buildDeptTree(legalId);
  992. },
  993. onDeptChange(deptId) {
  994. if (this._suppressDeptPositionReset) return;
  995. this.editing.department = this.deptNameOf(deptId);
  996. this.clearPositionCascade();
  997. this.sequenceOptions = [];
  998. // 手动选部门:该部门没有维护岗位序列时给出指引
  999. this.loadPositionSequences(deptId, { notifyEmpty: true });
  1000. },
  1001. /** 接口返回可能是数组 / {list|records|rows|data},统一取数组 */
  1002. toOptionList(payload) {
  1003. if (Array.isArray(payload)) return payload.filter(Boolean);
  1004. const list =
  1005. payload?.list || payload?.records || payload?.rows || payload?.data;
  1006. return Array.isArray(list) ? list.filter(Boolean) : [];
  1007. },
  1008. /** 清空「岗位序列」及以下的级联字段与下拉选项(与录用管理一致) */
  1009. clearPositionCascade() {
  1010. this.editing.positionSequenceId = "";
  1011. this.editing.positionSequence = "";
  1012. this.editing.positionId = null;
  1013. this.editing.position = "";
  1014. this.editing.positionName = "";
  1015. this.editing.positionType = "";
  1016. this.editing.positionTypeName = "";
  1017. this.editing.positionLevelId = "";
  1018. this.editing.positionLevel = "";
  1019. this.positionOptions = [];
  1020. this.levelOptions = [];
  1021. },
  1022. /**
  1023. * 入职部门 → 岗位序列下拉(GET /fm/tmaindepartmentposition/sequence/list)
  1024. * notifyEmpty:用户主动切换部门时,该部门下没有任何岗位序列则提示去组织机构维护
  1025. */
  1026. async loadPositionSequences(deptId, { notifyEmpty = false } = {}) {
  1027. if (deptId == null || deptId === "") {
  1028. this.sequenceOptions = [];
  1029. this.sequenceLoading = false;
  1030. return;
  1031. }
  1032. this.sequenceLoading = true;
  1033. let requestFailed = false;
  1034. try {
  1035. const data = await getDepartmentPositionSequences(deptId).catch(() => {
  1036. requestFailed = true;
  1037. return [];
  1038. });
  1039. this.sequenceOptions = this.toOptionList(data)
  1040. .filter((item) => item && item.positionSequenceId != null)
  1041. .map((item) => ({
  1042. id: item.positionSequenceId,
  1043. name: item.positionSequence || "",
  1044. }));
  1045. // 接口异常时不误报,只有确实没有配置才提示
  1046. if (notifyEmpty && !requestFailed && !this.sequenceOptions.length) {
  1047. this.$message.warning(
  1048. "该部门下暂无配置岗位信息,请在组织机构的该部门补充岗位信息",
  1049. );
  1050. }
  1051. } catch (error) {
  1052. this.sequenceOptions = [];
  1053. } finally {
  1054. this.sequenceLoading = false;
  1055. }
  1056. },
  1057. /** 入职部门 + 岗位序列 → 岗位下拉 */
  1058. async loadDepartmentPositions() {
  1059. const { deptId, positionSequenceId } = this.editing;
  1060. if (!deptId || !positionSequenceId) {
  1061. this.positionOptions = [];
  1062. return;
  1063. }
  1064. this.positionLoading = true;
  1065. try {
  1066. const data = await getDepartmentPositions({
  1067. deptId,
  1068. positionSequenceId,
  1069. });
  1070. this.positionOptions = this.toOptionList(data)
  1071. .filter((item) => item && item.positionId != null)
  1072. .map((item) => ({
  1073. id: item.positionId,
  1074. name: item.positionName || "",
  1075. }));
  1076. } catch (error) {
  1077. this.positionOptions = [];
  1078. } finally {
  1079. this.positionLoading = false;
  1080. }
  1081. },
  1082. /** 入职部门 + 岗位 + 序列 → 岗位性质(接口返回数组,取第一条自动带出) */
  1083. async loadPositionType() {
  1084. const { deptId, positionId, positionSequenceId } = this.editing;
  1085. if (!deptId || !positionId || !positionSequenceId) return;
  1086. try {
  1087. const data = await getDepartmentPositionType({
  1088. deptId,
  1089. positionId,
  1090. positionSequenceId,
  1091. });
  1092. const first = this.toOptionList(data)[0];
  1093. this.editing.positionType = first?.positionType || "";
  1094. this.editing.positionTypeName = first?.positionTypeName || "";
  1095. } catch (error) {
  1096. this.editing.positionType = "";
  1097. this.editing.positionTypeName = "";
  1098. }
  1099. },
  1100. /** 入职部门 + 岗位 + 序列 + 岗位性质 → 岗位层级下拉 */
  1101. async loadPositionLevels() {
  1102. const { deptId, positionId, positionSequenceId, positionType } =
  1103. this.editing;
  1104. if (!deptId || !positionId || !positionSequenceId || !positionType) {
  1105. this.levelOptions = [];
  1106. return;
  1107. }
  1108. this.levelLoading = true;
  1109. try {
  1110. const data = await getDepartmentPositionLevels({
  1111. deptId,
  1112. positionId,
  1113. positionSequenceId,
  1114. positionType,
  1115. });
  1116. this.levelOptions = this.toOptionList(data)
  1117. .filter((item) => item && item.positionLevelId != null)
  1118. .map((item) => ({
  1119. id: item.positionLevelId,
  1120. name: item.positionLevel || "",
  1121. }));
  1122. } catch (error) {
  1123. this.levelOptions = [];
  1124. } finally {
  1125. this.levelLoading = false;
  1126. }
  1127. },
  1128. /** 选择岗位序列:清空岗位及以下级联,加载该序列下的岗位 */
  1129. async onPositionSequenceChange(positionSequenceId) {
  1130. const hit = this.sequenceOptions.find(
  1131. (item) => String(item.id) === String(positionSequenceId),
  1132. );
  1133. this.clearPositionCascade();
  1134. if (positionSequenceId == null || positionSequenceId === "") return;
  1135. this.editing.positionSequenceId = positionSequenceId;
  1136. this.editing.positionSequence = hit?.name || "";
  1137. await this.loadDepartmentPositions();
  1138. },
  1139. /** 选择岗位:自动带出岗位性质,再加载岗位层级下拉 */
  1140. async onPositionChange(id) {
  1141. const hit = this.positionOptions.find(
  1142. (item) => String(item.id) === String(id),
  1143. );
  1144. this.editing.positionType = "";
  1145. this.editing.positionTypeName = "";
  1146. this.editing.positionLevelId = "";
  1147. this.editing.positionLevel = "";
  1148. this.levelOptions = [];
  1149. if (id == null || id === "") {
  1150. this.editing.positionId = null;
  1151. this.editing.position = "";
  1152. this.editing.positionName = "";
  1153. return;
  1154. }
  1155. this.editing.positionId = id;
  1156. this.editing.position = hit?.name || "";
  1157. this.editing.positionName = hit?.name || "";
  1158. await this.loadPositionType();
  1159. await this.loadPositionLevels();
  1160. },
  1161. /** 岗位层级下拉切换:同步 id 与名称 */
  1162. onPositionLevelChange(value) {
  1163. const hit = this.levelOptions.find(
  1164. (item) => String(item.id) === String(value),
  1165. );
  1166. this.editing.positionLevel = hit ? hit.name : "";
  1167. },
  1168. /** 回显:按 部门 → 序列 → 岗位 → 层级 逐级预热下拉选项(与录用管理一致) */
  1169. async primePositionCascade() {
  1170. const {
  1171. deptId,
  1172. positionSequenceId,
  1173. positionId,
  1174. positionType,
  1175. positionLevelId,
  1176. } = this.editing;
  1177. if (!deptId) {
  1178. this.sequenceOptions = [];
  1179. this.positionOptions = [];
  1180. this.levelOptions = [];
  1181. return;
  1182. }
  1183. await this.loadPositionSequences(deptId);
  1184. if (
  1185. positionSequenceId &&
  1186. !this.sequenceOptions.some(
  1187. (item) => String(item.id) === String(positionSequenceId),
  1188. )
  1189. ) {
  1190. this.sequenceOptions.unshift({
  1191. id: positionSequenceId,
  1192. name: this.editing.positionSequence || "",
  1193. });
  1194. }
  1195. if (!positionSequenceId) {
  1196. this.positionOptions = [];
  1197. this.levelOptions = [];
  1198. return;
  1199. }
  1200. await this.loadDepartmentPositions();
  1201. if (
  1202. positionId &&
  1203. !this.positionOptions.some(
  1204. (item) => String(item.id) === String(positionId),
  1205. )
  1206. ) {
  1207. this.positionOptions.unshift({
  1208. id: positionId,
  1209. name: this.editing.positionName || this.editing.position || "",
  1210. });
  1211. }
  1212. if (!positionId || !positionType) {
  1213. this.levelOptions = [];
  1214. return;
  1215. }
  1216. await this.loadPositionLevels();
  1217. if (
  1218. positionLevelId &&
  1219. !this.levelOptions.some(
  1220. (item) => String(item.id) === String(positionLevelId),
  1221. )
  1222. ) {
  1223. this.levelOptions.unshift({
  1224. id: positionLevelId,
  1225. name: this.editing.positionLevel || "",
  1226. });
  1227. }
  1228. },
  1229. onNameBlur() {
  1230. this.searchHiringByName();
  1231. },
  1232. mapPositionType(raw) {
  1233. return this.resolvePositionType(raw);
  1234. },
  1235. pickHiringMatch(list = [], name = "") {
  1236. const target = String(name || "").trim();
  1237. const rows = (Array.isArray(list) ? list : []).filter((item) => {
  1238. const candidate = String(item.candidateName || item.name || "").trim();
  1239. return !target || candidate === target || candidate.includes(target);
  1240. });
  1241. if (!rows.length) return null;
  1242. const exact = rows.filter(
  1243. (item) =>
  1244. String(item.candidateName || item.name || "").trim() === target,
  1245. );
  1246. const pool = exact.length ? exact : rows;
  1247. return (
  1248. pool.find((item) => item.statusCode === "APPROVED") ||
  1249. pool.find((item) => item.statusCode === "PENDING") ||
  1250. pool[0]
  1251. );
  1252. },
  1253. /** 清空由录用信息带出的字段(保留当前输入的姓名与已选所属机构) */
  1254. clearHiringAutofill() {
  1255. const keepName = this.editing.name;
  1256. const keepLegalId = this.editing.legalId;
  1257. const keepCompany = this.editing.company;
  1258. this.editing.hiringApprovalId = "";
  1259. this.editing.candidateId = "";
  1260. this.editing.userId = "";
  1261. this.editing.hireDate = "";
  1262. this.editing.phone = "";
  1263. this.editing.idNumber = "";
  1264. this.editing.deptId = null;
  1265. this.editing.department = "";
  1266. this.clearPositionCascade();
  1267. this.sequenceOptions = [];
  1268. this.editing.name = keepName;
  1269. /* 所属机构不随姓名检索变化:只有用户手动改机构(onLegalChange)才重置 */
  1270. this.editing.legalId = keepLegalId;
  1271. this.editing.company = keepCompany;
  1272. this.buildDeptTree(keepLegalId || null);
  1273. this.$nextTick(() => this.$refs.form?.clearValidate());
  1274. },
  1275. async searchHiringByName() {
  1276. if (this.readOnly || this.editing.createMode === "invite") return;
  1277. const name = String(this.editing.name || "").trim();
  1278. if (!name || name.length < 2) {
  1279. if (this.editing.hiringApprovalId || this.lastHiringSearchName) {
  1280. this.clearHiringAutofill();
  1281. this.lastHiringSearchName = "";
  1282. }
  1283. return;
  1284. }
  1285. // 姓名未变且刚检索过,避免失焦重复请求;换名则必定重新检索填充
  1286. if (name === this.lastHiringSearchName) return;
  1287. this.hiringSearching = true;
  1288. try {
  1289. const data = await getHiringApprovalPage({
  1290. pageNum: 1,
  1291. size: 20,
  1292. offset: 0,
  1293. keyword: name,
  1294. });
  1295. const toList = (payload) => {
  1296. const raw = Array.isArray(payload?.list)
  1297. ? payload.list
  1298. : Array.isArray(payload?.records)
  1299. ? payload.records
  1300. : Array.isArray(payload)
  1301. ? payload
  1302. : [];
  1303. return raw.map((item) => adaptHiringApproval(item));
  1304. };
  1305. let list = toList(data);
  1306. let hit = this.pickHiringMatch(list, name);
  1307. if (!hit) {
  1308. const approved = await getHiringApprovalPage({
  1309. pageNum: 1,
  1310. size: 50,
  1311. offset: 0,
  1312. approvalStatus: "APPROVED",
  1313. });
  1314. list = toList(approved);
  1315. hit = this.pickHiringMatch(list, name);
  1316. }
  1317. if (hit?.id) {
  1318. try {
  1319. hit = await getHiringApprovalById(hit.id);
  1320. } catch (e) {
  1321. /* 列表数据足够时直接使用 */
  1322. }
  1323. }
  1324. this.lastHiringSearchName = name;
  1325. if (!hit) {
  1326. this.clearHiringAutofill();
  1327. return;
  1328. }
  1329. await this.applyHiringToForm(hit);
  1330. this.$message.success(
  1331. `已带出录用信息:${hit.candidateName || hit.name || name}`,
  1332. );
  1333. } catch (error) {
  1334. showRequestError(error, "检索录用信息失败");
  1335. } finally {
  1336. this.hiringSearching = false;
  1337. }
  1338. },
  1339. async applyHiringToForm(hit = {}) {
  1340. const name = hit.candidateName || hit.name || this.editing.name;
  1341. const hireDate = hit.hireDate || hit.expectedEntryDate || "";
  1342. const phone = hit.phone || hit.mobile || hit.contactMobile || "";
  1343. const idNumber = hit.idNumber || hit.idCard || "";
  1344. const deptId = hit.hiringDeptId || hit.departmentId || hit.deptId || null;
  1345. const positionId = hit.positionId || null;
  1346. const positionName = hit.positionName || "";
  1347. const positionType = this.resolvePositionType(
  1348. hit.positionType || hit.employmentType || hit.staffingType,
  1349. );
  1350. const positionTypeName = this.matchPositionTypeName(
  1351. hit.positionTypeName ||
  1352. hit.employmentType ||
  1353. hit.staffingType,
  1354. );
  1355. this._suppressDeptPositionReset = true;
  1356. try {
  1357. // 先清空再写入,保证「没有的字段」不会残留上一次的值
  1358. this.clearHiringAutofill();
  1359. this.editing.name = name;
  1360. this.editing.hiringApprovalId = hit.id || "";
  1361. this.editing.candidateId = hit.candidateId || hit.userId || "";
  1362. this.editing.hireDate = hireDate;
  1363. this.editing.phone = phone;
  1364. this.editing.idNumber = idNumber;
  1365. this.editing.positionType = positionType;
  1366. this.editing.positionTypeName = positionTypeName;
  1367. // 回显岗位序列/层级(adaptHiringApproval 已保留这些字段)
  1368. this.editing.positionSequence = hit.positionSequence || "";
  1369. this.editing.positionSequenceId = hit.positionSequenceId || "";
  1370. this.editing.positionLevelId = hit.positionLevelId || "";
  1371. this.editing.positionLevel = hit.positionLevel || "";
  1372. if (deptId) {
  1373. this.editing.deptId = deptId;
  1374. this.editing.department =
  1375. hit.hiringDeptName || hit.departmentName || this.deptNameOf(deptId);
  1376. const parents = new Map(
  1377. this.orgList.map((item) => [String(item.id), item]),
  1378. );
  1379. let cursor = this.orgList.find(
  1380. (item) => String(item.id) === String(deptId),
  1381. );
  1382. let root = cursor;
  1383. while (cursor && parents.has(String(cursor.parentId || 0))) {
  1384. cursor = parents.get(String(cursor.parentId));
  1385. if (cursor) root = cursor;
  1386. }
  1387. /* 只有反推到的根节点确实是机构选项时才覆盖所属机构,否则沿用用户已选值 */
  1388. const rootLegal = root?.id
  1389. ? this.legalEntities.find((item) => String(item.id) === String(root.id))
  1390. : null;
  1391. if (rootLegal) {
  1392. this.editing.legalId = root.id;
  1393. this.editing.company = root.name || rootLegal.name || "";
  1394. }
  1395. this.buildDeptTree(this.editing.legalId || null);
  1396. } else {
  1397. /* 录用信息没有目标部门:只清部门相关字段,已选所属机构保持不动 */
  1398. this.editing.deptId = null;
  1399. this.editing.department = "";
  1400. this.buildDeptTree(this.editing.legalId || null);
  1401. }
  1402. this.editing.positionId = positionId;
  1403. this.editing.position = positionName;
  1404. this.editing.positionName = positionName;
  1405. // 与录用管理一致:按 部门 → 序列 → 岗位 → 层级 逐级预热下拉选项
  1406. await this.primePositionCascade();
  1407. const matched = this.positionOptions.find(
  1408. (item) => String(item.id) === String(this.editing.positionId),
  1409. );
  1410. if (matched) {
  1411. this.editing.position = matched.name;
  1412. this.editing.positionName = matched.name;
  1413. }
  1414. this.$nextTick(() => this.$refs.form?.clearValidate());
  1415. } finally {
  1416. this.$nextTick(() => {
  1417. this._suppressDeptPositionReset = false;
  1418. });
  1419. }
  1420. },
  1421. resetAccountState() {
  1422. this.accountChecking = false;
  1423. this.accountChecked = false;
  1424. this.platformAccount = null;
  1425. this.hiringSearching = false;
  1426. this.lastHiringSearchName = "";
  1427. },
  1428. openCreate() {
  1429. this.readOnly = false;
  1430. this.isCreate = true;
  1431. this.dialogTitle = "办理入职";
  1432. this.contractAlreadySaved = false;
  1433. this.contractIdFromStatus = "";
  1434. this.contractStatusCode = "";
  1435. this.editing = empty();
  1436. /* 默认带出当前登录用户所属机构(登录缓存) */
  1437. const currentOrgId = getCurrentOrgId();
  1438. if (currentOrgId) {
  1439. this.editing.legalId = currentOrgId;
  1440. this.editing.company = getOrgName(currentOrgId);
  1441. }
  1442. this.currentStep = "profile";
  1443. this.receiptData = null;
  1444. this.receiptApprovalStarted = false;
  1445. this.resetAccountState();
  1446. this.positionOptions = [];
  1447. this.buildDeptTree(this.editing.legalId);
  1448. this.dialogVisible = true;
  1449. this.$nextTick(() => this.$refs.form?.clearValidate());
  1450. },
  1451. async hydrateEditing(row = {}, { readOnly = false } = {}) {
  1452. const userId = row.userId || row.id;
  1453. this.receiptData = null;
  1454. this.receiptApprovalStarted = false;
  1455. this.editing = {
  1456. ...empty(),
  1457. ...JSON.parse(JSON.stringify(row)),
  1458. createMode: "manual",
  1459. fileIds: this.normalizeFileIds(row),
  1460. };
  1461. this.editing.positionType = this.resolvePositionType(
  1462. row.positionType || row.employmentType || row.staffStatus || row.status,
  1463. );
  1464. this.editing.positionTypeName = this.matchPositionTypeName(
  1465. row.positionTypeName ||
  1466. row.employmentType ||
  1467. row.staffStatus ||
  1468. row.status,
  1469. );
  1470. this.resetAccountState();
  1471. this.buildDeptTree(this.editing.legalId);
  1472. this.dialogVisible = true;
  1473. // 无 userId(如新建)直接用当前 deptId 预热级联下拉,避免重复请求
  1474. if (!userId) {
  1475. this.primePositionCascade();
  1476. return;
  1477. }
  1478. try {
  1479. const detail = await getOnboardingByUserId(userId);
  1480. this.editing = {
  1481. ...this.editing,
  1482. ...detail,
  1483. createMode: "manual",
  1484. fileIds: this.normalizeFileIds(detail),
  1485. };
  1486. this.editing.positionType = this.resolvePositionType(
  1487. detail.positionType || detail.employmentType || detail.staffStatus,
  1488. );
  1489. this.editing.positionTypeName = this.matchPositionTypeName(
  1490. detail.positionTypeName || detail.employmentType || detail.staffStatus,
  1491. );
  1492. this.buildDeptTree(this.editing.legalId);
  1493. // 详情加载完成后按最终 deptId 预热级联下拉(部门 → 序列 → 岗位 → 层级)
  1494. this.primePositionCascade();
  1495. this.markPersonalDirectoryIfArchived(this.editing);
  1496. } catch (error) {
  1497. showRequestError(error, "入职详情加载失败");
  1498. }
  1499. },
  1500. async openEdit(row) {
  1501. this.readOnly = false;
  1502. this.isCreate = false;
  1503. this.dialogTitle = "办理入职";
  1504. this.contractAlreadySaved = false;
  1505. this.contractIdFromStatus = "";
  1506. this.contractStatusCode = "";
  1507. this.contractDetailRow = null;
  1508. // 查询合同流程状态:接口 data 有返回(非空)则锁定到"签署合同"步骤
  1509. const employeeId = row.userId || row.id;
  1510. if (employeeId) {
  1511. try {
  1512. const res = await getEmployeeContractStatus(employeeId);
  1513. // 响应结构:{ hasContract, contracts: [{ contractId, contractStatus, ... }] }
  1514. const hasContract = res?.hasContract || (Array.isArray(res?.contracts) && res.contracts.length > 0);
  1515. if (hasContract) {
  1516. this.contractAlreadySaved = true;
  1517. const firstContract = (res.contracts && res.contracts[0]) || {};
  1518. this.contractIdFromStatus = firstContract.contractId || firstContract.id || "";
  1519. this.contractStatusCode = String(
  1520. firstContract.contractStatus || firstContract.statusCode || "",
  1521. ).toUpperCase();
  1522. // 拉合同完整详情(签署合同节点用)
  1523. this.loadContractDetail(this.contractIdFromStatus);
  1524. }
  1525. } catch (error) {
  1526. // 接口不可用或无合同记录,继续走原有入职办理流程
  1527. }
  1528. }
  1529. // 先 hydrateEditing(把 onboardingProgress 写入 editing),再用 progressToActiveStep 统一算当前步骤
  1530. this.hydrateEditing(row);
  1531. const activeStep = this.progressToActiveStep;
  1532. this.currentStep = activeStep || (this.contractStatusCode === "EFFECTIVE"
  1533. ? "assets"
  1534. : this.contractAlreadySaved
  1535. ? "contract"
  1536. : "profile");
  1537. },
  1538. async openDetail(row) {
  1539. this.readOnly = true;
  1540. this.isCreate = false;
  1541. this.dialogTitle = "入职详情";
  1542. this.contractAlreadySaved = false;
  1543. this.contractIdFromStatus = "";
  1544. this.contractStatusCode = "";
  1545. this.contractDetailRow = null;
  1546. // 详情同样查询合同状态:有合同数据则锁定到"签署合同"步骤
  1547. const employeeId = row.userId || row.id;
  1548. if (employeeId) {
  1549. try {
  1550. const res = await getEmployeeContractStatus(employeeId);
  1551. const hasContract = res?.hasContract || (Array.isArray(res?.contracts) && res.contracts.length > 0);
  1552. if (hasContract) {
  1553. this.contractAlreadySaved = true;
  1554. const firstContract = (res.contracts && res.contracts[0]) || {};
  1555. this.contractIdFromStatus = firstContract.contractId || firstContract.id || "";
  1556. this.contractStatusCode = String(
  1557. firstContract.contractStatus || firstContract.statusCode || "",
  1558. ).toUpperCase();
  1559. this.loadContractDetail(this.contractIdFromStatus);
  1560. }
  1561. } catch (error) {
  1562. // 接口不可用或无合同记录
  1563. }
  1564. }
  1565. this.hydrateEditing(row, { readOnly: true });
  1566. const activeStep = this.progressToActiveStep;
  1567. this.currentStep = activeStep || (this.contractStatusCode === "EFFECTIVE"
  1568. ? "assets"
  1569. : this.contractAlreadySaved
  1570. ? "contract"
  1571. : "profile");
  1572. },
  1573. normalizeFileIds(row = {}) {
  1574. if (Array.isArray(row.fileIds) && row.fileIds.length) return row.fileIds.slice();
  1575. const raw = row.attachmentFileIds || row.fileIds || "";
  1576. if (Array.isArray(raw)) return raw.slice();
  1577. if (!raw) return [];
  1578. return String(raw)
  1579. .split(",")
  1580. .map((item) => item.trim())
  1581. .filter(Boolean);
  1582. },
  1583. nextStep(r) {
  1584. if (r.flow !== "档案已完善" && r.status === "待办理") return "profile";
  1585. if (
  1586. !r.contract ||
  1587. r.contract === "未发起" ||
  1588. r.contract === "未签署" ||
  1589. r.contract === "待签署"
  1590. )
  1591. return "contract";
  1592. if (r.assets !== "已领用") return "assets";
  1593. return "complete";
  1594. },
  1595. stepDone(k) {
  1596. // 当前进度节点也算 done(这样用户切走后它不会变灰,保持已完成的蓝边白底样式)
  1597. if (k === this.progressToActiveStep) return true;
  1598. return this.doneStepSet[k];
  1599. },
  1600. stepPrerequisite(key) {
  1601. const order = ["offer", "profile", "contract", "assets", "complete"];
  1602. const idx = order.indexOf(key);
  1603. if (idx <= 0) return null;
  1604. return order[idx - 1];
  1605. },
  1606. /** 当前步骤是否可点击进入:done 的 + 当前高亮的可进;offer 单独禁用 */
  1607. canEnterStep(key) {
  1608. if (key === "offer") return false;
  1609. if (this.stepDone(key)) return true; // 已完成的节点,点击查看
  1610. if (key === this.currentStep) return true; // 当前高亮节点
  1611. return false;
  1612. },
  1613. /** 灰色禁用样式:既没 done 又不是当前高亮节点(以 UI 的 currentStep 为准) */
  1614. isStepLocked(key) {
  1615. return !this.stepDone(key) && key !== this.currentStep;
  1616. },
  1617. /** 按钮 disabled:offer 和未解锁的 */
  1618. isStepDisabled(key) {
  1619. if (key === "offer") return true;
  1620. return !this.canEnterStep(key);
  1621. },
  1622. stepLockTip(key) {
  1623. const prev = this.stepPrerequisite(key);
  1624. const labels = {
  1625. offer: "Offer发起",
  1626. profile: "员工档案录入",
  1627. contract: "签署合同",
  1628. assets: "物品领用",
  1629. complete: "完成",
  1630. };
  1631. if (key === "profile" && this.contractAlreadySaved) {
  1632. return "合同已保存,员工档案不可再修改";
  1633. }
  1634. if (key === "contract" && !this.isProfileSaved) {
  1635. return "请先保存员工档案录入信息,再进入签署合同";
  1636. }
  1637. if (prev) return `请先完成「${labels[prev] || prev}」`;
  1638. return "请先完成前置步骤";
  1639. },
  1640. stepDesc(k) {
  1641. if (k === "contract" && !this.isProfileSaved && !this.contractAlreadySaved) return "需先保存员工档案";
  1642. if (k === "assets" && !this.stepDone("contract")) return "需先签署合同";
  1643. if (k === "complete" && !this.stepDone("assets")) return "需先物品领用";
  1644. return {
  1645. offer: "已发起",
  1646. profile: this.contractAlreadySaved
  1647. ? "已保存"
  1648. : this.isProfileSaved
  1649. ? this.editing.flow || "已保存"
  1650. : "待保存",
  1651. contract:
  1652. this.contractStatusCode === "EFFECTIVE"
  1653. ? "已生效"
  1654. : this.editing.contract || "未签署",
  1655. // 物品领用审批中:步骤副标题显示"待审批"(已完成则仍显示已领用)
  1656. assets:
  1657. this.editing.assets === "已领用"
  1658. ? "已领用"
  1659. : this.receiptApproving
  1660. ? "待审批"
  1661. : this.editing.assets || "未领用",
  1662. complete: this.editing.status === "已完成" ? "已完成" : "待确认",
  1663. }[k];
  1664. },
  1665. handleStep(step) {
  1666. // Offer 默认已点亮,不可点击切换
  1667. if (step.key === "offer") return;
  1668. if (!this.canEnterStep(step.key)) {
  1669. return this.$message.warning(this.stepLockTip(step.key));
  1670. }
  1671. if (this.readOnly) {
  1672. this.currentStep = step.key;
  1673. return;
  1674. }
  1675. if (step.key === "profile") {
  1676. this.currentStep = "profile";
  1677. return;
  1678. }
  1679. if (step.key === "contract") {
  1680. this.currentStep = "contract";
  1681. return;
  1682. }
  1683. if (step.key === "assets") {
  1684. this.currentStep = "assets";
  1685. return;
  1686. }
  1687. if (step.key === "complete") {
  1688. this.currentStep = "complete";
  1689. }
  1690. },
  1691. /** 物品领用单加载完成:已提交的领用单回写为已领用 */
  1692. onReceiptLoaded(receipt) {
  1693. this.receiptData = receipt || null;
  1694. if (receipt && String(receipt.receiptStatus || "").toUpperCase() === "COMPLETED") {
  1695. this.editing.assets = "已领用";
  1696. }
  1697. },
  1698. /** 查看物品领用审批记录(复用 BPM 流程详情弹框) */
  1699. openReceiptApprovalRecord() {
  1700. const row = { ...this.editing, ...(this.receiptData || {}) };
  1701. this.$refs.approvalRecordDialog?.open(row);
  1702. },
  1703. /** 物品领用单提交成功:入职流程完成 */
  1704. onReceiptCompleted() {
  1705. this.editing.assets = "已领用";
  1706. this.editing.status = "已完成";
  1707. this.reloadTable();
  1708. },
  1709. async saveReceipt() {
  1710. const panel = this.$refs.receiptPanel;
  1711. if (!panel) return;
  1712. this.receiptSaving = true;
  1713. try {
  1714. await panel.saveDraft();
  1715. } finally {
  1716. this.receiptSaving = false;
  1717. }
  1718. },
  1719. async submitReceipt() {
  1720. const panel = this.$refs.receiptPanel;
  1721. if (!panel) return;
  1722. this.receiptSubmitting = true;
  1723. try {
  1724. await panel.submit();
  1725. } finally {
  1726. this.receiptSubmitting = false;
  1727. }
  1728. },
  1729. /** 物品领用提交:领用单已先保存/修改完成,这里弹出公共的提交审批弹框 */
  1730. openReceiptApproval(receipt) {
  1731. const id = receipt?.id;
  1732. if (!id) {
  1733. this.$message.warning("领用单保存失败,无法提交审批");
  1734. return;
  1735. }
  1736. this.approvalSource = "receipt";
  1737. // 提交成功后立即锁定:隐藏保存/提交,仅保留审批记录
  1738. this.receiptApprovalStarted = true;
  1739. this.processSubmitDialogFlag = true;
  1740. this.$nextTick(() => {
  1741. const currentUser = getCurrentUser() || {};
  1742. this.$refs.processSubmitDialogRef?.init({
  1743. businessId: id,
  1744. businessKey: RECEIPT_APPROVAL_BUSINESS_KEY,
  1745. formCreateUserId:
  1746. receipt.createUserId || currentUser.id || currentUser.userId || "",
  1747. variables: {
  1748. businessCode: String(id),
  1749. businessName: "入职物品领用",
  1750. businessType: "入职物品领用审批",
  1751. },
  1752. });
  1753. });
  1754. },
  1755. mergeSavedResult(saved) {
  1756. const userId = saved.userId || this.editing.userId;
  1757. const next = {
  1758. ...this.editing,
  1759. ...saved,
  1760. userId,
  1761. id: userId || saved.id || this.editing.id,
  1762. onboardingId: saved.onboardingId || userId || this.editing.onboardingId,
  1763. jobNumber: saved.jobNumber || this.editing.jobNumber || "",
  1764. name: saved.name || this.editing.name,
  1765. company: saved.company || this.editing.company,
  1766. /* 只认接口返回的部门名,其次用刚在弹框里选的那个(同一份数据);
  1767. 不再按 deptId 去组织树反查名字顶替 */
  1768. department: saved.department || this.editing.department,
  1769. position:
  1770. saved.position ||
  1771. saved.positionName ||
  1772. this.editing.position,
  1773. createMode: this.editing.createMode,
  1774. positionType: this.resolvePositionType(
  1775. saved.positionType ||
  1776. saved.employmentType ||
  1777. saved.staffStatus ||
  1778. this.editing.positionType,
  1779. ),
  1780. positionTypeName: this.matchPositionTypeName(
  1781. saved.positionTypeName ||
  1782. saved.employmentType ||
  1783. saved.staffStatus ||
  1784. this.editing.positionTypeName ||
  1785. this.editing.positionType,
  1786. ),
  1787. fileIds: Array.isArray(saved.fileIds)
  1788. ? saved.fileIds
  1789. : Array.isArray(this.editing.fileIds)
  1790. ? this.editing.fileIds
  1791. : [],
  1792. };
  1793. this.editing = next;
  1794. this.isCreate = false;
  1795. return next;
  1796. },
  1797. persistOnboarding() {
  1798. return new Promise((resolve, reject) => {
  1799. this.$refs.form.validate(async (valid) => {
  1800. if (!valid) {
  1801. reject(new Error("表单校验未通过"));
  1802. return;
  1803. }
  1804. this.saving = true;
  1805. try {
  1806. const payload = {
  1807. ...this.editing,
  1808. employmentTypeLabel:
  1809. this.editing.positionTypeName ||
  1810. this.positionTypeLabel(this.editing.positionType),
  1811. };
  1812. const userIdBefore = this.editing.userId;
  1813. let alreadyArchived = hasJobNumber(this.editing);
  1814. if (userIdBefore && !alreadyArchived) {
  1815. try {
  1816. const detail = await getOnboardingByUserId(userIdBefore);
  1817. alreadyArchived = hasJobNumber(detail);
  1818. if (detail.jobNumber) {
  1819. this.editing.jobNumber = detail.jobNumber;
  1820. }
  1821. this.markPersonalDirectoryIfArchived({
  1822. ...detail,
  1823. userId: userIdBefore,
  1824. });
  1825. } catch (error) {
  1826. /* 以保存前表单中的工号为准 */
  1827. }
  1828. }
  1829. const saved = userIdBefore
  1830. ? await updateOnboarding(payload)
  1831. : await createOnboarding(payload);
  1832. const merged = this.mergeSavedResult(saved);
  1833. await this.maybeCreatePersonalDirectory(merged, alreadyArchived);
  1834. this.reloadTable();
  1835. resolve(merged);
  1836. } catch (error) {
  1837. showRequestError(error, "保存入职办理失败");
  1838. reject(error);
  1839. } finally {
  1840. this.saving = false;
  1841. }
  1842. });
  1843. });
  1844. },
  1845. rememberPersonalDirectoryUser(userId) {
  1846. const key = String(userId || "").trim();
  1847. if (!key) return;
  1848. if (!this.personalDirectoryUserIds.includes(key)) {
  1849. this.personalDirectoryUserIds.push(key);
  1850. }
  1851. },
  1852. markPersonalDirectoryIfArchived(record = {}) {
  1853. if (!hasJobNumber(record)) return;
  1854. this.rememberPersonalDirectoryUser(record.userId || record.id);
  1855. },
  1856. /** 创建成功才持久化;失败不落盘,保证下次仍可重试 */
  1857. rememberCreatedDirectoryUser(userId) {
  1858. const key = String(userId || "").trim();
  1859. if (!key || this.createdDirectoryUsers.includes(key)) return;
  1860. this.createdDirectoryUsers.push(key);
  1861. writeCreatedDirectoryUsers(this.createdDirectoryUsers);
  1862. },
  1863. async maybeCreatePersonalDirectory(record = {}, existedJobNumber = false) {
  1864. const userId = record.userId || record.id;
  1865. if (!userId) return;
  1866. const key = String(userId);
  1867. // 打开详情时已有工号:员工已入库归档,禁止再创建个人文档区
  1868. if (existedJobNumber) {
  1869. this.rememberPersonalDirectoryUser(key);
  1870. return;
  1871. }
  1872. if (this.personalDirectoryUserIds.includes(key)) return;
  1873. // 已成功创建过(含刷新页面、重开弹框后再次保存的场景)不再重复创建
  1874. if (this.createdDirectoryUsers.includes(key)) return;
  1875. try {
  1876. await createPersonalDirectory(userId);
  1877. this.rememberPersonalDirectoryUser(key);
  1878. this.rememberCreatedDirectoryUser(key);
  1879. } catch (error) {
  1880. showRequestError(error, "创建个人文档区失败");
  1881. }
  1882. },
  1883. async saveDraft() {
  1884. try {
  1885. await this.persistOnboarding();
  1886. this.$message.success("入职资料已保存");
  1887. } catch (error) {
  1888. /* 已提示 */
  1889. }
  1890. },
  1891. async goNextStep() {
  1892. try {
  1893. await this.persistOnboarding();
  1894. try {
  1895. await this.$confirm(
  1896. "录用信息已保存,是否跳转到合同签署页面?",
  1897. "提示",
  1898. {
  1899. confirmButtonText: "确定",
  1900. cancelButtonText: "取消",
  1901. type: "info",
  1902. },
  1903. );
  1904. } catch (error) {
  1905. this.currentStep = "profile";
  1906. return;
  1907. }
  1908. this.jumpToContractCreate();
  1909. } catch (error) {
  1910. /* 保存失败已提示 */
  1911. }
  1912. },
  1913. jumpToContractCreate() {
  1914. const empId =
  1915. this.platformAccount?.id ||
  1916. this.editing.candidateId ||
  1917. this.editing.userId ||
  1918. "";
  1919. this.navigateToContractCreate({
  1920. employeeId: empId ? String(empId) : "",
  1921. employeeName: this.editing.name || "",
  1922. employeeNo: this.editing.employeeNo || "",
  1923. phone: this.editing.phone || "",
  1924. departmentId: this.editing.deptId ? String(this.editing.deptId) : "",
  1925. department:
  1926. this.editing.department ||
  1927. this.deptNameOf(this.editing.deptId) ||
  1928. "",
  1929. positionId: this.editing.positionId
  1930. ? String(this.editing.positionId)
  1931. : "",
  1932. positionName:
  1933. this.editing.position || this.editing.positionName || "",
  1934. legalEntityId: this.editing.legalId
  1935. ? String(this.editing.legalId)
  1936. : "",
  1937. legalEntity: this.editing.company || "",
  1938. });
  1939. },
  1940. navigateToContractCreate(query = {}) {
  1941. this.dialogVisible = false;
  1942. const path = resolveContractManagementPath(this);
  1943. const go = () => {
  1944. const location = {
  1945. path,
  1946. query: {
  1947. action: "create",
  1948. ...query,
  1949. },
  1950. };
  1951. const open = this.$route.path === path ? "replace" : "push";
  1952. this.$router[open](location).catch((err) => {
  1953. if (err && err.name === "NavigationDuplicated") return;
  1954. if (path !== "/contract-management") {
  1955. return this.$router
  1956. .push({ ...location, path: "/contract-management" })
  1957. .catch(() => {
  1958. this.$message.error("无法打开合同管理,请确认该菜单已配置");
  1959. });
  1960. }
  1961. this.$message.error("无法打开合同管理,请确认该菜单已配置");
  1962. });
  1963. };
  1964. this.$nextTick(go);
  1965. },
  1966. /** 根据合同ID拉取完整详情(入职签署合同节点展示) */
  1967. async loadContractDetail(contractId) {
  1968. if (!contractId) {
  1969. this.contractDetailRow = null;
  1970. return;
  1971. }
  1972. try {
  1973. const detail = await getContractById(contractId);
  1974. this.contractDetailRow = adaptContract(detail || {});
  1975. } catch (error) {
  1976. this.contractDetailRow = null;
  1977. }
  1978. },
  1979. /** 跳转合同管理 - 查看合同详情 */
  1980. goViewContract() {
  1981. const contractId = this.contractIdFromStatus;
  1982. if (!contractId) {
  1983. this.$message.warning("未获取到合同ID,无法查看合同详情");
  1984. return;
  1985. }
  1986. this.dialogVisible = false;
  1987. const path = resolveContractManagementPath(this);
  1988. const location = {
  1989. path,
  1990. query: {
  1991. action: "view",
  1992. contractId: String(contractId),
  1993. employeeId: String(this.editing.userId || this.editing.id || ""),
  1994. employeeName: this.editing.name || "",
  1995. },
  1996. };
  1997. const open = this.$route.path === path ? "replace" : "push";
  1998. this.$router[open](location).catch((err) => {
  1999. if (err && err.name === "NavigationDuplicated") return;
  2000. if (path !== "/contract-management") {
  2001. return this.$router
  2002. .push({ ...location, path: "/contract-management" })
  2003. .catch(() => {
  2004. this.$message.error("无法打开合同管理,请确认该菜单已配置");
  2005. });
  2006. }
  2007. this.$message.error("无法打开合同管理,请确认该菜单已配置");
  2008. });
  2009. },
  2010. launchApproval(row) {
  2011. const id = row?.userId || row?.id;
  2012. if (!id) {
  2013. this.$message.warning("缺少员工主键,无法提交审批");
  2014. return;
  2015. }
  2016. this.currentLaunchRow = row;
  2017. this.processSubmitDialogFlag = true;
  2018. this.$nextTick(() => {
  2019. const currentUser = getCurrentUser() || {};
  2020. const params = {
  2021. businessId: id,
  2022. businessKey: ONBOARDING_APPROVAL_BUSINESS_KEY,
  2023. formCreateUserId:
  2024. row.createUserId || currentUser.id || currentUser.userId || "",
  2025. variables: {
  2026. businessCode: row.employeeNo || row.invitationNo || "",
  2027. businessName: row.name || row.employeeName || "",
  2028. businessType: "入职审批",
  2029. departmentName: row.department || "",
  2030. positionName: row.position || row.positionName || "",
  2031. },
  2032. };
  2033. this.$refs.processSubmitDialogRef?.init(params);
  2034. });
  2035. },
  2036. onProcessSubmitReload() {
  2037. if (this.approvalSource === "receipt") {
  2038. this.approvalSource = null;
  2039. // 审批提交后刷新领用单(状态以后端为准:审批中 / 已完成)
  2040. this.$refs.receiptPanel?.load();
  2041. this.refresh();
  2042. return;
  2043. }
  2044. this.currentLaunchRow = null;
  2045. this.refresh();
  2046. },
  2047. async checkPlatformAccount() {
  2048. const keyword =
  2049. this.editing.phone || this.editing.name || this.editing.idNumber;
  2050. if (!keyword) {
  2051. this.$message.warning("请先完善候选人手机号、姓名或身份证号");
  2052. return;
  2053. }
  2054. this.accountChecking = true;
  2055. this.accountChecked = false;
  2056. this.platformAccount = null;
  2057. try {
  2058. const data = await searchUsersByKeyword(keyword, {
  2059. pageNum: 1,
  2060. size: 30,
  2061. });
  2062. const list = Array.isArray(data?.list) ? data.list : [];
  2063. const phone = String(this.editing.phone || "").trim();
  2064. const idNumber = String(this.editing.idNumber || "").trim().toUpperCase();
  2065. const name = String(this.editing.name || "").trim();
  2066. const hit =
  2067. list.find((u) => phone && String(u.phone || "") === phone) ||
  2068. list.find((u) => {
  2069. const card = String(u.idCard || u.idNumber || "")
  2070. .trim()
  2071. .toUpperCase();
  2072. return idNumber && card === idNumber;
  2073. }) ||
  2074. list.find((u) => name && String(u.name || "").trim() === name) ||
  2075. null;
  2076. if (!hit) {
  2077. this.accountChecked = true;
  2078. this.$message.warning("未检索到平台账号");
  2079. return;
  2080. }
  2081. const deptId =
  2082. (hit.deptIds && hit.deptIds[0]) || hit.deptId || hit.groupId || null;
  2083. const org = this.orgList.find(
  2084. (item) => String(item.id) === String(deptId || ""),
  2085. );
  2086. this.platformAccount = {
  2087. id: hit.id,
  2088. name: hit.name || this.editing.name,
  2089. employeeNo: hit.jobNumber || "",
  2090. phone: hit.phone || this.editing.phone,
  2091. department: hit.deptName || org?.name || this.editing.department,
  2092. departmentId: deptId,
  2093. position: hit.postName || this.editing.position,
  2094. positionId: hit.postId || this.editing.positionId,
  2095. legalEntity: this.editing.company,
  2096. legalEntityId: this.editing.legalId,
  2097. };
  2098. this.accountChecked = true;
  2099. this.$message.success("已找到平台账号");
  2100. } catch (error) {
  2101. showRequestError(error, "检索平台账号失败");
  2102. } finally {
  2103. this.accountChecking = false;
  2104. }
  2105. },
  2106. goSignContract() {
  2107. if (!this.platformAccount?.id) {
  2108. this.$message.warning("请先检索并确认候选人平台账号");
  2109. return;
  2110. }
  2111. const emp = this.platformAccount;
  2112. this.navigateToContractCreate({
  2113. employeeId: String(emp.id),
  2114. employeeName: emp.name || "",
  2115. employeeNo: emp.employeeNo || "",
  2116. phone: emp.phone || "",
  2117. departmentId: emp.departmentId ? String(emp.departmentId) : "",
  2118. department: emp.department || "",
  2119. positionId: emp.positionId ? String(emp.positionId) : "",
  2120. positionName: emp.position || "",
  2121. legalEntityId: emp.legalEntityId ? String(emp.legalEntityId) : "",
  2122. legalEntity: emp.legalEntity || "",
  2123. });
  2124. },
  2125. /** 入职进度已完成(终态):操作栏是否还提供「办理」的唯一判据(onboardingProgress) */
  2126. isProgressCompleted(row) {
  2127. return String(row?.onboardingProgress || "").toUpperCase() === "COMPLETED";
  2128. },
  2129. /** 只有草稿才能删除:未进入审批流程的入职单(办理入职 / 合同草稿) */
  2130. canDelete(row) {
  2131. const progress = String(row?.onboardingProgress || "").toUpperCase();
  2132. return progress === "ONBOARDING" || progress === "CONTRACT_DRAFT";
  2133. },
  2134. async deleteRecord(row) {
  2135. if (!this.canDelete(row)) {
  2136. this.$message.warning("只有草稿状态的入职单可以删除");
  2137. return;
  2138. }
  2139. const userId = row?.userId || row?.id;
  2140. if (!userId) {
  2141. this.$message.warning("缺少员工主键,无法删除");
  2142. return;
  2143. }
  2144. try {
  2145. await this.$confirm("确认删除该入职记录?", "提示", { type: "warning" });
  2146. await deleteOnboarding(userId);
  2147. this.$message.success("已删除");
  2148. this.reloadTable();
  2149. } catch (error) {
  2150. if (error === "cancel" || error === "close") return;
  2151. showRequestError(error, "删除入职记录失败");
  2152. }
  2153. },
  2154. copyInviteLink() {
  2155. const text = this.inviteLink;
  2156. if (!text) return;
  2157. if (navigator.clipboard?.writeText) {
  2158. navigator.clipboard.writeText(text).then(
  2159. () => this.$message.success("链接已复制"),
  2160. () => this.$message.warning("复制失败,请手动复制"),
  2161. );
  2162. return;
  2163. }
  2164. this.$message.info(text);
  2165. },
  2166. statusClass(s) {
  2167. if (s === "离职" || s === "已完成") return "completed";
  2168. if (["全职", "兼职", "实习", "正式", "试用", "已登记"].includes(s)) {
  2169. return "processing";
  2170. }
  2171. return { 待办理: "pending", 办理中: "processing", 已完成: "completed" }[s];
  2172. },
  2173. /** 合同状态 → CSS 类(复用合同管理的配色) */
  2174. contractStatusClass(s) {
  2175. const map = {
  2176. 草稿: "draft", 审批中: "pending", 待签署: "pending", 待签: "pending", 签署中: "signing",
  2177. 已签待生效: "pending", 待生效: "pending", 生效: "active", 生效中: "active",
  2178. 已变更: "changed", 已续签: "active", 已解除: "terminated", 已终止: "terminated",
  2179. 已作废: "expired", 已取消: "expired", 已归档: "archived", 试用期: "trial", 已到期: "expired", 已过期: "expired",
  2180. };
  2181. return map[s] || "pending";
  2182. },
  2183. badgeClass(v) {
  2184. return v === "档案已完善" || v === "已登记" ? "success" : "warning";
  2185. },
  2186. statusTag(s) {
  2187. if (s === "已完成" || s === "正式" || s === "全职") return "success";
  2188. if (s === "办理中" || s === "试用" || s === "实习" || s === "兼职") {
  2189. return "warning";
  2190. }
  2191. return "info";
  2192. },
  2193. statusTagType(s) {
  2194. return this.statusTag(s);
  2195. },
  2196. /** 办理进度枚举(onboardingProgress)→ 流程进度标签配色:
  2197. * 草稿类(ONBOARDING / CONTRACT_DRAFT / ITEM_RECEIPT_DRAFT)灰、
  2198. * 审批中(CONTRACT_APPROVING / ITEM_RECEIPT_APPROVING)橙、
  2199. * 合同审批通过(CONTRACT_APPROVED)主色蓝(流程未走完)、
  2200. * 完成(COMPLETED)绿;与「岗位性质」标签(实底)区分,这里用 effect="plain" 描边样式 */
  2201. progressTagType(progress) {
  2202. const map = {
  2203. ONBOARDING: "info",
  2204. CONTRACT_DRAFT: "info",
  2205. ITEM_RECEIPT_DRAFT: "info",
  2206. CONTRACT_APPROVING: "warning",
  2207. ITEM_RECEIPT_APPROVING: "warning",
  2208. CONTRACT_APPROVED: "",
  2209. COMPLETED: "success",
  2210. };
  2211. const code = String(progress || "").toUpperCase();
  2212. return map[code] != null ? map[code] : "info";
  2213. },
  2214. },
  2215. };
  2216. </script>
  2217. <style src="../../../styles/views/onboardingHandling/index.scss" scoped lang="scss"></style>
  2218. <!-- 复用合同管理的详情样式(非 scoped,否则 cm-create-grid 等穿透不过) -->
  2219. <style lang="scss" src="../../../styles/views/contractManagement/index.scss"></style>