Bläddra i källkod

面试管理列表

Xiao_123 1 månad sedan
förälder
incheckning
f75477e595
5 ändrade filer med 368 tillägg och 233 borttagningar
  1. 27 0
      api/interview.js
  2. 0 13
      api/resume.js
  3. 116 89
      pagesA/interview/index.vue
  4. 223 130
      pagesA/interview/item.vue
  5. 2 1
      pagesA/resume/index.vue

+ 27 - 0
api/interview.js

@@ -0,0 +1,27 @@
+import request from "@/utils/request"
+
+// 完成面试
+export const completedInterviewInvite = async (id) => {
+	return request({
+		url: `/app-api/menduner/system/recruit/interview-invite/completed?id=${id}`,
+		method: 'POST',
+		custom: {
+			showLoading: false,
+			openEncryption: true,
+			auth: true
+		}
+	})
+}
+
+// 面试信息分页
+export const getInterviewInvitePage = async (params) => {
+	return request({
+		url: '/app-api/menduner/system/recruit/interview-invite/page',
+		params,
+		method: 'GET',
+		custom: {
+			showLoading: false,
+			auth: true
+		}
+	})
+}

+ 0 - 13
api/resume.js

@@ -13,19 +13,6 @@ export const getPersonCvPage = async (params) => {
   })
   })
 }
 }
 
 
