Преглед на файлове

Merge branch 'dev' of https://git.citupro.com/zhengnaiwen_citu/menduner into dev

lifanagju_citu преди 7 месеца
родител
ревизия
8c3f886227

+ 17 - 0
src/router/modules/common.js

@@ -77,6 +77,23 @@ const common = [
       title: '集团介绍'
     },
     component: () => import('@/views/recruit/personal/home/components/advertisement/dynamic/intercontinental.vue')
+  },
+  {
+    path: '/recruit/enterprise/talentRecommendation',
+    component: () => import('@/views/recruit/enterprise/talentRecommendation/index.vue'),
+    name: 'talentRecommendation',
+    meta: {
+      title: '人才推荐'
+    }
+  },
+  {
+    path: '/recruit/enterprise/talentRecommendation/details/:id',
+    component: () => import('@/views/recruit/enterprise/talentRecommendation/details'),
+    name: 'talentRecommendationDetails',
+    meta: {
+      title: '人才详情',
+      hideSide: true
+    }
   }
 ]
 

+ 5 - 2
src/store/user.js

@@ -163,8 +163,11 @@ export const useUserStore = defineStore('user',
         await this.getEnterpriseUserAccountInfo()
         updateEventList(false)
         Snackbar.success(res?.type === 'emailLogin' ? '登录成功' : '切换成功')
-        await this.checkEnterpriseBaseInfo() // 校验企业必填信息
-        setTimeout(() => { window.location.href = '/enterprise' }, 1000)
+        // 人才推荐不需要跳转
+        if (!res.noJump) {
+          await this.checkEnterpriseBaseInfo() // 校验企业必填信息
+          setTimeout(() => { window.location.href = '/enterprise' }, 1000)
+        }
       },
       // 获取当前登录的企业用户信息
       async getEnterpriseInfo () {

+ 15 - 11
src/views/login/components/passwordPage.vue

@@ -1,6 +1,6 @@
 <template>
   <v-form ref="passwordForm" @submit.prevent>
-    <v-text-field v-model="loginData.phone" :disabled="props.phoneDisabled" placeholder="请输入手机号码(企业请输入邮箱登录)" color="primary" 
+    <v-text-field v-model="loginData.phone" :disabled="props.phoneDisabled" :placeholder="placeholder ? placeholder : '请输入手机号码(企业请输入邮箱登录)'" color="primary" 
     variant="outlined" density="compact" :rules="phoneRules" validate-on="input" prepend-inner-icon="mdi-cellphone" >
       <!-- <template v-slot:prepend-inner>
         <span class="d-flex">
@@ -38,23 +38,29 @@
 <script setup name="passwordPage">
 import { ref, reactive } from 'vue'
 defineOptions({ name: 'password-form' })
-// import { useI18n } from '@/hooks/web/useI18n'
-// const { t } = useI18n()
-const props = defineProps({ phoneDisabled: Boolean })
+import { checkEmail } from '@/utils/validate'
+
+const props = defineProps({ phoneDisabled: Boolean, placeholder: String, validEmail: Boolean })
 const passwordType = ref(false)
 const emits = defineEmits(['handleEnter'])
 
 const phoneRules = ref([
   value => {
     if (value) return true
-    return '请输入手机号码或邮箱'
+    return props.placeholder ? props.placeholder : '请输入手机号码或邮箱'
   }
-  // value => {
-  //   if (value?.length <= 11 && /^1[3456789]\d{9}$/.test(value)) return true
-  //   return t('login.correctPhoneNumber')
-  // }
 ])
 
+// 邮箱效验
+if (props.validEmail) {
+  phoneRules.value.push(
+    value => {
+      if (checkEmail(value)) return true
+      return '请输入正确的企业邮箱'
+    }
+  )
+}
+
 // 手机号区域
 // const currentArea = ref('0086')
 // const items = [
@@ -72,8 +78,6 @@ const loginData = reactive({
 
 // 设置默认账号密码便于开发快捷登录
 if (window.location.hostname === 'localhost' || window.location.hostname === '192.168.3.152') {
-  // loginData.phone = '18400000022@qq.com'
-  // loginData.password = 'Citu123456'
   loginData.phone = '13229740092'
   loginData.password = 'Citu123456'
 }

+ 81 - 0
src/views/recruit/enterprise/talentRecommendation/details.vue

@@ -0,0 +1,81 @@
+<!-- 人才库 - 人才详情 -->
+<template>
+  <div class="d-flex justify-center mb-8">
+    <div v-if="Object.keys(cvData).length" style="width: 940px;background: #fff;" class="px-8 pb-12 pt-3 my-n3 mr-3">
+      <!-- 基本信息 -->
+      <baseInfo class="mt-5" :data="cvData.person"></baseInfo>
+      <!-- 个人优势 -->
+      <div class="d-flex mt-8" v-if="cvData?.person?.advantage">
+        <span class="mr-6">{{ $t('resume.personalAdvantages') }}</span>
+        <div style="flex: 1; white-space: pre-line; font-size: 15px;" v-if="cvData?.person?.advantage" v-html="cvData.person.advantage"></div>
+      </div>
+      <!-- 职业技能 -->
+      <div class="d-flex mt-8" v-if="cvData?.skillList?.length">
+        <span class="mr-6">{{ $t('resume.vocationalSkills') }}</span>
+        <vocationalSkills style="flex: 1;" :data="cvData.skillList"></vocationalSkills>
+      </div>
+      <!-- 求职意向 -->
+      <div class="d-flex mt-8" v-if="cvData?.interestedList?.length">
+        <span class="mr-6">{{ $t('resume.jobIntention') }}</span>
+        <jobIntention style="flex: 1;" :data="cvData.interestedList"></jobIntention>
+      </div>
+      <!-- 工作经历 -->
+      <div class="d-flex mt-8" v-if="cvData?.workList?.length">
+        <span class="mr-6">{{ $t('resume.workExperience') }}</span>
+        <workExperience style="flex: 1;" :data="cvData.workList"></workExperience>
+      </div>
+      <!-- 项目经历 -->
+      <div class="d-flex mt-8" v-if="cvData?.projectList?.length">
+        <span class="mr-6">{{ $t('resume.projectExperience') }}</span>
+        <projectExperience style="flex: 1;" :data="cvData.projectList"></projectExperience>
+      </div>
+      <!-- 培训经历 -->
+      <div class="d-flex mt-8" v-if="cvData?.trainList?.length">
+        <span class="mr-6">{{ $t('resume.trainingExperience') }}</span>
+        <trainingExperience style="flex: 1;" :data="cvData.trainList"></trainingExperience>
+      </div>
+      <!-- 教育经历 -->
+      <div class="d-flex mt-8" v-if="cvData?.eduList?.length">
+        <span class="mr-6">{{ $t('resume.educationExp') }}</span>
+        <educationExp style="flex: 1;" :data="cvData.eduList"></educationExp>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+defineOptions({name: 'enterprise-talentPool-details'})
+import baseInfo from './details/baseInfo.vue'
+import vocationalSkills from '@/views/recruit/enterprise/talentPool/components/details/vocationalSkills.vue'
+import jobIntention from '@/views/recruit/enterprise/talentPool/components/details/jobIntention.vue'
+import workExperience from '@/views/recruit/enterprise/talentPool/components/details/workExperience.vue'
+import projectExperience from '@/views/recruit/enterprise/talentPool/components/details/projectExperience.vue'
+import trainingExperience from '@/views/recruit/enterprise/talentPool/components/details/trainingExperience.vue'
+import educationExp from '@/views/recruit/enterprise/talentPool/components/details/educationExp.vue'
+import { getPersonCvDetail } from '@/api/enterprise'
+import { ref } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+
+const route = useRoute()
+const router = useRouter()
+
+// 获取人才详情
+const cvData = ref({})
+const getCvDetail = async () => {
+  const { id } = route.query
+  const { id: userId } = router.currentRoute.value.params
+  if (!id || !userId) return
+  const data = await getPersonCvDetail(userId, id)
+  cvData.value = data
+}
+getCvDetail()
+
+</script>
+<style lang="scss" scoped>
+.operate {
+  width: 240px;
+  height: 500px; // 272px
+  position: sticky;
+  top: 60px;
+}
+</style>

+ 109 - 0
src/views/recruit/enterprise/talentRecommendation/details/baseInfo.vue

@@ -0,0 +1,109 @@
+<!-- 基本信息 -->
+<template>
+  <div>
+    <!-- 头像 -->
+    <div class="avatarsBox">
+      <v-badge
+        v-if="info?.sex === '1' || info?.sex === '2'"
+        bordered 
+        offset-x="-25" 
+        offset-y="33" 
+        :color="info?.sex ? (info?.sex === '1' ? '#1867c0' : 'error') : 'error'" 
+        :icon="info?.sex ? (info?.sex === '1' ? 'mdi-gender-male' : 'mdi-gender-female') : 'mdi-gender-female'">
+        <v-avatar size=80 :image="info?.avatar || 'https://minio.citupro.com/dev/menduner/7.png'"></v-avatar>
+      </v-badge>
+      <v-avatar v-else size=80 :image="info?.avatar || 'https://minio.citupro.com/dev/menduner/7.png'"></v-avatar>
+    </div>
+    <div class="text-center mt-3 color-666 font-size-20 font-weight-bold">{{ info?.name }}</div>
+    <div>
+      <div class="mt-5 d-flex">
+        <div class="listBox">
+          <div v-for="k in list" :key="k.key" class="mb-1">
+            <span :class="[k.icon, { 'mdi': k.icon }]">{{ k.label ? k.label : '' }}</span>
+            <span>{{ k.isTime ?  timesTampChange(info[k.key], 'Y-M-D') || '未知' : info[k.key] || '未知' }}</span>
+          </div>
+        </div>
+      </div>
+      <div class="mt-4">
+        <span style="font-size: 15px;">个人画像:</span>
+        <span v-if="info?.tagList && info?.tagList.length > 0">
+          <v-chip size="small" label v-for="(k, i) in info.tagList" :key="i" class="mr-2 mb-2" color="primary">{{ k }}</v-chip>
+        </span>
+        <span v-else>暂无</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+defineOptions({name: 'enterprise-talentPool-details-baseInfo'})
+import { ref } from 'vue'
+import { timesTampChange } from '@/utils/date'
+import { dealDictObjData } from '@/utils/position'
+
+const props = defineProps({
+  data: Object
+})
+
+const list = [
+  { key: 'areaName', icon: 'mdi-map-marker-outline' },
+  { key: 'phone', icon: 'mdi-phone-outline' },
+  { key: 'email', icon: 'mdi-email-outline' },
+  { key: 'expName', icon: 'mdi-calendar-blank-outline' },
+  { key: 'eduName', icon: 'mdi-school-outline' },
+  { key: 'jobStatusName', icon: 'mdi-tag-outline' },
+  { key: 'birthday', icon: 'mdi-cake-variant-outline', isTime: true },
+  { key: 'maritalStatusName', icon: 'mdi-account-heart' },
+  { key: 'firstWorkTime', label: '首次工作时间:', isTime: true }
+]
+const info = ref({})
+if (props.data && Object.keys(props.data).length) {
+  info.value = dealDictObjData({}, props.data)
+}
+</script>
+
+<style lang="scss" scoped>
+.avatarsBox {
+  height: 80px;
+  width: 80px;
+  position: relative;
+  // margin-right: 40px;
+  margin: auto;
+  .img {
+    width: 100%;
+    height: 100%;
+  }
+  .mdi {
+    font-size: 42px;
+    color: #fff;
+  }
+  div {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    border-radius: 50%;
+  }
+}
+.listBox {
+  display: flex;
+  flex-wrap: wrap; /* 允许换行 */
+  width: 100%; /* 设置容器宽度 */
+  overflow: hidden;
+  color: var(--color-666);
+  div {
+    width: 50%;
+    &:nth-child(2n) {
+      padding-left: 20px;
+    }
+    span {
+      height: 32px;
+      line-height: 32px;
+    }
+    .mdi {
+      font-size: 22px;
+      margin-right: 8px;
+    }
+  }
+}
+</style>

+ 183 - 0
src/views/recruit/enterprise/talentRecommendation/index.vue

@@ -0,0 +1,183 @@
+<template>
+  <div>
+    <div v-if="!getToken(1)" class="login-content">
+      <div class="login-content-box pa-10">
+        <div class="login-content-box-title text-center mt-4">请登录您的企业账号</div>
+        <passwordFrom class="mt-10" ref="passRef" placeholder="请输入企业邮箱" :validEmail="true"></passwordFrom>
+        <v-btn :loading="loading" color="primary" class="white--text mt-5" min-width="340" @click="handleLogin">登录</v-btn>
+      </div>
+    </div>
+    <div v-else class="content py-3" @scroll="handleScroll">
+      <v-card class="py-3 px-5" :style="{'width': isMobile ? '100%' : '750px'}" style="min-height: calc(100vh - 24px); box-sizing: border-box; margin: 0 auto;">
+        <div class="d-flex align-center" style="width: 340px; margin: auto;">
+          <Autocomplete v-model="query.jobId" :item="selectItems" @change="handleChange"></Autocomplete>
+        </div>
+        <v-divider></v-divider>
+        <div v-if="items.length">
+          <div v-for="(val, index) in items" :key="val.id" @click="handleDetail(val)">
+            <div class="py-3 d-flex align-center">
+              <v-avatar size="large">
+                <v-img :src="getUserAvatar(val.avatar, val.sex)" width="50" height="50"></v-img>
+              </v-avatar>
+              <div class="ml-3 d-flex flex-column">
+                <p class="font-size-20">{{ val.name }}</p>
+                <p class="color-999 font-size-16">
+                  {{ val.jobStatusName }}
+                  <span v-if="val.jobStatusName && val.expType" class="septal-line"></span>
+                  {{ val.expType }}
+                  <span v-if="val.eduType" class="septal-line"></span>
+                  {{ val.eduType }}
+                </p>
+              </div>
+            </div>
+            <div class="bg-box" v-if="index !== items.length - 1"></div>
+          </div>
+        </div>
+        <Empty v-else :elevation="false" message="暂无数据,请更换搜索条件后再试"></Empty>
+      </v-card>
+    </div>
+
+    <Loading :visible="loading"></Loading>
+  </div>
+</template>
+
+<script setup>
+defineOptions({ name: 'talentRecommendation'})
+import { ref, onMounted } from 'vue'
+import { getToken } from '@/utils/auth'
+import passwordFrom from '@/views/login/components/passwordPage.vue'
+import Snackbar from '@/plugins/snackbar'
+import { useUserStore } from '@/store/user'
+import { passwordLogin } from '@/api/common'
+import { getPersonRecommendPage, getJobAdvertised } from '@/api/enterprise'
+import { dealDictArrayData } from '@/utils/position'
+import { getUserAvatar } from '@/utils/avatar'
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+const loading = ref(false)
+const passRef = ref(null)
+const items = ref([])
+const total = ref(0)
+const query = ref({
+  pageNo: 1,
+  pageSize: 20,
+  jobId: null
+})
+const selectItems = ref({
+  label: '已发布职位',
+  placeholder: '请选择要进行推荐的职位',
+  clearable: true,
+  width: 600,
+  items: []
+})
+
+// 已发布职位列表
+const getJobList = async () => {
+  const data = await getJobAdvertised({})
+  if (data.length) {
+    const list = dealDictArrayData([], data)
+    selectItems.value.items = list.map(e => {
+      return { label: `${e.name}${e.areaName ? '_' + e.areaName : ''} ${e.payFrom ? e.payFrom + '-' : ''}${e.payTo}${e.payName ? '/' + e.payName : ''}`, value: e.id }
+    })
+  }
+}
+
+// 组件挂载后添加事件监听器  
+const isMobile = ref(false)
+onMounted(() => {
+  if (!getToken(1)) Snackbar.warning('请先登录')
+  else getJobList()
+  const userAgent = navigator.userAgent
+  isMobile.value = /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i.test(userAgent)
+})
+
+// 列表
+const getData = async (isEmpty) => {
+  // isEmpty:是否清空列表
+  loading.value = true
+  try {
+    const res = await getPersonRecommendPage(query.value)
+    const list = res.list || []
+    items.value = list.length ? !isEmpty ? [...items.value, ...dealDictArrayData([], list)] : dealDictArrayData([], list) : []
+    total.value = res.total
+  } catch (err) {
+    loading.value = false
+  } finally {
+    loading.value = false
+  }
+}
+getData()
+
+// 底部加载
+const handleScroll = (e) => {
+  if (e.srcElement.scrollTop + e.srcElement.clientHeight > e.srcElement.scrollHeight - 10) {
+    if (items.value.length < total.value) {
+      query.value.pageNo++
+      getData()
+    }
+  }
+}
+
+// 选择发布职位
+const handleChange = () => {
+  query.value.pageNo = 1
+  getData(true)
+}
+
+// 登录
+const handleLogin = async () => {
+  const { valid } = await passRef.value.passwordForm.validate()
+  if (!valid) return
+  loading.value = true
+  try {
+    const data = await passwordLogin({ ...passRef.value.loginData, account: passRef.value.loginData.phone })
+    await useUserStore().changeRole({ ...data, type: 'emailLogin', noJump: true })
+    await getJobList()
+    await getData()
+  } catch (err) {
+    Snackbar.warning(err.msg)
+  } finally {
+    loading.value = false
+  }
+}
+
+// 人才详情
+const handleDetail = ({ userId, id }) => {
+  if (!userId || !id) return
+  router.push(`/recruit/enterprise/talentRecommendation/details/${userId}?id=${id}`)
+}
+</script>
+
+<style scoped lang="scss">
+.content {
+  background-color: #f2f4f7;
+  height: 100vh;
+  overflow-y: auto;
+}
+.login-content {
+  position: relative;
+  width: 100%;
+  height: 100vh;
+  background-image: url('https://www.mendunerhr.com/images/userfiles/92d7e4a755e2428b94aab3636d5047f3/images/recruitment/adImages/2018/11/1920x940.jpg');
+  background-size: cover;
+  &-box {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    translate: -50% -50%;
+    width: 420px;
+    height: 380px;
+    background-color: #fff;
+    border-radius: 10px;
+    &-title {
+      color: #4c4c4c;
+      font-size: 24px;
+    }
+  }
+}
+.bg-box {
+  height: 10px;
+  background-color: #f2f4f7;
+}
+</style>