| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582 |
- <template>
- <div class="ele-body regularization-page" v-loading="loading">
- <el-card shadow="never">
- <el-form label-width="100px" 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="姓名" class="name-item">
- <el-select v-model="draftFilters.userId" filterable remote clearable reserve-keyword
- :remote-method="searchEmployees" :loading="employeeLoading" placeholder="输入姓名 / 工号搜索"
- style="width: 100%">
- <el-option v-for="item in employeeOptions" :key="String(item.id)"
- :label="`${item.name}(${item.employeeNo || '-'})`" :value="item.id" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
- <el-form-item label="所属机构">
- <el-select v-model="draftFilters.organizationId" clearable filterable placeholder="请选择"
- style="width: 100%">
- <el-option v-for="item in legalEntities" :key="String(item.id)" :label="item.name" :value="item.id" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
- <el-form-item label="转正类型">
- <el-select v-model="draftFilters.regularizationType" clearable placeholder="请选择" style="width: 100%">
- <el-option v-for="(label, value) in typeOptions" :key="value" :label="label" :value="value" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
- <el-form-item label="计划转正日期">
- <el-date-picker v-model="draftFilters.dateRange" type="daterange" value-format="yyyy-MM-dd"
- range-separator="至" start-placeholder="请选择" end-placeholder="请选择" style="width: 100%" />
- </el-form-item>
- </el-col>
- <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
- <el-form-item label="办理状态" class="status-item">
- <el-select v-model="activeStatus" placeholder="请选择" style="width: 100%" @change="reloadTable">
- <el-option v-for="tab in tabs" :key="tab.value" :label="tab.label" :value="tab.value" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
- <el-form-item label="实际生效日期">
- <el-date-picker v-model="draftFilters.effectiveRange" type="daterange" value-format="yyyy-MM-dd"
- range-separator="至" start-placeholder="请选择" end-placeholder="请选择" style="width: 100%" />
- </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" :columns="columns" :datasource="fetchPage" :need-page="true"
- :page-size="20" :cache-key="$tableCacheKey(cacheKey)" class="regularization-table" @selection-change="selectedRows = $event">
- <template v-slot:toolbar>
- <el-button v-if="$hasPermission('main:hrRegularization:save')" type="primary" icon="el-icon-plus"
- class="ele-btn-icon" @click="openCreate()">
- 新建
- </el-button>
- <el-button v-if="$hasPermission('main:hrRegularization:export')" icon="el-icon-download"
- class="ele-btn-icon" @click="exportRows">
- 导出
- </el-button>
- </template>
- <!-- 点员工姓名开详情,替代原操作列「查看」入口 -->
- <template v-slot:name="{ row }">
- <el-link v-if="row.name" type="primary" :underline="false" @click.stop="openDetail(row)">
- {{ row.name }}
- </el-link>
- <span v-else>-</span>
- </template>
- <template v-slot:position="{ row }">
- {{ row.position || "-" }}
- </template>
- <template v-slot:date="{ row }">
- {{ row.effectiveDate || row.regularDate || row.plannedDate || "-" }}
- </template>
- <template v-slot:status="{ row }">
- <el-tag v-if="row.status" size="mini" :type="statusTagType(row.status)">
- {{ row.statusLabel || statusLabel(row.status) }}
- </el-tag>
- <span v-else>-</span>
- </template>
- <template v-slot:approval="{ row }">
- <el-tag size="mini" :type="approvalType(row.approvalStatus)">
- {{ row.approvalStatusLabel || row.approvalStatus || "未提交" }}
- </el-tag>
- </template>
- <template v-slot:action="{ row }">
- <el-link v-if="canEdit(row) && $hasPermission('main:hrRegularization:update')" type="primary"
- :underline="false" icon="el-icon-edit" @click.stop="openCreate(row)">
- 编辑
- </el-link>
- <el-link v-if="canSubmit(row) && $hasPermission('main:hrRegularization:submit')" type="primary"
- :underline="false" icon="el-icon-s-promotion" @click.stop="launchApproval(row)">
- 提交
- </el-link>
- <el-link v-if="canViewApproval(row)" type="primary" :underline="false" icon="el-icon-connection"
- @click.stop="openApprovalRecord(row)">
- 审批记录
- </el-link>
- <el-link v-if="canCancel(row) && $hasPermission('main:hrRegularization:cancel')" type="danger"
- :underline="false" icon="el-icon-close" @click.stop="cancelRow(row)">
- 取消
- </el-link>
- </template>
- <template slot="empty">
- <el-empty description="暂无试用期员工或转正单据" />
- </template>
- </ele-pro-table>
- </el-card>
- <el-drawer :visible.sync="drawerVisible" direction="rtl" :size="drawerSize" append-to-body
- custom-class="regularization-drawer" :wrapper-closable="false">
- <template slot="title">
- <div class="drawer-title">
- <span>
- <i :class="readOnly ? 'el-icon-document' : 'el-icon-edit-outline'"></i>
- </span>
- <div>
- <h2>
- {{ readOnly ? "转正申请详情" : "发起转正申请" }}
- <!-- 状态文案一律用接口返回的中文名(生效状态优先,其次审批状态),本地不再翻译 -->
- <el-tag v-if="form.effectiveStatusLabel || form.approvalStatusLabel" size="mini"
- :type="statusTagType(form.status)" style="margin-left:8px">
- {{ form.effectiveStatusLabel || form.approvalStatusLabel }}
- </el-tag>
- </h2>
- <p>{{ form.employeeNo || "选择员工并完善申请资料" }}</p>
- </div>
- </div>
- </template>
- <div class="drawer-body" v-loading="drawerLoading">
- <el-form ref="applyForm" :model="form" :rules="rules" label-position="top" :disabled="readOnly"
- class="apply-form">
- <section class="form-section">
- <header>
- <span><i class="el-icon-user"></i></span>
- <div>
- <h3>员工信息</h3>
- <p>从试用期员工档案自动带出</p>
- </div>
- </header>
- <div class="form-grid">
- <el-form-item label="申请员工" prop="userId">
- <el-select v-model="form.userId" filterable placeholder="请选择试用期员工" :disabled="readOnly"
- @change="selectEmployee">
- <el-option v-for="item in selectableEmployees" :key="item.userId"
- :label="`${item.name}(${item.employeeNo || '-'})`" :value="item.userId" />
- </el-select>
- </el-form-item>
- <el-form-item label="手机号" prop="phone">
- <el-input v-model.trim="form.phone" maxlength="11" :disabled="readOnly" placeholder="请输入 11 位手机号码" />
- </el-form-item>
- <el-form-item label="所属机构" prop="groupId">
- <el-select v-model="form.groupId" filterable clearable :disabled="readOnly" placeholder="请选择所属机构"
- style="width: 100%" @change="onCascadeLegalChange">
- <el-option v-for="item in legalEntities" :key="item.id" :label="item.name" :value="item.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="所属部门" prop="deptId">
- <el-select v-model="form.deptId" filterable clearable :disabled="readOnly || !form.groupId"
- :placeholder="form.groupId ? '请选择所属部门' : '请先选择所属机构'" style="width: 100%"
- @change="onCascadeDeptChange">
- <el-option v-for="item in deptOptions" :key="item.id" :label="item.name" :value="item.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="岗位序列" prop="positionSequenceId">
- <el-select v-model="form.positionSequenceId" filterable clearable :loading="sequenceLoading"
- :disabled="readOnly || !form.deptId" :placeholder="form.deptId ? '请选择岗位序列' : '请先选择所属部门'"
- style="width: 100%" @change="onCascadeSequenceChange">
- <el-option v-for="item in sequenceOptions" :key="item.id" :label="item.name" :value="item.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="岗位名称" prop="positionId">
- <el-select v-model="form.positionId" filterable clearable :loading="positionLoading"
- :disabled="readOnly || !form.positionSequenceId" :placeholder="form.positionSequenceId ? '请选择岗位名称' : '请先选择岗位序列'
- " style="width: 100%" @change="onCascadePositionChange">
- <el-option v-for="item in cascadePositionOptions" :key="item.id" :label="item.name"
- :value="item.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="岗位性质" prop="positionType">
- <el-input v-model="form.positionTypeName" readonly placeholder="选择岗位名称后自动带出" />
- </el-form-item>
- <el-form-item label="岗位层级" prop="positionLevelId">
- <el-select v-model="form.positionLevelId" filterable clearable :loading="levelLoading"
- :disabled="readOnly || !form.positionId" :placeholder="form.positionId ? '请选择岗位层级' : '请先选择岗位名称'"
- style="width: 100%" @change="onCascadeLevelChange">
- <el-option v-for="item in levelOptions" :key="item.id" :label="item.name" :value="item.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="直属上级">
- <el-select v-model="form.managerUserId" filterable remote clearable :remote-method="searchManagers"
- :loading="managerLoading" :disabled="readOnly" placeholder="输入姓名/工号搜索全员" style="width: 100%"
- @change="onManagerChange">
- <el-option v-for="item in managerOptions" :key="item.id"
- :label="`${item.name}(${item.employeeNo || '-'})`" :value="item.id" />
- </el-select>
- </el-form-item>
- </div>
- </section>
- <section class="form-section">
- <header>
- <span><i class="el-icon-date"></i></span>
- <div>
- <h3>转正信息</h3>
- <p>设置转正类型、生效日期与签署方式</p>
- </div>
- </header>
- <div class="form-grid">
- <el-form-item label="入职日期">
- <el-input v-model="form.hireDate" disabled />
- </el-form-item>
- <el-form-item label="转正类型" prop="regularizationType">
- <el-select v-model="form.regularizationType" placeholder="请选择转正类型">
- <el-option v-for="(label, value) in typeOptions" :key="value" :label="label" :value="value" />
- </el-select>
- </el-form-item>
- <el-form-item label="计划转正日期" prop="plannedRegularDate">
- <el-date-picker v-model="form.plannedRegularDate" type="date" value-format="yyyy-MM-dd" />
- </el-form-item>
- <el-form-item label="实际生效日期">
- <el-date-picker v-model="form.effectiveDate" type="date" value-format="yyyy-MM-dd"
- placeholder="为空则按计划日期生效" />
- </el-form-item>
- <el-form-item label="签署方式">
- <div class="sign-method-switch">
- <!-- 线上模板功能未开通:按钮弱化展示,点击只提示不切换 -->
- <button type="button" class="sign-method-btn is-unavailable" :class="{ 'is-active': isOnlineSign }"
- :disabled="readOnly" title="暂无开通模板功能,敬请期待"
- @click="!readOnly && setSigningMethod('ONLINE')">
- 线上签署
- </button>
- <button type="button" class="sign-method-btn" :class="{ 'is-active': !isOnlineSign }"
- :disabled="readOnly" @click="!readOnly && setSigningMethod('OFFLINE')">
- 线下签署
- </button>
- </div>
- </el-form-item>
- <!-- 转正申请模板 / 申请资料:二者只能出现一个,占签署方式右侧同一单元格 -->
- <el-form-item v-if="isOnlineSign" label="转正申请模板" prop="onlineDocumentTemplateId">
- <el-select v-model="form.onlineDocumentTemplateId" filterable clearable :loading="templateLoading"
- placeholder="请选择已启用线上模板" @visible-change="onTemplateDropdown">
- <el-option v-for="item in enabledTemplates" :key="item.id" :label="item.templateName || item.name"
- :value="item.id" />
- </el-select>
- </el-form-item>
- <el-form-item v-else label="申请资料" prop="offlineFileIds">
- <fileMain v-model="offlineDocIds" :type="readOnly ? 'view' : ''" size="small" />
- </el-form-item>
- </div>
- <template v-if="form.regularizationType === 'EXTENDED'">
- <div class="form-grid">
- <el-form-item label="延期天数" prop="extensionDays">
- <el-input-number v-model="form.extensionDays" :min="1" controls-position="right" />
- </el-form-item>
- <el-form-item label="延期后试用结束日" prop="newProbationEndDate">
- <el-date-picker v-model="form.newProbationEndDate" type="date" value-format="yyyy-MM-dd" />
- </el-form-item>
- </div>
- <el-form-item label="延期原因" prop="extensionReason">
- <el-input v-model="form.extensionReason" type="textarea" :rows="2" maxlength="200" show-word-limit />
- </el-form-item>
- </template>
- </section>
- </el-form>
- </div>
- <div class="drawer-footer">
- <span>
- <i class="el-icon-info"></i>
- M02 转正模块:提交审批走 BPM 公共流程
- </span>
- <div>
- <el-button :disabled="submitting" @click="drawerVisible = false">关闭</el-button>
- <el-button v-if="!readOnly" :loading="submitting" @click="saveDraft">保存</el-button>
- <el-button v-if="!readOnly && !isLockedForSubmit && $hasPermission('main:hrRegularization:submit')"
- type="primary" :loading="submitting" @click="submitApplication">提交审核</el-button>
- </div>
- </div>
- </el-drawer>
- <!-- BPM 公共审批发起弹框 -->
- <processSubmitDialog :processSubmitDialogFlag.sync="processSubmitDialogFlag" v-if="processSubmitDialogFlag"
- ref="processSubmitDialogRef" @reload="onProcessSubmitReload" />
- <!-- 审批流程详情弹框 -->
- <BpmProcessDetailDialog ref="approvalRecordDialog" />
- </div>
- </template>
- <script>
- import * as XLSX from "xlsx";
- import { showRequestError } from "@/utils/message";
- import { getCurrentUser } from "@/utils/token-util";
- import processSubmitDialog from "@/BIZComponents/processSubmitDialog/processSubmitDialog.vue";
- import BpmProcessDetailDialog from "@/views/bpm/processInstance/detailDialog.vue";
- import {
- adaptEmployee,
- fetchAllUsers,
- getEmployeeById,
- searchUsersByKeyword,
- } from "@/api/organization";
- /* 岗位性质字典与岗位档案同源:api/sys 的 getDictByCode 已兼容结构化项与「{ 1: "全职" }」老格式 */
- import { getDictByCode } from "@/api/sys";
- import {
- REGULARIZATION_TYPE_LABELS,
- DISPLAY_STATUS_LABELS,
- cancelRegularization,
- createRegularization,
- getEmploymentPeriods,
- getRegularizationById,
- getRegularizationPage,
- getRegularizationTemplatePage,
- mergeEmployeeRegularization,
- removeRegularizationBillId,
- resolveRegularizationForUser,
- resolveRegularizationIdFromHistory,
- saveRegularizationBillId,
- updateRegularization,
- } from "@/api/hr/regularization";
- import { REGULARIZATION_APPROVAL_BUSINESS_KEY } from "@/api/hr";
- import positionCascade from "@/views/hrManagement/mixins/positionCascade";
- /* 员工状态码统一取 hr_user_status 字典(不硬编码码值),见 resolveProbationStatusCode */
- import { loadEmployeeStatusOptions } from "@/utils/employee-status-dict";
- /* 所属机构默认值取登录缓存(loginChangeGroupVOList 第一条) */
- import { getCurrentOrgId } from "@/utils/current-org";
- /** 办理状态页签 → 接口查询条件(POST /main/regularizations/page)
- * Swagger 字段 handleStatus 枚举:DRAFT/APPROVING/APPROVED/REJECTED/WITHDRAWN/CANCELLED
- */
- const STATUS_TAB_QUERY = {
- draft: { handleStatus: "DRAFT" },
- reviewing: { handleStatus: "APPROVING" },
- done: { handleStatus: "APPROVED" },
- pending_effective: { handleStatus: "APPROVED" },
- rejected: { handleStatus: "REJECTED" },
- withdrawn: { handleStatus: "WITHDRAWN" },
- cancelled: { handleStatus: "CANCELLED" },
- };
- const emptyFilters = () => ({
- userId: null,
- /** 所属机构主键(后端 organizationId:筛选范围含该机构及全部下级部门),默认登录机构列表第一个 */
- organizationId: getCurrentOrgId() || null,
- regularizationType: "",
- dateRange: [],
- effectiveRange: [],
- });
- const emptyForm = () => ({
- userId: "",
- regularizationId: null,
- employeeNo: "",
- name: "",
- company: "",
- department: "",
- position: "",
- level: "",
- hireDate: "",
- plannedDate: "",
- plannedRegularDate: "",
- regularDate: "",
- effectiveDate: "",
- phone: "",
- manager: "",
- managerUserId: null,
- managerUserName: "",
- // 签署方式(接口字段 signingMethod):ONLINE 线上 / OFFLINE 线下
- signingMethod: "OFFLINE",
- online: false,
- onlineDocumentTemplateId: null,
- approvalDocumentFileId: null,
- offlineFileIds: [],
- regularizationType: "NORMAL",
- regularizationTypeLabel: REGULARIZATION_TYPE_LABELS.NORMAL,
- extensionDays: undefined,
- extensionReason: "",
- newProbationEndDate: "",
- employmentPeriodId: null,
- remark: "",
- status: "pending_apply",
- statusLabel: DISPLAY_STATUS_LABELS.pending_apply,
- approvalStatus: "",
- approvalStatusLabel: "未提交",
- approvalComment: "",
- approvalInstanceId: "",
- // 机构/部门/岗位主键(级联下拉用)
- groupId: null,
- deptId: null,
- positionId: null,
- // 岗位序列/层级/性质字段(提交给后端)
- positionSequenceId: "",
- positionSequence: "",
- positionLevelId: "",
- positionLevel: "",
- positionType: "",
- positionTypeName: "",
- });
- /** 机构 → 部门 → 岗位序列 → 岗位名称 → 岗位性质 → 岗位层级 级联字段映射 */
- const CASCADE_CONFIG = {
- formKey: "form",
- formRef: "applyForm",
- legalId: "groupId",
- legalName: "company",
- deptId: "deptId",
- deptName: "department",
- sequenceId: "positionSequenceId",
- sequenceName: "positionSequence",
- positionId: "positionId",
- positionName: "position",
- typeId: "positionType",
- typeName: "positionTypeName",
- levelId: "positionLevelId",
- levelName: "positionLevel",
- };
- export default {
- name: "RegularizationHandling",
- components: { processSubmitDialog, BpmProcessDetailDialog },
- mixins: [positionCascade],
- data() {
- return {
- loading: false,
- drawerLoading: false,
- submitting: false,
- templateLoading: false,
- rows: [],
- totalCount: 0,
- enabledTemplates: [],
- cacheKey: "hr-regularization-handling-v1",
- /** 查询表单(未提交) */
- draftFilters: emptyFilters(),
- /** 已生效的查询条件 */
- filters: emptyFilters(),
- activeStatus: "all",
- tabs: [
- { label: "全部", value: "all" },
- { label: "待申请", value: "pending_apply" },
- { label: "草稿", value: "draft" },
- { label: "审批中", value: "reviewing" },
- { label: "待生效", value: "pending_effective" },
- { label: "已转正", value: "done" },
- { label: "已驳回", value: "rejected" },
- { label: "已撤回", value: "withdrawn" },
- { label: "已取消", value: "cancelled" },
- ],
- /** 姓名筛选:远程搜索员工 */
- employeeOptions: [],
- employeeLoading: false,
- /** 直属上级:远程搜索全员 */
- managerOptions: [],
- managerLoading: false,
- /** 全量组织(含部门):仅「待申请」按机构上溯匹配员工部门用 */
- orgOptions: [],
- /** 试用期员工(转正申请可选人员 + 待申请列表) */
- trialEmployees: [],
- /** 已有转正单的员工集合(待申请列表用,按需加载) */
- billUserIds: null,
- /** 岗位性质字典(position_nature),用于新建/编辑转正申请时选择岗位性质 */
- positionNatureOptions: [],
- typeOptions: REGULARIZATION_TYPE_LABELS,
- selectedRows: [],
- drawerVisible: false,
- readOnly: false,
- form: emptyForm(),
- approvalDocIds: [],
- offlineDocIds: [],
- processSubmitDialogFlag: false,
- cascadeConfig: CASCADE_CONFIG,
- rules: {
- // 机构/部门/岗位序列/岗位名称/岗位性质/岗位层级/手机号 必填
- ...this.cascadeRequiredRules(CASCADE_CONFIG),
- userId: [{ required: true, message: "请选择员工", trigger: "change" }],
- regularizationType: [
- { required: true, message: "请选择转正类型", trigger: "change" },
- ],
- plannedRegularDate: [
- { required: true, message: "请选择计划转正日期", trigger: "change" },
- ],
- onlineDocumentTemplateId: [
- {
- validator: (rule, value, callback) => {
- if (this.isOnlineSign && !value) {
- callback(new Error("请选择启用模板"));
- } else {
- callback();
- }
- },
- trigger: "change",
- },
- ],
- extensionDays: [
- {
- validator: (rule, value, callback) => {
- if (
- this.form.regularizationType === "EXTENDED" &&
- !(Number(value) > 0)
- ) {
- callback(new Error("请填写延期天数"));
- } else {
- callback();
- }
- },
- trigger: "change",
- },
- ],
- extensionReason: [
- {
- validator: (rule, value, callback) => {
- if (
- this.form.regularizationType === "EXTENDED" &&
- !String(value || "").trim()
- ) {
- callback(new Error("请填写延期原因"));
- } else {
- callback();
- }
- },
- trigger: "blur",
- },
- ],
- },
- };
- },
- computed: {
- styleResponsive() {
- return this.$store.state.theme?.styleResponsive !== false;
- },
- drawerSize() {
- return window.innerWidth < 900 ? "96%" : "720px";
- },
- /**
- * 是否线上签署:以表单 signingMethod(详情接口值)为准,
- * 老数据没有该字段时回退到线上签署开关 online
- */
- isOnlineSign() {
- const method = String(this.form.signingMethod || "").toUpperCase();
- if (method === "ONLINE") return true;
- if (method === "OFFLINE") return false;
- return this.form.online === true;
- },
- /** 提交审批是否锁住(审批中/已转正/已取消时不可再提) */
- isLockedForSubmit() {
- if (!this.form.regularizationId) return false; // 新建中永远允许提交
- return [
- "reviewing",
- "done",
- "pending_effective",
- "cancelled",
- ].includes(this.form.status);
- },
- columns() {
- return [
- { type: "selection", width: 48, align: "center" },
- { type: "index", label: "序号", width: 60, align: "center" },
- {
- prop: "name",
- label: "员工",
- minWidth: 90,
- align: "left",
- showOverflowTooltip: true,
- slot: "name",
- },
- {
- /* 所属机构直取接口 groupName(2026-10-29 用户确认字段口径),不再走 slot 拼 company */
- prop: "groupName",
- label: "所属机构",
- minWidth: 240,
- align: "left",
- showOverflowTooltip: true,
- formatter: (_r, _c, value) => value || "-",
- },
- {
- /* 所属部门直取接口 deptName */
- prop: "deptName",
- label: "所属部门",
- minWidth: 140,
- align: "left",
- showOverflowTooltip: true,
- formatter: (_r, _c, value) => value || "-",
- },
- {
- prop: "position",
- label: "岗位信息",
- minWidth: 130,
- align: "left",
- showOverflowTooltip: true,
- slot: "position",
- },
- {
- prop: "hireDate",
- label: "入职日期",
- width: 100,
- align: "left",
- formatter: (_r, _c, value) => value || "-",
- },
- {
- prop: "plannedRegularDate",
- label: "转正日期",
- width: 100,
- align: "left",
- slot: "date",
- },
- {
- prop: "approvalStatus",
- label: "审核进度",
- minWidth: 90,
- align: "left",
- slot: "approval",
- },
- {
- prop: "createUserName",
- label: "创建人",
- width: 100,
- align: "left",
- formatter: (_r, _c, v) => v || "-",
- },
- {
- prop: "createTime",
- label: "创建时间",
- width: 100,
- align: "left",
- formatter: (_r, _c, v) => v || "-",
- },
- {
- prop: "updateUserName",
- label: "修改人",
- width: 100,
- align: "left",
- formatter: (_r, _c, v) => v || "-",
- },
- {
- prop: "updateTime",
- label: "修改时间",
- width: 100,
- align: "left",
- formatter: (_r, _c, v) => v || "-",
- },
- {
- columnKey: "action",
- label: "操作",
- minWidth: 220,
- slot: "action",
- fixed: "right",
- align: "left",
- },
- ];
- },
- /**
- * 转正申请的「申请员工」下拉:**尚未转正(没有转正单)的试用期员工**。
- * 数据源 trialEmployees 只按 状态=试用期/未离职 拉全量,本身不含「是否已有转正单」的排除,
- * 故这里用 billUserIds(「已有转正单」的员工集合,与「待申请」页签同一份)把已申请的人剔除。
- * ⚠ 编辑/查看已存在单据时,必须把当前申请人保留在选项里,否则 el-select 回显不出 label(会显示裸 id)。
- */
- selectableEmployees() {
- const employees = this.trialEmployees;
- const bill = this.billUserIds;
- const filtered = bill
- ? employees.filter((item) => !bill.has(String(item.userId)))
- : employees;
- const current = this.form?.userId;
- if (!current) return filtered;
- const key = String(current);
- if (filtered.some((item) => String(item.userId) === key)) return filtered;
- /* 回显:当前申请人已被差集剔除(或已不在试用期列表)时补回选项;
- 姓名/工号取单据自身字段,取不到就留空,不编造 */
- const inTrial = employees.find((item) => String(item.userId) === key);
- if (inTrial) return [inTrial, ...filtered];
- return [
- {
- userId: key,
- id: key,
- name: this.form.name || "",
- employeeNo: this.form.employeeNo || "",
- },
- ...filtered,
- ];
- },
- },
- created() {
- // 表格挂载后会自动加载第一页(datasource: fetchPage)
- this.loadTemplates();
- this.loadEmployees();
- /* 「已有转正单的员工集合」改为懒加载(loadBillUserIds 会全量分页拉转正单,
- 进页面就拉会与列表第一页请求重复,见 ensureBillUserIds) */
- this.loadOrganizations();
- this.loadPositionNatureOptions();
- },
- watch: {
- drawerVisible(val) {
- // 弹框打开时预拉一次直属上级候选,确保自动带出的 manager 能回显;
- // 查看详情(readOnly)下拉是禁用的纯回显,不需要候选,省一次 getUserPage
- if (val && !this.readOnly) {
- this.searchManagers("");
- }
- },
- "form.managerUserId"(val) {
- if (!val || !this.drawerVisible) return;
- // 详情态:单据里已带出直属上级名称,本地补一条选项即可回显,不再打 getUserPage
- if (this.readOnly) {
- this.ensureManagerOption();
- return;
- }
- // selectEmployee 异步带出 managerUserId 时,确保 options 里有对应选项
- this.searchManagers("");
- },
- },
- methods: {
- statusLabel(status) {
- return DISPLAY_STATUS_LABELS[status] || status || "-";
- },
- countStatus(s) {
- return s === "all"
- ? this.rows.length
- : this.rows.filter((r) => r.status === s).length;
- },
- canApply(row) {
- return ["pending_apply", "draft", "rejected", "withdrawn"].includes(
- row.status,
- );
- },
- /** 可提交审批:草稿/已撤回/已驳回/已取消 + 有转正单主键(已取消后可重新编辑发起) */
- canEdit(row) {
- return Boolean(row.regularizationId) && ["draft", "withdrawn", "rejected", "cancelled"].includes(row.status);
- },
- canSubmit(row) {
- return (
- row.regularizationId &&
- ["draft", "withdrawn", "rejected", "cancelled"].includes(row.status)
- );
- },
- /** 可查看审批记录:必须有有效的审批实例 id(approvalInstanceId / processInstanceId) */
- canViewApproval(row) {
- return Boolean(
- row?.approvalInstanceId || row?.processInstanceId,
- );
- },
- canCancel(row) {
- return (
- row.regularizationId &&
- ["draft", "pending_apply", "withdrawn", "rejected"].includes(row.status)
- );
- },
- applyFilters() {
- this.filters = {
- userId: this.draftFilters.userId,
- organizationId: this.draftFilters.organizationId,
- regularizationType: this.draftFilters.regularizationType,
- dateRange: this.draftFilters.dateRange
- ? [...this.draftFilters.dateRange]
- : [],
- effectiveRange: this.draftFilters.effectiveRange
- ? [...this.draftFilters.effectiveRange]
- : [],
- };
- this.reloadTable();
- },
- resetFilters() {
- this.draftFilters = emptyFilters();
- this.filters = emptyFilters();
- this.activeStatus = "all";
- this.reloadTable();
- },
- async loadTemplates() {
- this.templateLoading = true;
- try {
- const data = await getRegularizationTemplatePage({
- pageNum: 1,
- size: 200,
- templateStatus: "ENABLED",
- });
- this.enabledTemplates = Array.isArray(data?.list) ? data.list : [];
- } catch (error) {
- this.enabledTemplates = [];
- showRequestError(error, "转正模板加载失败");
- } finally {
- this.templateLoading = false;
- }
- },
- /** 已生效的查询条件 → 分页查询参数(POST /main/regularizations/page) */
- listQuery(extra = {}) {
- const plan = this.filters.dateRange || [];
- const effective = this.filters.effectiveRange || [];
- return {
- userId: this.filters.userId || undefined,
- // Swagger:organizationId 按机构筛选(含全部下级部门),deptId 是部门主键
- organizationId: this.filters.organizationId || undefined,
- regularizationType: this.filters.regularizationType || undefined,
- // Swagger 只有 regularDateStart/End(转正日期),前端计划/生效区间合起来
- regularDateStart:
- plan[0] || effective[0] || undefined,
- regularDateEnd: plan[1] || effective[1] || undefined,
- sortName: "createTime",
- orderBy: "descending",
- ...(STATUS_TAB_QUERY[this.activeStatus] || {}),
- ...extra,
- };
- },
- reloadTable() {
- this.selectedRows = [];
- this.billUserIds = null;
- this.$nextTick(() => this.$refs.table?.reload({ page: 1 }));
- },
- /** 列表数据源:转正单分页接口 + 待申请(试用期员工本地拼装) */
- async fetchPage({ page, limit }) {
- const pageNum = Number(page) || 1;
- const size = Number(limit) || 8;
- this.loading = true;
- try {
- if (this.activeStatus === "pending_apply") {
- const all = await this.buildPendingRows();
- const start = (pageNum - 1) * size;
- this.rows = all;
- this.totalCount = all.length;
- return { list: all.slice(start, start + size), count: all.length };
- }
- const data = await getRegularizationPage(
- this.listQuery({ pageNum, size, offset: (pageNum - 1) * size }),
- );
- /* ★ 不再逐行回查员工档案(原 withRegularizationEmployeeInfo,属前端兜底 + N+1),
- 接口没返回的字段由 row.fieldIssues 带出并明确提示后端补返回 */
- const list = Array.isArray(data.list) ? data.list : [];
- this.rows = list;
- this.totalCount = Number(data.count ?? list.length);
- const issues = [...new Set(list.flatMap((row) => row.fieldIssues || []))];
- if (issues.length) {
- this.$message.warning(`转正单接口未返回字段:${issues.join("、")},请后端补充返回`);
- }
- return { list, count: this.totalCount };
- } catch (error) {
- this.rows = [];
- this.totalCount = 0;
- showRequestError(error, "转正列表加载失败");
- return { list: [], count: 0 };
- } finally {
- this.loading = false;
- }
- },
- /**
- * 待申请:试用期员工里还没有转正单的人。
- * 接口没有「待申请」这种状态,所以先取试用期员工,再排除已有转正单的员工。
- */
- async buildPendingRows() {
- const employees = await this.loadEmployees();
- await this.ensureBillUserIds();
- const f = this.filters;
- const plan = f.dateRange || [];
- const deptKey =
- f.organizationId == null ? "" : String(f.organizationId);
- const parents = new Map(
- this.orgOptions.map((item) => [
- String(item.id),
- item.parentId == null ? "" : String(item.parentId),
- ]),
- );
- // 选中上级机构时,其下级部门也要命中
- const underSelectedOrg = (emp) => {
- if (!deptKey) return true;
- const keys = [emp.deptId, ...(emp.deptIds || [])]
- .filter((id) => id !== null && id !== undefined && id !== "")
- .map((id) => String(id));
- return keys.some((key) => {
- let cursor = key;
- let guard = 0;
- while (cursor && guard < 20) {
- if (cursor === deptKey) return true;
- cursor = parents.get(cursor) || "";
- guard += 1;
- }
- return false;
- });
- };
- return employees
- .filter((emp) => !this.billUserIds.has(String(emp.userId)))
- .filter((emp) => !f.userId || String(emp.userId) === String(f.userId))
- .filter(underSelectedOrg)
- .filter((emp) => {
- const date = emp.plannedRegularDate || emp.plannedDate || "";
- return !plan.length || (date && date >= plan[0] && date <= plan[1]);
- })
- .map((emp) => ({ ...emp, id: `pending-${emp.userId}` }));
- },
- /**
- * 「已有转正单的员工集合」懒加载入口:
- * 只在切到「待申请」页签、或打开新建申请弹窗(申请员工下拉要剔除已申请的人)时拉取一次,
- * 避免进页面就全量分页拉转正单、与列表第一页请求重复。
- */
- async ensureBillUserIds() {
- if (this.billUserIds) return this.billUserIds;
- this.billUserIds = await this.loadBillUserIds();
- return this.billUserIds;
- },
- /** 已有转正单的员工 id 集合(分页拉全量,用于「待申请」差集) */
- async loadBillUserIds() {
- const ids = new Set();
- const size = 200;
- let pageNum = 1;
- let total = Infinity;
- while (pageNum <= 10 && ids.size < total) {
- const { list, count } = await getRegularizationPage({ pageNum, size });
- total = Number(count) || 0;
- if (!list.length) break;
- list.forEach((item) => {
- if (item.userId !== null && item.userId !== undefined && item.userId !== "") {
- ids.add(String(item.userId));
- }
- });
- if (list.length < size) break;
- pageNum += 1;
- }
- return ids;
- },
- /**
- * 试用期状态码:从员工状态字典 hr_user_status 里按文案「试用期」取(value 即数字状态码)。
- * ★ 原先硬编码 status:"5" 是错的(字典口径 6=试用期);这里不再猜码:
- * 字典没配「试用期」或字典加载失败 → 直接抛错,由 loadEmployees 明确提示。
- */
- async resolveProbationStatusCode() {
- const options = await loadEmployeeStatusOptions();
- const hit = options.find((item) => String(item.label || "").includes("试用期"));
- if (!hit) {
- throw new Error(
- "员工状态字典(hr_user_status)中未找到「试用期」选项,请在字典管理中配置后再使用转正办理",
- );
- }
- return hit.value;
- },
- /** 试用期员工(转正申请可选人员 / 待申请列表数据源) */
- async loadEmployees(force = false) {
- if (!force && this.trialEmployees.length) return this.trialEmployees;
- try {
- /* 状态码取自字典(试用期),不硬编码 */
- const probationStatusCode = await this.resolveProbationStatusCode();
- const { list } = await fetchAllUsers(
- { status: probationStatusCode, isQueryLZ: 0 },
- { pageSize: 200, maxPages: 30 },
- );
- this.trialEmployees = (list || []).map((item) => {
- const employee = adaptEmployee(item);
- const row = mergeEmployeeRegularization(employee, null);
- return {
- ...row,
- userId: employee.id,
- deptIds: employee.deptIds || [],
- };
- });
- } catch (error) {
- this.trialEmployees = [];
- showRequestError(error, "试用期员工加载失败");
- }
- return this.trialEmployees;
- },
- async loadOrganizations() {
- // 机构/部门基础数据与级联下拉共用一次请求
- if (!this.orgList.length) await this.loadCascadeOrganizations();
- // 全量组织(含部门)平铺:仅用于「待申请」按机构上溯匹配员工部门;
- // 筛选区「所属机构」下拉统一用 mixin 的 legalEntities(机构,不放部门)
- this.orgOptions = this.orgList.map((item) => ({
- id: item.id,
- name: item.name,
- parentId: item.parentId,
- }));
- },
- /** 加载岗位性质字典(position_nature);getDictByCode 已归一两种返回结构,无需再解析 res.data */
- async loadPositionNatureOptions() {
- try {
- const items = await getDictByCode("position_nature");
- this.positionNatureOptions = (Array.isArray(items) ? 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;
- /* 选项值统一字符串:接口编码多为数字,回填值是字符串时不转类型匹配不上 */
- return {
- id: String(value),
- value: String(value),
- key: item.key != null ? String(item.key) : String(value),
- label,
- };
- })
- .filter(Boolean);
- } catch (error) {
- this.positionNatureOptions = [];
- }
- },
- /** 岗位性质字典项:兼容「字典值/编码」与「历史名称文案」两种输入 */
- findPositionNature(raw) {
- const text = String(raw ?? "").trim();
- if (!text) return null;
- return (
- this.positionNatureOptions.find(
- (item) =>
- String(item.id) === text ||
- String(item.value) === text ||
- String(item.key) === text ||
- String(item.label) === text,
- ) || null
- );
- },
- /** 岗位性质名称/字典值 → 字典值(字符串,保证与下拉选项同类型) */
- matchPositionTypeId(label) {
- const hit = this.findPositionNature(label);
- return hit ? String(hit.id) : "";
- },
- /** 岗位性质下拉切换:同步 id 与名称 */
- onPositionTypeChange(value) {
- const matched = this.positionNatureOptions.find(
- (item) => String(item.id) === String(value),
- );
- this.form.positionType = matched ? matched.id : "";
- this.form.positionTypeName = matched ? matched.label : "";
- },
- async searchEmployees(keyword) {
- this.employeeLoading = true;
- try {
- const data = await searchUsersByKeyword(keyword, {
- pageNum: 1,
- size: 30,
- });
- const list = Array.isArray(data?.list) ? data.list : [];
- this.employeeOptions = list.map((item) => {
- const employee = adaptEmployee(item);
- return { ...employee, id: String(employee.id) };
- });
- } catch (error) {
- this.employeeOptions = [];
- } finally {
- this.employeeLoading = false;
- }
- },
- async searchManagers(keyword) {
- this.managerLoading = true;
- try {
- const data = await searchUsersByKeyword(keyword || "", {
- pageNum: 1,
- size: 30,
- });
- const list = Array.isArray(data?.list) ? data.list : [];
- this.managerOptions = list.map((item) => {
- const employee = adaptEmployee(item);
- return {
- ...employee,
- id: String(employee.id),
- name: employee.name || employee.employeeName || "",
- employeeNo: employee.employeeNo || employee.jobNumber || "",
- };
- });
- // 回显兜底:如果 form.managerUserId 有值但选项里没匹配上,加一条假选项让 el-select 能显示
- this.ensureManagerOption();
- } catch (error) {
- this.managerOptions = [];
- } finally {
- this.managerLoading = false;
- }
- },
- /** 直属上级回显:选项里没有当前 managerUserId 时本地补一条,不发请求 */
- ensureManagerOption() {
- const val = this.form?.managerUserId;
- if (!val) return;
- const hasMatch = this.managerOptions.some(
- (it) => String(it.id) === String(val),
- );
- if (!hasMatch) {
- this.managerOptions.unshift({
- id: String(val),
- name: this.form.managerUserName || this.form.manager || "已离职员工",
- employeeNo: "",
- });
- }
- },
- onManagerChange(userId) {
- if (!userId) {
- this.form.managerUserId = null;
- this.form.managerUserName = "";
- this.form.manager = "";
- return;
- }
- const match =
- this.managerOptions.find((it) => String(it.id) === String(userId)) || {};
- this.form.managerUserId = String(userId);
- this.form.managerUserName = match.name || "";
- this.form.manager = match.name || "";
- },
- /** 行 / 详情数据 → 规范签署方式:ONLINE 线上 / OFFLINE 线下 */
- normalizeSigningMethod(raw = {}) {
- const method = String(raw.signingMethod || raw.signMethod || "")
- .trim()
- .toUpperCase();
- if (method === "ONLINE" || method === "OFFLINE") return method;
- // 老数据没有签署方式字段时,按线上单据字段推断
- return raw.online === true ||
- raw.onlineDocumentTemplateId ||
- raw.onlineDocumentId
- ? "ONLINE"
- : "OFFLINE";
- },
- /** 行 / 详情数据 → 表单签署方式与线上开关 */
- signStateOf(raw = {}) {
- const signingMethod = this.normalizeSigningMethod(raw);
- return { signingMethod, online: signingMethod === "ONLINE" };
- },
- /** 线上模板功能未开通的统一提示:先关掉上一条,避免连点堆叠多条提示 */
- warnTemplateDisabled() {
- this.$message.closeAll();
- this.$message.warning("暂无开通模板功能,敬请期待");
- },
- /** 转正申请模板下拉展开时同样提示(历史线上转正单该字段仍会渲染) */
- onTemplateDropdown(visible) {
- if (visible) this.warnTemplateDisabled();
- },
- /** 切换签署方式:同时更新 signingMethod 与线上开关,并清掉另一侧取值 */
- setSigningMethod(method) {
- const next = String(method).toUpperCase() === "ONLINE" ? "ONLINE" : "OFFLINE";
- if (next === "ONLINE") {
- // 线上模板未开通:只提示,不切换签署方式
- this.warnTemplateDisabled();
- return;
- }
- if (this.form.signingMethod === next) return;
- this.form.signingMethod = next;
- this.form.online = next === "ONLINE";
- // 线上模板与线下附件互斥:切换时两侧都清空,避免提交时同时带上模板与附件
- this.form.onlineDocumentTemplateId = null;
- this.form.offlineFileIds = [];
- this.form.approvalDocumentFileId = null;
- this.offlineDocIds = [];
- this.$nextTick(() => {
- this.$refs.applyForm?.clearValidate(["onlineDocumentTemplateId", "offlineFileIds"]);
- });
- },
- /** 取员工的转正单主键:优先列表行,其次分页接口,最后本地缓存兜底 */
- async resolveBillId(row = {}) {
- if (row.regularizationId) return row.regularizationId;
- const userId = row.userId || this.form.userId;
- if (!userId) return null;
- try {
- const bill = await resolveRegularizationForUser(userId);
- if (bill?.regularizationId) return bill.regularizationId;
- } catch (error) {
- /* 接口不可用时回退本地缓存 */
- }
- return resolveRegularizationIdFromHistory(userId);
- },
- async fillEmploymentPeriod(userId) {
- try {
- const periods = await getEmploymentPeriods(userId);
- const list = Array.isArray(periods)
- ? periods
- : Array.isArray(periods?.list)
- ? periods.list
- : [];
- const active =
- list.find((p) => String(p.periodStatus || "").toUpperCase() === "ACTIVE") ||
- list[0];
- if (!active) return;
- // json-bigint storeAsString 后 id 应为字符串,强制 String 兜底防精度丢失
- const id = String(active.id ?? "").trim();
- if (id && /^\d+$/.test(id)) {
- this.form.employmentPeriodId = id;
- }
- if (!this.form.plannedRegularDate) {
- this.form.plannedRegularDate =
- (active.probationEndDate || active.regularDate || "").slice(0, 10) ||
- "";
- this.form.plannedDate = this.form.plannedRegularDate;
- }
- } catch (e) {
- // 任职周期可选
- }
- },
- async selectEmployee(userId) {
- const row =
- this.trialEmployees.find((x) => String(x.userId) === String(userId)) ||
- this.rows.find((x) => String(x.userId) === String(userId));
- if (row) {
- this.form = {
- ...emptyForm(),
- ...JSON.parse(JSON.stringify(row)),
- regularizationId: null,
- approvalInstanceId: null,
- approvalStatus: "",
- approvalStatusLabel: "未提交",
- status: "",
- statusLabel: "",
- userId: row.userId,
- plannedRegularDate:
- row.plannedRegularDate || row.plannedDate || row.regularDate || "",
- effectiveDate: row.effectiveDate || "",
- ...this.signStateOf({}),
- };
- } else {
- try {
- const raw = await getEmployeeById(userId);
- const emp = adaptEmployee(raw || {});
- this.form = {
- ...emptyForm(),
- ...mergeEmployeeRegularization(emp, null),
- userId: emp.id,
- };
- } catch (error) {
- showRequestError(error, "员工信息加载失败");
- return;
- }
- }
- // 岗位性质:档案带出编码(兼容历史名称文案),统一反查成「字典值 + 名称」
- const natureOption = this.findPositionNature(
- this.form.positionType || this.form.positionTypeName,
- );
- this.form.positionType = natureOption ? String(natureOption.id) : "";
- if (natureOption) this.form.positionTypeName = natureOption.label;
- await this.fillEmploymentPeriod(userId);
- // 逐级预热机构/部门/岗位序列/岗位/层级下拉,保证带出的值能正常显示
- await this.primeCascade();
- /* 员工档案没带出机构时,默认登录机构列表第一个 */
- if (!this.form.groupId) this.applyDefaultCascadeOrg();
- // selectEmployee 始终是新建语义(regularizationId=null),
- // 不再 hydrateBill 把旧单状态覆盖回来
- },
- /**
- * 接口字段异常提示(★ 项目约定 2026-10-16:前端不做兜底)
- * 接口没返回的字段不拿别处数据拼凑,只明确提示「该字段接口未返回」,由后端补字段。
- */
- warnFieldIssues(record) {
- const issues = record?.fieldIssues || [];
- if (!issues.length) return;
- this.$message.warning(
- `转正单接口未返回字段:${issues.join("、")},请后端补充返回`,
- );
- },
- async hydrateBill(billId) {
- if (!billId) return;
- try {
- const bill = await getRegularizationById(billId, {
- userId: this.form.userId,
- employeeNo: this.form.employeeNo,
- name: this.form.name,
- phone: this.form.phone,
- company: this.form.company,
- department: this.form.department,
- position: this.form.position,
- level: this.form.level,
- manager: this.form.manager,
- // 机构/部门/岗位主键:后端没返回时用员工档案已带出的值兜底
- groupId: this.form.groupId,
- deptId: this.form.deptId,
- positionId: this.form.positionId,
- // 岗位序列/层级/性质:后端没返回时用员工档案已带出的值兜底
- positionSequenceId: this.form.positionSequenceId,
- positionSequence: this.form.positionSequence,
- positionLevelId: this.form.positionLevelId,
- positionLevel: this.form.positionLevel,
- positionType: this.form.positionType,
- positionTypeName: this.form.positionTypeName,
- });
- // 签署方式以详情接口 signingMethod 为准(ONLINE 线上 / OFFLINE 线下)
- const signingState = this.signStateOf(bill);
- /* 接口字段异常明确提示(前端不做兜底):字段缺失属接口问题,需后端补返回 */
- this.warnFieldIssues(bill);
- this.form = {
- ...this.form,
- ...bill,
- id: this.form.userId || bill.userId,
- userId: this.form.userId || bill.userId,
- regularizationId: bill.regularizationId || bill.billId || bill.id,
- signingMethod: signingState.signingMethod,
- online: signingState.online,
- // 防御:如果后端返回的 employmentPeriodId 是负值/非数字(精度丢失),保留 fillEmploymentPeriod 已设置的正确值
- employmentPeriodId: (() => {
- const raw = bill.employmentPeriodId;
- if (raw === undefined || raw === null || raw === "")
- return this.form.employmentPeriodId;
- const text = String(raw).trim();
- return /^\d+$/.test(text) ? text : this.form.employmentPeriodId;
- })(),
- };
- // 申请资料(approvalDocumentFileId):多附件主键以英文逗号拼接,回显时拆成数组
- const approvalDocIds = String(bill.approvalDocumentFileId ?? "")
- .split(",")
- .map((id) => id.trim())
- .filter(Boolean);
- this.approvalDocIds = approvalDocIds;
- // 线下附件回显:优先用申请资料字段,其次兼容旧的附件字段
- const offlineRaw = approvalDocIds.length
- ? approvalDocIds
- : bill.offlineFileIds || bill.offlineFileId || bill.attachmentIds;
- this.offlineDocIds = Array.isArray(offlineRaw)
- ? offlineRaw
- : offlineRaw
- ? String(offlineRaw).split(",").filter(Boolean)
- : [];
- } catch (error) {
- showRequestError(error, "转正单加载失败");
- }
- },
- async openCreate(row) {
- this.readOnly = false;
- this.drawerLoading = true;
- this.drawerVisible = true;
- this.approvalDocIds = [];
- this.offlineDocIds = [];
- try {
- if (row) {
- const existingId = row.regularizationId || row.billId || row.id;
- const isEdit = Boolean(existingId);
- this.form = {
- ...emptyForm(),
- ...JSON.parse(JSON.stringify(row)),
- regularizationId: existingId || null,
- approvalInstanceId: null,
- approvalStatus: "",
- userId: row.userId,
- plannedRegularDate:
- row.plannedRegularDate || row.plannedDate || row.regularDate || "",
- ...this.signStateOf(isEdit ? row : {}),
- };
- await this.fillEmploymentPeriod(row.userId);
- if (isEdit) {
- await this.hydrateBill(existingId);
- }
- await this.primeCascade();
- } else {
- this.form = emptyForm();
- /* 新建:申请员工下拉要剔除已有转正单的人,先确保「已有转正单」集合已加载 */
- await this.ensureBillUserIds();
- }
- this.$nextTick(() => this.$refs.applyForm?.clearValidate());
- } finally {
- this.drawerLoading = false;
- }
- },
- async openDetail(row) {
- this.readOnly = true;
- this.drawerLoading = true;
- this.drawerVisible = true;
- try {
- this.form = {
- ...emptyForm(),
- ...JSON.parse(JSON.stringify(row)),
- userId: row.userId,
- ...this.signStateOf(row),
- };
- const billId = await this.resolveBillId(row);
- if (billId) await this.hydrateBill(billId);
- await this.primeCascade();
- } finally {
- this.drawerLoading = false;
- }
- },
- async ensureDraftId() {
- // 模板里「申请资料」文件上传器 v-model 绑的是 offlineDocIds
- // 接口字段 approvalDocumentFileId:多个附件主键以英文逗号拼接
- const docIds = (this.offlineDocIds || [])
- .map((id) => String(id ?? "").trim())
- .filter(Boolean);
- this.form.approvalDocumentFileId = docIds.length ? docIds.join(",") : null;
- const payload = {
- ...this.form,
- regularizationId: this.form.regularizationId,
- userId: this.form.userId,
- };
- if (this.form.regularizationId) {
- await updateRegularization(payload);
- return this.form.regularizationId;
- }
- const id = await createRegularization(payload);
- this.form.regularizationId = id;
- saveRegularizationBillId(this.form.userId, id);
- return id;
- },
- saveDraft() {
- if (!this.form.userId) {
- this.$message.warning("请先选择员工");
- return;
- }
- this.$refs.applyForm.validate(async (valid) => {
- if (!valid) return;
- this.submitting = true;
- try {
- await this.ensureDraftId();
- this.$message.success("草稿已保存");
- this.drawerVisible = false;
- this.reloadTable();
- } catch (error) {
- showRequestError(error, "保存草稿失败");
- } finally {
- this.submitting = false;
- }
- });
- },
- /**
- * 提交转正审批:转正模块 Swagger 没有 /submit endpoint,
- * 先保存草稿,再通过 BPM 公共组件发起审批流程。
- */
- submitApplication() {
- this.$refs.applyForm.validate(async (valid) => {
- if (!valid) return;
- this.submitting = true;
- try {
- const id = await this.ensureDraftId();
- this.submitting = false;
- // 不关抽屉,弹 BPM 发起审批;BPM 提交成功后 @reload 再统一关
- this.launchApproval(this.form);
- } catch (error) {
- this.submitting = false;
- showRequestError(error, "保存草稿失败");
- }
- });
- },
- /** 弹 BPM processSubmitDialog 发起审批 */
- launchApproval(row) {
- const currentUser = getCurrentUser() || {};
- this.processSubmitDialogFlag = true;
- this.$nextTick(() => {
- this.$refs.processSubmitDialogRef?.init({
- businessId: row.regularizationId || row.id,
- businessKey: REGULARIZATION_APPROVAL_BUSINESS_KEY,
- formCreateUserId:
- row.createUserId ||
- currentUser.id ||
- currentUser.userId ||
- "",
- variables: {
- businessCode:
- row.regularizationId ? String(row.regularizationId) : "",
- businessName: row.name || "",
- businessType:
- (row.regularizationTypeLabel || "转正") +
- "转正",
- },
- });
- });
- },
- onProcessSubmitReload() {
- this.drawerVisible = false;
- this.processSubmitDialogFlag = false;
- this.reloadTable();
- },
- /** 查看审批流程 */
- openApprovalRecord(row) {
- // 跟离职一致:直接调 BpmProcessDetailDialog.open,组件内部会从
- // row.processInstanceId / row.approvalInstanceId / row.processInstance 解析 id
- this.$refs.approvalRecordDialog?.open(row);
- },
- cancelRow(row) {
- this.$prompt("请输入取消原因", "取消转正单", {
- inputType: "textarea",
- inputValidator: (v) =>
- Boolean(String(v || "").trim()) || "请填写取消原因",
- })
- .then(async ({ value }) => {
- try {
- await cancelRegularization(row.regularizationId, { reason: value });
- removeRegularizationBillId(row.userId);
- this.$message.success("已取消");
- this.reloadTable();
- } catch (error) {
- showRequestError(error, "取消失败");
- }
- })
- .catch(() => { });
- },
- statusClass(s) {
- return (
- {
- pending_apply: "pending",
- draft: "pending",
- reviewing: "reviewing",
- done: "success",
- rejected: "danger",
- withdrawn: "pending",
- cancelled: "danger",
- }[s] || "pending"
- );
- },
- statusTagType(s) {
- const cls = this.statusClass(s);
- if (cls === "success") return "success";
- if (cls === "danger") return "danger";
- if (cls === "reviewing") return "warning";
- return "info";
- },
- approvalType(s) {
- const code = String(s || "").toUpperCase();
- if (code === "APPROVED") return "success";
- if (code === "REJECTED" || code === "CANCELLED") return "danger";
- if (!code || code === "DRAFT") return "info";
- return "warning";
- },
- dateHint(r) {
- if (r.status === "done") return "已生效";
- if (r.status === "reviewing") return "审批中";
- return "待办理";
- },
- exportRows() {
- const rows = this.selectedRows.length ? this.selectedRows : this.rows;
- const headers = [
- "工号",
- "姓名",
- "所属机构",
- "所属部门",
- "岗位",
- "入职日期",
- "转正日期",
- "转正类型",
- "办理状态",
- "审核状态",
- "创建人",
- "创建时间",
- "修改人",
- "修改时间",
- ];
- const data = rows.map((r) => ({
- 工号: r.employeeNo || "",
- 姓名: r.name || "",
- 所属机构: r.company || "",
- 所属部门: r.department || "",
- 岗位: r.position || "",
- 入职日期: r.hireDate || "",
- 转正日期:
- r.effectiveDate || r.regularDate || r.plannedRegularDate || "",
- 转正类型: r.regularizationTypeLabel || "",
- 办理状态: r.effectiveStatusLabel || r.approvalStatusLabel || "",
- 审核状态: r.approvalStatusLabel || "",
- 创建人: r.createUserName || "",
- 创建时间: r.createTime || "",
- 修改人: r.updateUserName || "",
- 修改时间: r.updateTime || "",
- }));
- const ws = XLSX.utils.json_to_sheet(data, { header: headers });
- // 列宽适配
- ws["!cols"] = [
- { wch: 12 }, // 工号
- { wch: 12 }, // 姓名
- { wch: 28 }, // 所属机构
- { wch: 18 }, // 所属部门
- { wch: 14 }, // 岗位
- { wch: 14 }, // 入职日期
- { wch: 14 }, // 转正日期
- { wch: 12 }, // 转正类型
- { wch: 12 }, // 办理状态
- { wch: 12 }, // 审核状态
- { wch: 10 }, // 创建人
- { wch: 20 }, // 创建时间
- { wch: 10 }, // 修改人
- { wch: 20 }, // 修改时间
- ];
- const wb = XLSX.utils.book_new();
- XLSX.utils.book_append_sheet(wb, ws, "转正办理");
- const fileName = `转正办理_${new Date().toISOString().slice(0, 10)}.xlsx`;
- XLSX.writeFile(wb, fileName);
- },
- },
- };
- </script>
- <style src="../../../styles/views/regularizationHandling/index.scss" scoped lang="scss"></style>
- <style scoped>
- .upload-tip {
- margin: 8px 0 0;
- color: #7e8b9d;
- font-size: 14px;
- }
- </style>
|