| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285 |
- <template>
- <div class="ele-body" v-loading="loading">
- <el-card shadow="never">
- <el-form label-width="80px" class="ele-form-search" @keyup.enter.native="applyFilters" @submit.native.prevent>
- <el-row :gutter="15">
- <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
- <el-form-item label="合同编号">
- <el-input v-model.trim="filters.keyword" clearable placeholder="编号 / 工号 / 姓名" />
- </el-form-item>
- </el-col>
- <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
- <el-form-item label="合同类型">
- <el-select v-model="filters.contractType" clearable placeholder="请选择">
- <el-option v-for="t in contractTypes" :key="t.value" :label="t.label" :value="t.value" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
- <el-form-item label="合同状态">
- <el-select v-model="filters.contractStatus" clearable placeholder="请选择">
- <el-option v-for="s in contractStatus" :key="s.value" :label="s.label" :value="s.value" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
- <el-form-item label="所属部门">
- <el-select v-model="filters.departmentId" clearable filterable placeholder="请选择">
- <el-option v-for="d in departments" :key="String(d.id)" :label="d.name" :value="d.id" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
- <div class="ele-form-actions">
- <el-button type="primary" icon="el-icon-search" class="ele-btn-icon" @click="applyFilters">
- 查询
- </el-button>
- <el-button type="primary" icon="el-icon-refresh" class="ele-btn-icon" @click="resetFilters">
- 重置
- </el-button>
- </div>
- </el-col>
- </el-row>
- </el-form>
- <ele-pro-table ref="table" row-key="id" :row-class-name="tableRowClassName" :columns="columns"
- :datasource="fetchPage" :need-page="true" :page-size="20" :page-sizes="[10, 20, 50, 100]" :cache-key="$tableCacheKey('hr-contract-management')" class="cm-pro-table">
- <template v-slot:toolbar>
- <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openCreate">
- 发起合同
- </el-button>
- <el-button type="primary" icon="el-icon-download" class="ele-btn-icon" :loading="exporting"
- @click="exportList">
- 导出
- </el-button>
- </template>
- <template v-slot:contractNo="{ row }">
- <!-- 点合同编号开详情,替代原操作列「详情」入口 -->
- <el-link v-if="row.contractNo" type="primary" :underline="false" @click="viewDetail(row)">{{ row.contractNo }}</el-link>
- <span v-else>{{ row.contractNo }}</span>
- </template>
- <template v-slot:employee="{ row }">
- {{
- row.employeeName || "-"
- }}{{
- row.employeeNo || row.employeeCode
- ? " / " + (row.employeeNo || row.employeeCode)
- : ""
- }}
- </template>
- <template v-slot:signMethod="{ row }">
- <el-tag v-if="row.signMethod" size="mini" :type="signMethodTagType(row.signMethod)">{{ row.signMethod
- }}</el-tag>
- <span v-else>-</span>
- </template>
- <template v-slot:signingStatus="{ row }">
- <el-tag v-if="row.signingStatusLabel && row.signingStatusLabel !== '-'" size="mini"
- :type="signingStatusTagType(row.signingStatusLabel)">{{ row.signingStatusLabel }}</el-tag>
- <span v-else>-</span>
- </template>
- <template v-slot:status="{ row }">
- <el-tag v-if="row.status && row.status !== '-'" size="mini" :type="statusTagType(row.status)">{{ row.status
- }}</el-tag>
- <span v-else>-</span>
- </template>
- <template v-slot:archived="{ row }">
- <el-tag size="mini" :type="isArchived(row) ? 'success' : 'info'">
- {{ isArchived(row) ? "已归档" : "未归档" }}
- </el-tag>
- </template>
- <template v-slot:action="{ row }">
- <template v-if="isDraft(row)">
- <el-link type="primary" :underline="false" icon="el-icon-edit" @click="openEdit(row)">
- 编辑
- </el-link>
- <el-link type="primary" :underline="false" icon="el-icon-close" @click="openVoid(row)">
- 作废
- </el-link>
- </template>
- <template v-if="isEffective(row)">
- <el-link type="primary" :underline="false" icon="el-icon-sort" @click="openChange(row)">
- 变更
- </el-link>
- <el-link type="primary" :underline="false" icon="el-icon-refresh" @click="openRenew(row)">
- 续签
- </el-link>
- <el-link type="primary" :underline="false" icon="el-icon-remove-outline" @click="openTerminate(row)">
- 解除
- </el-link>
- </template>
- <el-link v-if="isDraft(row)" type="primary" :underline="false" icon="el-icon-s-promotion"
- @click="launchApproval(row)">
- 提交
- </el-link>
- <el-link v-if="isApproving(row)" type="warning" :underline="false" icon="el-icon-tickets"
- @click="openApprovalRecord(row)">
- 审批记录
- </el-link>
- </template>
- <template slot="empty">
- <el-empty description="暂无数据" />
- </template>
- </ele-pro-table>
- </el-card>
- <!-- 详情查看弹窗 -->
- <ele-modal :visible.sync="detailVisible" width="900px" title="合同详情" append-to-body custom-class="cm-detail-dialog"
- :movable="true" :maxable="true">
- <div v-if="detailRow" class="cm-create-page cm-detail-page">
- <div class="cm-create-employee-card cm-detail-employee">
- <span class="cm-employee-avatar">{{ detailRow.employeeName ? detailRow.employeeName.slice(0, 1) : '?'
- }}</span>
- <div class="cm-employee-info">
- <b>员工姓名:{{ detailRow.employeeName }}<span class="cm-employee-no">员工工号:{{ detailRow.employeeNo }}</span></b>
- </div>
- <div class="cm-detail-status">
- <small>合同状态</small>
- <span class="cm-status" :class="statusClass(detailRow.status)">{{ detailRow.status }}</span>
- </div>
- </div>
- <div class="cm-create-section">
- <div class="cm-create-section-title">合同基本信息</div>
- <div class="cm-create-grid">
- <div class="cm-compare-item"><span class="cm-compare-label">合同编号:</span><span class="cm-compare-value">{{
- detailRow.contractNo }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">合同类型:</span><span class="cm-compare-value">{{
- detailRow.contractType }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">所属机构:</span><span class="cm-compare-value">{{
- detailRow.legalEntity }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">所属部门:</span><span class="cm-compare-value">{{
- detailRow.department }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">岗位名称:</span><span class="cm-compare-value">{{
- detailRow.positionName }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">岗位序列:</span><span class="cm-compare-value">{{
- detailRow.positionSequence || '-' }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">岗位层级:</span><span class="cm-compare-value">{{
- detailRow.level || '-' }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">岗位性质:</span><span class="cm-compare-value">{{
- positionNatureText(detailRow) || '-' }}</span></div>
- <!-- <div class="cm-compare-item"><span class="cm-compare-label">直属上级:</span><span class="cm-compare-value">{{ detailRow.manager || '-' }}</span></div> -->
- </div>
- </div>
- <div class="cm-create-section">
- <div class="cm-create-section-title">合同期限与薪资</div>
- <div class="cm-create-grid">
- <div class="cm-compare-item"><span class="cm-compare-label">合同起始:</span><span class="cm-compare-value">{{
- detailRow.startDate || '-' }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">合同终止:</span><span class="cm-compare-value">{{
- detailRow.endDate || '长期' }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">月薪资:</span><span
- class="cm-compare-value is-highlight">{{ detailRow.salary ? ('¥ ' +
- Number(detailRow.salary).toLocaleString() + ' 元/月') : '待定' }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">试用期至:</span><span class="cm-compare-value">{{
- detailRow.probationEnd || '无试用期' }}</span></div>
- </div>
- </div>
- <div class="cm-create-section">
- <div class="cm-create-section-title">签署信息</div>
- <div class="cm-create-grid">
- <div class="cm-compare-item"><span class="cm-compare-label">签署方式:</span><span class="cm-compare-value">{{
- detailRow.signMethod || '-' }}</span></div>
- <!-- <div class="cm-compare-item"><span class="cm-compare-label">签署状态:</span><span class="cm-compare-value">{{ detailRow.signingStatusLabel || '-' }}</span></div> -->
- <div v-if="detailRow.flowSignStatus" class="cm-compare-item"><span
- class="cm-compare-label">流程节点电子签章:</span><span class="cm-compare-value">{{ detailRow.flowSignStatusLabel
- ||
- '-' }}</span></div>
- <div class="cm-compare-item is-span-2" v-if="detailRow.flowSignNodes && detailRow.flowSignNodes.length">
- <span class="cm-compare-label">签章节点:</span>
- <span class="cm-compare-value">
- {{detailRow.flowSignNodes.map((n) => (n.name || n.nodeName || n.taskName || '节点') + '(' +
- (n.statusLabel ||
- '-') + ')').join('、') }}
- </span>
- </div>
- <div class="cm-compare-item"><span class="cm-compare-label">合同附件:</span><span class="cm-compare-value">
- <fileMain v-if="detailRow.attachmentFileIds && detailRow.attachmentFileIds.length"
- :value="detailRow.attachmentFileIds" type="view" size="small" />
- <span v-else class="cm-detail-muted">暂无附件</span>
- </span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">签订日期:</span><span class="cm-compare-value">{{
- detailRow.signDate || detailRow.createdAt || '-' }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">创建时间:</span><span class="cm-compare-value">{{
- detailRow.createdAt || '-' }}</span></div>
- </div>
- <div v-if="detailRow.remark" class="cm-detail-remark">
- <strong>备注:</strong>{{ detailRow.remark }}
- </div>
- </div>
- <div class="cm-create-footer">
- <div class="cm-footer-actions">
- <el-button @click="detailVisible = false">关闭</el-button>
- </div>
- </div>
- </div>
- </ele-modal>
- <!-- 发起合同弹窗 -->
- <ele-modal :visible.sync="createVisible" width="920px" :title="createDialogTitle" append-to-body
- :close-on-click-modal="false" :before-close="closeCreate" :movable="true" :maxable="true">
- <div class="cm-create-page">
- <div class="cm-create-section">
- <div class="cm-create-section-title">{{ createBaseSectionTitle }}</div>
- <el-form ref="createFormRef" :model="createForm" :rules="createRules" label-position="top">
- <div class="cm-create-grid">
- <el-form-item label="签订员工" prop="employeeId">
- <el-select v-model="createForm.employeeId" filterable remote :remote-method="searchEmployees"
- :loading="employeeLoading" :disabled="!isCreateMode && !isEditMode" placeholder="请选择员工"
- style="width:100%;" @change="onEmployeeChange" @visible-change="onEmployeeDropdownVisible">
- <el-option v-for="e in employeeOptions" :key="String(e.id)"
- :label="`${e.name || '-'}(${e.employeeNo || '无工号'})`" :value="e.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="合同编号">
- <el-input v-model="createForm.contractNo" disabled
- :placeholder="isRenewMode ? '续签后由系统生成新编号' : '提交后由系统生成'" />
- </el-form-item>
- <el-form-item label="合同类型" prop="contractType">
- <el-select v-model="createForm.contractType" placeholder="请选择合同类型" @change="onTemplateDepsChange">
- <el-option v-for="t in contractTypes" :key="t.value" :label="t.label" :value="t.value" />
- </el-select>
- </el-form-item>
- <el-form-item label="拟定薪资" prop="salary">
- <el-input v-model="createForm.salary" placeholder="选择员工后自动带出,可手动修改">
- <template slot="append">元/月</template>
- </el-input>
- </el-form-item>
- <el-form-item label="所属机构" prop="legalEntityName">
- <el-input v-model="createForm.legalEntityName" :disabled="isCreateMode || isEditMode"
- :placeholder="(isCreateMode || isEditMode) ? '选择员工后自动带出' : '请输入所属机构'" />
- </el-form-item>
- <el-form-item label="所属部门" prop="departmentName">
- <el-input v-model="createForm.departmentName" :disabled="isCreateMode || isEditMode"
- :placeholder="(isCreateMode || isEditMode) ? '选择员工后自动带出' : '请输入所属部门'" />
- </el-form-item>
- <el-form-item label="岗位名称" prop="positionName">
- <el-input v-model="createForm.positionName" :disabled="isCreateMode || isEditMode"
- :placeholder="(isCreateMode || isEditMode) ? '选择员工后自动带出' : '请输入岗位名称'" />
- </el-form-item>
- <el-form-item label="岗位序列" prop="positionSequence">
- <el-input v-model="createForm.positionSequence" placeholder="选择员工后自动带出,可手动修改" />
- </el-form-item>
- <el-form-item label="岗位层级" prop="positionLevel">
- <el-input v-model="createForm.positionLevel" placeholder="选择员工后自动带出,可手动修改" />
- </el-form-item>
- <el-form-item label="岗位性质" prop="positionType">
- <el-select v-model="createForm.positionType" clearable filterable placeholder="请选择岗位性质"
- style="width:100%;" @change="onPositionTypeChange">
- <el-option v-for="item in positionNatureOptions"
- :key="item.id != null ? item.id : item.key || item.value" :label="item.label" :value="item.value" />
- </el-select>
- </el-form-item>
- <el-form-item label="合同起始日期" prop="startDate">
- <el-date-picker v-model="createForm.startDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择"
- style="width:100%;" />
- </el-form-item>
- <el-form-item label="合同终止日期" prop="endDate">
- <el-date-picker v-model="createForm.endDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择终止日期"
- style="width:100%;" />
- </el-form-item>
- <el-form-item label="签署方式" prop="signMethod">
- <div class="cm-create-sign-method">
- <button type="button" class="cm-method-btn is-unavailable"
- :class="{ 'is-active': createForm.signMethod === '线上' }" title="暂无开通模板功能,敬请期待"
- @click="onSignMethodChange('线上')">线上签订</button>
- <button type="button" class="cm-method-btn" :class="{ 'is-active': createForm.signMethod === '线下' }"
- @click="onSignMethodChange('线下')">线下签订</button>
- </div>
- </el-form-item>
- <el-form-item v-if="isOnlineSign" label="合同模版" prop="templateId">
- <!-- 模板功能暂未开通:编辑历史线上合同时该字段仍会渲染,点开下拉即提示 -->
- <el-select v-model="createForm.templateId" placeholder="请选择合同模版" @change="onTemplateChange"
- @visible-change="onTemplateDropdown">
- <el-option v-for="t in contractTemplates" :key="String(t.id)" :label="t.name" :value="t.id" />
- </el-select>
- </el-form-item>
- <!-- 合同附件与合同模版同占签署方式右侧的同一单元格,不再整行铺开 -->
- <el-form-item v-else label="合同附件" prop="fileIds" required>
- <fileMain v-model="createForm.fileIds" size="small" />
- </el-form-item>
- </div>
- </el-form>
- </div>
- <div v-if="isCompareMode" class="cm-create-section cm-compare-section">
- <div class="cm-create-section-title">{{ isRenewMode ? '原合同信息 / 续签拟定信息' : '原合同信息 / 发起后信息' }}</div>
- <div class="cm-create-compare">
- <div class="cm-compare-card">
- <div class="cm-compare-card-head">
- <span class="cm-compare-card-title">员工当前信息</span>
- </div>
- <div class="cm-compare-list">
- <div class="cm-compare-item"><span class="cm-compare-label">岗位:</span><span class="cm-compare-value">{{
- changeSnapshot.position || '-' }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">岗位层级:</span><span
- class="cm-compare-value">{{
- changeSnapshot.level || '-' }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">部门:</span><span class="cm-compare-value">{{
- changeSnapshot.department || '-' }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">直属上级:</span><span
- class="cm-compare-value">{{
- changeSnapshot.manager || '-' }}</span></div>
- <div class="cm-compare-item is-span-2"><span class="cm-compare-label">当前薪资:</span><span
- class="cm-compare-value">{{ formatSalary(changeSnapshot.salary) }}/月</span></div>
- </div>
- </div>
- <div class="cm-compare-card">
- <div class="cm-compare-card-head">
- <span class="cm-compare-card-title">{{ isRenewMode ? '续签拟定信息' : '合同拟定信息' }}</span>
- </div>
- <div class="cm-compare-list">
- <div class="cm-compare-item"><span class="cm-compare-label">合同类型:</span><span
- class="cm-compare-value is-highlight">{{ typeLabel(createForm.contractType) || '请选择合同类型' }}</span>
- </div>
- <div class="cm-compare-item"><span class="cm-compare-label">合同编号:</span><span
- class="cm-compare-value">{{
- createForm.contractNo || '-' }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">合同起始:</span><span
- class="cm-compare-value">{{
- createForm.startDate || '请选择起始日期' }}</span></div>
- <div class="cm-compare-item"><span class="cm-compare-label">合同终止:</span><span
- class="cm-compare-value">{{
- createForm.endDate || '请选择终止日期' }}</span></div>
- <div class="cm-compare-item is-span-2"><span class="cm-compare-label">拟定薪资:</span><span
- class="cm-compare-value is-highlight">{{ createForm.salary ? formatSalary(createForm.salary) + '/月'
- :
- '请填写薪资' }}</span></div>
- </div>
- </div>
- </div>
- </div>
- <div class="cm-create-footer">
- <div class="cm-footer-actions">
- <el-button @click="closeCreate">取消</el-button>
- <el-button :loading="saving" @click="saveCreateDraft">保存</el-button>
- <el-button type="primary" :loading="saving" @click="submitCreate">提交</el-button>
- </div>
- </div>
- </div>
- </ele-modal>
- <ele-modal :visible.sync="terminateVisible" width="640px" title="解除 / 终止劳动合同" append-to-body :movable="true"
- :maxable="true">
- <div class="cm-create-page">
- <div class="cm-create-employee-card cm-terminate-employee">
- <span class="cm-employee-avatar">{{ terminateForm.employeeName ? terminateForm.employeeName.slice(0, 1) : '?'
- }}</span>
- <div class="cm-employee-info">
- <b>员工姓名:{{ terminateForm.employeeName || '-' }}<span class="cm-employee-no">合同编号:{{ terminateForm.contractNo
- ||
- '-' }}</span></b>
- </div>
- </div>
- <div class="cm-create-section">
- <el-form ref="terminateFormRef" :model="terminateForm" :rules="terminateRules" label-position="top">
- <div class="cm-create-grid">
- <el-form-item label="业务类型" prop="terminationType">
- <el-radio-group v-model="terminateForm.terminationType">
- <el-radio label="TERMINATE">解除</el-radio>
- <el-radio label="END">终止</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="签署方式" prop="signingMethod">
- <el-radio-group v-model="terminateForm.signingMethod">
- <el-radio label="线上">线上</el-radio>
- <el-radio label="线下">线下</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="原因编码" prop="reasonCode">
- <el-select v-model="terminateForm.reasonCode" filterable placeholder="请选择解除或终止原因" style="width:100%;"
- @change="onTerminateReasonCode">
- <el-option v-for="item in terminateReasons" :key="item.value" :label="item.label"
- :value="item.value" />
- </el-select>
- </el-form-item>
- <el-form-item v-if="terminateForm.signingMethod === '线上'" label="离职证明模板" prop="templateId">
- <el-select v-model="terminateForm.templateId" placeholder="请选择离职证明模板" style="width:100%;"
- @change="onTerminateTemplateChange">
- <el-option v-for="t in contractTemplates" :key="String(t.id)" :label="t.name" :value="t.id" />
- </el-select>
- </el-form-item>
- <el-form-item v-else label="离职证明附件" prop="offlineFileIds">
- <fileMain v-model="terminateForm.offlineFileIds" size="small" />
- </el-form-item>
- <el-form-item label="生效日期" prop="effectiveDate">
- <el-date-picker v-model="terminateForm.effectiveDate" type="date" value-format="yyyy-MM-dd"
- placeholder="yyyy-MM-dd" style="width:100%;" />
- </el-form-item>
- <el-form-item label="最后工作日期" prop="lastWorkingDate">
- <el-date-picker v-model="terminateForm.lastWorkingDate" type="date" value-format="yyyy-MM-dd"
- placeholder="yyyy-MM-dd" style="width:100%;" />
- </el-form-item>
- <el-form-item label="原因说明" prop="reason" style="grid-column: span 2;">
- <el-input v-model="terminateForm.reason" type="textarea" :rows="3" maxlength="500" show-word-limit
- placeholder="请填写解除或终止原因说明" />
- </el-form-item>
- </div>
- </el-form>
- </div>
- <div class="cm-create-footer">
- <div class="cm-footer-actions">
- <el-button @click="terminateVisible = false">取消</el-button>
- <el-button type="primary" :loading="terminateSubmitting" @click="submitTerminate">确定</el-button>
- </div>
- </div>
- </div>
- </ele-modal>
- <ele-modal :visible.sync="voidVisible" width="480px" title="作废劳动合同" append-to-body>
- <el-form ref="voidFormRef" :model="voidForm" :rules="voidRules" label-position="top">
- <el-form-item label="作废原因" prop="reason">
- <el-input v-model="voidForm.reason" type="textarea" :rows="4" maxlength="200" show-word-limit
- placeholder="已签署且生效的合同不能作废" />
- </el-form-item>
- </el-form>
- <span slot="footer">
- <el-button @click="voidVisible = false">取消</el-button>
- <el-button type="primary" :loading="voidSubmitting" @click="submitVoid">确定</el-button>
- </span>
- </ele-modal>
- <ele-modal :visible.sync="archiveVisible" width="520px" title="归档劳动合同" append-to-body>
- <el-form ref="archiveFormRef" :model="archiveForm" :rules="archiveRules" label-position="top">
- <el-form-item label="双方签署扫描件" prop="fileIds">
- <fileMain v-model="archiveForm.fileIds" size="small" />
- </el-form-item>
- <el-form-item label="实际签署日期" prop="signingDate">
- <el-date-picker v-model="archiveForm.signingDate" type="date" value-format="yyyy-MM-dd"
- placeholder="yyyy-MM-dd" style="width:100%;" />
- </el-form-item>
- <el-form-item label="归档说明">
- <el-input v-model="archiveForm.remark" type="textarea" :rows="3" maxlength="200" show-word-limit
- placeholder="选填" />
- </el-form-item>
- </el-form>
- <span slot="footer">
- <el-button @click="archiveVisible = false">取消</el-button>
- <el-button type="primary" :loading="archiveSubmitting" @click="submitArchive">确定</el-button>
- </span>
- </ele-modal>
- <processSubmitDialog :processSubmitDialogFlag.sync="processSubmitDialogFlag" v-if="processSubmitDialogFlag"
- ref="processSubmitDialogRef" @reload="onProcessSubmitReload" />
- <BpmProcessDetailDialog ref="approvalRecordDialog" />
- </div>
- </template>
- <script>
- import {
- getContractPage,
- adaptContract,
- saveContract,
- getContractById,
- updateContract,
- matchContractTemplate,
- changeContract,
- renewContract,
- terminateContract,
- archiveContractOffline,
- voidContract,
- getContractTemplatePage,
- getContractTemplateVersions,
- exportContractLedger,
- CONTRACT_APPROVAL_BUSINESS_KEY,
- CONTRACT_STATUS_OPTIONS,
- contractStatusLabel,
- contractSigningStatusLabel,
- contractFlowSignStatusLabel,
- pickFlowSignStatus,
- adaptContractFlowSignNodes,
- getDepartmentPositionSequences,
- getDepartmentPositions,
- getDepartmentPositionType,
- getDepartmentPositionLevels,
- } from '@/api/hr';
- import { listOrganizations, getUserPage, getEmployeeById, getProposedSalary, adaptEmployee as adaptOrgEmployee } from '@/api/organization';
- import processSubmitDialog from '@/BIZComponents/processSubmitDialog/processSubmitDialog.vue';
- import BpmProcessDetailDialog from '@/views/bpm/processInstance/detailDialog.vue';
- import { getCurrentUser } from '@/utils/token-util';
- import { getByCode } from '@/api/system/dictionary-data';
- // 岗位性质字典与岗位档案同源:api/sys 的 getDictByCode 已兼容结构化项与「{ 1: "全职" }」老格式
- import { getDictByCode } from '@/api/sys';
- const CONTRACT_TYPES = [
- { value: 'FIXED', label: '固定期限劳动合同' },
- { value: 'NON_FIXED', label: '无固定期限劳动合同' },
- { value: 'TASK', label: '以完成一定任务为期限' },
- { value: 'DISPATCH', label: '劳务派遣合同' },
- { value: 'INTERN', label: '实习协议' },
- { value: 'CONFIDENTIAL', label: '保密协议' },
- { value: 'NON_COMPETE', label: '竞业限制协议' },
- { value: 'CHANGE', label: '合同变更协议' },
- { value: 'TERMINATION', label: '合同终止协议' },
- ];
- const TERMINATE_REASON_FALLBACK = [
- { value: 'MUTUAL', label: '协商一致解除' },
- { value: 'EMPLOYEE', label: '员工提出解除' },
- { value: 'MISCONDUCT', label: '严重违纪' },
- { value: 'CONTRACT_EXPIRE', label: '合同到期终止' },
- { value: 'OTHER', label: '其他' },
- ];
- function compact(object) {
- return Object.keys(object).reduce((result, key) => {
- const value = object[key];
- if (value !== '' && value !== null && value !== undefined) result[key] = value;
- return result;
- }, {});
- }
- function formatDate(value) {
- if (!value) return '';
- return String(value).slice(0, 10);
- }
- function formatDateTime(value) {
- if (!value) return '';
- return String(value).replace('T', ' ').slice(0, 16);
- }
- function todayStr() {
- const d = new Date();
- const pad = (n) => String(n).padStart(2, '0');
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
- }
- function addMonths(dateStr, months) {
- const d = dateStr ? new Date(dateStr) : new Date();
- d.setMonth(d.getMonth() + Number(months || 0));
- const pad = (n) => String(n).padStart(2, '0');
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
- }
- function addDays(dateStr, days) {
- const d = dateStr ? new Date(dateStr) : new Date();
- d.setDate(d.getDate() + Number(days || 0));
- const pad = (n) => String(n).padStart(2, '0');
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
- }
- function monthsBetween(start, end) {
- if (!start || !end) return undefined;
- const a = new Date(start);
- const b = new Date(end);
- const months = (b.getFullYear() - a.getFullYear()) * 12 + (b.getMonth() - a.getMonth());
- return months > 0 ? months : undefined;
- }
- function typeLabel(value) {
- return CONTRACT_TYPES.find((t) => t.value === value)?.label || value || '';
- }
- function toSigningMethod(label) {
- if (label === '线上' || label === 'ONLINE') return 'ONLINE';
- if (label === '线下' || label === 'OFFLINE') return 'OFFLINE';
- return 'ONLINE';
- }
- function signMethodLabel(value) {
- if (value === 'ONLINE') return '线上';
- if (value === 'OFFLINE') return '线下';
- return value || '';
- }
- function fileNameOf(file) {
- if (!file) return '';
- if (typeof file === 'string') return file;
- return file.name || file.fileName || '';
- }
- /** 合同附件主键:接口字段 attachmentIds 为英文逗号分隔字符串 */
- function parseAttachmentIds(value) {
- if (value == null || value === '') return [];
- if (Array.isArray(value)) {
- return value
- .map((item) => {
- if (item == null || item === '') return null;
- if (typeof item === 'object') return item.fileId || item.id || null;
- return item;
- })
- .filter((id) => id != null && id !== '')
- .map((id) => String(id));
- }
- return String(value)
- .split(',')
- .map((item) => item.trim())
- .filter(Boolean);
- }
- function joinAttachmentIds(value) {
- const ids = parseAttachmentIds(value);
- return ids.length ? ids.join(',') : undefined;
- }
- function isArchived(row = {}) {
- return Boolean(row.archived || row.statusCode === 'ARCHIVED' || row.status === '已归档');
- }
- function isVoided(row = {}) {
- const code = String(row.statusCode || '').toUpperCase();
- return code === 'VOIDED' || code === 'CANCELLED' || row.status === '已作废';
- }
- /** 草稿:只保存未提交审批,可编辑 / 作废 / 提交审批 */
- function isDraft(row = {}) {
- const code = String(row.statusCode || row.contractStatus || '').toUpperCase();
- return code === 'DRAFT' || row.status === '草稿';
- }
- /** 审批中(签署 / 变更 / 解除任一审批未完成):不允许其他操作 */
- function isApproving(row = {}) {
- const code = String(row.statusCode || row.contractStatus || '').toUpperCase();
- return ['PENDING_APPROVAL', 'PENDING_SIGN', 'SIGNING', 'PENDING_EFFECTIVE'].includes(code);
- }
- /** 审批通过且签署成功:可变更 / 续签 / 解除 */
- function isEffective(row = {}) {
- const code = String(row.statusCode || row.contractStatus || '').toUpperCase();
- return ['EFFECTIVE', 'EXPIRING', 'CHANGED'].includes(code);
- }
- function blankTerminateForm() {
- return {
- id: null,
- employeeName: '',
- contractNo: '',
- terminationType: 'TERMINATE',
- reasonCode: '',
- reason: '',
- effectiveDate: todayStr(),
- lastWorkingDate: todayStr(),
- signingMethod: '线下',
- templateId: null,
- templateVersionId: null,
- offlineFileIds: [],
- };
- }
- function blankVoidForm() {
- return { id: null, reason: '' };
- }
- function blankArchiveForm() {
- return { id: null, fileIds: [], signingDate: todayStr(), remark: '' };
- }
- function blankCreateForm() {
- return {
- id: null,
- employeeId: null,
- employeeNo: '',
- contractNo: '',
- contractType: 'FIXED',
- templateId: null,
- templateVersionId: null,
- templateVersionNo: null,
- legalEntity: '',
- department: '',
- departmentId: null,
- departmentName: '',
- legalEntityId: null,
- legalEntityName: '',
- positionId: null,
- positionName: '',
- positionSequenceId: '',
- positionSequence: '',
- positionLevelId: '',
- positionLevel: '',
- positionType: '',
- positionTypeName: '',
- startDate: '',
- endDate: '',
- probationEnd: '',
- salary: '',
- manager: '',
- // 签署方式默认线下签订
- signMethod: '线下',
- attachment: '',
- attachmentName: '',
- fileIds: [],
- fileId: null,
- };
- }
- function blankChangeSnapshot() {
- return {
- position: '',
- level: '',
- department: '',
- manager: '',
- salary: '',
- endDate: '',
- };
- }
- export default {
- name: 'ContractManagement',
- components: { processSubmitDialog, BpmProcessDetailDialog },
- data() {
- return {
- loading: false,
- saving: false,
- exporting: false,
- employeeLoading: false,
- processSubmitDialogFlag: false,
- currentLaunchRow: null,
- rows: [],
- orgById: {},
- filters: { keyword: '', contractType: '', contractStatus: '', departmentId: '' },
- departments: [],
- contractTypes: CONTRACT_TYPES,
- contractStatus: CONTRACT_STATUS_OPTIONS,
- contractTemplates: [],
- employeeOptions: [],
- positionNatureOptions: [],
- createVisible: false,
- createMode: 'create',
- changeSnapshot: blankChangeSnapshot(),
- createForm: blankCreateForm(),
- createRules: {
- employeeId: [{ required: true, message: '请选择员工', trigger: 'change' }],
- contractType: [{ required: true, message: '请选择合同类型', trigger: 'change' }],
- salary: [{ required: true, message: '请填写拟定薪资', trigger: 'blur' }],
- legalEntityName: [{ required: true, message: '请输入所属机构', trigger: 'blur' }],
- departmentName: [{ required: true, message: '请输入所属部门', trigger: 'blur' }],
- positionName: [{ required: true, message: '请输入岗位名称', trigger: 'blur' }],
- positionSequence: [{ required: true, message: '请填写岗位序列', trigger: 'blur' }],
- positionLevel: [{ required: true, message: '请填写岗位层级', trigger: 'blur' }],
- positionType: [{ required: true, message: '请选择岗位性质', trigger: 'change' }],
- startDate: [{ required: true, message: '请选择合同起始日期', trigger: 'change' }],
- endDate: [{ required: true, message: '请选择合同终止日期', trigger: 'change' }],
- signMethod: [{ required: true, message: '请选择签署方式', trigger: 'change' }],
- templateId: [
- {
- validator: (_rule, value, callback) => {
- if (this.createForm.signMethod !== '线上') return callback();
- if (value) return callback();
- callback(new Error('线上签订请选择合同模版'));
- },
- trigger: 'change',
- },
- ],
- fileIds: [
- {
- validator: (_rule, value, callback) => {
- if (this.createForm.signMethod !== '线下') return callback();
- if (Array.isArray(value) && value.length) return callback();
- callback(new Error('线下签订请上传合同附件'));
- },
- trigger: 'change',
- },
- ],
- },
- detailVisible: false,
- detailRow: null,
- terminateVisible: false,
- terminateSubmitting: false,
- terminateForm: blankTerminateForm(),
- terminateReasons: TERMINATE_REASON_FALLBACK,
- terminateRules: {
- terminationType: [{ required: true, message: '请选择业务类型', trigger: 'change' }],
- signingMethod: [{ required: true, message: '请选择签署方式', trigger: 'change' }],
- reasonCode: [{ required: true, message: '请选择原因编码', trigger: 'change' }],
- reason: [{ required: true, message: '请填写原因说明', trigger: 'blur' }],
- effectiveDate: [{ required: true, message: '请选择生效日期', trigger: 'change' }],
- lastWorkingDate: [{ required: true, message: '请选择最后工作日期', trigger: 'change' }],
- templateId: [
- {
- validator: (_rule, value, callback) => {
- if (this.terminateForm.signingMethod !== '线上') return callback();
- if (value) return callback();
- callback(new Error('线上解除请选择离职证明模板'));
- },
- trigger: 'change',
- },
- ],
- offlineFileIds: [
- {
- validator: (_rule, value, callback) => {
- if (this.terminateForm.signingMethod !== '线下') return callback();
- if (Array.isArray(value) && value.length) return callback();
- callback(new Error('线下解除请上传离职证明附件'));
- },
- trigger: 'change',
- },
- ],
- },
- voidVisible: false,
- voidSubmitting: false,
- voidForm: blankVoidForm(),
- voidRules: {
- reason: [{ required: true, message: '请填写作废原因', trigger: 'blur' }],
- },
- archiveVisible: false,
- archiveSubmitting: false,
- archiveForm: blankArchiveForm(),
- archiveRules: {
- fileIds: [
- {
- validator: (_rule, value, callback) => {
- if (Array.isArray(value) && value.length) return callback();
- callback(new Error('请上传双方签署完成的扫描件'));
- },
- trigger: 'change',
- },
- ],
- signingDate: [{ required: true, message: '请选择实际签署日期', trigger: 'change' }],
- },
- columns: [
- { type: 'index', label: '序号', width: 50, align: 'center', headerAlign: 'center' },
- { prop: 'contractNo', label: '合同编号', minWidth: 140, slot: 'contractNo', showOverflowTooltip: true },
- { prop: 'employeeName', label: '员工', minWidth: 200, slot: 'employee' },
- { prop: 'legalEntity', label: '所属机构', minWidth: 150, showOverflowTooltip: true },
- { prop: 'department', label: '所属部门', minWidth: 120, showOverflowTooltip: true },
- { prop: 'positionName', label: '岗位名称', minWidth: 120, showOverflowTooltip: true },
- { prop: 'contractType', label: '合同类型', minWidth: 140, showOverflowTooltip: true },
- { prop: 'signMethod', label: '签署方式', width: 100, align: 'center', slot: 'signMethod' },
- // { prop: 'signingStatusLabel', label: '签署状态', width: 110, align: 'center', slot: 'signingStatus' },
- { prop: 'status', label: '合同状态', width: 120, slot: 'status', align: 'center' },
- { prop: 'archived', label: '是否已归档', width: 130, slot: 'archived', align: 'center' },
- { prop: 'signDate', label: '签订日期', width: 100, align: 'center' },
- { prop: 'createTime', label: '创建时间', width: 100, align: 'center' },
- { prop: 'createUserName', label: '创建人', width: 110, align: 'center' },
- { prop: 'updateTime', label: '修改时间', width: 100, align: 'center' },
- { prop: 'updateUserName', label: '修改人', width: 110, align: 'center' },
- { prop: 'action', label: '操作', width: 340, slot: 'action', fixed: 'right' },
- ],
- };
- },
- computed: {
- styleResponsive() {
- return this.$store.state.theme?.styleResponsive !== false;
- },
- selectedEmployee() {
- return this.employeeOptions.find((e) => String(e.id) === String(this.createForm.employeeId)) || null;
- },
- isOnlineSign() {
- return this.createForm.signMethod === '线上';
- },
- isChangeMode() {
- return this.createMode === 'change';
- },
- isRenewMode() {
- return this.createMode === 'renew';
- },
- isCreateMode() {
- return this.createMode === 'create';
- },
- isEditMode() {
- return this.createMode === 'edit';
- },
- isCompareMode() {
- return this.isChangeMode || this.isRenewMode;
- },
- /** 合同基础信息区块标题:按弹框模式区分 */
- createBaseSectionTitle() {
- if (this.isRenewMode) return '续签合同基础信息';
- if (this.isChangeMode) return '变更合同基础信息';
- if (this.isEditMode) return '编辑合同基础信息';
- return '合同基础信息';
- },
- createDialogTitle() {
- if (this.isRenewMode) return '续签合同';
- if (this.isChangeMode) return '变更合同';
- if (this.isEditMode) return '编辑合同';
- return '发起合同';
- },
- },
- watch: {
- '$route.query': {
- deep: true,
- handler() {
- this.consumeCreateQuery();
- this.consumeViewEditQuery();
- },
- },
- },
- created() {
- this.consumeCreateQuery();
- this.consumeViewEditQuery();
- this.loadOptions();
- },
- activated() {
- this.consumeCreateQuery();
- this.consumeViewEditQuery();
- },
- methods: {
- typeLabel,
- formatSalary(value) {
- if (value === null || value === undefined || value === '') return '-';
- const num = Number(value);
- if (!Number.isFinite(num)) return String(value);
- return `¥ ${num.toLocaleString()}`;
- },
- formatRemainingDays(value) {
- if (value === null || value === undefined || value === '') return '-';
- const days = Number(value);
- if (!Number.isFinite(days)) return String(value);
- if (days < 0) return `已超期 ${Math.abs(days)} 天`;
- return `${days} 天`;
- },
- remainingClass(value) {
- if (value === null || value === undefined || value === '') return '';
- const days = Number(value);
- if (!Number.isFinite(days)) return '';
- if (days < 0) return 'is-overdue';
- if (days <= 30) return 'is-urgent';
- if (days <= 90) return 'is-warn';
- return 'is-safe';
- },
- nowStr() {
- const d = new Date();
- const pad = (n) => String(n).padStart(2, '0');
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
- },
- signMethodTagType(value) {
- return value === '线上' || value === 'ONLINE' ? 'primary' : 'info';
- },
- signingStatusTagType(label) {
- const map = {
- 未签署: 'info',
- 待签署: 'warning',
- 签署中: '',
- 已签署: 'success',
- 已拒签: 'danger',
- 已过期: 'warning',
- 已取消: 'info',
- };
- return map[label] || 'info';
- },
- statusClass(s) {
- const label = contractStatusLabel(s, s);
- const map = {
- 草稿: 'draft', 审批中: 'pending', 待签署: 'pending', 待签: 'pending', 签署中: 'signing',
- 已签待生效: 'pending', 待生效: 'pending', 生效: 'active', 生效中: 'active', 临期: 'expiring', 即将到期: 'expiring',
- 已变更: 'changed', 已续签: 'active', 已解除: 'terminated', 已终止: 'terminated',
- 已作废: 'expired', 已取消: 'expired', 已归档: 'archived', 试用期: 'trial', 已到期: 'expired', 已过期: 'expired',
- };
- return map[label] || map[s] || 'pending';
- },
- statusTagType(s) {
- const tone = this.statusClass(s);
- const map = {
- draft: 'info',
- pending: 'warning',
- signing: '',
- active: 'success',
- expiring: 'warning',
- changed: '',
- terminated: 'danger',
- expired: 'info',
- archived: 'success',
- trial: 'warning',
- };
- return map[tone] || 'info';
- },
- rootOrg(id) {
- let node = this.orgById[String(id || '')];
- const seen = new Set();
- while (node && node.parentId && this.orgById[String(node.parentId)] && !seen.has(String(node.id))) {
- seen.add(String(node.id));
- node = this.orgById[String(node.parentId)];
- }
- return node || null;
- },
- pickId(...values) {
- for (const value of values) {
- if (value == null || value === '') continue;
- return String(value);
- }
- return null;
- },
- positionLevelOf(user = {}, orgEmp = {}) {
- const list = Array.isArray(user.positionList) ? user.positionList : [];
- const primary = list.find((item) => Number(item.primaryFlag) === 1) || list[0] || {};
- return String(
- orgEmp.level ||
- primary.positionLevelName ||
- user.positionLevelName ||
- user.positionLevel ||
- user.level ||
- '',
- );
- },
- adaptEmployee(user = {}, seed = {}) {
- const orgEmp = adaptOrgEmployee(user || {});
- const departmentId = this.pickId(
- orgEmp.deptId,
- Array.isArray(user.deptIds) ? user.deptIds[0] : null,
- user.deptId,
- user.targetDepartmentId,
- user.groupId,
- seed.departmentId,
- );
- const org = this.orgById[String(departmentId || '')];
- const legal = this.rootOrg(departmentId || user.groupId || orgEmp.groupId || seed.legalEntityId);
- const positionBindings = Array.isArray(orgEmp.positionBindings) ? orgEmp.positionBindings : [];
- const primaryPosition = positionBindings.find((item) => Number(item.primaryFlag) === 1) || positionBindings[0] || {};
- return {
- id: this.pickId(orgEmp.id, user.id, user.userId, seed.employeeId),
- employeeNo: String(orgEmp.employeeNo || user.jobNumber || user.employeeNo || seed.employeeNo || ''),
- name: String(orgEmp.name || user.name || user.employeeName || seed.employeeName || ''),
- department: String(orgEmp.deptName || user.deptName || org?.name || seed.department || ''),
- departmentId,
- position: String(orgEmp.position || user.postName || user.positionName || user.position || seed.positionName || ''),
- positionId: this.pickId(orgEmp.positionId, user.postId, user.positionId, seed.positionId),
- positionSequenceId: this.pickId(
- primaryPosition.positionSequenceId,
- user.positionSequenceId,
- ),
- positionSequence: String(
- primaryPosition.positionSequenceName ||
- user.positionSequenceName ||
- user.positionSequence ||
- '',
- ),
- legalEntity: String(orgEmp.legalEntity || orgEmp.company || legal?.name || user.company || user.groupName || seed.legalEntity || ''),
- legalEntityId: this.pickId(orgEmp.groupId, legal?.id, user.groupId, seed.legalEntityId),
- positionLevelId: this.pickId(
- primaryPosition.positionLevelId,
- user.positionLevelId,
- ),
- positionLevel: String(
- primaryPosition.positionLevelName ||
- user.positionLevelName ||
- user.positionLevel ||
- user.level ||
- '',
- ),
- // 岗位性质:编码(字典值)与名称分别取,不能把编码当名称(否则下拉显示成 "1")
- positionType: String(
- primaryPosition.positionType ||
- user.positionType ||
- orgEmp.employmentType ||
- user.employmentType ||
- '',
- ),
- positionTypeName: String(
- primaryPosition.positionTypeName ||
- user.positionTypeName ||
- orgEmp.employmentTypeName ||
- user.employmentTypeName ||
- '',
- ),
- salary: Number(user.salaryStandard || user.salary || orgEmp.salary || 0) || 0,
- manager: String(orgEmp.manager || user.managerUserName || user.directSuperiorName || user.managerName || user.manager || ''),
- phone: String(orgEmp.phone || user.phone || seed.phone || ''),
- };
- },
- async loadOptions() {
- try {
- const list = await listOrganizations();
- const records = Array.isArray(list) ? list : [];
- const map = {};
- records.forEach((item) => { map[String(item.id)] = item; });
- this.orgById = map;
- this.departments = records
- .filter((item) => item.parentId && map[String(item.parentId)])
- .map((item) => ({ id: item.id, name: item.name }));
- if (!this.departments.length) {
- this.departments = records.map((item) => ({ id: item.id, name: item.name }));
- }
- } catch (e) {
- this.departments = [];
- }
- try {
- const data = await getContractTemplatePage({ templateStatus: 'ENABLED', pageNum: 1, size: 200 });
- this.contractTemplates = (data?.list || []).map((item) => ({
- id: item.id,
- name: item.templateName || item.name,
- templateVersionId: item.currentVersionId || item.templateVersionId,
- templateVersionNo: item.currentVersionNo || item.templateVersionNo,
- }));
- } catch (e) {
- this.contractTemplates = [];
- }
- await this.searchEmployees('');
- await this.loadTerminateReasons();
- await this.loadPositionNatureOptions();
- },
- isArchived,
- isVoided,
- isDraft,
- isApproving,
- isEffective,
- isDraft,
- isApproving,
- isEffective,
- async loadTerminateReasons() {
- try {
- const res = await getByCode('hr_contract_terminate');
- const list = (res?.data || [])
- .map((item) => {
- const entry = Object.entries(item || {})[0] || [];
- return { value: entry[0], label: entry[1] };
- })
- .filter((item) => item.value);
- this.terminateReasons = list.length ? list : TERMINATE_REASON_FALLBACK;
- } catch (e) {
- this.terminateReasons = TERMINATE_REASON_FALLBACK;
- }
- },
- /**
- * 加载岗位性质字典
- * 与岗位档案(PositionEditDialog)同源:api/sys 的 getDictByCode 已兼容
- * 结构化项与「{ 1: "全职" }」老格式,这里不再自行解析 res.data(直接解析会丢成空选项)
- */
- async loadPositionNatureOptions() {
- try {
- const items = await getDictByCode('position_nature');
- this.positionNatureOptions = (items || [])
- .map((item) => {
- const label = String(item.label ?? '').trim();
- /* 提交口径与岗位档案一致:字典主键 id 优先,其次老格式的 map key */
- const value = item.id != null && item.id !== '' ? item.id : item.key;
- if (!label || value == null || value === '') return null;
- /* 选项值统一转字符串:接口编码可能是数字、回填值可能是字符串,
- 不统一类型时 el-select 严格比较会失配,选中项会显示成裸值 "1" */
- return {
- label,
- value: String(value),
- key: item.key != null ? String(item.key) : String(value),
- id: item.id != null && item.id !== '' ? item.id : value,
- };
- })
- .filter(Boolean);
- } catch (e) {
- this.positionNatureOptions = [];
- }
- },
- /** 岗位性质字典值(字典 id / code)→ 字典文案 */
- positionNatureLabel(value) {
- if (value == null || value === '') return '';
- const hit = this.positionNatureOptions.find(
- (item) => String(item.value) === String(value),
- );
- return hit ? hit.label : '';
- },
- /** 岗位性质文本(名称或字典值)→ 字典值,返回值恒为字符串,保证下拉能匹配回显 */
- matchPositionTypeId(text) {
- if (text == null || text === '') return '';
- const key = String(text).trim();
- if (!key) return '';
- const hit = this.positionNatureOptions.find(
- (item) => String(item.label) === key || String(item.value) === key,
- );
- return hit ? String(hit.value) : '';
- },
- /**
- * 合同详情「岗位性质」展示文本
- * 合同 DTO 存的是 positionTypeName(名称)/ positionType(字典值);
- * 名称缺失或曾被写成编码时,用 position_nature 字典按值反查文案
- */
- positionNatureText(row) {
- if (!row) return '';
- return (
- this.positionNatureLabel(row.positionType) ||
- String(row.positionTypeName || '').trim() ||
- row.employmentType ||
- ''
- );
- },
- /** 岗位性质下拉切换:同步 positionTypeName,并把值规范成字符串 */
- onPositionTypeChange(value) {
- const key = value == null || value === '' ? '' : String(value);
- this.createForm.positionType = key;
- const hit = this.positionNatureOptions.find(
- (item) => String(item.value) === key,
- );
- this.createForm.positionTypeName = hit ? hit.label : '';
- },
- // ===== 岗位序列 / 岗位层级 / 岗位性质级联(与录用管理、办理入职同源接口)=====
- /** 级联接口返回可能是数组 / {list|records|rows|data},统一取数组 */
- toCascadeList(payload) {
- if (Array.isArray(payload)) return payload.filter(Boolean);
- const list = payload?.list || payload?.records || payload?.rows || payload?.data;
- return Array.isArray(list) ? list.filter(Boolean) : [];
- },
- async fetchDeptSequences(deptId) {
- const data = await getDepartmentPositionSequences(deptId);
- return this.toCascadeList(data)
- .filter((item) => item && item.positionSequenceId != null)
- .map((item) => ({ id: item.positionSequenceId, name: item.positionSequence || '' }));
- },
- async fetchDeptPositions(deptId, positionSequenceId) {
- const data = await getDepartmentPositions({ deptId, positionSequenceId });
- return this.toCascadeList(data)
- .filter((item) => item && item.positionId != null)
- .map((item) => ({ id: item.positionId, name: item.positionName || '' }));
- },
- async fetchDeptPositionType(deptId, positionId, positionSequenceId) {
- const data = await getDepartmentPositionType({ deptId, positionId, positionSequenceId });
- return this.toCascadeList(data)[0] || null;
- },
- async fetchDeptPositionLevels(deptId, positionId, positionSequenceId, positionType) {
- if (!positionType) return [];
- const data = await getDepartmentPositionLevels({
- deptId,
- positionId,
- positionSequenceId,
- positionType,
- });
- return this.toCascadeList(data)
- .filter((item) => item && item.positionLevelId != null)
- .map((item) => ({ id: item.positionLevelId, name: item.positionLevel || '' }));
- },
- /** 字典里没有该岗位性质时补一条,保证下拉能显示接口回填的名称 */
- ensurePositionTypeOption(value, label) {
- const key = value == null || value === '' ? '' : String(value);
- if (!key) return '';
- /* 展示文案优先取字典按值反查的文案,其次才是传入名称,
- 最后才回落到编码本身——避免把 code 当名称塞进下拉 */
- const showLabel =
- this.positionNatureLabel(key) || String(label || '').trim() || key;
- const exists = this.positionNatureOptions.some(
- (item) => String(item.value) === key,
- );
- if (!exists) {
- this.positionNatureOptions = [
- ...this.positionNatureOptions,
- { id: key, key, value: key, label: showLabel },
- ];
- }
- return key;
- },
- /**
- * 选择员工后自动补全 岗位序列 / 岗位层级 / 岗位性质
- * 仅补员工自带数据缺失的项;接口异常时静默失败,保留手动填写
- */
- async fillPositionCascadeFromEmployee() {
- const form = this.createForm;
- if (!form || !form.employeeId) return;
- const deptId = this.pickId(form.departmentId);
- const positionId = this.pickId(form.positionId);
- if (!deptId || !positionId) return;
- try {
- // ① 岗位序列:缺失时按部门下各序列反查该岗位所属序列
- const sequences = await this.fetchDeptSequences(deptId);
- if (!form.positionSequenceId) {
- for (const seq of sequences) {
- const positions = await this.fetchDeptPositions(deptId, seq.id);
- const hit =
- positions.find((item) => String(item.id) === String(positionId)) ||
- (form.positionName
- ? positions.find((item) => String(item.name) === String(form.positionName))
- : null);
- if (hit) {
- form.positionSequenceId = seq.id;
- if (!form.positionSequence) form.positionSequence = seq.name;
- break;
- }
- }
- } else if (!form.positionSequence) {
- const seq = sequences.find(
- (item) => String(item.id) === String(form.positionSequenceId),
- );
- if (seq) form.positionSequence = seq.name;
- }
- if (!form.positionSequenceId) return;
- // ② 岗位性质:取接口第一条,回填名称并匹配字典值
- const typeItem = await this.fetchDeptPositionType(
- deptId,
- positionId,
- form.positionSequenceId,
- );
- const typeValue = String(typeItem?.positionType || '').trim();
- /* 名称只认接口给的 positionTypeName,取不到时用字典按编码反查;
- 不能拿编码兜底当名称,否则下拉里出现 "1" */
- const typeName =
- String(typeItem?.positionTypeName || '').trim() ||
- this.positionNatureLabel(typeValue);
- if (!form.positionTypeName && typeName) form.positionTypeName = typeName;
- if (!form.positionType && (typeValue || typeName)) {
- form.positionType =
- this.matchPositionTypeId(form.positionTypeName) ||
- this.ensurePositionTypeOption(typeValue, typeName);
- }
- // ③ 岗位层级:按名称/主键反查,字典唯一时直接回填
- const levels = await this.fetchDeptPositionLevels(
- deptId,
- positionId,
- form.positionSequenceId,
- typeValue,
- );
- if (levels.length) {
- let hit = form.positionLevel
- ? levels.find((item) => String(item.name) === String(form.positionLevel))
- : null;
- if (!hit && form.positionLevelId) {
- hit = levels.find((item) => String(item.id) === String(form.positionLevelId)) || null;
- }
- if (!hit && !form.positionLevel && levels.length === 1) hit = levels[0];
- if (hit) {
- if (!form.positionLevelId) form.positionLevelId = hit.id;
- if (!form.positionLevel) form.positionLevel = hit.name || '';
- }
- }
- } catch (e) {
- // 级联查询失败:保留员工自带数据,允许手动填写
- }
- this.$nextTick(() => {
- this.$refs.createFormRef?.clearValidate([
- 'positionSequence',
- 'positionLevel',
- 'positionType',
- ]);
- });
- },
- applyFilters() {
- this.$refs.table && this.$refs.table.reload();
- },
- resetFilters() {
- this.filters = { keyword: '', contractType: '', contractStatus: '', departmentId: '' };
- this.applyFilters();
- },
- async fetchPage({ page, limit }) {
- try {
- const data = await getContractPage({
- pageNum: page,
- size: limit,
- keyword: this.filters.keyword || undefined,
- contractType: this.filters.contractType || undefined,
- contractStatus: this.filters.contractStatus || undefined,
- departmentId: this.filters.departmentId || undefined,
- });
- const list = (data?.list || data?.records || []).map(adaptContract);
- this.rows = list;
- return { list, count: Number(data?.count ?? data?.total ?? list.length) };
- } catch (e) {
- this.rows = [];
- this.$message.error(e.message || '合同列表加载失败');
- return { list: [], count: 0 };
- }
- },
- /**
- * 员工下拉展开时按空关键字重新拉取一次
- * 远端搜索会把上一次的关键字结果留在 employeeOptions 里,
- * 再次发起合同展开下拉就会只剩上次搜到的人,这里主动刷新规避
- */
- onEmployeeDropdownVisible(visible) {
- if (!visible) return;
- this.searchEmployees('');
- },
- async searchEmployees(keyword) {
- // 请求序号:保证并发搜索时只有最后一次的结果生效
- const seq = (this._employeeSearchSeq || 0) + 1;
- this._employeeSearchSeq = seq;
- this.employeeLoading = true;
- try {
- const data = await getUserPage({
- pageNum: 1,
- size: 50,
- name: keyword || undefined,
- });
- if (seq !== this._employeeSearchSeq) return;
- const list = Array.isArray(data?.list) ? data.list : [];
- const mapped = list.map((item) => this.adaptEmployee(item));
- const selectedId = this.createForm.employeeId;
- const kept = this.employeeOptions.filter(
- (item) => selectedId != null && String(item.id) === String(selectedId),
- );
- const ids = new Set(kept.map((item) => String(item.id)));
- this.employeeOptions = [
- ...kept,
- ...mapped.filter((item) => !ids.has(String(item.id))),
- ];
- } catch (e) {
- if (seq !== this._employeeSearchSeq) return;
- this.employeeOptions = [];
- } finally {
- if (seq === this._employeeSearchSeq) this.employeeLoading = false;
- }
- },
- /**
- * 拟定薪资回填:员工档案(getUserPage / getById)不返回拟定薪资,单独走
- * GET /main/user/current/proposed-salary?userId={userId}。
- * 换员工时以该接口为准(返回空则清空,避免残留上一个人的薪资)。
- */
- async fillProposedSalary(userId) {
- const id = userId || this.createForm.employeeId;
- if (!id) {
- this.createForm.salary = '';
- return;
- }
- try {
- const amount = await getProposedSalary(id);
- this.createForm.salary = amount == null ? '' : String(amount);
- } catch (error) {
- /* 失败不静默:明确报错,保留当前值由用户判断 */
- this.$message.error(error.message || '拟定薪资查询失败');
- }
- },
- async onEmployeeChange(id) {
- if (!id) return;
- /* 字典未就绪时先补一次:否则岗位性质只能回填裸编码(下拉显示 "1") */
- if (!this.positionNatureOptions.length) await this.loadPositionNatureOptions();
- try {
- const detail = await getEmployeeById(id);
- const employee = this.adaptEmployee(detail || {});
- const idx = this.employeeOptions.findIndex((item) => String(item.id) === String(id));
- if (idx > -1) this.$set(this.employeeOptions, idx, employee);
- else this.employeeOptions.unshift(employee);
- this.createForm.employeeNo = employee.employeeNo;
- this.createForm.legalEntity = employee.legalEntity;
- this.createForm.legalEntityId = employee.legalEntityId;
- this.createForm.legalEntityName = employee.legalEntity;
- this.createForm.department = employee.department;
- this.createForm.departmentId = employee.departmentId;
- this.createForm.departmentName = employee.department;
- this.createForm.positionId = employee.positionId || null;
- this.createForm.positionName = employee.position;
- this.createForm.positionSequenceId = employee.positionSequenceId || '';
- this.createForm.positionSequence = employee.positionSequence || '';
- this.createForm.positionLevelId = employee.positionLevelId || '';
- this.createForm.positionLevel = employee.positionLevel || '';
- // 岗位性质:名称取员工档案名称,编码走字典值;名称缺失时用编码反查文案
- this.createForm.positionTypeName =
- employee.positionTypeName || this.positionNatureLabel(employee.positionType);
- this.createForm.positionType =
- this.matchPositionTypeId(employee.positionTypeName) ||
- (employee.positionType ? String(employee.positionType) : '');
- this.createForm.manager = employee.manager;
- // 拟定薪资走独立接口回填(员工档案里没有该字段)
- await this.fillProposedSalary(id);
- // 员工档案缺失时,用部门岗位级联接口补全 序列 / 层级 / 性质
- await this.fillPositionCascadeFromEmployee();
- this.onTemplateDepsChange();
- } catch (e) {
- this.$message.error(e.message || '员工信息加载失败');
- }
- },
- async onTemplateDepsChange() {
- const emp = this.selectedEmployee;
- if (!emp || !this.createForm.contractType || !emp.legalEntityId || !emp.departmentId) return;
- try {
- const matched = await matchContractTemplate({
- contractType: this.createForm.contractType,
- departmentId: emp.departmentId,
- departmentName: emp.department,
- employeeCode: emp.employeeNo,
- employeeId: emp.id,
- employeeName: emp.name,
- legalEntityId: emp.legalEntityId,
- legalEntityName: emp.legalEntity,
- signingMethod: toSigningMethod(this.createForm.signMethod),
- startDate: this.createForm.startDate || todayStr(),
- templateId: this.createForm.templateId || this.contractTemplates[0]?.id,
- templateVersionId: this.createForm.templateVersionId || this.contractTemplates[0]?.templateVersionId,
- positionId: emp.positionId || undefined,
- positionName: emp.position || undefined,
- });
- const list = Array.isArray(matched) ? matched : (matched?.list || []);
- if (!list.length) return;
- this.contractTemplates = list.map((item) => ({
- id: item.id || item.templateId,
- name: item.templateName || item.name,
- templateVersionId: item.templateVersionId || item.currentVersionId,
- templateVersionNo: item.templateVersionNo || item.currentVersionNo,
- })).filter((item) => item.id);
- } catch (e) {
- // 匹配失败时保留已启用模板列表
- }
- },
- async onTemplateChange(id) {
- const version = await this.resolveTemplateVersion(id);
- this.createForm.templateVersionId = version.templateVersionId || null;
- this.createForm.templateVersionNo = version.templateVersionNo || null;
- },
- async resolveTemplateVersion(templateId) {
- if (!templateId) return {};
- const cached = this.contractTemplates.find((item) => String(item.id) === String(templateId));
- if (cached?.templateVersionId) {
- return { templateVersionId: cached.templateVersionId, templateVersionNo: cached.templateVersionNo };
- }
- try {
- const list = await getContractTemplateVersions(templateId);
- const versions = Array.isArray(list) ? list : (list?.list || []);
- const current = versions.find((item) => Number(item.currentFlag) === 1)
- || versions[versions.length - 1]
- || versions[0];
- return {
- templateVersionId: current?.id,
- templateVersionNo: current?.versionNo || current?.currentVersionNo,
- };
- } catch (e) {
- return {};
- }
- },
- async viewDetail(row) {
- this.loading = true;
- try {
- const detail = await getContractById(row.id);
- this.detailRow = adaptContract(detail || row);
- this.detailVisible = true;
- } catch (e) {
- this.$message.error(e.message || '合同详情加载失败');
- } finally {
- this.loading = false;
- }
- },
- /**
- * 导出劳动合同台账。
- * ★ 走后端 POST /hr/contracts/export:复用分页查询的组织范围 + 当前页面全部筛选条件,
- * pageNum / size 不传(文档:不限制导出范围),由后端生成 Excel 文件流。
- * 失败(含「HTTP 200 + JSON 业务错误」)由 downloadBlob 抛出 → 这里统一提示,不做兜底。
- */
- async exportList() {
- if (this.exporting) return;
- this.exporting = true;
- try {
- await exportContractLedger({
- keyword: this.filters.keyword || undefined,
- contractType: this.filters.contractType || undefined,
- contractStatus: this.filters.contractStatus || undefined,
- departmentId: this.filters.departmentId || undefined,
- });
- this.$message.success('导出成功');
- } catch (error) {
- this.$message.error(error.message || '导出失败');
- } finally {
- this.exporting = false;
- }
- },
- async openCreate() {
- this.createMode = 'create';
- this.changeSnapshot = blankChangeSnapshot();
- this.createForm = blankCreateForm();
- this.createVisible = true;
- if (!this.employeeOptions.length) await this.searchEmployees('');
- },
- upsertEmployeeOption(employee) {
- if (!employee?.id) return;
- const idx = this.employeeOptions.findIndex(
- (item) => String(item.id) === String(employee.id),
- );
- if (idx > -1) this.$set(this.employeeOptions, idx, employee);
- else this.employeeOptions.unshift(employee);
- },
- applyEmployeeToForm(employee = {}) {
- this.createForm.employeeId = employee.id || null;
- this.createForm.employeeNo = employee.employeeNo || '';
- this.createForm.legalEntity = employee.legalEntity || '';
- this.createForm.legalEntityId = employee.legalEntityId || null;
- this.createForm.legalEntityName = employee.legalEntity || '';
- this.createForm.department = employee.department || '';
- this.createForm.departmentId = employee.departmentId || null;
- this.createForm.departmentName = employee.department || '';
- this.createForm.positionId = employee.positionId || null;
- this.createForm.positionName = employee.position || '';
- this.createForm.positionSequenceId = employee.positionSequenceId || '';
- this.createForm.positionSequence = employee.positionSequence || '';
- this.createForm.positionLevelId = employee.positionLevelId || '';
- this.createForm.positionLevel = employee.positionLevel || '';
- this.createForm.positionTypeName =
- employee.positionTypeName || this.positionNatureLabel(employee.positionType);
- this.createForm.positionType =
- this.matchPositionTypeId(employee.positionTypeName) ||
- (employee.positionType ? String(employee.positionType) : '');
- this.createForm.manager = employee.manager || '';
- if (employee.salary) this.createForm.salary = employee.salary;
- // 员工档案缺失时,用部门岗位级联接口补全 序列 / 层级 / 性质
- this.fillPositionCascadeFromEmployee();
- },
- /** 从办理入职等页面跳转时,自动打开发起合同并带入员工 */
- async consumeCreateQuery() {
- const query = this.$route?.query || {};
- if (query.action !== 'create') return;
- const consumeKey = [
- query.employeeId,
- query.employeeName,
- query.departmentId,
- query.positionId,
- ].join('|');
- if (this._consumedCreateKey === consumeKey || this._consumingCreateQuery) return;
- this._consumingCreateQuery = true;
- this._consumedCreateKey = consumeKey;
- try {
- await this.openCreateWithEmployee(query);
- } catch (error) {
- this._consumedCreateKey = '';
- this.$message.error(error?.message || '带入员工信息失败');
- } finally {
- this._consumingCreateQuery = false;
- }
- },
- clearCreateQuery() {
- const query = this.$route?.query || {};
- if (query.action !== 'create') return;
- const nextQuery = { ...query };
- [
- 'action',
- 'employeeId',
- 'employeeName',
- 'employeeNo',
- 'phone',
- 'departmentId',
- 'department',
- 'positionId',
- 'positionName',
- 'legalEntityId',
- 'legalEntity',
- ].forEach((key) => {
- delete nextQuery[key];
- });
- this._consumedCreateKey = '';
- this.$router.replace({ path: this.$route.path, query: nextQuery }).catch(() => { });
- },
- /** 从办理入职跳转时,自动打开合同详情(view) */
- async consumeViewEditQuery() {
- const query = this.$route?.query || {};
- if (query.action !== 'view') return;
- const contractId = query.contractId;
- if (!contractId) return;
- const consumeKey = `view|${contractId}`;
- if (this._consumedViewEditKey === consumeKey || this._consumingViewEditQuery) return;
- this._consumingViewEditQuery = true;
- this._consumedViewEditKey = consumeKey;
- try {
- await this.viewDetail({ id: contractId });
- this.clearViewEditQuery();
- } catch (error) {
- this._consumedViewEditKey = '';
- this.$message.error(error?.message || '合同详情加载失败');
- } finally {
- this._consumingViewEditQuery = false;
- }
- },
- clearViewEditQuery() {
- const query = this.$route?.query || {};
- if (query.action !== 'view') return;
- const nextQuery = { ...query };
- ['action', 'contractId', 'employeeId', 'employeeName'].forEach((key) => {
- delete nextQuery[key];
- });
- this._consumedViewEditKey = '';
- this.$router.replace({ path: this.$route.path, query: nextQuery }).catch(() => { });
- },
- async openCreateWithEmployee(seed = {}) {
- this.createMode = 'create';
- this.changeSnapshot = blankChangeSnapshot();
- this.createForm = blankCreateForm();
- this.createVisible = true;
- const employeeId = seed.employeeId != null && seed.employeeId !== ''
- ? String(seed.employeeId)
- : '';
- const seeded = this.adaptEmployee({}, seed);
- if (employeeId) seeded.id = employeeId;
- if (seeded.id) {
- this.upsertEmployeeOption(seeded);
- this.applyEmployeeToForm(seeded);
- await this.fillProposedSalary(seeded.id);
- }
- if (!employeeId) {
- const name = String(seed.employeeName || '').trim();
- if (name) {
- await this.searchEmployees(name);
- const hit = this.employeeOptions.find((item) => item.name === name)
- || this.employeeOptions[0];
- if (hit) {
- this.applyEmployeeToForm(hit);
- await this.fillProposedSalary(hit.id);
- await this.onTemplateDepsChange();
- return;
- }
- }
- if (!this.employeeOptions.length) await this.searchEmployees('');
- return;
- }
- try {
- const detail = await getEmployeeById(employeeId);
- const employee = this.adaptEmployee(detail || {}, seed);
- employee.id = String(employee.id || employeeId);
- this.upsertEmployeeOption(employee);
- this.applyEmployeeToForm(employee);
- await this.fillProposedSalary(employee.id);
- await this.onTemplateDepsChange();
- } catch (error) {
- await this.onTemplateDepsChange();
- }
- },
- closeCreate(done) {
- this.createVisible = false;
- this.createMode = 'create';
- this.changeSnapshot = blankChangeSnapshot();
- this.createForm = blankCreateForm();
- this.clearCreateQuery();
- if (typeof done === 'function') done();
- },
- async buildCreatePayload(form, extra = {}) {
- const emp = this.selectedEmployee;
- if (!emp) {
- this.$message.warning('请选择员工');
- return null;
- }
- const departmentId = form.departmentId || emp.departmentId;
- const departmentName = form.departmentName || emp.department;
- const legalEntityId = form.legalEntityId || emp.legalEntityId;
- const legalEntityName = form.legalEntityName || emp.legalEntity;
- if (!legalEntityId || !departmentId) {
- this.$message.warning('员工缺少所属机构或部门主键,无法创建合同');
- return null;
- }
- const online = form.signMethod === '线上';
- let templateId = form.templateId;
- let templateVersionId = form.templateVersionId;
- let templateVersionNo = form.templateVersionNo;
- if (!online && !templateId) {
- templateId = this.contractTemplates[0]?.id;
- templateVersionId = this.contractTemplates[0]?.templateVersionId;
- templateVersionNo = this.contractTemplates[0]?.templateVersionNo;
- }
- if (templateId && !templateVersionId) {
- const version = await this.resolveTemplateVersion(templateId);
- templateVersionId = version.templateVersionId;
- templateVersionNo = version.templateVersionNo;
- }
- if (online && (!templateId || !templateVersionId)) {
- this.$message.warning('线上签订请选择已启用模板及其版本');
- return null;
- }
- if (!online && !(form.fileIds || []).length) {
- this.$message.warning('线下签订请上传合同附件');
- return null;
- }
- if (!form.endDate) {
- this.$message.warning('请选择合同终止日期');
- return null;
- }
- const salary = Number(String(form.salary || '').replace(/[^\d.]/g, '')) || undefined;
- return compact({
- contractType: form.contractType,
- departmentId,
- departmentName,
- employeeCode: emp.employeeNo,
- employeeId: emp.id,
- employeeName: emp.name,
- legalEntityId,
- legalEntityName,
- signingMethod: toSigningMethod(form.signMethod),
- startDate: form.startDate,
- endDate: form.endDate,
- contractTermMonths: monthsBetween(form.startDate, form.endDate),
- templateId,
- templateVersionId,
- templateVersionNo,
- positionId: form.positionId || emp.positionId || undefined,
- positionName: form.positionName || emp.position || undefined,
- positionSequenceId: form.positionSequenceId || emp.positionSequenceId || undefined,
- positionSequence: form.positionSequence || emp.positionSequence || undefined,
- positionLevelId: form.positionLevelId || emp.positionLevelId || undefined,
- positionLevel: form.positionLevel || emp.positionLevel || undefined,
- positionType: form.positionType || emp.positionType || undefined,
- positionTypeName: form.positionTypeName || emp.positionTypeName || undefined,
- salaryStandard: salary,
- workLocation: extra.workLocation,
- attachmentIds: online ? undefined : joinAttachmentIds(form.fileIds || form.fileId),
- ...extra,
- });
- },
- pickCreatedId(created) {
- if (created == null || created === '') return null;
- if (typeof created === 'object') return created.id || created.contractId || null;
- return created;
- },
- async persistCreate() {
- const valid = this.$refs.createFormRef
- ? await this.$refs.createFormRef.validate().catch(() => false)
- : false;
- if (!valid) return null;
- const payload = await this.buildCreatePayload(this.createForm);
- if (!payload) return null;
- if (this.isChangeMode) {
- const form = this.createForm;
- await changeContract(form.id, compact({
- changeType: 'OTHER',
- changeReason: '合同变更',
- effectiveDate: form.startDate || todayStr(),
- signingMethod: payload.signingMethod,
- offlineFileId: form.signMethod === '线下' ? (form.fileIds?.[0] || form.fileId) : undefined,
- templateId: payload.templateId,
- templateVersionId: payload.templateVersionId,
- changedValues: compact({
- salaryStandard: payload.salaryStandard,
- startDate: payload.startDate,
- endDate: payload.endDate,
- contractType: payload.contractType,
- legalEntityId: payload.legalEntityId,
- legalEntityName: payload.legalEntityName,
- departmentId: payload.departmentId,
- departmentName: payload.departmentName,
- positionId: payload.positionId,
- positionName: payload.positionName,
- positionSequenceId: payload.positionSequenceId,
- positionSequence: payload.positionSequence,
- positionLevelId: payload.positionLevelId,
- positionLevel: payload.positionLevel,
- positionType: payload.positionType,
- positionTypeName: payload.positionTypeName,
- }),
- }));
- return { id: form.id, payload };
- }
- if (this.isRenewMode) {
- const created = await renewContract(this.createForm.id, {
- contract: compact({
- ...payload,
- renewal: true,
- probationMonths: 0,
- }),
- });
- return { id: this.pickCreatedId(created) || this.createForm.id, payload };
- }
- if (this.isEditMode) {
- const form = this.createForm;
- if (!form.id) {
- this.$message.warning('缺少合同主键,无法修改');
- return null;
- }
- await updateContract(form.id, compact({ ...payload, id: form.id }));
- return { id: form.id, payload };
- }
- const created = await saveContract(payload);
- return { id: this.pickCreatedId(created), payload };
- },
- async saveCreateDraft() {
- this.saving = true;
- try {
- const result = await this.persistCreate();
- if (!result) return;
- this.$message.success(
- this.isRenewMode
- ? '续签草稿已保存'
- : this.isChangeMode
- ? '合同变更已保存'
- : this.isEditMode
- ? '合同草稿已修改'
- : '合同已保存',
- );
- this.closeCreate();
- this.applyFilters();
- } catch (e) {
- this.$message.error(e.message || '保存失败');
- } finally {
- this.saving = false;
- }
- },
- async submitCreate() {
- this.saving = true;
- try {
- const result = await this.persistCreate();
- if (!result) return;
- if (!result.id) {
- this.$message.warning('合同已保存,但未返回主键,无法提交审批');
- this.closeCreate();
- this.applyFilters();
- return;
- }
- const emp = this.selectedEmployee || {};
- const launchRow = {
- id: result.id,
- contractNo: this.createForm.contractNo,
- employeeName: emp.name,
- createUserId: emp.createUserId,
- };
- this.closeCreate();
- this.applyFilters();
- this.$nextTick(() => this.launchApproval(launchRow));
- } catch (e) {
- this.$message.error(e.message || '提交失败');
- } finally {
- this.saving = false;
- }
- },
- launchApproval(row) {
- if (!row?.id) {
- this.$message.warning('请先保存合同后再提交审批');
- return;
- }
- this.currentLaunchRow = row;
- this.processSubmitDialogFlag = true;
- this.$nextTick(() => {
- const currentUser = getCurrentUser() || {};
- this.$refs.processSubmitDialogRef?.init({
- businessId: row.id,
- businessKey: CONTRACT_APPROVAL_BUSINESS_KEY,
- formCreateUserId:
- row.createUserId || currentUser.id || currentUser.userId || '',
- variables: {
- businessCode: row.contractNo || '',
- businessName: row.employeeName || '',
- businessType: '劳动合同',
- },
- });
- });
- },
- onProcessSubmitReload() {
- this.currentLaunchRow = null;
- this.applyFilters();
- },
- /** 已作废合同整行画删除线 */
- tableRowClassName({ row }) {
- if (isVoided(row)) return 'cm-row-voided';
- return '';
- },
- /** 查看审批记录:优先用列表 row 上的 processInstanceId;没有就调详情接口补查,再没有才提示 */
- async openApprovalRecord(row) {
- const rowAdapted = adaptContract(row || {});
- let id = rowAdapted.processInstanceId || rowAdapted.approvalInstanceId;
- if (!id && row?.id) {
- try {
- const detail = await getContractById(row.id);
- const detailAdapted = adaptContract(detail || {});
- id = detailAdapted.processInstanceId || detailAdapted.approvalInstanceId;
- } catch (e) {
- // 详情拿不到也没关系,后面统一提示
- }
- }
- if (!id) {
- this.$message.warning('暂无审批记录(后端未返回流程实例 id)');
- return;
- }
- this.$refs.approvalRecordDialog?.open({ processInstanceId: id });
- },
- /** 线上模板功能未开通的统一提示:先关掉上一条,避免连点堆叠多条提示 */
- warnTemplateDisabled() {
- this.$message.closeAll();
- this.$message.warning('暂无开通模板功能,敬请期待');
- },
- /** 合同模版下拉展开时提示(编辑历史线上合同时会落到这里) */
- onTemplateDropdown(visible) {
- if (visible) this.warnTemplateDisabled();
- },
- onSignMethodChange(method) {
- // 线上签订依赖模板生成正文,模板未开通时只提示、不切换签署方式
- if (method === '线上') {
- this.warnTemplateDisabled();
- return;
- }
- if (this.createForm.signMethod === method) return;
- this.createForm.signMethod = method;
- // 模板与附件互斥:切换时把另一侧已选/已传的内容清空,避免提交时同时带上模板与附件造成重复
- this.createForm.templateId = null;
- this.createForm.templateVersionId = null;
- this.createForm.templateVersionNo = null;
- this.createForm.fileIds = [];
- this.createForm.fileId = null;
- this.$nextTick(() => this.$refs.createFormRef?.clearValidate(['templateId', 'fileIds']));
- },
- // generateAndSubmit 已废弃:/hr/contracts/{id}/submit 接口不再使用,提交审批改由 launchApproval 走公共审批组件
- async fillCreateFormFromContract(row) {
- const detail = adaptContract(await getContractById(row.id));
- this.createForm = {
- ...blankCreateForm(),
- id: detail.id,
- employeeId: detail.employeeId || null,
- employeeNo: detail.employeeNo || '',
- contractNo: detail.contractNo || '',
- contractType: detail.contractTypeCode || 'FIXED',
- templateId: detail.templateId || null,
- templateVersionId: detail.templateVersionId || null,
- templateVersionNo: detail.templateVersionNo || null,
- legalEntity: detail.legalEntity || '',
- legalEntityId: detail.legalEntityId || null,
- legalEntityName: detail.legalEntityName || detail.legalEntity || '',
- department: detail.department || '',
- departmentId: detail.departmentId || null,
- departmentName: detail.departmentName || detail.department || '',
- positionId: detail.positionId || null,
- positionName: detail.positionName || '',
- positionSequenceId: detail.positionSequenceId || '',
- positionSequence: detail.positionSequence || '',
- positionLevelId: detail.positionLevelId || '',
- positionLevel: detail.positionLevel || detail.level || '',
- // 合同详情回显:编码统一字符串(与下拉选项同类型),名称能按编码反查到字典文案时以字典为准
- positionType:
- detail.positionType != null && detail.positionType !== ''
- ? String(detail.positionType)
- : '',
- positionTypeName:
- this.positionNatureLabel(detail.positionType) ||
- String(detail.positionTypeName || ''),
- startDate: detail.startDate || '',
- endDate: detail.endDate || '',
- salary: detail.salary != null && detail.salary !== '' ? String(detail.salary) : '',
- manager: detail.manager || '',
- signMethod: ['线上', '线下'].includes(detail.signMethod) ? detail.signMethod : '线下',
- fileIds: detail.attachmentFileIds || [],
- fileId: (detail.attachmentFileIds && detail.attachmentFileIds[0]) || null,
- };
- this.changeSnapshot = {
- position: detail.positionName || '',
- level: detail.level || '',
- department: detail.department || '',
- manager: detail.manager || '',
- salary: detail.salary || '',
- endDate: detail.endDate || '',
- };
- if (detail.employeeId) {
- try {
- const employee = this.adaptEmployee(await getEmployeeById(detail.employeeId), {
- employeeId: detail.employeeId,
- employeeName: detail.employeeName,
- employeeNo: detail.employeeNo,
- departmentId: detail.departmentId,
- department: detail.department,
- legalEntityId: detail.legalEntityId,
- legalEntity: detail.legalEntity,
- positionId: detail.positionId,
- positionName: detail.positionName,
- });
- this.upsertEmployeeOption(employee);
- this.createForm.employeeId = employee.id;
- this.createForm.employeeNo = employee.employeeNo || this.createForm.employeeNo;
- if (!this.createForm.departmentId) this.createForm.departmentId = employee.departmentId;
- if (!this.createForm.departmentName) {
- this.createForm.departmentName = employee.department;
- this.createForm.department = employee.department;
- }
- if (!this.createForm.legalEntityId) this.createForm.legalEntityId = employee.legalEntityId;
- if (!this.createForm.legalEntityName) {
- this.createForm.legalEntityName = employee.legalEntity;
- this.createForm.legalEntity = employee.legalEntity;
- }
- if (!this.createForm.positionId) this.createForm.positionId = employee.positionId || null;
- if (!this.createForm.positionName) this.createForm.positionName = employee.position || '';
- if (!this.createForm.positionSequenceId) this.createForm.positionSequenceId = employee.positionSequenceId || '';
- if (!this.createForm.positionSequence) this.createForm.positionSequence = employee.positionSequence || '';
- if (!this.createForm.positionLevelId) this.createForm.positionLevelId = employee.positionLevelId || '';
- if (!this.createForm.positionLevel) this.createForm.positionLevel = employee.positionLevel || '';
- if (!this.createForm.positionTypeName) {
- this.createForm.positionTypeName =
- employee.positionTypeName || this.positionNatureLabel(employee.positionType);
- }
- if (!this.createForm.positionType) {
- this.createForm.positionType =
- this.matchPositionTypeId(employee.positionTypeName) ||
- (employee.positionType ? String(employee.positionType) : '');
- }
- this.createForm.manager = employee.manager || this.createForm.manager;
- this.changeSnapshot.position = employee.position || this.changeSnapshot.position;
- this.changeSnapshot.level = employee.level || this.changeSnapshot.level;
- this.changeSnapshot.department = employee.department || this.changeSnapshot.department;
- this.changeSnapshot.manager = employee.manager || this.changeSnapshot.manager;
- if (employee.salary) this.changeSnapshot.salary = employee.salary;
- } catch (error) {
- this.upsertEmployeeOption({
- id: detail.employeeId,
- name: detail.employeeName || String(detail.employeeId),
- employeeNo: detail.employeeNo || '',
- });
- }
- }
- return detail;
- },
- async openEdit(row) {
- if (!row?.id) {
- this.$message.warning('缺少合同主键,无法编辑');
- return;
- }
- if (!isDraft(row)) {
- this.$message.warning('仅未提交审批的合同草稿可以修改');
- return;
- }
- this.createMode = 'edit';
- this.createForm = blankCreateForm();
- this.changeSnapshot = blankChangeSnapshot();
- this.createVisible = true;
- this.loading = true;
- try {
- await this.fillCreateFormFromContract(row);
- this.$nextTick(() => this.$refs.createFormRef?.clearValidate());
- } catch (e) {
- this.$message.error(e.message || '合同详情加载失败');
- this.closeCreate();
- } finally {
- this.loading = false;
- }
- },
- async openChange(row) {
- if (!row?.id) {
- this.$message.warning('缺少合同主键,无法变更');
- return;
- }
- if (!isEffective(row)) {
- this.$message.warning('仅审批通过并签署成功的合同可以变更');
- return;
- }
- this.createMode = 'change';
- this.createForm = blankCreateForm();
- this.changeSnapshot = blankChangeSnapshot();
- this.createVisible = true;
- this.loading = true;
- try {
- await this.fillCreateFormFromContract(row);
- this.$nextTick(() => this.$refs.createFormRef?.clearValidate());
- } catch (e) {
- this.$message.error(e.message || '合同详情加载失败');
- this.closeCreate();
- } finally {
- this.loading = false;
- }
- },
- async openRenew(row) {
- if (!row?.id) {
- this.$message.warning('缺少合同主键,无法续签');
- return;
- }
- if (!isEffective(row)) {
- this.$message.warning('仅审批通过并签署成功的合同可以续签');
- return;
- }
- this.createMode = 'renew';
- this.createForm = blankCreateForm();
- this.changeSnapshot = blankChangeSnapshot();
- this.createVisible = true;
- this.loading = true;
- try {
- const detail = await this.fillCreateFormFromContract(row);
- this.createForm.contractNo = '';
- if (detail.endDate) {
- this.createForm.startDate = addDays(detail.endDate, 1);
- const months = Number(detail.contractTermMonths) || monthsBetween(detail.startDate, detail.endDate);
- if (this.createForm.contractType !== 'NON_FIXED' && months) {
- this.createForm.endDate = addMonths(this.createForm.startDate, months);
- }
- }
- this.$nextTick(() => this.$refs.createFormRef?.clearValidate());
- } catch (e) {
- this.$message.error(e.message || '合同详情加载失败');
- this.closeCreate();
- } finally {
- this.loading = false;
- }
- },
- openTerminate(row) {
- if (!isEffective(row)) {
- this.$message.warning('仅审批通过并签署成功的合同可以解除');
- return;
- }
- this.terminateForm = {
- ...blankTerminateForm(),
- id: row.id,
- employeeName: row.employeeName || '',
- contractNo: row.contractNo || '',
- signingMethod: row.signMethod || '线下',
- };
- this.terminateVisible = true;
- this.$nextTick(() => this.$refs.terminateFormRef?.clearValidate());
- },
- onTerminateReasonCode(code) {
- const item = this.terminateReasons.find((r) => r.value === code);
- if (item && !this.terminateForm.reason) this.terminateForm.reason = item.label;
- },
- async onTerminateTemplateChange(id) {
- const version = await this.resolveTemplateVersion(id);
- this.terminateForm.templateVersionId = version.templateVersionId || null;
- },
- async submitTerminate() {
- const valid = this.$refs.terminateFormRef
- ? await this.$refs.terminateFormRef.validate().catch(() => false)
- : false;
- if (!valid) return;
- const form = this.terminateForm;
- const signingMethod = toSigningMethod(form.signingMethod);
- this.terminateSubmitting = true;
- try {
- await terminateContract(form.id, compact({
- terminationType: form.terminationType,
- reason: form.reason,
- reasonCode: form.reasonCode,
- effectiveDate: form.effectiveDate,
- lastWorkingDate: form.lastWorkingDate,
- signingMethod,
- templateId: signingMethod === 'ONLINE' ? form.templateId : undefined,
- templateVersionId: signingMethod === 'ONLINE' ? form.templateVersionId : undefined,
- offlineFileId: signingMethod === 'OFFLINE' ? form.offlineFileIds?.[0] : undefined,
- }));
- this.$message.success('已保存解除/终止草稿');
- this.terminateVisible = false;
- this.applyFilters();
- } catch (e) {
- this.$message.error(e.message || '解除失败');
- } finally {
- this.terminateSubmitting = false;
- }
- },
- openVoid(row) {
- if (!isDraft(row)) {
- this.$message.warning('仅未提交审批的合同草稿可以作废');
- return;
- }
- this.voidForm = { id: row.id, reason: '' };
- this.voidVisible = true;
- this.$nextTick(() => this.$refs.voidFormRef?.clearValidate());
- },
- async submitVoid() {
- const valid = this.$refs.voidFormRef
- ? await this.$refs.voidFormRef.validate().catch(() => false)
- : false;
- if (!valid) return;
- this.voidSubmitting = true;
- try {
- await voidContract(this.voidForm.id, { reason: this.voidForm.reason });
- this.$message.success('合同已作废');
- this.voidVisible = false;
- this.applyFilters();
- } catch (e) {
- this.$message.error(e.message || '作废失败');
- } finally {
- this.voidSubmitting = false;
- }
- },
- async handleSign(row) {
- // /hr/contracts/{id}/submit 接口已废弃,合同提交审批改由 launchApproval 走公共审批组件
- this.$message.info('请使用「提交审批」按钮发起合同审批流程');
- },
- openArchive(row) {
- if ((row.signingMethod || toSigningMethod(row.signMethod)) !== 'OFFLINE') {
- this.$message.warning('仅线下签署合同可调用归档接口');
- return;
- }
- this.archiveForm = { ...blankArchiveForm(), id: row.id };
- this.archiveVisible = true;
- this.$nextTick(() => this.$refs.archiveFormRef?.clearValidate());
- },
- async submitArchive() {
- const valid = this.$refs.archiveFormRef
- ? await this.$refs.archiveFormRef.validate().catch(() => false)
- : false;
- if (!valid) return;
- this.archiveSubmitting = true;
- try {
- await archiveContractOffline(this.archiveForm.id, compact({
- fileId: this.archiveForm.fileIds[0],
- signingDate: this.archiveForm.signingDate,
- remark: this.archiveForm.remark,
- }));
- this.$message.success('合同已归档');
- this.archiveVisible = false;
- this.applyFilters();
- } catch (e) {
- this.$message.error(e.message || '归档失败');
- } finally {
- this.archiveSubmitting = false;
- }
- },
- },
- };
- </script>
- <style scoped lang="scss" src="../../styles/views/contractManagement/index.scss"></style>
- <!-- 已作废合同整行删除线:el-table 的行是子组件渲染,必须用非 scoped 穿透 -->
- <style lang="scss">
- .cm-pro-table .cm-row-voided td,
- .cm-pro-table .cm-row-voided td .cell,
- .cm-pro-table .cm-row-voided td .cell * {
- text-decoration: line-through;
- color: #a8abb2;
- }
- </style>
|