candidateList.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. <template>
  2. <div class="app-container backcheck-candidate-page">
  3. <div class="bg-white p-4 rounded main-box">
  4. <!-- 顶部工具栏 -->
  5. <div class="header-bar flex justify-between items-center mb-4">
  6. <el-input v-model="queryParams.keyword" placeholder="请输入姓名/手机号" style="width: 320px" clearable @keyup.enter="handleQuery">
  7. <template #append>
  8. <el-button type="primary" @click="handleQuery">
  9. <el-icon><i-ep-search /></el-icon>
  10. </el-button>
  11. </template>
  12. </el-input>
  13. <div class="flex gap-2">
  14. <el-button type="primary" @click="handleSync">同步至单位</el-button>
  15. <el-button type="primary" plain @click="handleExport">导出列表</el-button>
  16. </div>
  17. </div>
  18. <!-- 数据表格 -->
  19. <el-table
  20. v-loading="loading"
  21. :data="filteredTableData"
  22. style="width: 100%"
  23. header-cell-class-name="custom-header"
  24. class="custom-table"
  25. @selection-change="handleSelectionChange"
  26. >
  27. <el-table-column type="selection" width="50" align="center" />
  28. <el-table-column label="姓名" min-width="160">
  29. <template #default="scope">
  30. <div class="flex flex-col">
  31. <el-link type="primary" :underline="false" @click="handleDetail(scope.row)">
  32. {{ scope.row.name }}
  33. </el-link>
  34. <span class="text-xs text-gray-400">{{ scope.row.studentNo }}</span>
  35. </div>
  36. </template>
  37. </el-table-column>
  38. <el-table-column prop="idType" label="身份证类型" min-width="140" />
  39. <el-table-column label="身份证号码" min-width="140">
  40. <template #default="scope">
  41. <div class="flex items-center gap-2">
  42. <span>{{ scope.row.idNoDisplay }}</span>
  43. <el-icon
  44. v-if="scope.row.idNo && scope.row.idNo !== '****'"
  45. :class="['cursor-pointer', scope.row.showIdNo ? 'text-primary' : 'text-gray-400']"
  46. @click="toggleIdNo(scope.row)"
  47. >
  48. <i-ep-view v-if="!scope.row.showIdNo" />
  49. <i-ep-hide v-else />
  50. </el-icon>
  51. </div>
  52. </template>
  53. </el-table-column>
  54. <el-table-column prop="mobile" label="手机号" min-width="140" />
  55. <el-table-column label="状态" min-width="100">
  56. <template #default="scope">
  57. <div class="flex items-center">
  58. <span :class="['status-dot', scope.row.status === '完成' ? 'dot-success' : 'dot-danger']"></span>
  59. <span>{{ scope.row.status }}</span>
  60. </div>
  61. </template>
  62. </el-table-column>
  63. <el-table-column label="系统内供职公司" min-width="280">
  64. <template #default="scope">
  65. <div class="flex items-center gap-2 flex-wrap">
  66. <template v-if="scope.row.companies && scope.row.companies.length > 0">
  67. <template v-for="(comp, index) in scope.row.companies" :key="index">
  68. <div class="company-tag-wrapper">
  69. <span class="company-name">{{ comp.name }}</span>
  70. <span :class="['tag-status', getStatusClass(comp.status)]" @click="handleTagClick(comp, scope.row)">
  71. {{ comp.status }}
  72. </span>
  73. </div>
  74. </template>
  75. </template>
  76. <span v-else>-</span>
  77. </div>
  78. </template>
  79. </el-table-column>
  80. <el-table-column prop="finishTime" label="完成时间" min-width="180" />
  81. <el-table-column label="操作" width="220" align="left" fixed="right">
  82. <template #default="scope">
  83. <div class="operation-links">
  84. <el-link type="primary" :underline="false" @click="handleDetail(scope.row)">详情</el-link>
  85. <!-- 情况1:完成状态 -> 显示打款 -->
  86. <template v-if="scope.row.status === '完成'">
  87. <el-link type="primary" :underline="false" @click="openPayDialog(scope.row)">打款</el-link>
  88. </template>
  89. <!-- 情况2:未完成且有待审核公司 -> 显示审核通过/拒绝、退款 -->
  90. <template v-else-if="scope.row.status === '未完成' && scope.row.companies && scope.row.companies.length > 0">
  91. <el-link type="primary" :underline="false" @click="handleAudit(scope.row, '通过')">审核通过</el-link>
  92. <el-link type="primary" :underline="false" @click="handleAudit(scope.row, '拒绝')">审核拒绝</el-link>
  93. <el-link type="danger" :underline="false" @click="handleRefund(scope.row)">退款</el-link>
  94. </template>
  95. <!-- 情况3:未完成且无供职公司 -> 显示开始核验、退款 -->
  96. <template v-else-if="scope.row.status === '未完成'">
  97. <el-link type="primary" :underline="false" @click="handleStartCheck(scope.row)">开始核验</el-link>
  98. <el-link type="danger" :underline="false" @click="handleRefund(scope.row)">退款</el-link>
  99. </template>
  100. </div>
  101. </template>
  102. </el-table-column>
  103. </el-table>
  104. <!-- 分页栏 -->
  105. <div class="pagination-wrapper mt-4 flex justify-end">
  106. <el-pagination
  107. v-model:current-page="queryParams.pageNum"
  108. v-model:page-size="queryParams.pageSize"
  109. :total="total"
  110. background
  111. layout="total, sizes, prev, pager, next"
  112. @size-change="handleSizeChange"
  113. @current-change="handleCurrentChange"
  114. />
  115. </div>
  116. </div>
  117. <!-- 弹窗部分 -->
  118. <el-dialog v-model="rejectDialog.visible" title="审核说明" width="520px" class="custom-dialog">
  119. <div class="dialog-content">
  120. <div class="info-row"><span class="label">结果:</span><span class="value">{{ rejectDialog.form.result }}</span></div>
  121. <div class="info-row mt-4">
  122. <span class="label">备注:</span>
  123. <el-input v-model="rejectDialog.form.remark" type="textarea" :rows="4" placeholder="请输入说明" />
  124. </div>
  125. </div>
  126. <template #footer>
  127. <div class="dialog-footer">
  128. <el-button @click="rejectDialog.visible = false">取消</el-button>
  129. <el-button type="primary" @click="confirmReject">确定</el-button>
  130. </div>
  131. </template>
  132. </el-dialog>
  133. <el-dialog v-model="sendDialog.visible" title="发送至公司" width="520px" class="custom-dialog">
  134. <div class="dialog-content">
  135. <div class="info-row"><span class="label">{{ sendDialog.form.companyName }}</span></div>
  136. <div class="info-row mt-1"><span class="label">总价:</span><span class="value">{{ sendDialog.form.orderNo }}</span></div>
  137. <div class="info-row mt-4 flex items-center">
  138. <span class="label required">可获得金额</span>
  139. <el-input v-model="sendDialog.form.amount" style="width: 200px; margin-left: 10px;">
  140. <template #suffix>元</template>
  141. </el-input>
  142. </div>
  143. </div>
  144. <template #footer>
  145. <div class="dialog-footer">
  146. <el-button @click="sendDialog.visible = false">取消</el-button>
  147. <el-button type="primary" @click="confirmSend">确定</el-button>
  148. </div>
  149. </template>
  150. </el-dialog>
  151. <el-dialog v-model="payDialog.visible" title="打款" width="520px" class="custom-dialog">
  152. <div class="dialog-content">
  153. <div class="info-row"><span class="label">{{ payDialog.form.companyName }}</span></div>
  154. <div class="info-row mt-1"><span class="label">总价:</span><span class="value">{{ payDialog.form.orderNo }}</span></div>
  155. </div>
  156. <template #footer>
  157. <div class="dialog-footer">
  158. <el-button @click="payDialog.visible = false">取消</el-button>
  159. <el-button type="primary" @click="confirmPay">确定</el-button>
  160. </div>
  161. </template>
  162. </el-dialog>
  163. <el-dialog v-model="refundDialog.visible" title="确认退款" width="480px" class="custom-dialog">
  164. <div class="dialog-content">
  165. <div class="flex items-center gap-3">
  166. <el-icon color="#e6a23c" size="24"><i-ep-warning-filled /></el-icon>
  167. <span class="text-base">确定要为候选人 <span class="font-bold">{{ refundDialog.name }}</span> 办理退款吗?</span>
  168. </div>
  169. <div class="mt-4 text-gray-500 text-sm">退款金额将原路返回至下单账户。</div>
  170. </div>
  171. <template #footer>
  172. <div class="dialog-footer">
  173. <el-button @click="refundDialog.visible = false">取消</el-button>
  174. <el-button type="danger" @click="confirmRefund">确定退款</el-button>
  175. </div>
  176. </template>
  177. </el-dialog>
  178. </div>
  179. </template>
  180. <script setup lang="ts">
  181. import { reactive, ref, computed, onMounted } from 'vue';
  182. import { useRoute, useRouter } from 'vue-router';
  183. import { ElMessage } from 'element-plus';
  184. import { listBackRecord } from '@/api/system/backRecord';
  185. import { BackRecordVO } from '@/api/system/backRecord/types';
  186. const route = useRoute();
  187. const router = useRouter();
  188. const loading = ref(false);
  189. const total = ref(0);
  190. const queryParams = reactive({
  191. pageNum: 1,
  192. pageSize: 10,
  193. keyword: '',
  194. orderId: (route.query.id as string) || '',
  195. status: ''
  196. });
  197. onMounted(() => {
  198. if (queryParams.orderId) {
  199. getList();
  200. }
  201. });
  202. // 审核弹窗
  203. const rejectDialog = reactive({
  204. visible: false,
  205. form: {
  206. id: '',
  207. result: '',
  208. remark: ''
  209. }
  210. });
  211. // 发送弹窗
  212. const sendDialog = reactive({
  213. visible: false,
  214. form: {
  215. id: '',
  216. companyName: '',
  217. orderNo: 'ORD20250327001',
  218. amount: ''
  219. }
  220. });
  221. // 打款弹窗
  222. const payDialog = reactive({
  223. visible: false,
  224. form: {
  225. id: '',
  226. companyName: '',
  227. orderNo: 'ORD20250327001'
  228. }
  229. });
  230. // 退款弹窗
  231. const refundDialog = reactive({
  232. visible: false,
  233. id: '',
  234. name: ''
  235. });
  236. // 状态映射
  237. const statusMap: Record<string, string> = {
  238. '0': '待处理',
  239. '1': '进行中',
  240. '2': '已完成',
  241. '3': '失败'
  242. };
  243. const tableData = ref<any[]>([]);
  244. // 搜索过滤逻辑
  245. const filteredTableData = computed(() => {
  246. return tableData.value;
  247. });
  248. const getList = async () => {
  249. loading.value = true;
  250. try {
  251. const data = await listBackRecord({
  252. orderId: queryParams.orderId,
  253. status: queryParams.status || undefined,
  254. pageNum: queryParams.pageNum,
  255. pageSize: queryParams.pageSize
  256. });
  257. console.log('后端返回数据:', data);
  258. if (data && data.rows) {
  259. // 将后端数据转换为前端表格格式
  260. tableData.value = data.rows.map((record: BackRecordVO) => {
  261. const idCard = record.studentIdCard || '****';
  262. return {
  263. id: record.id,
  264. candidateId: record.candidateId,
  265. orderId: record.orderId,
  266. name: record.studentName || record.studentNo || `候选人${record.candidateId}`,
  267. studentNo: record.studentNo,
  268. idType: '居民身份证',
  269. idNo: record.studentIdCard || '****',
  270. idNoDisplay: maskIdCard(record.studentIdCard || '****'),
  271. showIdNo: false,
  272. mobile: record.studentMobile || '****',
  273. status: statusMap[record.status] || record.status,
  274. companies: [],
  275. finishTime: record.finishTime || '-',
  276. reportUrl: record.reportUrl,
  277. createTime: record.createTime
  278. };
  279. });
  280. total.value = data.total || 0;
  281. } else {
  282. tableData.value = [];
  283. total.value = 0;
  284. }
  285. } catch (error) {
  286. console.error('获取候选人列表失败:', error);
  287. tableData.value = [];
  288. total.value = 0;
  289. } finally {
  290. loading.value = false;
  291. }
  292. };
  293. // 隐藏身份证号码(显示为*)
  294. const maskIdCard = (idCard: string): string => {
  295. if (!idCard || idCard === '****') return '****';
  296. return idCard.replace(/./g, '*');
  297. };
  298. // 切换身份证号码显示/隐藏
  299. const toggleIdNo = (row: any) => {
  300. row.showIdNo = !row.showIdNo;
  301. row.idNoDisplay = row.showIdNo ? row.idNo : maskIdCard(row.idNo);
  302. };
  303. const handleQuery = () => { getList(); };
  304. const handleSelectionChange = () => {};
  305. const handleSizeChange = () => { getList(); };
  306. const handleCurrentChange = () => { getList(); };
  307. const handleDetail = (row: any) => {
  308. router.push({
  309. name: 'BackcheckReport',
  310. query: {
  311. id: row.id,
  312. name: row.name,
  313. status: row.status,
  314. studentNo: row.studentNo,
  315. mobile: row.mobile,
  316. idCard: row.idNo,
  317. gender: row.studentGender
  318. }
  319. });
  320. };
  321. const getStatusClass = (status: string) => {
  322. switch (status) {
  323. case '通过': return 'status-success';
  324. case '拒绝': return 'status-danger';
  325. case '发送': return 'status-primary';
  326. default: return 'status-info';
  327. }
  328. };
  329. const handleTagClick = (comp: any, row: any) => {
  330. if (comp.status === '发送') {
  331. openSendDialog(row, comp);
  332. } else if (comp.status === '拒绝') {
  333. handleAudit(row, '拒绝', comp);
  334. }
  335. };
  336. const handleAudit = (row: any, type: string, comp?: any) => {
  337. rejectDialog.form.id = row.id;
  338. rejectDialog.form.result = type;
  339. rejectDialog.form.remark = type === '拒绝' ? '说明说明说明说明说明说明说明说明' : '';
  340. // 如果是从标签点击进来的,可以记录具体的公司名称(可选,根据需求)
  341. console.log('正在审核公司:', comp?.name || '全选');
  342. rejectDialog.visible = true;
  343. };
  344. const handleStartCheck = (row: any) => {
  345. ElMessage.success(`已为候选人 ${row.name} 开始核验`);
  346. };
  347. const confirmReject = () => {
  348. rejectDialog.visible = false;
  349. ElMessage.success('审核说明已提交');
  350. };
  351. const openSendDialog = (row: any, comp?: any) => {
  352. sendDialog.form.id = row.id;
  353. sendDialog.form.companyName = comp?.name || row.companies?.[0]?.name || 'XXXXXXXX公司';
  354. sendDialog.form.amount = '198.00'; // 默认一个金额
  355. sendDialog.visible = true;
  356. };
  357. const confirmSend = () => {
  358. sendDialog.visible = false;
  359. ElMessage.success(`已发送至 ${sendDialog.form.companyName}`);
  360. };
  361. const openPayDialog = (row: any, comp?: any) => {
  362. payDialog.form.id = row.id;
  363. payDialog.form.companyName = comp?.name || row.companies?.[0]?.name || 'XXXXXXXX公司';
  364. payDialog.visible = true;
  365. };
  366. const confirmPay = () => {
  367. payDialog.visible = false;
  368. ElMessage.success('打款已确认');
  369. };
  370. const handleRefund = (row: any) => {
  371. refundDialog.id = row.id;
  372. refundDialog.name = row.name;
  373. refundDialog.visible = true;
  374. };
  375. const confirmRefund = () => {
  376. refundDialog.visible = false;
  377. ElMessage.success(`候选人 ${refundDialog.name} 的退款申请已提交`);
  378. };
  379. const handleSync = () => {
  380. loading.value = true;
  381. setTimeout(() => {
  382. loading.value = false;
  383. ElMessage.success('同步至单位成功');
  384. }, 500);
  385. };
  386. const handleExport = () => {
  387. ElMessage.success('正在导出列表...');
  388. };
  389. getList();
  390. </script>
  391. <style scoped lang="scss">
  392. .backcheck-candidate-page {
  393. padding: 20px;
  394. background-color: #f0f2f5;
  395. height: calc(100vh - 84px);
  396. display: flex;
  397. flex-direction: column;
  398. }
  399. .main-box {
  400. flex: 1;
  401. display: flex;
  402. flex-direction: column;
  403. overflow: hidden;
  404. }
  405. .custom-table {
  406. flex: 1;
  407. overflow: hidden;
  408. :deep(.el-table__inner-wrapper) {
  409. height: 100% !important;
  410. }
  411. }
  412. .status-dot {
  413. display: inline-block;
  414. width: 8px;
  415. height: 8px;
  416. border-radius: 50%;
  417. margin-right: 8px;
  418. }
  419. .dot-success { background-color: #67c23a; }
  420. .dot-danger { background-color: #f56c6c; }
  421. .company-tag-wrapper {
  422. display: flex;
  423. align-items: center;
  424. border: 1px solid #dcdfe6;
  425. padding: 0;
  426. border-radius: 4px;
  427. background-color: #fff;
  428. overflow: hidden;
  429. height: 24px;
  430. margin: 2px 0;
  431. }
  432. .company-name {
  433. font-size: 12px;
  434. color: #606266;
  435. padding: 0 8px;
  436. }
  437. .tag-status {
  438. height: 100%;
  439. padding: 0 8px;
  440. font-size: 12px;
  441. display: flex;
  442. align-items: center;
  443. cursor: pointer;
  444. &.status-success { color: #67c23a; background-color: #f0f9eb; }
  445. &.status-danger { color: #f56c6c; background-color: #fef0f0; }
  446. &.status-primary { color: #409eff; background-color: #ecf5ff; }
  447. &.status-info { color: #909399; background-color: #f4f4f5; }
  448. }
  449. .operation-links {
  450. display: flex;
  451. gap: 12px;
  452. }
  453. .custom-table {
  454. border: none !important;
  455. :deep(.el-table__inner-wrapper::before) { display: none; }
  456. }
  457. :deep(.custom-header th) {
  458. background-color: #f7f8fa !important;
  459. color: #4e5969;
  460. font-weight: 500;
  461. padding: 12px 0;
  462. border-bottom: none !important;
  463. }
  464. .pagination-wrapper {
  465. padding: 20px 0;
  466. background-color: #fff;
  467. display: flex;
  468. justify-content: flex-end;
  469. }
  470. :deep(.custom-dialog) {
  471. .el-dialog__header {
  472. border-bottom: 1px solid #f0f2f5;
  473. margin-right: 0;
  474. padding: 15px 20px;
  475. }
  476. .el-dialog__body { padding: 20px 40px; }
  477. .dialog-content {
  478. .info-row {
  479. font-size: 14px;
  480. color: #333;
  481. .label {
  482. font-weight: 500;
  483. &.required::before { content: '*'; color: #f56c6c; margin-right: 4px; }
  484. }
  485. }
  486. }
  487. .dialog-footer { display: flex; justify-content: flex-end; gap: 12px; }
  488. }
  489. </style>