index.vue 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123
  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.resignationNo" 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-input v-model.trim="draftFilters.documentCode" clearable placeholder="请输入" />
  14. </el-form-item>
  15. </el-col>
  16. <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
  17. <el-form-item label="员工">
  18. <el-select v-model="draftFilters.userId" clearable filterable remote reserve-keyword
  19. :remote-method="searchFilterEmployees" :loading="filterEmployeeLoading" placeholder="可搜索员工姓名"
  20. style="width: 100%">
  21. <el-option v-for="item in filterEmployeeOptions" :key="item.id" :label="employeeLabel(item)"
  22. :value="item.id" />
  23. </el-select>
  24. </el-form-item>
  25. </el-col>
  26. <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
  27. <el-form-item label="审批状态">
  28. <el-select v-model="draftFilters.approvalStatus" clearable placeholder="请选择" style="width: 100%">
  29. <el-option v-for="item in approvalStatusOptions" :key="item.value" :label="item.label"
  30. :value="item.value" />
  31. </el-select>
  32. </el-form-item>
  33. </el-col>
  34. <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
  35. <el-form-item label="交接状态">
  36. <el-select v-model="draftFilters.handoverStatus" clearable placeholder="请选择" style="width: 100%">
  37. <el-option v-for="item in handoverStatusOptions" :key="item.value" :label="item.label"
  38. :value="item.value" />
  39. </el-select>
  40. </el-form-item>
  41. </el-col>
  42. <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
  43. <el-form-item label="交接方式">
  44. <el-select v-model="draftFilters.handoverType" clearable placeholder="请选择" style="width: 100%">
  45. <el-option v-for="item in handoverTypeOptions" :key="item.value" :label="item.label"
  46. :value="item.value" />
  47. </el-select>
  48. </el-form-item>
  49. </el-col>
  50. <el-col v-bind="styleResponsive ? { lg: 12, md: 24 } : { span: 12 }">
  51. <el-form-item label="交接日期">
  52. <el-date-picker v-model="draftFilters.dateRange" type="daterange" value-format="yyyy-MM-dd"
  53. range-separator="至" start-placeholder="请选择" end-placeholder="请选择" style="width: 100%" />
  54. </el-form-item>
  55. </el-col>
  56. <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
  57. <div class="ele-form-actions">
  58. <el-button type="primary" icon="el-icon-search" class="ele-btn-icon" @click="applyFilters">
  59. 查询
  60. </el-button>
  61. <el-button type="primary" icon="el-icon-refresh" class="ele-btn-icon" @click="resetFilters">
  62. 重置
  63. </el-button>
  64. </div>
  65. </el-col>
  66. </el-row>
  67. </el-form>
  68. <ele-pro-table ref="table" row-key="id" :columns="columns" :datasource="fetchPage" :need-page="true"
  69. :page-size="20" :cache-key="$tableCacheKey('hr-resignation-handover')" class="resignation-table">
  70. <template v-slot:toolbar>
  71. <el-button v-if="$hasPermission('main:hrHandover:save')" type="primary" icon="el-icon-plus"
  72. class="ele-btn-icon" @click="openGenerate">
  73. 新建
  74. </el-button>
  75. <el-button v-if="$hasPermission('main:hrHandover:export')" icon="el-icon-download"
  76. class="ele-btn-icon" @click="exportData">
  77. 导出
  78. </el-button>
  79. </template>
  80. <!-- 点单据编码开详情,替代原操作列「查看」入口 -->
  81. <template v-slot:documentCode="{ row }">
  82. <el-link v-if="row.documentCode" type="primary" :underline="false" @click="openDetail(row)">
  83. {{ row.documentCode }}
  84. </el-link>
  85. <span v-else>-</span>
  86. </template>
  87. <template v-slot:person="{ row }">
  88. {{ row.employee || "-" }}
  89. </template>
  90. <template v-slot:handoverPerson="{ row }">
  91. {{ row.handoverUserName || "待指定" }}
  92. </template>
  93. <template v-slot:handoverDate="{ row }">
  94. {{ row.handoverDate || "-" }}
  95. </template>
  96. <template v-slot:handoverType="{ row }">
  97. <el-tag v-if="row.handoverTypeLabel" size="mini" type="info">
  98. {{ row.handoverTypeLabel }}
  99. </el-tag>
  100. <span v-else>-</span>
  101. </template>
  102. <!-- 审批状态:实心浅色 tag,表示流程审批结果 -->
  103. <template v-slot:approval="{ row }">
  104. <el-tag v-if="row.approvalStatusLabel" size="mini" effect="light" :type="approvalTagType(row.approvalStatus)">
  105. {{ row.approvalStatusLabel }}
  106. </el-tag>
  107. <span v-else>-</span>
  108. </template>
  109. <!-- 交接状态:描边 tag,表示交接办理进度,与审批状态的实心样式区分 -->
  110. <template v-slot:status="{ row }">
  111. <el-tag v-if="row.handoverStatusLabel" size="mini" effect="plain" class="handover-status-tag"
  112. :type="handoverStatusTagType(row.handoverStatus)">
  113. {{ row.handoverStatusLabel }}
  114. </el-tag>
  115. <span v-else>-</span>
  116. </template>
  117. <template v-slot:createTime="{ row }">
  118. <div v-if="row.createTime" class="datetime-cell">
  119. <span>{{ dateTimeDatePart(row.createTime) }}</span>
  120. <span>{{ dateTimeTimePart(row.createTime) }}</span>
  121. </div>
  122. <span v-else>-</span>
  123. </template>
  124. <template v-slot:updateTime="{ row }">
  125. <div v-if="row.updateTime" class="datetime-cell">
  126. <span>{{ dateTimeDatePart(row.updateTime) }}</span>
  127. <span>{{ dateTimeTimePart(row.updateTime) }}</span>
  128. </div>
  129. <span v-else>-</span>
  130. </template>
  131. <template v-slot:action="{ row }">
  132. <el-link v-if="canEdit(row) && $hasPermission('main:hrHandover:update')" type="primary"
  133. :underline="false" icon="el-icon-edit" @click="openEdit(row)">
  134. 编辑
  135. </el-link>
  136. <el-link v-if="canLaunch(row) && $hasPermission('main:hrHandover:submit')" type="primary"
  137. :underline="false" icon="el-icon-s-promotion" @click="launchApproval(row)">
  138. 提交
  139. </el-link>
  140. <el-link v-if="canShowApprovalRecord(row)" type="primary" :underline="false" icon="el-icon-tickets"
  141. @click="openApprovalRecord(row)">
  142. 审批记录
  143. </el-link>
  144. <el-link v-if="canDelete(row) && $hasPermission('main:hrHandover:delete')" type="primary"
  145. :underline="false" icon="el-icon-delete" :disabled="submitting" @click="removeRow(row)">
  146. 删除
  147. </el-link>
  148. <el-link v-if="canArchive(row) && $hasPermission('main:hrHandover:archive')" type="primary"
  149. :underline="false" icon="el-icon-archive" :disabled="submitting" @click="handleCommand('archive', row)">
  150. 归档
  151. </el-link>
  152. <!-- <el-link v-if="canForce(row)" type="danger" :underline="false" icon="el-icon-warning-outline"
  153. :disabled="submitting" @click="handleCommand('force', row)">
  154. 强制办结
  155. </el-link> -->
  156. </template>
  157. <template slot="empty">
  158. <el-empty :description="emptyText" />
  159. </template>
  160. </ele-pro-table>
  161. </el-card>
  162. <!-- 创建 / 编辑离职交接单:与离职申请页「发起交接单」共用同一抽屉组件 -->
  163. <HandoverGenerateDialog :visible.sync="generateVisible" :row="generateRow" @success="reloadTable" />
  164. <!-- 详情 -->
  165. <ele-modal title="离职交接单详情" :visible="detailVisible" width="900px" custom-class="resignation-detail-modal"
  166. :close-on-click-modal="false" append-to-body @update:visible="detailVisible = $event">
  167. <div v-if="detailRow" class="detail-modal-body" v-loading="detailLoading">
  168. <div class="detail-hero">
  169. <div class="detail-avatar">
  170. {{ (detailRow.employee || "?").slice(-1) }}
  171. </div>
  172. <div class="detail-hero-main">
  173. <h2>{{ detailRow.employee || detailRow.resignationNo || "-" }}</h2>
  174. <p>
  175. {{ detailRow.resignationNo || "-" }} ·
  176. {{ detailRow.department || "-" }}
  177. </p>
  178. </div>
  179. <span class="detail-status" :class="handoverClass(detailRow)">
  180. <i></i>{{ detailRow.handoverStatusLabel || "未开始" }}
  181. </span>
  182. </div>
  183. <div class="detail-stat-grid">
  184. <div>
  185. <span>单据编码</span>
  186. <strong>{{ detailRow.documentCode || "尚未生成" }}</strong>
  187. </div>
  188. <div>
  189. <span>交接方式</span>
  190. <strong>{{ detailRow.handoverTypeLabel || "-" }}</strong>
  191. </div>
  192. <div>
  193. <span>交接人</span>
  194. <strong>{{ detailRow.handoverUserName || "待指定" }}</strong>
  195. </div>
  196. <div>
  197. <span>交接日期</span>
  198. <strong>{{ detailRow.handoverDate || "-" }}</strong>
  199. </div>
  200. <div>
  201. <span>所属组织</span>
  202. <strong>{{ detailRow.deptName || detailRow.department || "-" }}</strong>
  203. </div>
  204. <div>
  205. <span>审批状态</span>
  206. <strong>{{ detailRow.approvalStatusLabel || "-" }}</strong>
  207. </div>
  208. <div>
  209. <span>交接进度</span>
  210. <strong>{{ detailRow.progress || 0 }}%</strong>
  211. </div>
  212. <div>
  213. <span>创建时间</span>
  214. <strong>{{ detailRow.createTime || "-" }}</strong>
  215. </div>
  216. </div>
  217. <div class="detail-section-card">
  218. <div class="detail-section-head">
  219. <h3>四类交接状态</h3>
  220. </div>
  221. <div class="category-status">
  222. <em>工作:{{ categoryLabel(detailRow.workStatus) }}</em>
  223. <em>资产:{{ categoryLabel(detailRow.assetStatus) }}</em>
  224. <em>财务:{{ categoryLabel(detailRow.financeStatus) }}</em>
  225. <em>HR:{{ categoryLabel(detailRow.hrStatus) }}</em>
  226. </div>
  227. </div>
  228. <div class="detail-section-card" style="margin-top: 14px">
  229. <div class="detail-section-head">
  230. <h3>资产归还清单</h3>
  231. <span>{{ detailReturnRows.length }} 项</span>
  232. </div>
  233. <el-table v-loading="detailAssetLoading" :data="detailReturnRows" size="mini" border class="detail-table"
  234. empty-text="暂无资产归还清单">
  235. <el-table-column type="index" label="序号" width="60" align="center" />
  236. <el-table-column label="物品名称" min-width="150">
  237. <template slot-scope="scope">
  238. {{ scope.row.itemName || "-" }}
  239. </template>
  240. </el-table-column>
  241. <el-table-column label="资产编码" min-width="140">
  242. <template slot-scope="scope">
  243. {{ scope.row.assetCode || "-" }}
  244. </template>
  245. </el-table-column>
  246. <el-table-column label="归还状态" width="110" align="center">
  247. <template slot-scope="scope">
  248. <el-tag v-if="!scope.row.inReturnList" size="mini" type="info">
  249. 未纳入清单
  250. </el-tag>
  251. <el-tag v-else size="mini" :type="scope.row.returned ? 'success' : 'warning'">
  252. {{
  253. scope.row.returnStatusLabel ||
  254. (scope.row.returnStatus === "RETURNED" ? "已归还" : "未归还")
  255. }}
  256. </el-tag>
  257. </template>
  258. </el-table-column>
  259. <el-table-column label="归还时间" width="160">
  260. <template slot-scope="scope">
  261. {{ scope.row.returnedTime || "-" }}
  262. </template>
  263. </el-table-column>
  264. <el-table-column label="归还备注" min-width="150">
  265. <template slot-scope="scope">
  266. {{ scope.row.returnRemark || "-" }}
  267. </template>
  268. </el-table-column>
  269. </el-table>
  270. <p class="detail-description">
  271. {{
  272. detailAssetLoading
  273. ? "正在加载该员工入职物品领用记录…"
  274. : detailReturnRows.length
  275. ? `领用单号 ${detailReceiptCode || "-"};共 ${detailReturnRows.length} 项,其中 ${detailReturnRows.filter((row) =>
  276. row.inReturnList).length
  277. } 项已生成归还清单。`
  278. : "该员工暂无入职物品领用记录,且当前交接单没有归还清单"
  279. }}
  280. </p>
  281. </div>
  282. <div class="detail-section-card" style="margin-top: 14px">
  283. <div class="detail-section-head">
  284. <h3>交接事项</h3>
  285. <span>{{ (detailRow.items || []).length }} 项</span>
  286. </div>
  287. <el-progress :percentage="detailRow.progress || 0" :stroke-width="8" />
  288. <el-table :data="detailRow.items || []" size="mini" border class="detail-table" style="margin-top: 12px"
  289. empty-text="当前交接单没有交接事项">
  290. <el-table-column type="index" label="序号" width="60" align="center" />
  291. <el-table-column label="交接分类" width="100" align="center">
  292. <template slot-scope="scope">
  293. <el-tag size="mini" type="info">
  294. {{ scope.row.categoryLabel || scope.row.itemCategory || "-" }}
  295. </el-tag>
  296. </template>
  297. </el-table-column>
  298. <el-table-column label="事项名称" min-width="140">
  299. <template slot-scope="scope">
  300. {{ scope.row.itemName || "-" }}
  301. </template>
  302. </el-table-column>
  303. <el-table-column label="事项说明" min-width="170">
  304. <template slot-scope="scope">
  305. {{ scope.row.itemContent || "-" }}
  306. </template>
  307. </el-table-column>
  308. <el-table-column label="接收人" width="110">
  309. <template slot-scope="scope">
  310. {{ receiverName(scope.row) || "-" }}
  311. </template>
  312. </el-table-column>
  313. <el-table-column label="状态" width="92" align="center">
  314. <template slot-scope="scope">
  315. <el-tag size="mini" :type="itemStatusTagType(scope.row.itemStatus)">
  316. {{ scope.row.itemStatusLabel || "待交接" }}
  317. </el-tag>
  318. </template>
  319. </el-table-column>
  320. <el-table-column label="完成说明" min-width="140">
  321. <template slot-scope="scope">
  322. {{ scope.row.completionRemark || "-" }}
  323. </template>
  324. </el-table-column>
  325. <el-table-column label="操作" width="118" align="center">
  326. <template slot-scope="scope">
  327. <el-button v-if="canCompleteItem(scope.row) && $hasPermission('main:hrHandover:complete')"
  328. type="text" size="mini" :loading="acting" @click="completeItem(scope.row)">
  329. 完成
  330. </el-button>
  331. <el-button v-if="canRollbackItem(scope.row) && $hasPermission('main:hrHandover:rollback')"
  332. type="text" size="mini" :loading="acting" @click="rollbackItem(scope.row)">
  333. 回退
  334. </el-button>
  335. <span v-if="
  336. !canCompleteItem(scope.row) && !canRollbackItem(scope.row)
  337. " class="sub-text">
  338. -
  339. </span>
  340. </template>
  341. </el-table-column>
  342. </el-table>
  343. </div>
  344. <div v-if="detailRow.remark" class="detail-section-card" style="margin-top: 14px">
  345. <div class="detail-section-head">
  346. <h3>交接说明</h3>
  347. </div>
  348. <p class="detail-description">{{ detailRow.remark }}</p>
  349. </div>
  350. </div>
  351. <template v-slot:footer>
  352. <el-button @click="detailVisible = false">关闭</el-button>
  353. </template>
  354. </ele-modal>
  355. <!-- BPM 公共审批发起弹框 -->
  356. <processSubmitDialog :processSubmitDialogFlag.sync="processSubmitDialogFlag" v-if="processSubmitDialogFlag"
  357. ref="processSubmitDialogRef" @reload="onProcessSubmitReload" />
  358. <!-- 审批流程详情弹框 -->
  359. <BpmProcessDetailDialog ref="approvalRecordDialog" />
  360. </div>
  361. </template>
  362. <script>
  363. import {
  364. HANDOVER_APPROVAL_STATUS_OPTIONS,
  365. HANDOVER_APPROVAL_BUSINESS_KEY,
  366. HANDOVER_STATUS_LABELS,
  367. HANDOVER_STATUS_OPTIONS,
  368. HANDOVER_TYPE_OPTIONS,
  369. archiveResignation,
  370. canArchiveResignation,
  371. canCompleteHandoverItem,
  372. canDeleteHandover,
  373. canEditHandover,
  374. canForceCompleteResignation,
  375. canLaunchHandover,
  376. canRollbackHandoverItem,
  377. canShowHandoverApprovalRecord,
  378. completeResignationHandoverItem,
  379. composeAssetReturnRows,
  380. dateTimeDatePart,
  381. dateTimeTimePart,
  382. deleteHandover,
  383. forceCompleteResignation,
  384. getHandoverById,
  385. getHandoverPage,
  386. handoverDisplayStatus,
  387. rollbackHandoverItem,
  388. } from "@/api/hr/resignation";
  389. import {
  390. decorateReceiptDetailsForHandover,
  391. getLatestItemReceipt,
  392. } from "@/api/hr/onboardingItemReceipt";
  393. import { showRequestError } from "@/utils/message";
  394. import { getCurrentUser } from "@/utils/token-util";
  395. import {
  396. adaptEmployee,
  397. getEmployeeById,
  398. getUserPage,
  399. searchUsersByKeyword,
  400. } from "@/api/organization";
  401. import processSubmitDialog from "@/BIZComponents/processSubmitDialog/processSubmitDialog.vue";
  402. import BpmProcessDetailDialog from "@/views/bpm/processInstance/detailDialog.vue";
  403. import HandoverGenerateDialog from "@/views/hrManagement/resignationHandoverOrder/HandoverGenerateDialog.vue";
  404. import submitGuard from "@/mixins/submit-guard";
  405. /** 员工下拉统一结构:{ id, name, employeeNo, department } */
  406. function normalizeEmployee(raw) {
  407. const employee = adaptEmployee(raw);
  408. return {
  409. id: String(employee.id),
  410. name: employee.name || employee.employeeName || "",
  411. employeeNo: employee.employeeNo || employee.jobNumber || "",
  412. department: employee.deptName || employee.department || "",
  413. };
  414. }
  415. export default {
  416. name: "ResignationHandoverOrder",
  417. components: {
  418. processSubmitDialog,
  419. BpmProcessDetailDialog,
  420. HandoverGenerateDialog,
  421. },
  422. mixins: [submitGuard],
  423. data() {
  424. return {
  425. loading: false,
  426. acting: false,
  427. detailLoading: false,
  428. // 详情「资产归还清单」:最新领用物品与已生成归还清单合并后的行
  429. detailReturnRows: [],
  430. detailReceiptCode: "",
  431. detailAssetLoading: false,
  432. totalCount: 0,
  433. exportRows: [],
  434. draftFilters: {
  435. resignationNo: "",
  436. documentCode: "",
  437. userId: "",
  438. approvalStatus: "",
  439. handoverStatus: "",
  440. handoverType: "",
  441. dateRange: [],
  442. },
  443. filters: {
  444. resignationNo: "",
  445. documentCode: "",
  446. userId: "",
  447. approvalStatus: "",
  448. handoverStatus: "",
  449. handoverType: "",
  450. dateRange: [],
  451. },
  452. generateVisible: false,
  453. // 编辑中的交接单行;为空即创建(创建/编辑抽屉与离职申请页「发起交接单」共用组件)
  454. generateRow: null,
  455. // 详情接收人姓名回显用的员工缓存
  456. employeeOptions: [],
  457. filterEmployeeOptions: [],
  458. filterEmployeeLoading: false,
  459. detailVisible: false,
  460. detailRow: null,
  461. approvalStatusOptions: HANDOVER_APPROVAL_STATUS_OPTIONS,
  462. handoverStatusOptions: HANDOVER_STATUS_OPTIONS,
  463. handoverTypeOptions: HANDOVER_TYPE_OPTIONS,
  464. processSubmitDialogFlag: false,
  465. currentLaunchRow: null,
  466. };
  467. },
  468. computed: {
  469. styleResponsive() {
  470. return this.$store.state.theme?.styleResponsive !== false;
  471. },
  472. emptyText() {
  473. return "暂无交接单";
  474. },
  475. columns() {
  476. return [
  477. { type: "index", label: "序号", width: 60, align: "center" },
  478. {
  479. prop: "documentCode",
  480. label: "单据编码",
  481. minWidth: 250,
  482. align: "left",
  483. showOverflowTooltip: true,
  484. slot: "documentCode",
  485. },
  486. {
  487. prop: "resignationNo",
  488. label: "离职单号",
  489. minWidth: 250,
  490. align: "left",
  491. showOverflowTooltip: true,
  492. formatter: (_r, _c, value) => value || "-",
  493. },
  494. {
  495. prop: "employee",
  496. label: "员工",
  497. minWidth: 90,
  498. align: "left",
  499. slot: "person",
  500. },
  501. {
  502. /* 部门 / 机构只认交接单接口原始字段 groupName、deptName(adaptHandover ...raw 透传),
  503. 没返回就显示 -,不取员工档案等其他来源顶替 */
  504. prop: "groupName",
  505. label: "所属机构",
  506. minWidth: 180,
  507. align: "left",
  508. showOverflowTooltip: true,
  509. formatter: (_r, _c, value) => value || "-",
  510. },
  511. {
  512. prop: "deptName",
  513. label: "所属部门",
  514. minWidth: 150,
  515. align: "left",
  516. showOverflowTooltip: true,
  517. formatter: (_r, _c, value) => value || "-",
  518. },
  519. {
  520. prop: "handoverUserName",
  521. label: "交接人",
  522. width: 100,
  523. align: "left",
  524. slot: "handoverPerson",
  525. },
  526. {
  527. prop: "handoverDate",
  528. label: "交接日期",
  529. width: 110,
  530. align: "left",
  531. slot: "handoverDate",
  532. },
  533. {
  534. prop: "handoverType",
  535. label: "交接方式",
  536. width: 100,
  537. align: "left",
  538. slot: "handoverType",
  539. },
  540. {
  541. prop: "approvalStatus",
  542. label: "审批状态",
  543. width: 100,
  544. align: "left",
  545. slot: "approval",
  546. },
  547. {
  548. prop: "handoverStatus",
  549. label: "交接状态",
  550. width: 100,
  551. align: "left",
  552. slot: "status",
  553. },
  554. {
  555. prop: "createUserName",
  556. label: "创建人",
  557. width: 90,
  558. align: "left",
  559. formatter: (_r, _c, v) => v || "-",
  560. },
  561. {
  562. prop: "createTime",
  563. label: "创建时间",
  564. width: 112,
  565. align: "left",
  566. slot: "createTime",
  567. },
  568. {
  569. prop: "updateUserName",
  570. label: "修改人",
  571. width: 90,
  572. align: "left",
  573. formatter: (_r, _c, v) => v || "-",
  574. },
  575. {
  576. prop: "updateTime",
  577. label: "修改时间",
  578. width: 112,
  579. align: "left",
  580. slot: "updateTime",
  581. },
  582. {
  583. columnKey: "action",
  584. label: "操作",
  585. minWidth: 300,
  586. fixed: "right",
  587. align: "left",
  588. slot: "action",
  589. },
  590. ];
  591. },
  592. },
  593. methods: {
  594. canArchive: canArchiveResignation,
  595. canForce: canForceCompleteResignation,
  596. canCompleteItem: canCompleteHandoverItem,
  597. canRollbackItem: canRollbackHandoverItem,
  598. canDelete: canDeleteHandover,
  599. canEdit: canEditHandover,
  600. canLaunch: canLaunchHandover,
  601. canShowApprovalRecord: canShowHandoverApprovalRecord,
  602. dateTimeDatePart,
  603. dateTimeTimePart,
  604. handoverClass: handoverDisplayStatus,
  605. /** 员工下拉 label:姓名 · 工号 · 部门 */
  606. employeeLabel(item = {}) {
  607. const name = item.name || item.employeeName || "";
  608. const empNo = item.employeeNo ? " · " + item.employeeNo : "";
  609. const dept = item.department ? " · " + item.department : "";
  610. return name + empNo + dept;
  611. },
  612. categoryLabel(value) {
  613. return (
  614. HANDOVER_STATUS_LABELS[String(value || "").toUpperCase()] ||
  615. value ||
  616. "未开始"
  617. );
  618. },
  619. /** 交接明细状态 tag 颜色 */
  620. itemStatusTagType(code) {
  621. const key = String(code || "").toUpperCase();
  622. if (["COMPLETED", "DONE", "FINISHED"].includes(key)) return "success";
  623. if (["IN_PROGRESS", "PROCESSING"].includes(key)) return "warning";
  624. return "info";
  625. },
  626. /** 总体交接状态 tag 颜色 */
  627. handoverStatusTagType(code) {
  628. const key = String(code || "").toUpperCase();
  629. if (["COMPLETED", "FORCE_COMPLETED"].includes(key)) return "success";
  630. if (["IN_PROGRESS", "PROCESSING"].includes(key)) return "warning";
  631. if (["PENDING", "NOT_STARTED"].includes(key)) return "";
  632. return "info";
  633. },
  634. /** 审批状态 tag 颜色:已通过 success / 驳回、撤回、取消 danger / 审批中 warning / 草稿 info */
  635. approvalTagType(code) {
  636. const key = String(code || "").toLowerCase();
  637. if (["approved", "passed"].includes(key)) return "success";
  638. if (["rejected", "cancelled", "withdrawn"].includes(key)) return "danger";
  639. if (["pending", "approving", "waiting", "reviewing"].includes(key)) {
  640. return "warning";
  641. }
  642. return "info";
  643. },
  644. /** 详情里接收人姓名:优先取已加载的员工缓存 */
  645. receiverName(item = {}) {
  646. if (!item.receiverUserId) return "";
  647. const matched = this.employeeOptions.find(
  648. (employee) => String(employee.id) === String(item.receiverUserId),
  649. );
  650. return matched?.name || "";
  651. },
  652. queryParams(pageNum, size, extra = {}) {
  653. const range = this.filters.dateRange || [];
  654. return {
  655. pageNum,
  656. size,
  657. offset: (Number(pageNum) - 1) * Number(size),
  658. resignationNo: this.filters.resignationNo || undefined,
  659. documentCode: this.filters.documentCode || undefined,
  660. userId: this.filters.userId || undefined,
  661. approvalStatus: this.filters.approvalStatus || undefined,
  662. handoverStatus: this.filters.handoverStatus || undefined,
  663. handoverType: this.filters.handoverType || undefined,
  664. handoverDateStart: range[0],
  665. handoverDateEnd: range[1],
  666. sortName: "createTime",
  667. orderBy: "descending",
  668. ...extra,
  669. };
  670. },
  671. applyFilters() {
  672. this.filters = {
  673. resignationNo: this.draftFilters.resignationNo,
  674. documentCode: this.draftFilters.documentCode,
  675. userId: this.draftFilters.userId,
  676. approvalStatus: this.draftFilters.approvalStatus,
  677. handoverStatus: this.draftFilters.handoverStatus,
  678. handoverType: this.draftFilters.handoverType,
  679. dateRange: this.draftFilters.dateRange
  680. ? [...this.draftFilters.dateRange]
  681. : [],
  682. };
  683. this.reloadTable();
  684. },
  685. resetFilters() {
  686. const empty = {
  687. resignationNo: "",
  688. documentCode: "",
  689. userId: "",
  690. approvalStatus: "",
  691. handoverStatus: "",
  692. handoverType: "",
  693. dateRange: [],
  694. };
  695. this.draftFilters = { ...empty };
  696. this.filters = { ...empty };
  697. this.reloadTable();
  698. },
  699. reloadTable() {
  700. this.$nextTick(() => this.$refs.table?.reload({ page: 1 }));
  701. },
  702. refresh() {
  703. this.reloadTable();
  704. },
  705. async fetchPage({ page, limit }) {
  706. try {
  707. const pageNum = Number(page) || 1;
  708. const size = Number(limit) || 20;
  709. const data = await getHandoverPage(this.queryParams(pageNum, size));
  710. this.totalCount = Number(data?.count ?? 0);
  711. this.exportRows = data?.list || [];
  712. return { list: data?.list || [], count: this.totalCount };
  713. } catch (error) {
  714. showRequestError(error, "离职交接单列表加载失败");
  715. this.totalCount = 0;
  716. this.exportRows = [];
  717. return { list: [], count: 0 };
  718. }
  719. },
  720. /** 列表筛选用员工下拉(独立缓存,避免与弹框混用) */
  721. async searchFilterEmployees(keyword = "") {
  722. const text = String(keyword || "").trim();
  723. this.filterEmployeeLoading = true;
  724. try {
  725. const data = text
  726. ? await searchUsersByKeyword(text, { pageNum: 1, size: 50 })
  727. : await getUserPage({ pageNum: 1, size: 50 });
  728. const list = (Array.isArray(data?.list) ? data.list : []).map(
  729. normalizeEmployee,
  730. );
  731. const merged = new Map(
  732. this.filterEmployeeOptions.map((item) => [String(item.id), item]),
  733. );
  734. if (text) {
  735. const selectedId = String(this.draftFilters.userId || "");
  736. [...merged.keys()].forEach((key) => {
  737. if (key !== selectedId) merged.delete(key);
  738. });
  739. if (!list.length) {
  740. this.$message.warning(`未搜索到「${text}」匹配的员工`);
  741. }
  742. }
  743. list.forEach((item) => {
  744. if (item?.id) merged.set(String(item.id), item);
  745. });
  746. this.filterEmployeeOptions = [...merged.values()];
  747. } catch (error) {
  748. /* 忽略,保留已有选项 */
  749. } finally {
  750. this.filterEmployeeLoading = false;
  751. }
  752. },
  753. mergeEmployees(list = []) {
  754. const merged = new Map(
  755. this.employeeOptions.map((item) => [String(item.id), item]),
  756. );
  757. list.forEach((item) => {
  758. if (item?.id) merged.set(String(item.id), item);
  759. });
  760. this.employeeOptions = [...merged.values()];
  761. },
  762. /** 详情/编辑回显时补齐缺失的员工姓名(下拉需要 option 才能显示 label) */
  763. async ensureEmployees(userIds = []) {
  764. const missing = [...new Set(userIds.map(String))].filter(
  765. (id) =>
  766. id &&
  767. id !== "undefined" &&
  768. !this.employeeOptions.some((item) => String(item.id) === id),
  769. );
  770. if (!missing.length) return;
  771. const loaded = await Promise.all(
  772. missing.map(async (id) => {
  773. try {
  774. return normalizeEmployee(await getEmployeeById(id));
  775. } catch (error) {
  776. return null;
  777. }
  778. }),
  779. );
  780. this.mergeEmployees(loaded.filter(Boolean));
  781. },
  782. /** 创建:打开与离职申请页「发起交接单」共用的抽屉组件,由组件内部加载可选离职申请 */
  783. openGenerate() {
  784. this.generateRow = null;
  785. this.generateVisible = true;
  786. },
  787. /** 编辑:打开同一抽屉组件的编辑态,详情回填与保存均由组件承担 */
  788. openEdit(row) {
  789. if (!row?.id) return;
  790. this.generateRow = row;
  791. this.generateVisible = true;
  792. },
  793. async openDetail(row) {
  794. if (!row?.id) {
  795. this.$message.warning("缺少交接单主键,无法查看详情");
  796. return;
  797. }
  798. this.detailVisible = true;
  799. this.detailLoading = true;
  800. this.detailRow = row;
  801. this.detailReturnRows = [];
  802. this.detailReceiptCode = "";
  803. try {
  804. const detail = await getHandoverById(row.id);
  805. this.detailRow = detail;
  806. if (detail?.handoverUserId) this.ensureEmployees([detail.handoverUserId]);
  807. const receiverIds = (detail?.items || [])
  808. .map((item) => item.receiverUserId)
  809. .filter(Boolean);
  810. if (receiverIds.length) this.ensureEmployees(receiverIds);
  811. await this.loadDetailReturnRows(detail);
  812. } catch (error) {
  813. showRequestError(error, "交接单详情加载失败");
  814. } finally {
  815. this.detailLoading = false;
  816. }
  817. },
  818. /**
  819. * 详情「资产归还清单」与编辑态口径一致:
  820. * 详情返回的归还清单 + 该员工最新入职领用物品合并(GET /main/onboarding-item-receipts/latest/{employeeId}),
  821. * 保证清单尚未生成时也能看到领用物品行(标记「未纳入清单」)
  822. */
  823. async loadDetailReturnRows(detail) {
  824. const returnList = detail?.assetReturns || [];
  825. this.detailReturnRows = composeAssetReturnRows(returnList);
  826. this.detailReceiptCode = "";
  827. const employeeId = detail?.userId;
  828. if (!employeeId) return;
  829. this.detailAssetLoading = true;
  830. try {
  831. const receipt = await getLatestItemReceipt(employeeId);
  832. this.detailReceiptCode = receipt?.documentCode || "";
  833. this.detailReturnRows = composeAssetReturnRows(
  834. returnList,
  835. decorateReceiptDetailsForHandover(receipt),
  836. );
  837. } catch (error) {
  838. showRequestError(error, "入职物品领用记录加载失败");
  839. } finally {
  840. this.detailAssetLoading = false;
  841. }
  842. },
  843. /** 打开 BPM 提交审批弹框 */
  844. launchApproval(row) {
  845. if (!row?.id) {
  846. this.$message.warning("请先创建交接单后再提交审批");
  847. return;
  848. }
  849. if (!this.canLaunch(row)) {
  850. this.$message.warning("当前交接单不可提交审批");
  851. return;
  852. }
  853. this.currentLaunchRow = row;
  854. this.processSubmitDialogFlag = true;
  855. this.$nextTick(() => {
  856. const currentUser = getCurrentUser() || {};
  857. this.$refs.processSubmitDialogRef?.init({
  858. businessId: row.id,
  859. businessKey: HANDOVER_APPROVAL_BUSINESS_KEY,
  860. formCreateUserId:
  861. row.createUserId || currentUser.id || currentUser.userId || "",
  862. variables: {
  863. businessCode: row.documentCode || row.resignationNo || "",
  864. businessName: row.employee || row.userName || "",
  865. businessType: "离职交接单",
  866. groupId: row.groupId || row.deptId || "",
  867. groupName: row.deptName || row.company || "",
  868. },
  869. });
  870. });
  871. },
  872. /** 打开审批流程详情弹框:优先列表 row 上的实例 id,没有则拉详情 */
  873. async openApprovalRecord(row) {
  874. let id = row?.processInstanceId || row?.approvalInstanceId;
  875. if (!id && row?.id) {
  876. try {
  877. const detail = await getHandoverById(row.id);
  878. id = detail?.processInstanceId || detail?.approvalInstanceId;
  879. } catch (_) {
  880. // 详情拿不到也没关系,后面统一提示
  881. }
  882. }
  883. if (!id) {
  884. this.$message.warning("暂无审批记录(后端未返回流程实例 id)");
  885. return;
  886. }
  887. this.$refs.approvalRecordDialog?.open({ processInstanceId: id });
  888. },
  889. /** BPM 提交审批弹框回调:刷新列表 */
  890. onProcessSubmitReload() {
  891. this.currentLaunchRow = null;
  892. this.refresh();
  893. },
  894. async promptReason(title, required) {
  895. try {
  896. const { value } = await this.$prompt(
  897. required ? "请填写原因(必填)" : "可填写完成说明,也可留空",
  898. title,
  899. {
  900. confirmButtonText: "确定",
  901. cancelButtonText: "取消",
  902. inputPlaceholder: "原因 / 完成说明",
  903. inputValidator: required
  904. ? (val) => (String(val || "").trim() ? true : "请填写原因")
  905. : undefined,
  906. },
  907. );
  908. return String(value || "").trim();
  909. } catch (error) {
  910. return null;
  911. }
  912. },
  913. async handleCommand(command, row) {
  914. if (command === "archive") {
  915. if (!row?.resignationId) {
  916. this.$message.warning("缺少离职单主键,无法归档");
  917. return;
  918. }
  919. await this.runSubmit(
  920. () =>
  921. this.runAction("归档离职单", () =>
  922. archiveResignation(row.resignationId),
  923. ),
  924. { errorMessage: "归档离职单失败" },
  925. );
  926. return;
  927. }
  928. if (command === "force") {
  929. if (!row?.resignationId) {
  930. this.$message.warning("缺少离职单主键,无法强制办结");
  931. return;
  932. }
  933. await this.runSubmit(
  934. async () => {
  935. const reason = await this.promptReason("强制办结离职单", true);
  936. if (!reason) return;
  937. await this.runAction("强制办结离职单", () =>
  938. forceCompleteResignation(row.resignationId, { reason }),
  939. );
  940. },
  941. { errorMessage: "强制办结离职单失败" },
  942. );
  943. }
  944. },
  945. async removeRow(row) {
  946. await this.runSubmit(
  947. async () => {
  948. await this.$confirm(
  949. "确认删除该交接单?删除后离职单将恢复为未创建交接。",
  950. "删除交接单",
  951. { type: "warning" },
  952. );
  953. await this.runAction("删除离职交接单", () => deleteHandover(row.id));
  954. },
  955. { errorMessage: "删除离职交接单失败" },
  956. );
  957. },
  958. async runAction(name, task) {
  959. this.acting = true;
  960. try {
  961. await task();
  962. this.$message.success(`${name}成功`);
  963. this.reloadTable();
  964. if (this.detailVisible && this.detailRow?.id) {
  965. try {
  966. const detail = await getHandoverById(this.detailRow.id);
  967. this.detailRow = detail;
  968. await this.loadDetailReturnRows(detail);
  969. } catch (error) {
  970. this.detailVisible = false;
  971. }
  972. }
  973. } catch (error) {
  974. showRequestError(error, `${name}失败`);
  975. } finally {
  976. this.acting = false;
  977. }
  978. },
  979. async completeItem(item) {
  980. if (!this.detailRow?.resignationId || !item?.id) {
  981. this.$message.warning("缺少离职单或事项主键,无法完成");
  982. return;
  983. }
  984. const reason = await this.promptReason("完成交接事项", false);
  985. if (reason === null) return;
  986. await this.runAction("完成交接事项", () =>
  987. completeResignationHandoverItem(this.detailRow.resignationId, item.id, {
  988. reason,
  989. }),
  990. );
  991. },
  992. async rollbackItem(item) {
  993. if (!this.detailRow?.id || !item?.id) return;
  994. const reason = await this.promptReason("回退交接事项", true);
  995. if (!reason) return;
  996. await this.runAction("回退交接事项", () =>
  997. rollbackHandoverItem(this.detailRow.id, item.id, { reason }),
  998. );
  999. },
  1000. exportData() {
  1001. const header = [
  1002. "单据编码",
  1003. "离职单号",
  1004. "员工",
  1005. "所属部门",
  1006. "交接人",
  1007. "交接日期",
  1008. "交接方式",
  1009. "审批状态",
  1010. "交接状态",
  1011. "创建人",
  1012. "创建时间",
  1013. "修改人",
  1014. "修改时间",
  1015. ].join(",");
  1016. const body = this.exportRows
  1017. .map((row) =>
  1018. [
  1019. row.documentCode,
  1020. row.resignationNo,
  1021. row.employee,
  1022. row.department,
  1023. row.handoverUserName,
  1024. row.handoverDate,
  1025. row.handoverTypeLabel,
  1026. row.approvalStatusLabel,
  1027. row.handoverStatusLabel,
  1028. row.createUserName,
  1029. row.createTime,
  1030. row.updateUserName,
  1031. row.updateTime,
  1032. ]
  1033. .map((value) => JSON.stringify(value ?? ""))
  1034. .join(","),
  1035. )
  1036. .join("\n");
  1037. const blob = new Blob([`${header}\n${body}`], {
  1038. type: "text/csv;charset=utf-8;",
  1039. });
  1040. const link = document.createElement("a");
  1041. link.href = URL.createObjectURL(blob);
  1042. link.download = "离职交接单.csv";
  1043. link.click();
  1044. URL.revokeObjectURL(link.href);
  1045. },
  1046. },
  1047. };
  1048. </script>
  1049. <style lang="scss" src="@/styles/views/resignation/index.scss"></style>
  1050. <style lang="scss">
  1051. /* 创建 / 编辑交接单抽屉:抽屉 append-to-body,作用域失效,用 custom-class 穿透。
  1052. 通用的 .handover-drawer 外壳样式在 @/styles/views/resignation/index.scss 中共享。 */
  1053. .handover-edit-drawer .handover-items {
  1054. width: 100%;
  1055. }
  1056. .handover-edit-drawer .handover-item-table {
  1057. width: 100%;
  1058. }
  1059. .handover-edit-drawer .handover-item-table .el-form-item {
  1060. margin-bottom: 0;
  1061. }
  1062. .handover-edit-drawer .apply-form .el-form-item {
  1063. margin-bottom: 14px;
  1064. }
  1065. .handover-edit-drawer .form-section .el-form-item:last-child {
  1066. margin-bottom: 0;
  1067. }
  1068. /* 详情弹框内的明细表格 */
  1069. .resignation-detail-modal .detail-table th.el-table__cell {
  1070. padding: 7px 0;
  1071. background: #f7f9fc;
  1072. }
  1073. .resignation-detail-modal .detail-table td.el-table__cell {
  1074. padding: 7px 0;
  1075. }
  1076. .resignation-detail-modal .detail-table {
  1077. font-size: 14px;
  1078. }
  1079. /* .category-status 原本挂在 .resignation-page 下,详情弹框 apped-to-body 后需要补一份 */
  1080. .resignation-detail-modal .category-status {
  1081. display: flex;
  1082. flex-wrap: wrap;
  1083. gap: 6px;
  1084. }
  1085. .resignation-detail-modal .category-status em {
  1086. padding: 3px 9px;
  1087. border-radius: 8px;
  1088. color: #65758b;
  1089. background: #f3f6fb;
  1090. font-size: 14px;
  1091. font-style: normal;
  1092. }
  1093. /* 交接状态 tag:描边 + 胶囊圆角,与审批状态的实心方角 tag 在填充和形状上双重区分 */
  1094. .handover-status-tag {
  1095. border-radius: 10px;
  1096. font-weight: 600;
  1097. }
  1098. </style>