index.vue 46 KB

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