-// 面试信息分页
-export const getInterviewInvitePage = async (params) => {
-  return request({
-    url: '/app-api/menduner/system/recruit/interview-invite/page',
-    params,
-    method: 'GET',
-    custom: {
-      showLoading: false,
-      auth: true
-    }
-  })
-}
-
 // 不合适简历分页
 // 不合适简历分页
 export const personCvUnfitPage = async (params) => {
 export const personCvUnfitPage = async (params) => {
   return request({
   return request({

+ 116 - 89
pagesA/interview/index.vue

@@ -1,122 +1,149 @@
 <template>
 <template>
-  <view>
-    <uni-segmented-control :current="current" :values="controlList" @clickItem="handleChange" styleType="text" activeColor="#00B760"></uni-segmented-control>
-    <scroll-view class="scrollBox defaultBgc" scroll-y="true" @scrolltolower="loadingMore" style="height: calc(100vh - 36px);">
-      <view v-if="items.length">
-        <PositionList v-if="current === 0" class="pb-10" :list="items" :noMore="false"></PositionList>
-        <Items v-else class="pb-10" :list="items" @action="handleAction"></Items>
-        <uni-load-more :status="more" />
-      </view>
-      <view v-else class="nodata-img-parent">
-        <image src="https://minio.citupro.com/dev/static/nodata.png" mode="widthFix" style="width: 100vw;height: 100vh;"></image>
+  <view class="box defaultBgc">
+    <view style="background-color: #fff;">
+      <uni-segmented-control 
+        :current="current"
+        :values="tabList.map(e => e.label)"
+        @clickItem="changeControl"
+        styleType="text"
+        activeColor="#00B760"
+      ></uni-segmented-control>
+
+      <!-- 条件搜索 -->
+      <view style="margin: 20rpx;">
+        <uni-data-select 
+          v-model="query.jobId" 
+          :clear="false" 
+          :localdata="jobList" 
+          @change="handleChangeJob" 
+          placeholder="招聘中职位"
+			></uni-data-select>
       </view>
       </view>
-    </scroll-view>
-
-    <!-- 同意、拒绝面试 -->
-    <uni-popup ref="popup" type="dialog">
-      <uni-popup-dialog :type="type === 'agree' ? 'success' : 'warn'" cancelText="取消" confirmText="确认" 
-        title="系统提示" :content="type === 'agree' ? '确认接受面试吗?' : '确认拒绝面试吗?'" @confirm="handleConfirm" @close="handleClose"
-      ></uni-popup-dialog>
-    </uni-popup>
+    </view>
+
+    <scroll-view class="scrollBox" :scroll-y="true" @scrolltolower="loadingMore" style="position:relative;">
+			<CardItem v-if="items?.length" :items="items" :statusList="tabList" @refresh="handleRefresh" />
+			<uni-load-more :status="more" />
+		</scroll-view>
   </view>
   </view>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
 import { ref } from 'vue'
 import { ref } from 'vue'
-import { getJobDeliveryList, getUserInterviewInvitePage, userInterviewInviteConsent, userInterviewInviteReject } from '@/api/user'
-import { dealDictObjData } from '@/utils/position'
-import PositionList from '@/components/PositionList'
-import Items from './item.vue'
-import { userStore } from '@/store/user'
-import { onLoad } from '@dcloudio/uni-app'
-
-const useUserStore = userStore()
-const current = ref(0)
-const controlList = ['已投递', '待同意', '待面试', '已完成', '已拒绝']
-const statusList = [0, 1, 3, 98]
+import CardItem from './item.vue'
+import { getInterviewInvitePage } from '@/api/interview'
+import { getJobAdvertised } from '@/api/search'
+import { onShow, onLoad } from '@dcloudio/uni-app'
+import { getDict } from '@/hooks/useDictionaries'
+import { formatName } from '@/utils/getText'
 
 
+const current = ref(0)
+const tabList = ref([])
 const more = ref('more')
 const more = ref('more')
+const total = ref(0)
 const items = ref([])
 const items = ref([])
-const queryParams = ref({
+const query = ref({
   pageNo: 1,
   pageNo: 1,
-  pageSize: 10
+  pageSize: 10,
+  status: null,
+  jobId: null
 })
 })
-const popup = ref()
-const type = ref('')
-const id = ref(null)
-
-onLoad((options) => {
-  if (options?.index) {
-    current.value = Number(options.index)
-    items.value = []
+
+// 职位列表
+const jobList = ref([])
+const getJobList = async () => {
+  const { data } = await getJobAdvertised({ status: 0 })
+  if (data.length) {
+    jobList.value = data.map(e => {
+      return { text: `${formatName(e.name)}`, value: e.id }
+    })
   }
   }
-  getList()
+}
+
+onLoad(() => {
+  getDict('menduner_interview_invite_status').then(({ data }) => {
+    tabList.value = data.data ?? []
+
+    if (!tabList.value.length) return
+    query.value.pageNo = 1
+    getList()
+  })
+  getJobList()
 })
 })
 
 
+// 获取面试列表
 const getList = async () => {
 const getList = async () => {
-  const api = current.value === 0 ? getJobDeliveryList : getUserInterviewInvitePage
-  if (current.value !== 0) queryParams.value.status = statusList[current.value - 1]
-  const { data } = await api(queryParams.value)
-  const list = data?.list || []
-  if (!list.length && queryParams.value.pageNo === 1) {
-    items.value = []
-    return
-  }
-  if (list?.length) {
-    list.forEach(e => {
-      e.job = { ...e.job, ...dealDictObjData({}, e.job) }
-      e.enterprise = { ...e.enterprise, ...dealDictObjData({}, e.enterprise)}
-    })
+  more.value = 'loading'
+
+  query.value.status = tabList.value[current.value].value
+  try {
+    const { data } = await getInterviewInvitePage(query.value)
+    const { list, total: number } = data
+
+    if (!list.length) {
+      more.value = 'noMore'
+      return
+    }
+
+    total.value = number
     items.value = items.value.concat(list)
     items.value = items.value.concat(list)
+
+    if (items.value.length === +number) {
+      more.value = 'noMore'
+      return
+    }
+  } catch {
+    query.value.pageNo--
+    more.value = 'more'
   }
   }
-  more.value = items.value?.length === +data.total ? 'noMore' : 'more'
 }
 }
 
 
-const handleChange = (e) => {
-  items.value = []
-  queryParams.value.pageNo = 1
-  current.value = e.currentIndex
+onShow(() => {
+  if (!tabList.value.length) return
   getList()
   getList()
-}
+})
 
 
-// 加载更多
-const loadingMore = () => {
-  more.value = 'loading'
-  queryParams.value.pageNo++
-  getList()
+// 选择招聘中职位
+const handleChangeJob = (e) => {
+	query.value.pageNo = 1
+	items.value = []
+	total.value = 0
+	if (e) getList()
 }
 }
 
 
-// 同意、拒绝
-const handleAction = (item, typeVal) => {
-  id.value = item.id
-  type.value = typeVal
-  popup.value.open()
+const handleRefresh = () => {
+  items.value = []
+  total.value = 0
+  query.value.pageNo = 1
+  getList()
 }
 }
 
 
-const handleClose = () => {
-  popup.value.close()
-  type.value = ''
-  id.value = null
+const changeControl = (e) => {
+  current.value = e.currentIndex
+  handleRefresh()
 }
 }
 
 
-const handleConfirm = async () => {
-  if (!id.value) return
-  const api = type.value === 'agree' ? userInterviewInviteConsent : userInterviewInviteReject
-  // 同意需提交手机号
-  let phone = ''
-  if (useUserStore?.baseInfo?.phone) phone = useUserStore?.baseInfo?.phone
-  await api(type.value === 'agree' ? { id: id.value, phone } : id.value)
-  handleClose()
-  uni.showToast({
-    title: '操作成功',
-    icon: 'success'
-  })
-  queryParams.value.pageNo = 1
-  items.value = []
-  getList()
+// 加载更多
+const loadingMore = () => {
+	if (more.value === 'noMore') return
+  more.value = 'loading'
+  query.value.pageNo++
+	getList()
 }
 }
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">
-
+.box {
+  height: 100vh;
+  overflow: hidden;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+}
+.scrollBox{
+	flex: 1;
+  padding-bottom: 24rpx;
+  box-sizing: border-box;
+	height: 0 !important;
+}
 </style>
 </style>

+ 223 - 130
pagesA/interview/item.vue

@@ -1,158 +1,251 @@
 <template>
 <template>
-  <view v-if="list.length > 0" class="ss-m-x-20">
-    <view v-for="(item, index) in list" :key="index">
-      <view class="sub-li-bottom" @click.stop="jumpToEnterpriseDetail(item.enterprise?.id)">
-        <view class="avatarBox">
-          <image class="enterAvatar" :src="item.enterprise?.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'"></image>
-        </view>
-        <view>
-          <span class="ss-m-x-20 color-66" style="font-weight: bold;">{{ item.contact?.name || ' -- ' }}</span>
-          <span>{{ item.contact?.postNameCn }}</span>
-          <span class="divider tag-gap1 ss-m-x-10" v-if="item.contact?.postNameCn && item.invitePhone"> | </span>
-          <span class="mr">{{ item.invitePhone }}</span>
-        </view>
-      </view>
-      <!-- 职位信息 -->
-      <view class="list-shape">
-        <view class="titleBox my-5" @click="toDetail(item)">
-          <span style="font-size: 16px;font-weight: 700; color: #0E100F;">{{ formatName(item.job?.name) }}</span>
-          <span v-if="!item.job?.payFrom && !item.job?.payTo" class="salary-text">面议</span>
-          <span v-else class="salary-text">{{ item.job?.payFrom }}-{{ item.job?.payTo }}{{ item.job?.payName ? '/' + item.job?.payName : '' }}</span>
-        </view>
-        <!-- 面试时间、地点 -->
-        <view class="color-666 font-size-14 ss-m-t-20" @click="toDetail(item)">
-          <view>面试时间:{{ timesTampChange(item.time, 'Y-M-D h:m') }}</view>
-          <view class="ss-m-t-20">面试地点:{{ item.address }}</view>
-        </view>
-
-        <view v-if="item.status === '0'">
-					<view class="divided-line"></view>
-					<view class="d-flex justify-end">
-						<span style="color: #dd524d;text-decoration: underline;" @click="handleAction(item, 'refuse')">拒绝</span>
-						<span style="color: #00B760;margin-left: 65rpx;text-decoration: underline;" @click="handleAction(item, 'agree')">同意</span>
+	<view>
+		<uni-card v-for="(val, index) in items" :key="index" :is-shadow="true" @tap.stop="handleDetail(val)" :border='false' shadow="0px 0px 3px 1px rgba(0,0,0,0.1)">
+			<!-- 基本信息 -->
+			<view class="d-flex align-center">
+				<view class="user-avatar">
+					<image class="user-avatar-img" :src="getUserAvatar(val.person?.avatar, val.person?.sex)" mode="scaleToFill"></image>
+					<image class="user-avatar-sex" :src="val?.person?.sex ? val?.person?.sex === '1' ? '/static/img/man.png' : '/static/img/female.png' : ''" alt="" mode="scaleToFill" />
+				</view>
+				<view style="flex: 1; margin-left: 10px;">
+					<view class="d-flex justify-space-between align-center">
+						<view class="font-size-18">{{ val.person?.name }}</view>
+            <view :style="{'color': colorData[val.status]}">
+              {{ val.status ? statusList.find(i => i.value === val.status)?.label : '' }}
+            </view>
 					</view>
 					</view>
 				</view>
 				</view>
-      </view>
-    </view>
-  </view>
+			</view>
+
+			<view class="ss-m-t-15 color-999">
+				<view>
+					投递职位:
+					<image v-if="val.jobFairId" src="/static/svg/jobFair.svg" style="width: 15px; height: 15px;"></image>
+					{{ formatName(val.job?.name) }}
+				</view>
+        <view>联系电话:{{ val.person?.phone ?? '未填写' }}</view>
+				<view>面试时间:{{ timesTampChange(val.time, 'Y-M-D h:m') }}</view>
+			</view>
+
+			<view class="sub-li-bottom ss-m-t-20">
+        <template v-if="val.job?.status !== '1'" >
+          <view v-if="editStatus.indexOf(val.status) !== -1" class="sub-li-bottom-item color-primary" @tap.stop="handleActionClick('edit', val)">修改面试</view>
+          <view v-if="againStatus.indexOf(val.status) !== -1" class="sub-li-bottom-item color-primary" @tap.stop="handleActionClick('edit', val)">重新邀约</view>
+        </template>
+        <view v-if="val.status === '1'" class="sub-li-bottom-item color-primary" @click="handleActionClick('completed', val)">完成面试</view>
+        <view v-if="val.status === '3'" class="sub-li-bottom-item color-primary" @click="handleActionClick('feedback', val)">填写反馈</view>
+				<view 
+					class="sub-li-bottom-item d-flex align-center justify-center" 
+					@tap.stop="handleLoadMore(val)" 
+					:style="{'color': !actionItems(val)?.length ? '#ccc' : '#00B760'}"
+				>
+					<view>更多操作</view>
+					<uni-icons type="list" class="ss-m-l-10" size="20" :color="!actionItems(val)?.length ? '#ccc' : '#00B760'"></uni-icons>
+				</view>
+			</view>
+		</uni-card>
 
 
+
+		<!-- 更多操作 -->
+		<uni-popup ref="popup" type="bottom" :mask-click="true">
+      <view class="actions" v-if="itemData && Object.keys(itemData).length">
+				<view
+					class="action-item"
+					v-for="(val, index) in actionItems(itemData)" 
+					:key="index"
+					@tap.stop="handleActionClick(val.type, itemData)"
+				>{{ val.title }}</view>
+			</view>
+      <button class="big-cancel-button" @tap.stop="handleClosePopup">取消</button>
+    </uni-popup>
+
+    <!-- 完成面试 -->
+    <uni-popup ref="finishPopup" type="dialog">
+				<uni-popup-dialog 
+          type="warn" 
+          cancelText="取消"
+          confirmText="确定" 
+          title="系统提示" 
+          content="是否确认已完成面试?"
+          @confirm="handleFinishConfirm"
+          @close="handleFinishClose"
+        ></uni-popup-dialog>
+			</uni-popup>
+	</view>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
+import { ref } from 'vue'
 import { timesTampChange } from '@/utils/date'
 import { timesTampChange } from '@/utils/date'
+import { getUserAvatar } from '@/utils/avatar'
 import { formatName } from '@/utils/getText'
 import { formatName } from '@/utils/getText'
-import { jumpToEnterpriseDetail } from '@/utils/position'
+import { completedInterviewInvite } from '@/api/interview'
 
 
-const emits = defineEmits(['action'])
-const props = defineProps({
-  list: { type: Array, default: () => [] }
-})
+const emit = defineEmits(['refresh'])
+const props = defineProps({ items: Array, current: [Number, String], statusList: Array })
 
 
-//岗位详情
-const toDetail = (item) =>{
-  uni.navigateTo({ url: `/pagesB/positionDetail/index?id=${item.job?.id}&area=${item.job.area?.str ?? '全国'}` })
+const editStatus = ['0'] // 修改面试状态
+const againStatus = ['98', '99'] // 重新邀约状态
+const colorData = {
+  '0': 'orange',
+  '1': 'green',
+  '2': 'green',
+  '3': '#00B760',
+  '4': '#999',
+  '5': '#FE574A',
+  '98': '#FE574A',
+  '99': '#999'
 }
 }
 
 
-const handleAction = (item, type) => {
-  emits('action', item, type)
-}
+const popup = ref()
+const finishPopup = ref()
+const itemData = ref({})
 
 
-</script>
-
-<style scoped lang="scss">
-.noMore{
-  margin: 20px 0;
-}
-
-.date-time{
-  color:#d9d0d2;
-  float: right;
+// 更多操作
+const handleLoadMore = (val) => {
+	if (!actionItems(val).length) {
+		itemData.value = {}
+		uni.showToast({ title: '暂无更多操作', icon: 'none' })
+		return
+	}
+	itemData.value = val
+	popup.value.open()
 }
 }
 
 
-.divided-line {
-  width: 100%;
-  height: 1px;
-  background-color: #f0f2f7;
-  margin: 20px 0;
-
-}
-.enterAvatar{
-	width: 40px;
-	height: 40px;
-	border-radius: 50%;
-	margin: auto;
+// 关闭操作弹窗
+const handleClosePopup = () => {
+	popup.value.close()
+	itemData.value = {}
 }
 }
 
 
-.sub-li-bottom {
-  margin-top: 10px;
-  display: flex;
-  // justify-content:space-between;
-  align-items: center;
-  background: linear-gradient(90deg, #f5fcfc 0, #fcfbfa 100%);
-  font-size: 13px;
-  padding: 5px 30rpx;
-  border-radius: 12px 12px 0 0;
-  .avatarBox {
-    max-width: 40px;
-    max-height: 40px;
+// 完成面试
+const handleFinishClose = () => {
+  if (actionItems(itemData.value).length && actionItems(itemData.value).find(e => e.value === 'completed')) handleClosePopup()
+  else itemData.value = {}
+  finishPopup.value.close()
+}
+const handleFinishConfirm = async () => {
+  if (!itemData.value || !itemData.value.id) return
+  try {
+    await completedInterviewInvite(itemData.value.id)
+    uni.showToast({ title: '操作成功', icon: 'none' })
+    emit('refresh')
+    handleFinishClose()
+  } catch {
+    handleFinishClose()
   }
   }
 }
 }
 
 
-.salary-text {
-	float: right;
-	color: #00B760;
-  font-weight: 700;
-}
-.list-shape {
-	padding: 10px 30rpx 10px;
-  background-color: #fff;
-  border-radius: 0 0 12px 12px;
-  .titleBox {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
+// 邀请面试
+// const handleInterviewInvite = (item) => {
+// 	if (item?.job?.status === '1') {
+// 		uni.showToast({ title: '职位已关闭', icon: 'none' })
+// 		return
+// 	}
+// 	uni.navigateTo({
+// 	  url: `/pagesB/InviteInterview/index?id=${item.userId}&jobId=${item.job.id}`
+// 	})
+// 	handleClosePopup()
+// }
+
+const handleActionClick = (type, val) => {
+  if (val.job?.status === '1' && (type !== 'completed')) {
+    uni.showToast({ title: '职位已关闭', icon: 'none' })
+    return
+  }
+  itemData.value = val
+  // 完成面试
+  if (type === 'completed') {
+    finishPopup.value.open()
   }
   }
-}
-.tag-gap{
-	margin: 10rpx 10rpx 10rpx 0;
-}
-.tag-gap1{
-  margin-bottom: 20px;
-}
-.divider-mx{
-	margin: 0 10rpx;
-}
-.divider {
-	color:#e4d4d2;
 }
 }
 
 
-//公司名称
-.cer-end{
-  position: absolute;
-  top: 85%;
-  right: 16%;
+const obj = {
+  '0': [1],
+  '1': [4, 1, 3],
+  '2': [3]
+}
+const actions = ref([
+  { title: '完成面试', value: 'completed' },
+  { title: '取消面试', value: 'cancel' },
+  { title: '填写反馈', value: 'feedback' },
+  { title: '爽约', value: 'attended' },
+  { title: '修改面试', value: 'edit' }
+])
+const actionItems = (item) => {
+  const status = item?.status
+  const jobClosed = item.job?.status === '1'
+  const type = jobClosed && obj[status] ? [0] : obj[status] // 职位已关闭只能操作完成面试
+  if (!type || !type.length) return []
+  let data = type.map(e => actions.value[e])
+  return data
 }
 }
-.cer-text{
-  text-decoration: underline;
-  margin: 0 5rpx;
-}
-//一行展示不全...
-.dis{
-	display: flex;
-	align-items: center;
+</script>
+
+<style scoped lang="scss">
+.user-avatar {
+	position: relative;
+	&-img {
+		width: 45px;
+		height: 45px;
+		border-radius: 50%;
+	}
+	&-sex {
+		position: absolute;
+		right: 0;
+		bottom: 2px;
+		width: 20px;
+		height: 20px;
+		background-color: #fff;
+		border-radius: 50%;
+	}
+}
+.action {
+  font-size: 28rpx;
+	&-item {
+		text-align: center;
+		width: 90vw;
+		border-bottom: 1px solid #eee;
+		height:44px;
+		line-height: 44px;
+		margin: 0 auto;
+		color: #00B760;
+		background-color: #fff !important;
+		&:first-child {
+			border-radius: 5px 5px 0 0;
+		}
+		&:last-child {
+			border-radius: 0 0 5px 5px;
+			border-bottom: none;
+		}
+	}
+}
+.big-cancel-button {
+  width: 90vw;
+  height:44px;
+  line-height: 44px;
+  margin: 10px auto;
+	color: #fe574a;
+  background-color: #fff !important;
+  font-size: 28rpx;
 }
 }
-.show-more{
-	width: 26vw;
-	white-space: nowrap;
-	overflow: hidden;
-	text-overflow: ellipsis;
+.sub-li-bottom {
+  display: flex;
+	justify-content: space-between;
+	// align-items: flex-end;
+  margin-top: 10px;
+  font-size: 13px;
+	&-item {
+		width: 50%;
+		height: 35px;
+		line-height: 35px;
+		text-align: center;
+		margin-right: 15px;
+		background-color: #f7f8fa;
+		border-radius: 4px;
+		&:nth-child(2) {
+			margin-right: 0;
+		}
+	}
 }
 }
-/* 列表触底暂无更多 */
-.noMore{ text-align:center; color:grey; }
-.mt { margin-top: 10rpx; }
-.mb { margin-bottom: 10rpx; }
-.ml { margin-left: 20rpx; }
-.mr { margin-right: 20rpx; }
-.mr-10{ margin-right: 10rpx; }
-.my-5{ margin: 5px 0; }
 </style>
 </style>

+ 2 - 1
pagesA/resume/index.vue

@@ -37,7 +37,8 @@ import { timesTampChange } from '@/utils/date'
 import { dealDictObjData } from '@/utils/position'
 import { dealDictObjData } from '@/utils/position'
 import CardItem from './item.vue'
 import CardItem from './item.vue'
 import FilterList from '@/components/FilterList'
 import FilterList from '@/components/FilterList'
-import { getPersonCvPage, getInterviewInvitePage, personCvUnfitPage } from '@/api/resume'
+import { getInterviewInvitePage } from '@/api/interview'
+import { getPersonCvPage, personCvUnfitPage } from '@/api/resume'
 import { getJobFairList } from '@/api/jobFair'
 import { getJobFairList } from '@/api/jobFair'
 import { getJobAdvertised } from '@/api/search'
 import { getJobAdvertised } from '@/api/search'
 import { onLoad } from '@dcloudio/uni-app'
 import { onLoad } from '@dcloudio/uni-app'