index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. <!-- -->
  2. <template>
  3. <div>
  4. <ContentWrap>
  5. <!-- 搜索工作栏 -->
  6. <el-form
  7. class="-mb-15px"
  8. :model="queryParams"
  9. ref="queryFormRef"
  10. :inline="true"
  11. >
  12. <div>
  13. <el-form-item label="企业" prop="enterpriseId">
  14. <el-select
  15. v-model="queryParams.enterpriseId"
  16. filterable
  17. placeholder="请选择企业"
  18. clearable
  19. class="!w-240px"
  20. @change="enterpriseIdSelectChange"
  21. >
  22. <el-option
  23. v-for="dict in enterpriseOption"
  24. :key="dict.value"
  25. :label="dict.name"
  26. :value="dict.id"
  27. />
  28. </el-select>
  29. </el-form-item>
  30. <el-form-item label="部门" prop="deptId">
  31. <el-select
  32. v-model="queryParams.deptId"
  33. placeholder="请选择部门"
  34. clearable
  35. class="!w-240px"
  36. >
  37. <el-option
  38. v-for="dict in deptOption"
  39. :key="dict.value"
  40. :label="dict.label"
  41. :value="dict.value"
  42. />
  43. </el-select>
  44. </el-form-item>
  45. <el-form-item label="用户" prop="userId">
  46. <el-select
  47. v-model="queryParams.userId"
  48. placeholder="请选择用户"
  49. clearable
  50. class="!w-240px"
  51. @change="userIdSelectChange"
  52. >
  53. <el-option
  54. v-for="dict in userOption"
  55. :key="dict.value"
  56. :label="dict.name"
  57. :value="dict.userId"
  58. />
  59. </el-select>
  60. </el-form-item>
  61. <el-form-item label="职位" prop="jobId">
  62. <el-select
  63. v-model="queryParams.jobId"
  64. placeholder="请选择职位"
  65. :no-data-text="!jobOption.length && queryParams.enterpriseId ? '当前选中企业暂无发布职位' : '请先选择企业'"
  66. clearable
  67. class="!w-240px"
  68. >
  69. <el-option
  70. v-for="dict in jobOption"
  71. :key="dict.value"
  72. :label="dict.name"
  73. :value="dict.id"
  74. />
  75. </el-select>
  76. </el-form-item>
  77. </div>
  78. <el-form-item label="" prop="type">
  79. <el-radio-group v-model="queryParams.type" @change="typeChange">
  80. <el-radio-button label="全部" value="-1" />
  81. <el-radio-button label="24小时内" value="3" />
  82. <el-radio-button label="7天内" value="0" />
  83. <!-- <el-radio-button label="上个月" value="1" /> -->
  84. <el-radio-button label="30天内" value="4" />
  85. <el-radio-button label="3个月内" value="6" />
  86. <!-- <el-radio-button label="上个季度" value="2" /> -->
  87. <el-radio-button label="1年内" value="5" />
  88. </el-radio-group>
  89. </el-form-item>
  90. <el-form-item label="" prop="time">
  91. <span class="mr-3">自定义日期</span>
  92. <el-date-picker
  93. v-model="queryParams.time"
  94. value-format="YYYY-MM-DD HH:mm:ss"
  95. type="daterange"
  96. start-placeholder="开始日期"
  97. end-placeholder="结束日期"
  98. :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
  99. class="!w-240px"
  100. @change="timeRangeChange"
  101. />
  102. </el-form-item>
  103. <el-form-item>
  104. <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
  105. <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
  106. </el-form-item>
  107. </el-form>
  108. </ContentWrap>
  109. <div class="flex flex-col">
  110. <!-- 统计 -->
  111. <div>
  112. <CardTitle title="招聘进展" class="m-l-5px"/>
  113. <div style="text-align: end;">
  114. <el-button type="success" plain @click="handleExport" :loading="exportLoading">
  115. <Icon icon="ep:download" class="mr-5px" /> 明细导出
  116. </el-button>
  117. </div>
  118. <el-row :gutter="16" class="row m-t-10px">
  119. <el-col v-for="item in statisticList" :key="item.name" :md="4" :sm="12" :xs="24" :loading="loading">
  120. <ComparisonCard
  121. :title="item.title"
  122. :value="statistic[item.name]"
  123. style="cursor: pointer;"
  124. @click="openDialog(item)"
  125. />
  126. </el-col>
  127. </el-row>
  128. </div>
  129. <!-- 职位发布情况 -->
  130. <JobStatistics :queryParams="queryParams" class="m-b-10px" />
  131. <!-- 图表 -->
  132. <div>
  133. <CardTitle title="应聘简历分析" class="m-l-5px" />
  134. <el-row :gutter="16" class="row m-t-20px">
  135. <el-col :md="12">
  136. <SexDistribution :data="distribution.sexDistributionData" title="性别分布" />
  137. </el-col>
  138. <el-col :md="12">
  139. <ageDistribution :data="distribution.ageDistributionData" title="年龄分布" />
  140. </el-col>
  141. </el-row>
  142. <el-row :gutter="16" class="row">
  143. <el-col :md="12">
  144. <workExperience :data="distribution.workExperienceData" title="工作年限分布" />
  145. </el-col>
  146. <el-col :md="12">
  147. <education :data="distribution.educationData" title="学历分布" />
  148. </el-col>
  149. </el-row>
  150. </div>
  151. </div>
  152. <!-- 弹窗 -->
  153. <Dialog :title="currentItem.title+'详情'" v-model="showDialog" width="1200" @close="closeDialog">
  154. <ContentWrap>
  155. <el-table v-loading="dialogLoading" :data="tableData" :stripe="true">
  156. <el-table-column
  157. v-for="item in tableHeaders[currentItem.name]"
  158. :key="item.prop"
  159. :prop="item.prop"
  160. :label="item.name"
  161. :align="item.align || 'center'"
  162. />
  163. </el-table>
  164. <!-- 分页 -->
  165. <Pagination
  166. :total="total"
  167. v-model:page="page.pageNo"
  168. v-model:limit="page.pageSize"
  169. @pagination="paginationChange"
  170. />
  171. </ContentWrap>
  172. </Dialog>
  173. </div>
  174. </template>
  175. <script setup>
  176. import ComparisonCard from './components/ComparisonCard.vue'
  177. import SexDistribution from './components/SexDistribution.vue'
  178. import ageDistribution from './components/AgeDistribution.vue'
  179. import workExperience from './components/WorkExperience.vue'
  180. import education from './components/Education.vue'
  181. import JobStatistics from './job.vue'
  182. import { statisticAnalysisApi } from '@/api/menduner/system/analysis/statisticAnalysis'
  183. defineOptions({name: 'StatisticAnalysis'})
  184. import download from '@/utils/download'
  185. const loading = ref(true) // 加载中
  186. const dialogLoading = ref(false)
  187. /** 初始化 **/
  188. onMounted(async () => {
  189. loading.value = true
  190. })
  191. const message = useMessage() // 消息弹窗
  192. const exportLoading = ref(false)
  193. const page = reactive({ pageNo: 1, pageSize: 10 })
  194. const queryParams = reactive({
  195. type: '0',
  196. enterpriseId: undefined,
  197. deptId: undefined,
  198. userId: undefined,
  199. jobId: undefined,
  200. time: [],
  201. })
  202. // 招聘进展明细导出
  203. const handleExport = async () => {
  204. try {
  205. // 导出的二次确认
  206. await message.exportConfirm()
  207. // 发起导出
  208. exportLoading.value = true
  209. const data = await statisticAnalysisApi.analysisExport({ ...queryParams, jobStatus: '0' })
  210. download.excel(data, '招聘进展明细')
  211. } catch {
  212. } finally {
  213. exportLoading.value = false
  214. }
  215. }
  216. const queryFormRef = ref() // 搜索的表单
  217. /** 重置按钮操作 */
  218. const resetQuery = () => {
  219. queryFormRef.value.resetFields()
  220. handleQuery()
  221. }
  222. const typeChange = (value) => { //
  223. if (value) {
  224. queryParams.time = []
  225. }
  226. handleQuery()
  227. }
  228. const timeRangeChange = (value) => {
  229. if (value?.length) queryParams.type = '99' // 自定义
  230. else queryParams.type = '0'
  231. }
  232. const apiArr = reactive({
  233. // 统计
  234. pageViewsTotal: statisticAnalysisApi.getAnalysisJobBrowseNum, // 职位浏览量-总数据
  235. pageViews: statisticAnalysisApi.getAnalysisJobBrowseNumPage, // 职位浏览量-钻取
  236. resumeReceived: statisticAnalysisApi.getAnalysisJobCvNewPage, // 钻取
  237. pushNum: statisticAnalysisApi.getAnalysisJobNumPage, // 钻取
  238. resumeViewed: statisticAnalysisApi.getAnalysisJobCvLookPage, // 钻取
  239. invitedInterviews: statisticAnalysisApi.getAnalysisInterviewWaitPage, // 钻取
  240. invitedCompleted: statisticAnalysisApi.getAnalysisInterviewCompletePage, // 钻取
  241. // 分布
  242. sexDistributionData: statisticAnalysisApi.getAnalysisJobCvSexCount,
  243. ageDistributionData: statisticAnalysisApi.getAnalysisJobCvAgeCount,
  244. workExperienceData: statisticAnalysisApi.getAnalysisJobCvExpCount,
  245. educationData: statisticAnalysisApi.getAnalysisJobCvEduCount,
  246. })
  247. // 统计
  248. const statisticList = [
  249. { title: '发布职位数量', name: 'pushNum' },
  250. { title: '职位浏览量', name: 'pageViews' },
  251. { title: '收到的简历', name: 'resumeReceived' },
  252. { title: '已查看简历', name: 'resumeViewed' },
  253. { title: '已邀面试', name: 'invitedInterviews' },
  254. { title: '面试完成', name: 'invitedCompleted' },
  255. ]
  256. // 统计
  257. const statistic = reactive({
  258. pageViews: 0,
  259. pushNum: 0,
  260. resumeReceived: 0,
  261. resumeViewed: 0,
  262. invitedInterviews: 0,
  263. invitedCompleted: 0,
  264. })
  265. // 分布
  266. const distribution = reactive({
  267. sexDistributionData: {},
  268. ageDistributionData: {},
  269. workExperienceData: {},
  270. educationData: {},
  271. })
  272. // 统计
  273. const tableData = ref([])
  274. const total = ref(0)
  275. const getList = async (typeName, details = '') => {
  276. loading.value = true
  277. try {
  278. let data
  279. if (!details && typeName === 'pageViews') {
  280. // 职位浏览量-总数据
  281. const res = await apiArr.pageViewsTotal(queryParams)
  282. data = { total: res || 0 }
  283. } else {
  284. // 使用钻取接口
  285. const params = { ...queryParams, ...page }
  286. if (typeName === 'pushNum') params.jobStatus = 0 // 发布职位数量单独加上jobStatus字段
  287. data = await apiArr[typeName](params)
  288. }
  289. if (details) {
  290. tableData.value = data.list || []
  291. total.value = data.total || 0
  292. dealTableData()
  293. } else {
  294. statistic[typeName] = data.total || 0
  295. }
  296. } finally {
  297. loading.value = false
  298. dialogLoading.value = false
  299. }
  300. }
  301. import { getDictOptions } from '@/utils/transform/getText'
  302. import { timesTampChange } from '@/utils/transform/date'
  303. import { DICT_TYPE, getDictLabel } from '@/utils/dict'
  304. const getText = (value, arr, itemText = 'name', itemValue = 'id') => { // 一维数组
  305. if (!arr?.length || !(value && value !== 0)) return
  306. const item = arr.find(formItem => formItem[itemValue] === value)
  307. if (!item) return
  308. return item[itemText]
  309. }
  310. // 分布
  311. const getDistributionCount = async (typeName) => {
  312. try {
  313. const data = await apiArr[typeName](queryParams)
  314. distribution[typeName] = data || {}
  315. } catch (error) {
  316. console.log(error)
  317. }
  318. }
  319. /** 搜索按钮操作 */
  320. const handleQuery = () => {
  321. if (Object.keys(statistic).length) {
  322. Object.keys(statistic).forEach(name => {
  323. getList(name)
  324. })
  325. }
  326. //
  327. if (Object.keys(distribution).length) {
  328. Object.keys(distribution).forEach(name => {
  329. getDistributionCount(name)
  330. })
  331. }
  332. }
  333. handleQuery()
  334. const showDialog = ref(false)
  335. const currentItem = ref({})
  336. // 打开弹窗
  337. const openDialog = (item) => {
  338. dialogLoading.value = true
  339. currentItem.value = item
  340. page.pageNo = 1
  341. tableData.value = []
  342. getList(item.name, '钻取')
  343. showDialog.value = true
  344. }
  345. const closeDialog = () => {
  346. }
  347. const paginationChange = () => {
  348. getList(currentItem.value.name, '钻取')
  349. }
  350. const tableHeaders = {
  351. // 职位浏览量
  352. pageViews: [
  353. { name: '浏览量', prop: 'num' },
  354. { name: '招聘职位', prop: 'name' },
  355. { name: '发布企业', prop: 'enterpriseName' },
  356. { name: '薪酬', prop: 'salaryDisplay' },
  357. { name: '工作地区', prop: 'areaName' },
  358. { name: '工作经验', prop: 'expName' },
  359. { name: '学历要求', prop: 'eduName' },
  360. ],
  361. // 发布职位数量
  362. pushNum: [
  363. { name: '职位名称', prop: 'name' },
  364. { name: '发布企业', prop: 'enterpriseName' },
  365. { name: '职位类型', prop: 'positionName' },
  366. { name: '薪酬', prop: 'salaryDisplay' },
  367. { name: '工作地区', prop: 'areaName' },
  368. { name: '工作经验', prop: 'expName' },
  369. { name: '学历要求', prop: 'eduName' },
  370. { name: '众聘', prop: 'hire' },
  371. ],
  372. // 收到的简历
  373. resumeReceived: [
  374. { name: '投递人', prop: 'personName' },
  375. { name: '求职状态', prop: 'jobStatus' },
  376. { name: '投递职位', prop: 'job.name' },
  377. { name: '发布企业', prop: 'enterpriseName' },
  378. { name: '薪酬', prop: 'salaryDisplay' },
  379. { name: '工作地区', prop: 'areaName' },
  380. { name: '工作经验', prop: 'expName' },
  381. { name: '学历要求', prop: 'eduName' },
  382. ],
  383. // 已查看简历
  384. resumeViewed: [
  385. { name: '简历标题', prop: 'title' },
  386. { name: '投递人', prop: 'personName' },
  387. { name: '投递职位', prop: 'job.name' },
  388. { name: '发布企业', prop: 'enterpriseName' },
  389. { name: '投递类型', prop: 'typeName' },
  390. { name: '推荐人', prop: 'recommendPersonName' },
  391. ],
  392. // 已邀面试
  393. invitedInterviews: [
  394. { name: '求职者', prop: 'personName' },
  395. { name: '面试岗位', prop: 'jobName' },
  396. { name: '面试类型', prop: 'typeName' },
  397. { name: '面试时间', prop: 'timeName' },
  398. { name: '面试地点', prop: 'addressName' },
  399. ],
  400. // 面试完成
  401. invitedCompleted: [
  402. { name: '求职者', prop: 'personName' },
  403. { name: '面试岗位', prop: 'jobName' },
  404. { name: '发布企业', prop: 'enterpriseName' },
  405. { name: '面试类型', prop: 'typeName' },
  406. { name: '面试时间', prop: 'timeName' },
  407. { name: '面试地点', prop: 'addressName' },
  408. { name: '反馈评价', prop: 'evaluate' },
  409. ],
  410. }
  411. const dealTableData = async () => {
  412. // 职位浏览量
  413. if (currentItem.value.name === 'pageViews') {
  414. const areaList = await getDictOptions('areaList')
  415. tableData.value = tableData.value.map(item => {
  416. item.salaryDisplay = item.payFrom && item.payTo ? `${item.payFrom}-${item.payTo}/${getDictLabel(DICT_TYPE.MENDUNER_PAY_UNIT, item.payUnit)}` : '面议'
  417. item.areaName = !item.areaId ? '全国' : getText(item.areaId, areaList)
  418. item.expName = getDictLabel(DICT_TYPE.MENDUNER_EXP_TYPE, item.expType)
  419. item.eduName = getDictLabel(DICT_TYPE.MENDUNER_EDUCATION_TYPE, item.eduType)
  420. return item
  421. })
  422. }
  423. // 发布职位数量
  424. if (currentItem.value.name === 'pushNum') {
  425. const areaList = await getDictOptions('areaList')
  426. const positionData = await getDictOptions('positionData')
  427. tableData.value = tableData.value.map(item => {
  428. item.areaName = !item.areaId ? '全国' : getText(item.areaId, areaList)
  429. item.positionName = getText(item.positionId, positionData, 'nameCn', 'id')
  430. item.salaryDisplay = item.payFrom && item.payTo ? `${item.payFrom}-${item.payTo}/${getDictLabel(DICT_TYPE.MENDUNER_PAY_UNIT, item.payUnit)}` : '面议'
  431. item.jobStatus = getDictLabel(DICT_TYPE.MENDUNER_JOB_SEEK_STATUS, item.jobStatus)
  432. item.expName = getDictLabel(DICT_TYPE.MENDUNER_EXP_TYPE, item.expType)
  433. item.eduName = getDictLabel(DICT_TYPE.MENDUNER_EDUCATION_TYPE, item.eduType)
  434. item.hire = item.hire ? '是' : '否'
  435. return item
  436. })
  437. }
  438. // 收到的简历
  439. if (currentItem.value.name === 'resumeReceived') {
  440. const areaList = await getDictOptions('areaList')
  441. tableData.value = tableData.value.map(item => {
  442. item.areaName = !item.job.areaId ? '全国' : getText(item.job.areaId, areaList)
  443. item.salaryDisplay = item.job?.payFrom && item.job.payTo ? `${item.job.payFrom}-${item.job.payTo}/${getDictLabel(DICT_TYPE.MENDUNER_PAY_UNIT, item.job.payUnit)}` : '面议'
  444. item.jobStatus = getDictLabel(DICT_TYPE.MENDUNER_JOB_SEEK_STATUS, item.person.jobStatus)
  445. item.expName = getDictLabel(DICT_TYPE.MENDUNER_EXP_TYPE, item.job.expType)
  446. item.eduName = getDictLabel(DICT_TYPE.MENDUNER_EDUCATION_TYPE, item.job.eduType)
  447. item.personName = item.person.name
  448. return item
  449. })
  450. }
  451. // 已查看简历
  452. if (currentItem.value.name === 'resumeViewed') {
  453. tableData.value = tableData.value.map(item => {
  454. item.personName = item.person.name
  455. item.address = item.job.address
  456. item.typeName = Number(item.type) === 0 ? '平台投递': '赏金投递'
  457. item.recommendPersonName = item.recommendPerson?.name || ''
  458. return item
  459. })
  460. }
  461. // 已邀面试
  462. if (currentItem.value.name === 'invitedInterviews') {
  463. tableData.value = tableData.value.map(item => {
  464. item.personName = item.person.name
  465. item.jobName = item.job.name
  466. item.typeName = item.type === 0 ? '线上面试': '线下面试'
  467. item.timeName = timesTampChange(item.time, 'Y-M-D h:m')
  468. item.addressName = item.job.address
  469. return item
  470. })
  471. }
  472. // 面试完成
  473. if (currentItem.value.name === 'invitedCompleted') {
  474. tableData.value = tableData.value.map(item => {
  475. item.personName = item.person.name
  476. item.jobName = item.job.name
  477. item.typeName = item.type === 0 ? '线上面试': '线下面试'
  478. item.timeName = timesTampChange(item.time, 'Y-M-D h:m')
  479. item.addressName = item.job.address
  480. return item
  481. })
  482. }
  483. }
  484. // 企业
  485. const enterpriseOption = ref([])
  486. const getEnterpriseOption = async () => {
  487. try {
  488. const data = await statisticAnalysisApi.getAnalysisEnterpriseSimpleList()
  489. enterpriseOption.value = data || []
  490. } catch (error) {
  491. console.log(error)
  492. }
  493. }
  494. getEnterpriseOption()
  495. // 部门
  496. const deptOption = ref([])
  497. // 用户
  498. const userOption = ref([])
  499. const getUserOption = async () => {
  500. try {
  501. const params ={ enterpriseId: queryParams.enterpriseId }
  502. const data = await statisticAnalysisApi.getAnalysisEnterpriseUserList(params)
  503. userOption.value = data || []
  504. } catch (error) {
  505. console.log(error)
  506. }
  507. }
  508. getUserOption()
  509. // 职位
  510. const jobOption = ref([])
  511. const getJobOption = async () => {
  512. try {
  513. const params ={
  514. enterpriseId: queryParams.enterpriseId,
  515. deptId: queryParams.deptId,
  516. status: 0,
  517. }
  518. const data = await statisticAnalysisApi.getAnalysisJobAdvertisedList(params)
  519. jobOption.value = data || []
  520. } catch (error) {
  521. console.log(error)
  522. }
  523. }
  524. // getJobOption()
  525. const enterpriseIdSelectChange = (val) => {
  526. queryParams.deptId = undefined
  527. queryParams.jobId = undefined
  528. getUserOption()
  529. getJobOption()
  530. }
  531. const userIdSelectChange = (val) => {
  532. queryParams.jobId = undefined
  533. getJobOption()
  534. }
  535. </script>
  536. <style lang="scss" scoped>
  537. .row {
  538. .el-col {
  539. margin-bottom: 1rem;
  540. }
  541. }
  542. </style>