瀏覽代碼

在招职位

lifanagju_citu 3 月之前
父節點
當前提交
b24b9b5416

+ 1 - 1
src/views/recruit/personal/jobFair/details.vue

@@ -51,7 +51,7 @@ const breadcrumbs = ref([
 const list = ref([
 	{
 		name: '厦门嘉逸希尔顿格芮精选酒店厦门嘉逸希尔顿格芮精选酒店',
-		id: 1,
+		id: '83602245358325760',
 		industryName: '全服务中档酒店/4星级',
 		jobNum: 5,
 		logoUrl: 'https://minio.menduner.com/dev/enterprise/1872204532362121217/img/3e1ae92796fd6bc6bbbb38333dbab5ac886887b01d5bec19a90e6134e6c34b21.jpeg',

+ 235 - 1
src/views/recruit/personal/jobFair/enterprise/index.vue

@@ -1,9 +1,243 @@
 <template>
-  <div>vue3PageInit</div>
+  <div class="default-width banner px-6">
+    <div v-if="Object.keys(info).length">
+      <div class="banner-title pt-3" v-if="Object.keys(info).length">
+        <div class="float-left d-flex align-center">
+          <v-img width="60" height="60" :src="info.enterprise.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'"></v-img>
+          <div class="ml-4">
+            <div class="contact-name">{{ formatName(info.enterprise.anotherName || info.enterprise.name) }}</div>
+            <div class="contact-info">
+              {{ info.scaleName }}
+              <span v-if="info.industryName && info.scaleName">·</span> 
+              {{ info.industryName }}
+            </div>
+          </div>
+        </div>
+        <div class="float-right d-flex">
+          <v-btn color="primary" variant="text" size="large" @click.stop="handleReturn" prepend-icon="mdi-chevron-triple-left">返回上一页</v-btn>
+        </div>
+      </div>
+      <div class="text-end mb-3">
+        <v-tooltip location="bottom">
+          <template v-slot:activator="{ props }">
+            <v-icon v-bind="props" class="ml-5 mr-2" size="25" :color="isCollection ? 'error' : ''" @click.stop="handleFollow">{{ isCollection ? 'mdi-heart' : 'mdi-heart-outline' }}</v-icon>
+          </template>
+          <span>关注该企业</span>
+        </v-tooltip>
+      </div>
+      <v-divider></v-divider>
+      <div class="mt-3">
+        <div class="d-flex" v-if="Object.keys(info).length">
+          <div class="content-left">
+            <recruitmentPositions :info="info"/>
+          </div>
+          <div class="content-right">
+            <div class="welfare mb-3">
+              <h4>福利</h4>
+              <div v-if="info?.enterprise?.welfareList?.length" class="welfare-tags mt-3">
+                <v-chip size="small" label v-for="(k, i) in info?.enterprise?.welfareList?.slice(0, 6)" :key="i" class="mb-2 welfare-tags-item ellipsis" color="primary">{{ k }}</v-chip>
+              </div>
+              <div v-else class="color-666 font-size-14 mt-3">暂无</div>
+            </div>
+            <div class="welfare">
+              <h4>工商信息</h4>
+              <div :class="['mt-2', 'business-item']" v-for="val in businessList" :key="val.value">
+                <div>{{ val.label }}</div>
+                <div class="business-value ellipsis">
+                  {{ info?.business ? info.business[val.value] : '暂无' }}
+                  <span v-if="info?.business && val.value === 'registeredCapital' && info?.business[val.value] && info?.business[val.value].indexOf('万元') === -1">万元</span>
+                </div>
+                <div :class="['my-3']"></div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    
+    <!-- 快速登录 -->
+    <loginPage v-if="showLogin" @loginSuccess="loginSuccess" @close="loginClose"></loginPage>
+  </div>
 </template>
 
 <script setup>
 defineOptions({name: 'jobFair-enterprise'})
+import { ref } from 'vue'
+import {
+  getEnterpriseDetails,
+  getEnterpriseSubscribeCheck,
+  getEnterpriseSubscribe,
+  getEnterpriseUnsubscribe
+} from '@/api/enterprise'
+import { dealDictObjData } from '@/utils/position'
+import { timesTampChange } from '@/utils/date'
+import { useRouter } from 'vue-router'; const router = useRouter()
+import { formatName } from '@/utils/getText'
+import { getToken } from '@/utils/auth'
+import loginPage from '@/views/common/loginDialog.vue'
+import Snackbar from '@/plugins/snackbar'
+import recruitmentPositions from './positions.vue'
+
+const { id } = router.currentRoute.value.params
+
+// 企业详情
+const info = ref({})
+const getDetails = async () => {
+  if (!id) return
+  const data = await getEnterpriseDetails({ id })
+  // 成立日期
+  if (data?.business?.establishmentTime) {
+    data.business.establishmentTime = timesTampChange(data.business.establishmentTime, 'Y-M-D')
+  }
+
+  info.value = { ...data, ...dealDictObjData({}, data.enterprise) }
+  getCollectionStatus(id)
+}
+getDetails()
+
+// 返回上一页
+const handleReturn = () => {
+  if (window.history.state.back) {
+    router.back()
+  } else router.push('/recruit/personal/jobFair')
+}
+
+// 效验求职者是否关注该企业
+const isCollection = ref(false)
+const getCollectionStatus = async (id) => {
+  if (!getToken()) return isCollection.value = false
+  const data = await getEnterpriseSubscribeCheck({ enterpriseId: id })
+  isCollection.value = data
+}
+
+// 关注&取消关注企业
+const handleFollow = async () => {
+  if (!getToken()) {
+    showLogin.value = true // 打开快速登录弹窗
+    Snackbar.warning('您还未登录,请先登录后再试')
+    nextFunc.value = handleFollow // 登录成功后要执行的操作
+    loginCloseWarningWord = '您已取消登录,无法关注企业' // 取消登录提示语
+    return
+  }
+  const api = isCollection.value ? getEnterpriseUnsubscribe : getEnterpriseSubscribe
+  await api(isCollection.value ? id : { enterpriseId: id })
+  getCollectionStatus(id)
+}
+
+const showLogin = ref(false)
+const nextFunc = ref(null)
+let loginCloseWarningWord = ''
+// 快速登录
+const loginSuccess = () => {
+  showLogin.value = false
+  Snackbar.success('登录成功')
+  if (nextFunc.value) nextFunc.value()
+}
+
+const loginClose = () => {
+  showLogin.value = false
+  Snackbar.warning(loginCloseWarningWord)
+}
+
 </script>
 <style lang="scss" scoped>
+.banner {
+  background-color: #fff;
+  padding: 0 0 20px;
+}
+.banner-title {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 125px;
+}
+.content-left {
+  width: 844px;
+  padding: 20px;
+}
+.content-right {
+  flex: 1;
+  padding: 20px 20px 0 0;
+}
+.contact {
+  height: 60px;
+  line-height: 60px;
+}
+.contact-name {
+  font-size: 28px;
+  font-weight: 700;
+  color: #37576c;
+  line-height: 28px;
+}
+.contact-info {
+  font-size: 15px;
+  font-weight: 500;
+  color: var(--color-222);
+  line-height: 21px;
+  margin-top: 8px;
+}
+.tools-box {
+  height: 80px;
+  width: 80px;
+  background-color: var(--color-d5e6e8);
+  border-radius: 5px;
+}
+.tools-box-number {
+  font-size: 30px;
+  font-weight: 700;
+  color: var(--v-primary-base);
+}
+.tools-box-text {
+  color: var(--v-primary-base);
+  font-size: 14px;
+}
+.welfare {
+  background-color: var(--color-f3);
+  border-radius: 8px;
+  padding: 12px;
+}
+.welfare-tags {
+  display: flex;
+  width: 242px;
+  flex-wrap: wrap;
+  height: 100px;
+  overflow: hidden;
+  text-align: center;
+}
+.welfare-tags-item {
+  display: block;
+  width: 117px;
+  max-width: 117px;
+  text-align: center;
+  line-height: 26px;
+  margin-right: 8px;
+  &:nth-child(2n) {
+    margin-right: 0;
+  }
+}
+.business-item {
+  font-size: 14px;
+  color: var(--color-666);
+  font-weight: 600;
+}
+.business-value {
+  width: 228px;
+  color: #000;
+  font-weight: 500;
+}
+.business-source {
+  font-size: 14px;
+  color: var(--color-666);
+}
+.desc {
+  width: 180px;
+  color: var(--color-666);
+  font-weight: 500;
+  font-size: 12px;
+}
+.position-name {
+  width: 180px;
+  font-size: 14px;
+  font-weight: 600;
+}
 </style>

+ 370 - 0
src/views/recruit/personal/jobFair/enterprise/positions.vue

@@ -0,0 +1,370 @@
+<template>
+  <div class="top">
+    <div class="d-flex" v-if="positionCategory.length">
+      <div class="font-weight-bold position-category-left">职位类别:</div>
+      <div class="position-category-right">
+        <span 
+          :class="['category-item', {'default-active': k.active}, {'font-weight-bold': k.active}]" 
+          v-for="k in positionCategory" 
+          :key="k.id"
+          @click.stop="handleClickCategory(k)"
+        >{{ k.id === -1 ? `${k.label}` : `${k.label} (${k.number})` }}</span>
+      </div>
+    </div>
+    <div class="d-flex mt-1 justify-space-between">
+      <conditionFilter v-if="show" ref="conditionFilterRef" :showFilterList="showFilterList" @reset="handleReset" @change="handleQueryChange"></conditionFilter>
+      <div style="width: 220px;" class="mt-2">
+        <v-text-field
+          v-model="query.content"
+          variant="outlined" 
+          label="查找职位关键字"
+          hide-details
+          color="primary"
+          append-inner-icon="mdi-magnify"
+          @click:append-inner="handleSearch('content', { values: query.content })"
+          @keyup.enter="handleSearch('content', { values: query.content })"
+        >
+        </v-text-field>
+      </div>
+    </div>
+  </div>
+  <v-divider class="mt-5"></v-divider>
+  <div class="bottom mt-4">
+    <div v-if="list.length">
+      <div 
+        v-for="(val, i) in list" 
+        :key="i" 
+        :class="['bottom-item', {'border-bottom-dashed': i !== list.length -1}, 'd-flex', 'justify-space-between', 'cursor-pointer']" 
+        @mouseenter="val.active = true"
+        @mouseleave="val.active = false"
+      >
+        <div>
+          <p v-if="val.job.name.includes('style')" :class="['name', {'default-active': val.active }]" v-html="val.job.name" @click.stop="handlePosition(val)"></p>
+          <p v-else :class="['name', {'default-active': val.active }]" @click.stop="handlePosition(val)">{{ formatName(val.job.name) }}</p>
+          <div style="line-height: 40px;">
+            <span v-for="k in desc" :key="k.mdi">
+              <span v-if="val.job[k.value] || k.value === 'areaName'" class="mr-5">
+                <v-icon color="var(--color-666)" size="15">{{ k.mdi }}</v-icon>
+                <span class="ml-1 tag-text">
+                  {{ k.value === 'areaName' ? !val.job.areaId ? '全国' : val.job.area?.str : val.job[k.value] }}
+                  <!-- {{ (k.value === 'areaName' && !val.job.areaId) ? '全国' : val.job[k.value] }} -->
+                </span>
+              </span>
+            </span>
+          </div>
+        </div>
+        <div v-if="!val.active" class="text-right">
+          <p v-if="!val.job.payFrom && !val.job.payTo" class="salary">面议</p>
+          <p v-else class="salary">{{ val.job.payFrom ? val.job.payFrom + '-' : '' }}{{ val.job.payTo }}{{ val.job.payName ? '/' + val.job.payName : '' }}</p>
+          <div class="update-time">{{ timesTampChange(val.job.updateTime) }} 刷新</div>
+        </div>
+        <div v-else class="account-info">
+          <v-avatar :image="getUserAvatar(val.contact.avatar, val.contact.sex)"></v-avatar>
+          <span class="account-label">{{ val.contact.name }}{{ val.contact.postNameCn ? ' · ' + val.contact.postNameCn : '' }}</span>
+          <span>
+            <v-btn class="half-button" color="primary" size="small" @click="toDetails(val)">立即沟通</v-btn>
+          </span>
+        </div>
+      </div>
+      <MPagination
+        :total="total"
+        :page="pageInfo.pageNo"
+        :limit="pageInfo.pageSize"
+        @handleChange="handleChangePage"
+      ></MPagination>
+    </div>
+    <Empty v-else :elevation="false"></Empty>
+
+    <!-- 快速登录 -->
+    <loginPage v-if="showLogin" @loginSuccess="loginSuccess" @close="loginClose"></loginPage>
+  </div>
+</template>
+
+<script setup>
+defineOptions({ name: 'recruitment-positions'})
+import { reactive, ref } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { timesTampChange } from '@/utils/date'
+import { getDict } from '@/hooks/web/useDictionaries'
+import { dealDictObjData } from '@/utils/position'
+import { prologue, defaultText } from '@/hooks/web/useIM'
+import { getUserAvatar } from '@/utils/avatar'
+import { getJobAdvertisedPositionCount, getJobAreaByEnterpriseId, getJobAdvertisedSearch } from '@/api/position'
+import MPagination from '@/components/CtPagination'
+import conditionFilter from '@/views/recruit/personal/position/components/conditionFilter'
+import loginPage from '@/views/common/loginDialog.vue'
+import { getToken } from '@/utils/auth'
+import Snackbar from '@/plugins/snackbar'
+import { checkPersonBaseInfo } from '@/utils/check'
+import dialogExtend from '@/plugins/dialogExtend'
+import { formatName } from '@/utils/getText'
+
+const props = defineProps({
+  info: {
+    type: Object,
+    default: () => {}
+  }
+})
+
+const total = ref(0)
+const pageInfo = ref({
+  pageSize: 10,
+  pageNo: 1
+})
+let query = reactive({})
+const route = useRoute(); const router = useRouter()
+let routeQuery = (route?.query && route.query && Object.keys(route?.query).length) ? route.query : null
+if (routeQuery?.content) query.content = routeQuery?.content || ''
+if (routeQuery) query = routeQuery
+
+// 职位详情
+const handlePosition = (val) => {
+  window.open(`/recruit/personal/position/details/${val.job.id}`)
+}
+
+// 职位类型
+const positionList = ref([])
+const getDictData = async () => {
+  const { data } = await getDict('positionData', {}, 'positionData')
+  positionList.value = data
+}
+getDictData()
+
+const show = ref(false)
+const showFilterList = ref([
+  { key: 'expType', isSingle: true },
+  { key: 'eduType', isSingle: true },
+  { key: 'payScope', isSingle: true },
+])
+const getProvideData = (list) => {
+  if (!list?.length) return show.value = true
+  getDict('menduner_area_type', {}, 'areaList').then(({ data }) => {
+    data = data?.length && data || []
+    const arr = list.map(e => {
+      const obj = data.find(k => k.id === Number(e.key))
+      if (!obj) return
+      return { label: obj.name, value: obj.id }
+    }).filter(Boolean)
+    if (arr?.length) showFilterList.value.unshift({ key: 'areaIds', isSingle: true, provideData: arr})
+    show.value = true
+  })
+}
+
+// 职位类别&工作地点
+const positionCategory = ref([])
+const getData = async () => {
+  const data = await getJobAdvertisedPositionCount({ enterpriseId: props.info.enterprise.id })
+  const areaList = await getJobAreaByEnterpriseId({ enterpriseId: props.info.enterprise.id })
+  getProvideData(areaList)
+  const list = data.map(val => {
+    const value = positionList.value.find(e => Number(e.id) === Number(val.key))
+    if (!value) return
+    return { id: value.id, label: value.nameCn, number: val.value, active: false }
+  }).filter(Boolean)
+  positionCategory.value = [{ id: -1, label: '全部', active: true }, ...list]
+}
+const getPoAr = async () => {
+  await getData()
+  // 职位类别回显
+  if (routeQuery?.positionId) {
+    positionCategory.value.map(e => e.active = false)
+    positionCategory.value.find(e => e.id === Number(routeQuery.positionId)).active = true
+  }
+}
+getPoAr()
+
+// 职位类别选中
+const handleClickCategory = (k) => {
+  positionCategory.value.map(e => e.active = false)
+  k.active = !k.active
+  handleSearch('positionId', { values: [k.id] })
+}
+
+const dealRouteQuery = () => {
+  const arr = Object.keys(query).map(e => {
+    if (Array.isArray(query[e]) && !query[e].length) {
+      delete query[e]
+    }
+    if (!query[e]) delete query[e]
+    if (e !== 'pageSize' && e !== 'pageNo' && e !== 'enterpriseId') return `${e}=${query[e]}`
+  }).filter(Boolean)
+  const str = ['key=recruitmentPositions', ...arr].join('&')
+  if (str) router.replace(`${route.path}?${str}`)
+}
+
+const handleSearch = (key, { values = [] }) => {
+  if (key) {
+    if (values === -1 || !values || values[0] === -1 || !values.length) delete query[key]
+    else if (['payScope', 'positionId'].includes(key)) query[key] = values?.length ? values[values.length-1] : '' // 单选且传递字符串
+    else query[key] = values
+  }
+  dealRouteQuery()
+  getPositionList(true)
+}
+
+// 职位列表
+const list = ref([])
+// 职位列表
+const getPositionList = async (isSearch) => {
+  const queryParams = {
+    ...query,
+    ...pageInfo.value,
+    enterpriseId: props.info.enterprise.id
+  }
+  delete queryParams.key
+  for (const key in queryParams) {
+    if (['expType', 'eduType'].includes(key) && queryParams[key] === '9999') delete queryParams[key]
+  }
+
+  if (isSearch) query.pageNo = 1
+  const { list: arr, total: number } = await getJobAdvertisedSearch(queryParams)
+  total.value = number
+  list.value = arr.map(e => {
+    e.active = false
+    e.job = { ...e.job, ...dealDictObjData({}, e.job) }
+    return e
+  })
+}
+getPositionList()
+
+const handleChangePage = (index) => {
+  pageInfo.value.pageNo = index
+  getPositionList()
+}
+
+// 参数改变
+const handleQueryChange = (key, val) => { // val为字符串,数组的话用_下划线分隔
+  pageInfo.value.pageNo = 1
+  const values = val ? val.split('_') : []
+  handleSearch(key, { values })
+}
+
+// 清空筛选条件
+const handleReset = async () => {
+  pageInfo.value.pageNo = 1
+  showFilterList.value.forEach(e => {
+    delete query[e.key]
+  })
+  handleSearch(null, {})
+}
+
+// 城市、学历、工作经验
+const desc = [
+  { mdi: 'mdi-map-marker-outline', value: 'areaName' },
+  { mdi: 'mdi-school-outline', value: 'eduName' },
+  { mdi: 'mdi-clock-time-ten-outline', value: 'expName' }
+]
+
+let toDetailsInfo = {}
+// 沟通
+const toDetails = async (info) => {
+  if (info) toDetailsInfo = info // 快速登录弹窗回调使用
+  else info = toDetailsInfo
+  if (!getToken()) {
+    showLogin.value = true // 打开快速登录弹窗
+    Snackbar.warning('您还未登录,请先登录后再试')
+    //
+    loginCloseWarningWord = '您已取消登录,无法对职位进行沟通' // 取消登录提示语
+    nextFunc.value = toDetails // 登录成功后要执行的操作 (toDetails执行不成功,原因未找到)
+    return
+  }
+  if (!checkPersonBaseInfo()) { // 强制填写个人信息
+    dialogExtend('necessaryInfoDialog').then(() => {
+      toDetails(toDetailsInfo)
+    })
+    return
+  }
+  const userId = info.contact.userId
+  const enterpriseId = info.contact.enterpriseId
+  const textObj = {
+    text: defaultText,
+    positionInfo: { ...info.job, enterprise: info.enterprise, contact: info.contact },
+  }
+  await prologue({userId, enterpriseId, text: JSON.stringify(textObj)})
+  let url = `/recruit/personal/message?id=${info.job.id}`
+  if (info.contact.enterpriseId) {
+    url += `&enterprise=${info.contact.enterpriseId}`
+  }
+
+  window.open(url)
+}
+
+const showLogin = ref(false)
+const nextFunc = ref(null)
+let loginCloseWarningWord = ''
+// 快速登录
+const loginSuccess = () => {
+  showLogin.value = false
+  Snackbar.success('登录成功')
+  if (nextFunc.value) nextFunc.value()
+}
+
+const loginClose = () => {
+  showLogin.value = false
+  Snackbar.warning(loginCloseWarningWord)
+}
+</script>
+
+<style scoped lang="scss">
+.bottom-item {
+  width: 100%;
+  height: 68px;
+  margin-bottom: 12px;
+}
+.name {
+  position: relative;
+  max-width: 30vw;
+  margin-right: 8px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-weight: 600;
+}
+.salary {
+  font-size: 16px;
+  font-weight: 700;
+  color: var(--v-error-base);
+  line-height: 22px;
+  flex: none;
+}
+.tag-text {
+  color: var(--color-222);
+  font-size: 14px;
+}
+.update-time {
+  color: var(--color-666);
+  font-size: 14px;
+  line-height: 40px;
+}
+.account-info {
+  line-height: 52px;
+  .account-label {
+    color: var(--color-666);
+    font-size: 14px;
+    font-weight: 600;
+    margin: 0 10px;
+  }
+}
+.position-category-left {
+  width: 80px;
+}
+.position-category-right {
+  flex: 1;
+}
+.category-item {
+  display: inline-block;
+  margin-right: 20px;
+  font-size: 15px;
+  color: var(--color-666);
+  cursor: pointer;
+  &:hover {
+    color: var(--v-primary-base);
+  }
+}
+:deep(.v-field__input) {
+  height: 28px;
+  padding: 0 0 0 10px;
+  font-size: 12px;
+  min-height: 28px;
+}
+</style>

+ 1 - 1
src/views/recruit/personal/jobFair/index.vue

@@ -2,7 +2,7 @@
 	<div class="default-width">
 		<buttons :current="3" style="position: sticky;" class="mx-4 mb-3"></buttons>
 		<div class="px-3 content">
-			<v-card elevation="5" v-for="val in list" :key="val.id" class="cursor-pointer">
+			<v-card elevation="5" v-for="val in list" :key="val.id">
 				<img :src="val.headImg" alt="" style="object-fit: contain; width: 100%;">
 				<div class="pa-3">
 					<div class="color-666">活动时间:{{ timesTampChange(val.startTime, 'Y-M-D') }}至{{ timesTampChange(val.endTime, 'Y-M-D') }}</div>