index.vue 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216
  1. <template>
  2. <div class="ele-body" v-loading="loading">
  3. <el-card shadow="never">
  4. <el-form label-width="80px" class="ele-form-search" @keyup.enter.native="applyFilters" @submit.native.prevent>
  5. <el-row :gutter="15">
  6. <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
  7. <el-form-item label="岗位名称">
  8. <el-input v-model.trim="draftFilters.keyword" clearable placeholder="请输入" />
  9. </el-form-item>
  10. </el-col>
  11. <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
  12. <el-form-item label="所属机构">
  13. <el-select v-model="draftFilters.organizationId" clearable filterable placeholder="请选择">
  14. <el-option v-for="item in legalEntities" :key="item.id" :label="item.name" :value="item.id" />
  15. </el-select>
  16. </el-form-item>
  17. </el-col>
  18. <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
  19. <el-form-item label="状态">
  20. <el-select v-model="draftFilters.status" clearable placeholder="请选择">
  21. <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
  22. </el-select>
  23. </el-form-item>
  24. </el-col>
  25. <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
  26. <div class="ele-form-actions">
  27. <el-button type="primary" icon="el-icon-search" class="ele-btn-icon" @click="applyFilters">
  28. 查询
  29. </el-button>
  30. <el-button type="primary" icon="el-icon-refresh" class="ele-btn-icon" @click="resetFilters">
  31. 重置
  32. </el-button>
  33. </div>
  34. </el-col>
  35. </el-row>
  36. </el-form>
  37. <ele-pro-table ref="table" row-key="id" :columns="columns" :datasource="fetchPage" :need-page="true"
  38. :page-size="20" :cache-key="$tableCacheKey(cacheKeyUrl)" class="application-table">
  39. <template v-slot:toolbar>
  40. <el-button v-if="$hasPermission('hr:hrPositionApply:save')" type="primary" icon="el-icon-plus"
  41. class="ele-btn-icon" @click="openCreate">
  42. 新增
  43. </el-button>
  44. </template>
  45. <template v-slot:documentCode="{ row }">
  46. <!-- 点流程编号开详情,替代原操作列「详情」入口 -->
  47. <el-link v-if="row.documentCode" type="primary" :underline="false" @click.stop="openView(row)">{{
  48. row.documentCode }}</el-link>
  49. <span v-else>-</span>
  50. </template>
  51. <template v-slot:code="{ row }">
  52. <el-link v-if="row.code" type="primary" :underline="false" @click.stop="openView(row)">{{
  53. row.code }}</el-link>
  54. <span v-else>-</span>
  55. </template>
  56. <template v-slot:name="{ row }">
  57. {{ row.positionName || "-" }}
  58. </template>
  59. <template v-slot:duties="{ row }">
  60. {{ (row.duties || [])[0] || "-" }}
  61. </template>
  62. <template v-slot:status="{ row }">
  63. <el-tag v-if="row.status && row.status !== '-'" size="mini" :type="statusTagType(row.status)">
  64. {{ statusName(row.status) }}
  65. </el-tag>
  66. <span v-else>-</span>
  67. </template>
  68. <template v-slot:action="{ row }">
  69. <el-link v-if="canShowApprovalRecord(row)" type="primary" :underline="false" icon="el-icon-s-order"
  70. @click.stop="openApprovalRecord(row)">
  71. 审批记录
  72. </el-link>
  73. <el-link v-if="canLaunch(row) && $hasPermission('hr:hrPositionApply:submit')" type="primary"
  74. :underline="false" icon="el-icon-position" @click.stop="launchApplication(row)">
  75. 提交
  76. </el-link>
  77. <el-link v-if="canEdit(row) && $hasPermission('hr:hrPositionApply:update')" type="primary"
  78. :underline="false" icon="el-icon-edit" @click.stop="openEdit(row)">
  79. 编辑
  80. </el-link>
  81. <el-link v-if="canDelete(row) && $hasPermission('hr:hrPositionApply:delete')" type="danger"
  82. :underline="false" icon="el-icon-delete" @click.stop="removeApplication(row)">
  83. 删除
  84. </el-link>
  85. </template>
  86. <template slot="empty">
  87. <el-empty description="暂无数据" />
  88. </template>
  89. </ele-pro-table>
  90. </el-card>
  91. <el-drawer :visible.sync="drawerVisible" direction="rtl" :size="drawerSize" custom-class="application-drawer"
  92. append-to-body :before-close="closeDrawer">
  93. <template slot="title">
  94. <div class="drawer-title">
  95. <span><i :class="readOnly ? 'el-icon-view' : 'el-icon-edit-outline'"></i></span>
  96. <div>
  97. <h2>{{ drawerTitle }}</h2>
  98. <p>{{ form.code || "保存后自动生成岗位编码" }}</p>
  99. </div>
  100. <em v-if="mode !== 'create'" :class="statusTone(form.status)"><i></i>{{ statusName(form.status) }}</em>
  101. </div>
  102. </template>
  103. <div class="drawer-body">
  104. <template v-if="readOnly">
  105. <section class="detail-overview">
  106. <span :class="'type-' + form.type">{{ typeName(form.type) }}</span>
  107. <div>
  108. <h3>{{ form.positionName }}</h3>
  109. <p>
  110. {{ form.code }} · {{ orgName(form.organizationId) }} ·
  111. {{ levelText(form.positionLevel) || "未设置层级" }} ·
  112. 申请招聘 {{ form.headcount }} 人
  113. </p>
  114. </div>
  115. </section>
  116. <section class="detail-section">
  117. <div class="detail-heading">
  118. <span><i class="el-icon-document"></i></span>
  119. <div>
  120. <h3>申请信息</h3>
  121. <p>岗位申请单据的关键信息</p>
  122. </div>
  123. </div>
  124. <div class="detail-grid">
  125. <div>
  126. <span>岗位编码</span><strong>{{ form.code }}</strong>
  127. </div>
  128. <div>
  129. <span>所属机构</span><strong>{{ orgName(form.organizationId) }}</strong>
  130. </div>
  131. <div>
  132. <span>岗位层级</span><strong>{{ levelText(form.positionLevel) || "-" }}</strong>
  133. </div>
  134. <div>
  135. <span>岗位性质</span><strong>{{ typeName(form.type) }}</strong>
  136. </div>
  137. <div>
  138. <span>岗位序列</span><strong>{{ form.positionSequence || "-" }}</strong>
  139. </div>
  140. <div>
  141. <span>岗位薪资区间</span><strong>{{ salaryText }}</strong>
  142. </div>
  143. <div>
  144. <span>申请招聘人数</span><strong>{{ form.headcount }} 人</strong>
  145. </div>
  146. <div>
  147. <span>申请人</span><strong>{{ form.applicant || "-" }}</strong>
  148. </div>
  149. <div>
  150. <span>提交时间</span><strong>{{ form.updatedAt || "-" }}</strong>
  151. </div>
  152. </div>
  153. </section>
  154. <section class="detail-section">
  155. <div class="detail-heading">
  156. <span><i class="el-icon-s-cooperation"></i></span>
  157. <div>
  158. <h3>岗位职责</h3>
  159. </div>
  160. </div>
  161. <div class="detail-block">
  162. <ol class="duty-list">
  163. <li v-for="item in form.duties" :key="item">{{ item }}</li>
  164. </ol>
  165. </div>
  166. </section>
  167. <section class="detail-section">
  168. <div class="detail-heading">
  169. <span><i class="el-icon-user"></i></span>
  170. <div>
  171. <h3>技能要求</h3>
  172. <p>岗位任职能力、学历经验与技能证书要求</p>
  173. </div>
  174. </div>
  175. <div class="detail-grid">
  176. <div>
  177. <span>工作年限要求</span><strong>{{ form.expYears || 0 }} 年</strong>
  178. </div>
  179. <div>
  180. <span>学历要求</span><strong>{{ form.education || "-" }}</strong>
  181. </div>
  182. <div>
  183. <span>行业经验</span><strong>{{ form.industryExp || "-" }}</strong>
  184. </div>
  185. <div>
  186. <span>专业能力</span><strong>{{ form.professionalAbility || "-" }}</strong>
  187. </div>
  188. <div>
  189. <span>其他要求</span><strong>{{ form.otherRequirement || "-" }}</strong>
  190. </div>
  191. </div>
  192. <div class="detail-block">
  193. <span>技能要求</span>
  194. <div class="detail-tags" v-if="form.skills && form.skills.length">
  195. <em v-for="skill in form.skills" :key="skill.name">{{ skill.name }} · {{ skill.level
  196. }}{{ skill.required ? "(必备)" : "" }}</em>
  197. </div>
  198. <p v-else class="detail-muted">-</p>
  199. </div>
  200. <div class="detail-block">
  201. <span>技能证书</span>
  202. <div class="detail-tags" v-if="form.certificates && form.certificates.length">
  203. <em v-for="cert in form.certificates" :key="cert.name">{{ cert.name }} · {{ cert.category
  204. }}</em>
  205. </div>
  206. <p v-else class="detail-muted">-</p>
  207. </div>
  208. </section>
  209. <section class="detail-audit">
  210. <span><i class="el-icon-time"></i>
  211. {{ form.updatedAt || "当前为模拟数据环境" }}</span>
  212. </section>
  213. </template>
  214. <el-form v-else ref="applicationForm" :model="form" :rules="rules" label-position="top"
  215. class="application-form">
  216. <div class="form-guide">
  217. <i class="el-icon-info"></i><span><strong>{{
  218. mode === "create" ? "发起岗位申请" : "调整岗位申请配置"
  219. }}</strong><small v-if="mode === 'create'">带 <b>*</b> 的项目为必填项,保存后进入部门负责人 / HR 审核。</small><small v-else>带
  220. <b>*</b> 的项目为必填项,保存草稿后可继续修改,提交后进入部门负责人 / HR 审核。</small></span>
  221. </div>
  222. <section class="form-section">
  223. <div class="section-title">
  224. <span><i class="el-icon-suitcase"></i></span>
  225. <div>
  226. <h3>岗位基本信息</h3>
  227. <p>填写申请岗位的基础信息与定薪区间</p>
  228. </div>
  229. </div>
  230. <div class="form-grid">
  231. <el-form-item label="岗位名称" prop="positionName"><el-input v-model.trim="form.positionName" maxlength="40"
  232. show-word-limit placeholder="如:数控车床操作工" /></el-form-item>
  233. <el-form-item label="所属机构" prop="organizationId"><el-select v-model="form.organizationId" filterable
  234. placeholder="请选择所属机构"><el-option v-for="item in legalEntities" :key="item.id" :label="item.name"
  235. :value="item.id" /></el-select></el-form-item>
  236. <el-form-item label="岗位性质" prop="type"><el-select v-model="form.type" clearable
  237. placeholder="请选择岗位性质"><el-option v-for="item in typeOptions" :key="String(item.value)"
  238. :label="item.label" :value="item.value" /></el-select></el-form-item>
  239. <el-form-item label="岗位序列" prop="positionSequence"><el-select v-model="form.positionSequence" filterable
  240. placeholder="请选择岗位序列" @change="handleSequenceChange"><el-option v-for="item in sequenceOptions"
  241. :key="item" :label="item" :value="item" /></el-select></el-form-item>
  242. <el-form-item label="岗位层级" prop="positionLevel"><el-select v-model="form.positionLevel"
  243. placeholder="请选择岗位层级(随岗位序列联动)"><el-option v-for="item in levelOptions" :key="item.value"
  244. :label="item.label" :value="item.value" /></el-select></el-form-item>
  245. <el-form-item label="申请招聘人数" prop="headcount"><el-input-number v-model="form.headcount" :min="1"
  246. :max="999" controls-position="right" /></el-form-item>
  247. <el-form-item label="岗位薪资区间" prop="salaryMax">
  248. <div class="salary-range-field">
  249. <div class="salary-range-item">
  250. <span class="salary-range-label">下限</span>
  251. <el-input-number v-model="form.salaryMin" :min="0" :step="500" controls-position="right"
  252. placeholder="下限" @change="syncSalaryMid" />
  253. </div>
  254. <div class="salary-range-item">
  255. <span class="salary-range-label">中位值</span>
  256. <el-input-number v-model="form.salaryMid" :min="0" :step="500" controls-position="right"
  257. placeholder="中位值" />
  258. </div>
  259. <div class="salary-range-item">
  260. <span class="salary-range-label">上限</span>
  261. <el-input-number v-model="form.salaryMax" :min="0" :step="500" controls-position="right"
  262. placeholder="上限" @change="syncSalaryMid" />
  263. </div>
  264. </div>
  265. <p class="field-help">
  266. <i class="el-icon-info"></i>
  267. 单位:元,修改上下限自动计算中位值。
  268. </p>
  269. </el-form-item>
  270. </div>
  271. <el-form-item label="岗位职责" prop="dutiesText"><el-input v-model.trim="form.dutiesText" type="textarea"
  272. :rows="5" maxlength="300" show-word-limit placeholder="1. 负责……&#10;2. 落实……&#10;3. 协同……" />
  273. <p class="field-help">
  274. <i class="el-icon-info"></i>
  275. 建议按职责重要性由高到低逐行填写。
  276. </p>
  277. </el-form-item>
  278. </section>
  279. <section class="form-section">
  280. <div class="section-title">
  281. <span><i class="el-icon-user"></i></span>
  282. <div>
  283. <h3>技能要求</h3>
  284. <p>定义岗位任职能力、学历经验与技能证书要求</p>
  285. </div>
  286. </div>
  287. <div class="form-grid">
  288. <el-form-item label="工作年限要求"><el-input-number v-model="form.expYears" :min="0" :max="40"
  289. controls-position="right" placeholder="单位:年" /></el-form-item>
  290. <el-form-item label="学历要求"><el-select v-model="form.education" placeholder="请选择学历要求"><el-option
  291. v-for="item in educationOptions" :key="item" :label="item"
  292. :value="item" /></el-select></el-form-item>
  293. <el-form-item label="行业经验"><el-input v-model.trim="form.industryExp"
  294. placeholder="如:3 年以上汽车零部件制造行业" /></el-form-item>
  295. <el-form-item label="专业能力"><el-input v-model.trim="form.professionalAbility"
  296. placeholder="如:熟悉 IATF16949、APQP 五大工具" /></el-form-item>
  297. <el-form-item label="其他要求"><el-input v-model.trim="form.otherRequirement"
  298. placeholder="补充能力、素质或工作制要求" /></el-form-item>
  299. </div>
  300. <div class="form-subtitle">技能</div>
  301. <div v-for="(skill, index) in form.skills" :key="index" class="dynamic-row">
  302. <el-input v-model.trim="skill.name" placeholder="技能名称" />
  303. <el-select v-model="skill.level" placeholder="技能等级">
  304. <el-option v-for="item in skillLevelOptions" :key="item" :label="item" :value="item" />
  305. </el-select>
  306. <el-switch v-model="skill.required" active-text="必备" />
  307. <el-button icon="el-icon-delete" @click="removeSkill(index)" />
  308. </div>
  309. <el-button class="add-line" icon="el-icon-plus" @click="addSkill">新增技能</el-button>
  310. <div class="form-subtitle">技能证书</div>
  311. <div v-for="(cert, index) in form.certificates" :key="index" class="dynamic-row cert-row">
  312. <el-input v-model.trim="cert.name" placeholder="证书名称" />
  313. <el-select v-model="cert.category" placeholder="证书分类">
  314. <el-option v-for="item in idTypeOptions" :key="item.key || item.value" :label="item.label"
  315. :value="item.value" />
  316. </el-select>
  317. <el-button icon="el-icon-delete" @click="removeCert(index)" />
  318. </div>
  319. <el-button class="add-line" icon="el-icon-plus" @click="addCert">新增证书</el-button>
  320. </section>
  321. </el-form>
  322. </div>
  323. <div class="drawer-footer">
  324. <span><i class="el-icon-lock"></i>
  325. 岗位申请将用于招聘需求岗位基础数据<em class="drawer-hint">新建或已驳回再次保存后将弹出审批提交流程</em></span>
  326. <div v-if="readOnly">
  327. <el-button @click="drawerVisible = false">关闭</el-button><el-button v-if="canEdit(form)" type="primary"
  328. icon="el-icon-edit" @click="mode = 'edit'">编辑</el-button>
  329. </div>
  330. <div v-else>
  331. <el-button :disabled="saving" @click="closeDrawer()">取消</el-button>
  332. <el-button v-if="mode === 'create'" type="primary" icon="el-icon-check" :loading="saving"
  333. @click="saveApplication()">保存</el-button>
  334. <el-button v-else type="primary" icon="el-icon-check" :loading="saving"
  335. @click="saveApplication(false)">保存</el-button>
  336. </div>
  337. </div>
  338. </el-drawer>
  339. <processSubmitDialog :processSubmitDialogFlag.sync="processSubmitDialogFlag" v-if="processSubmitDialogFlag"
  340. ref="processSubmitDialogRef" @reload="onProcessSubmitReload" />
  341. <bpm-process-detail-dialog ref="approvalRecordDialog" />
  342. </div>
  343. </template>
  344. <script>
  345. import {
  346. adaptPositionApplication,
  347. adaptPositionApplicationDetail,
  348. createPositionApplication,
  349. deletePositionApplication,
  350. getPositionApplicationById,
  351. getPositionApplicationPage,
  352. getPositionSequenceList,
  353. serializePositionApplication,
  354. submitPositionApplication,
  355. updatePositionApplication,
  356. } from "@/api/hr";
  357. /* 机构选项取登录缓存;组织列表接口仅用于部门与机构名称展示 */
  358. import { listOrganizations } from "@/api/organization";
  359. import { getOrgOptions, getCurrentOrgId } from "@/utils/current-org";
  360. import { getDict, getDictByCode } from "@/api/sys";
  361. import processSubmitDialog from "@/BIZComponents/processSubmitDialog/processSubmitDialog.vue";
  362. import BpmProcessDetailDialog from "@/views/bpm/processInstance/detailDialog.vue";
  363. import { getCurrentUser } from "@/utils/token-util";
  364. /** 岗位申请审批流程 businessKey,需与 BPM 流程定义 code 一致 */
  365. const POSITION_APPLY_BUSINESS_KEY = "hr_position_application";
  366. /**
  367. * 列表一次拉取的最大条数。
  368. *
  369. * 顶部指标卡(总数 / 待审核 / 已通过 / 招聘人数合计)必须基于**全量**统计,
  370. * 而表格是分页展示。原来两者各调一次 `POST /hr/position/apply/page`
  371. * (指标卡 size=1000、表格 size=20),首屏与每次增删改后同一接口都要请求两遍。
  372. * 现在改为:本页只发**一次**请求拉全量,表格在本地切片分页,指标卡复用同一份数据。
  373. */
  374. const APPLY_PAGE_MAX_SIZE = 1000;
  375. const STATUS_LABELS = {
  376. DRAFT: "未提交",
  377. PENDING: "待审核",
  378. APPROVED: "已通过",
  379. REJECTED: "已驳回",
  380. };
  381. const statusOptions = Object.entries(STATUS_LABELS).map(([value, label]) => ({
  382. value,
  383. label,
  384. }));
  385. const OPERATION_LABELS = {
  386. CREATE: "创建",
  387. UPDATE: "编辑",
  388. DELETE: "删除",
  389. SUBMIT: "提交审批",
  390. APPROVE: "审批通过",
  391. REJECT: "驳回",
  392. EFFECTIVE: "生效",
  393. };
  394. const educationOptions = [
  395. "不限",
  396. "初中及以上",
  397. "高中/中专及以上",
  398. "大专及以上",
  399. "本科及以上",
  400. "硕士及以上",
  401. ];
  402. const skillLevelOptions = ["初级", "中级", "高级", "技师", "高级技师"];
  403. const emptyFilters = () => ({
  404. keyword: "",
  405. /* 所属机构默认登录机构列表第一个(用户所属多个机构时默认第一个) */
  406. organizationId: getCurrentOrgId() || "",
  407. status: "",
  408. });
  409. const emptyForm = () => ({
  410. id: null,
  411. code: "",
  412. positionName: "",
  413. organizationId: null,
  414. deptName: "",
  415. positionLevel: null,
  416. positionSequence: "",
  417. type: "",
  418. headcount: 1,
  419. salaryMin: 0,
  420. salaryMid: 0,
  421. salaryMax: 0,
  422. expYears: 0,
  423. education: "",
  424. industryExp: "",
  425. professionalAbility: "",
  426. otherRequirement: "",
  427. skills: [],
  428. certificates: [],
  429. applicant: "",
  430. dutiesText: "",
  431. duties: [],
  432. status: "DRAFT",
  433. reviewer: "",
  434. reviewTime: "",
  435. reviewOpinion: "",
  436. operationLogs: [],
  437. updatedAt: "",
  438. });
  439. export default {
  440. name: "PositionApplication",
  441. components: {
  442. processSubmitDialog,
  443. BpmProcessDetailDialog,
  444. },
  445. data() {
  446. return {
  447. loading: false,
  448. saving: false,
  449. processSubmitDialogFlag: false,
  450. currentLaunchRow: null,
  451. rows: [],
  452. totalCount: 0,
  453. overview: [],
  454. /* 全量缓存对应的筛选条件 key:相同则复用,避免表格与指标卡重复请求同一个接口 */
  455. overviewKey: "",
  456. legalEntities: [],
  457. departments: [],
  458. orgMap: {},
  459. draftFilters: emptyFilters(),
  460. filters: emptyFilters(),
  461. levelOptions: [],
  462. typeOptions: [],
  463. sequenceOptions: [],
  464. // 岗位序列 → 该序列下的层级下拉(用于联动加载)
  465. sequenceLevelMap: {},
  466. educationOptions,
  467. skillLevelOptions,
  468. // 技能证书类别:与岗位设定资质证书一致,走 /system/dict/id_type
  469. idTypeOptions: [],
  470. statusOptions,
  471. cacheKeyUrl: "hr-pc-position-application-v4",
  472. drawerVisible: false,
  473. mode: "view",
  474. form: emptyForm(),
  475. rules: {
  476. positionName: [
  477. { required: true, message: "请输入岗位名称", trigger: "blur" },
  478. ],
  479. organizationId: [
  480. { required: true, message: "请选择所属机构", trigger: "change" },
  481. ],
  482. type: [{ required: true, message: "请选择岗位性质", trigger: "change" }],
  483. // 岗位层级选项由岗位序列联动产生,序列不选则层级无可选项,故两者一并必填
  484. positionSequence: [
  485. { required: true, message: "请选择岗位序列", trigger: "change" },
  486. ],
  487. positionLevel: [
  488. { required: true, message: "请选择岗位层级", trigger: "change" },
  489. ],
  490. headcount: [
  491. { required: true, message: "请输入申请招聘人数", trigger: "change" },
  492. ],
  493. salaryMax: [{ validator: this.salaryRangeValidator, trigger: "change" }],
  494. dutiesText: [
  495. { required: true, message: "请输入岗位职责", trigger: "blur" },
  496. ],
  497. },
  498. };
  499. },
  500. computed: {
  501. styleResponsive() {
  502. return this.$store.state.theme?.styleResponsive !== false;
  503. },
  504. columns() {
  505. return [
  506. { type: "index", label: "序号", width: 60, align: "center" },
  507. {
  508. prop: "documentCode",
  509. columnKey: "documentCode",
  510. label: "流程编号",
  511. minWidth: 170,
  512. slot: "documentCode",
  513. formatter: (_r, _c, value) => value || "-",
  514. },
  515. {
  516. prop: "code",
  517. columnKey: "code",
  518. label: "岗位编码",
  519. width: 150,
  520. slot: "code",
  521. align: "center",
  522. },
  523. {
  524. columnKey: "name",
  525. label: "岗位名称",
  526. minWidth: 190,
  527. slot: "name",
  528. },
  529. {
  530. prop: "positionLevel",
  531. label: "岗位层级",
  532. width: 100,
  533. align: "center",
  534. formatter: (row) =>
  535. row.positionLevel ? this.levelText(row.positionLevel) : "-",
  536. },
  537. {
  538. prop: "type",
  539. label: "岗位性质",
  540. width: 100,
  541. align: "center",
  542. formatter: (row) => this.typeName(row.type),
  543. },
  544. {
  545. prop: "headcount",
  546. label: "申请招聘人数",
  547. width: 110,
  548. align: "center",
  549. formatter: (row) => `${row.headcount} 人`,
  550. },
  551. { prop: "applicant", label: "申请人", width: 100, align: "center" },
  552. // {
  553. // columnKey: "duties",
  554. // label: "岗位职责",
  555. // minWidth: 230,
  556. // slot: "duties",
  557. // },
  558. {
  559. columnKey: "status",
  560. label: "状态",
  561. width: 100,
  562. align: "center",
  563. slot: "status",
  564. },
  565. {
  566. prop: "createUserName",
  567. label: "创建人",
  568. width: 100,
  569. align: "center",
  570. },
  571. {
  572. prop: "createTime",
  573. label: "创建时间",
  574. width: 100,
  575. align: "center",
  576. },
  577. {
  578. columnKey: "action",
  579. label: "操作",
  580. minWidth: 150,
  581. align: 'left',
  582. fixed: "right",
  583. resizable: false,
  584. slot: "action",
  585. },
  586. ];
  587. },
  588. metrics() {
  589. const total = this.overview.length;
  590. const pending = this.overview.filter(
  591. (item) => item.status === "PENDING",
  592. ).length;
  593. const approved = this.overview.filter(
  594. (item) => item.status === "APPROVED",
  595. ).length;
  596. const headcount = this.overview.reduce(
  597. (sum, item) => sum + Number(item.headcount || 0),
  598. 0,
  599. );
  600. return [
  601. {
  602. label: "申请总数",
  603. value: total,
  604. unit: "单",
  605. note: "岗位申请单据合计",
  606. icon: "el-icon-document-copy",
  607. tone: "blue",
  608. },
  609. {
  610. label: "待审核",
  611. value: pending,
  612. unit: "单",
  613. note: "需部门负责人 / HR 处理",
  614. icon: "el-icon-time",
  615. tone: "orange",
  616. },
  617. {
  618. label: "已审核",
  619. value: approved,
  620. unit: "单",
  621. note: "可进入招聘需求岗位库",
  622. icon: "el-icon-circle-check",
  623. tone: "green",
  624. },
  625. {
  626. label: "招聘人数",
  627. value: headcount,
  628. unit: "人",
  629. note: "申请编制招聘人数合计",
  630. icon: "el-icon-user",
  631. tone: "cyan",
  632. },
  633. ];
  634. },
  635. salaryText() {
  636. const min = Number(this.form.salaryMin || 0);
  637. const mid = Number(this.form.salaryMid || 0);
  638. const max = Number(this.form.salaryMax || 0);
  639. if (!min && !max) return "-";
  640. const range = `¥${min.toLocaleString("zh-CN")} - ¥${max.toLocaleString(
  641. "zh-CN",
  642. )}`;
  643. return mid ? `${range}(中位 ¥${mid.toLocaleString("zh-CN")})` : range;
  644. },
  645. hasFilters() {
  646. return Boolean(
  647. this.filters.keyword ||
  648. this.filters.organizationId ||
  649. this.filters.status,
  650. );
  651. },
  652. readOnly() {
  653. return this.mode === "view";
  654. },
  655. drawerTitle() {
  656. return this.mode === "create"
  657. ? "新增岗位申请"
  658. : this.mode === "edit"
  659. ? "编辑岗位申请"
  660. : "岗位申请详情";
  661. },
  662. drawerSize() {
  663. return window.innerWidth < 900 ? "96%" : "760px";
  664. },
  665. },
  666. created() {
  667. this.loadOrganizations();
  668. this.loadSequences();
  669. this.loadTypeOptions();
  670. this.loadIdTypeOptions();
  671. /* 列表与指标卡的第 1 次数据请求由表格 datasource(fetchPage)发起,这里不再单独拉概览 */
  672. },
  673. methods: {
  674. // 表格数据源:本页只发一次请求拉全量,在本地切片分页(指标卡复用同一份数据)
  675. async fetchPage({ page, limit }) {
  676. const all = await this.loadAllApplications();
  677. const start = (page - 1) * limit;
  678. const list = all.slice(start, start + limit);
  679. this.rows = list;
  680. this.totalCount = all.length;
  681. return { list, count: all.length };
  682. },
  683. /**
  684. * 拉取当前筛选条件下的**全量**岗位申请,供表格本地分页与顶部指标卡共用。
  685. *
  686. * 为什么合并:原来表格(size=20)与指标卡(size=1000)各调一次
  687. * `POST /hr/position/apply/page`,首屏以及每次增删改后都会请求同一个接口两遍。
  688. * 现在按筛选条件缓存一次结果,两者共用,请求数减半。
  689. *
  690. * 失败必须明确报错(项目红线:不做静默兜底)——弹错误提示并把异常抛给
  691. * ele-pro-table,让表格自身也进入错误态,而不是悄悄显示一份空列表。
  692. */
  693. async loadAllApplications() {
  694. const key = JSON.stringify([
  695. this.filters.keyword || "",
  696. this.filters.organizationId || "",
  697. this.filters.status || "",
  698. ]);
  699. if (this.overviewKey === key) return this.overview;
  700. try {
  701. const data = await getPositionApplicationPage({
  702. pageNum: 1,
  703. size: APPLY_PAGE_MAX_SIZE,
  704. keyword: this.filters.keyword || undefined,
  705. organizationId: this.filters.organizationId || undefined,
  706. status: this.filters.status || undefined,
  707. });
  708. const list = (data?.list || []).map(adaptPositionApplication);
  709. const total = Number(data?.count ?? list.length);
  710. this.overview = list;
  711. this.overviewKey = key;
  712. this.fallbackOrganizations();
  713. /* 命中取数上限:明确告知「只加载了前 N 条」,避免指标卡与分页看起来像全量 */
  714. if (total > APPLY_PAGE_MAX_SIZE) {
  715. this.$message({
  716. type: "warning",
  717. showClose: true,
  718. duration: 0,
  719. message: `岗位申请共 ${total} 条,当前仅加载前 ${APPLY_PAGE_MAX_SIZE} 条(指标卡与分页基于已加载数据),请用筛选条件缩小范围。`,
  720. });
  721. }
  722. return list;
  723. } catch (error) {
  724. this.overview = [];
  725. this.overviewKey = "";
  726. this.$message.error(error.message || "岗位申请列表加载失败");
  727. throw error;
  728. }
  729. },
  730. // 机构选项取登录缓存;部门(向上回溯归属机构)与机构名称仍用组织列表接口
  731. async loadOrganizations() {
  732. /* 登录缓存先落地:即使组织接口失败,所属机构下拉仍可用 */
  733. this.legalEntities = getOrgOptions().map((item) => ({
  734. id: item.id,
  735. name: item.name,
  736. code: "ZY",
  737. }));
  738. try {
  739. const list = await listOrganizations();
  740. const records = Array.isArray(list) ? list : [];
  741. const parents = new Map(records.map((item) => [String(item.id), item]));
  742. this.departments = records.map((item) => {
  743. let current = item;
  744. while (parents.has(String(current.parentId)))
  745. current = parents.get(String(current.parentId));
  746. return {
  747. id: item.id,
  748. name: item.name,
  749. code: item.groupCode || item.code || "",
  750. legalId: current.id,
  751. };
  752. });
  753. this.orgMap = Object.fromEntries(
  754. records.map((item) => [String(item.id), item.name]),
  755. );
  756. } catch (error) {
  757. /*
  758. * 明确报错,不再静默:组织接口失败会让「部门」下拉与机构名称展示不全,
  759. * 用户必须知道是接口挂了,而不是以为公司没有部门。
  760. * 注意:不要在这里清空 legalEntities —— 它来自登录缓存,与组织接口无关。
  761. */
  762. this.departments = [];
  763. this.orgMap = {};
  764. this.$message.error(
  765. error.message || "组织列表加载失败,部门下拉与机构名称可能显示不全",
  766. );
  767. }
  768. },
  769. // 加载岗位序列及其层级:序列下拉 + 序列→层级联动映射(仅启用配置)
  770. async loadSequences() {
  771. try {
  772. const list = await getPositionSequenceList();
  773. if (Array.isArray(list) && list.length) {
  774. const names = [];
  775. const map = {};
  776. list.forEach((item) => {
  777. const name = (item.sequence || "").trim();
  778. if (!name) return;
  779. names.push(name);
  780. // 序列层级:仅取启用层级,value/label 为层级文本(如 P1/T4);无启用层级保持空选项
  781. const levels = (item.levelList || [])
  782. .filter((level) => Number(level.status) !== 0)
  783. .map((level) => ({
  784. value: level.value || level.label || "",
  785. label: level.label || level.value || "",
  786. }))
  787. .filter((level) => level.value);
  788. map[name] = levels.length ? levels : [];
  789. });
  790. this.sequenceOptions = names;
  791. this.sequenceLevelMap = map;
  792. this.syncLevelOptions(this.form.positionSequence);
  793. } else {
  794. this.sequenceOptions = [];
  795. this.sequenceLevelMap = {};
  796. }
  797. } catch (error) {
  798. this.sequenceOptions = [];
  799. this.sequenceLevelMap = {};
  800. this.$message.error(error.message || "岗位序列加载失败,岗位序列下拉暂不可用");
  801. }
  802. },
  803. // 组织接口无数据时,从岗位申请数据推导机构和部门,保证下拉可用
  804. fallbackOrganizations() {
  805. if (this.departments.length || !this.overview.length) return;
  806. this.legalEntities = [
  807. ...new Map(
  808. this.overview
  809. .filter((item) => item.organizationId)
  810. .map((item) => [
  811. String(item.organizationId),
  812. {
  813. id: item.organizationId,
  814. name: item.organizationName || `组织${item.organizationId}`,
  815. },
  816. ]),
  817. ).values(),
  818. ];
  819. this.departments = [
  820. ...new Map(
  821. this.overview
  822. .filter((item) => item.deptId)
  823. .map((item) => [
  824. String(item.deptId),
  825. {
  826. id: item.deptId,
  827. name: item.deptName || `部门${item.deptId}`,
  828. legalId: item.organizationId,
  829. },
  830. ]),
  831. ).values(),
  832. ];
  833. },
  834. /* 数据变化后统一走这里:一次刷新表格与顶部指标卡(两者共用同一份全量数据) */
  835. refreshData() {
  836. this.reloadTable();
  837. },
  838. applyFilters() {
  839. this.filters = { ...this.draftFilters };
  840. this.reloadTable();
  841. },
  842. resetFilters() {
  843. this.draftFilters = emptyFilters();
  844. this.filters = emptyFilters();
  845. this.reloadTable();
  846. },
  847. clearFilter(key) {
  848. this.draftFilters[key] = "";
  849. this.filters[key] = "";
  850. this.reloadTable();
  851. },
  852. reloadTable() {
  853. /* 清掉全量缓存:增删改 / 筛选变化都要重拉,顶部指标卡随之刷新 */
  854. this.overviewKey = "";
  855. this.$nextTick(() => this.$refs.table?.reload({ page: 1 }));
  856. },
  857. openCreate() {
  858. this.mode = "create";
  859. this.form = emptyForm();
  860. // 默认带出当前登录用户所属机构
  861. this.form.organizationId =
  862. getCurrentOrgId() ?? this.legalEntities[0]?.id ?? null;
  863. this.syncLevelOptions(this.form.positionSequence);
  864. this.drawerVisible = true;
  865. this.$nextTick(() => this.$refs.applicationForm?.clearValidate());
  866. },
  867. async openEdit(row) {
  868. if (!this.canEdit(row)) return;
  869. this.mode = "edit";
  870. this.drawerVisible = true;
  871. try {
  872. const detail = await getPositionApplicationById(row.id);
  873. this.form = this.detailToForm(detail);
  874. } catch (error) {
  875. /* 详情接口失败:用列表行数据兜底展示,但必须明确告知用户(不静默) */
  876. this.form = this.rowToForm(row);
  877. this.$message.warning(
  878. error.message ||
  879. "岗位申请详情加载失败,当前展示列表行信息,部分字段可能为空",
  880. );
  881. }
  882. this.syncLevelOptions(this.form.positionSequence);
  883. this.$nextTick(() => this.$refs.applicationForm?.clearValidate());
  884. },
  885. async openView(row) {
  886. this.mode = "view";
  887. this.drawerVisible = true;
  888. try {
  889. const detail = await getPositionApplicationById(row.id);
  890. this.form = this.detailToForm(detail);
  891. } catch (error) {
  892. /* 详情接口失败:用列表行数据兜底展示,但必须明确告知用户(不静默) */
  893. this.form = this.rowToForm(row);
  894. this.$message.warning(
  895. error.message ||
  896. "岗位申请详情加载失败,当前展示列表行信息,部分字段可能为空",
  897. );
  898. }
  899. this.syncLevelOptions(this.form.positionSequence);
  900. },
  901. // 详情接口返回 → 表单/详情页字段
  902. detailToForm(detail) {
  903. const adapted = adaptPositionApplicationDetail(detail);
  904. return {
  905. ...adapted,
  906. type: this.resolveTypeValue(adapted.type),
  907. deptName: adapted.deptName || "",
  908. positionLevel: adapted.positionLevel || "",
  909. headcount: adapted.headcount || 1,
  910. };
  911. },
  912. // 列表行 → 表单/详情页字段(详情接口失败兜底)
  913. rowToForm(row) {
  914. return {
  915. ...emptyForm(),
  916. ...row,
  917. type: this.resolveTypeValue(row.type),
  918. deptName: row.deptName || "",
  919. duties: row.duties || [],
  920. dutiesText: (row.duties || []).join("\n"),
  921. };
  922. },
  923. // 岗位序列变化时联动岗位层级选项,并清空已选层级
  924. handleSequenceChange() {
  925. this.form.positionLevel = null;
  926. this.syncLevelOptions(this.form.positionSequence);
  927. },
  928. // 根据岗位序列切换到对应层级下拉(未配置或未选序列时用兜底层级)
  929. syncLevelOptions(sequence) {
  930. this.levelOptions =
  931. (sequence && this.sequenceLevelMap[sequence]) || [];
  932. },
  933. // 修改薪资上下限时自动计算中位值
  934. syncSalaryMid() {
  935. const min = Number(this.form.salaryMin || 0);
  936. const max = Number(this.form.salaryMax || 0);
  937. if (max >= min) {
  938. this.form.salaryMid = Math.round((min + max) / 2);
  939. }
  940. },
  941. // 薪资区间校验:必填 + 上下限关系;定义在 methods 保证 rules 里能取到有效引用
  942. salaryRangeValidator(_rule, _value, callback) {
  943. const min = Number(this.form.salaryMin || 0);
  944. const max = Number(this.form.salaryMax || 0);
  945. if (!min || !max) {
  946. callback(new Error("请输入完整的岗位薪资区间"));
  947. } else if (max < min) {
  948. callback(new Error("薪资上限不能低于下限"));
  949. } else {
  950. callback();
  951. }
  952. },
  953. addSkill() {
  954. this.form.skills.push({ name: "", level: "中级", required: true });
  955. },
  956. removeSkill(index) {
  957. this.form.skills.splice(index, 1);
  958. },
  959. addCert() {
  960. this.form.certificates.push({
  961. name: "",
  962. category: this.idTypeOptions[0]?.value || "",
  963. });
  964. },
  965. removeCert(index) {
  966. this.form.certificates.splice(index, 1);
  967. },
  968. closeDrawer(done) {
  969. this.drawerVisible = false;
  970. if (typeof done === "function") done();
  971. },
  972. // 保存岗位申请;新建或再次编辑已驳回单据保存后,弹出 BPM 审批流程
  973. saveApplication(validate = true) {
  974. const persist = async () => {
  975. const payload = serializePositionApplication(this.form);
  976. this.saving = true;
  977. const wasRejected = this.form.status === "REJECTED";
  978. try {
  979. let id = this.form.id;
  980. const isCreate = !id;
  981. if (id) {
  982. await updatePositionApplication(id, payload);
  983. } else {
  984. const created = await createPositionApplication(payload);
  985. id = created?.id ?? created;
  986. }
  987. const savedRow = {
  988. ...this.form,
  989. id,
  990. status: this.form.status || "DRAFT",
  991. code: this.form.code || this.form.positionCode || "",
  992. positionName: this.form.positionName || "",
  993. createUserId: this.form.createUserId,
  994. };
  995. this.drawerVisible = false;
  996. this.$message.success("岗位申请已保存");
  997. this.reloadTable();
  998. // 新建,或已驳回再次编辑保存后,重新弹出提交审批弹框
  999. if (id && (isCreate || wasRejected)) {
  1000. this.$nextTick(() => this.launchApplication(savedRow));
  1001. }
  1002. } catch (error) {
  1003. this.$message.error(error.message || "岗位申请保存失败");
  1004. } finally {
  1005. this.saving = false;
  1006. }
  1007. };
  1008. if (validate)
  1009. this.$refs.applicationForm.validate((valid) => valid && persist());
  1010. else persist();
  1011. },
  1012. // 未提交、已驳回可发起 / 重新拉起流程
  1013. canLaunch(row) {
  1014. return !row?.status || ["DRAFT", "REJECTED"].includes(row.status);
  1015. },
  1016. /** 尚未发起审批时不展示审批记录 */
  1017. canShowApprovalRecord(row) {
  1018. return Boolean(
  1019. row?.processInstanceId ||
  1020. row?.approvalInstanceId ||
  1021. row?.processInstance,
  1022. );
  1023. },
  1024. // 发起岗位申请:打开 BPM 提交审核弹框(提交直接调 /bpm/process-instance/create)
  1025. launchApplication(row) {
  1026. if (!row?.id) {
  1027. this.$message.warning("缺少岗位申请主键,无法发起审批");
  1028. return;
  1029. }
  1030. if (!this.canLaunch(row)) return;
  1031. this.currentLaunchRow = row;
  1032. this.processSubmitDialogFlag = true;
  1033. this.$nextTick(() => {
  1034. const currentUser = getCurrentUser() || {};
  1035. const params = {
  1036. businessId: row.id,
  1037. businessKey: POSITION_APPLY_BUSINESS_KEY,
  1038. formCreateUserId:
  1039. row.createUserId || currentUser.id || currentUser.userId || "",
  1040. variables: {
  1041. businessCode: row.code || row.positionCode || "",
  1042. businessName: row.positionName || "",
  1043. businessType: "岗位申请",
  1044. },
  1045. };
  1046. this.$refs.processSubmitDialogRef?.init(params);
  1047. });
  1048. },
  1049. onProcessSubmitReload() {
  1050. this.currentLaunchRow = null;
  1051. this.reloadTable();
  1052. },
  1053. openApprovalRecord(row) {
  1054. this.$refs.approvalRecordDialog?.open(row);
  1055. },
  1056. // 仅待审核(PENDING)状态显示审批按钮
  1057. canSubmit(row) {
  1058. return row.status === "PENDING";
  1059. },
  1060. // 提交岗位申请审批(操作栏独立按钮触发,仅 PENDING 可操作)
  1061. submitApproval(row) {
  1062. if (!this.canSubmit(row)) return;
  1063. this.$confirm(
  1064. `确认对岗位申请「${row.positionName}」执行审批吗?`,
  1065. "岗位申请审批",
  1066. { type: "warning" },
  1067. )
  1068. .then(async () => {
  1069. try {
  1070. await submitPositionApplication(row.id);
  1071. this.$message.success("岗位申请已提交审批");
  1072. this.reloadTable();
  1073. } catch (error) {
  1074. this.$message.error(error.message || "提交审批失败");
  1075. }
  1076. })
  1077. .catch(() => { });
  1078. },
  1079. // 未提交与已驳回可删除
  1080. canDelete(row) {
  1081. return ["DRAFT", "REJECTED"].includes(row.status);
  1082. },
  1083. // 删除岗位申请(DELETE /hr/position/apply/{id})
  1084. removeApplication(row) {
  1085. if (!this.canDelete(row)) return;
  1086. this.$confirm(
  1087. `确认删除岗位申请「${row.positionName}」吗?删除后不可恢复。`,
  1088. "删除确认",
  1089. { type: "warning" },
  1090. )
  1091. .then(async () => {
  1092. try {
  1093. await deletePositionApplication(row.id);
  1094. this.$message.success("岗位申请已删除");
  1095. this.reloadTable();
  1096. } catch (error) {
  1097. this.$message.error(error.message || "删除失败");
  1098. }
  1099. })
  1100. .catch(() => { });
  1101. },
  1102. // 岗位性质:后端字典 /system/dict/getByCode/position_nature
  1103. // 下拉 value 与提交后端的 positionType 统一用「字典 id」,label 用字典文案
  1104. async loadTypeOptions() {
  1105. try {
  1106. const items = await getDictByCode("position_nature");
  1107. this.typeOptions = items
  1108. .map((item) => {
  1109. const label = String(item.label ?? item.value ?? "").trim();
  1110. const id = String(item.id ?? item.key ?? "").trim();
  1111. if (!label || !id) return null;
  1112. return {
  1113. label,
  1114. value: id,
  1115. key: item.key != null ? String(item.key) : id,
  1116. };
  1117. })
  1118. .filter(Boolean);
  1119. } catch (error) {
  1120. this.typeOptions = [];
  1121. this.$message.error(error.message || "岗位性质字典加载失败");
  1122. }
  1123. },
  1124. // 技能证书类别:与岗位设定资质证书一致,/system/dict/id_type
  1125. async loadIdTypeOptions() {
  1126. try {
  1127. const items = await getDict("id_type");
  1128. this.idTypeOptions = items.filter((item) => item.value);
  1129. } catch (error) {
  1130. this.idTypeOptions = [];
  1131. this.$message.error(error.message || "证书类别字典加载失败");
  1132. }
  1133. },
  1134. // 后端返回值(字典 id / 历史文案 / 编码)统一转成选项 value(字典 id),便于下拉回显
  1135. resolveTypeValue(raw) {
  1136. if (raw === null || raw === undefined || raw === "") return "";
  1137. const text = String(raw);
  1138. const hit = this.typeOptions.find(
  1139. (item) =>
  1140. String(item.value) === text ||
  1141. String(item.label) === text ||
  1142. String(item.key) === text,
  1143. );
  1144. return hit ? hit.value : text;
  1145. },
  1146. orgName(id) {
  1147. if (id === null || id === undefined || id === "") return "-";
  1148. return this.orgMap[String(id)] || "-";
  1149. },
  1150. // 展示用:字典 id / 文案 / 编码 统一反查字典文案(兼容历史文本快照)
  1151. typeName(value) {
  1152. if (value === null || value === undefined || value === "") return "-";
  1153. const text = String(value);
  1154. const hit = this.typeOptions.find(
  1155. (item) =>
  1156. String(item.value) === text ||
  1157. String(item.label) === text ||
  1158. String(item.key) === text,
  1159. );
  1160. return hit ? hit.label : text;
  1161. },
  1162. // 岗位层级展示:数字(离线兜底 1-5)显示「N级」,文本(序列层级如 P1/T4)原样展示
  1163. levelText(value) {
  1164. if (value === null || value === undefined || value === "") return "";
  1165. return Number(value) ? `${value}级` : value;
  1166. },
  1167. statusName(value) {
  1168. return STATUS_LABELS[value] || value || "-";
  1169. },
  1170. statusClass(value) {
  1171. return {
  1172. DRAFT: "is-inactive",
  1173. PENDING: "is-warning",
  1174. APPROVED: "is-active",
  1175. REJECTED: "is-danger",
  1176. }[value];
  1177. },
  1178. statusTone(value) {
  1179. return {
  1180. DRAFT: "draft",
  1181. PENDING: "pending",
  1182. APPROVED: "approved",
  1183. REJECTED: "rejected",
  1184. }[value];
  1185. },
  1186. statusTagType(value) {
  1187. return {
  1188. DRAFT: "info",
  1189. PENDING: "warning",
  1190. APPROVED: "success",
  1191. REJECTED: "danger",
  1192. }[value];
  1193. },
  1194. operationName(value) {
  1195. return OPERATION_LABELS[value] || value || "";
  1196. },
  1197. canEdit(row) {
  1198. // 未提交与已驳回均可编辑后重新提交
  1199. return ["DRAFT", "REJECTED"].includes(row.status);
  1200. },
  1201. },
  1202. };
  1203. </script>
  1204. <style lang="scss" scoped src="../../../styles/views/positionApplication/index.scss"></style>