index.vue 16 KB

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