소스 검색

招聘会统计分析

Xiao_123 4 달 전
부모
커밋
7e339d0b37

+ 30 - 0
src/api/menduner/system/jobFair/manage/index.ts

@@ -51,5 +51,35 @@ export const JobFairManageApi = {
   // 导出招聘会管理 Excel
   exportJobFair: async (params) => {
     return await request.download({ url: `/menduner/system/job-fair/export-excel`, params })
+  },
+
+  // 企业职位面试的情况或入职情况
+  getEnterpriseInterviewInvite: async (params) => {
+    return await request.get({ url: `/menduner/system/job-fair/analysis/enterprise/interview-invite`, params })
+  },
+
+  // 企业职位的情况
+  getEnterpriseJob: async (params) => {
+    return await request.get({ url: `/menduner/system/job-fair/analysis/enterprise/job`, params })
+  },
+
+  // 企业职位投递的简历情况
+  getEnterpriseJobCvRel: async (params) => {
+    return await request.get({ url: `/menduner/system/job-fair/analysis/enterprise/job/cv-rel`, params })
+  },
+
+  // 企业每个时间的投递简历数量统计
+  getEnterpriseJobCvRelBar: async (params) => {
+    return await request.get({ url: `/menduner/system/job-fair/analysis/get/job/cv-rel/bar`, params })
+  },
+
+  // 用户面试的情况 or 入职情况
+  getUserInterviewInvite: async (params) => {
+    return await request.get({ url: `/menduner/system/job-fair/analysis/user/interview-invite`, params })
+  },
+
+  // 用户投递的简历情况
+    getUserJobCvRel: async (params) => {
+    return await request.get({ url: `/menduner/system/job-fair/analysis/user/job/cv-rel`, params })
   }
 }

+ 42 - 0
src/views/menduner/system/jobFair/manage/statistics/components/ComparisonCard.vue

@@ -0,0 +1,42 @@
+<template>
+  <div class="flex flex-col gap-2 bg-[var(--el-bg-color-overlay)] p-6">
+    <div class="flex items-center justify-between text-gray-500">
+      <span>{{ title }}</span>
+      <!-- <el-tag>{{ tag }}</el-tag> -->
+    </div>
+    <div class="flex flex-row items-baseline justify-between">
+      <CountTo :prefix="prefix" :end-val="value" :decimals="decimals" class="text-3xl" />
+      <!-- <span :class="toNumber(percent) > 0 ? 'text-red-500' : 'text-green-500'">
+        {{ Math.abs(toNumber(percent)) }}%
+        <Icon :icon="toNumber(percent) > 0 ? 'ep:caret-top' : 'ep:caret-bottom'" class="!text-sm" />
+      </span> -->
+    </div>
+    <!-- <el-divider class="mb-1! mt-2!" />
+    <div class="flex flex-row items-center justify-between text-sm">
+      <span class="text-gray-500">昨日数据</span>
+      <span>{{ prefix || '' }}{{ reference }}</span>
+    </div> -->
+  </div>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+// import { toNumber } from 'lodash-es'
+// import { calculateRelativeRate } from '@/utils'
+
+/** 交易对照卡片 */
+defineOptions({ name: 'ComparisonCard' })
+
+defineProps({
+  title: propTypes.string.def('').isRequired,
+  tag: propTypes.string.def(''),
+  prefix: propTypes.string.def(''),
+  value: propTypes.number.def(0).isRequired,
+  // reference: propTypes.number.def(0).isRequired,
+  decimals: propTypes.number.def(0)
+})
+
+// 计算环比
+// const percent = computed(() =>
+//   calculateRelativeRate(props.value as number, props.reference as number)
+// )
+</script>

+ 65 - 0
src/views/menduner/system/jobFair/manage/statistics/components/delivery.vue

