Xiao_123 10 місяців тому
батько
коміт
c8bfe83a7b

+ 23 - 0
src/api/recruit/enterprise/interview/index.js

@@ -22,4 +22,27 @@ export const cancelInterviewInvite = async (data) => {
     url: '/app-admin-api/menduner/system/interview-invite/cancellation',
     data
   })
+}
+
+// 完成面试
+export const completedInterviewInvite = async (id) => {
+  return await request.post({
+    url: `/app-admin-api/menduner/system/interview-invite/completed?id=${id}`
+  })
+}
+
+// 未能爽约面试
+export const noAttendInterviewInvite = async (data) => {
+  return await request.post({
+    url: '/app-admin-api/menduner/system/interview-invite/not/attended',
+    data
+  })
+}
+
+// 面试反馈
+export const feedbackInterviewInvite = async (data) => {
+  return await request.post({
+    url: '/app-admin-api/menduner/system/interview-invite/feedback',
+    data
+  })
 }

+ 241 - 0
src/views/recruit/enterprise/interview/components/item.vue

@@ -0,0 +1,241 @@
+<template>
+  <div class="listItem d-flex align-center pa-3 mb-3" v-for="(item, index) in items" :key="'item_' + index">
+    <div class="d-flex align-center">
+      <span class="mr-5 font-size-16" style="color: orange;">{{ timesTampChange(item.time) }}</span>
+      <v-avatar class="mr-2" size=40 :image="item?.person?.avatar || 'https://minio.citupro.com/dev/menduner/7.png'"></v-avatar>
+      <div class="d-flex flex-column mr-3" style="width: 110px;">
+        <span class="ellipsis mb-1">{{ item?.person?.name }}</span>
+        <span class="ellipsis" style="color: var(--color-999);">{{ item?.job?.name }}</span>
+      </div>
+    </div>
+    <div class="d-flex align-center right-item">
+      <div style="min-width: 80px;text-align: center;">
+        <v-icon v-if="item?.phone" class="mx-1" size="20" color="primary">mdi-phone-outline</v-icon>
+        <span>{{ item?.phone || '-' }}</span>
+      </div>
+      <div>
+        <!-- 面试类型: 线下面试 -->
+        <span v-if="item.type === '1'">
+          <v-icon class="mx-3" size="20" color="primary">mdi-account-multiple-outline</v-icon>
+          <span>{{ $t('interview.offlineInterview') }}</span>
+        </span>
+        <!-- 面试类型: 线上面试 -->
+        <span v-else class="d-flex">
+          <v-icon class="mx-3 mt-2" size="20" color="primary">mdi mdi-video-account</v-icon>
+          <span class="d-flex flex-column">
+            <span>{{ $t('interview.onlineInterview') }}</span>
+            <span style="color: var(--color-999);">腾讯会议</span>
+          </span>
+        </span>
+      </div>
+      <!-- 面试状态: '待接受'/'已取消' -->
+      <div :style="{ 'color': item.status !== '98' ? 'orange' :'var(--color-999)'}">
+        <v-icon size="30">mdi mdi-circle-small</v-icon>
+        <span>{{ statusList.find(e => e.value === item.status)?.label }}</span>
+      </div>
+      <div>
+        <span v-if="editStatus.indexOf(item.status) !== -1" class="font-size-15 color-primary" @click="handleActionClick('edit', item)">修改面试</span>
+        <span v-if="againStatus.indexOf(item.status) !== -1" class="font-size-15 color-primary" @click="handleActionClick('edit', item)">重新邀约</span>
+        <v-menu v-if="actionItems(item.status).length">
+          <template v-slot:activator="{ props }">
+            <v-icon v-bind="props" class="mx-3" size="20" color="primary">mdi-dots-horizontal</v-icon>
+          </template>
+          <v-list>
+            <v-list-item
+              v-for="(k, index) in actionItems(item.status)"
+              :key="index"
+              :value="index"
+              color="primary"
+              @click="handleActionClick(k.value, item)"
+            >
+              <v-list-item-title>{{ k.title }}</v-list-item-title>
+            </v-list-item>
+          </v-list>
+        </v-menu>
+      </div>
+    </div>
+  </div>
+
+  <!-- 修改面试、重新邀约 -->
+  <CtDialog :visible="showInvite" :widthType="2" titleClass="text-h6" title="面试信息" @close="handleEditClose" @submit="handleEditSubmit">
+    <InvitePage v-if="showInvite" ref="inviteRef" :itemData="itemData" :position="positionItems"></InvitePage>
+  </CtDialog>
+
+  <!-- 取消面试 -->
+  <CtDialog :visible="cancelInvite" :widthType="2" titleClass="text-h6" title="取消面试" @close="handleCancelClose" @submit="handleCancelSubmit">
+    <TextArea v-model="cancelQuery.reason" :item="textItems"></TextArea>
+  </CtDialog>
+
+  <!-- 爽约、填写反馈 -->
+  <CtDialog :visible="show" :widthType="2" titleClass="text-h6" :title="currentAction === 'feedback' ? '填写反馈' : '填写爽约原因'" @close="handleClose" @submit="handleSubmit">
+    <TextArea v-if="currentAction === 'feedback'" v-model="query.evaluate" :item="textItems2"></TextArea>
+    <TextArea v-else v-model="query.reason" :item="textItems2"></TextArea>
+  </CtDialog>
+
+</template>
+
+<script setup>
+defineOptions({ name: 'interview-item'})
+import { ref } from 'vue'
+import { timesTampChange } from '@/utils/date'
+import { useI18n } from '@/hooks/web/useI18n'
+import { completedInterviewInvite, cancelInterviewInvite, saveInterviewInvite, noAttendInterviewInvite, feedbackInterviewInvite } from '@/api/recruit/enterprise/interview'
+import InvitePage from './invite.vue'
+import Snackbar from '@/plugins/snackbar'
+import Confirm from '@/plugins/confirm'
+
+defineProps({
+  items: Array,
+  statusList: Array
+})
+const emit = defineEmits(['refresh', 'action'])
+
+const { t } = useI18n()
+const editStatus = ['1', '0'] // 修改面试状态
+const againStatus = ['98', '99'] // 重新邀约状态
+const actions = ref([
+  { title: '完成面试', value: 'completed' },
+  { title: '取消面试', value: 'cancel' },
+  { title: '填写反馈', value: 'feedback' },
+  { title: '爽约', value: 'attended' }
+])
+// 邀请
+const itemData = ref({})
+const showInvite = ref(false)
+const inviteRef = ref()
+// 取消
+const cancelInvite = ref(false)
+const cancelQuery = ref({
+  id: null,
+  reason: null
+})
+const textItems = ref({
+  label: '取消原因 *',
+  clearable: true
+})
+
+// 爽约、反馈
+const currentAction = ref('feedback')
+const show = ref(false)
+const query = ref({})
+const textItems2 = ref({
+  label: '反馈 *',
+  clearable: true
+})
+
+const obj = {
+  '0': [1],
+  '1': [1],
+  '2': [0],
+  '3': [2]
+}
+const actionItems = (status) => {
+  const type = obj[status]
+  if (!type || !type.length) return []
+  const data = type.map(e => actions.value[e])
+  return data
+}
+
+// 完成面试
+const handleFinish = (item) => {
+  if (!item.id) return
+  Confirm(t('common.confirmTitle'), '是否确认已完成面试?').then(async () => {
+    await completedInterviewInvite(item.id)
+    Snackbar.success(t('common.operationSuccessful'))
+    emit('refresh')
+  })
+}
+
+// 操作按钮
+const handleActionClick = (value, item) => {
+  // 修改、重新邀约
+  if (value === 'edit') {
+    itemData.value = item
+    showInvite.value = true
+  }
+  // 取消
+  if (value === 'cancel') {
+    cancelQuery.value.id = item.id
+    cancelInvite.value = true
+  }
+  // 完成
+  if (value === 'completed') handleFinish(item)
+  // 爽约、反馈
+  if (value === 'feedback' || value === 'attended') {
+    currentAction.value = value
+    textItems2.value.label = value === 'feedback' ? '反馈 *' : '爽约原因 *'
+    query.value = value === 'feedback' ? { id: item.id, evaluate: null } : { id: item.id, reason: null }
+    show.value = true
+  }
+}
+
+// 修改面试、重新邀约
+const handleEditClose = () => {
+  itemData.value = {}
+  showInvite.value = false
+}
+
+const handleEditSubmit = async () => {
+  const query = inviteRef.value.getQuery()
+  if (!Object.keys(query).length) return
+  await saveInterviewInvite(query)
+  Snackbar.success(t('common.operationSuccessful'))
+  handleEditClose()
+  emit('refresh')
+}
+
+// 取消面试
+const handleCancelClose = () => {
+  cancelInvite.value = false
+  cancelQuery.value = {
+    id: null,
+    reason: null
+  }
+}
+
+const handleCancelSubmit = async () => {
+  if (!cancelQuery.value.reason) return Snackbar.warning('请填写取消原因')
+  await cancelInterviewInvite(cancelQuery.value)
+  Snackbar.success(t('common.operationSuccessful'))
+  handleCancelClose()
+  emit('refresh')
+}
+
+// 爽约、反馈
+const handleClose = () => {
+  show.value = false
+  query.value = {}
+}
+
+const handleSubmit = async () => {
+  const key = currentAction.value === 'feedback' ? 'evaluate' : 'reason'
+  if (!query.value[key]) return Snackbar.warning('请填写您的' + (currentAction.value === 'feedback' ? '反馈' : '爽约原因'))
+  const api = currentAction.value === 'feedback' ? feedbackInterviewInvite : noAttendInterviewInvite
+  await api(query.value)
+  Snackbar.success(t('common.operationSuccessful'))
+  emit('refresh')
+  handleClose()
+}
+</script>
+
+<style scoped lang="scss">
+.listItem {
+  cursor: pointer;
+  width: 100%;
+  min-width: 600px;
+  overflow: auto;
+  height: 76px;
+  border: 1px solid #e5e6eb;
+  border-radius: 5px;
+  
+  &:hover {
+    background-color: var(--color-f8);
+  }
+  .right-item {
+    width: 100%;
+    div {
+      width: 25%;
+    }
+  }
+}
+</style>

