Pārlūkot izejas kodu

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

zhengnaiwen_citu 10 mēneši atpakaļ
vecāks
revīzija
35146e892d

+ 42 - 25
src/router/modules/components/recruit/enterprise.js

@@ -140,9 +140,9 @@ const enterprise = [
     ]
   },
   {
-    path: '/recruit/enterprise/personnelManagement',
+    path: '/recruit/enterprise/elite',
     component: Layout,
-    name: 'personnelManagement',
+    name: 'eliteManagement',
     meta: {
       title: '精英管理',
       enName: 'Meritocracy',
@@ -150,31 +150,48 @@ const enterprise = [
     },
     children: [
       {
-        path: '/recruit/enterprise/personnelManagement',
+        path: '/recruit/enterprise/elite',
         show: true,
-        component: () => import('@/views/recruit/enterprise/personnelManagement/index.vue')
+        component: () => import('@/views/recruit/enterprise/elite/index.vue')
       }
     ]
   },
-  {
-    path: '/recruit/enterprise/publicRecruitmentManagement',
-    component: Layout,
-    redirect: '',
-    name: 'publicRecruitmentManagement',
-    meta: {
-      title: '众聘管理',
-      enName: 'Crowdsourcing management',
-      icon: 'mdi-calendar-blank-multiple'
-    },
-    children: [
-      {
-        path: '/recruit/enterprise/publicRecruitmentManagement/deliver',
-        meta: {
-          title: '投递管理',
-          enName: 'Delivery Management'
-        },
-        component: () => import('@/views/recruit/enterprise/publicRecruitmentManagement/deliver')
-      },
+  // {
+  //   path: '/recruit/enterprise/personnelManagement',
+  //   component: Layout,
+  //   name: 'personnelManagement',
+  //   meta: {
+  //     title: '精英管理',
+  //     enName: 'Meritocracy',
+  //     icon: 'mdi-account-settings-outline'
+  //   },
+  //   children: [
+  //     {
+  //       path: '/recruit/enterprise/personnelManagement',
+  //       show: true,
+  //       component: () => import('@/views/recruit/enterprise/personnelManagement/index.vue')
+  //     }
+  //   ]
+  // },
+  // {
+    // path: '/recruit/enterprise/publicRecruitmentManagement',
+    // component: Layout,
+    // redirect: '',
+    // name: 'publicRecruitmentManagement',
+    // meta: {
+    //   title: '众聘管理',
+    //   enName: 'Crowdsourcing management',
+    //   icon: 'mdi-calendar-blank-multiple'
+    // },
+    // children: [
+      // {
+      //   path: '/recruit/enterprise/publicRecruitmentManagement/deliver',
+      //   meta: {
+      //     title: '投递管理',
+      //     enName: 'Delivery Management'
+      //   },
+      //   component: () => import('@/views/recruit/enterprise/publicRecruitmentManagement/deliver')
+      // },
       // {
       //   path: '/recruit/enterprise/publicRecruitmentManagement/commission',
       //   meta: {
@@ -183,8 +200,8 @@ const enterprise = [
       //   },
       //   component: () => import('@/views/recruit/enterprise/publicRecruitmentManagement/commission')
       // }
-    ]
-  },
+    // ]
+  // },
   {
     path: '/recruit/enterprise/informationManagement',
     component: Layout,

+ 41 - 0
src/views/recruit/enterprise/elite/components/commonStyle.vue

@@ -0,0 +1,41 @@
+<template>
+  <v-menu 
+    open-on-hover 
+    :close-delay="1" 
+    :open-delay="0" 
+    v-bind="$attrs" 
+    location="bottom" 
+    max-height="400"
+    :close-on-content-click="closeOnContentClick"
+  >
+    <template v-slot:activator="{ isActive, props }">
+      <v-btn
+        class="mr-3 py-0 px-2"
+        density="comfortable"
+        :append-icon="isActive ? 'mdi mdi-menu-up' : 'mdi mdi-menu-down'"
+        color="primary" variant="tonal"
+        v-bind="props"
+      >
+        {{ defineProps.btnTitle }}
+      </v-btn>
+    </template>
+    <slot></slot>
+  </v-menu>
+</template>
+<script setup>
+
+defineOptions({name: 'conditionFilter-index-page'})
+const defineProps = defineProps({
+  btnTitle: {
+    type: String,
+    default: 'Text'
+  },
+  closeOnContentClick: {
+    type: Boolean,
+    default: true
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 102 - 0
src/views/recruit/enterprise/elite/components/invite.vue

@@ -0,0 +1,102 @@
+<template>
+  <CtForm ref="CtFormRef" :items="formItems" style="height: 420px;">
+    <template #time="{ item }">
+      <VueDatePicker 
+        v-model="item.value"
+        placeholder="面试时间 *"
+        class="mb-4"
+        model-type="timestamp"
+        :text-input="{ format: 'MM.dd.yyyy HH:mm' }" />
+    </template>
+  </CtForm>
+</template>
+
+<script setup>
+defineOptions({ name: 'formPage'})
+import { ref } from 'vue'
+
+const props = defineProps({
+  itemData: {
+    type: Object,
+    default: () => {}
+  }
+})
+
+const CtFormRef = ref()
+const formItems = ref({
+  options: [
+    {
+      slotName: 'time',
+      key: 'time',
+      value: null,
+      rules: [v => !!v || '请选择面试时间'],
+    },
+    {
+      type: 'text',
+      key: 'position',
+      value: '',
+      noParam: true,
+      disabled: true,
+      label: '面试岗位'
+    },
+    {
+      type: 'text',
+      key: 'address',
+      value: '',
+      label: '面试地点 *',
+      rules: [v => !!v || '请输入面试地点'],
+    },
+    {
+      type: 'text',
+      key: 'invitePhone',
+      value: null,
+      label: '联系电话 *',
+      outlined: true,
+      rules: [v => !!v || '请填写联系电话']
+    },
+    {
+      type: 'textarea',
+      key: 'remark',
+      value: '',
+      label: '备注事项',
+      counter: 140,
+      rules: [
+        value => {
+          if (value?.length <= 140) return true
+          return '请输入备注事项,最多140字'
+        }
+      ]
+    }
+  ]
+})
+
+if (Object.keys(props.itemData).length) {
+  const obj = formItems.value.options.find(e => e.key === 'position')
+  obj.value = `${props.itemData?.job?.name}${props.itemData?.job?.areaName ? '_' + props.itemData?.job?.areaName : ''} ${props.itemData?.job?.payFrom}-${props.itemData?.job?.payTo}/${props.itemData?.job?.payName}`
+  formItems.value.options.find(e => e.key === 'address').value = props.itemData.job?.address
+}
+
+const getQuery = () => {
+  const obj = {
+    type: 1,
+    jobId: props.itemData.job.id,
+    userId: props.itemData.userId,
+    latitude: props.itemData.job?.latitude,
+    longitude: props.itemData.job?.longitude
+  }
+  formItems.value.options.forEach(item => {
+    if (item.noParam) return
+    obj[item.key] = item.value
+  })
+  return obj
+}
+
+defineExpose({
+  CtFormRef,
+  getQuery
+})
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 145 - 0
src/views/recruit/enterprise/elite/components/screen.vue

@@ -0,0 +1,145 @@
+<template>
+  <div class="d-flex align-center">
+    <CommonStyle v-for="(val, i) in list" :key="i" :btnTitle="val.title">
+      <v-list>
+        <v-list-item
+          v-for="(item, index) in val.items"
+          :key="index"
+          :active="val.selected.includes(item.value)"
+          color="primary"
+          :value="item.value"
+          @click="handleClick(item, val)"
+        >
+          <v-list-item-title>{{ item.label }}</v-list-item-title>
+        </v-list-item>
+      </v-list>
+    </CommonStyle>
+    <div class="mr-5 d-flex align-center" v-if="props.tab === 0">
+      <v-radio-group v-model="selected" inline style="height: 28px;" @update:modelValue="handleChangeSelected">
+        <v-radio v-model="selected" label="新投递" value="0" color="primary" hide-details density="compact" class="mr-3"></v-radio>
+        <v-radio v-model="selected" label="已查看" value="1" color="primary" hide-details density="compact"></v-radio>
+      </v-radio-group>
+      <v-checkbox class="ml-3" v-model="bounty" label="赏金职位" color="primary" hide-details density="compact" @update:model-value="handleChangeBounty"></v-checkbox>
+    </div>
+    <span class="reset-text cursor-pointer ml-3" @click="handleReset">重置</span>
+  </div>
+</template>
+
+<script setup>
+defineOptions({ name: 'screen-page'})
+import { ref, watch } from 'vue'
+import { getJobAdvertised } from '@/api/enterprise'
+import { getDict } from '@/hooks/web/useDictionaries'
+import { dealDictArrayData } from '@/utils/position'
+import CommonStyle from './commonStyle.vue'
+
+const emit = defineEmits(['search', 'reset', 'select', 'change'])
+const props = defineProps({
+  tab: Number
+})
+
+const selected = ref()
+const bounty = ref(false)
+
+const list = ref([
+  {
+    title: '应聘岗位',
+    defaultTitle: '应聘岗位',
+    key: 'jobId',
+    selected: [],
+    api: getJobAdvertised,
+    items: []
+  },
+  {
+    title: '学历',
+    defaultTitle: '学历',
+    key: 'eduType',
+    dictTypeName: 'menduner_education_type',
+    selected: [],
+    items: []
+  },
+  {
+    title: '工作经验',
+    defaultTitle: '工作经验',
+    key: 'expType',
+    dictTypeName: 'menduner_exp_type',
+    selected: [],
+    items: []
+  },
+  {
+    title: '求职状态',
+    defaultTitle: '求职状态',
+    key: 'jobStatus',
+    dictTypeName: 'menduner_job_status',
+    selected: [],
+    items: []
+  }
+])
+
+// 获取字典数据
+list.value.forEach(k => {
+  if (k.dictTypeName) {
+    getDict(k.dictTypeName).then(({ data }) => {
+      data = data?.length && data || []
+      k.items = data
+    })
+  }
+  if (k.api) {
+    k.api({ hire: false }).then(data => {
+      if (data.length) {
+        const list = dealDictArrayData([], data)
+        k.items = list.map(e => {
+          return { label: `${e.name}${e.areaName ? '_' + e.areaName : ''} ${e.payFrom}-${e.payTo}/${e.payName}`, value: e.id }
+        })
+      }
+    })
+  }
+})
+
+// 单击
+const handleClick = (item, val) => {
+  const obj = val.selected.includes(item.value)
+  val.selected = obj ? val.selected.filter(i => i !== item.value) : [item.value]
+  val.title = obj ? val.defaultTitle : item.label
+  emit('search', { key: val.key, value: obj ? '' : item.value })
+}
+
+// 重置
+const handleReset = () => {
+  list.value.map(e => {
+    e.selected = []
+    e.title = e.defaultTitle
+    return e
+  })
+  selected.value = ''
+  bounty.value = false
+  emit('reset')
+}
+
+// 新投递&已查看选择
+const handleChangeSelected = (e) => {
+  emit('select', e)
+}
+
+// 赏金职位
+const handleChangeBounty = (e) => {
+  emit('change', e)
+}
+
+watch(
+  () => props.tab,
+  () => {
+    handleReset()
+  }
+)
+</script>
+
+<style scoped lang="scss">
+.reset-text {
+  font-size: 14px;
+  color: var(--color-666);
+  &:hover {
+    color: var(--v-primary-base);
+  }
+}
+</style>

+ 173 - 0
src/views/recruit/enterprise/elite/components/table.vue

@@ -0,0 +1,173 @@
+<template>
+  <div>
+    <v-data-table
+      class="mt-3"
+      :items="items"
+      :headers="headers"
+      hover
+      :disable-sort="true"
+      height="60vh"
+      item-value="id"
+    >
+      <template #bottom></template>
+      <template v-slot:item.name="{ item }">
+        <div class="d-flex align-center cursor-pointer" @click="handleToPersonDetail(item)">
+          <v-badge
+            bordered
+            offset-y="6"
+            :color="badgeColor(item)"
+            :icon="badgeIcon(item)">
+            <v-avatar size="40" :image="item.person.avatar || 'https://minio.citupro.com/dev/menduner/7.png'"></v-avatar>
+          </v-badge>
+          <span class="defaultLink ml-3">{{ item?.person?.name }}</span>
+        </div>
+      </template>
+      <template v-slot:item.actions="{ item }">
+        <div v-if="tab === 0">
+          <v-btn color="primary" variant="text" @click="handlePreviewResume(item)">查看附件</v-btn>
+          <v-btn color="primary" variant="text" @click="handleInterviewInvite(item)">邀请面试</v-btn>
+        </div>
+        <v-btn v-if="tab === 0 || tab === 1" color="primary" variant="text" @click="handleEliminate(item)">不合适</v-btn>
+        <div v-if="tab === 1">
+          <v-btn color="primary" variant="text" @click="handleEnterByEnterprise(item)">入职</v-btn>
+        </div>
+        <v-btn v-if="tab === 4" color="primary" variant="text" @click="handleCancelEliminate(item)">取消不合适</v-btn>
+      </template>
+    </v-data-table>
+
+    <!-- 邀请面试 -->
+    <CtDialog :visible="showInvite" :widthType="2" titleClass="text-h6" title="面试信息" @close="handleEditClose" @submit="handleEditSubmit">
+      <InvitePage v-if="showInvite" ref="inviteRef" :itemData="itemData"></InvitePage>
+    </CtDialog>
+  </div>
+</template>
+
+<script setup>
+defineOptions({ name: 'table-page'})
+import { ref, computed, watch } from 'vue'
+import { previewFile } from '@/utils'
+import { personJobCvLook, joinEliminate, personEntryByEnterprise, personCvUnfitCancel } from '@/api/recruit/enterprise/personnel'
+import { saveInterviewInvite } from '@/api/recruit/enterprise/interview'
+import { useI18n } from '@/hooks/web/useI18n'
+import Snackbar from '@/plugins/snackbar'
+import InvitePage from './invite.vue'
+
+const { t } = useI18n()
+const emit = defineEmits(['refresh'])
+const props = defineProps({
+  tab: Number,
+  items: Array,
+  statusList: Array
+})
+const badgeColor = computed(() => (item) => {
+  return (item.person && item.person.sex) ? (item.person.sex === '1' ? '#1867c0' : 'error') : 'error'
+})
+
+const badgeIcon = computed(() => (item) => {
+  return (item.person && item.person.sex) ? (item.person.sex === '1' ? 'mdi-gender-male' : 'mdi-gender-female') : 'mdi-gender-female'
+})
+
+const inviteRef = ref()
+const showInvite = ref(false)
+const headers = ref([
+  { title: '姓名', value: 'name', sortable: false },
+  { title: '应聘职位', value: 'job.name', sortable: false },
+  { title: '求职状态', key: 'person.jobStatusName', sortable: false },
+  { title: '工作经验', key: 'person.expName', sortable: false },
+  { title: '最高学历', key: 'person.eduName', sortable: false },
+  { title: '岗位薪资', key: 'job', value: item => `${item.job.payFrom}-${item.job.payTo}/${item.job.payName}`, sortable: false },
+  { title: '状态', key: 'status', sortable: false, value: item => item.status ? props.statusList.find(i => i.value === item.status).label : '' },
+  { title: '操作', value: 'actions', sortable: false }
+])
+const unfit = { title: '类型', key: 'unfitType', sortable: false, value: item => item.type === '0' ? '简历不合适' : '面试不合适' }
+const delivery = { title: '类型', key: 'deliveryType', sortable: false, value: item => item.status === '0' ? '新投递' : '已查看' }
+
+const list = [0, 4]
+watch(
+  () => props.tab,
+  (val) => {
+    if (list.indexOf(val) !== -1) {
+      headers.value.splice(-1, 0, val === 0 ? delivery : unfit)
+    } else {
+      const index = headers.value.indexOf(item => item.key === val === 0 ? 'deliveryType' : 'unfitType')
+      if (index !== -1) headers.value.splice(index, 1)
+    }
+  },
+  { immediate: true }
+)
+
+// 人才详情
+const handleToPersonDetail = ({ userId, id }) => {
+  if (!userId || !id) return
+  window.open(`/recruit/enterprise/talentPool/details/${userId}?id=${id}`)
+}
+
+// 入职
+const handleEnterByEnterprise = async (item) => {
+  if (!item.id) return
+  await personEntryByEnterprise(item.id)
+  Snackbar.success(t('common.operationSuccessful'))
+  emit('refresh')
+}
+
+// 不合适
+const handleEliminate = async (item) => {
+  if (!item.id || !item?.job?.id) return
+  const query = {
+    bizId: item.id,
+    jobId: item.job.id,
+    userId: item.userId,
+    type: props.tab === 0 ? '0' : '1' // 投递简历0 已邀约1
+  }
+  await joinEliminate(query)
+  Snackbar.success(t('common.operationSuccessful'))
+  emit('refresh')
+}
+
+// 取消不合适
+const handleCancelEliminate = async (item) => {
+  if (!item.id) return
+  await personCvUnfitCancel(item.id)
+  Snackbar.success(t('common.operationSuccessful'))
+  emit('refresh')
+}
+
+// 查看简历
+const handlePreviewResume = async ({ url, id }) => {
+  if (!url || !id) return
+  await personJobCvLook(id)
+  previewFile(url)
+}
+
+// 邀请面试
+const itemData = ref({})
+const handleInterviewInvite = (item) => {
+  itemData.value = item
+  showInvite.value = true
+}
+
+const handleEditClose = () => {
+  showInvite.value = false
+  itemData.value = {}
+}
+
+const handleEditSubmit = async () => {
+  const { valid } = await inviteRef.value.CtFormRef.formRef.validate()
+  if (!valid) return
+  const query = inviteRef.value.getQuery()
+  if (!query?.time) return Snackbar.warning('请选择面试时间')
+  await saveInterviewInvite(query)
+  Snackbar.success(t('common.operationSuccessful'))
+  handleEditClose()
+  emit('refresh')
+}
+</script>
+
+<style scoped lang="scss">
+:deep(.v-table > .v-table__wrapper > table > thead) {
+  background-color: #f7f8fa !important;
+}
+:deep(.v-selection-control__input) {
+  color: var(--v-primary-base) !important;
+}
+</style>

+ 145 - 0
src/views/recruit/enterprise/elite/index.vue

@@ -0,0 +1,145 @@
+<!-- 精英管理 -->
+<template>
+  <v-card class="pa-3 card-box">
+    <div class="d-flex justify-space-between">
+      <v-tabs v-model="tab" align-tabs="start" color="primary" bg-color="#f7f8fa">
+        <v-tab v-for="k in tabList" :value="k.value" :key="k.value">{{ k.label }}</v-tab>
+      </v-tabs>
+      <TextInput v-model="textItems.value" :item="textItems" @appendInnerClick="handleSearch" @enter="handleSearch"></TextInput>
+    </div>
+    <Screen :tab="tab" @search="handleScreen" @reset="handleScreenReset" @select="handleSelect" @change="handleChangeBounty"></Screen>
+
+    <v-window v-model="tab" class="mt-1">
+      <v-window-item v-for="k in tabList" :value="k.value" :key="k.value">
+        <TablePage :items="items" :tab="k.value" :statusList="statusList" @refresh="getList"></TablePage>
+        <CtPagination
+          v-if="total > 0"
+          :total="total"
+          :page="query.pageNo"
+          :limit="query.pageSize"
+          @handleChange="handleChangePage"
+        ></CtPagination>
+      </v-window-item>
+    </v-window>
+  </v-card>
+</template>
+
+<script setup>
+defineOptions({ name: 'enterprise-elite-management'})
+import { ref } from 'vue'
+import { getPersonCvPage } from '@/api/enterprise'
+import { personCvUnfitPage } from '@/api/recruit/enterprise/personnel'
+import { dealDictObjData } from '@/utils/position'
+import { getDict } from '@/hooks/web/useDictionaries'
+import { getInterviewInvitePage } from '@/api/recruit/enterprise/interview'
+import TablePage from './components/table.vue'
+import Screen from './components/screen.vue'
+
+const total = ref(0)
+const query = ref({
+  pageNo: 1,
+  pageSize: 10,
+  status: null,
+  type: 0
+})
+const tab = ref(0)
+const tabList = ref([
+  { label: '投递简历', value: 0, api: getPersonCvPage, status: null },
+  { label: '已邀约', value: 1, api: getInterviewInvitePage, status: '0' },
+  { label: '已入职', value: 2, api: getInterviewInvitePage, status: '1' },
+  { label: '已结算', value: 3, api: getInterviewInvitePage, status: '2' },
+  { label: '不合适', value: 4, api: personCvUnfitPage },
+])
+const textItems = ref({
+  type: 'text',
+  value: '',
+  width: 250,
+  label: '搜索姓名',
+  clearable: true,
+  appendInnerIcon: 'mdi-magnify'
+})
+
+// 状态字典
+const statusList = ref([])
+getDict('menduner_interview_invite_status').then(({data}) => {
+  if (data && data.length) statusList.value = data
+})
+
+// 获取牛人列表
+const items = ref([])
+const getList = async () => {
+  const api = tabList.value[tab.value].api
+  if (tab.value !== 0) {
+    query.value.conversationStatus = tabList.value[tab.value].status
+    delete query.value.status
+    delete query.value.type
+  }
+
+  const { list, total: number } = await api(query.value)
+  if (!list.length) {
+    items.value = []
+    total.value = 0
+    return
+  }
+  total.value = number
+  items.value = list.map(e => {
+    let obj = e
+    obj.person = Object.assign(e.person, dealDictObjData({}, e.person))
+    obj.job = Object.assign(e.job, dealDictObjData({}, e.job))
+    return obj
+  })
+}
+getList()
+
+// 分页
+const handleChangePage = (i) => {
+  query.value.pageNo = i
+  getList()
+}
+
+// 牛人姓名检索
+const handleSearch = () => {
+  if (textItems.value.value) query.value.name = textItems.value.value
+  else delete query.value.name
+  query.value.pageNo = 1
+  getList()
+}
+
+// 下拉筛选
+const handleScreen = ({ value, key }) => {
+  if (value) query.value[key] = value
+  else delete query.value[key]
+  getList()
+}
+
+// 下拉筛选重置
+const handleScreenReset = () => {
+  query.value = {
+    pageSize: 10,
+    pageNo: 1,
+    status: tab.value
+  }
+  if (tab.value === 0) {
+    query.value.status = null
+    query.value.type = 0
+  }
+  if (textItems.value.value) query.value.name = textItems.value.value
+  getList()
+}
+
+const handleSelect = (e) => {
+  query.value.pageNo = 1
+  query.value.status = e
+  getList()
+}
+
+// 赏金职位
+const handleChangeBounty = (e) => {
+  query.value.type = e ? 1 : 0
+  query.value.pageNo = 1
+  getList()
+}
+</script>
+
+<style scoped lang="scss">
+</style>