| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959 |
- <template>
- <el-dialog
- :visible.sync="dialogVisible"
- :width="fullscreen ? '100%' : '1080px'"
- :top="fullscreen ? '0' : '3vh'"
- append-to-body
- :close-on-click-modal="false"
- :close-on-press-escape="true"
- :before-close="handleBeforeClose"
- :custom-class="'employee-archive-dialog' + (fullscreen ? ' is-fullscreen' : '')"
- @closed="resetForm"
- >
- <div slot="title" class="archive-dialog-title">
- <span>{{ isEdit ? '编辑员工档案' : '新增员工档案' }}</span>
- <button
- type="button"
- class="archive-dialog-fullscreen"
- :title="fullscreen ? '退出全屏' : '全屏'"
- @click.stop="toggleFullscreen"
- >
- <i :class="fullscreen ? 'el-icon-copy-document' : 'el-icon-full-screen'"></i>
- </button>
- </div>
- <div class="archive-form-summary">
- <span class="summary-icon"><i class="el-icon-user"></i></span>
- <div class="summary-copy">
- <strong>{{ form.name || '新员工档案' }}</strong>
- <p>{{ form.employeeNo || '待生成工号' }}</p>
- </div>
- <div class="summary-completion">
- <div><span>资料完整度</span><strong>{{ completionRate }}%</strong></div>
- <span class="completion-track"><i :style="{ width: `${completionRate}%` }"></i></span>
- </div>
- </div>
- <el-form ref="form" :model="form" :rules="rules" label-position="top" size="small" class="archive-form-shell">
- <el-tabs v-model="activeTab" tab-position="left" class="archive-form-tabs">
- <el-tab-pane v-for="section in sections" :key="section.name" :name="section.name">
- <span slot="label" class="section-tab-label">
- <i :class="section.icon"></i>
- <span><strong>{{ section.label }}</strong><small>{{ section.subtitle }}</small></span>
- <em>{{ sectionProgress[section.name] }}</em>
- </span>
- <div v-if="section.name === 'base'" class="section-content">
- <header class="section-title">
- <div>
- <h3>基本信息</h3>
- <p>员工身份、组织任职与联系方式,字段与档案模型一一对应</p>
- </div><span>必填 {{ requiredProgress.base }}</span>
- </header>
- <div class="subsection-title"><strong>身份信息</strong></div>
- <el-row :gutter="16">
- <el-col :xs="24" :sm="12"><el-form-item label="姓名" prop="name"><el-input v-model.trim="form.name"
- maxlength="20" placeholder="请输入员工真实姓名" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="性别" prop="gender"><el-select v-model="form.gender"
- class="full-width"><el-option v-for="item in genderOptions" :key="item" :label="item"
- :value="item" /></el-select></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="身份证" prop="idCard"><el-input
- v-model.trim="form.idCard" maxlength="18" placeholder="请输入18位身份证号"
- @input="handleIdCardInput" @blur="parseIdCard" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="出生日期" prop="birthDate"><el-date-picker
- v-model="form.birthDate" value-format="yyyy-MM-dd" type="date" placeholder="可由身份证自动生成"
- class="full-width" @change="syncAge" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="年龄"><el-input :value="form.age || ''" readonly
- placeholder="根据出生日期自动计算" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="民族"><el-select v-model="form.ethnic" filterable
- allow-create default-first-option class="full-width"><el-option v-for="item in ethnicOptions"
- :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="政治面貌"><el-input v-model.trim="form.politicalStatus"
- maxlength="20" placeholder="党员、预备党员、群众等" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="婚姻状态"><el-select v-model="form.maritalStatus"
- class="full-width"><el-option v-for="item in maritalOptions" :key="item" :label="item"
- :value="item" /></el-select></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="籍贯"><el-input v-model.trim="form.nativePlace"
- maxlength="30" placeholder="省 / 市" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="最高学历"><el-select v-model="form.education"
- class="full-width"><el-option v-for="item in educationOptions" :key="item" :label="item"
- :value="item" /></el-select></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="毕业院校"><el-input v-model.trim="form.school" maxlength="40"
- placeholder="请输入毕业院校" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="户籍地址"><el-input v-model.trim="form.registeredAddress"
- maxlength="100" placeholder="请输入户籍地址" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="现居住地址"><el-input v-model.trim="form.currentAddress"
- maxlength="100" placeholder="请输入现居住地址" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="工作地址"><el-input v-model.trim="form.workAddress"
- maxlength="60" placeholder="实际办公地点" /></el-form-item></el-col>
- </el-row>
- <div class="subsection-title"><strong>组织与任职</strong></div>
- <el-row :gutter="16">
- <el-col :xs="24" :sm="12"><el-form-item label="工号" prop="employeeNo"><el-input
- v-model.trim="form.employeeNo" maxlength="20" placeholder="集团全局唯一"><el-button slot="append"
- :disabled="isEdit" @click="generateEmployeeNo">生成</el-button></el-input></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="用户账号"><el-input v-model.trim="form.account" maxlength="30"
- placeholder="系统登录账号,支持自动生成、手动修改"><template
- slot="prepend">@</template></el-input></el-form-item></el-col>
- <el-col :xs="24" :sm="12">
- <el-form-item label="所属机构" prop="groupId">
- <el-select
- v-model="form.groupId"
- filterable
- class="full-width"
- placeholder="请选择法人组织"
- @change="handleLegalEntityChange"
- >
- <el-option
- v-for="item in legalEntities"
- :key="String(item.id)"
- :label="item.name"
- :value="String(item.id)"
- />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :xs="24" :sm="12">
- <el-form-item label="所属工厂">
- <el-select v-model="form.factoryId" filterable clearable class="full-width"
- :loading="factoryLoading"
- :disabled="!form.groupId"
- :placeholder="form.groupId ? '请选择所属工厂' : '请先选择所属机构'"
- @change="handleFactoryChange">
- <el-option v-for="item in factoryOptions" :key="String(item.id)" :label="item.name"
- :value="item.id" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="隶属部门"><el-cascader
- :key="`dept-cascader-${form.groupId || 'none'}-${form.factoryId || 'none'}-${deptCascaderKey}`"
- v-model="form.deptIds"
- :options="deptOptions" :props="deptCascaderProps" filterable clearable collapse-tags
- class="full-width"
- :disabled="!form.factoryId"
- :placeholder="form.factoryId ? '请选择隶属部门(可多选)' : (form.groupId ? '请先选择所属工厂' : '请先选择所属机构')"
- @change="handleOrganizationChange" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12">
- <el-form-item label="直属上级">
- <el-select v-model="form.managerId" filterable clearable class="full-width" :loading="managerLoading"
- :disabled="!hasDept" :placeholder="hasDept ? '请选择直属上级' : '请先选择隶属部门'"
- @change="handleManagerChange">
- <el-option v-for="item in managerOptions" :key="item.id" :label="item.name" :value="item.id" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="入职时间" prop="hireDate"><el-date-picker
- v-model="form.hireDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择入职日期"
- class="full-width" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="转正日期"><el-date-picker v-model="form.regularDate"
- value-format="yyyy-MM-dd" type="date" placeholder="转正生效日期"
- class="full-width" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12">
- <el-form-item label="岗位" prop="positionId">
- <el-select :key="`position-select-${positionSelectKey}`" v-model="form.positionId"
- filterable clearable class="full-width"
- :loading="positionLoading"
- :disabled="!hasDept"
- :placeholder="hasDept ? '请选择本部门已通过申请的岗位' : '请先选择隶属部门'"
- @change="handlePositionChange">
- <el-option v-for="item in positionOptions" :key="`pos-${String(item.id)}`"
- :label="positionOptionLabel(item)" :value="String(item.id)" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="员工状态" prop="status"><el-select v-model="form.status"
- class="full-width"><el-option v-for="item in statusOptions" :key="item" :label="item"
- :value="item" /></el-select></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="岗位性质"><el-select v-model="form.employmentType" filterable
- clearable class="full-width" placeholder="请选择岗位性质">
- <el-option v-for="item in employmentOptions" :key="item.key || item.value" :label="item.label"
- :value="item.value" />
- </el-select></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="岗位层级"><el-select v-model="form.staffingLevel"
- class="full-width" :disabled="!staffingLevelOptions.length"
- :placeholder="form.positionId ? '请选择岗位层级' : '请先选择岗位'"
- @change="handleStaffingLevelChange">
- <el-option v-for="item in staffingLevelOptions" :key="item" :label="item"
- :value="item" /></el-select></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="关键工序"><el-input v-model.trim="form.keyProcess"
- maxlength="30" placeholder="标记特种工序岗位人员" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="是否启用">
- <div class="enable-field"><el-switch v-model="form.enabled" active-color="#18a875" /><span>{{
- form.enabled ? '启用' :
- '待岗' }}</span></div>
- </el-form-item></el-col>
- </el-row>
- <div class="subsection-title"><strong>联系方式</strong></div>
- <el-row :gutter="16">
- <el-col :xs="24" :sm="12"><el-form-item label="手机号" prop="phone"><el-input v-model.trim="form.phone"
- maxlength="11" placeholder="请输入 11 位手机号码"
- prefix-icon="el-icon-mobile-phone" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="邮箱"><el-input v-model.trim="form.email" maxlength="50"
- placeholder="name@company.com" prefix-icon="el-icon-message" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="微信号"><el-input v-model.trim="form.wechat" maxlength="30"
- placeholder="请输入微信号" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="紧急联系人"><el-input v-model.trim="form.emergencyContact"
- maxlength="20" placeholder="请输入联系人姓名" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="紧急联系人关系"><el-input v-model.trim="form.emergencyRelation"
- maxlength="20" placeholder="如配偶、父母、子女" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="紧急联系电话"><el-input v-model.trim="form.emergencyPhone"
- maxlength="11" placeholder="请输入联系电话" /></el-form-item></el-col>
- </el-row>
- </div>
- <div v-else-if="section.name === 'experience'" class="section-content">
- <header class="section-title">
- <div>
- <h3>教育与工作履历</h3>
- <p>最高教育经历和入职前工作经历</p>
- </div><span>{{ form.previousJobs.length }} 段履历</span>
- </header>
- <div class="subsection-title"><strong>教育经历</strong><el-button type="text" icon="el-icon-plus"
- @click="addEducation">添加教育</el-button></div>
- <el-alert v-if="form.educations.length > 1" type="warning" :closable="false" show-icon
- title="后端目前仅支持一条最高学历字段(educationBackground/school/major),多条教育经历保存时只会提交第一条。" style="margin-bottom: 10px" />
- <div v-if="form.educations.length" class="repeat-list">
- <div v-for="(item, index) in form.educations" :key="`edu-${index}`" class="repeat-block">
- <div class="repeat-block-heading"><strong>教育经历 {{ index + 1 }}</strong><el-button type="text"
- icon="el-icon-delete" @click="removeItem('educations', index)">删除</el-button></div>
- <el-row :gutter="14">
- <el-col :xs="12" :sm="6"><el-form-item label="最高学历"><el-select v-model="item.education"
- class="full-width"><el-option v-for="opt in educationOptions" :key="opt" :label="opt"
- :value="opt" /></el-select></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="学位"><el-select v-model="item.degree" clearable
- class="full-width" placeholder="请选择"><el-option v-for="opt in ['无', '学士', '硕士', '博士']" :key="opt"
- :label="opt" :value="opt" /></el-select></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="毕业日期"><el-date-picker v-model="item.graduationDate"
- value-format="yyyy-MM-dd" type="date" placeholder="请选择"
- class="full-width" /></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="毕业院校"><el-input v-model.trim="item.school"
- maxlength="40" placeholder="请输入毕业院校" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="所学专业"><el-input v-model.trim="item.major"
- maxlength="40" placeholder="请输入所学专业" /></el-form-item></el-col>
- </el-row>
- </div>
- </div>
- <div v-else class="inline-empty">暂无教育经历</div>
- <div class="subsection-title"><strong>工作履历</strong><el-button type="text" icon="el-icon-plus"
- @click="addPreviousJob">添加履历</el-button></div>
- <div v-if="form.previousJobs.length" class="repeat-list">
- <div v-for="(item, index) in form.previousJobs" :key="`job-${index}`" class="repeat-block">
- <div class="repeat-block-heading"><strong>工作经历 {{ index + 1 }}</strong><el-button type="text"
- icon="el-icon-delete" @click="removeItem('previousJobs', index)">删除</el-button></div>
- <el-row :gutter="14">
- <el-col :xs="12" :sm="4"><el-form-item label="序号"><el-input :value="index + 1"
- readonly /></el-form-item></el-col>
- <el-col :xs="12" :sm="5"><el-form-item label="工作开始日期" required><el-date-picker
- v-model="item.startDate" value-format="yyyy-MM-dd" type="date" placeholder="开始日期"
- class="full-width" /></el-form-item></el-col>
- <el-col :xs="12" :sm="5"><el-form-item label="工作结束日期" required><el-date-picker v-model="item.endDate"
- value-format="yyyy-MM-dd" type="date" placeholder="结束日期"
- class="full-width" /></el-form-item></el-col>
- <el-col :xs="24" :sm="5"><el-form-item label="前公司名称" required><el-input v-model.trim="item.company"
- maxlength="50" placeholder="请输入公司名称" /></el-form-item></el-col>
- <el-col :xs="24" :sm="5"><el-form-item label="前公司岗位" required><el-input v-model.trim="item.position"
- maxlength="30" /></el-form-item></el-col>
- <el-col :xs="24" :sm="6"><el-form-item label="前公司部门"><el-input v-model.trim="item.department"
- maxlength="30" /></el-form-item></el-col>
- <el-col :xs="24" :sm="6"><el-form-item label="离职原因"><el-input v-model.trim="item.leaveReason"
- maxlength="40" /></el-form-item></el-col>
- <el-col :span="24"><el-form-item label="工作描述"><el-input v-model.trim="item.description"
- type="textarea" :rows="2" maxlength="300" show-word-limit /></el-form-item></el-col>
- </el-row>
- </div>
- </div>
- <div v-else class="inline-empty">暂无入职前工作履历</div>
- </div>
- <div v-else-if="section.name === 'contract'" class="section-content">
- <header class="section-title">
- <div>
- <h3>合同与薪酬银行</h3>
- <p>劳动合同只读同步自合同管理;工资卡可在此维护</p>
- </div><span>{{ contractState }}</span>
- </header>
- <div class="subsection-title"><strong>劳动合同(只读)</strong></div>
- <el-row :gutter="16">
- <el-col :xs="24" :sm="12"><el-form-item label="分类"><el-input :value="form.contract.category" readonly
- placeholder="—" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="合同编号/名称"><el-input
- :value="form.contract.name || form.contract.number" readonly
- placeholder="—" /></el-form-item></el-col>
- <el-col :xs="24" :sm="8"><el-form-item label="签订时间"><el-input :value="form.contract.signDate" readonly
- placeholder="—" /></el-form-item></el-col>
- <el-col :xs="24" :sm="8"><el-form-item label="开始时间"><el-input :value="form.contract.startDate" readonly
- placeholder="—" /></el-form-item></el-col>
- <el-col :xs="24" :sm="8"><el-form-item label="结束时间"><el-input :value="form.contract.endDate" readonly
- placeholder="无固定期限/未同步" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="生效状态"><el-input :value="form.contract.status" readonly
- placeholder="—" /></el-form-item></el-col>
- <el-col :span="24"><el-form-item label="备注"><el-input :value="form.contract.remark" type="textarea"
- :rows="2" readonly placeholder="—" /></el-form-item></el-col>
- </el-row>
- <div class="subsection-title"><strong>薪酬银行</strong><el-button type="text" icon="el-icon-plus"
- @click="addBankAccount">添加银行</el-button></div>
- <div v-if="form.bankAccounts.length" class="repeat-list compact-list">
- <div v-for="(item, index) in form.bankAccounts" :key="`bank-${index}`" class="repeat-row bank-row">
- <el-input v-model.trim="item.bankName" maxlength="50" placeholder="开户行" />
- <el-input v-model.trim="item.bankAccount" maxlength="24" placeholder="银行卡号" />
- <el-input v-model.trim="item.bankAccountName" maxlength="20" placeholder="姓名(必填)" />
- <el-tooltip content="删除" placement="top"><el-button class="remove-button" icon="el-icon-delete"
- @click="removeItem('bankAccounts', index)"></el-button></el-tooltip>
- </div>
- </div>
- <div v-else class="inline-empty">暂无银行账户</div>
- </div>
- <div v-else-if="section.name === 'social'" class="section-content">
- <header class="section-title">
- <div>
- <h3>社保、资质与技能</h3>
- <p>社保公积金、工种、证照与技能列表</p>
- </div><span>{{ form.socialSecurityRecords.length }} 条社保记录</span>
- </header>
- <div class="subsection-title"><strong>社保公积金</strong><el-button type="text" icon="el-icon-plus"
- @click="addSocialSecurity">添加记录</el-button></div>
- <div v-if="form.socialSecurityRecords.length" class="repeat-list">
- <div v-for="(item, index) in form.socialSecurityRecords" :key="`ss-${index}`" class="repeat-block">
- <div class="repeat-block-heading"><strong>社保记录 {{ index + 1 }}</strong><el-button type="text"
- icon="el-icon-delete" @click="removeItem('socialSecurityRecords', index)">删除</el-button></div>
- <el-row :gutter="14">
- <el-col :xs="12" :sm="6"><el-form-item label="社保类型"><el-select v-model="item.type" clearable
- class="full-width"><el-option v-for="opt in socialSecurityOptions" :key="opt" :label="opt"
- :value="opt" /></el-select></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="基数"><el-input-number v-model="item.base" :min="0"
- :precision="2" :controls="false" class="full-width" /></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="参保日期"><el-date-picker v-model="item.insuredDate"
- value-format="yyyy-MM-dd" type="date" placeholder="请选择"
- class="full-width" /></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="停保日期"><el-date-picker v-model="item.stopDate"
- value-format="yyyy-MM-dd" type="date" placeholder="在职可为空"
- class="full-width" /></el-form-item></el-col>
- <el-col :xs="24" :sm="8"><el-form-item label="停缴记录"><el-select v-model="item.stopReason" clearable
- class="full-width" placeholder="请选择"><el-option v-for="opt in ['离职', '调转', '其他']" :key="opt"
- :label="opt" :value="opt" /></el-select></el-form-item></el-col>
- </el-row>
- </div>
- </div>
- <div v-else class="inline-empty">暂无社保记录</div>
- <div class="subsection-title"><strong>工种</strong><el-button type="text" icon="el-icon-plus"
- @click="addWorkType">添加工种</el-button></div>
- <div v-if="form.workTypes.length" class="repeat-list">
- <div v-for="(item, index) in form.workTypes" :key="`wt-${index}`" class="repeat-block">
- <div class="repeat-block-heading"><strong>工种 {{ index + 1 }}</strong><el-button type="text"
- icon="el-icon-delete" @click="removeItem('workTypes', index)">删除</el-button></div>
- <el-row :gutter="14">
- <el-col :xs="12" :sm="6"><el-form-item label="类型"><el-select v-model="item.type" clearable
- class="full-width"><el-option v-for="opt in workTypeCategoryOptions" :key="opt" :label="opt"
- :value="opt" /></el-select></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="编码"><el-input v-model.trim="item.code"
- maxlength="20" /></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="名称"><el-input v-model.trim="item.name"
- maxlength="30" /></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="等级"><el-select v-model="item.level" clearable
- class="full-width"><el-option v-for="opt in workTypeLevelOptions" :key="opt" :label="opt"
- :value="opt" /></el-select></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="标准工时费"><el-input-number v-model="item.hourlyWage"
- :min="0" :precision="2" :controls="false" class="full-width" /></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="工时费单位"><el-select v-model="item.wageUnit" clearable
- class="full-width"><el-option v-for="opt in ['元/小时', '元/天', '元/件']" :key="opt" :label="opt"
- :value="opt" /></el-select></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="健康状况"><el-select v-model="item.healthStatus" clearable
- class="full-width"><el-option v-for="opt in ['优', '良', '一般', '差']" :key="opt" :label="opt"
- :value="opt" /></el-select></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="是否特种作业人员"><el-select v-model="item.specialOperator"
- class="full-width"><el-option label="是" value="是" /><el-option label="否"
- value="否" /></el-select></el-form-item></el-col>
- <el-col :span="24"><el-form-item label="证件照">
- <fileMain v-model="item.photo" size="mini" />
- </el-form-item></el-col>
- </el-row>
- </div>
- </div>
- <div v-else class="inline-empty">暂无工种记录</div>
- <div class="subsection-title"><strong>证照列表</strong><el-button type="text" icon="el-icon-plus"
- @click="addCertificate">添加证照</el-button></div>
- <div v-if="form.certificates.length" class="repeat-list">
- <div v-for="(item, index) in form.certificates" :key="`cert-${index}`" class="repeat-block">
- <div class="repeat-block-heading"><strong>证照 {{ index + 1 }}</strong><el-button type="text"
- icon="el-icon-delete" @click="removeItem('certificates', index)">删除</el-button></div>
- <el-row :gutter="14">
- <el-col :xs="12" :sm="6"><el-form-item label="证照类型" required><el-select v-model="item.type"
- class="full-width"><el-option v-for="opt in certificateTypeOptions" :key="opt" :label="opt"
- :value="opt" /></el-select></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="证照编号" required><el-input v-model.trim="item.number"
- maxlength="30" /></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="等级"><el-select v-model="item.level" clearable
- class="full-width"><el-option v-for="opt in certificateLevelOptions" :key="opt" :label="opt"
- :value="opt" /></el-select></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="状态" required><el-select v-model="item.status"
- class="full-width"><el-option label="有效" value="有效" /><el-option label="失效"
- value="失效" /></el-select></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="有效期开始时间" required><el-date-picker
- v-model="item.startDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择"
- class="full-width" /></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="有效期结束时间" required><el-date-picker v-model="item.endDate"
- value-format="yyyy-MM-dd" type="date" placeholder="请选择"
- class="full-width" /></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="颁布日期"><el-date-picker v-model="item.issueDate"
- value-format="yyyy-MM-dd" type="date" placeholder="请选择"
- class="full-width" /></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="证照附件">
- <fileMain v-model="item.attachment" size="mini" />
- </el-form-item></el-col>
- <el-col :span="24"><el-form-item label="备注"><el-input v-model.trim="item.remark" maxlength="200"
- placeholder="补充说明信息" /></el-form-item></el-col>
- </el-row>
- </div>
- </div>
- <div v-else class="inline-empty">暂无证照记录</div>
- <div class="subsection-title"><strong>技能列表</strong><el-button type="text" icon="el-icon-plus"
- @click="addSkill">添加技能</el-button></div>
- <div v-if="form.skills.length" class="repeat-list">
- <div v-for="(item, index) in form.skills" :key="`skill-${index}`" class="repeat-block">
- <div class="repeat-block-heading"><strong>技能 {{ index + 1 }}</strong><el-button type="text"
- icon="el-icon-delete" @click="removeItem('skills', index)">删除</el-button></div>
- <el-row :gutter="14">
- <el-col :xs="12" :sm="6"><el-form-item label="分类"><el-select v-model="item.category" clearable
- class="full-width"><el-option v-for="opt in skillCategoryOptions" :key="opt" :label="opt"
- :value="opt" /></el-select></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="技能名称" required><el-input v-model.trim="item.name"
- maxlength="30" /></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="技能等级"><el-select v-model="item.level" clearable
- class="full-width"><el-option v-for="opt in skillLevelOptions" :key="opt" :label="opt"
- :value="opt" /></el-select></el-form-item></el-col>
- <el-col :xs="12" :sm="6"><el-form-item label="技能经验(年限)"><el-input-number v-model="item.years" :min="0"
- :precision="1" :controls="false" class="full-width" /></el-form-item></el-col>
- <el-col :xs="24" :sm="12"><el-form-item label="证照附件">
- <fileMain v-model="item.attachment" size="mini" />
- </el-form-item></el-col>
- <el-col :span="24"><el-form-item label="备注"><el-input v-model.trim="item.remark" type="textarea"
- :rows="2" maxlength="200" placeholder="培训经历、考核情况" /></el-form-item></el-col>
- </el-row>
- </div>
- </div>
- <div v-else class="inline-empty">暂无技能记录</div>
- <div class="subsection-title attachment-title"><strong>档案附件</strong>
- <fileMain v-model="form.files" size="mini" />
- </div>
- <el-form-item label="档案备注" class="remarks-item"><el-input v-model.trim="form.remarks" type="textarea"
- :rows="3" maxlength="500" show-word-limit placeholder="补充需要说明的员工档案信息" /></el-form-item>
- </div>
- <div v-else class="section-content record-section-content">
- <employee-archive-records ref="records" :archive="form" />
- </div>
- </el-tab-pane>
- </el-tabs>
- </el-form>
- <span slot="footer" class="dialog-footer">
- <span></span>
- <span>
- <el-button @click="closeDialog">取消</el-button>
- <el-button type="primary" :loading="saving" @click="submit">保存</el-button>
- </span>
- </span>
- </el-dialog>
- </template>
- <script>
- import EmployeeArchiveRecords from './EmployeeArchiveRecords.vue';
- import {
- getPositionPage,
- getPositionById,
- adaptPosition,
- getPositionSequenceList,
- getPositionSequencePage,
- getPositionSequenceLevels,
- } from '@/api/hr';
- import { listOrganizations, getFactoryAreaPage, getUserPage } from '@/api/organization';
- import { getDictByCode } from '@/api/sys';
- import { buildOrganizationTree, flattenOrganizationTree } from '@/utils/organization-tree';
- import { toDocIds } from '@/components/addDoc/util';
- const emptyContract = () => ({ category: '', name: '', signDate: '', startDate: '', endDate: '', files: [], status: '生效', remark: '' });
- const emptyForm = () => ({
- id: null, avatar: '', name: '', employeeNo: '', gender: '男', birthDate: '', age: null, idCard: '', idCardExpiry: '', ethnic: '汉族',
- maritalStatus: '未婚', politicalStatus: '群众', nativePlace: '', householdType: '', healthStatus: '优',
- company: '中盈产业集团有限公司', legalEntity: '', groupId: null, factory: '', factoryId: null, deptId: null, deptIds: [], deptName: '',
- position: '', positionId: null, positionIds: [], positionBindings: [],
- positionType: '', level: '', staffingLevel: '', dutyLevel: '', manager: '', managerId: null,
- employmentType: '', status: '全职', hireDate: '', regularDate: '', account: '', enabled: true,
- keyProcess: '',
- phone: '', email: '', wechat: '', workAddress: '', currentAddress: '', registeredAddress: '', emergencyContact: '', emergencyRelation: '', emergencyPhone: '', familyMembers: [],
- education: '本科', school: '',
- educations: [],
- previousJobs: [],
- contract: emptyContract(), bankAccounts: [],
- socialSecurityRecords: [],
- workTypes: [],
- certificates: [],
- skills: [],
- files: [], remarks: '',
- attendanceRecords: [], performanceRecords: [], trainingRecords: [], rewardPunishmentRecords: [], awards: [], positionTransfers: [], contractChanges: [],
- leaveInfo: { enabled: false, position: '', department: '', leaveDate: '', leaveType: '', leaveReason: '', serviceYears: '', handoverStatus: '' }
- });
- export default {
- name: 'EmployeeArchiveForm',
- components: { EmployeeArchiveRecords },
- props: {
- visible: Boolean,
- employee: { type: Object, default: null },
- organizations: { type: Array, default: () => [] },
- existingNumbers: { type: Array, default: () => [] }
- },
- data() {
- const validatePhone = (rule, value, callback) => {
- if (!value || /^1\d{10}$/.test(value) || /^1\d{2}\*{4}\d{4}$/.test(value)) callback();
- else callback(new Error('请输入正确的 11 位手机号码'));
- };
- const validateIdCard = (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- const text = String(value || '').trim();
- if (!text) return callback();
- // 脱敏展示(如 4301********1234)允许通过
- if (/^\d{6}\*{8}[\dXx]{4}$/.test(text)) return callback();
- if (!/^\d{17}[\dXx]$/.test(text)) {
- return callback(new Error('请输入正确的身份证号码'));
- }
- const birth = text.slice(6, 14);
- const y = Number(birth.slice(0, 4));
- const m = Number(birth.slice(4, 6));
- const d = Number(birth.slice(6, 8));
- const date = new Date(y, m - 1, d);
- if (
- date.getFullYear() !== y ||
- date.getMonth() !== m - 1 ||
- date.getDate() !== d ||
- y < 1900 ||
- date.getTime() > Date.now()
- ) {
- return callback(new Error('请输入正确的身份证号码'));
- }
- const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
- const codes = '10X98765432';
- let sum = 0;
- for (let i = 0; i < 17; i += 1) sum += Number(text[i]) * weights[i];
- if (codes[sum % 11] !== text[17].toUpperCase()) {
- return callback(new Error('请输入正确的身份证号码'));
- }
- callback();
- };
- const validateNumber = (rule, value, callback) => {
- const duplicate = this.existingNumbers.some((item) => item === value && item !== this.employee?.employeeNo);
- if (duplicate) callback(new Error('员工编号已存在'));
- else callback();
- };
- return {
- activeTab: 'base', saving: false, fullscreen: false, form: emptyForm(), organizationTree: [],
- sections: [
- { name: 'base', label: '基本信息', subtitle: '身份/任职/联系', icon: 'el-icon-user' },
- { name: 'experience', label: '教育与履历', subtitle: '学习与工作经历', icon: 'el-icon-notebook-2' },
- { name: 'contract', label: '合同与薪酬', subtitle: '合同与银行', icon: 'el-icon-document-checked' },
- { name: 'social', label: '社保与资质', subtitle: '证照与附件', icon: 'el-icon-medal' },
- { name: 'records', label: '业务记录', subtitle: '考勤绩效与异动', icon: 'el-icon-tickets' }
- ],
- // 岗位设定列表(岗位下拉数据源)
- positionOptions: [],
- // HR 岗位分页真实主键集合(主数据 postId 如 1,4 不在此集合,禁止对其调 getById)
- hrPositionIdSet: {},
- // 岗位序列 → 层级主键,用于提交 positionLevelId / positionSequenceId
- sequenceLevelMap: {},
- // 强制重建隶属部门级联,避免 options 清空后内部 activePath 残留导致 node.level 报错
- deptCascaderKey: 0,
- // 岗位单选回显:用 key 重建,避免 options 后到时只显示 ID
- positionSelectKey: 0,
- positionLoading: false,
- // 直属上级:按隶属部门联动人员
- managerOptions: [],
- managerLoading: false,
- ethnicOptions: ['汉族', '土家族', '苗族', '侗族', '回族', '瑶族', '其他'],
- genderOptions: ['男', '女', '未知'],
- statusOptions: ['全职', '兼职', '实习', '正式', '试用', '劳务', '离职'],
- maritalOptions: ['未婚', '已婚', '离异', '丧偶'],
- educationOptions: ['小学', '初中', '高中', '大专', '本科', '硕士', '博士'],
- employmentOptions: [],
- staffingLevelOptions: [],
- factoryOptions: [],
- factoryLoading: false,
- socialSecurityOptions: ['四险', '四险一金', '五险', '五险一金', '六险一金'],
- contractTypeOptions: ['固定期限', '无固定期限', '以完成一定工作任务为期限', '劳务协议', '实习协议'],
- workTypeCategoryOptions: ['维修', '装配', '检验', '机加', '焊接', '其他'],
- workTypeLevelOptions: ['初级', '中级', '高级', '技师', '高级技师'],
- certificateTypeOptions: ['身份证', '护照', '特种作业证', '职称证', '职业资格证', '其他'],
- certificateLevelOptions: ['初级', '中级', '高级'],
- skillCategoryOptions: ['专业技能', '办公技能', '语言技能', '特种操作技能', '其他'],
- skillLevelOptions: ['了解', '熟练', '精通', '初级', '中级', '高级'],
- organizationProps: { value: 'id', label: 'name', children: 'children', emitPath: false, checkStrictly: true },
- rules: {
- name: [{ required: true, message: '请输入员工姓名', trigger: 'blur' }],
- employeeNo: [{ required: true, message: '请输入工号', trigger: 'blur' }, { pattern: /^[A-Za-z0-9_-]{2,20}$/, message: '仅支持字母、数字、下划线和短横线', trigger: 'blur' }, { validator: validateNumber, trigger: 'blur' }],
- gender: [{
- validator: (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- if (!value) callback(new Error('请选择性别'));
- else callback();
- },
- trigger: 'change',
- }],
- groupId: [{
- required: true,
- validator: (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- if (value == null || value === '') callback(new Error('请选择所属机构'));
- else callback();
- },
- trigger: 'change',
- }],
- status: [{
- validator: (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- if (!value) callback(new Error('请选择员工状态'));
- else callback();
- },
- trigger: 'change',
- }],
- hireDate: [{
- required: true,
- validator: (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- if (!value) callback(new Error('请选择入职时间'));
- else callback();
- },
- trigger: 'change',
- }],
- positionId: [{
- required: true,
- validator: (rule, value, callback) => {
- if (this._skipFormValidate) return callback();
- if (value == null || value === '') {
- callback(new Error('请选择岗位'));
- return;
- }
- callback();
- },
- trigger: 'change',
- }],
- phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }, { validator: validatePhone, trigger: 'blur' }],
- email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }],
- idCard: [{ validator: validateIdCard, trigger: ['blur', 'change'] }],
- }
- };
- },
- computed: {
- dialogVisible: {
- get() { return this.visible; },
- set(value) {
- if (!value) this.saving = false;
- this.$emit('update:visible', value);
- }
- },
- isEdit() { return Boolean(this.employee?.id); },
- nameInitial() { return (this.form.name || '新').slice(-1); },
- avatarStyle() { const tones = ['#1768e5', '#13a471', '#7c5ce7', '#d08716', '#d25567']; const code = [...(this.form.name || '新')].reduce((sum, char) => sum + char.charCodeAt(0), 0); return { '--avatar-color': tones[code % tones.length] }; },
- currentSectionLabel() { return this.sections.find((item) => item.name === this.activeTab)?.label || ''; },
- // 所属机构:法人组织(集团/公司),来源真实机构接口 getGroupList
- legalEntities() {
- const tree = this.organizationTree.length ? this.organizationTree : this.organizations;
- const isLegalEntity = (item) => ['10', '20', 'group', 'company'].includes(String(item.type));
- const list = flattenOrganizationTree(tree || [])
- .filter(isLegalEntity)
- .map((item) => ({ ...item, id: String(item.id) }));
- // 当前已选机构若不在过滤结果中(类型异常),补进选项以免下拉只显示 ID
- const currentId = this.form.groupId != null && this.form.groupId !== '' ? String(this.form.groupId) : '';
- if (currentId && !list.some((item) => String(item.id) === currentId)) {
- const flat = flattenOrganizationTree(tree || []);
- const hit = flat.find((item) => String(item.id) === currentId);
- if (hit) {
- list.unshift({ ...hit, id: String(hit.id) });
- } else if (this.form.legalEntity || this.form.company) {
- list.unshift({
- id: currentId,
- name: this.form.legalEntity || this.form.company,
- type: '20'
- });
- }
- }
- return list;
- },
- // 隶属部门:先选机构、工厂后再展示。优先工厂节点下的部门,否则回退到机构下部门
- deptOptions() {
- const tree = this.organizationTree.length ? this.organizationTree : this.organizations;
- if (!this.form.groupId || !this.form.factoryId) return [];
- const find = (nodes, id) => {
- for (const node of nodes || []) {
- if (!node) continue;
- if (String(node.id) === String(id)) return node;
- const matched = find(node.children || [], id);
- if (matched) return matched;
- }
- return null;
- };
- const factoryNode = find(tree || [], this.form.factoryId);
- const orgNode = find(tree || [], this.form.groupId);
- const source = factoryNode?.children?.length
- ? factoryNode.children
- : (orgNode?.children || []);
- const sanitize = (nodes) =>
- (nodes || [])
- .filter(Boolean)
- .map((node) => {
- const children = sanitize(node.children || []);
- const next = {
- ...node,
- id: node.id != null ? String(node.id) : node.id,
- name: node.name || '',
- };
- if (children.length) next.children = children;
- else delete next.children;
- return next;
- });
- return sanitize(source);
- },
- deptCascaderProps() {
- return { ...this.organizationProps, multiple: true };
- },
- hasDept() {
- return this.normalizeDeptIds(this.form.deptIds).length > 0;
- },
- requiredProgress() {
- const keys = ['name', 'employeeNo', 'gender', 'groupId', 'status', 'hireDate', 'positionId', 'phone'];
- return { base: `${keys.filter((key) => { const value = this.form[key]; return Array.isArray(value) ? value.length : Boolean(value); }).length}/${keys.length}` };
- },
- sectionProgress() {
- const count = (keys) => `${keys.filter((key) => { const value = this.form[key]; return Array.isArray(value) ? value.length : Boolean(value); }).length}/${keys.length}`;
- return {
- base: count(['name', 'employeeNo', 'groupId', 'factory', 'deptIds', 'status', 'hireDate', 'positionId', 'employmentType', 'staffingLevel', 'gender', 'birthDate', 'idCard', 'ethnic', 'politicalStatus', 'maritalStatus', 'nativePlace', 'education', 'school', 'phone', 'email', 'wechat', 'currentAddress', 'registeredAddress', 'workAddress', 'emergencyContact', 'emergencyPhone', 'keyProcess']),
- experience: count(['educations', 'previousJobs']),
- contract: `${[this.form.contract.number, this.form.contract.type, this.form.bankName, this.form.bankAccount].filter(Boolean).length}/4`,
- social: count(['socialSecurityRecords', 'workTypes', 'certificates', 'skills', 'files']),
- records: `${['attendanceRecords', 'performanceRecords', 'trainingRecords', 'rewardPunishmentRecords', 'awards', 'positionTransfers', 'contractChanges'].reduce((sum, key) => sum + this.form[key].length, 0) + (this.form.leaveInfo.enabled ? 1 : 0)}`
- };
- },
- completionRate() {
- const values = [this.form.name, this.form.employeeNo, this.form.groupId, this.form.positionId, this.form.status, this.form.hireDate, this.form.phone, this.form.gender, this.form.email, this.form.birthDate, this.form.education, this.form.school, this.form.currentAddress, this.form.contract.number, this.form.bankAccount, this.form.insuredDate, this.form.skills.length, this.form.files.length];
- return Math.round(values.filter(Boolean).length / values.length * 100);
- },
- contractState() { return this.form.contract.name ? (this.form.contract.endDate ? '合同资料已填写' : '待补结束日期') : '待录入合同'; }
- },
- watch: { visible(value) { if (value) this.loadForm(); } },
- methods: {
- loadForm() {
- this._skipFormValidate = true;
- this.activeTab = 'base';
- const source = this.employee ? JSON.parse(JSON.stringify(this.employee)) : {};
- const empty = emptyForm();
- const statusMap = { active: '全职', probation: '试用', inactive: '离职', disabled: '离职' };
- this.form = {
- ...empty, ...source,
- groupId: source.groupId != null && source.groupId !== ''
- ? String(source.groupId)
- : (source.legalEntityId != null && source.legalEntityId !== '' ? String(source.legalEntityId) : null),
- legalEntity: source.legalEntity || source.company || '',
- company: source.company || source.legalEntity || '',
- status: source.formStatus || statusMap[source.status] || source.status || '全职',
- hireDate: source.hireDate || source.joinDate || '',
- factoryId: source.factoryId != null && source.factoryId !== '' ? String(source.factoryId) : null,
- factory: source.factory || source.factoryName || '',
- deptIds: this.normalizeDeptIds(source.deptIds?.length ? source.deptIds : source.deptId),
- deptName: source.deptName || '',
- managerId: source.managerId != null && source.managerId !== '' ? String(source.managerId) : null,
- manager: source.manager || '',
- positionIds: this.normalizePositionIds(source).slice(0, 1),
- positionBindings: Array.isArray(source.positionBindings)
- ? source.positionBindings.slice(0, 1)
- : [],
- positionId: this.normalizePositionIds(source)[0] || null,
- position: source.position || source.postName || '',
- contract: { ...emptyContract(), ...(source.contract || {}) },
- leaveInfo: { ...empty.leaveInfo, ...(source.leaveInfo || {}) },
- familyMembers: source.familyMembers || [], files: toDocIds(source.files),
- // 教育经历:兼容旧的单条 education/degree/school 字段
- educations: source.educations?.length
- ? source.educations
- : (source.education
- ? [{ education: source.education || '本科', degree: source.degree || '', graduationDate: source.graduationDate || '', school: source.school || '', major: source.major || '' }]
- : []),
- // 薪酬银行:兼容旧的单条 bankName/bankAccount 字段
- bankAccounts: source.bankAccounts?.length
- ? source.bankAccounts
- : (source.bankName || source.bankAccount
- ? [{ bankName: source.bankName || '', bankAccount: source.bankAccount || '', bankAccountName: source.bankAccountName || '' }]
- : []),
- // 工作履历:兼容旧的 dateRange
- previousJobs: (source.previousJobs || []).map((item) => ({
- ...item,
- startDate: item.startDate || item.dateRange?.[0] || '',
- endDate: item.endDate || item.dateRange?.[1] || '',
- })),
- // 技能:兼容旧字符串数组
- skills: (source.skills || []).map((item) => (typeof item === 'string' ? { name: item, category: '', level: '', years: 0, attachment: [], remark: '' } : { ...item, attachment: toDocIds(item.attachment) })),
- certificates: (source.certificates || []).map((item) => ({ ...item, attachment: toDocIds(item.attachment) })),
- socialSecurityRecords: source.socialSecurityRecords || [],
- workTypes: (source.workTypes || []).map((item) => ({ ...item, photo: toDocIds(item.photo) })),
- attendanceRecords: source.attendanceRecords || [], performanceRecords: source.performanceRecords || [],
- trainingRecords: (source.trainingRecords || []).map((item) => ({ ...item, attachment: toDocIds(item.attachment) })),
- rewardPunishmentRecords: (source.rewardPunishmentRecords || []).map((item) => ({ ...item, attachment: toDocIds(item.attachment) })), awards: source.awards || [], positionTransfers: source.positionTransfers || [], contractChanges: source.contractChanges || []
- };
- this.loadEmploymentTypeOptions();
- this.loadSequences()
- .then(() => this.loadPositions())
- .then(() => {
- this.remapLegacyPositionIds();
- this.mergeBoundPositionOptions();
- this.syncStaffingLevelsFromPosition();
- });
- this.loadOrganizations().then(() => {
- this.normalizeDeptAgainstOptions();
- this.loadFactories().then(() => this.syncFactoryDisplay());
- this.loadManagerOptions({ keepSelection: true });
- });
- this.syncAge();
- if (!this.employee) this.generateEmployeeNo();
- this.$nextTick(() => {
- this.$refs.form?.clearValidate();
- this.$nextTick(() => {
- this._skipFormValidate = false;
- });
- });
- },
- normalizePositionIds(source = {}) {
- let ids = [];
- if (Array.isArray(source.positionBindings) && source.positionBindings.length) {
- // 主岗优先,避免回显顺序错乱
- ids = [...source.positionBindings]
- .sort((a, b) => Number(b.primaryFlag || 0) - Number(a.primaryFlag || 0))
- .map((item) => (item.positionId != null ? String(item.positionId) : ''))
- .filter(Boolean);
- } else if (Array.isArray(source.positionIds) && source.positionIds.length) {
- ids = source.positionIds.map(String);
- } else if (source.positionId != null && source.positionId !== '') {
- ids = [String(source.positionId)];
- } else if (source.postId != null && source.postId !== '') {
- ids = [String(source.postId)];
- }
- // 兼容 "id1,id2" 整串
- return Array.from(
- new Set(
- ids
- .flatMap((id) => String(id).split(/[,,]/))
- .map((item) => item.trim())
- .filter(Boolean),
- ),
- );
- },
- positionFallbackNames() {
- return String(this.form.position || '')
- .split(/[,,、]/)
- .map((item) => item.trim())
- .filter(Boolean);
- },
- positionOptionLabel(item) {
- return item.positionName || item.name || (item.id ? `岗位 ${item.id}` : '未命名岗位');
- },
- dedupePositionOptions(list = []) {
- const map = new Map();
- list.forEach((item) => {
- if (item == null || item.id == null || item.id === '') return;
- const id = String(item.id);
- if (!map.has(id)) {
- map.set(id, { ...item, id });
- return;
- }
- // 后写入的若带名称则补全
- const prev = map.get(id);
- map.set(id, {
- ...prev,
- ...item,
- id,
- positionName: item.positionName || item.name || prev.positionName || prev.name,
- name: item.name || item.positionName || prev.name || prev.positionName
- });
- });
- return Array.from(map.values());
- },
- mergeBoundPositionOptions() {
- const fallbackNames = this.positionFallbackNames();
- const bindings = this.form.positionBindings || [];
- const selectedIds = (this.form.positionIds || []).map(String).filter(Boolean);
- const extras = [];
- const pushExtra = (positionId, positionName, binding = {}) => {
- if (!positionId) return;
- extras.push({
- id: String(positionId),
- positionName: positionName || binding.positionName || '',
- name: positionName || binding.positionName || '',
- positionSequence: binding.positionSequenceName || '',
- positionSequenceName: binding.positionSequenceName || '',
- positionSequenceId: binding.positionSequenceId || '',
- level: binding.positionLevelName || '',
- positionLevel: binding.positionLevelName || '',
- positionLevelId: binding.positionLevelId || '',
- sequenceId: binding.positionSequenceId || '',
- });
- };
- selectedIds.forEach((id, index) => {
- const binding = bindings.find((item) => String(item.positionId) === id) || {};
- const name =
- binding.positionName ||
- fallbackNames[index] ||
- '';
- pushExtra(id, name, binding);
- });
- bindings.forEach((item, index) => {
- if (item.positionId == null || item.positionId === '') return;
- if (selectedIds.includes(String(item.positionId))) return;
- pushExtra(
- item.positionId,
- item.positionName || fallbackNames[index] || '',
- item,
- );
- });
- // 已选岗位放到前面,保证 el-select 标签优先匹配到名称
- this.positionOptions = this.dedupePositionOptions([
- ...extras,
- ...(this.positionOptions || []),
- ]);
- this.syncPositionDisplay();
- this.refreshPositionSelectEcho();
- },
- /**
- * 主数据短 postId(如 1,4)按名称映射;长整型视为 HR 岗位主键,缺失时保留并补名称,禁止误删导致回显成裸 ID
- */
- remapLegacyPositionIds() {
- const ids = (this.form.positionIds || []).map(String).filter(Boolean);
- if (!ids.length) return;
- const fallbackNames = this.positionFallbackNames();
- const hrOptions = (this.positionOptions || []).filter(
- (item) => item && this.hrPositionIdSet[String(item.id)],
- );
- const findHrByName = (name) => {
- const text = String(name || '').trim();
- if (!text) return null;
- return (
- hrOptions.find((item) => {
- const label = String(item.positionName || item.name || '').trim();
- return label === text;
- }) || null
- );
- };
- const isLikelyHrPositionId = (id) => /^\d{15,}$/.test(String(id).trim());
- const nextIds = [];
- const nextBindings = [];
- let changed = false;
- ids.forEach((key, index) => {
- const binding =
- (this.form.positionBindings || []).find((item) => String(item.positionId) === key) || {};
- const opt = this.positionOptions.find((item) => String(item.id) === key) || {};
- let name =
- binding.positionName ||
- opt.positionName ||
- opt.name ||
- fallbackNames[index] ||
- '';
- if (this.hrPositionIdSet[key] || isLikelyHrPositionId(key)) {
- if (!name && fallbackNames[index]) {
- name = fallbackNames[index];
- changed = true;
- }
- nextIds.push(key);
- nextBindings.push({
- ...binding,
- positionId: key,
- positionName: name || binding.positionName || '',
- primaryFlag: nextIds.length === 1 ? 1 : 0,
- });
- return;
- }
- const matched = findHrByName(name);
- if (matched) {
- const realId = String(matched.id);
- if (!nextIds.includes(realId)) {
- nextIds.push(realId);
- nextBindings.push({
- ...binding,
- positionId: realId,
- positionName: matched.positionName || matched.name || name,
- primaryFlag: nextIds.length === 1 ? 1 : 0,
- });
- }
- changed = true;
- return;
- }
- // 仅丢弃无法识别的短旧 postId
- changed = true;
- });
- if (!changed && nextIds.join(',') === ids.join(',')) {
- // 名称可能仍需回填
- const needNameFill = nextBindings.some((item, index) => !item.positionName && fallbackNames[index]);
- if (!needNameFill) return;
- }
- this.form.positionIds = nextIds.slice(0, 1);
- this.form.positionBindings = nextBindings.slice(0, 1).map((item) => ({
- ...item,
- positionName: item.positionName || fallbackNames[0] || '',
- primaryFlag: 1,
- }));
- this.form.positionId = this.form.positionIds[0] || null;
- this.form.position =
- this.form.positionBindings[0]?.positionName || this.form.position;
- this.syncPositionDisplay();
- },
- /** Element 多选在 options 异步到达后需重建一次,否则标签可能只剩裸 ID */
- refreshPositionSelectEcho() {
- const id = this.form.positionId != null && this.form.positionId !== ''
- ? String(this.form.positionId)
- : (this.form.positionIds || []).map(String).filter(Boolean)[0] || null;
- this.form.positionId = id;
- this.form.positionIds = id ? [id] : [];
- this.positionSelectKey += 1;
- this.$nextTick(() => {
- this.syncPositionDisplay();
- this.$refs.form?.clearValidate('positionId');
- });
- },
- syncPositionDisplay() {
- const fallbackNames = this.positionFallbackNames();
- const key = this.form.positionId != null && this.form.positionId !== ''
- ? String(this.form.positionId)
- : '';
- const selected = key
- ? (() => {
- const opt = this.positionOptions.find((item) => String(item.id) === key) || {};
- const binding =
- (this.form.positionBindings || []).find((item) => String(item.positionId) === key) || {};
- const meta = this.resolvePositionLevelMeta(opt, {
- ...binding,
- positionLevelName: this.form.staffingLevel || binding.positionLevelName,
- });
- return [{
- positionId: key,
- positionName:
- opt.positionName ||
- opt.name ||
- binding.positionName ||
- fallbackNames[0] ||
- '',
- positionLevelId: meta.positionLevelId,
- positionLevelName: meta.positionLevelName,
- positionSequenceId: meta.positionSequenceId,
- positionSequenceName: meta.positionSequenceName,
- primaryFlag: 1,
- }];
- })()
- : [];
- this.form.positionBindings = selected;
- this.form.positionIds = key ? [key] : [];
- this.form.position = selected[0]?.positionName || '';
- this.form.positionId = selected[0]?.positionId || null;
- this.form.level = selected[0]?.positionLevelName || this.form.level;
- if (selected[0]?.positionLevelName) {
- this.form.staffingLevel = selected[0].positionLevelName;
- }
- },
- isNumericId(value) {
- return value != null && value !== '' && /^-?\d+$/.test(String(value).trim());
- },
- findSequenceMeta(sequenceId = '', sequenceName = '') {
- const map = this.sequenceLevelMap || {};
- const name = String(sequenceName || '').trim();
- if (name && map[name]) return map[name];
- const sid = this.isNumericId(sequenceId) ? String(sequenceId).trim() : '';
- if (sid && map[`id:${sid}`]) return map[`id:${sid}`];
- if (sid) {
- return Object.values(map).find((item) => String(item.sequenceId) === sid) || null;
- }
- if (name) {
- return (
- Object.values(map).find(
- (item) => String(item.sequenceName || '').trim() === name,
- ) || null
- );
- }
- return null;
- },
- /**
- * 人员岗位关联必填:positionLevelId / positionSequenceId
- * 岗位分页通常只有序列名与层级文案,需用岗位序列接口映射主键
- */
- resolvePositionLevelMeta(opt = {}, binding = {}) {
- let positionSequenceId = binding.positionSequenceId || opt.sequenceId || opt.positionSequenceId || '';
- let positionSequenceName = String(
- binding.positionSequenceName || opt.positionSequence || opt.positionSequenceName || '',
- ).trim();
- let positionLevelId = binding.positionLevelId || opt.positionLevelId || '';
- let positionLevelName =
- binding.positionLevelName || opt.positionLevel || opt.level || '';
- const levelNames = [];
- const pushName = (value) => {
- const text = String(value || '').trim();
- if (text && !levelNames.includes(text)) levelNames.push(text);
- };
- (opt.positionLevels || []).forEach(pushName);
- pushName(positionLevelName);
- pushName(opt.positionLevel);
- pushName(opt.level);
- const seqMeta = this.findSequenceMeta(positionSequenceId, positionSequenceName);
- if (seqMeta) {
- if (!positionSequenceName) positionSequenceName = seqMeta.sequenceName || '';
- if (!this.isNumericId(positionSequenceId) && this.isNumericId(seqMeta.sequenceId)) {
- positionSequenceId = String(seqMeta.sequenceId).trim();
- }
- }
- const levels = seqMeta?.levels || [];
- if (!this.isNumericId(positionLevelId) && levels.length) {
- const matched =
- levels.find((item) =>
- levelNames.some(
- (name) =>
- name === item.label ||
- name === item.value ||
- String(item.id) === name,
- ),
- ) || levels[0];
- if (matched && this.isNumericId(matched.id)) {
- positionLevelId = String(matched.id).trim();
- positionLevelName = matched.label || matched.value || positionLevelName;
- }
- } else if (this.isNumericId(positionLevelId) && !positionLevelName && levels.length) {
- const matched = levels.find((item) => String(item.id) === String(positionLevelId));
- if (matched) positionLevelName = matched.label || matched.value || '';
- }
- return {
- positionSequenceId: this.isNumericId(positionSequenceId) ? String(positionSequenceId).trim() : '',
- positionSequenceName: positionSequenceName || '',
- positionLevelId: this.isNumericId(positionLevelId) ? String(positionLevelId).trim() : '',
- positionLevelName: positionLevelName || '',
- };
- },
- /** 写入 sequenceLevelMap:按序列名 + id:主键 双索引 */
- putSequenceMeta(map, item = {}) {
- const name = String(item.sequence || item.label || item.name || '').trim();
- const sequenceId = this.isNumericId(item.id) ? String(item.id).trim() : '';
- if (!name && !sequenceId) return;
- const levels = (item.levelList || item.children || [])
- .map((level) => {
- const id = this.isNumericId(level.id) ? String(level.id).trim() : '';
- const label = level.label || level.value || level.level || level.positionLevel || '';
- return { id, label, value: label };
- })
- .filter((level) => level.id);
- const entry = { sequenceId, sequenceName: name, levels };
- if (name) {
- const prev = map[name];
- map[name] = {
- ...(prev || {}),
- ...entry,
- levels: levels.length ? levels : (prev?.levels || []),
- sequenceId: sequenceId || prev?.sequenceId || '',
- };
- }
- if (sequenceId) map[`id:${sequenceId}`] = entry;
- },
- async loadSequences() {
- const map = {};
- try {
- // 与岗位档案编辑一致:page 接口 levelList 带层级主键
- const page = await getPositionSequencePage({ pageNum: 1, size: 1000, status: '1' });
- (page?.list || []).forEach((item) => this.putSequenceMeta(map, item));
- } catch (error) {
- // page 失败时再走列表接口
- }
- if (!Object.keys(map).length) {
- try {
- const list = await getPositionSequenceList();
- (Array.isArray(list) ? list : []).forEach((item) => this.putSequenceMeta(map, item));
- } catch (error) {
- // ignore
- }
- }
- this.sequenceLevelMap = map;
- },
- enrichSelectedPositions() {
- this.remapLegacyPositionIds();
- this.mergeBoundPositionOptions();
- },
- /** 保存前确保每岗都有序列/层级主键:本地映射 → 缺则拉 getById 再映射 */
- async ensurePositionMetaForSave() {
- this.enrichSelectedPositions();
- const missing = (this.form.positionBindings || []).filter(
- (item) => !item.positionLevelId || !item.positionSequenceId,
- );
- if (!missing.length) return true;
- await Promise.all(
- missing.map(async (item) => {
- const key = String(item.positionId || '');
- if (!key) return;
- try {
- const detail = await Promise.race([
- getPositionById(key),
- new Promise((_, reject) =>
- setTimeout(() => reject(new Error('岗位详情超时')), 8000),
- ),
- ]);
- if (!detail) return;
- // 若序列层级表里还没有该序列,按序列名补拉层级
- const seqName = detail.positionSequence || '';
- if (seqName && !this.findSequenceMeta('', seqName)?.levels?.length) {
- try {
- const levels = await getPositionSequenceLevels(seqName);
- const seqMeta = this.findSequenceMeta('', seqName);
- const sequenceId = seqMeta?.sequenceId || '';
- const mapped = (Array.isArray(levels) ? levels : []).map((level) => ({
- id: level.id != null ? String(level.id) : '',
- label: level.level || level.levelName || level.label || '',
- value: level.level || level.levelName || level.value || '',
- })).filter((level) => this.isNumericId(level.id));
- if (seqName) {
- this.$set(this.sequenceLevelMap, seqName, {
- sequenceId,
- sequenceName: seqName,
- levels: mapped,
- });
- }
- } catch (error) {
- // ignore
- }
- }
- const merged = {
- id: key,
- positionName: detail.positionName || detail.name || item.positionName || '',
- name: detail.name || detail.positionName || item.positionName || '',
- positionSequence: detail.positionSequence || '',
- positionSequenceName: detail.positionSequence || '',
- sequenceId: detail.sequenceId != null ? String(detail.sequenceId) : '',
- positionSequenceId: detail.sequenceId != null ? String(detail.sequenceId) : '',
- positionLevel: detail.positionLevel || '',
- positionLevels: detail.positionLevels || [],
- };
- // 用序列名反查序列主键
- if (!merged.sequenceId && merged.positionSequence) {
- const seqMeta = this.findSequenceMeta('', merged.positionSequence);
- if (seqMeta?.sequenceId) {
- merged.sequenceId = seqMeta.sequenceId;
- merged.positionSequenceId = seqMeta.sequenceId;
- }
- }
- this.positionOptions = this.dedupePositionOptions([
- merged,
- ...(this.positionOptions || []),
- ]);
- } catch (error) {
- // 单岗失败不阻断其余岗
- }
- }),
- );
- this.syncPositionDisplay();
- const stillMissing = (this.form.positionBindings || []).find(
- (item) => !item.positionLevelId || !item.positionSequenceId,
- );
- if (stillMissing) {
- this.activeTab = 'base';
- this.$message.warning(
- `岗位「${stillMissing.positionName || stillMissing.positionId}」缺少岗位序列或层级配置,请在岗位档案中维护后再保存`,
- );
- return false;
- }
- return true;
- },
- /** 强制关闭:复位 loading,并清掉可能残留的 Element 遮罩 */
- closeDialog() {
- this.saving = false;
- this.dialogVisible = false;
- this.clearOrphanOverlays();
- },
- handleBeforeClose(done) {
- this.saving = false;
- if (typeof done === 'function') done();
- this.$nextTick(() => this.clearOrphanOverlays());
- },
- clearOrphanOverlays() {
- this.$nextTick(() => {
- try {
- const openDialog = Array.from(document.querySelectorAll('.el-dialog__wrapper')).some((el) => {
- if (!el || el.style.display === 'none') return false;
- const style = window.getComputedStyle(el);
- return style.display !== 'none' && style.visibility !== 'hidden';
- });
- if (openDialog) return;
- document.querySelectorAll('body > .v-modal').forEach((el) => el.remove());
- document.body.classList.remove('el-popup-parent--hidden');
- if (document.body.style.overflow === 'hidden') {
- document.body.style.removeProperty('overflow');
- }
- } catch (error) {
- // ignore
- }
- });
- },
- toggleFullscreen() {
- this.fullscreen = !this.fullscreen;
- },
- resetForm() {
- this._skipFormValidate = true;
- this.saving = false;
- this.fullscreen = false;
- // 先抬高 key,避免空 options 触发 Cascader 内部 null.level
- this.deptCascaderKey += 1;
- this.positionSelectKey += 1;
- this.activeTab = 'base';
- this.managerOptions = [];
- this.managerLoading = false;
- this.factoryOptions = [];
- this.factoryLoading = false;
- this.staffingLevelOptions = [];
- this.positionOptions = [];
- this._managerRequestKey = '';
- this.$nextTick(() => {
- this.form = emptyForm();
- this.$refs.form?.clearValidate();
- this.clearOrphanOverlays();
- });
- },
- generateEmployeeNo() { const maxNo = this.existingNumbers.reduce((max, item) => Math.max(max, Number(String(item).match(/\d+/)?.[0] || 0)), 0); this.form.employeeNo = `ZY${String(maxNo + 1).padStart(4, '0')}`; this.$nextTick(() => this.$refs.form?.clearValidate('employeeNo')); },
- normalizeDeptIds(value) {
- const list = Array.isArray(value) ? value : value != null && value !== '' ? [value] : [];
- return Array.from(
- new Set(
- list
- .flat(2)
- .map((id) => String(id == null ? '' : id).trim())
- .filter((id) => id && id !== 'null' && id !== 'undefined'),
- ),
- );
- },
- findDeptNode(id, nodes = this.deptOptions) {
- for (const node of nodes || []) {
- if (!node) continue;
- if (String(node.id) === String(id)) return node;
- const matched = this.findDeptNode(id, node.children || []);
- if (matched) return matched;
- }
- return null;
- },
- applyDeptIds(ids, { resetDependents = false } = {}) {
- const list = this.normalizeDeptIds(ids).filter(
- (id) => String(id) !== String(this.form.groupId || ''),
- );
- this.form.deptIds = list;
- this.form.deptId = list[0] || null;
- this.form.deptName = list
- .map((id) => this.findDeptNode(id)?.name)
- .filter(Boolean)
- .join('、');
- if (resetDependents) {
- this.loadManagerOptions({ keepSelection: false });
- this.resetPositionsByDepartment();
- }
- },
- handleOrganizationChange(ids) {
- this.applyDeptIds(ids, { resetDependents: true });
- },
- handleLegalEntityChange(id) {
- const selected = this.legalEntities.find((item) => String(item.id) === String(id));
- this.form.legalEntity = selected?.name || '';
- this.form.company = selected?.name || this.form.company;
- this.deptCascaderKey += 1;
- this.form.factoryId = null;
- this.form.factory = '';
- this.factoryOptions = [];
- this.applyDeptIds([]);
- this.form.managerId = null;
- this.form.manager = '';
- this.managerOptions = [];
- this.resetPositionsByDepartment();
- this.loadFactories();
- },
- normalizeDeptAgainstOptions() {
- const ids = this.normalizeDeptIds(
- this.form.deptIds?.length ? this.form.deptIds : this.form.deptId,
- ).filter((id) => String(id) !== String(this.form.groupId || ''));
- const valid = ids.filter((id) => this.findDeptNode(id));
- this.applyDeptIds(valid.length ? valid : ids);
- },
- handleManagerChange(id) {
- const matched = this.managerOptions.find((item) => String(item.id) === String(id));
- this.form.managerId = id != null && id !== '' ? String(id) : null;
- this.form.manager = matched?.name || '';
- },
- mergeCurrentManagerOption() {
- const id = this.form.managerId;
- if (id == null || id === '') return;
- const exists = this.managerOptions.some((item) => String(item.id) === String(id));
- if (exists) return;
- this.managerOptions.unshift({
- id: String(id),
- name: this.form.manager || '未命名人员'
- });
- },
- async loadManagerOptions({ keepSelection = false } = {}) {
- const deptId = this.form.deptId;
- if (!keepSelection) {
- this.form.managerId = null;
- this.form.manager = '';
- }
- if (!deptId) {
- this.managerOptions = [];
- return;
- }
- const requestKey = String(deptId);
- this._managerRequestKey = requestKey;
- this.managerLoading = true;
- try {
- const data = await getUserPage({
- pageNum: 1,
- size: 99999,
- groupId: deptId,
- isQueryLZ: 0
- });
- if (this._managerRequestKey !== requestKey) return;
- const selfId = this.form.id != null ? String(this.form.id) : '';
- this.managerOptions = (data?.list || [])
- .map((item) => ({
- id: item.id == null ? item.id : String(item.id),
- name: item.name || ''
- }))
- .filter((item) => item.id != null && item.id !== '' && item.name && String(item.id) !== selfId);
- if (this.form.managerId != null && this.form.managerId !== '') {
- this.form.managerId = String(this.form.managerId);
- }
- this.mergeCurrentManagerOption();
- } catch (error) {
- if (this._managerRequestKey !== requestKey) return;
- this.managerOptions = [];
- this.mergeCurrentManagerOption();
- } finally {
- if (this._managerRequestKey === requestKey) {
- this.managerLoading = false;
- }
- }
- },
- async loadOrganizations() {
- try {
- const list = await listOrganizations();
- this.organizationTree = buildOrganizationTree(Array.isArray(list) ? list : []);
- } catch (error) {
- this.organizationTree = [];
- }
- },
- collectFactoryOrgs(groupId) {
- if (!groupId) return [];
- const tree = this.organizationTree.length ? this.organizationTree : this.organizations;
- const find = (nodes) => {
- for (const node of nodes || []) {
- if (!node) continue;
- if (String(node.id) === String(groupId)) return node;
- const matched = find(node.children || []);
- if (matched) return matched;
- }
- return null;
- };
- const list = [];
- const walk = (nodes) => {
- (nodes || []).forEach((node) => {
- if (!node) return;
- const type = String(node.type);
- if (type === '40' || type === 'factory') {
- list.push({
- id: String(node.id),
- name: node.name || '',
- code: node.groupCode || '',
- groupId: String(groupId),
- });
- }
- walk(node.children || []);
- });
- };
- walk(find(tree || [])?.children || []);
- return list.filter((item) => item.id && item.name);
- },
- factoryBelongsToOrg(item = {}, groupId) {
- if (!groupId) return false;
- const orgId = String(groupId);
- const ids = [
- item.groupId,
- item.organizationId,
- item.companyId,
- item.parentId,
- item.legalId,
- ]
- .filter((value) => value != null && value !== '')
- .map((value) => String(value));
- if (!ids.length) return true;
- return ids.includes(orgId);
- },
- async loadFactories() {
- const groupId = this.form.groupId;
- if (!groupId) {
- this.factoryOptions = [];
- this.factoryLoading = false;
- return;
- }
- this.factoryLoading = true;
- try {
- const fromTree = this.collectFactoryOrgs(groupId);
- if (fromTree.length) {
- this.factoryOptions = fromTree;
- return;
- }
- const data = await getFactoryAreaPage({
- pageNum: 1,
- size: 999,
- type: 1,
- groupId,
- organizationId: groupId,
- });
- this.factoryOptions = (Array.isArray(data?.list) ? data.list : (Array.isArray(data) ? data : []))
- .map((item) => ({
- id: String(item.id),
- name: item.name || item.factoryName || item.pathName || `工厂 ${item.id}`,
- code: item.code || '',
- enabled: item.enabled,
- groupId: item.groupId || item.organizationId || item.companyId || item.parentId || '',
- }))
- .filter((item) => item.id && item.name && this.factoryBelongsToOrg(item, groupId));
- } catch (error) {
- this.factoryOptions = this.collectFactoryOrgs(groupId);
- if (!this.factoryOptions.length) {
- this.$message.error(error.message || '工厂列表加载失败');
- }
- } finally {
- this.factoryLoading = false;
- }
- },
- syncFactoryDisplay() {
- if (this.form.factoryId == null || this.form.factoryId === '') {
- this.form.factoryId = null;
- return;
- }
- this.form.factoryId = String(this.form.factoryId);
- const matched = this.factoryOptions.find((item) => String(item.id) === String(this.form.factoryId));
- if (matched) {
- this.form.factory = matched.name;
- return;
- }
- // 详情带回的工厂不在分页结果里时,补一条选项避免只显示 ID
- if (this.form.factory) {
- this.factoryOptions.unshift({
- id: String(this.form.factoryId),
- name: this.form.factory,
- code: '',
- enabled: 1
- });
- }
- },
- handleFactoryChange(id) {
- const matched = this.factoryOptions.find((item) => String(item.id) === String(id));
- this.form.factory = matched?.name || '';
- this.form.factoryId = id != null && id !== '' ? String(id) : null;
- this.deptCascaderKey += 1;
- this.applyDeptIds([]);
- this.form.managerId = null;
- this.form.manager = '';
- this.managerOptions = [];
- this.resetPositionsByDepartment();
- },
- handlePositionChange() {
- this.form.positionId =
- this.form.positionId != null && this.form.positionId !== ''
- ? String(this.form.positionId)
- : null;
- this.form.positionIds = this.form.positionId ? [this.form.positionId] : [];
- this.syncStaffingLevelsFromPosition();
- this.syncPositionDisplay();
- this.enrichSelectedPositions();
- this.$nextTick(() => this.$refs.form?.validateField('positionId'));
- },
- handleAvatarUpload(file) { if (file.size > 2 * 1024 * 1024) { this.$message.warning('员工照片不能超过 2MB'); return false; } const reader = new FileReader(); reader.onload = (event) => { this.form.avatar = event.target.result; }; reader.readAsDataURL(file); return false; },
- addEducation() { this.form.educations.push({ education: '本科', degree: '学士', graduationDate: '', school: '', major: '' }); },
- addBankAccount() { this.form.bankAccounts.push({ bankName: '', bankAccount: '', bankAccountName: '' }); },
- addPreviousJob() { this.form.previousJobs.push({ startDate: '', endDate: '', company: '', position: '', department: '', leaveReason: '', description: '' }); },
- addSocialSecurity() { this.form.socialSecurityRecords.push({ type: '五险一金', base: 0, insuredDate: '', stopDate: '', stopReason: '' }); },
- addWorkType() { this.form.workTypes.push({ type: '', code: '', name: '', level: '', hourlyWage: 0, wageUnit: '元/小时', healthStatus: '优', specialOperator: '否', photo: [] }); },
- addCertificate() { this.form.certificates.push({ type: '', number: '', level: '', status: '有效', startDate: '', endDate: '', issueDate: '', attachment: [], remark: '' }); },
- addSkill() { this.form.skills.push({ category: '', name: '', level: '', years: 0, attachment: [], remark: '' }); },
- removeItem(key, index) { this.form[key].splice(index, 1); },
- calculateAge(date) {
- if (!date) return null;
- const birthday = new Date(date);
- if (Number.isNaN(birthday.getTime())) return null;
- const today = new Date();
- let age = today.getFullYear() - birthday.getFullYear();
- if (
- today.getMonth() < birthday.getMonth() ||
- (today.getMonth() === birthday.getMonth() && today.getDate() < birthday.getDate())
- ) {
- age -= 1;
- }
- return age >= 0 ? age : null;
- },
- syncAge() {
- this.form.age = this.calculateAge(this.form.birthDate);
- },
- /** 从 18 位身份证解析出生日期并回填年龄(格式合法时覆盖生成) */
- parseIdCard() {
- const value = String(this.form.idCard || '').trim().toUpperCase();
- this.form.idCard = value;
- if (!/^\d{17}[\dXx]$/.test(value)) return false;
- const y = value.slice(6, 10);
- const m = value.slice(10, 12);
- const d = value.slice(12, 14);
- const birth = `${y}-${m}-${d}`;
- const date = new Date(Number(y), Number(m) - 1, Number(d));
- if (
- date.getFullYear() !== Number(y) ||
- date.getMonth() !== Number(m) - 1 ||
- date.getDate() !== Number(d)
- ) {
- return false;
- }
- this.form.birthDate = birth;
- this.form.age = this.calculateAge(birth);
- return true;
- },
- handleIdCardInput(value) {
- const text = String(value || '').trim();
- if (text.length === 18) this.parseIdCard();
- },
- sameDepartment(item = {}) {
- const selected = this.normalizeDeptIds(this.form.deptIds);
- if (!selected.length) return false;
- if (item.deptId == null || item.deptId === '') return true;
- return selected.includes(String(item.deptId));
- },
- extractPositionLevelNames(opt = {}) {
- const names = [];
- const add = (value) => {
- if (value == null || value === '') return;
- const text =
- typeof value === 'object'
- ? value.level ||
- value.levelName ||
- value.label ||
- value.positionLevel ||
- value.value ||
- ''
- : value;
- const name = String(text || '').trim();
- if (name && !names.includes(name)) names.push(name);
- };
- (opt.positionLevels || []).forEach(add);
- add(opt.positionLevel);
- add(opt.level);
- return names;
- },
- syncStaffingLevelsFromPosition() {
- const primaryId = this.form.positionId || (this.form.positionIds || [])[0];
- if (!primaryId) {
- this.staffingLevelOptions = [];
- this.form.staffingLevel = '';
- this.form.level = '';
- return;
- }
- const opt =
- this.positionOptions.find((item) => String(item.id) === String(primaryId)) || {};
- const levels = this.extractPositionLevelNames(opt);
- this.staffingLevelOptions = levels;
- if (!levels.length) {
- this.form.staffingLevel = '';
- this.form.level = '';
- return;
- }
- if (!levels.includes(this.form.staffingLevel)) {
- this.form.staffingLevel = levels[0];
- }
- this.form.level = this.form.staffingLevel;
- },
- handleStaffingLevelChange(value) {
- this.form.staffingLevel = value || '';
- this.form.level = value || '';
- this.syncPositionDisplay();
- },
- clearSelectedPositions() {
- this.form.positionIds = [];
- this.form.positionBindings = [];
- this.form.positionId = null;
- this.form.position = '';
- this.form.staffingLevel = '';
- this.form.level = '';
- this.staffingLevelOptions = [];
- },
- resetPositionsByDepartment() {
- this.clearSelectedPositions();
- this.positionOptions = [];
- this.hrPositionIdSet = {};
- this.loadPositions();
- },
- resolveEmploymentType(raw) {
- if (raw == null || raw === '') return '';
- const text = String(raw);
- const hit = this.employmentOptions.find(
- (item) =>
- String(item.value) === text ||
- String(item.label) === text ||
- String(item.key) === text,
- );
- return hit ? hit.value : text;
- },
- async loadEmploymentTypeOptions() {
- try {
- const items = await getDictByCode('position_nature');
- this.employmentOptions = (items || [])
- .map((item) => {
- const label = String(item.label ?? item.value ?? '').trim();
- const value = String(item.value ?? item.label ?? '').trim();
- if (!label && !value) return null;
- return {
- label: label || value,
- value: value || label,
- key: item.key != null ? String(item.key) : value || label,
- };
- })
- .filter(Boolean);
- if (this.form.employmentType) {
- this.form.employmentType = this.resolveEmploymentType(this.form.employmentType);
- }
- } catch (error) {
- this.employmentOptions = [];
- }
- },
- async loadPositions() {
- const deptIds = this.normalizeDeptIds(this.form.deptIds);
- if (!deptIds.length) {
- this.positionOptions = [];
- this.hrPositionIdSet = {};
- this.positionLoading = false;
- return;
- }
- this.positionLoading = true;
- try {
- const pages = await Promise.all(
- deptIds.map((deptId) =>
- getPositionPage({
- pageNum: 1,
- size: 1000,
- status: 1,
- deptId,
- }),
- ),
- );
- const list = pages
- .flatMap((data) => data?.list || [])
- .map((item) => adaptPosition(item))
- .filter((item) => {
- const name = String(item.positionName || item.name || '').trim();
- if (!name) return false;
- if (!this.sameDepartment(item)) return false;
- return Boolean(item.id);
- })
- .map((item) => {
- const name = item.positionName || item.name || '';
- return {
- ...item,
- id: String(item.id),
- positionName: name,
- name,
- sequenceId: item.sequenceId != null ? String(item.sequenceId) : '',
- positionSequenceId: item.sequenceId != null ? String(item.sequenceId) : '',
- positionSequence: item.positionSequence || '',
- positionLevels: item.positionLevels || [],
- };
- });
- const idSet = {};
- list.forEach((item) => {
- if (item.id) idSet[String(item.id)] = true;
- });
- this.hrPositionIdSet = idSet;
- this.positionOptions = this.dedupePositionOptions(list);
- this.positionOptions = this.positionOptions.map((item) => {
- if (this.isNumericId(item.sequenceId || item.positionSequenceId)) return item;
- const seqMeta = this.findSequenceMeta('', item.positionSequence || item.positionSequenceName);
- if (!seqMeta?.sequenceId) return item;
- return {
- ...item,
- sequenceId: seqMeta.sequenceId,
- positionSequenceId: seqMeta.sequenceId,
- positionSequence: item.positionSequence || seqMeta.sequenceName || '',
- };
- });
- } catch (error) {
- this.positionOptions = [];
- this.hrPositionIdSet = {};
- } finally {
- this.positionLoading = false;
- }
- },
- // 保存前整体校验:基础必填 + 各分区必填 + 日期逻辑 + 记录分区,校验不过跳转到对应 tab
- async validateSections() {
- // 1. 基础 el-form(base tab 必填)
- if (!this.$refs.form) {
- this.$message.warning('表单未就绪,请稍后重试');
- return false;
- }
- this._skipFormValidate = false;
- const formValid = await new Promise((resolve) => {
- try {
- this.$refs.form.validate((valid) => resolve(Boolean(valid)));
- } catch (error) {
- resolve(false);
- }
- });
- if (!formValid) {
- this.activeTab = 'base';
- this.$message.warning('请完善基本信息中标记的必填项');
- return false;
- }
- // 同步岗位绑定,并解析 positionList 必填的序列/层级主键(界面仍只展示岗位名)
- const positionMetaOk = await this.ensurePositionMetaForSave();
- if (!positionMetaOk) return false;
- // 2. 各分区重复列表必填/日期逻辑
- const sectionInvalid = this.checkRepeatLists();
- if (sectionInvalid) {
- this.activeTab = sectionInvalid.tab;
- this.$message.warning(sectionInvalid.message);
- return false;
- }
- // 3. 记录分区(考勤/绩效/培训/奖惩)
- const recordsResult = this.$refs.records?.validateRecords?.();
- if (recordsResult && !recordsResult.valid) {
- this.activeTab = 'records';
- this.$message.warning(recordsResult.message);
- return false;
- }
- return true;
- },
- // 校验各分区重复列表必填与日期逻辑,返回首个不满足项 { tab, message } 或 null
- checkRepeatLists() {
- const gt = (start, end) => start && end && start > end;
- // 日期逻辑(基本信息)
- if (gt(this.form.birthDate, this.form.hireDate)) return { tab: 'base', message: '入职日期不能早于出生日期' };
- if (gt(this.form.hireDate, this.form.regularDate)) return { tab: 'base', message: '转正日期不能早于入职日期' };
- // 工作履历
- for (const item of this.form.previousJobs || []) {
- if (!item.startDate || !item.endDate || !item.company || !item.position) {
- return { tab: 'experience', message: '工作履历的必填项未完善(工作开始/结束日期、前公司名称、前公司岗位)' };
- }
- if (gt(item.startDate, item.endDate)) return { tab: 'experience', message: '工作结束日期必须晚于开始日期' };
- }
- // 劳动合同已改为只读,不再在档案页校验必填/日期
- // 薪酬银行:姓名必填
- for (const item of this.form.bankAccounts || []) {
- if (!item.bankAccountName) return { tab: 'contract', message: '薪酬银行的姓名(开户人)必填' };
- }
- // 社保记录:停保≥参保
- for (const item of this.form.socialSecurityRecords || []) {
- if (gt(item.insuredDate, item.stopDate)) return { tab: 'social', message: '社保停保日期不能早于参保日期' };
- }
- // 证照:类型/编号/状态/有效期必填,有效期结束≥开始
- for (const item of this.form.certificates || []) {
- if (!item.type || !item.number || !item.status || !item.startDate || !item.endDate) {
- return { tab: 'social', message: '证照的必填项未完善(证照类型、证照编号、状态、有效期起止)' };
- }
- if (gt(item.startDate, item.endDate)) return { tab: 'social', message: '证照有效期结束时间不能早于开始时间' };
- }
- // 技能:技能名称必填
- for (const item of this.form.skills || []) {
- if (!item.name) return { tab: 'social', message: '技能名称必填' };
- }
- return null;
- },
- async submit() {
- if (this.saving) return;
- this.saving = true;
- // 兜底:无论父级是否回调,都要结束 loading,避免异常后整页卡死
- const stopSaving = () => {
- this.saving = false;
- };
- const safetyTimer = setTimeout(stopSaving, 45000);
- try {
- const ok = await this.validateSections();
- if (!ok) return;
- const today = new Date().toISOString().slice(0, 10);
- const certificates = (this.form.certificates || [])
- .filter((item) => item.type)
- .map((item) => ({
- ...item,
- status: item.endDate && item.endDate < today ? '失效' : (item.status || '有效'),
- }));
- const contract = { ...(this.form.contract || {}) };
- const leaveEnabled = this.form.leaveInfo?.enabled && Boolean(this.form.leaveInfo?.leaveDate);
- const awards = (this.form.awards || [])
- .filter((item) => item.name)
- .map((item) => ({ ...item, level: item.level || '公司级' }));
- const bankAccounts = (this.form.bankAccounts || []).map((item, i) => ({
- ...item,
- bankAccountName: item.bankAccountName || (i === 0 ? this.form.name : ''),
- }));
- if ((this.form.educations || []).length > 1) {
- this.$message.info('多条教育经历仅第一条会写入员工主数据字段');
- }
- const payload = {
- ...this.form,
- certificates,
- contract,
- awards,
- bankAccounts,
- status: leaveEnabled ? '离职' : this.form.status,
- age: this.calculateAge(this.form.birthDate),
- };
- // done 必须被调用;用回调而不是依赖 resolve/reject,避免漏调导致永久 await
- const saveError = await new Promise((resolve) => {
- let settled = false;
- const done = (error) => {
- if (settled) return;
- settled = true;
- resolve(error || null);
- };
- try {
- this.$emit('save', payload, done);
- } catch (error) {
- done(error);
- }
- setTimeout(() => done(new Error('保存超时,请重试')), 40000);
- });
- if (saveError) {
- if (saveError.message && !saveError.__toastShown) {
- // 父级未提示时再兜底(例如超时)
- if (saveError.message === '保存超时,请重试') {
- this.$message.error(saveError.message);
- }
- }
- return;
- }
- this.dialogVisible = false;
- } catch (error) {
- this.$message.error(error?.message || '保存失败,请重试');
- } finally {
- clearTimeout(safetyTimer);
- stopSaving();
- }
- }
- }
- };
- </script>
- <style src="../../../../styles/views/employeeArchive/components/EmployeeArchiveForm.scss" lang="scss" scoped></style>
- <style src="../../../../styles/views/employeeArchive/components/EmployeeArchiveForm.global.scss" lang="scss"></style>
|