+ 8 - 164
src/views/recruit/enterprise/interview/index.vue

@@ -58,65 +58,7 @@
       <v-divider style="height: auto;" class="mr-5" vertical></v-divider>
       <div style="flex: 1;overflow: hidden;">
         <div v-if="items.length">
-          <div
-            class="listItem d-flex align-center pa-3 mb-3"
-            v-for="(item, index) in items" :key="'item_' + index"
-          >
-            <div class="d-flex align-center">
-              <span class="mr-5 font-size-16" style="color: orange;">{{ timesTampChange(item.time) }}</span>
-              <v-avatar class="mr-2" size=40 :image="item?.person?.avatar || 'https://minio.citupro.com/dev/menduner/7.png'"></v-avatar>
-              <div class="d-flex flex-column mr-3" style="width: 110px;">
-                <span class="ellipsis mb-1">{{ item?.person?.name }}</span>
-                <span class="ellipsis" style="color: var(--color-999);">{{ item?.job?.name }}</span>
-              </div>
-            </div>
-            <div class="d-flex align-center right-item">
-              <div style="min-width: 80px;text-align: center;">
-                <v-icon v-if="item?.phone" class="mx-1" size="20" color="primary">mdi-phone-outline</v-icon>
-                <span>{{ item?.phone || '-' }}</span>
-              </div>
-              <div>
-                <!-- 面试类型: 线下面试 -->
-                <span v-if="item.type === '1'">
-                  <v-icon class="mx-3" size="20" color="primary">mdi-account-multiple-outline</v-icon>
-                  <span>{{ $t('interview.offlineInterview') }}</span>
-                </span>
-                <!-- 面试类型: 线上面试 -->
-                <span v-else class="d-flex">
-                  <v-icon class="mx-3 mt-2" size="20" color="primary">mdi mdi-video-account</v-icon>
-                  <span class="d-flex flex-column">
-                    <span>{{ $t('interview.onlineInterview') }}</span>
-                    <span style="color: var(--color-999);">腾讯会议</span>
-                  </span>
-                </span>
-              </div>
-              <!-- 面试状态: '待接受'/'已取消' -->
-              <div :style="{ 'color': item.status !== '99' ? 'orange' :'var(--color-999)'}">
-                <v-icon size="30">mdi mdi-circle-small</v-icon>
-                <span>{{ statusList.find(e => e.value === item.status)?.label }}</span>
-              </div>
-              <div>
-                <span v-if="editStatus.indexOf(item.status) !== -1" class="font-size-15 color-primary" @click="handleActionClick(2, item)">修改面试</span>
-                <span v-if="againStatus.indexOf(item.status) !== -1" class="font-size-15 color-primary" @click="handleActionClick(2, item)">重新邀约</span>
-                <v-menu>
-                  <template v-slot:activator="{ props }">
-                    <v-icon v-bind="props" class="mx-3" size="20" color="primary">mdi-dots-horizontal</v-icon>
-                  </template>
-                  <v-list>
-                    <v-list-item
-                      v-for="(k, index) in actionItems(item.status)"
-                      :key="index"
-                      :value="index"
-                      color="primary"
-                      @click="handleActionClick(k.value, item)"
-                    >
-                      <v-list-item-title>{{ k.title }}</v-list-item-title>
-                    </v-list-item>
-                  </v-list>
-                </v-menu>
-              </div>
-            </div>
-          </div>
+          <itemPage :items="items" :statusList="statusList" @refresh="handleRefresh"></itemPage>
           <CtPagination
             v-if="total > 0"
             :total="total"