@@ -0,0 +1,65 @@
+<!--  -->
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <CardTitle :title="props.title" />
+    </template>
+    <div>
+      <Echart :height="300" :options="chartOptions" />
+    </div>
+  </el-card>
+</template>
+
+<script lang="ts" setup>
+// import Echart from '@/components/Echart'
+import { EChartsOption } from 'echarts'
+defineOptions({name: 'AgeDistribution'})
+const props = defineProps({
+  title: {
+    type: String,
+    default: () => ''
+  },
+  data: {
+    type: Object,
+    default: () => {}
+  },
+})
+
+const chartOptions = reactive<EChartsOption>({
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  grid: {
+    left: '3%',
+    right: '4%',
+    bottom: '3%',
+    containLabel: true
+  },
+  xAxis: {
+    type: 'category',
+    data: []
+  },
+  yAxis: {
+    type: 'value'
+  },
+  series: [
+    {
+      data: [],
+      type: 'bar'
+    }
+  ]
+}) as EChartsOption
+
+watch(() => props.data, (newVal: any) => {
+  if (!Object.keys(newVal).length) {
+    chartOptions.xAxis.data = []
+    chartOptions.series![0].data = []
+    return
+  }
+  chartOptions.xAxis.data = newVal.x || []
+  chartOptions.series![0].data = newVal.y || []
+})
+</script>

+ 78 - 0
src/views/menduner/system/jobFair/manage/statistics/components/enterpriseJob.vue

@@ -0,0 +1,78 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <CardTitle :title="props.title" />
+    </template>
+		<el-table v-loading="loading" :data="list" :stripe="true">
+			<el-table-column label="发布企业" align="center" prop="enterprise.name">
+        <template #default="scope">{{ formatName(scope.row.enterprise.anotherName || scope.row.enterprise.name) }}</template>
+      </el-table-column>
+      <el-table-column label="职位名称" align="center" prop="name">
+        <template #default="scope">{{ formatName(scope.row.name) }}</template>
+      </el-table-column>
+			<el-table-column label="薪资" align="center" prop="payFrom">
+        <template #default="scope">{{ scope.row.payFrom && scope.row.payTo ? scope.row.payFrom + '-' + scope.row.payTo + '/' + getIntDictOptions(DICT_TYPE.MENDUNER_PAY_UNIT).find(e => e.value === Number(scope.row.payUnit))?.label : '面议' }}</template>
+      </el-table-column>
+      <el-table-column label="工作经验" align="center" prop="expType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.MENDUNER_EXP_TYPE" :value="scope.row.expType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="学历要求" align="center" prop="eduType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.MENDUNER_EDUCATION_TYPE" :value="scope.row.eduType" />
+        </template>
+      </el-table-column>
+			<el-table-column label="地区" align="center" prop="areaId">
+        <template #default="scope">{{ !scope.row.areaId ? '全国' : scope.row.area.str }}</template>
+      </el-table-column>
+			<el-table-column label="更新时间" align="center" prop="updateTime" :formatter="dateFormatter" width="180px" />
+      <el-table-column label="发布时间" align="center" prop="createTime" :formatter="dateFormatter" width="180px" />
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+		/>
+  </el-card>
+</template>
+
+<script lang="ts" setup>
+defineOptions({name: 'EnterpriseJob'})
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { JobFairManageApi } from '@/api/menduner/system/jobFair/manage'
+import { formatName } from '@/utils'
+
+const props = defineProps({
+  title: {
+    type: String,
+    default: () => ''
+  },
+  query: {
+    type: Object,
+    default: () => {}
+  }
+})
+
+const total = ref(0)
+const loading = ref(false)
+const list = ref([])
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10
+})
+
+const getList = async () => {
+	const result = await JobFairManageApi.getEnterpriseJob(Object.assign(queryParams, props.query))
+
+	list.value = result.list
+	total.value = result.total
+}
+
+watch(() => props.query, () => {
+	getList()
+}, { deep: true, immediate: true })
+</script>

+ 68 - 0
src/views/menduner/system/jobFair/manage/statistics/components/userJobDelivery.vue

@@ -0,0 +1,68 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <CardTitle :title="props.title" />
+    </template>
+		<el-table v-loading="loading" :data="list" :stripe="true">
+			<el-table-column label="投递人" align="center" prop="person.name" />
+      <el-table-column label="投递职位" align="center" prop="name">
+        <template #default="scope">{{ formatName(scope.row.job.name) }}</template>
+      </el-table-column>
+			<el-table-column label="职位发布企业" align="center" prop="enterpriseName">
+        <template #default="scope">{{ formatName(scope.row.enterpriseName) }}</template>
+      </el-table-column>
+			<el-table-column label="投递简历名" align="center" prop="title" />
+      <el-table-column
+        label="投递时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+		/>
+  </el-card>
+</template>
+
+<script lang="ts" setup>
+defineOptions({name: 'UserJobDelivery'})
+import { dateFormatter } from '@/utils/formatTime'
+import { JobFairManageApi } from '@/api/menduner/system/jobFair/manage'
+import { formatName } from '@/utils'
+
+const props = defineProps({
+  title: {
+    type: String,
+    default: () => ''
+  },
+  query: {
+    type: Object,
+    default: () => {}
+  }
+})
+
+const total = ref(0)
+const loading = ref(false)
+const list = ref([])
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10
+})
+
+const getList = async () => {
+	const result = await JobFairManageApi.getUserJobCvRel(Object.assign(queryParams, props.query))
+
+	list.value = result.list
+	total.value = result.total
+}
+
+watch(() => props.query, () => {
+	getList()
+}, { deep: true, immediate: true })
+</script>

