index.vue 14 KB

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