@@ -129,45 +71,20 @@
       </div>
     </div>
   </v-card>
-
-  <!-- 修改面试 -->
-  <CtDialog :visible="showInvite" :widthType="2" titleClass="text-h6" title="面试信息" @close="handleClose" @submit="handleSubmit">
-    <InvitePage v-if="showInvite" ref="inviteRef" :itemData="itemData" :position="positionItems"></InvitePage>
-  </CtDialog>
-
-  <CtDialog :visible="cancelInvite" :widthType="2" titleClass="text-h6" title="取消面试" @close="handleCancelClose" @submit="handleCancelSubmit">
-    <TextInput v-model="cancelQuery.reason" :item="textItems"></TextInput>
-  </CtDialog>
 </template>
 
 <script setup>
 defineOptions({ name: 'enterprise-interview'})
 import { ref } from 'vue'
-import { getInterviewInvitePage, saveInterviewInvite, cancelInterviewInvite } from '@/api/recruit/enterprise/interview'
-import InvitePage from './components/invite.vue'
+import { getInterviewInvitePage } from '@/api/recruit/enterprise/interview'
 import { getDict } from '@/hooks/web/useDictionaries'
-import Snackbar from '@/plugins/snackbar'
 import { getJobAdvertised } from '@/api/enterprise'
 import { dealDictArrayData } from '@/utils/position'
 import { timesTampChange, getStartAndEndOfDay } from '@/utils/date'
