index.vue 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317
  1. <template>
  2. <fs-page>
  3. <div class="job-application-container">
  4. <!-- 添加顶部标签页 -->
  5. <div class="tabs-container">
  6. <el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
  7. <el-tab-pane label="职位申请" name="application">
  8. <!-- 第一个tab:保留现有功能 -->
  9. <div class="tab-content">
  10. <div class="sidebar">
  11. <div class="tree-container">
  12. <!-- 职位分类 -->
  13. <div class="category-title" @click="togglePositionList">
  14. <el-icon><Briefcase /></el-icon>
  15. <span>职位</span>
  16. <el-icon class="expand-icon" :class="{ 'is-expanded': showPositionList }">
  17. <ArrowRight />
  18. </el-icon>
  19. </div>
  20. <!-- 职位列表 -->
  21. <div v-if="showPositionList" class="position-list">
  22. <!-- 全部职位 -->
  23. <div class="tree-item" :class="{ active: activeNode === 'position-all' }" @click="handleNodeClick({ type: 'all', id: 'position-all' })">
  24. <div class="item-content">
  25. <span>全部</span>
  26. </div>
  27. </div>
  28. <!-- 职位列表项 -->
  29. <div
  30. v-for="position in positions"
  31. :key="'position-' + position.id"
  32. class="tree-item"
  33. :class="{ active: activeNode === 'position-' + position.id }"
  34. @click="handleNodeClick({ type: 'position', value: position.title, id: 'position-' + position.id })"
  35. >
  36. <div class="item-content">
  37. <el-icon><ArrowRight /></el-icon>
  38. <span>{{ position.title }}</span>
  39. </div>
  40. <div class="item-count" v-if="position.count">{{ position.count }}</div>
  41. </div>
  42. </div>
  43. <!-- 全部 -->
  44. <div class="tree-item" :class="{ active: activeNode === 'all' }" @click="handleNodeClick({ type: 'all' })">
  45. <div class="item-content">
  46. <el-icon><Grid /></el-icon>
  47. <span>全部</span>
  48. </div>
  49. <div class="item-count">{{ totalCount }}</div>
  50. </div>
  51. <!-- 待安排 -->
  52. <div class="tree-item" :class="{ active: activeNode === 'status-0' }" @click="handleNodeClick({ type: 'status', value: 0, id: 'status-0' })">
  53. <div class="item-content">
  54. <el-icon><Message /></el-icon>
  55. <span>待通知</span>
  56. </div>
  57. <div class="item-count" v-if="statusCounts[1]">{{ statusCounts[1] }}</div>
  58. </div>
  59. <!-- 待安排 -->
  60. <div class="tree-item" :class="{ active: activeNode === 'status-1' }" @click="handleNodeClick({ type: 'status', value: 1, id: 'status-1' })">
  61. <div class="item-content">
  62. <el-icon><Clock /></el-icon>
  63. <span>已通知待面试</span>
  64. </div>
  65. <div class="item-count" v-if="statusCounts[1]">{{ statusCounts[5] }}</div>
  66. </div>
  67. <!-- 推进中 -->
  68. <div class="tree-item" :class="{ active: activeNode === 'status-2' }" @click="handleNodeClick({ type: 'status', value: 2, id: 'status-2' })">
  69. <div class="item-content">
  70. <el-icon><ArrowRight /></el-icon>
  71. <span>已面试待处理</span>
  72. </div>
  73. <div class="item-count" v-if="statusCounts[2]">{{ statusCounts[2] }}</div>
  74. </div>
  75. <!-- 已入职 -->
  76. <div class="tree-item" :class="{ active: activeNode === 'status-3' }" @click="handleNodeClick({ type: 'status', value: 3, id: 'status-3' })">
  77. <div class="item-content">
  78. <el-icon><Check /></el-icon>
  79. <span>已录用</span>
  80. </div>
  81. <div class="item-count" v-if="statusCounts[3]">{{ statusCounts[3] }}</div>
  82. </div>
  83. <!-- 待召回 -->
  84. <div class="tree-item" :class="{ active: activeNode === 'status-4' }" @click="handleNodeClick({ type: 'status', value: 4, id: 'status-4' })">
  85. <div class="item-content">
  86. <el-icon><RefreshRight /></el-icon>
  87. <span>拒不录用</span>
  88. </div>
  89. <div class="item-count" v-if="statusCounts[4]">{{ statusCounts[4] }}</div>
  90. </div>
  91. </div>
  92. </div>
  93. <div class="content">
  94. <fs-crud ref="crudRef" v-bind="crudBinding">
  95. <!-- <template #actionbar-right>
  96. </template> -->
  97. </fs-crud>
  98. </div>
  99. </div>
  100. </el-tab-pane>
  101. <el-tab-pane label="待通知列表" name="resume">
  102. <!-- 第二个tab:待通知列表 -->
  103. <div class="tab-content">
  104. <div class="notification-container">
  105. <!-- 搜索表单 -->
  106. <div class="search-form">
  107. <el-form :model="notificationSearchForm" :inline="true" size="default">
  108. <el-form-item label="姓名">
  109. <el-input
  110. v-model="notificationSearchForm.name"
  111. placeholder="请输入姓名"
  112. clearable
  113. style="width: 200px"
  114. />
  115. </el-form-item>
  116. <el-form-item label="手机号">
  117. <el-input
  118. v-model="notificationSearchForm.phone"
  119. placeholder="请输入手机号"
  120. clearable
  121. style="width: 200px"
  122. />
  123. </el-form-item>
  124. <el-form-item>
  125. <el-button type="primary" @click="handleNotificationSearch">
  126. <el-icon><Search /></el-icon>
  127. 搜索
  128. </el-button>
  129. <el-button @click="handleNotificationReset">
  130. <el-icon><Refresh /></el-icon>
  131. 重置
  132. </el-button>
  133. </el-form-item>
  134. </el-form>
  135. </div>
  136. <!-- 操作按钮 -->
  137. <div class="action-bar">
  138. <div class="left-actions">
  139. <el-button type="primary" @click="handleBatchNotify" size="small" :disabled="!selectedNotificationRows.length">
  140. <el-icon><Message /></el-icon>
  141. 批量通知
  142. </el-button>
  143. <el-button type="danger" @click="handleBatchDelete" size="small" :disabled="!selectedNotificationRows.length">
  144. <el-icon><Delete /></el-icon>
  145. 批量删除
  146. </el-button>
  147. <div class="action-button-wrapper">
  148. <el-upload
  149. class="upload-demo"
  150. :action="uploadUrl"
  151. :headers="{
  152. Authorization: `JWT ${Session.get('token')}`
  153. }"
  154. :on-success="handleImportSuccess"
  155. :on-error="handleImportError"
  156. accept=".xlsx,.xls"
  157. :show-file-list="false"
  158. >
  159. <el-button type="primary" size="small">
  160. <el-icon><Download /></el-icon>
  161. 导入
  162. </el-button>
  163. </el-upload>
  164. </div>
  165. <el-button type="primary" @click="handleBatchInterview" size="small" >
  166. <!-- <el-icon><Clock /></el-icon> -->
  167. 下载导入模板
  168. </el-button>
  169. </div>
  170. <!-- <div class="right-actions">
  171. <el-button @click="handleNotificationRefresh">
  172. <el-icon><Refresh /></el-icon>
  173. 刷新
  174. </el-button>
  175. </div> -->
  176. </div>
  177. <!-- 数据表格 -->
  178. <div class="table-container">
  179. <el-table
  180. :data="notificationList"
  181. @selection-change="handleNotificationSelectionChange"
  182. :loading="notificationLoading"
  183. style="width: 100%"
  184. height="calc(100vh - 400px)"
  185. >
  186. <el-table-column type="selection" width="55" />
  187. <el-table-column prop="name" label="姓名" />
  188. <el-table-column prop="phone" label="手机号" />
  189. <el-table-column prop="status" label="状态">
  190. <template #default="scope">
  191. <el-tag type="warning">待通知</el-tag>
  192. </template>
  193. </el-table-column>
  194. <el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
  195. <el-table-column label="操作" fixed="right">
  196. <template #default="scope">
  197. <el-button
  198. type="text"
  199. size="small"
  200. @click="handleViewDetail(scope.row)"
  201. >
  202. 查看详情
  203. </el-button>
  204. <el-button
  205. type="text"
  206. size="small"
  207. @click="handleNotify(scope.row)"
  208. >
  209. 编辑
  210. </el-button>
  211. <el-button
  212. type="text"
  213. size="small"
  214. @click="handleInterview(scope.row)"
  215. >
  216. 删除
  217. </el-button>
  218. </template>
  219. </el-table-column>
  220. </el-table>
  221. </div>
  222. <!-- 分页 -->
  223. <div class="pagination-container">
  224. <el-pagination
  225. v-model:current-page="notificationPagination.current"
  226. v-model:page-size="notificationPagination.size"
  227. :page-sizes="[10, 20, 50, 100]"
  228. :total="notificationPagination.total"
  229. layout="total, sizes, prev, pager, next, jumper"
  230. @size-change="handleNotificationSizeChange"
  231. @current-change="handleNotificationCurrentChange"
  232. />
  233. </div>
  234. </div>
  235. </div>
  236. </el-tab-pane>
  237. </el-tabs>
  238. </div>
  239. </div>
  240. <BatchTagsDialog ref="batchTagsDialogRef" :crudExpose="crudExpose" />
  241. <BatchStatusDialog ref="batchStatusDialogRef" :crudExpose="crudExpose" @success="handleBatchStatusSuccess" />
  242. <smSnotification ref="smSnotificationRef" :crudExpose="crudExpose" />
  243. <!-- 详情弹窗 -->
  244. <el-dialog
  245. v-model="detailDialogVisible"
  246. title="职位申请详情"
  247. width="60%"
  248. :before-close="handleDetailDialogClose"
  249. >
  250. <div v-if="currentDetailData" class="detail-content">
  251. <el-descriptions :column="2" border>
  252. <el-descriptions-item label="申请人姓名">{{ currentDetailData.name }}</el-descriptions-item>
  253. <el-descriptions-item label="联系电话">{{ currentDetailData.phone }}</el-descriptions-item>
  254. <el-descriptions-item label="申请职位">{{ currentDetailData.position_title }}</el-descriptions-item>
  255. <el-descriptions-item label="申请时间">{{ currentDetailData.apply_time }}</el-descriptions-item>
  256. <el-descriptions-item label="当前状态">
  257. <el-tag :type="getStatusTagType(currentDetailData.status)">
  258. {{ getStatusText(currentDetailData.status) }}
  259. </el-tag>
  260. </el-descriptions-item>
  261. <el-descriptions-item label="来源渠道">{{ currentDetailData.source }}</el-descriptions-item>
  262. <el-descriptions-item label="备注信息" :span="2">{{ currentDetailData.remark || '无' }}</el-descriptions-item>
  263. </el-descriptions>
  264. </div>
  265. <template #footer>
  266. <span class="dialog-footer">
  267. <el-button @click="detailDialogVisible = false">关闭</el-button>
  268. <el-button type="primary" @click="handleEditDetail">编辑</el-button>
  269. </span>
  270. </template>
  271. </el-dialog>
  272. <!-- 编辑弹窗 -->
  273. <el-dialog
  274. v-model="editDialogVisible"
  275. title="编辑通知信息"
  276. width="520px"
  277. :before-close="handleEditDialogClose"
  278. >
  279. <el-form :model="editForm" :rules="editRules" ref="editFormRef" label-width="90px">
  280. <el-form-item label="姓名" prop="name">
  281. <el-input v-model="editForm.name" placeholder="请输入姓名" />
  282. </el-form-item>
  283. <el-form-item label="手机号" prop="phone">
  284. <el-input v-model="editForm.phone" placeholder="请输入手机号" />
  285. </el-form-item>
  286. <el-form-item label="身份证" prop="id_card">
  287. <el-input v-model="editForm.id_card" placeholder="请输入身份证号" />
  288. </el-form-item>
  289. <el-form-item label="备注" prop="description">
  290. <el-input type="textarea" v-model="editForm.description" placeholder="请输入备注" :rows="3" />
  291. </el-form-item>
  292. </el-form>
  293. <template #footer>
  294. <span class="dialog-footer">
  295. <el-button @click="editDialogVisible = false">取消</el-button>
  296. <el-button type="primary" @click="handleEditSubmit">提交</el-button>
  297. </span>
  298. </template>
  299. </el-dialog>
  300. </fs-page>
  301. </template>
  302. <script lang="ts" setup name="areas">
  303. import { onMounted, ref, reactive, computed } from 'vue';
  304. import { useFs } from '@fast-crud/fast-crud';
  305. import { createCrudOptions } from './crud';
  306. import { GetPermission } from './api';
  307. import { handleColumnPermission } from '/@/utils/columnPermission';
  308. import { Grid, Clock, ArrowRight, Check, RefreshRight, Briefcase, Message, Upload, Download, Search, Refresh } from '@element-plus/icons-vue';
  309. import BatchTagsDialog from './components/index.vue';
  310. import BatchStatusDialog from './components/BatchStatusDialog.vue';
  311. import smSnotification from './components/smSnotification.vue';
  312. import { getApplicationStatusSummary,downloadImportTemplate,bulkDelete, updateApplicationStatus, getApplicationDetail, batchUpdateApplicationStatus, updateSmsNotification } from './api';
  313. import axios from 'axios';
  314. import { useRoute } from 'vue-router';
  315. import { ElMessage,ElMessageBox } from 'element-plus';
  316. import { Session } from '/@/utils/storage';
  317. import { Delete } from '@element-plus/icons-vue'
  318. const route = useRoute();
  319. // 添加标签页相关变量
  320. const activeTab = ref('application');
  321. // 待通知列表相关数据
  322. interface NotificationItem {
  323. id: number;
  324. name: string;
  325. phone: string;
  326. position_title: string;
  327. apply_time: string;
  328. status: string;
  329. source: string;
  330. remark: string;
  331. }
  332. const notificationList = ref<NotificationItem[]>([]);
  333. const notificationLoading = ref(false);
  334. const selectedNotificationRows = ref<NotificationItem[]>([]);
  335. const notificationSearchForm = reactive({
  336. name: '',
  337. phone: '',
  338. position: '',
  339. dateRange: [] as string[]
  340. });
  341. const notificationPagination = reactive({
  342. current: 1,
  343. size: 20,
  344. total: 0
  345. });
  346. // 定义上传地址
  347. const uploadUrl = `${import.meta.env.VITE_API_URL}/api/system/sms_notifications/import_data/`;
  348. const { crudBinding, crudRef, crudExpose, crudOptions, resetCrudOptions } = useFs({
  349. createCrudOptions,
  350. context: {
  351. openBatchTagsDialog: (selection: any) => {
  352. batchTagsDialogRef.value.open(selection);
  353. },
  354. openBatchStatusDialog: (selection: any) => {
  355. batchStatusDialogRef.value.open(selection);
  356. },
  357. openSmsNotification:(selection:any)=>{
  358. smSnotificationRef.value.open(selection)
  359. },
  360. selectedRows: [] // 存储选中的行
  361. }
  362. });
  363. const batchTagsDialogRef = ref();
  364. const batchStatusDialogRef = ref();
  365. const smSnotificationRef=ref()
  366. // 状态计数
  367. const totalCount = ref(0);
  368. const statusCounts = reactive<Record<number, number>>({
  369. 1: 0, // 待面试
  370. 2: 0, // 已面试
  371. 3: 0, // 已录用
  372. 4: 0, // 已拒绝
  373. 5:0//已通知待面试
  374. });
  375. // 职位列表
  376. const showPositionList = ref(true);
  377. const getStepStyle = computed(() => (step: { name: string }) => {
  378. return {
  379. marginLeft: (step.name === '已通过' || step.name === '已淘汰') ? '32px' : '0'
  380. };
  381. });
  382. const positions = ref<Array<{id: number|string, title: string, count?: number}>>([]);
  383. // 获取职位列表
  384. const fetchPositions = async () => {
  385. try {
  386. const res = await fetch(`${import.meta.env.VITE_API_URL}/api/system/job/list?page=1&limit=100&tenant_id=${Session.get('tenant_id')}`);
  387. const data = await res.json();
  388. if (data.code === 2000 && data.data ) {
  389. positions.value = data.data.map((item: any) => ({
  390. id: item.id,
  391. title: item.title || item.name || item.job_name,
  392. count: 0
  393. }));
  394. } else {
  395. console.error('获取职位列表失败:', data.msg || '未知错误');
  396. }
  397. } catch (error) {
  398. console.error('获取职位列表异常:', error);
  399. // 设置默认职位,以防接口失败
  400. positions.value = [{ id: 1, title: '流水线操作工', count: 0 }];
  401. }
  402. };
  403. // 获取申请状态统计数据
  404. const fetchStatusSummary = async () => {
  405. try {
  406. const res = await getApplicationStatusSummary();
  407. const data = res;
  408. console.log(data)
  409. if (data.code === 2000 && data.data) {
  410. totalCount.value = data.data.total || 0;
  411. // 更新状态计数
  412. if (data.data.status_data && Array.isArray(data.data.status_data)) {
  413. data.data.status_data.forEach((item: any) => {
  414. // 注意:API返回的status可能与前端定义的不完全一致,需要映射
  415. const statusMap: Record<number, number> = {
  416. 0: 1, // API中的0对应前端的1(待面试)
  417. 2: 2, // 已面试
  418. 3: 3, // 已录用
  419. 4: 4, // 已拒绝
  420. 1:5//已通知待面试
  421. };
  422. const frontendStatus = statusMap[item.status];
  423. if (frontendStatus && frontendStatus in statusCounts) {
  424. statusCounts[frontendStatus] = item.count;
  425. }
  426. });
  427. } else {
  428. // 如果没有status_data,则使用单独的字段
  429. statusCounts[1] = data.data.pending || 0;
  430. statusCounts[2] = data.data.interviewed || 0;
  431. statusCounts[3] = data.data.hired || 0;
  432. statusCounts[4] = data.data.rejected || 0;
  433. statusCounts[5] = data.data.notified || 0;
  434. }
  435. } else {
  436. console.error('获取申请状态统计失败:', data.message || '未知错误');
  437. }
  438. } catch (error) {
  439. console.error('获取申请状态统计异常:', error);
  440. }
  441. };
  442. // 切换职位列表显示
  443. const togglePositionList = () => {
  444. showPositionList.value = !showPositionList.value;
  445. };
  446. // 当前激活的节点
  447. const activeNode = ref('all');
  448. // 处理树节点点击
  449. const handleNodeClick = (data: any) => {
  450. activeNode.value = data.id || 'all';
  451. if (data.type === 'all') {
  452. try {
  453. // 尝试重置表单,如果方法存在的话
  454. const searchRef = crudExpose.getSearchRef();
  455. if (searchRef && typeof searchRef.resetFields === 'function') {
  456. searchRef.resetFields();
  457. }
  458. } catch (error) {
  459. console.warn('重置表单失败:', error);
  460. }
  461. // 无论如何都执行搜索,确保数据刷新
  462. crudExpose.doSearch({
  463. form: {}
  464. });
  465. } else if (data.type === 'status') {
  466. // 按状态筛选
  467. crudExpose.doSearch({
  468. form: {
  469. status: data.value
  470. }
  471. });
  472. } else if (data.type === 'position') {
  473. // 按职位筛选
  474. crudExpose.doSearch({
  475. form: {
  476. position_title: data.value
  477. }
  478. });
  479. }
  480. };
  481. // 更新计数
  482. const updateCounts = (data: any) => {
  483. if (!data || !data.records) return;
  484. // 统计各职位数量
  485. // 重置职位计数
  486. positions.value.forEach(position => {
  487. position.count = 0;
  488. });
  489. // 统计职位数量
  490. data.records.forEach((record: any) => {
  491. if (record.position_title) {
  492. const position = positions.value.find(p => p.title === record.position_title);
  493. if (position) {
  494. position.count = (position.count || 0) + 1;
  495. }
  496. }
  497. });
  498. };
  499. // 处理批量状态修改成功
  500. const handleBatchStatusSuccess = () => {
  501. // 刷新数据和状态统计
  502. crudExpose.doRefresh();
  503. fetchStatusSummary();
  504. };
  505. // 处理导入成功
  506. const handleImportSuccess = (response: any) => {
  507. console.log(response)
  508. if (response.code === 2000) {
  509. ElMessage.success('导入成功');
  510. // 刷新数据和状态统计
  511. crudExpose.doRefresh();
  512. fetchNotifications();
  513. } else {
  514. ElMessage.error(response.message || '导入失败');
  515. }
  516. };
  517. // 处理导入失败
  518. const handleImportError = (error: any) => {
  519. console.error('导入失败:', error);
  520. ElMessage.error('导入失败,请检查文件格式或网络连接');
  521. };
  522. // 处理标签页切换
  523. const handleTabClick = (tab: any) => {
  524. console.log('切换到标签页:', tab.name);
  525. // 这里可以根据需要添加标签页切换时的逻辑
  526. if (tab.name === 'application') {
  527. // 切换到职位申请标签页时的处理
  528. console.log('显示职位申请功能');
  529. } else if (tab.name === 'resume') {
  530. // 切换到待通知列表标签页时的处理
  531. console.log('显示待通知列表功能');
  532. fetchNotificationList();
  533. }
  534. };
  535. // 获取待通知列表数据
  536. const fetchNotificationList = async () => {
  537. notificationLoading.value = true;
  538. try {
  539. // 构建查询参数
  540. const params: Record<string, string | number> = {
  541. page: notificationPagination.current,
  542. limit: notificationPagination.size,
  543. status: 0, // 待通知状态
  544. };
  545. // 添加搜索条件
  546. if (notificationSearchForm.name) params.name = notificationSearchForm.name;
  547. if (notificationSearchForm.phone) params.phone = notificationSearchForm.phone;
  548. if (notificationSearchForm.position) params.position = notificationSearchForm.position;
  549. // 调用API获取待通知列表
  550. const response = await fetch(`${import.meta.env.VITE_API_URL}/api/job/application/list?${new URLSearchParams(params as Record<string, string>)}`, {
  551. headers: {
  552. 'Authorization': `Bearer ${Session.get('token')}`,
  553. 'Content-Type': 'application/json'
  554. }
  555. });
  556. const data = await response.json();
  557. if (data.code === 2000 && data.data) {
  558. notificationList.value = data.data.records || [];
  559. notificationPagination.total = data.data.total || 0;
  560. } else {
  561. ElMessage.error(data.message || '获取待通知列表失败');
  562. // 如果API调用失败,显示测试数据
  563. showTestData();
  564. }
  565. } catch (error) {
  566. console.error('获取待通知列表失败:', error);
  567. ElMessage.error('获取待通知列表失败');
  568. // 如果API调用失败,显示测试数据
  569. showTestData();
  570. } finally {
  571. notificationLoading.value = false;
  572. }
  573. };
  574. // 显示测试数据
  575. const showTestData = () => {
  576. notificationList.value = [];
  577. notificationPagination.total = 3;
  578. };
  579. // 搜索待通知列表
  580. const handleNotificationSearch = () => {
  581. notificationPagination.current = 1;
  582. fetchNotifications();
  583. };
  584. // 重置搜索条件
  585. const handleNotificationReset = () => {
  586. Object.assign(notificationSearchForm, {
  587. name: '',
  588. phone: '',
  589. position: '',
  590. dateRange: []
  591. });
  592. notificationPagination.current = 1;
  593. fetchNotifications();
  594. };
  595. // 刷新待通知列表
  596. const handleNotificationRefresh = () => {
  597. fetchNotificationList();
  598. };
  599. // 处理表格选择变化
  600. const handleNotificationSelectionChange = (selection: NotificationItem[]) => {
  601. selectedNotificationRows.value = selection;
  602. };
  603. // 处理分页大小变化
  604. const handleNotificationSizeChange = (size: number) => {
  605. notificationPagination.size = size;
  606. notificationPagination.current = 1;
  607. fetchNotifications();
  608. };
  609. // 处理当前页变化
  610. const handleNotificationCurrentChange = (current: number) => {
  611. notificationPagination.current = current;
  612. fetchNotifications();
  613. };
  614. // 单个通知(此处打开编辑弹窗回显数据)
  615. const handleNotify = async (row: NotificationItem) => {
  616. try {
  617. currentDetailData.value = row;
  618. editForm.id = (row as any).id;
  619. editForm.name = (row as any).name || '';
  620. editForm.phone = (row as any).phone || '';
  621. editForm.id_card = (row as any).id_card || '';
  622. editForm.description = (row as any).description || '';
  623. editDialogVisible.value = true;
  624. } catch (error) {
  625. console.error('打开编辑弹窗失败:', error);
  626. ElMessage.error('操作失败,请重试');
  627. }
  628. };
  629. // 安排面试
  630. const handleInterview = async (row: NotificationItem) => {
  631. try {
  632. const ids = [row.id]
  633. // 使用API函数更新申请状态为"已安排面试"
  634. const response = await bulkDelete({
  635. ids
  636. });
  637. if (response.code === 2000) {
  638. ElMessage.success('删除成功');
  639. // 刷新列表数据
  640. await fetchNotifications();
  641. } else {
  642. ElMessage.error(response.message || '删除失败');
  643. }
  644. } catch (error) {
  645. console.error('删除失败:', error);
  646. ElMessage.error('删除失败,请重试');
  647. }
  648. };
  649. // 查看详情
  650. const handleViewDetail = async (row: NotificationItem) => {
  651. try {
  652. showDetailDialog(row);
  653. } catch (error) {
  654. console.error('获取详情失败:', error);
  655. ElMessage.error('获取详情失败,请重试');
  656. }
  657. };
  658. // 详情弹窗相关变量
  659. const detailDialogVisible = ref(false);
  660. const currentDetailData = ref<any>(null);
  661. // 编辑弹窗相关变量
  662. const editDialogVisible = ref(false);
  663. const editFormRef = ref();
  664. const editForm = reactive<{ id?: number; name: string; phone: string; id_card: string; description: string }>({
  665. id: undefined,
  666. name: '',
  667. phone: '',
  668. id_card: '',
  669. description: ''
  670. });
  671. const editRules = {
  672. name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
  673. phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
  674. id_card: [{ required: true, message: '请输入身份证号', trigger: 'blur' }],
  675. description: [{ required: true, message: '请输入备注', trigger: 'blur' }]
  676. };
  677. // 显示详情弹窗
  678. const showDetailDialog = (detailData: any) => {
  679. currentDetailData.value = detailData;
  680. detailDialogVisible.value = true;
  681. };
  682. // 关闭详情弹窗
  683. const handleDetailDialogClose = () => {
  684. detailDialogVisible.value = false;
  685. currentDetailData.value = null;
  686. };
  687. // 编辑详情
  688. const handleEditDetail = () => {
  689. if (currentDetailData.value) {
  690. // 打开编辑弹窗并回显当前数据
  691. editForm.id = (currentDetailData.value as any).id;
  692. editForm.name = (currentDetailData.value as any).name || '';
  693. editForm.phone = (currentDetailData.value as any).phone || '';
  694. editForm.id_card = (currentDetailData.value as any).id_card || '';
  695. editForm.description = (currentDetailData.value as any).description || '';
  696. editDialogVisible.value = true;
  697. }
  698. };
  699. // 关闭编辑弹窗
  700. const handleEditDialogClose = () => {
  701. editDialogVisible.value = false;
  702. };
  703. // 提交编辑
  704. const handleEditSubmit = () => {
  705. (editFormRef.value as any).validate(async (valid: boolean) => {
  706. if (!valid) return;
  707. try {
  708. if (!editForm.id) {
  709. ElMessage.error('缺少记录ID');
  710. return;
  711. }
  712. const res = await updateSmsNotification(editForm.id, {
  713. name: editForm.name,
  714. phone: editForm.phone,
  715. id_card: editForm.id_card,
  716. description: editForm.description,
  717. });
  718. if ((res as any).code === 2000) {
  719. ElMessage.success('更新成功');
  720. editDialogVisible.value = false;
  721. // 刷新列表
  722. await fetchNotifications();
  723. } else {
  724. ElMessage.error((res as any).message || '更新失败');
  725. }
  726. } catch (e) {
  727. console.error(e);
  728. ElMessage.error('更新失败,请重试');
  729. }
  730. });
  731. };
  732. // 获取状态文本
  733. const getStatusText = (status: number) => {
  734. const statusMap: Record<number, string> = {
  735. 0: '待通知',
  736. 1: '已通知待面试',
  737. 2: '已面试待处理',
  738. 3: '已录用',
  739. 4: '拒不录用'
  740. };
  741. return statusMap[status] || '未知状态';
  742. };
  743. // 获取状态标签类型
  744. const getStatusTagType = (status: number) => {
  745. const typeMap: Record<number, string> = {
  746. 0: 'warning',
  747. 1: 'info',
  748. 2: 'primary',
  749. 3: 'success',
  750. 4: 'danger'
  751. };
  752. return typeMap[status] || 'info';
  753. };
  754. // 批量通知
  755. const handleBatchNotify = () => {
  756. if (selectedNotificationRows.value.length === 0) {
  757. ElMessage.warning('请选择要通知的申请');
  758. return;
  759. }
  760. console.log(selectedNotificationRows.value);
  761. smSnotificationRef.value.open(selectedNotificationRows.value);
  762. };
  763. // 批量删除方法
  764. const handleBatchDelete = async () => {
  765. if (!selectedNotificationRows.value || selectedNotificationRows.value.length === 0) {
  766. ElMessage.warning('请选择要删除的记录')
  767. return
  768. }
  769. try {
  770. await ElMessageBox.confirm(
  771. `确定要删除选中的 ${selectedNotificationRows.value.length} 条记录吗?`,
  772. '确认删除',
  773. {
  774. confirmButtonText: '确定',
  775. cancelButtonText: '取消',
  776. type: 'warning',
  777. }
  778. )
  779. const ids = selectedNotificationRows.value.map(row => row.id)
  780. const response = await bulkDelete({ ids })
  781. console.log(response)
  782. if (response.code==2000) {
  783. ElMessage.success('批量删除成功')
  784. // 刷新数据
  785. await fetchNotifications()
  786. // 清空选中状态
  787. selectedNotificationRows.value = []
  788. } else {
  789. const errorData = await response.json()
  790. ElMessage.error(errorData.message || '删除失败')
  791. }
  792. } catch (error) {
  793. if (error !== 'cancel') {
  794. console.error('批量删除失败:', error)
  795. ElMessage.error('删除失败,请重试')
  796. }
  797. }
  798. }
  799. // 批量更新状态方法
  800. const handleBatchUpdateStatus = async (newStatus: number, action: string) => {
  801. if (!selectedNotificationRows.value || selectedNotificationRows.value.length === 0) {
  802. ElMessage.warning('请选择要更新的记录')
  803. return
  804. }
  805. try {
  806. const statusText = {
  807. 1: '已通知',
  808. 2: '已安排面试',
  809. 3: '已录用',
  810. 4: '拒不录用'
  811. }[newStatus] || '未知状态'
  812. await ElMessageBox.confirm(
  813. `确定要将选中的 ${selectedNotificationRows.value.length} 条记录状态更新为"${statusText}"吗?`,
  814. '确认更新',
  815. {
  816. confirmButtonText: '确定',
  817. cancelButtonText: '取消',
  818. type: 'warning',
  819. }
  820. )
  821. const ids = selectedNotificationRows.value.map(row => row.id)
  822. const response = await batchUpdateApplicationStatus({
  823. ids,
  824. status: newStatus,
  825. action,
  826. note: `批量更新为${statusText}`
  827. })
  828. if (response.code === 2000) {
  829. ElMessage.success(`批量更新状态成功,已更新为${statusText}`)
  830. // 刷新数据
  831. await fetchNotificationList()
  832. // 清空选中状态
  833. selectedNotificationRows.value = []
  834. } else {
  835. ElMessage.error(response.message || '批量更新状态失败')
  836. }
  837. } catch (error) {
  838. if (error !== 'cancel') {
  839. console.error('批量更新状态失败:', error)
  840. ElMessage.error('批量更新状态失败,请重试')
  841. }
  842. }
  843. }
  844. // 下载模板
  845. const handleBatchInterview = async () => {
  846. try {
  847. const response = await axios.get(`${import.meta.env.VITE_API_URL}/api/system/sms_notifications/download_template/`, {
  848. headers: {
  849. 'Authorization': `JWT ${Session.get('token')}`
  850. },
  851. responseType: 'blob' // 确保响应类型为 blob,以便下载文件
  852. });
  853. // 创建一个 URL 对象
  854. const url = window.URL.createObjectURL(new Blob([response.data]));
  855. const link = document.createElement('a');
  856. link.href = url;
  857. link.setAttribute('download', 'template.xlsx'); // 设置下载文件名
  858. document.body.appendChild(link);
  859. link.click();
  860. link.remove();
  861. } catch (error) {
  862. console.error('下载模板失败:', error);
  863. }
  864. };
  865. // 页面打开后获取列表数据
  866. onMounted(async () => {
  867. // 获取职位列表
  868. await fetchPositions();
  869. // 获取申请状态统计
  870. await fetchStatusSummary();
  871. // 设置列权限
  872. const newOptions = await handleColumnPermission(GetPermission, crudOptions);
  873. // 添加数据加载后的回调,用于更新职位计数
  874. if (newOptions && newOptions.crudOptions && newOptions.crudOptions.request) {
  875. const originalPageRequest = newOptions.crudOptions.request.pageRequest;
  876. newOptions.crudOptions.request.pageRequest = async (query: any) => {
  877. const res = await originalPageRequest(query);
  878. updateCounts(res.data);
  879. return res;
  880. };
  881. }
  882. // 重置crudBinding
  883. resetCrudOptions(newOptions);
  884. /* getPositionList() */
  885. // 处理URL参数
  886. const urlStatus = route.query.status;
  887. console.log(urlStatus)
  888. if (urlStatus) {
  889. // 根据URL状态参数设置活动节点
  890. const statusValue = Number(urlStatus);
  891. switch (statusValue) {
  892. case 0:
  893. activeNode.value = 'status-0';
  894. break;
  895. case 1:
  896. activeNode.value = 'status-1';
  897. break;
  898. case 2:
  899. activeNode.value = 'status-2';
  900. break;
  901. case 3:
  902. activeNode.value = 'status-3';
  903. break;
  904. case 4:
  905. activeNode.value = 'status-4';
  906. break;
  907. default:
  908. activeNode.value = 'all';
  909. }
  910. // 使用doSearch进行带状态参数的搜索
  911. crudExpose.doSearch({
  912. form: {
  913. status: statusValue
  914. }
  915. });
  916. } else {
  917. // 没有URL参数时,执行普通刷新
  918. crudExpose.doRefresh();
  919. }
  920. fetchNotifications()
  921. });
  922. const fetchNotifications=async()=> {
  923. notificationLoading.value = true; // 开始加载
  924. try {
  925. const response = await axios.get(`${import.meta.env.VITE_API_URL}/api/system/sms_notifications/`,{
  926. params:{
  927. page: notificationPagination.current,
  928. limit: notificationPagination.size,
  929. name: notificationSearchForm.name,
  930. phone: notificationSearchForm.phone,
  931. },
  932. headers: {
  933. 'Authorization': `JWT ${Session.get('token')}`
  934. }
  935. });
  936. console.log(response.data)
  937. if(response.data.code === 2000){
  938. notificationList.value = response.data.data; // 更新通知列表
  939. notificationPagination.total = response.data.total || 0;
  940. }else{
  941. ElMessage.error(response.data.message || '获取通知列表失败');
  942. }
  943. } catch (error) {
  944. console.error('获取通知列表失败:', error);
  945. } finally {
  946. notificationLoading.value = false; // 结束加载
  947. }
  948. }
  949. const getPositionList = async () => {
  950. try {
  951. // 从环境变量或配置中获取
  952. const appid = 'wxc9655eeaa3223b75';
  953. const secret = 'c0f031b6e07ded1928fded435a913902';
  954. if (!appid || !secret) {
  955. throw new Error('微信配置信息不完整');
  956. }
  957. const response = await fetch(`${import.meta.env.VITE_API_URL}/wechat/generatescheme`);
  958. const data = await response.json();
  959. if (data.access_token) {
  960. return data.access_token;
  961. } else {
  962. throw new Error(data.errmsg || '获取access_token失败');
  963. }
  964. } catch (error) {
  965. console.error('获取微信access_token失败:', error);
  966. ElMessage.error('获取微信access_token失败');
  967. throw error;
  968. }
  969. }
  970. </script>
  971. <style scoped>
  972. .job-application-container {
  973. display: flex;
  974. flex-direction: column;
  975. height: 100%;
  976. margin-top: 15px;
  977. overflow: hidden;
  978. }
  979. /* 标签页容器样式 */
  980. .tabs-container {
  981. background-color: #fff;
  982. border-radius: 8px;
  983. margin-bottom: 10px;
  984. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  985. }
  986. .tabs-container :deep(.el-tabs__header) {
  987. margin: 0;
  988. padding: 0 20px;
  989. height: auto;
  990. }
  991. .tabs-container :deep(.el-tabs__nav-wrap) {
  992. padding: 0;
  993. }
  994. .tabs-container :deep(.el-tabs__item) {
  995. height: 50px;
  996. line-height: 50px;
  997. font-size: 14px;
  998. font-weight: 500;
  999. }
  1000. .tabs-container :deep(.el-tabs__content) {
  1001. padding: 20px;
  1002. }
  1003. /* 标签页内容样式 */
  1004. .tab-content {
  1005. display: flex;
  1006. height: calc(100vh - 200px);
  1007. }
  1008. /* 简历库样式 */
  1009. .resume-container {
  1010. width: 100%;
  1011. padding: 20px;
  1012. }
  1013. .resume-header {
  1014. text-align: center;
  1015. margin-bottom: 40px;
  1016. }
  1017. .resume-header h3 {
  1018. font-size: 24px;
  1019. color: #303133;
  1020. margin-bottom: 10px;
  1021. }
  1022. .resume-header p {
  1023. color: #909399;
  1024. font-size: 14px;
  1025. }
  1026. .resume-content {
  1027. display: flex;
  1028. justify-content: center;
  1029. align-items: center;
  1030. min-height: 300px;
  1031. }
  1032. /* 待通知列表样式 */
  1033. .notification-container {
  1034. width: 100%;
  1035. padding: 0;
  1036. }
  1037. /* 详情弹窗样式 */
  1038. .detail-content {
  1039. padding: 20px 0;
  1040. }
  1041. .detail-content .el-descriptions {
  1042. margin-bottom: 20px;
  1043. }
  1044. .detail-content .el-descriptions__label {
  1045. font-weight: 600;
  1046. color: #606266;
  1047. }
  1048. .detail-content .el-descriptions__content {
  1049. color: #303133;
  1050. }
  1051. .dialog-footer {
  1052. display: flex;
  1053. justify-content: flex-end;
  1054. gap: 10px;
  1055. }
  1056. /* 状态标签样式 */
  1057. .el-tag {
  1058. font-weight: 500;
  1059. }
  1060. .search-form {
  1061. background-color: #fff;
  1062. padding: 20px;
  1063. border-radius: 8px;
  1064. }
  1065. .action-bar {
  1066. display: flex;
  1067. justify-content: space-between;
  1068. align-items: center;
  1069. background-color: #fff;
  1070. padding: 16px 20px;
  1071. border-radius: 8px;
  1072. }
  1073. .left-actions {
  1074. display: flex;
  1075. gap: 12px;
  1076. }
  1077. .right-actions {
  1078. display: flex;
  1079. gap: 12px;
  1080. }
  1081. .table-container {
  1082. background-color: #fff;
  1083. border-radius: 8px;
  1084. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  1085. overflow: hidden;
  1086. }
  1087. .pagination-container {
  1088. background-color: #fff;
  1089. padding: 16px 20px;
  1090. border-radius: 8px;
  1091. /* display: flex;
  1092. justify-content: center; */
  1093. }
  1094. /* 原有的侧边栏和内容样式 */
  1095. .sidebar {
  1096. overflow: auto;
  1097. margin-top: 0;
  1098. width: 200px;
  1099. margin-right: 5px;
  1100. flex-shrink: 0;
  1101. background-color: #fff;
  1102. border-right: 1px solid #ebeef5;
  1103. border-radius: 10px;
  1104. }
  1105. .tree-container {
  1106. padding: 8px 0;
  1107. }
  1108. .tree-item {
  1109. display: flex;
  1110. justify-content: space-between;
  1111. align-items: center;
  1112. padding: 10px 16px;
  1113. cursor: pointer;
  1114. transition: background-color 0.3s;
  1115. width: 185px;
  1116. }
  1117. .item-content span{
  1118. overflow: hidden;
  1119. text-overflow: ellipsis;
  1120. white-space: nowrap;
  1121. width: 100%;
  1122. }
  1123. .tree-item:hover {
  1124. background-color: #f5f7fa;
  1125. }
  1126. .tree-item.active {
  1127. background-color: #f0f7ff;
  1128. color: #409eff;
  1129. border-right: 2px solid #409eff;
  1130. }
  1131. .item-content {
  1132. display: flex;
  1133. align-items: center;
  1134. gap: 8px;
  1135. min-width: 0;
  1136. flex: 1;
  1137. }
  1138. .item-count {
  1139. font-size: 12px;
  1140. color: #909399;
  1141. }
  1142. .category-title {
  1143. display: flex;
  1144. align-items: center;
  1145. padding: 12px 16px;
  1146. margin-top: 8px;
  1147. font-weight: 500;
  1148. border-top: 1px solid #ebeef5;
  1149. border-bottom: 1px solid #ebeef5;
  1150. gap: 8px;
  1151. cursor: pointer;
  1152. }
  1153. .expand-icon {
  1154. margin-left: auto;
  1155. transition: transform 0.3s;
  1156. }
  1157. .expand-icon.is-expanded {
  1158. transform: rotate(90deg);
  1159. }
  1160. .position-list {
  1161. padding-left: 8px;
  1162. }
  1163. .content {
  1164. flex: 1;
  1165. overflow: hidden;
  1166. }
  1167. .action-button-wrapper {
  1168. display: flex;
  1169. align-items: center;
  1170. height: 100%;
  1171. margin-left: 8px;
  1172. }
  1173. .action-button-wrapper :deep(.el-upload) {
  1174. display: flex;
  1175. align-items: center;
  1176. }
  1177. .action-button-wrapper :deep(.el-button) {
  1178. margin: 0;
  1179. }
  1180. :deep(.upload-demo){
  1181. height: 24px;
  1182. }
  1183. </style>