index.vue 99 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285
  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.keyword" 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.contractType" clearable placeholder="请选择">
  14. <el-option v-for="t in contractTypes" :key="t.value" :label="t.label" :value="t.value" />
  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.contractStatus" clearable placeholder="请选择">
  21. <el-option v-for="s in contractStatus" :key="s.value" :label="s.label" :value="s.value" />
  22. </el-select>
  23. </el-form-item>
  24. </el-col>
  25. <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
  26. <el-form-item label="所属部门">
  27. <el-select v-model="filters.departmentId" clearable filterable placeholder="请选择">
  28. <el-option v-for="d in departments" :key="String(d.id)" :label="d.name" :value="d.id" />
  29. </el-select>
  30. </el-form-item>
  31. </el-col>
  32. <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
  33. <div class="ele-form-actions">
  34. <el-button type="primary" icon="el-icon-search" class="ele-btn-icon" @click="applyFilters">
  35. 查询
  36. </el-button>
  37. <el-button type="primary" icon="el-icon-refresh" class="ele-btn-icon" @click="resetFilters">
  38. 重置
  39. </el-button>
  40. </div>
  41. </el-col>
  42. </el-row>
  43. </el-form>
  44. <ele-pro-table ref="table" row-key="id" :row-class-name="tableRowClassName" :columns="columns"
  45. :datasource="fetchPage" :need-page="true" :page-size="20" :page-sizes="[10, 20, 50, 100]" :cache-key="$tableCacheKey('hr-contract-management')" class="cm-pro-table">
  46. <template v-slot:toolbar>
  47. <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openCreate">
  48. 发起合同
  49. </el-button>
  50. <el-button type="primary" icon="el-icon-download" class="ele-btn-icon" :loading="exporting"
  51. @click="exportList">
  52. 导出
  53. </el-button>
  54. </template>
  55. <template v-slot:contractNo="{ row }">
  56. <!-- 点合同编号开详情,替代原操作列「详情」入口 -->
  57. <el-link v-if="row.contractNo" type="primary" :underline="false" @click="viewDetail(row)">{{ row.contractNo }}</el-link>
  58. <span v-else>{{ row.contractNo }}</span>
  59. </template>
  60. <template v-slot:employee="{ row }">
  61. {{
  62. row.employeeName || "-"
  63. }}{{
  64. row.employeeNo || row.employeeCode
  65. ? " / " + (row.employeeNo || row.employeeCode)
  66. : ""
  67. }}
  68. </template>
  69. <template v-slot:signMethod="{ row }">
  70. <el-tag v-if="row.signMethod" size="mini" :type="signMethodTagType(row.signMethod)">{{ row.signMethod
  71. }}</el-tag>
  72. <span v-else>-</span>
  73. </template>
  74. <template v-slot:signingStatus="{ row }">
  75. <el-tag v-if="row.signingStatusLabel && row.signingStatusLabel !== '-'" size="mini"
  76. :type="signingStatusTagType(row.signingStatusLabel)">{{ row.signingStatusLabel }}</el-tag>
  77. <span v-else>-</span>
  78. </template>
  79. <template v-slot:status="{ row }">
  80. <el-tag v-if="row.status && row.status !== '-'" size="mini" :type="statusTagType(row.status)">{{ row.status
  81. }}</el-tag>
  82. <span v-else>-</span>
  83. </template>
  84. <template v-slot:archived="{ row }">
  85. <el-tag size="mini" :type="isArchived(row) ? 'success' : 'info'">
  86. {{ isArchived(row) ? "已归档" : "未归档" }}
  87. </el-tag>
  88. </template>
  89. <template v-slot:action="{ row }">
  90. <template v-if="isDraft(row)">
  91. <el-link type="primary" :underline="false" icon="el-icon-edit" @click="openEdit(row)">
  92. 编辑
  93. </el-link>
  94. <el-link type="primary" :underline="false" icon="el-icon-close" @click="openVoid(row)">
  95. 作废
  96. </el-link>
  97. </template>
  98. <template v-if="isEffective(row)">
  99. <el-link type="primary" :underline="false" icon="el-icon-sort" @click="openChange(row)">
  100. 变更
  101. </el-link>
  102. <el-link type="primary" :underline="false" icon="el-icon-refresh" @click="openRenew(row)">
  103. 续签
  104. </el-link>
  105. <el-link type="primary" :underline="false" icon="el-icon-remove-outline" @click="openTerminate(row)">
  106. 解除
  107. </el-link>
  108. </template>
  109. <el-link v-if="isDraft(row)" type="primary" :underline="false" icon="el-icon-s-promotion"
  110. @click="launchApproval(row)">
  111. 提交
  112. </el-link>
  113. <el-link v-if="isApproving(row)" type="warning" :underline="false" icon="el-icon-tickets"
  114. @click="openApprovalRecord(row)">
  115. 审批记录
  116. </el-link>
  117. </template>
  118. <template slot="empty">
  119. <el-empty description="暂无数据" />
  120. </template>
  121. </ele-pro-table>
  122. </el-card>
  123. <!-- 详情查看弹窗 -->
  124. <ele-modal :visible.sync="detailVisible" width="900px" title="合同详情" append-to-body custom-class="cm-detail-dialog"
  125. :movable="true" :maxable="true">
  126. <div v-if="detailRow" class="cm-create-page cm-detail-page">
  127. <div class="cm-create-employee-card cm-detail-employee">
  128. <span class="cm-employee-avatar">{{ detailRow.employeeName ? detailRow.employeeName.slice(0, 1) : '?'
  129. }}</span>
  130. <div class="cm-employee-info">
  131. <b>员工姓名:{{ detailRow.employeeName }}<span class="cm-employee-no">员工工号:{{ detailRow.employeeNo }}</span></b>
  132. </div>
  133. <div class="cm-detail-status">
  134. <small>合同状态</small>
  135. <span class="cm-status" :class="statusClass(detailRow.status)">{{ detailRow.status }}</span>
  136. </div>
  137. </div>
  138. <div class="cm-create-section">
  139. <div class="cm-create-section-title">合同基本信息</div>
  140. <div class="cm-create-grid">
  141. <div class="cm-compare-item"><span class="cm-compare-label">合同编号:</span><span class="cm-compare-value">{{
  142. detailRow.contractNo }}</span></div>
  143. <div class="cm-compare-item"><span class="cm-compare-label">合同类型:</span><span class="cm-compare-value">{{
  144. detailRow.contractType }}</span></div>
  145. <div class="cm-compare-item"><span class="cm-compare-label">所属机构:</span><span class="cm-compare-value">{{
  146. detailRow.legalEntity }}</span></div>
  147. <div class="cm-compare-item"><span class="cm-compare-label">所属部门:</span><span class="cm-compare-value">{{
  148. detailRow.department }}</span></div>
  149. <div class="cm-compare-item"><span class="cm-compare-label">岗位名称:</span><span class="cm-compare-value">{{
  150. detailRow.positionName }}</span></div>
  151. <div class="cm-compare-item"><span class="cm-compare-label">岗位序列:</span><span class="cm-compare-value">{{
  152. detailRow.positionSequence || '-' }}</span></div>
  153. <div class="cm-compare-item"><span class="cm-compare-label">岗位层级:</span><span class="cm-compare-value">{{
  154. detailRow.level || '-' }}</span></div>
  155. <div class="cm-compare-item"><span class="cm-compare-label">岗位性质:</span><span class="cm-compare-value">{{
  156. positionNatureText(detailRow) || '-' }}</span></div>
  157. <!-- <div class="cm-compare-item"><span class="cm-compare-label">直属上级:</span><span class="cm-compare-value">{{ detailRow.manager || '-' }}</span></div> -->
  158. </div>
  159. </div>
  160. <div class="cm-create-section">
  161. <div class="cm-create-section-title">合同期限与薪资</div>
  162. <div class="cm-create-grid">
  163. <div class="cm-compare-item"><span class="cm-compare-label">合同起始:</span><span class="cm-compare-value">{{
  164. detailRow.startDate || '-' }}</span></div>
  165. <div class="cm-compare-item"><span class="cm-compare-label">合同终止:</span><span class="cm-compare-value">{{
  166. detailRow.endDate || '长期' }}</span></div>
  167. <div class="cm-compare-item"><span class="cm-compare-label">月薪资:</span><span
  168. class="cm-compare-value is-highlight">{{ detailRow.salary ? ('¥ ' +
  169. Number(detailRow.salary).toLocaleString() + ' 元/月') : '待定' }}</span></div>
  170. <div class="cm-compare-item"><span class="cm-compare-label">试用期至:</span><span class="cm-compare-value">{{
  171. detailRow.probationEnd || '无试用期' }}</span></div>
  172. </div>
  173. </div>
  174. <div class="cm-create-section">
  175. <div class="cm-create-section-title">签署信息</div>
  176. <div class="cm-create-grid">
  177. <div class="cm-compare-item"><span class="cm-compare-label">签署方式:</span><span class="cm-compare-value">{{
  178. detailRow.signMethod || '-' }}</span></div>
  179. <!-- <div class="cm-compare-item"><span class="cm-compare-label">签署状态:</span><span class="cm-compare-value">{{ detailRow.signingStatusLabel || '-' }}</span></div> -->
  180. <div v-if="detailRow.flowSignStatus" class="cm-compare-item"><span
  181. class="cm-compare-label">流程节点电子签章:</span><span class="cm-compare-value">{{ detailRow.flowSignStatusLabel
  182. ||
  183. '-' }}</span></div>
  184. <div class="cm-compare-item is-span-2" v-if="detailRow.flowSignNodes && detailRow.flowSignNodes.length">
  185. <span class="cm-compare-label">签章节点:</span>
  186. <span class="cm-compare-value">
  187. {{detailRow.flowSignNodes.map((n) => (n.name || n.nodeName || n.taskName || '节点') + '(' +
  188. (n.statusLabel ||
  189. '-') + ')').join('、') }}
  190. </span>
  191. </div>
  192. <div class="cm-compare-item"><span class="cm-compare-label">合同附件:</span><span class="cm-compare-value">
  193. <fileMain v-if="detailRow.attachmentFileIds && detailRow.attachmentFileIds.length"
  194. :value="detailRow.attachmentFileIds" type="view" size="small" />
  195. <span v-else class="cm-detail-muted">暂无附件</span>
  196. </span></div>
  197. <div class="cm-compare-item"><span class="cm-compare-label">签订日期:</span><span class="cm-compare-value">{{
  198. detailRow.signDate || detailRow.createdAt || '-' }}</span></div>
  199. <div class="cm-compare-item"><span class="cm-compare-label">创建时间:</span><span class="cm-compare-value">{{
  200. detailRow.createdAt || '-' }}</span></div>
  201. </div>
  202. <div v-if="detailRow.remark" class="cm-detail-remark">
  203. <strong>备注:</strong>{{ detailRow.remark }}
  204. </div>
  205. </div>
  206. <div class="cm-create-footer">
  207. <div class="cm-footer-actions">
  208. <el-button @click="detailVisible = false">关闭</el-button>
  209. </div>
  210. </div>
  211. </div>
  212. </ele-modal>
  213. <!-- 发起合同弹窗 -->
  214. <ele-modal :visible.sync="createVisible" width="920px" :title="createDialogTitle" append-to-body
  215. :close-on-click-modal="false" :before-close="closeCreate" :movable="true" :maxable="true">
  216. <div class="cm-create-page">
  217. <div class="cm-create-section">
  218. <div class="cm-create-section-title">{{ createBaseSectionTitle }}</div>
  219. <el-form ref="createFormRef" :model="createForm" :rules="createRules" label-position="top">
  220. <div class="cm-create-grid">
  221. <el-form-item label="签订员工" prop="employeeId">
  222. <el-select v-model="createForm.employeeId" filterable remote :remote-method="searchEmployees"
  223. :loading="employeeLoading" :disabled="!isCreateMode && !isEditMode" placeholder="请选择员工"
  224. style="width:100%;" @change="onEmployeeChange" @visible-change="onEmployeeDropdownVisible">
  225. <el-option v-for="e in employeeOptions" :key="String(e.id)"
  226. :label="`${e.name || '-'}(${e.employeeNo || '无工号'})`" :value="e.id" />
  227. </el-select>
  228. </el-form-item>
  229. <el-form-item label="合同编号">
  230. <el-input v-model="createForm.contractNo" disabled
  231. :placeholder="isRenewMode ? '续签后由系统生成新编号' : '提交后由系统生成'" />
  232. </el-form-item>
  233. <el-form-item label="合同类型" prop="contractType">
  234. <el-select v-model="createForm.contractType" placeholder="请选择合同类型" @change="onTemplateDepsChange">
  235. <el-option v-for="t in contractTypes" :key="t.value" :label="t.label" :value="t.value" />
  236. </el-select>
  237. </el-form-item>
  238. <el-form-item label="拟定薪资" prop="salary">
  239. <el-input v-model="createForm.salary" placeholder="选择员工后自动带出,可手动修改">
  240. <template slot="append">元/月</template>
  241. </el-input>
  242. </el-form-item>
  243. <el-form-item label="所属机构" prop="legalEntityName">
  244. <el-input v-model="createForm.legalEntityName" :disabled="isCreateMode || isEditMode"
  245. :placeholder="(isCreateMode || isEditMode) ? '选择员工后自动带出' : '请输入所属机构'" />
  246. </el-form-item>
  247. <el-form-item label="所属部门" prop="departmentName">
  248. <el-input v-model="createForm.departmentName" :disabled="isCreateMode || isEditMode"
  249. :placeholder="(isCreateMode || isEditMode) ? '选择员工后自动带出' : '请输入所属部门'" />
  250. </el-form-item>
  251. <el-form-item label="岗位名称" prop="positionName">
  252. <el-input v-model="createForm.positionName" :disabled="isCreateMode || isEditMode"
  253. :placeholder="(isCreateMode || isEditMode) ? '选择员工后自动带出' : '请输入岗位名称'" />
  254. </el-form-item>
  255. <el-form-item label="岗位序列" prop="positionSequence">
  256. <el-input v-model="createForm.positionSequence" placeholder="选择员工后自动带出,可手动修改" />
  257. </el-form-item>
  258. <el-form-item label="岗位层级" prop="positionLevel">
  259. <el-input v-model="createForm.positionLevel" placeholder="选择员工后自动带出,可手动修改" />
  260. </el-form-item>
  261. <el-form-item label="岗位性质" prop="positionType">
  262. <el-select v-model="createForm.positionType" clearable filterable placeholder="请选择岗位性质"
  263. style="width:100%;" @change="onPositionTypeChange">
  264. <el-option v-for="item in positionNatureOptions"
  265. :key="item.id != null ? item.id : item.key || item.value" :label="item.label" :value="item.value" />
  266. </el-select>
  267. </el-form-item>
  268. <el-form-item label="合同起始日期" prop="startDate">
  269. <el-date-picker v-model="createForm.startDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择"
  270. style="width:100%;" />
  271. </el-form-item>
  272. <el-form-item label="合同终止日期" prop="endDate">
  273. <el-date-picker v-model="createForm.endDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择终止日期"
  274. style="width:100%;" />
  275. </el-form-item>
  276. <el-form-item label="签署方式" prop="signMethod">
  277. <div class="cm-create-sign-method">
  278. <button type="button" class="cm-method-btn is-unavailable"
  279. :class="{ 'is-active': createForm.signMethod === '线上' }" title="暂无开通模板功能,敬请期待"
  280. @click="onSignMethodChange('线上')">线上签订</button>
  281. <button type="button" class="cm-method-btn" :class="{ 'is-active': createForm.signMethod === '线下' }"
  282. @click="onSignMethodChange('线下')">线下签订</button>
  283. </div>
  284. </el-form-item>
  285. <el-form-item v-if="isOnlineSign" label="合同模版" prop="templateId">
  286. <!-- 模板功能暂未开通:编辑历史线上合同时该字段仍会渲染,点开下拉即提示 -->
  287. <el-select v-model="createForm.templateId" placeholder="请选择合同模版" @change="onTemplateChange"
  288. @visible-change="onTemplateDropdown">
  289. <el-option v-for="t in contractTemplates" :key="String(t.id)" :label="t.name" :value="t.id" />
  290. </el-select>
  291. </el-form-item>
  292. <!-- 合同附件与合同模版同占签署方式右侧的同一单元格,不再整行铺开 -->
  293. <el-form-item v-else label="合同附件" prop="fileIds" required>
  294. <fileMain v-model="createForm.fileIds" size="small" />
  295. </el-form-item>
  296. </div>
  297. </el-form>
  298. </div>
  299. <div v-if="isCompareMode" class="cm-create-section cm-compare-section">
  300. <div class="cm-create-section-title">{{ isRenewMode ? '原合同信息 / 续签拟定信息' : '原合同信息 / 发起后信息' }}</div>
  301. <div class="cm-create-compare">
  302. <div class="cm-compare-card">
  303. <div class="cm-compare-card-head">
  304. <span class="cm-compare-card-title">员工当前信息</span>
  305. </div>
  306. <div class="cm-compare-list">
  307. <div class="cm-compare-item"><span class="cm-compare-label">岗位:</span><span class="cm-compare-value">{{
  308. changeSnapshot.position || '-' }}</span></div>
  309. <div class="cm-compare-item"><span class="cm-compare-label">岗位层级:</span><span
  310. class="cm-compare-value">{{
  311. changeSnapshot.level || '-' }}</span></div>
  312. <div class="cm-compare-item"><span class="cm-compare-label">部门:</span><span class="cm-compare-value">{{
  313. changeSnapshot.department || '-' }}</span></div>
  314. <div class="cm-compare-item"><span class="cm-compare-label">直属上级:</span><span
  315. class="cm-compare-value">{{
  316. changeSnapshot.manager || '-' }}</span></div>
  317. <div class="cm-compare-item is-span-2"><span class="cm-compare-label">当前薪资:</span><span
  318. class="cm-compare-value">{{ formatSalary(changeSnapshot.salary) }}/月</span></div>
  319. </div>
  320. </div>
  321. <div class="cm-compare-card">
  322. <div class="cm-compare-card-head">
  323. <span class="cm-compare-card-title">{{ isRenewMode ? '续签拟定信息' : '合同拟定信息' }}</span>
  324. </div>
  325. <div class="cm-compare-list">
  326. <div class="cm-compare-item"><span class="cm-compare-label">合同类型:</span><span
  327. class="cm-compare-value is-highlight">{{ typeLabel(createForm.contractType) || '请选择合同类型' }}</span>
  328. </div>
  329. <div class="cm-compare-item"><span class="cm-compare-label">合同编号:</span><span
  330. class="cm-compare-value">{{
  331. createForm.contractNo || '-' }}</span></div>
  332. <div class="cm-compare-item"><span class="cm-compare-label">合同起始:</span><span
  333. class="cm-compare-value">{{
  334. createForm.startDate || '请选择起始日期' }}</span></div>
  335. <div class="cm-compare-item"><span class="cm-compare-label">合同终止:</span><span
  336. class="cm-compare-value">{{
  337. createForm.endDate || '请选择终止日期' }}</span></div>
  338. <div class="cm-compare-item is-span-2"><span class="cm-compare-label">拟定薪资:</span><span
  339. class="cm-compare-value is-highlight">{{ createForm.salary ? formatSalary(createForm.salary) + '/月'
  340. :
  341. '请填写薪资' }}</span></div>
  342. </div>
  343. </div>
  344. </div>
  345. </div>
  346. <div class="cm-create-footer">
  347. <div class="cm-footer-actions">
  348. <el-button @click="closeCreate">取消</el-button>
  349. <el-button :loading="saving" @click="saveCreateDraft">保存</el-button>
  350. <el-button type="primary" :loading="saving" @click="submitCreate">提交</el-button>
  351. </div>
  352. </div>
  353. </div>
  354. </ele-modal>
  355. <ele-modal :visible.sync="terminateVisible" width="640px" title="解除 / 终止劳动合同" append-to-body :movable="true"
  356. :maxable="true">
  357. <div class="cm-create-page">
  358. <div class="cm-create-employee-card cm-terminate-employee">
  359. <span class="cm-employee-avatar">{{ terminateForm.employeeName ? terminateForm.employeeName.slice(0, 1) : '?'
  360. }}</span>
  361. <div class="cm-employee-info">
  362. <b>员工姓名:{{ terminateForm.employeeName || '-' }}<span class="cm-employee-no">合同编号:{{ terminateForm.contractNo
  363. ||
  364. '-' }}</span></b>
  365. </div>
  366. </div>
  367. <div class="cm-create-section">
  368. <el-form ref="terminateFormRef" :model="terminateForm" :rules="terminateRules" label-position="top">
  369. <div class="cm-create-grid">
  370. <el-form-item label="业务类型" prop="terminationType">
  371. <el-radio-group v-model="terminateForm.terminationType">
  372. <el-radio label="TERMINATE">解除</el-radio>
  373. <el-radio label="END">终止</el-radio>
  374. </el-radio-group>
  375. </el-form-item>
  376. <el-form-item label="签署方式" prop="signingMethod">
  377. <el-radio-group v-model="terminateForm.signingMethod">
  378. <el-radio label="线上">线上</el-radio>
  379. <el-radio label="线下">线下</el-radio>
  380. </el-radio-group>
  381. </el-form-item>
  382. <el-form-item label="原因编码" prop="reasonCode">
  383. <el-select v-model="terminateForm.reasonCode" filterable placeholder="请选择解除或终止原因" style="width:100%;"
  384. @change="onTerminateReasonCode">
  385. <el-option v-for="item in terminateReasons" :key="item.value" :label="item.label"
  386. :value="item.value" />
  387. </el-select>
  388. </el-form-item>
  389. <el-form-item v-if="terminateForm.signingMethod === '线上'" label="离职证明模板" prop="templateId">
  390. <el-select v-model="terminateForm.templateId" placeholder="请选择离职证明模板" style="width:100%;"
  391. @change="onTerminateTemplateChange">
  392. <el-option v-for="t in contractTemplates" :key="String(t.id)" :label="t.name" :value="t.id" />
  393. </el-select>
  394. </el-form-item>
  395. <el-form-item v-else label="离职证明附件" prop="offlineFileIds">
  396. <fileMain v-model="terminateForm.offlineFileIds" size="small" />
  397. </el-form-item>
  398. <el-form-item label="生效日期" prop="effectiveDate">
  399. <el-date-picker v-model="terminateForm.effectiveDate" type="date" value-format="yyyy-MM-dd"
  400. placeholder="yyyy-MM-dd" style="width:100%;" />
  401. </el-form-item>
  402. <el-form-item label="最后工作日期" prop="lastWorkingDate">
  403. <el-date-picker v-model="terminateForm.lastWorkingDate" type="date" value-format="yyyy-MM-dd"
  404. placeholder="yyyy-MM-dd" style="width:100%;" />
  405. </el-form-item>
  406. <el-form-item label="原因说明" prop="reason" style="grid-column: span 2;">
  407. <el-input v-model="terminateForm.reason" type="textarea" :rows="3" maxlength="500" show-word-limit
  408. placeholder="请填写解除或终止原因说明" />
  409. </el-form-item>
  410. </div>
  411. </el-form>
  412. </div>
  413. <div class="cm-create-footer">
  414. <div class="cm-footer-actions">
  415. <el-button @click="terminateVisible = false">取消</el-button>
  416. <el-button type="primary" :loading="terminateSubmitting" @click="submitTerminate">确定</el-button>
  417. </div>
  418. </div>
  419. </div>
  420. </ele-modal>
  421. <ele-modal :visible.sync="voidVisible" width="480px" title="作废劳动合同" append-to-body>
  422. <el-form ref="voidFormRef" :model="voidForm" :rules="voidRules" label-position="top">
  423. <el-form-item label="作废原因" prop="reason">
  424. <el-input v-model="voidForm.reason" type="textarea" :rows="4" maxlength="200" show-word-limit
  425. placeholder="已签署且生效的合同不能作废" />
  426. </el-form-item>
  427. </el-form>
  428. <span slot="footer">
  429. <el-button @click="voidVisible = false">取消</el-button>
  430. <el-button type="primary" :loading="voidSubmitting" @click="submitVoid">确定</el-button>
  431. </span>
  432. </ele-modal>
  433. <ele-modal :visible.sync="archiveVisible" width="520px" title="归档劳动合同" append-to-body>
  434. <el-form ref="archiveFormRef" :model="archiveForm" :rules="archiveRules" label-position="top">
  435. <el-form-item label="双方签署扫描件" prop="fileIds">
  436. <fileMain v-model="archiveForm.fileIds" size="small" />
  437. </el-form-item>
  438. <el-form-item label="实际签署日期" prop="signingDate">
  439. <el-date-picker v-model="archiveForm.signingDate" type="date" value-format="yyyy-MM-dd"
  440. placeholder="yyyy-MM-dd" style="width:100%;" />
  441. </el-form-item>
  442. <el-form-item label="归档说明">
  443. <el-input v-model="archiveForm.remark" type="textarea" :rows="3" maxlength="200" show-word-limit
  444. placeholder="选填" />
  445. </el-form-item>
  446. </el-form>
  447. <span slot="footer">
  448. <el-button @click="archiveVisible = false">取消</el-button>
  449. <el-button type="primary" :loading="archiveSubmitting" @click="submitArchive">确定</el-button>
  450. </span>
  451. </ele-modal>
  452. <processSubmitDialog :processSubmitDialogFlag.sync="processSubmitDialogFlag" v-if="processSubmitDialogFlag"
  453. ref="processSubmitDialogRef" @reload="onProcessSubmitReload" />
  454. <BpmProcessDetailDialog ref="approvalRecordDialog" />
  455. </div>
  456. </template>
  457. <script>
  458. import {
  459. getContractPage,
  460. adaptContract,
  461. saveContract,
  462. getContractById,
  463. updateContract,
  464. matchContractTemplate,
  465. changeContract,
  466. renewContract,
  467. terminateContract,
  468. archiveContractOffline,
  469. voidContract,
  470. getContractTemplatePage,
  471. getContractTemplateVersions,
  472. exportContractLedger,
  473. CONTRACT_APPROVAL_BUSINESS_KEY,
  474. CONTRACT_STATUS_OPTIONS,
  475. contractStatusLabel,
  476. contractSigningStatusLabel,
  477. contractFlowSignStatusLabel,
  478. pickFlowSignStatus,
  479. adaptContractFlowSignNodes,
  480. getDepartmentPositionSequences,
  481. getDepartmentPositions,
  482. getDepartmentPositionType,
  483. getDepartmentPositionLevels,
  484. } from '@/api/hr';
  485. import { listOrganizations, getUserPage, getEmployeeById, getProposedSalary, adaptEmployee as adaptOrgEmployee } from '@/api/organization';
  486. import processSubmitDialog from '@/BIZComponents/processSubmitDialog/processSubmitDialog.vue';
  487. import BpmProcessDetailDialog from '@/views/bpm/processInstance/detailDialog.vue';
  488. import { getCurrentUser } from '@/utils/token-util';
  489. import { getByCode } from '@/api/system/dictionary-data';
  490. // 岗位性质字典与岗位档案同源:api/sys 的 getDictByCode 已兼容结构化项与「{ 1: "全职" }」老格式
  491. import { getDictByCode } from '@/api/sys';
  492. const CONTRACT_TYPES = [
  493. { value: 'FIXED', label: '固定期限劳动合同' },
  494. { value: 'NON_FIXED', label: '无固定期限劳动合同' },
  495. { value: 'TASK', label: '以完成一定任务为期限' },
  496. { value: 'DISPATCH', label: '劳务派遣合同' },
  497. { value: 'INTERN', label: '实习协议' },
  498. { value: 'CONFIDENTIAL', label: '保密协议' },
  499. { value: 'NON_COMPETE', label: '竞业限制协议' },
  500. { value: 'CHANGE', label: '合同变更协议' },
  501. { value: 'TERMINATION', label: '合同终止协议' },
  502. ];
  503. const TERMINATE_REASON_FALLBACK = [
  504. { value: 'MUTUAL', label: '协商一致解除' },
  505. { value: 'EMPLOYEE', label: '员工提出解除' },
  506. { value: 'MISCONDUCT', label: '严重违纪' },
  507. { value: 'CONTRACT_EXPIRE', label: '合同到期终止' },
  508. { value: 'OTHER', label: '其他' },
  509. ];
  510. function compact(object) {
  511. return Object.keys(object).reduce((result, key) => {
  512. const value = object[key];
  513. if (value !== '' && value !== null && value !== undefined) result[key] = value;
  514. return result;
  515. }, {});
  516. }
  517. function formatDate(value) {
  518. if (!value) return '';
  519. return String(value).slice(0, 10);
  520. }
  521. function formatDateTime(value) {
  522. if (!value) return '';
  523. return String(value).replace('T', ' ').slice(0, 16);
  524. }
  525. function todayStr() {
  526. const d = new Date();
  527. const pad = (n) => String(n).padStart(2, '0');
  528. return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
  529. }
  530. function addMonths(dateStr, months) {
  531. const d = dateStr ? new Date(dateStr) : new Date();
  532. d.setMonth(d.getMonth() + Number(months || 0));
  533. const pad = (n) => String(n).padStart(2, '0');
  534. return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
  535. }
  536. function addDays(dateStr, days) {
  537. const d = dateStr ? new Date(dateStr) : new Date();
  538. d.setDate(d.getDate() + Number(days || 0));
  539. const pad = (n) => String(n).padStart(2, '0');
  540. return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
  541. }
  542. function monthsBetween(start, end) {
  543. if (!start || !end) return undefined;
  544. const a = new Date(start);
  545. const b = new Date(end);
  546. const months = (b.getFullYear() - a.getFullYear()) * 12 + (b.getMonth() - a.getMonth());
  547. return months > 0 ? months : undefined;
  548. }
  549. function typeLabel(value) {
  550. return CONTRACT_TYPES.find((t) => t.value === value)?.label || value || '';
  551. }
  552. function toSigningMethod(label) {
  553. if (label === '线上' || label === 'ONLINE') return 'ONLINE';
  554. if (label === '线下' || label === 'OFFLINE') return 'OFFLINE';
  555. return 'ONLINE';
  556. }
  557. function signMethodLabel(value) {
  558. if (value === 'ONLINE') return '线上';
  559. if (value === 'OFFLINE') return '线下';
  560. return value || '';
  561. }
  562. function fileNameOf(file) {
  563. if (!file) return '';
  564. if (typeof file === 'string') return file;
  565. return file.name || file.fileName || '';
  566. }
  567. /** 合同附件主键:接口字段 attachmentIds 为英文逗号分隔字符串 */
  568. function parseAttachmentIds(value) {
  569. if (value == null || value === '') return [];
  570. if (Array.isArray(value)) {
  571. return value
  572. .map((item) => {
  573. if (item == null || item === '') return null;
  574. if (typeof item === 'object') return item.fileId || item.id || null;
  575. return item;
  576. })
  577. .filter((id) => id != null && id !== '')
  578. .map((id) => String(id));
  579. }
  580. return String(value)
  581. .split(',')
  582. .map((item) => item.trim())
  583. .filter(Boolean);
  584. }
  585. function joinAttachmentIds(value) {
  586. const ids = parseAttachmentIds(value);
  587. return ids.length ? ids.join(',') : undefined;
  588. }
  589. function isArchived(row = {}) {
  590. return Boolean(row.archived || row.statusCode === 'ARCHIVED' || row.status === '已归档');
  591. }
  592. function isVoided(row = {}) {
  593. const code = String(row.statusCode || '').toUpperCase();
  594. return code === 'VOIDED' || code === 'CANCELLED' || row.status === '已作废';
  595. }
  596. /** 草稿:只保存未提交审批,可编辑 / 作废 / 提交审批 */
  597. function isDraft(row = {}) {
  598. const code = String(row.statusCode || row.contractStatus || '').toUpperCase();
  599. return code === 'DRAFT' || row.status === '草稿';
  600. }
  601. /** 审批中(签署 / 变更 / 解除任一审批未完成):不允许其他操作 */
  602. function isApproving(row = {}) {
  603. const code = String(row.statusCode || row.contractStatus || '').toUpperCase();
  604. return ['PENDING_APPROVAL', 'PENDING_SIGN', 'SIGNING', 'PENDING_EFFECTIVE'].includes(code);
  605. }
  606. /** 审批通过且签署成功:可变更 / 续签 / 解除 */
  607. function isEffective(row = {}) {
  608. const code = String(row.statusCode || row.contractStatus || '').toUpperCase();
  609. return ['EFFECTIVE', 'EXPIRING', 'CHANGED'].includes(code);
  610. }
  611. function blankTerminateForm() {
  612. return {
  613. id: null,
  614. employeeName: '',
  615. contractNo: '',
  616. terminationType: 'TERMINATE',
  617. reasonCode: '',
  618. reason: '',
  619. effectiveDate: todayStr(),
  620. lastWorkingDate: todayStr(),
  621. signingMethod: '线下',
  622. templateId: null,
  623. templateVersionId: null,
  624. offlineFileIds: [],
  625. };
  626. }
  627. function blankVoidForm() {
  628. return { id: null, reason: '' };
  629. }
  630. function blankArchiveForm() {
  631. return { id: null, fileIds: [], signingDate: todayStr(), remark: '' };
  632. }
  633. function blankCreateForm() {
  634. return {
  635. id: null,
  636. employeeId: null,
  637. employeeNo: '',
  638. contractNo: '',
  639. contractType: 'FIXED',
  640. templateId: null,
  641. templateVersionId: null,
  642. templateVersionNo: null,
  643. legalEntity: '',
  644. department: '',
  645. departmentId: null,
  646. departmentName: '',
  647. legalEntityId: null,
  648. legalEntityName: '',
  649. positionId: null,
  650. positionName: '',
  651. positionSequenceId: '',
  652. positionSequence: '',
  653. positionLevelId: '',
  654. positionLevel: '',
  655. positionType: '',
  656. positionTypeName: '',
  657. startDate: '',
  658. endDate: '',
  659. probationEnd: '',
  660. salary: '',
  661. manager: '',
  662. // 签署方式默认线下签订
  663. signMethod: '线下',
  664. attachment: '',
  665. attachmentName: '',
  666. fileIds: [],
  667. fileId: null,
  668. };
  669. }
  670. function blankChangeSnapshot() {
  671. return {
  672. position: '',
  673. level: '',
  674. department: '',
  675. manager: '',
  676. salary: '',
  677. endDate: '',
  678. };
  679. }
  680. export default {
  681. name: 'ContractManagement',
  682. components: { processSubmitDialog, BpmProcessDetailDialog },
  683. data() {
  684. return {
  685. loading: false,
  686. saving: false,
  687. exporting: false,
  688. employeeLoading: false,
  689. processSubmitDialogFlag: false,
  690. currentLaunchRow: null,
  691. rows: [],
  692. orgById: {},
  693. filters: { keyword: '', contractType: '', contractStatus: '', departmentId: '' },
  694. departments: [],
  695. contractTypes: CONTRACT_TYPES,
  696. contractStatus: CONTRACT_STATUS_OPTIONS,
  697. contractTemplates: [],
  698. employeeOptions: [],
  699. positionNatureOptions: [],
  700. createVisible: false,
  701. createMode: 'create',
  702. changeSnapshot: blankChangeSnapshot(),
  703. createForm: blankCreateForm(),
  704. createRules: {
  705. employeeId: [{ required: true, message: '请选择员工', trigger: 'change' }],
  706. contractType: [{ required: true, message: '请选择合同类型', trigger: 'change' }],
  707. salary: [{ required: true, message: '请填写拟定薪资', trigger: 'blur' }],
  708. legalEntityName: [{ required: true, message: '请输入所属机构', trigger: 'blur' }],
  709. departmentName: [{ required: true, message: '请输入所属部门', trigger: 'blur' }],
  710. positionName: [{ required: true, message: '请输入岗位名称', trigger: 'blur' }],
  711. positionSequence: [{ required: true, message: '请填写岗位序列', trigger: 'blur' }],
  712. positionLevel: [{ required: true, message: '请填写岗位层级', trigger: 'blur' }],
  713. positionType: [{ required: true, message: '请选择岗位性质', trigger: 'change' }],
  714. startDate: [{ required: true, message: '请选择合同起始日期', trigger: 'change' }],
  715. endDate: [{ required: true, message: '请选择合同终止日期', trigger: 'change' }],
  716. signMethod: [{ required: true, message: '请选择签署方式', trigger: 'change' }],
  717. templateId: [
  718. {
  719. validator: (_rule, value, callback) => {
  720. if (this.createForm.signMethod !== '线上') return callback();
  721. if (value) return callback();
  722. callback(new Error('线上签订请选择合同模版'));
  723. },
  724. trigger: 'change',
  725. },
  726. ],
  727. fileIds: [
  728. {
  729. validator: (_rule, value, callback) => {
  730. if (this.createForm.signMethod !== '线下') return callback();
  731. if (Array.isArray(value) && value.length) return callback();
  732. callback(new Error('线下签订请上传合同附件'));
  733. },
  734. trigger: 'change',
  735. },
  736. ],
  737. },
  738. detailVisible: false,
  739. detailRow: null,
  740. terminateVisible: false,
  741. terminateSubmitting: false,
  742. terminateForm: blankTerminateForm(),
  743. terminateReasons: TERMINATE_REASON_FALLBACK,
  744. terminateRules: {
  745. terminationType: [{ required: true, message: '请选择业务类型', trigger: 'change' }],
  746. signingMethod: [{ required: true, message: '请选择签署方式', trigger: 'change' }],
  747. reasonCode: [{ required: true, message: '请选择原因编码', trigger: 'change' }],
  748. reason: [{ required: true, message: '请填写原因说明', trigger: 'blur' }],
  749. effectiveDate: [{ required: true, message: '请选择生效日期', trigger: 'change' }],
  750. lastWorkingDate: [{ required: true, message: '请选择最后工作日期', trigger: 'change' }],
  751. templateId: [
  752. {
  753. validator: (_rule, value, callback) => {
  754. if (this.terminateForm.signingMethod !== '线上') return callback();
  755. if (value) return callback();
  756. callback(new Error('线上解除请选择离职证明模板'));
  757. },
  758. trigger: 'change',
  759. },
  760. ],
  761. offlineFileIds: [
  762. {
  763. validator: (_rule, value, callback) => {
  764. if (this.terminateForm.signingMethod !== '线下') return callback();
  765. if (Array.isArray(value) && value.length) return callback();
  766. callback(new Error('线下解除请上传离职证明附件'));
  767. },
  768. trigger: 'change',
  769. },
  770. ],
  771. },
  772. voidVisible: false,
  773. voidSubmitting: false,
  774. voidForm: blankVoidForm(),
  775. voidRules: {
  776. reason: [{ required: true, message: '请填写作废原因', trigger: 'blur' }],
  777. },
  778. archiveVisible: false,
  779. archiveSubmitting: false,
  780. archiveForm: blankArchiveForm(),
  781. archiveRules: {
  782. fileIds: [
  783. {
  784. validator: (_rule, value, callback) => {
  785. if (Array.isArray(value) && value.length) return callback();
  786. callback(new Error('请上传双方签署完成的扫描件'));
  787. },
  788. trigger: 'change',
  789. },
  790. ],
  791. signingDate: [{ required: true, message: '请选择实际签署日期', trigger: 'change' }],
  792. },
  793. columns: [
  794. { type: 'index', label: '序号', width: 50, align: 'center', headerAlign: 'center' },
  795. { prop: 'contractNo', label: '合同编号', minWidth: 140, slot: 'contractNo', showOverflowTooltip: true },
  796. { prop: 'employeeName', label: '员工', minWidth: 200, slot: 'employee' },
  797. { prop: 'legalEntity', label: '所属机构', minWidth: 150, showOverflowTooltip: true },
  798. { prop: 'department', label: '所属部门', minWidth: 120, showOverflowTooltip: true },
  799. { prop: 'positionName', label: '岗位名称', minWidth: 120, showOverflowTooltip: true },
  800. { prop: 'contractType', label: '合同类型', minWidth: 140, showOverflowTooltip: true },
  801. { prop: 'signMethod', label: '签署方式', width: 100, align: 'center', slot: 'signMethod' },
  802. // { prop: 'signingStatusLabel', label: '签署状态', width: 110, align: 'center', slot: 'signingStatus' },
  803. { prop: 'status', label: '合同状态', width: 120, slot: 'status', align: 'center' },
  804. { prop: 'archived', label: '是否已归档', width: 130, slot: 'archived', align: 'center' },
  805. { prop: 'signDate', label: '签订日期', width: 100, align: 'center' },
  806. { prop: 'createTime', label: '创建时间', width: 100, align: 'center' },
  807. { prop: 'createUserName', label: '创建人', width: 110, align: 'center' },
  808. { prop: 'updateTime', label: '修改时间', width: 100, align: 'center' },
  809. { prop: 'updateUserName', label: '修改人', width: 110, align: 'center' },
  810. { prop: 'action', label: '操作', width: 340, slot: 'action', fixed: 'right' },
  811. ],
  812. };
  813. },
  814. computed: {
  815. styleResponsive() {
  816. return this.$store.state.theme?.styleResponsive !== false;
  817. },
  818. selectedEmployee() {
  819. return this.employeeOptions.find((e) => String(e.id) === String(this.createForm.employeeId)) || null;
  820. },
  821. isOnlineSign() {
  822. return this.createForm.signMethod === '线上';
  823. },
  824. isChangeMode() {
  825. return this.createMode === 'change';
  826. },
  827. isRenewMode() {
  828. return this.createMode === 'renew';
  829. },
  830. isCreateMode() {
  831. return this.createMode === 'create';
  832. },
  833. isEditMode() {
  834. return this.createMode === 'edit';
  835. },
  836. isCompareMode() {
  837. return this.isChangeMode || this.isRenewMode;
  838. },
  839. /** 合同基础信息区块标题:按弹框模式区分 */
  840. createBaseSectionTitle() {
  841. if (this.isRenewMode) return '续签合同基础信息';
  842. if (this.isChangeMode) return '变更合同基础信息';
  843. if (this.isEditMode) return '编辑合同基础信息';
  844. return '合同基础信息';
  845. },
  846. createDialogTitle() {
  847. if (this.isRenewMode) return '续签合同';
  848. if (this.isChangeMode) return '变更合同';
  849. if (this.isEditMode) return '编辑合同';
  850. return '发起合同';
  851. },
  852. },
  853. watch: {
  854. '$route.query': {
  855. deep: true,
  856. handler() {
  857. this.consumeCreateQuery();
  858. this.consumeViewEditQuery();
  859. },
  860. },
  861. },
  862. created() {
  863. this.consumeCreateQuery();
  864. this.consumeViewEditQuery();
  865. this.loadOptions();
  866. },
  867. activated() {
  868. this.consumeCreateQuery();
  869. this.consumeViewEditQuery();
  870. },
  871. methods: {
  872. typeLabel,
  873. formatSalary(value) {
  874. if (value === null || value === undefined || value === '') return '-';
  875. const num = Number(value);
  876. if (!Number.isFinite(num)) return String(value);
  877. return `¥ ${num.toLocaleString()}`;
  878. },
  879. formatRemainingDays(value) {
  880. if (value === null || value === undefined || value === '') return '-';
  881. const days = Number(value);
  882. if (!Number.isFinite(days)) return String(value);
  883. if (days < 0) return `已超期 ${Math.abs(days)} 天`;
  884. return `${days} 天`;
  885. },
  886. remainingClass(value) {
  887. if (value === null || value === undefined || value === '') return '';
  888. const days = Number(value);
  889. if (!Number.isFinite(days)) return '';
  890. if (days < 0) return 'is-overdue';
  891. if (days <= 30) return 'is-urgent';
  892. if (days <= 90) return 'is-warn';
  893. return 'is-safe';
  894. },
  895. nowStr() {
  896. const d = new Date();
  897. const pad = (n) => String(n).padStart(2, '0');
  898. return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
  899. },
  900. signMethodTagType(value) {
  901. return value === '线上' || value === 'ONLINE' ? 'primary' : 'info';
  902. },
  903. signingStatusTagType(label) {
  904. const map = {
  905. 未签署: 'info',
  906. 待签署: 'warning',
  907. 签署中: '',
  908. 已签署: 'success',
  909. 已拒签: 'danger',
  910. 已过期: 'warning',
  911. 已取消: 'info',
  912. };
  913. return map[label] || 'info';
  914. },
  915. statusClass(s) {
  916. const label = contractStatusLabel(s, s);
  917. const map = {
  918. 草稿: 'draft', 审批中: 'pending', 待签署: 'pending', 待签: 'pending', 签署中: 'signing',
  919. 已签待生效: 'pending', 待生效: 'pending', 生效: 'active', 生效中: 'active', 临期: 'expiring', 即将到期: 'expiring',
  920. 已变更: 'changed', 已续签: 'active', 已解除: 'terminated', 已终止: 'terminated',
  921. 已作废: 'expired', 已取消: 'expired', 已归档: 'archived', 试用期: 'trial', 已到期: 'expired', 已过期: 'expired',
  922. };
  923. return map[label] || map[s] || 'pending';
  924. },
  925. statusTagType(s) {
  926. const tone = this.statusClass(s);
  927. const map = {
  928. draft: 'info',
  929. pending: 'warning',
  930. signing: '',
  931. active: 'success',
  932. expiring: 'warning',
  933. changed: '',
  934. terminated: 'danger',
  935. expired: 'info',
  936. archived: 'success',
  937. trial: 'warning',
  938. };
  939. return map[tone] || 'info';
  940. },
  941. rootOrg(id) {
  942. let node = this.orgById[String(id || '')];
  943. const seen = new Set();
  944. while (node && node.parentId && this.orgById[String(node.parentId)] && !seen.has(String(node.id))) {
  945. seen.add(String(node.id));
  946. node = this.orgById[String(node.parentId)];
  947. }
  948. return node || null;
  949. },
  950. pickId(...values) {
  951. for (const value of values) {
  952. if (value == null || value === '') continue;
  953. return String(value);
  954. }
  955. return null;
  956. },
  957. positionLevelOf(user = {}, orgEmp = {}) {
  958. const list = Array.isArray(user.positionList) ? user.positionList : [];
  959. const primary = list.find((item) => Number(item.primaryFlag) === 1) || list[0] || {};
  960. return String(
  961. orgEmp.level ||
  962. primary.positionLevelName ||
  963. user.positionLevelName ||
  964. user.positionLevel ||
  965. user.level ||
  966. '',
  967. );
  968. },
  969. adaptEmployee(user = {}, seed = {}) {
  970. const orgEmp = adaptOrgEmployee(user || {});
  971. const departmentId = this.pickId(
  972. orgEmp.deptId,
  973. Array.isArray(user.deptIds) ? user.deptIds[0] : null,
  974. user.deptId,
  975. user.targetDepartmentId,
  976. user.groupId,
  977. seed.departmentId,
  978. );
  979. const org = this.orgById[String(departmentId || '')];
  980. const legal = this.rootOrg(departmentId || user.groupId || orgEmp.groupId || seed.legalEntityId);
  981. const positionBindings = Array.isArray(orgEmp.positionBindings) ? orgEmp.positionBindings : [];
  982. const primaryPosition = positionBindings.find((item) => Number(item.primaryFlag) === 1) || positionBindings[0] || {};
  983. return {
  984. id: this.pickId(orgEmp.id, user.id, user.userId, seed.employeeId),
  985. employeeNo: String(orgEmp.employeeNo || user.jobNumber || user.employeeNo || seed.employeeNo || ''),
  986. name: String(orgEmp.name || user.name || user.employeeName || seed.employeeName || ''),
  987. department: String(orgEmp.deptName || user.deptName || org?.name || seed.department || ''),
  988. departmentId,
  989. position: String(orgEmp.position || user.postName || user.positionName || user.position || seed.positionName || ''),
  990. positionId: this.pickId(orgEmp.positionId, user.postId, user.positionId, seed.positionId),
  991. positionSequenceId: this.pickId(
  992. primaryPosition.positionSequenceId,
  993. user.positionSequenceId,
  994. ),
  995. positionSequence: String(
  996. primaryPosition.positionSequenceName ||
  997. user.positionSequenceName ||
  998. user.positionSequence ||
  999. '',
  1000. ),
  1001. legalEntity: String(orgEmp.legalEntity || orgEmp.company || legal?.name || user.company || user.groupName || seed.legalEntity || ''),
  1002. legalEntityId: this.pickId(orgEmp.groupId, legal?.id, user.groupId, seed.legalEntityId),
  1003. positionLevelId: this.pickId(
  1004. primaryPosition.positionLevelId,
  1005. user.positionLevelId,
  1006. ),
  1007. positionLevel: String(
  1008. primaryPosition.positionLevelName ||
  1009. user.positionLevelName ||
  1010. user.positionLevel ||
  1011. user.level ||
  1012. '',
  1013. ),
  1014. // 岗位性质:编码(字典值)与名称分别取,不能把编码当名称(否则下拉显示成 "1")
  1015. positionType: String(
  1016. primaryPosition.positionType ||
  1017. user.positionType ||
  1018. orgEmp.employmentType ||
  1019. user.employmentType ||
  1020. '',
  1021. ),
  1022. positionTypeName: String(
  1023. primaryPosition.positionTypeName ||
  1024. user.positionTypeName ||
  1025. orgEmp.employmentTypeName ||
  1026. user.employmentTypeName ||
  1027. '',
  1028. ),
  1029. salary: Number(user.salaryStandard || user.salary || orgEmp.salary || 0) || 0,
  1030. manager: String(orgEmp.manager || user.managerUserName || user.directSuperiorName || user.managerName || user.manager || ''),
  1031. phone: String(orgEmp.phone || user.phone || seed.phone || ''),
  1032. };
  1033. },
  1034. async loadOptions() {
  1035. try {
  1036. const list = await listOrganizations();
  1037. const records = Array.isArray(list) ? list : [];
  1038. const map = {};
  1039. records.forEach((item) => { map[String(item.id)] = item; });
  1040. this.orgById = map;
  1041. this.departments = records
  1042. .filter((item) => item.parentId && map[String(item.parentId)])
  1043. .map((item) => ({ id: item.id, name: item.name }));
  1044. if (!this.departments.length) {
  1045. this.departments = records.map((item) => ({ id: item.id, name: item.name }));
  1046. }
  1047. } catch (e) {
  1048. this.departments = [];
  1049. }
  1050. try {
  1051. const data = await getContractTemplatePage({ templateStatus: 'ENABLED', pageNum: 1, size: 200 });
  1052. this.contractTemplates = (data?.list || []).map((item) => ({
  1053. id: item.id,
  1054. name: item.templateName || item.name,
  1055. templateVersionId: item.currentVersionId || item.templateVersionId,
  1056. templateVersionNo: item.currentVersionNo || item.templateVersionNo,
  1057. }));
  1058. } catch (e) {
  1059. this.contractTemplates = [];
  1060. }
  1061. await this.searchEmployees('');
  1062. await this.loadTerminateReasons();
  1063. await this.loadPositionNatureOptions();
  1064. },
  1065. isArchived,
  1066. isVoided,
  1067. isDraft,
  1068. isApproving,
  1069. isEffective,
  1070. isDraft,
  1071. isApproving,
  1072. isEffective,
  1073. async loadTerminateReasons() {
  1074. try {
  1075. const res = await getByCode('hr_contract_terminate');
  1076. const list = (res?.data || [])
  1077. .map((item) => {
  1078. const entry = Object.entries(item || {})[0] || [];
  1079. return { value: entry[0], label: entry[1] };
  1080. })
  1081. .filter((item) => item.value);
  1082. this.terminateReasons = list.length ? list : TERMINATE_REASON_FALLBACK;
  1083. } catch (e) {
  1084. this.terminateReasons = TERMINATE_REASON_FALLBACK;
  1085. }
  1086. },
  1087. /**
  1088. * 加载岗位性质字典
  1089. * 与岗位档案(PositionEditDialog)同源:api/sys 的 getDictByCode 已兼容
  1090. * 结构化项与「{ 1: "全职" }」老格式,这里不再自行解析 res.data(直接解析会丢成空选项)
  1091. */
  1092. async loadPositionNatureOptions() {
  1093. try {
  1094. const items = await getDictByCode('position_nature');
  1095. this.positionNatureOptions = (items || [])
  1096. .map((item) => {
  1097. const label = String(item.label ?? '').trim();
  1098. /* 提交口径与岗位档案一致:字典主键 id 优先,其次老格式的 map key */
  1099. const value = item.id != null && item.id !== '' ? item.id : item.key;
  1100. if (!label || value == null || value === '') return null;
  1101. /* 选项值统一转字符串:接口编码可能是数字、回填值可能是字符串,
  1102. 不统一类型时 el-select 严格比较会失配,选中项会显示成裸值 "1" */
  1103. return {
  1104. label,
  1105. value: String(value),
  1106. key: item.key != null ? String(item.key) : String(value),
  1107. id: item.id != null && item.id !== '' ? item.id : value,
  1108. };
  1109. })
  1110. .filter(Boolean);
  1111. } catch (e) {
  1112. this.positionNatureOptions = [];
  1113. }
  1114. },
  1115. /** 岗位性质字典值(字典 id / code)→ 字典文案 */
  1116. positionNatureLabel(value) {
  1117. if (value == null || value === '') return '';
  1118. const hit = this.positionNatureOptions.find(
  1119. (item) => String(item.value) === String(value),
  1120. );
  1121. return hit ? hit.label : '';
  1122. },
  1123. /** 岗位性质文本(名称或字典值)→ 字典值,返回值恒为字符串,保证下拉能匹配回显 */
  1124. matchPositionTypeId(text) {
  1125. if (text == null || text === '') return '';
  1126. const key = String(text).trim();
  1127. if (!key) return '';
  1128. const hit = this.positionNatureOptions.find(
  1129. (item) => String(item.label) === key || String(item.value) === key,
  1130. );
  1131. return hit ? String(hit.value) : '';
  1132. },
  1133. /**
  1134. * 合同详情「岗位性质」展示文本
  1135. * 合同 DTO 存的是 positionTypeName(名称)/ positionType(字典值);
  1136. * 名称缺失或曾被写成编码时,用 position_nature 字典按值反查文案
  1137. */
  1138. positionNatureText(row) {
  1139. if (!row) return '';
  1140. return (
  1141. this.positionNatureLabel(row.positionType) ||
  1142. String(row.positionTypeName || '').trim() ||
  1143. row.employmentType ||
  1144. ''
  1145. );
  1146. },
  1147. /** 岗位性质下拉切换:同步 positionTypeName,并把值规范成字符串 */
  1148. onPositionTypeChange(value) {
  1149. const key = value == null || value === '' ? '' : String(value);
  1150. this.createForm.positionType = key;
  1151. const hit = this.positionNatureOptions.find(
  1152. (item) => String(item.value) === key,
  1153. );
  1154. this.createForm.positionTypeName = hit ? hit.label : '';
  1155. },
  1156. // ===== 岗位序列 / 岗位层级 / 岗位性质级联(与录用管理、办理入职同源接口)=====
  1157. /** 级联接口返回可能是数组 / {list|records|rows|data},统一取数组 */
  1158. toCascadeList(payload) {
  1159. if (Array.isArray(payload)) return payload.filter(Boolean);
  1160. const list = payload?.list || payload?.records || payload?.rows || payload?.data;
  1161. return Array.isArray(list) ? list.filter(Boolean) : [];
  1162. },
  1163. async fetchDeptSequences(deptId) {
  1164. const data = await getDepartmentPositionSequences(deptId);
  1165. return this.toCascadeList(data)
  1166. .filter((item) => item && item.positionSequenceId != null)
  1167. .map((item) => ({ id: item.positionSequenceId, name: item.positionSequence || '' }));
  1168. },
  1169. async fetchDeptPositions(deptId, positionSequenceId) {
  1170. const data = await getDepartmentPositions({ deptId, positionSequenceId });
  1171. return this.toCascadeList(data)
  1172. .filter((item) => item && item.positionId != null)
  1173. .map((item) => ({ id: item.positionId, name: item.positionName || '' }));
  1174. },
  1175. async fetchDeptPositionType(deptId, positionId, positionSequenceId) {
  1176. const data = await getDepartmentPositionType({ deptId, positionId, positionSequenceId });
  1177. return this.toCascadeList(data)[0] || null;
  1178. },
  1179. async fetchDeptPositionLevels(deptId, positionId, positionSequenceId, positionType) {
  1180. if (!positionType) return [];
  1181. const data = await getDepartmentPositionLevels({
  1182. deptId,
  1183. positionId,
  1184. positionSequenceId,
  1185. positionType,
  1186. });
  1187. return this.toCascadeList(data)
  1188. .filter((item) => item && item.positionLevelId != null)
  1189. .map((item) => ({ id: item.positionLevelId, name: item.positionLevel || '' }));
  1190. },
  1191. /** 字典里没有该岗位性质时补一条,保证下拉能显示接口回填的名称 */
  1192. ensurePositionTypeOption(value, label) {
  1193. const key = value == null || value === '' ? '' : String(value);
  1194. if (!key) return '';
  1195. /* 展示文案优先取字典按值反查的文案,其次才是传入名称,
  1196. 最后才回落到编码本身——避免把 code 当名称塞进下拉 */
  1197. const showLabel =
  1198. this.positionNatureLabel(key) || String(label || '').trim() || key;
  1199. const exists = this.positionNatureOptions.some(
  1200. (item) => String(item.value) === key,
  1201. );
  1202. if (!exists) {
  1203. this.positionNatureOptions = [
  1204. ...this.positionNatureOptions,
  1205. { id: key, key, value: key, label: showLabel },
  1206. ];
  1207. }
  1208. return key;
  1209. },
  1210. /**
  1211. * 选择员工后自动补全 岗位序列 / 岗位层级 / 岗位性质
  1212. * 仅补员工自带数据缺失的项;接口异常时静默失败,保留手动填写
  1213. */
  1214. async fillPositionCascadeFromEmployee() {
  1215. const form = this.createForm;
  1216. if (!form || !form.employeeId) return;
  1217. const deptId = this.pickId(form.departmentId);
  1218. const positionId = this.pickId(form.positionId);
  1219. if (!deptId || !positionId) return;
  1220. try {
  1221. // ① 岗位序列:缺失时按部门下各序列反查该岗位所属序列
  1222. const sequences = await this.fetchDeptSequences(deptId);
  1223. if (!form.positionSequenceId) {
  1224. for (const seq of sequences) {
  1225. const positions = await this.fetchDeptPositions(deptId, seq.id);
  1226. const hit =
  1227. positions.find((item) => String(item.id) === String(positionId)) ||
  1228. (form.positionName
  1229. ? positions.find((item) => String(item.name) === String(form.positionName))
  1230. : null);
  1231. if (hit) {
  1232. form.positionSequenceId = seq.id;
  1233. if (!form.positionSequence) form.positionSequence = seq.name;
  1234. break;
  1235. }
  1236. }
  1237. } else if (!form.positionSequence) {
  1238. const seq = sequences.find(
  1239. (item) => String(item.id) === String(form.positionSequenceId),
  1240. );
  1241. if (seq) form.positionSequence = seq.name;
  1242. }
  1243. if (!form.positionSequenceId) return;
  1244. // ② 岗位性质:取接口第一条,回填名称并匹配字典值
  1245. const typeItem = await this.fetchDeptPositionType(
  1246. deptId,
  1247. positionId,
  1248. form.positionSequenceId,
  1249. );
  1250. const typeValue = String(typeItem?.positionType || '').trim();
  1251. /* 名称只认接口给的 positionTypeName,取不到时用字典按编码反查;
  1252. 不能拿编码兜底当名称,否则下拉里出现 "1" */
  1253. const typeName =
  1254. String(typeItem?.positionTypeName || '').trim() ||
  1255. this.positionNatureLabel(typeValue);
  1256. if (!form.positionTypeName && typeName) form.positionTypeName = typeName;
  1257. if (!form.positionType && (typeValue || typeName)) {
  1258. form.positionType =
  1259. this.matchPositionTypeId(form.positionTypeName) ||
  1260. this.ensurePositionTypeOption(typeValue, typeName);
  1261. }
  1262. // ③ 岗位层级:按名称/主键反查,字典唯一时直接回填
  1263. const levels = await this.fetchDeptPositionLevels(
  1264. deptId,
  1265. positionId,
  1266. form.positionSequenceId,
  1267. typeValue,
  1268. );
  1269. if (levels.length) {
  1270. let hit = form.positionLevel
  1271. ? levels.find((item) => String(item.name) === String(form.positionLevel))
  1272. : null;
  1273. if (!hit && form.positionLevelId) {
  1274. hit = levels.find((item) => String(item.id) === String(form.positionLevelId)) || null;
  1275. }
  1276. if (!hit && !form.positionLevel && levels.length === 1) hit = levels[0];
  1277. if (hit) {
  1278. if (!form.positionLevelId) form.positionLevelId = hit.id;
  1279. if (!form.positionLevel) form.positionLevel = hit.name || '';
  1280. }
  1281. }
  1282. } catch (e) {
  1283. // 级联查询失败:保留员工自带数据,允许手动填写
  1284. }
  1285. this.$nextTick(() => {
  1286. this.$refs.createFormRef?.clearValidate([
  1287. 'positionSequence',
  1288. 'positionLevel',
  1289. 'positionType',
  1290. ]);
  1291. });
  1292. },
  1293. applyFilters() {
  1294. this.$refs.table && this.$refs.table.reload();
  1295. },
  1296. resetFilters() {
  1297. this.filters = { keyword: '', contractType: '', contractStatus: '', departmentId: '' };
  1298. this.applyFilters();
  1299. },
  1300. async fetchPage({ page, limit }) {
  1301. try {
  1302. const data = await getContractPage({
  1303. pageNum: page,
  1304. size: limit,
  1305. keyword: this.filters.keyword || undefined,
  1306. contractType: this.filters.contractType || undefined,
  1307. contractStatus: this.filters.contractStatus || undefined,
  1308. departmentId: this.filters.departmentId || undefined,
  1309. });
  1310. const list = (data?.list || data?.records || []).map(adaptContract);
  1311. this.rows = list;
  1312. return { list, count: Number(data?.count ?? data?.total ?? list.length) };
  1313. } catch (e) {
  1314. this.rows = [];
  1315. this.$message.error(e.message || '合同列表加载失败');
  1316. return { list: [], count: 0 };
  1317. }
  1318. },
  1319. /**
  1320. * 员工下拉展开时按空关键字重新拉取一次
  1321. * 远端搜索会把上一次的关键字结果留在 employeeOptions 里,
  1322. * 再次发起合同展开下拉就会只剩上次搜到的人,这里主动刷新规避
  1323. */
  1324. onEmployeeDropdownVisible(visible) {
  1325. if (!visible) return;
  1326. this.searchEmployees('');
  1327. },
  1328. async searchEmployees(keyword) {
  1329. // 请求序号:保证并发搜索时只有最后一次的结果生效
  1330. const seq = (this._employeeSearchSeq || 0) + 1;
  1331. this._employeeSearchSeq = seq;
  1332. this.employeeLoading = true;
  1333. try {
  1334. const data = await getUserPage({
  1335. pageNum: 1,
  1336. size: 50,
  1337. name: keyword || undefined,
  1338. });
  1339. if (seq !== this._employeeSearchSeq) return;
  1340. const list = Array.isArray(data?.list) ? data.list : [];
  1341. const mapped = list.map((item) => this.adaptEmployee(item));
  1342. const selectedId = this.createForm.employeeId;
  1343. const kept = this.employeeOptions.filter(
  1344. (item) => selectedId != null && String(item.id) === String(selectedId),
  1345. );
  1346. const ids = new Set(kept.map((item) => String(item.id)));
  1347. this.employeeOptions = [
  1348. ...kept,
  1349. ...mapped.filter((item) => !ids.has(String(item.id))),
  1350. ];
  1351. } catch (e) {
  1352. if (seq !== this._employeeSearchSeq) return;
  1353. this.employeeOptions = [];
  1354. } finally {
  1355. if (seq === this._employeeSearchSeq) this.employeeLoading = false;
  1356. }
  1357. },
  1358. /**
  1359. * 拟定薪资回填:员工档案(getUserPage / getById)不返回拟定薪资,单独走
  1360. * GET /main/user/current/proposed-salary?userId={userId}。
  1361. * 换员工时以该接口为准(返回空则清空,避免残留上一个人的薪资)。
  1362. */
  1363. async fillProposedSalary(userId) {
  1364. const id = userId || this.createForm.employeeId;
  1365. if (!id) {
  1366. this.createForm.salary = '';
  1367. return;
  1368. }
  1369. try {
  1370. const amount = await getProposedSalary(id);
  1371. this.createForm.salary = amount == null ? '' : String(amount);
  1372. } catch (error) {
  1373. /* 失败不静默:明确报错,保留当前值由用户判断 */
  1374. this.$message.error(error.message || '拟定薪资查询失败');
  1375. }
  1376. },
  1377. async onEmployeeChange(id) {
  1378. if (!id) return;
  1379. /* 字典未就绪时先补一次:否则岗位性质只能回填裸编码(下拉显示 "1") */
  1380. if (!this.positionNatureOptions.length) await this.loadPositionNatureOptions();
  1381. try {
  1382. const detail = await getEmployeeById(id);
  1383. const employee = this.adaptEmployee(detail || {});
  1384. const idx = this.employeeOptions.findIndex((item) => String(item.id) === String(id));
  1385. if (idx > -1) this.$set(this.employeeOptions, idx, employee);
  1386. else this.employeeOptions.unshift(employee);
  1387. this.createForm.employeeNo = employee.employeeNo;
  1388. this.createForm.legalEntity = employee.legalEntity;
  1389. this.createForm.legalEntityId = employee.legalEntityId;
  1390. this.createForm.legalEntityName = employee.legalEntity;
  1391. this.createForm.department = employee.department;
  1392. this.createForm.departmentId = employee.departmentId;
  1393. this.createForm.departmentName = employee.department;
  1394. this.createForm.positionId = employee.positionId || null;
  1395. this.createForm.positionName = employee.position;
  1396. this.createForm.positionSequenceId = employee.positionSequenceId || '';
  1397. this.createForm.positionSequence = employee.positionSequence || '';
  1398. this.createForm.positionLevelId = employee.positionLevelId || '';
  1399. this.createForm.positionLevel = employee.positionLevel || '';
  1400. // 岗位性质:名称取员工档案名称,编码走字典值;名称缺失时用编码反查文案
  1401. this.createForm.positionTypeName =
  1402. employee.positionTypeName || this.positionNatureLabel(employee.positionType);
  1403. this.createForm.positionType =
  1404. this.matchPositionTypeId(employee.positionTypeName) ||
  1405. (employee.positionType ? String(employee.positionType) : '');
  1406. this.createForm.manager = employee.manager;
  1407. // 拟定薪资走独立接口回填(员工档案里没有该字段)
  1408. await this.fillProposedSalary(id);
  1409. // 员工档案缺失时,用部门岗位级联接口补全 序列 / 层级 / 性质
  1410. await this.fillPositionCascadeFromEmployee();
  1411. this.onTemplateDepsChange();
  1412. } catch (e) {
  1413. this.$message.error(e.message || '员工信息加载失败');
  1414. }
  1415. },
  1416. async onTemplateDepsChange() {
  1417. const emp = this.selectedEmployee;
  1418. if (!emp || !this.createForm.contractType || !emp.legalEntityId || !emp.departmentId) return;
  1419. try {
  1420. const matched = await matchContractTemplate({
  1421. contractType: this.createForm.contractType,
  1422. departmentId: emp.departmentId,
  1423. departmentName: emp.department,
  1424. employeeCode: emp.employeeNo,
  1425. employeeId: emp.id,
  1426. employeeName: emp.name,
  1427. legalEntityId: emp.legalEntityId,
  1428. legalEntityName: emp.legalEntity,
  1429. signingMethod: toSigningMethod(this.createForm.signMethod),
  1430. startDate: this.createForm.startDate || todayStr(),
  1431. templateId: this.createForm.templateId || this.contractTemplates[0]?.id,
  1432. templateVersionId: this.createForm.templateVersionId || this.contractTemplates[0]?.templateVersionId,
  1433. positionId: emp.positionId || undefined,
  1434. positionName: emp.position || undefined,
  1435. });
  1436. const list = Array.isArray(matched) ? matched : (matched?.list || []);
  1437. if (!list.length) return;
  1438. this.contractTemplates = list.map((item) => ({
  1439. id: item.id || item.templateId,
  1440. name: item.templateName || item.name,
  1441. templateVersionId: item.templateVersionId || item.currentVersionId,
  1442. templateVersionNo: item.templateVersionNo || item.currentVersionNo,
  1443. })).filter((item) => item.id);
  1444. } catch (e) {
  1445. // 匹配失败时保留已启用模板列表
  1446. }
  1447. },
  1448. async onTemplateChange(id) {
  1449. const version = await this.resolveTemplateVersion(id);
  1450. this.createForm.templateVersionId = version.templateVersionId || null;
  1451. this.createForm.templateVersionNo = version.templateVersionNo || null;
  1452. },
  1453. async resolveTemplateVersion(templateId) {
  1454. if (!templateId) return {};
  1455. const cached = this.contractTemplates.find((item) => String(item.id) === String(templateId));
  1456. if (cached?.templateVersionId) {
  1457. return { templateVersionId: cached.templateVersionId, templateVersionNo: cached.templateVersionNo };
  1458. }
  1459. try {
  1460. const list = await getContractTemplateVersions(templateId);
  1461. const versions = Array.isArray(list) ? list : (list?.list || []);
  1462. const current = versions.find((item) => Number(item.currentFlag) === 1)
  1463. || versions[versions.length - 1]
  1464. || versions[0];
  1465. return {
  1466. templateVersionId: current?.id,
  1467. templateVersionNo: current?.versionNo || current?.currentVersionNo,
  1468. };
  1469. } catch (e) {
  1470. return {};
  1471. }
  1472. },
  1473. async viewDetail(row) {
  1474. this.loading = true;
  1475. try {
  1476. const detail = await getContractById(row.id);
  1477. this.detailRow = adaptContract(detail || row);
  1478. this.detailVisible = true;
  1479. } catch (e) {
  1480. this.$message.error(e.message || '合同详情加载失败');
  1481. } finally {
  1482. this.loading = false;
  1483. }
  1484. },
  1485. /**
  1486. * 导出劳动合同台账。
  1487. * ★ 走后端 POST /hr/contracts/export:复用分页查询的组织范围 + 当前页面全部筛选条件,
  1488. * pageNum / size 不传(文档:不限制导出范围),由后端生成 Excel 文件流。
  1489. * 失败(含「HTTP 200 + JSON 业务错误」)由 downloadBlob 抛出 → 这里统一提示,不做兜底。
  1490. */
  1491. async exportList() {
  1492. if (this.exporting) return;
  1493. this.exporting = true;
  1494. try {
  1495. await exportContractLedger({
  1496. keyword: this.filters.keyword || undefined,
  1497. contractType: this.filters.contractType || undefined,
  1498. contractStatus: this.filters.contractStatus || undefined,
  1499. departmentId: this.filters.departmentId || undefined,
  1500. });
  1501. this.$message.success('导出成功');
  1502. } catch (error) {
  1503. this.$message.error(error.message || '导出失败');
  1504. } finally {
  1505. this.exporting = false;
  1506. }
  1507. },
  1508. async openCreate() {
  1509. this.createMode = 'create';
  1510. this.changeSnapshot = blankChangeSnapshot();
  1511. this.createForm = blankCreateForm();
  1512. this.createVisible = true;
  1513. if (!this.employeeOptions.length) await this.searchEmployees('');
  1514. },
  1515. upsertEmployeeOption(employee) {
  1516. if (!employee?.id) return;
  1517. const idx = this.employeeOptions.findIndex(
  1518. (item) => String(item.id) === String(employee.id),
  1519. );
  1520. if (idx > -1) this.$set(this.employeeOptions, idx, employee);
  1521. else this.employeeOptions.unshift(employee);
  1522. },
  1523. applyEmployeeToForm(employee = {}) {
  1524. this.createForm.employeeId = employee.id || null;
  1525. this.createForm.employeeNo = employee.employeeNo || '';
  1526. this.createForm.legalEntity = employee.legalEntity || '';
  1527. this.createForm.legalEntityId = employee.legalEntityId || null;
  1528. this.createForm.legalEntityName = employee.legalEntity || '';
  1529. this.createForm.department = employee.department || '';
  1530. this.createForm.departmentId = employee.departmentId || null;
  1531. this.createForm.departmentName = employee.department || '';
  1532. this.createForm.positionId = employee.positionId || null;
  1533. this.createForm.positionName = employee.position || '';
  1534. this.createForm.positionSequenceId = employee.positionSequenceId || '';
  1535. this.createForm.positionSequence = employee.positionSequence || '';
  1536. this.createForm.positionLevelId = employee.positionLevelId || '';
  1537. this.createForm.positionLevel = employee.positionLevel || '';
  1538. this.createForm.positionTypeName =
  1539. employee.positionTypeName || this.positionNatureLabel(employee.positionType);
  1540. this.createForm.positionType =
  1541. this.matchPositionTypeId(employee.positionTypeName) ||
  1542. (employee.positionType ? String(employee.positionType) : '');
  1543. this.createForm.manager = employee.manager || '';
  1544. if (employee.salary) this.createForm.salary = employee.salary;
  1545. // 员工档案缺失时,用部门岗位级联接口补全 序列 / 层级 / 性质
  1546. this.fillPositionCascadeFromEmployee();
  1547. },
  1548. /** 从办理入职等页面跳转时,自动打开发起合同并带入员工 */
  1549. async consumeCreateQuery() {
  1550. const query = this.$route?.query || {};
  1551. if (query.action !== 'create') return;
  1552. const consumeKey = [
  1553. query.employeeId,
  1554. query.employeeName,
  1555. query.departmentId,
  1556. query.positionId,
  1557. ].join('|');
  1558. if (this._consumedCreateKey === consumeKey || this._consumingCreateQuery) return;
  1559. this._consumingCreateQuery = true;
  1560. this._consumedCreateKey = consumeKey;
  1561. try {
  1562. await this.openCreateWithEmployee(query);
  1563. } catch (error) {
  1564. this._consumedCreateKey = '';
  1565. this.$message.error(error?.message || '带入员工信息失败');
  1566. } finally {
  1567. this._consumingCreateQuery = false;
  1568. }
  1569. },
  1570. clearCreateQuery() {
  1571. const query = this.$route?.query || {};
  1572. if (query.action !== 'create') return;
  1573. const nextQuery = { ...query };
  1574. [
  1575. 'action',
  1576. 'employeeId',
  1577. 'employeeName',
  1578. 'employeeNo',
  1579. 'phone',
  1580. 'departmentId',
  1581. 'department',
  1582. 'positionId',
  1583. 'positionName',
  1584. 'legalEntityId',
  1585. 'legalEntity',
  1586. ].forEach((key) => {
  1587. delete nextQuery[key];
  1588. });
  1589. this._consumedCreateKey = '';
  1590. this.$router.replace({ path: this.$route.path, query: nextQuery }).catch(() => { });
  1591. },
  1592. /** 从办理入职跳转时,自动打开合同详情(view) */
  1593. async consumeViewEditQuery() {
  1594. const query = this.$route?.query || {};
  1595. if (query.action !== 'view') return;
  1596. const contractId = query.contractId;
  1597. if (!contractId) return;
  1598. const consumeKey = `view|${contractId}`;
  1599. if (this._consumedViewEditKey === consumeKey || this._consumingViewEditQuery) return;
  1600. this._consumingViewEditQuery = true;
  1601. this._consumedViewEditKey = consumeKey;
  1602. try {
  1603. await this.viewDetail({ id: contractId });
  1604. this.clearViewEditQuery();
  1605. } catch (error) {
  1606. this._consumedViewEditKey = '';
  1607. this.$message.error(error?.message || '合同详情加载失败');
  1608. } finally {
  1609. this._consumingViewEditQuery = false;
  1610. }
  1611. },
  1612. clearViewEditQuery() {
  1613. const query = this.$route?.query || {};
  1614. if (query.action !== 'view') return;
  1615. const nextQuery = { ...query };
  1616. ['action', 'contractId', 'employeeId', 'employeeName'].forEach((key) => {
  1617. delete nextQuery[key];
  1618. });
  1619. this._consumedViewEditKey = '';
  1620. this.$router.replace({ path: this.$route.path, query: nextQuery }).catch(() => { });
  1621. },
  1622. async openCreateWithEmployee(seed = {}) {
  1623. this.createMode = 'create';
  1624. this.changeSnapshot = blankChangeSnapshot();
  1625. this.createForm = blankCreateForm();
  1626. this.createVisible = true;
  1627. const employeeId = seed.employeeId != null && seed.employeeId !== ''
  1628. ? String(seed.employeeId)
  1629. : '';
  1630. const seeded = this.adaptEmployee({}, seed);
  1631. if (employeeId) seeded.id = employeeId;
  1632. if (seeded.id) {
  1633. this.upsertEmployeeOption(seeded);
  1634. this.applyEmployeeToForm(seeded);
  1635. await this.fillProposedSalary(seeded.id);
  1636. }
  1637. if (!employeeId) {
  1638. const name = String(seed.employeeName || '').trim();
  1639. if (name) {
  1640. await this.searchEmployees(name);
  1641. const hit = this.employeeOptions.find((item) => item.name === name)
  1642. || this.employeeOptions[0];
  1643. if (hit) {
  1644. this.applyEmployeeToForm(hit);
  1645. await this.fillProposedSalary(hit.id);
  1646. await this.onTemplateDepsChange();
  1647. return;
  1648. }
  1649. }
  1650. if (!this.employeeOptions.length) await this.searchEmployees('');
  1651. return;
  1652. }
  1653. try {
  1654. const detail = await getEmployeeById(employeeId);
  1655. const employee = this.adaptEmployee(detail || {}, seed);
  1656. employee.id = String(employee.id || employeeId);
  1657. this.upsertEmployeeOption(employee);
  1658. this.applyEmployeeToForm(employee);
  1659. await this.fillProposedSalary(employee.id);
  1660. await this.onTemplateDepsChange();
  1661. } catch (error) {
  1662. await this.onTemplateDepsChange();
  1663. }
  1664. },
  1665. closeCreate(done) {
  1666. this.createVisible = false;
  1667. this.createMode = 'create';
  1668. this.changeSnapshot = blankChangeSnapshot();
  1669. this.createForm = blankCreateForm();
  1670. this.clearCreateQuery();
  1671. if (typeof done === 'function') done();
  1672. },
  1673. async buildCreatePayload(form, extra = {}) {
  1674. const emp = this.selectedEmployee;
  1675. if (!emp) {
  1676. this.$message.warning('请选择员工');
  1677. return null;
  1678. }
  1679. const departmentId = form.departmentId || emp.departmentId;
  1680. const departmentName = form.departmentName || emp.department;
  1681. const legalEntityId = form.legalEntityId || emp.legalEntityId;
  1682. const legalEntityName = form.legalEntityName || emp.legalEntity;
  1683. if (!legalEntityId || !departmentId) {
  1684. this.$message.warning('员工缺少所属机构或部门主键,无法创建合同');
  1685. return null;
  1686. }
  1687. const online = form.signMethod === '线上';
  1688. let templateId = form.templateId;
  1689. let templateVersionId = form.templateVersionId;
  1690. let templateVersionNo = form.templateVersionNo;
  1691. if (!online && !templateId) {
  1692. templateId = this.contractTemplates[0]?.id;
  1693. templateVersionId = this.contractTemplates[0]?.templateVersionId;
  1694. templateVersionNo = this.contractTemplates[0]?.templateVersionNo;
  1695. }
  1696. if (templateId && !templateVersionId) {
  1697. const version = await this.resolveTemplateVersion(templateId);
  1698. templateVersionId = version.templateVersionId;
  1699. templateVersionNo = version.templateVersionNo;
  1700. }
  1701. if (online && (!templateId || !templateVersionId)) {
  1702. this.$message.warning('线上签订请选择已启用模板及其版本');
  1703. return null;
  1704. }
  1705. if (!online && !(form.fileIds || []).length) {
  1706. this.$message.warning('线下签订请上传合同附件');
  1707. return null;
  1708. }
  1709. if (!form.endDate) {
  1710. this.$message.warning('请选择合同终止日期');
  1711. return null;
  1712. }
  1713. const salary = Number(String(form.salary || '').replace(/[^\d.]/g, '')) || undefined;
  1714. return compact({
  1715. contractType: form.contractType,
  1716. departmentId,
  1717. departmentName,
  1718. employeeCode: emp.employeeNo,
  1719. employeeId: emp.id,
  1720. employeeName: emp.name,
  1721. legalEntityId,
  1722. legalEntityName,
  1723. signingMethod: toSigningMethod(form.signMethod),
  1724. startDate: form.startDate,
  1725. endDate: form.endDate,
  1726. contractTermMonths: monthsBetween(form.startDate, form.endDate),
  1727. templateId,
  1728. templateVersionId,
  1729. templateVersionNo,
  1730. positionId: form.positionId || emp.positionId || undefined,
  1731. positionName: form.positionName || emp.position || undefined,
  1732. positionSequenceId: form.positionSequenceId || emp.positionSequenceId || undefined,
  1733. positionSequence: form.positionSequence || emp.positionSequence || undefined,
  1734. positionLevelId: form.positionLevelId || emp.positionLevelId || undefined,
  1735. positionLevel: form.positionLevel || emp.positionLevel || undefined,
  1736. positionType: form.positionType || emp.positionType || undefined,
  1737. positionTypeName: form.positionTypeName || emp.positionTypeName || undefined,
  1738. salaryStandard: salary,
  1739. workLocation: extra.workLocation,
  1740. attachmentIds: online ? undefined : joinAttachmentIds(form.fileIds || form.fileId),
  1741. ...extra,
  1742. });
  1743. },
  1744. pickCreatedId(created) {
  1745. if (created == null || created === '') return null;
  1746. if (typeof created === 'object') return created.id || created.contractId || null;
  1747. return created;
  1748. },
  1749. async persistCreate() {
  1750. const valid = this.$refs.createFormRef
  1751. ? await this.$refs.createFormRef.validate().catch(() => false)
  1752. : false;
  1753. if (!valid) return null;
  1754. const payload = await this.buildCreatePayload(this.createForm);
  1755. if (!payload) return null;
  1756. if (this.isChangeMode) {
  1757. const form = this.createForm;
  1758. await changeContract(form.id, compact({
  1759. changeType: 'OTHER',
  1760. changeReason: '合同变更',
  1761. effectiveDate: form.startDate || todayStr(),
  1762. signingMethod: payload.signingMethod,
  1763. offlineFileId: form.signMethod === '线下' ? (form.fileIds?.[0] || form.fileId) : undefined,
  1764. templateId: payload.templateId,
  1765. templateVersionId: payload.templateVersionId,
  1766. changedValues: compact({
  1767. salaryStandard: payload.salaryStandard,
  1768. startDate: payload.startDate,
  1769. endDate: payload.endDate,
  1770. contractType: payload.contractType,
  1771. legalEntityId: payload.legalEntityId,
  1772. legalEntityName: payload.legalEntityName,
  1773. departmentId: payload.departmentId,
  1774. departmentName: payload.departmentName,
  1775. positionId: payload.positionId,
  1776. positionName: payload.positionName,
  1777. positionSequenceId: payload.positionSequenceId,
  1778. positionSequence: payload.positionSequence,
  1779. positionLevelId: payload.positionLevelId,
  1780. positionLevel: payload.positionLevel,
  1781. positionType: payload.positionType,
  1782. positionTypeName: payload.positionTypeName,
  1783. }),
  1784. }));
  1785. return { id: form.id, payload };
  1786. }
  1787. if (this.isRenewMode) {
  1788. const created = await renewContract(this.createForm.id, {
  1789. contract: compact({
  1790. ...payload,
  1791. renewal: true,
  1792. probationMonths: 0,
  1793. }),
  1794. });
  1795. return { id: this.pickCreatedId(created) || this.createForm.id, payload };
  1796. }
  1797. if (this.isEditMode) {
  1798. const form = this.createForm;
  1799. if (!form.id) {
  1800. this.$message.warning('缺少合同主键,无法修改');
  1801. return null;
  1802. }
  1803. await updateContract(form.id, compact({ ...payload, id: form.id }));
  1804. return { id: form.id, payload };
  1805. }
  1806. const created = await saveContract(payload);
  1807. return { id: this.pickCreatedId(created), payload };
  1808. },
  1809. async saveCreateDraft() {
  1810. this.saving = true;
  1811. try {
  1812. const result = await this.persistCreate();
  1813. if (!result) return;
  1814. this.$message.success(
  1815. this.isRenewMode
  1816. ? '续签草稿已保存'
  1817. : this.isChangeMode
  1818. ? '合同变更已保存'
  1819. : this.isEditMode
  1820. ? '合同草稿已修改'
  1821. : '合同已保存',
  1822. );
  1823. this.closeCreate();
  1824. this.applyFilters();
  1825. } catch (e) {
  1826. this.$message.error(e.message || '保存失败');
  1827. } finally {
  1828. this.saving = false;
  1829. }
  1830. },
  1831. async submitCreate() {
  1832. this.saving = true;
  1833. try {
  1834. const result = await this.persistCreate();
  1835. if (!result) return;
  1836. if (!result.id) {
  1837. this.$message.warning('合同已保存,但未返回主键,无法提交审批');
  1838. this.closeCreate();
  1839. this.applyFilters();
  1840. return;
  1841. }
  1842. const emp = this.selectedEmployee || {};
  1843. const launchRow = {
  1844. id: result.id,
  1845. contractNo: this.createForm.contractNo,
  1846. employeeName: emp.name,
  1847. createUserId: emp.createUserId,
  1848. };
  1849. this.closeCreate();
  1850. this.applyFilters();
  1851. this.$nextTick(() => this.launchApproval(launchRow));
  1852. } catch (e) {
  1853. this.$message.error(e.message || '提交失败');
  1854. } finally {
  1855. this.saving = false;
  1856. }
  1857. },
  1858. launchApproval(row) {
  1859. if (!row?.id) {
  1860. this.$message.warning('请先保存合同后再提交审批');
  1861. return;
  1862. }
  1863. this.currentLaunchRow = row;
  1864. this.processSubmitDialogFlag = true;
  1865. this.$nextTick(() => {
  1866. const currentUser = getCurrentUser() || {};
  1867. this.$refs.processSubmitDialogRef?.init({
  1868. businessId: row.id,
  1869. businessKey: CONTRACT_APPROVAL_BUSINESS_KEY,
  1870. formCreateUserId:
  1871. row.createUserId || currentUser.id || currentUser.userId || '',
  1872. variables: {
  1873. businessCode: row.contractNo || '',
  1874. businessName: row.employeeName || '',
  1875. businessType: '劳动合同',
  1876. },
  1877. });
  1878. });
  1879. },
  1880. onProcessSubmitReload() {
  1881. this.currentLaunchRow = null;
  1882. this.applyFilters();
  1883. },
  1884. /** 已作废合同整行画删除线 */
  1885. tableRowClassName({ row }) {
  1886. if (isVoided(row)) return 'cm-row-voided';
  1887. return '';
  1888. },
  1889. /** 查看审批记录:优先用列表 row 上的 processInstanceId;没有就调详情接口补查,再没有才提示 */
  1890. async openApprovalRecord(row) {
  1891. const rowAdapted = adaptContract(row || {});
  1892. let id = rowAdapted.processInstanceId || rowAdapted.approvalInstanceId;
  1893. if (!id && row?.id) {
  1894. try {
  1895. const detail = await getContractById(row.id);
  1896. const detailAdapted = adaptContract(detail || {});
  1897. id = detailAdapted.processInstanceId || detailAdapted.approvalInstanceId;
  1898. } catch (e) {
  1899. // 详情拿不到也没关系,后面统一提示
  1900. }
  1901. }
  1902. if (!id) {
  1903. this.$message.warning('暂无审批记录(后端未返回流程实例 id)');
  1904. return;
  1905. }
  1906. this.$refs.approvalRecordDialog?.open({ processInstanceId: id });
  1907. },
  1908. /** 线上模板功能未开通的统一提示:先关掉上一条,避免连点堆叠多条提示 */
  1909. warnTemplateDisabled() {
  1910. this.$message.closeAll();
  1911. this.$message.warning('暂无开通模板功能,敬请期待');
  1912. },
  1913. /** 合同模版下拉展开时提示(编辑历史线上合同时会落到这里) */
  1914. onTemplateDropdown(visible) {
  1915. if (visible) this.warnTemplateDisabled();
  1916. },
  1917. onSignMethodChange(method) {
  1918. // 线上签订依赖模板生成正文,模板未开通时只提示、不切换签署方式
  1919. if (method === '线上') {
  1920. this.warnTemplateDisabled();
  1921. return;
  1922. }
  1923. if (this.createForm.signMethod === method) return;
  1924. this.createForm.signMethod = method;
  1925. // 模板与附件互斥:切换时把另一侧已选/已传的内容清空,避免提交时同时带上模板与附件造成重复
  1926. this.createForm.templateId = null;
  1927. this.createForm.templateVersionId = null;
  1928. this.createForm.templateVersionNo = null;
  1929. this.createForm.fileIds = [];
  1930. this.createForm.fileId = null;
  1931. this.$nextTick(() => this.$refs.createFormRef?.clearValidate(['templateId', 'fileIds']));
  1932. },
  1933. // generateAndSubmit 已废弃:/hr/contracts/{id}/submit 接口不再使用,提交审批改由 launchApproval 走公共审批组件
  1934. async fillCreateFormFromContract(row) {
  1935. const detail = adaptContract(await getContractById(row.id));
  1936. this.createForm = {
  1937. ...blankCreateForm(),
  1938. id: detail.id,
  1939. employeeId: detail.employeeId || null,
  1940. employeeNo: detail.employeeNo || '',
  1941. contractNo: detail.contractNo || '',
  1942. contractType: detail.contractTypeCode || 'FIXED',
  1943. templateId: detail.templateId || null,
  1944. templateVersionId: detail.templateVersionId || null,
  1945. templateVersionNo: detail.templateVersionNo || null,
  1946. legalEntity: detail.legalEntity || '',
  1947. legalEntityId: detail.legalEntityId || null,
  1948. legalEntityName: detail.legalEntityName || detail.legalEntity || '',
  1949. department: detail.department || '',
  1950. departmentId: detail.departmentId || null,
  1951. departmentName: detail.departmentName || detail.department || '',
  1952. positionId: detail.positionId || null,
  1953. positionName: detail.positionName || '',
  1954. positionSequenceId: detail.positionSequenceId || '',
  1955. positionSequence: detail.positionSequence || '',
  1956. positionLevelId: detail.positionLevelId || '',
  1957. positionLevel: detail.positionLevel || detail.level || '',
  1958. // 合同详情回显:编码统一字符串(与下拉选项同类型),名称能按编码反查到字典文案时以字典为准
  1959. positionType:
  1960. detail.positionType != null && detail.positionType !== ''
  1961. ? String(detail.positionType)
  1962. : '',
  1963. positionTypeName:
  1964. this.positionNatureLabel(detail.positionType) ||
  1965. String(detail.positionTypeName || ''),
  1966. startDate: detail.startDate || '',
  1967. endDate: detail.endDate || '',
  1968. salary: detail.salary != null && detail.salary !== '' ? String(detail.salary) : '',
  1969. manager: detail.manager || '',
  1970. signMethod: ['线上', '线下'].includes(detail.signMethod) ? detail.signMethod : '线下',
  1971. fileIds: detail.attachmentFileIds || [],
  1972. fileId: (detail.attachmentFileIds && detail.attachmentFileIds[0]) || null,
  1973. };
  1974. this.changeSnapshot = {
  1975. position: detail.positionName || '',
  1976. level: detail.level || '',
  1977. department: detail.department || '',
  1978. manager: detail.manager || '',
  1979. salary: detail.salary || '',
  1980. endDate: detail.endDate || '',
  1981. };
  1982. if (detail.employeeId) {
  1983. try {
  1984. const employee = this.adaptEmployee(await getEmployeeById(detail.employeeId), {
  1985. employeeId: detail.employeeId,
  1986. employeeName: detail.employeeName,
  1987. employeeNo: detail.employeeNo,
  1988. departmentId: detail.departmentId,
  1989. department: detail.department,
  1990. legalEntityId: detail.legalEntityId,
  1991. legalEntity: detail.legalEntity,
  1992. positionId: detail.positionId,
  1993. positionName: detail.positionName,
  1994. });
  1995. this.upsertEmployeeOption(employee);
  1996. this.createForm.employeeId = employee.id;
  1997. this.createForm.employeeNo = employee.employeeNo || this.createForm.employeeNo;
  1998. if (!this.createForm.departmentId) this.createForm.departmentId = employee.departmentId;
  1999. if (!this.createForm.departmentName) {
  2000. this.createForm.departmentName = employee.department;
  2001. this.createForm.department = employee.department;
  2002. }
  2003. if (!this.createForm.legalEntityId) this.createForm.legalEntityId = employee.legalEntityId;
  2004. if (!this.createForm.legalEntityName) {
  2005. this.createForm.legalEntityName = employee.legalEntity;
  2006. this.createForm.legalEntity = employee.legalEntity;
  2007. }
  2008. if (!this.createForm.positionId) this.createForm.positionId = employee.positionId || null;
  2009. if (!this.createForm.positionName) this.createForm.positionName = employee.position || '';
  2010. if (!this.createForm.positionSequenceId) this.createForm.positionSequenceId = employee.positionSequenceId || '';
  2011. if (!this.createForm.positionSequence) this.createForm.positionSequence = employee.positionSequence || '';
  2012. if (!this.createForm.positionLevelId) this.createForm.positionLevelId = employee.positionLevelId || '';
  2013. if (!this.createForm.positionLevel) this.createForm.positionLevel = employee.positionLevel || '';
  2014. if (!this.createForm.positionTypeName) {
  2015. this.createForm.positionTypeName =
  2016. employee.positionTypeName || this.positionNatureLabel(employee.positionType);
  2017. }
  2018. if (!this.createForm.positionType) {
  2019. this.createForm.positionType =
  2020. this.matchPositionTypeId(employee.positionTypeName) ||
  2021. (employee.positionType ? String(employee.positionType) : '');
  2022. }
  2023. this.createForm.manager = employee.manager || this.createForm.manager;
  2024. this.changeSnapshot.position = employee.position || this.changeSnapshot.position;
  2025. this.changeSnapshot.level = employee.level || this.changeSnapshot.level;
  2026. this.changeSnapshot.department = employee.department || this.changeSnapshot.department;
  2027. this.changeSnapshot.manager = employee.manager || this.changeSnapshot.manager;
  2028. if (employee.salary) this.changeSnapshot.salary = employee.salary;
  2029. } catch (error) {
  2030. this.upsertEmployeeOption({
  2031. id: detail.employeeId,
  2032. name: detail.employeeName || String(detail.employeeId),
  2033. employeeNo: detail.employeeNo || '',
  2034. });
  2035. }
  2036. }
  2037. return detail;
  2038. },
  2039. async openEdit(row) {
  2040. if (!row?.id) {
  2041. this.$message.warning('缺少合同主键,无法编辑');
  2042. return;
  2043. }
  2044. if (!isDraft(row)) {
  2045. this.$message.warning('仅未提交审批的合同草稿可以修改');
  2046. return;
  2047. }
  2048. this.createMode = 'edit';
  2049. this.createForm = blankCreateForm();
  2050. this.changeSnapshot = blankChangeSnapshot();
  2051. this.createVisible = true;
  2052. this.loading = true;
  2053. try {
  2054. await this.fillCreateFormFromContract(row);
  2055. this.$nextTick(() => this.$refs.createFormRef?.clearValidate());
  2056. } catch (e) {
  2057. this.$message.error(e.message || '合同详情加载失败');
  2058. this.closeCreate();
  2059. } finally {
  2060. this.loading = false;
  2061. }
  2062. },
  2063. async openChange(row) {
  2064. if (!row?.id) {
  2065. this.$message.warning('缺少合同主键,无法变更');
  2066. return;
  2067. }
  2068. if (!isEffective(row)) {
  2069. this.$message.warning('仅审批通过并签署成功的合同可以变更');
  2070. return;
  2071. }
  2072. this.createMode = 'change';
  2073. this.createForm = blankCreateForm();
  2074. this.changeSnapshot = blankChangeSnapshot();
  2075. this.createVisible = true;
  2076. this.loading = true;
  2077. try {
  2078. await this.fillCreateFormFromContract(row);
  2079. this.$nextTick(() => this.$refs.createFormRef?.clearValidate());
  2080. } catch (e) {
  2081. this.$message.error(e.message || '合同详情加载失败');
  2082. this.closeCreate();
  2083. } finally {
  2084. this.loading = false;
  2085. }
  2086. },
  2087. async openRenew(row) {
  2088. if (!row?.id) {
  2089. this.$message.warning('缺少合同主键,无法续签');
  2090. return;
  2091. }
  2092. if (!isEffective(row)) {
  2093. this.$message.warning('仅审批通过并签署成功的合同可以续签');
  2094. return;
  2095. }
  2096. this.createMode = 'renew';
  2097. this.createForm = blankCreateForm();
  2098. this.changeSnapshot = blankChangeSnapshot();
  2099. this.createVisible = true;
  2100. this.loading = true;
  2101. try {
  2102. const detail = await this.fillCreateFormFromContract(row);
  2103. this.createForm.contractNo = '';
  2104. if (detail.endDate) {
  2105. this.createForm.startDate = addDays(detail.endDate, 1);
  2106. const months = Number(detail.contractTermMonths) || monthsBetween(detail.startDate, detail.endDate);
  2107. if (this.createForm.contractType !== 'NON_FIXED' && months) {
  2108. this.createForm.endDate = addMonths(this.createForm.startDate, months);
  2109. }
  2110. }
  2111. this.$nextTick(() => this.$refs.createFormRef?.clearValidate());
  2112. } catch (e) {
  2113. this.$message.error(e.message || '合同详情加载失败');
  2114. this.closeCreate();
  2115. } finally {
  2116. this.loading = false;
  2117. }
  2118. },
  2119. openTerminate(row) {
  2120. if (!isEffective(row)) {
  2121. this.$message.warning('仅审批通过并签署成功的合同可以解除');
  2122. return;
  2123. }
  2124. this.terminateForm = {
  2125. ...blankTerminateForm(),
  2126. id: row.id,
  2127. employeeName: row.employeeName || '',
  2128. contractNo: row.contractNo || '',
  2129. signingMethod: row.signMethod || '线下',
  2130. };
  2131. this.terminateVisible = true;
  2132. this.$nextTick(() => this.$refs.terminateFormRef?.clearValidate());
  2133. },
  2134. onTerminateReasonCode(code) {
  2135. const item = this.terminateReasons.find((r) => r.value === code);
  2136. if (item && !this.terminateForm.reason) this.terminateForm.reason = item.label;
  2137. },
  2138. async onTerminateTemplateChange(id) {
  2139. const version = await this.resolveTemplateVersion(id);
  2140. this.terminateForm.templateVersionId = version.templateVersionId || null;
  2141. },
  2142. async submitTerminate() {
  2143. const valid = this.$refs.terminateFormRef
  2144. ? await this.$refs.terminateFormRef.validate().catch(() => false)
  2145. : false;
  2146. if (!valid) return;
  2147. const form = this.terminateForm;
  2148. const signingMethod = toSigningMethod(form.signingMethod);
  2149. this.terminateSubmitting = true;
  2150. try {
  2151. await terminateContract(form.id, compact({
  2152. terminationType: form.terminationType,
  2153. reason: form.reason,
  2154. reasonCode: form.reasonCode,
  2155. effectiveDate: form.effectiveDate,
  2156. lastWorkingDate: form.lastWorkingDate,
  2157. signingMethod,
  2158. templateId: signingMethod === 'ONLINE' ? form.templateId : undefined,
  2159. templateVersionId: signingMethod === 'ONLINE' ? form.templateVersionId : undefined,
  2160. offlineFileId: signingMethod === 'OFFLINE' ? form.offlineFileIds?.[0] : undefined,
  2161. }));
  2162. this.$message.success('已保存解除/终止草稿');
  2163. this.terminateVisible = false;
  2164. this.applyFilters();
  2165. } catch (e) {
  2166. this.$message.error(e.message || '解除失败');
  2167. } finally {
  2168. this.terminateSubmitting = false;
  2169. }
  2170. },
  2171. openVoid(row) {
  2172. if (!isDraft(row)) {
  2173. this.$message.warning('仅未提交审批的合同草稿可以作废');
  2174. return;
  2175. }
  2176. this.voidForm = { id: row.id, reason: '' };
  2177. this.voidVisible = true;
  2178. this.$nextTick(() => this.$refs.voidFormRef?.clearValidate());
  2179. },
  2180. async submitVoid() {
  2181. const valid = this.$refs.voidFormRef
  2182. ? await this.$refs.voidFormRef.validate().catch(() => false)
  2183. : false;
  2184. if (!valid) return;
  2185. this.voidSubmitting = true;
  2186. try {
  2187. await voidContract(this.voidForm.id, { reason: this.voidForm.reason });
  2188. this.$message.success('合同已作废');
  2189. this.voidVisible = false;
  2190. this.applyFilters();
  2191. } catch (e) {
  2192. this.$message.error(e.message || '作废失败');
  2193. } finally {
  2194. this.voidSubmitting = false;
  2195. }
  2196. },
  2197. async handleSign(row) {
  2198. // /hr/contracts/{id}/submit 接口已废弃,合同提交审批改由 launchApproval 走公共审批组件
  2199. this.$message.info('请使用「提交审批」按钮发起合同审批流程');
  2200. },
  2201. openArchive(row) {
  2202. if ((row.signingMethod || toSigningMethod(row.signMethod)) !== 'OFFLINE') {
  2203. this.$message.warning('仅线下签署合同可调用归档接口');
  2204. return;
  2205. }
  2206. this.archiveForm = { ...blankArchiveForm(), id: row.id };
  2207. this.archiveVisible = true;
  2208. this.$nextTick(() => this.$refs.archiveFormRef?.clearValidate());
  2209. },
  2210. async submitArchive() {
  2211. const valid = this.$refs.archiveFormRef
  2212. ? await this.$refs.archiveFormRef.validate().catch(() => false)
  2213. : false;
  2214. if (!valid) return;
  2215. this.archiveSubmitting = true;
  2216. try {
  2217. await archiveContractOffline(this.archiveForm.id, compact({
  2218. fileId: this.archiveForm.fileIds[0],
  2219. signingDate: this.archiveForm.signingDate,
  2220. remark: this.archiveForm.remark,
  2221. }));
  2222. this.$message.success('合同已归档');
  2223. this.archiveVisible = false;
  2224. this.applyFilters();
  2225. } catch (e) {
  2226. this.$message.error(e.message || '归档失败');
  2227. } finally {
  2228. this.archiveSubmitting = false;
  2229. }
  2230. },
  2231. },
  2232. };
  2233. </script>
  2234. <style scoped lang="scss" src="../../styles/views/contractManagement/index.scss"></style>
  2235. <!-- 已作废合同整行删除线:el-table 的行是子组件渲染,必须用非 scoped 穿透 -->
  2236. <style lang="scss">
  2237. .cm-pro-table .cm-row-voided td,
  2238. .cm-pro-table .cm-row-voided td .cell,
  2239. .cm-pro-table .cm-row-voided td .cell * {
  2240. text-decoration: line-through;
  2241. color: #a8abb2;
  2242. }
  2243. </style>