-import cloneDeep from 'lodash/cloneDeep'
+import itemPage from './components/item.vue'
 
-const cancelInvite = ref(false)
-const showInvite = ref(false)
-const inviteRef = ref()
 const items = ref([])
-const cancelQuery = ref({
-  id: null,
-  reason: null
-})
-const editStatus = ['1', '0'] // 修改面试状态
-const againStatus = ['98', '99'] // 重新邀约状态
 const statusList = ref()
-const itemData = ref({})
-const actions = ref([
-  { title: '沟通', value: 1 },
-  { title: '面试记录', value: 4 }
-])
 const total = ref(0)
 const query = ref({
   pageSize: 10,
@@ -176,19 +93,6 @@ const query = ref({
   jobId: null,
   time: []
 })
-const textItems = ref({
-  type: 'text',
-  label: '取消原因 *',
-  clearable: true
-})
-
-// 操作状态
-// status:0待接受 1待面试 2即将面试 3已完成 4已反馈 5未能爽约 98拒绝 99已取消
-const actionItems = (status) => {
-  const data = cloneDeep(actions.value)
-  if (status === '0') data.splice(-1, 0, { title: '取消面试', value: 3 })
-  return data
-}
 
 // 状态字典
 const getStatusList = async () => {
@@ -205,6 +109,11 @@ const getData = async () => {
 }
 getData()
 
+const handleRefresh = () => {
+  query.value.pageNo = 1
+  getData()
+}
+
 // 分页
 const handleChangePage = (e) => {
   query.value.pageNo = e
@@ -256,72 +165,7 @@ const getPositionList = async () => {
   })
 }
 getPositionList()
-
-// 操作按钮
-const handleActionClick = (value, item) => {
-  // 修改、重新邀约
-  if (value === 2) {
-    itemData.value = item
-    showInvite.value = true
-  }
-  // 取消
-  if (value === 3) {
-    cancelQuery.value.id = item.id
-    cancelInvite.value = true
-  }
-}
-
-// 修改面试
-const handleClose = () => {
-  itemData.value = {}
-  showInvite.value = false
-}
-
-const handleSubmit = async () => {
-  const query = inviteRef.value.getQuery()
-  if (!Object.keys(query).length) return
-  await saveInterviewInvite(query)
-  Snackbar.success('操作成功')
-  handleClose()
-  getData()
-}
-
-// 取消面试
-const handleCancelClose = () => {
-  cancelInvite.value = false
-  cancelQuery.value = {
-    id: null,
-    reason: null
-  }
-}
-
-const handleCancelSubmit = async () => {
-  if (!cancelQuery.value.reason) return Snackbar.warning('请填写取消原因')
-  await cancelInterviewInvite(cancelQuery.value)
-  Snackbar.success('操作成功')
-  handleCancelClose()
-  getData()
-}
 </script>
 
 <style scoped lang="scss">
-.listItem {
-  cursor: pointer;
-  width: 100%;
-  min-width: 600px;
-  overflow: auto;
-  height: 76px;
-  border: 1px solid #e5e6eb;
-  border-radius: 5px;
-  
-  &:hover {
-    background-color: var(--color-f8);
-  }
-  .right-item {
-    width: 100%;
-    div {
-      width: 25%;
-    }
-  }
-}
 </style>