+ 354 - 5
src/views/menduner/system/jobFair/manage/statistics/index.vue

@@ -1,14 +1,363 @@
-<!-- 招聘会数据统计详情 -->
 <template>
-  <div>招聘会数据统计详情</div>
+  <div>
+    <!-- 搜索工作栏 -->
+    <ContentWrap>
+      <el-form
+        class="-mb-15px"
+        :model="queryParams"
+        ref="queryFormRef"
+        :inline="true"
+      >
+        <div>
+          <el-form-item label="企业" prop="enterpriseId">
+            <el-select
+              v-model="queryParams.enterpriseId"
+              filterable
+              placeholder="请选择企业"
+              clearable
+              class="!w-240px"
+              @change="enterpriseIdSelectChange"
+            >
+              <el-option
+                v-for="dict in enterpriseOption"
+                :key="dict.value"
+                :label="dict.name"
+                :value="dict.id"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="职位" prop="jobId">
+            <el-select
+              v-model="queryParams.jobId"
+              placeholder="请选择职位"
+              :no-data-text="!jobOption.length && queryParams.enterpriseId ? '当前选中企业暂无发布职位' : '请先选择企业'"
+              clearable
+              class="!w-240px"
+            >
+              <el-option
+                v-for="dict in jobOption"
+                :key="dict.value"
+                :label="dict.name"
+                :value="dict.id"
+              />
+            </el-select>
+          </el-form-item>
+        </div>
+        <el-form-item label="" prop="type">
+          <el-radio-group v-model="queryParams.type" @change="typeChange">
+            <el-radio-button label="全部" value="-1" />
+          <el-radio-button label="24小时内" value="3" />
+          <el-radio-button label="7天内" value="0" />
+          <el-radio-button label="30天内" value="4" />
+          <el-radio-button label="3个月内" value="6" />
+          <el-radio-button label="1年内" value="5" />
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="" prop="time">
+          <span class="mr-3">自定义日期</span>
+          <el-date-picker
+            v-model="queryParams.time"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="daterange"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="!w-240px"
+            @change="timeRangeChange"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+          <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+
+    <el-row :gutter="16" class="row m-y-20px">
+      <el-col v-for="item in statisticList" :key="item.name" :md="4" :sm="12" :xs="24" :loading="loading">
+        <ComparisonCard
+          :title="item.title"
+          :value="statistic[item.name]"
+          style="cursor: pointer;"
+          @click="openDialog(item)"
+        />
+      </el-col>
+    </el-row>
+
+    <Delivery :data="jobCvRelBar" title="企业职位简历投递量统计" />
+
+    <!-- <UserJobDelivery class="mt-10px" :query="queryParams" title="用户简历投递量明细" /> -->
+
+    <!-- <EnterpriseJob class="my-10px" :query="queryParams" title="企业发布职位明细" /> -->
+
+    <!-- 弹窗 -->
+    <Dialog :title="currentItem.title+'详情'" v-model="showDialog" width="1200" @close="showDialog = false">
+      <ContentWrap>
+        <el-table v-loading="dialogLoading" :data="tableData" :stripe="true">
+          <el-table-column
+            v-for="item in tableHeaders[currentItem.name]"
+            :key="item.prop"
+            :prop="item.prop"
+            :label="item.name"
+            :align="item.align || 'center'"
+          />
+        </el-table>
+        <!-- 分页 -->
+        <Pagination
+          :total="total"
+          v-model:page="page.pageNo"
+          v-model:limit="page.pageSize"
+          @pagination="paginationChange"
+        />
+      </ContentWrap>
+    </Dialog>
+  </div>
 </template>
 
-<script setup lang="ts">
+<script setup>
 /** 招聘会 表单 */
 defineOptions({ name: 'JobFairStatistics' })
-// const { query } = useRoute() // 路由信息
+import { JobFairManageApi } from '@/api/menduner/system/jobFair/manage'
+import { statisticAnalysisApi } from '@/api/menduner/system/analysis/statisticAnalysis'
+import Delivery from './components/delivery.vue'
+import UserJobDelivery from './components/userJobDelivery.vue'
+import EnterpriseJob from './components/enterpriseJob.vue'
+import ComparisonCard from './components/ComparisonCard.vue'
+import { formatName } from '@/utils'
+import { timesTampChange } from '@/utils/transform/date'
+import { DICT_TYPE, getDictLabel } from '@/utils/dict'
+
 const route = useRoute() // 路由信息
 const { id } = route.params
-console.log(id)
 
+const loading = ref(false)
+const dialogLoading = ref(false)
+const currentItem = ref({})
+const tableData = ref([])
+const showDialog = ref(false)
+const total = ref(0)
+const page = reactive({ pageNo: 1, pageSize: 10 })
+
+const queryFormRef = ref()
+const queryParams = reactive({
+  type: '0',
+  enterpriseId: undefined,
+  deptId: undefined,
+  userId: undefined,
+  jobId: undefined,
+  jobFairId: id,
+  time: [],
+})
+
+// 统计
+const statisticList = [
+  { title: '企业邀请面试数', name: 'inviteInterviewNum' },
+  { title: '企业职位数', name: 'jobNum' },
+  { title: '企业职位投递量', name: 'deliveryNum' },
+  { title: '用户面试数', name: 'interviewNum' },
+  { title: '用户简历投递数', name: 'cvDeliveryNum' }
+]
+// 统计
+const statistic = reactive({
+  inviteInterviewNum: 0,
+  jobNum: 0,
+  deliveryNum: 0,
+  interviewNum: 0,
+  cvDeliveryNum: 0
+})
+const tableHeaders = {
+  // 企业邀请面试数
+  inviteInterviewNum: [
+    { name: '求职者', prop: 'personName' },
+    { name: '邀请企业', prop: 'enterpriseName' },
+    { name: '面试岗位', prop: 'jobName' },
+    { name: '面试类型', prop: 'typeName' },
+    { name: '面试时间', prop: 'timeName' },
+    { name: '面试地点', prop: 'addressName' },
+  ],
+  // 企业职位情况
+  jobNum: [
+    { name: '职位名称', prop: 'name' },
+    { name: '发布企业', prop: 'enterpriseName' },
+    { name: '薪酬', prop: 'salaryDisplay' },
+    { name: '工作地区', prop: 'areaName' },
+    { name: '工作经验', prop: 'expName' },
+    { name: '学历要求', prop: 'eduName' },
+    { name: '发布时间', prop: 'createTime' }
+  ],
+  // 企业职位投递量
+  deliveryNum: [
+    { name: '投递人', prop: 'personName' },
+    { name: '职位名称', prop: 'name' },
+    { name: '发布企业', prop: 'enterpriseName' },
+    { name: '投递简历名称', prop: 'title' },
+    { name: '投递时间', prop: 'createTime' },
+  ],
+  // 用户面试数
+  interviewNum: [
+    { name: '求职者', prop: 'personName' },
+    { name: '邀请企业', prop: 'enterpriseName' },
+    { name: '面试岗位', prop: 'jobName' },
+    { name: '面试类型', prop: 'typeName' },
+    { name: '面试时间', prop: 'timeName' },
+    { name: '面试地点', prop: 'addressName' },
+  ],
+  // 用户投递数
+  cvDeliveryNum: [
+    { name: '投递人', prop: 'personName' },
+    { name: '投递职位', prop: 'name' },
+    { name: '发布企业', prop: 'enterpriseName' },
+    { name: '投递简历名称', prop: 'title' },
+    { name: '投递时间', prop: 'createTime' },
+  ]
+}
+
+const apiArr = reactive({
+  inviteInterviewNum: JobFairManageApi.getEnterpriseInterviewInvite,
+  jobNum: JobFairManageApi.getEnterpriseJob,
+  deliveryNum: JobFairManageApi.getEnterpriseJobCvRel,
+  interviewNum: JobFairManageApi.getUserInterviewInvite,
+  cvDeliveryNum: JobFairManageApi.getUserJobCvRel,
+})
+
+const dealTableData = () => {
+  // 企业邀请面试/用户面试
+  if (['interviewNum', 'inviteInterviewNum'].includes(currentItem.value.name)) {
+    tableData.value = tableData.value.map(item => {
+      item.personName = item.person.name
+      item.jobName = formatName(item.job.name)
+      item.enterpriseName = formatName(item.enterpriseName)
+      item.typeName = item.type === 0 ? '线上面试': '线下面试'
+      item.timeName = timesTampChange(item.time, 'Y-M-D h:m')
+      item.addressName = item.job.address
+      return item
+    })
+  }
+  // 企业职位
+  if (currentItem.value.name === 'jobNum') {
+    tableData.value = tableData.value.map(item => {
+      item.name = formatName(item.name)
+      item.enterpriseName = formatName(item.enterprise.anotherName || item.enterprise.name)
+      item.salaryDisplay = item.payFrom && item.payTo ? `${item.payFrom}-${item.payTo}/${getDictLabel(DICT_TYPE.MENDUNER_PAY_UNIT, item.payUnit)}` : '面议'
+      item.areaName = !item.areaId ? '全国' : item.area.str
+      item.expName = getDictLabel(DICT_TYPE.MENDUNER_EXP_TYPE, item.expType)
+      item.eduName = getDictLabel(DICT_TYPE.MENDUNER_EDUCATION_TYPE, item.eduType)
+      item.createTime = timesTampChange(item.createTime)
+      return item
+    })
+  }
+  // 企业职位投递量、用户投递量
+  if (['cvDeliveryNum', 'deliveryNum'].includes(currentItem.value.name)) {
+    tableData.value = tableData.value.map(item => {
+      item.personName = item.person.name || item.person.phone
+      item.name = formatName(item.job.name)
+      item.enterpriseName = formatName(item.enterpriseName)
+      item.createTime = timesTampChange(item.createTime)
+      return item
+    })
+  }
+}
+
+const getList = async (typeName, details = '') => {
+  loading.value = true
+  try {
+    let data
+    // 使用钻取接口
+    const params = { ...queryParams, ...page }
+    data = await apiArr[typeName](params)
+    if (details) {
+      tableData.value = data.list || []
+      total.value = data.total || 0
+      dealTableData()
+    } else {
+      statistic[typeName] = data.total || 0
+    }
+  } finally {
+    loading.value = false
+    dialogLoading.value = false
+  }
+}
+
+
+// 打开弹窗
+const openDialog = (item) => {
+  dialogLoading.value = true
+  currentItem.value = item
+  page.pageNo = 1
+  tableData.value = []
+  getList(item.name, '钻取')
+  showDialog.value = true
+}
+const paginationChange = () => {
+  getList(currentItem.value.name, '钻取')
+}
+
+// 企业职位简历投递量统计
+const jobCvRelBar = ref({})
+const getJobCvRelBar = async () => {
+  const data = await JobFairManageApi.getEnterpriseJobCvRelBar(queryParams)
+  jobCvRelBar.value = data || { x: [], y: []}
+}
+
+const handleQuery = () => {
+  if (Object.keys(statistic).length) {
+    Object.keys(statistic).forEach(name => {
+      getList(name)
+    })
+  }
+  getJobCvRelBar()
+}
+handleQuery()
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+const typeChange = (value) => { // 
+  if (value) {
+    queryParams.time = []
+  }
+  handleQuery()
+}
+const timeRangeChange = (value) => {
+  if (value?.length) queryParams.type = '99' // 自定义
+  else queryParams.type = '0'
+}
+
+// 企业
+const enterpriseOption = ref([])
+const getEnterpriseOption = async () => {
+  try {
+    const data = await statisticAnalysisApi.getAnalysisEnterpriseSimpleList({})
+    enterpriseOption.value = data || []
+  } catch (error) {
+    console.log(error)
+  }
+}
+getEnterpriseOption()
+
+// 职位
+const jobOption = ref([])
+const getJobOption = async () => {
+  try {
+    const params ={
+      enterpriseId: queryParams.enterpriseId,
+      deptId: queryParams.deptId,
+      status: 0,
+      jobFairId: id
+    }
+    const data = await statisticAnalysisApi.getAnalysisJobAdvertisedList(params)
+    jobOption.value = data || []
+  } catch (error) {
+    console.log(error)
+  }
+}
+
+const enterpriseIdSelectChange = () => {
+  queryParams.deptId = undefined
+  queryParams.jobId = undefined
+  getJobOption()
+}
 </script>