瀏覽代碼

Merge branch 'recruit-enterprise' of https://git.citupro.com/zhengnaiwen_citu/menduner-uniapp into recruit-enterprise

lifanagju_citu 1 月之前
父節點
當前提交
8ebdf914dc

+ 63 - 12
api/jobFair.js

@@ -1,14 +1,26 @@
 import request from "@/utils/request"
 
 // 获得招聘会列表
-export const getJobFairList = (params) => {
+export const getJobFairList = () => {
   return request({
-    url: '/app-api/menduner/system/job-fair/list',
+    url: '/app-api/menduner/system/recruit/job-fair/list',
     method: 'GET',
-    params,
     custom: {
       showLoading: false,
-      auth: false
+      auth: true
+    }
+  })
+}
+
+// 效验是否有权限参加招聘会
+export const checkJobFairPermission = (jobFairId) => {
+  return request({
+    url: '/app-api/menduner/system/recruit/job-fair/check/permission',
+    method: 'GET',
+    params: { jobFairId },
+    custom: {
+      showLoading: false,
+      auth: true
     }
   })
 }
@@ -16,12 +28,12 @@ export const getJobFairList = (params) => {
 // 获得招聘会-招聘会详情
 export const getJobFair = (id) => {
   return request({
-    url: '/app-api/menduner/system/job-fair/get',
+    url: '/app-api/menduner/system/recruit/job-fair/get',
     method: 'GET',
     params: { id },
     custom: {
       showLoading: false,
-      auth: false
+      auth: true
     }
   })
 }
@@ -34,20 +46,46 @@ export const getJobFairEnterprisePage = (params) => {
     params,
     custom: {
       showLoading: false,
-      auth: false
+      auth: true
     }
   })
 }
 
-// 根据企业id查询招聘会职位列表
-export const getJobFairEntJobPage = (params) => {
+// 查询企业招聘会职位列表
+export const getJobFairPosition = (id) => {
   return request({
-    url: '/app-api/menduner/system/job-fair/detail/page',
+    url: '/app-api/menduner/system/recruit/job-fair/get/by/job-fair',
     method: 'GET',
-    params,
+    params: { id },
     custom: {
       showLoading: false,
-      auth: false
+      auth: true
+    }
+  })
+}
+
+// 企业将职位移除招聘会
+export const quitJobFairPosition = (data) => {
+  return request({
+    url: '/app-api/menduner/system/recruit/job-fair/quit',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 企业将职位加入招聘会
+export const joinJobFairPosition = (data) => {
+  return request({
+    url: '/app-api/menduner/system/recruit/job-fair/join',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false,
+      auth: true
     }
   })
 }
@@ -77,3 +115,16 @@ export const getShareQueryById = (params) => {
     }
   })
 }
+
+// 根据招聘会id获取未加入招聘会的职位列表
+export const getJobFairPositionList = (params) => {
+  return request({
+    url: '/app-api/menduner/system/recruit/job-fair/get/not-joined-job/page',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}

+ 15 - 0
api/new/interview.js

@@ -0,0 +1,15 @@
+import request from "@/utils/request"
+
+// 面试-保存、重新邀约
+export const saveInterviewInvite = (data) => {
+	return request({
+		url: '/app-api/menduner/system/recruit/interview-invite/save',
+		method: "POST",
+		data,
+		custom: {
+			openEncryption: true,
+			auth: true,
+			showLoading: false
+		}
+	})
+}

+ 6 - 177
api/position.js

@@ -1,189 +1,19 @@
 import request from "@/utils/request"
 
-// 根据条件搜索招聘职位
-export const getJobAdvertisedSearch = (params) => {
-  return request({
-    url: '/app-api/menduner/system/job/advertised/search',
-    method: 'GET',
-    params,
-    custom: {
-      showLoading: false,
-      auth: false
-    }
-  })
-}
-
-// 推荐职位列表
-export const getPromotedPosition = (params) => {
-  return request({
-    url: '/app-api/menduner/system/job/advertised/get/recommended',
-    method: 'GET',
-    params,
-    custom: {
-      showLoading: false,
-      auth: false
-    }
-  })
-}
-
 // 职位详情
 export const getPositionDetails = (params) => {
   return request({
-    url: '/app-api/menduner/system/job/advertised/get/detail',
+    url: '/app-api/menduner/system/recruit/job-advertised/detail',
     method: 'GET',
     params,
     custom: {
+      openEncryption: true,
       showLoading: false,
       auth: false
     }
   })
 }
 
-// 效验招聘职位是否投递
-export const jobCvRelCheckSend = (params) => {
-  return request({
-    url: '/app-api/menduner/system/job-cv-rel/send/check',
-    method: 'GET',
-    params,
-    custom: {
-      showLoading: false,
-      auth: true
-    }
-  })
-}
-
-// 效验“招聘会”职位是否投递
-export const jobFairCvRelCheckSend = (params) => {
-  return request({
-    url: '/app-api/menduner/system/job-cv-rel/job-fair/send/check',
-    method: 'GET',
-    params,
-    custom: {
-      showLoading: false,
-      auth: true
-    }
-  })
-}
-
-// 效验求职者是否收藏该职位
-export const getJobFavoriteCheck = (params) => {
-  return request({
-    url: '/app-api/menduner/system/person/job/favorite/check',
-    method: 'GET',
-    params,
-    custom: {
-      showLoading: false,
-      auth: true
-    }
-  })
-}
-
-// 求职者收藏职位
-export const getPersonJobFavorite = (data) => {
-  return request({
-    url: '/app-api/menduner/system/person/job/favorite',
-    method: 'POST',
-    data,
-    custom: {
-      auth: true,
-      showLoading: false
-    }
-  })
-}
-
-// 求职者取消收藏职位
-export const getPersonJobUnfavorite = (jobId) => {
-  return request({
-    url: `/app-api/menduner/system/person/job/unfavorite?jobId=` + jobId,
-    method: 'DELETE',
-    // params: {
-    //   jobId
-    // },
-    custom: {
-      auth: true,
-      showLoading: false
-    }
-  })
-}
-
-// 投递简历
-export const jobCvRelSend = (data) => {
-  return request({
-    url: '/app-api/menduner/system/job-cv-rel/send',
-    method: 'POST',
-    data,
-    custom: {
-      auth: true,
-      showLoading: false
-    }
-  })
-}
-
-// 众聘分享-投递简历
-export const jobCvRelHireSend = (data) => {
-  return request({
-    url: '/app-api/menduner/system/job-cv-rel/hire/recommend/send',
-    method: 'POST',
-    data,
-    custom: {
-      auth: true,
-      showLoading: false
-    }
-  })
-}
-
-// 获取众聘职位分页
-export const getJobAdvertisedHire = (params) => {
-  return request({
-    url: '/app-api/menduner/system/job/advertised/get/hire',
-    method: 'GET',
-    params,
-    custom: {
-      showLoading: false,
-      auth: false
-    }
-  })
-}
-
-// 获取统计信息
-export const getRecommendCount = (params) => {
-  return request({
-    url: '/app-api/menduner/system/job-cv-rel/hire/get/recommend/count',
-    method: 'GET',
-    params,
-    custom: {
-      showLoading: false,
-      auth: true
-    }
-  })
-}
-
-// 二维码拉新记录
-export const getInviteRecord = (params) => {
-  return request({
-    url: '/app-api/menduner/system/person/get/invite/person/page',
-    method: 'GET',
-    params,
-    custom: {
-      showLoading: false,
-      auth: true
-    }
-  })
-}
-
-// 获取统计信息
-export const getRecommendationList = (params) => {
-  return request({
-    url: '/app-api/menduner/system/job-cv-rel/hire/page',
-    method: 'GET',
-    params,
-    custom: {
-      showLoading: false,
-      auth: true
-    }
-  })
-}
-
 // 根据id查询分享的职位id与推荐人id
 export const getShareDetail = (params) => {
   return request({
@@ -197,15 +27,14 @@ export const getShareDetail = (params) => {
   })
 }
 
-// 职位详情-获取相似职位
-export const getSimilarPosition = (params) => {
+// 获取企业发布职位类型权限
+export const getEnterprisePubJobTypePermission = () => {
   return request({
-    url: '/app-api/menduner/system/job/advertised/get/acquainted',
+    url: '/app-api/menduner/system/recruit/enterprise/get/pub-job-type-perm',
     method: 'GET',
-    params,
     custom: {
       showLoading: false,
-      auth: false
+      auth: true
     }
   })
 }

+ 1 - 80
api/resume.js

@@ -14,14 +14,6 @@ export const saveResumeBasicInfo = async (data) => {
   })
 }
 
-// // 保存个人优势
-// export const saveResumeAdvantage = async (data) => {
-//   return await request.post({
-//     url: '/app-api/menduner/system/person/resume/advantage/save',
-//     data
-//   })
-// }
-
 // 保存培训经历
 export const saveResumeTrainExp = async (data) => {
   return request({
@@ -133,33 +125,6 @@ export const saveResumeWorkExp = async (data) => {
   })
 }
 
-// // 保存项目经历
-// export const saveResumeProjectExp = async (data) => {
-//   return await request.post({
-//     url: '/app-api/menduner/system/person/resume/project/exp/save',
-//     data
-//   })
-// }
-
-// // 删除项目经历
-// export const deleteResumeProjectExp = async (id) => {
-//   return await request.delete({
-//     url: '/app-api/menduner/system/person/resume/project/exp/remove?id=' + id
-//   })
-// }
-
-// 获取项目经历
-// export const getResumeProjectExp = async () => {
-//   return request({
-//     url: '/app-api/menduner/system/person/resume/get/project/exp',
-//     method: 'GET',
-//     custom: {
-//       showLoading: false,
-//       auth: true
-//     }
-//   })
-// }
-
 // 获取-技能树形
 export const getSkillTree = async () => {
   return request({
@@ -283,48 +248,4 @@ export const enterpriseSearchByName = async (params) => {
       auth: true
     }
   })
-}
-
-// // 保存附件
-// export const savePersonResumeCv = async (data) => {
-//   return await request.post({
-//     url: '/app-api/menduner/system/person/resume/person/cv/save',
-//     data
-//   })
-// }
-
-// // 删除附件
-// export const deletePersonResumeCv = async (id) => {
-//   return await request.delete({
-//     url: '/app-api/menduner/system/person/resume/person/cv/remove?id=' + id
-//   })
-// }
-
-// // 获取附件列表
-// export const getPersonResumeCv = async () => {
-//   return await request.get({
-//     url: '/app-api/menduner/system/person/resume/get/person/cv'
-//   })
-// }
-
-// // 修改求职类型
-// export const updateJobStatus = async (data) => {
-//   return await request.post({
-//     url: '/app-api/menduner/system/person/resume/job/status/update?status=' + data,
-//   })
-// }
-
-// // 修改人才头像
-// export const updatePersonAvatar = async (url) => {
-//   return await request.post({
-//     url: `/app-api/menduner/system/person/resume/avatar/update?avatar=${url}`
-//   })
-// }
-
-// // 修改个人画像
-// export const savePersonPortrait = async (data) => {
-//   return await request.post({
-//     url: '/app-api/menduner/system/person/resume/tag/update',
-//     data
-//   })
-// }
+}

+ 42 - 0
api/search.js

@@ -0,0 +1,42 @@
+import request from "@/utils/request"
+
+// 获取发布的职位列表
+export const getJobAdvertised = async (params) => {
+	return request({
+		url: '/app-api/menduner/system/recruit/job-advertised/list',
+		method: 'GET',
+		params,
+		custom: {
+			showLoading: false,
+			auth: true
+		}
+	})
+}
+
+// 根据职位id获取推荐人才列表
+export const getPersonRecommendPage = (params) => {
+	return request({
+		url: '/app-api/menduner/system/recruit/person-recommend/page',
+		method: 'GET',
+		params,
+		custom: {
+			openEncryption: true,
+			showLoading: false,
+			auth: false
+		}
+	})
+}
+
+// 根据条件搜索人才
+export const getPersonConditionSearchPage = (params) => {
+	return request({
+		url: '/app-api/menduner/system/recruit/person-search/page',
+		method: 'GET',
+		params,
+		custom: {
+			openEncryption: true,
+			showLoading: false,
+			auth: false
+		}
+	})
+}

+ 1 - 1
components/PositionList/index.vue

@@ -185,7 +185,7 @@ const handleEdit = async (val) => {
 // 职位详情
 const handleDetail = (item) => {
   if (!item.id) return
-  let url = `/pagesB/positionDetail/index?id=${item.id}&area=${item.areaName}`
+  let url = `/pagesB/positionDetail/index?jobId=${item.id}&isEdit=${item.edit}`
   uni.navigateTo({ url })
 }
 

+ 0 - 94
components/ResumeStatus/index.vue

@@ -1,94 +0,0 @@
-<template>
-	<slot name="header"></slot>
-	<view class="content">
-		<view v-for="(item,index) in items" :key="item.label" class="content-box" @tap="handleTo(item)">
-			<view class="content-box-value">
-				{{ item.count }}
-			</view>
-			<view class="content-box-title">
-				{{ item.label }}
-			</view>
-		</view>
-	</view>
-	
-</template>
-
-<script setup>
-import { ref, watch } from 'vue';
-import { getRecommendCount } from '@/api/position.js'
-import { getDict } from '@/hooks/useDictionaries.js'
-import { onShow } from '@dcloudio/uni-app'
-// const props = defineProps({
-// 	type: {
-// 		type: String,
-// 		default: 'menduner_hire_job_cv_status'
-// 	}
-// })
-
-import { userStore } from '@/store/user'
-const useUserStore = userStore()
-
-// 监听登录状态
-watch(() => useUserStore.refreshToken, (newVal, oldVal) => {
-	const reset = Boolean(!newVal)
-	recommendCount(reset)
-})
-
-onShow(() => {
-	recommendCount()
-})
-
-const items = ref([])
-
-const handleTo = (item) => {
-	uni.navigateTo({
-		url: `/pagesA/recommendation/index?id=${item.value}`
-	})
-}
-
-async function recommendCount (reset = false) {
-	try {
-		const { data: dict } = await getDict('menduner_hire_job_cv_status')
-		if (!dict?.data) {
-			return
-		}
-		items.value = dict.data.map(e => {
-			return {
-				...e,
-				count: 0
-			}
-		})
-		// console.log(items)
-		if (reset) {
-			return
-		}
-		const { data } = await getRecommendCount()
-		if (!data) {
-			return
-		}
-		items.value.forEach(e => {
-			e.count = data.find(_e => _e.key === e.value)?.value || 0
-		})
-	} catch (error) {
-		// console.log(error)
-	}
-}
-
-</script>
-
-<style scoped lang="scss">
-.content {
-	display: flex;
-	justify-content: space-around;
-	padding: 36rpx 12rpx;
-	&-box {
-		font-size: 24rpx;
-		color: #999;
-		text-align: center;
-		&-value {
-			font-size: 1.8em;
-			color: #000;
-		}
-	}
-}
-</style>

+ 1 - 0
hooks/useIM.js

@@ -404,6 +404,7 @@ export async function prologue ({userId, enterpriseId, text}) {
 export async function talkToUser ({userId, text}) {
   const { channel, isNewTalk } = await checkConversation(userId)
   if (!isNewTalk) send(text, channel)
+  return channel
 }
 
 // 检测是否存在频道

+ 6 - 12
pages.json

@@ -185,15 +185,15 @@
 					}
 				},
 				{
-					"path": "jobFair/index",
+					"path": "jobFair/details",
 					"style": {
-						"navigationBarTitleText": "招聘会"
+						"navigationBarTitleText": "招聘会详情"
 					}
 				},
 				{
-					"path": "jobFair/jobFairShare",
+					"path": "jobFair/join",
 					"style": {
-						"navigationBarTitleText": "分享招聘会"
+						"navigationBarTitleText": "职位加入到招聘会"
 					}
 				},
 				{
@@ -203,15 +203,9 @@
 					}
 				},
 				{
-					"path": "jobFair/enterprisesClassification",
+					"path": "InviteInterview/index",
 					"style": {
-						"navigationBarTitleText": "招聘会/企业"
-					}
-				},
-				{
-					"path": "jobFair/positionClassification",
-					"style": {
-						"navigationBarTitleText": "招聘会/职位"
+						"navigationBarTitleText": "邀请面试"
 					}
 				}
 			]

+ 0 - 3
pages/index/communicate.vue

@@ -121,9 +121,6 @@ const handleTo = (item) => {
 	const query = {
 		id: userInfoVo?.userInfoResp?.userId,
 		name: thatName,
-		postName: postNameCn,
-		enterpriseName: formatName(enterpriseAnotherName),
-		enterpriseId: userInfoVo?.userInfoResp?.enterpriseId,
 		channelID: channel_id,
 		channelType: channel_type,
 		avatar: userInfoVo?.userInfoResp?.avatar,

+ 152 - 0
pages/index/components/condition.vue

@@ -0,0 +1,152 @@
+<template>
+	<view class="box defaultBgc">
+		<view style="background-color: #fff;">
+			<uni-search-bar
+        v-model="params.content"
+        radius="5"
+        placeholder="输入关键字"
+        cancelButton="none"
+        :focus="false"
+				@clear="handleSearch('content', null)"
+        @confirm="handleSearch('content', params.content)"
+      ></uni-search-bar>
+			<FilterList :list="filterList" idValue="label" @change="handleSearch"></FilterList>
+		</view>
+		
+		<scroll-view class="scrollBox" :scroll-y="true" @scrolltolower="loadingMore" style="position:relative;">
+			<TalentItem v-if="items?.length" :items="items" :showLastWorkExp="false" />
+			<uni-load-more v-if="items.length" :status="more" />
+			<view v-else class="noJobId">请选择条件搜索人才</view>
+		</scroll-view>
+	</view>
+</template>
+
+<script setup>
+import { ref, nextTick } from 'vue'
+import TalentItem from './talentItem.vue'
+import FilterList from '@/components/FilterList'
+import { getPersonConditionSearchPage } from '@/api/search'
+import { dealDictArrayData } from '@/utils/position'
+import { timesTampChange, getTimeDifferenceInChinese } from '@/utils/date'
+
+const query = ref({
+	pageNo: 1,
+	pageSize: 10
+})
+const params = ref({
+  content: null,
+  positionIds: [],
+  areaIds: [],
+  expType: '',
+  eduType: ''
+})
+const items = ref([])
+const more = ref('more')
+const total = ref(0)
+const filterList = ref([
+  { label: '职位', dictType: 'positionTreeData',key: 'positionId', map: { text: 'nameCn', value: 'id' } },
+  { label: '城市', multiple: true, dictType: 'areaTreeData', key: 'areaIds', map: { text: 'name', value: 'id' } },
+  { label: '最高学历', dictType: 'menduner_education_type', key: 'eduType' },
+  { label: '工作经验', dictType: 'menduner_exp_type', key: 'expType' },
+])
+
+// 根据条件搜索人才
+const getTalentList = async () => {
+  try {
+		more.value = 'loading'
+		const { data } = await getPersonConditionSearchPage(Object.assign(query.value, params.value))
+
+		if (!data.list.length) {
+			more.value = 'noMore'
+			items.value = []
+			total.value = 0
+			return
+		}
+
+		const list = dealDictArrayData([], data.list).map(e => {
+      if (e.workList?.length) {
+        e.workList.forEach(exp => {
+          exp.startTimeStr = exp.startTime ? timesTampChange(exp.startTime, 'Y-M') : '未填写工作时间'
+          exp.endTimeStr = exp.startTime ? exp.endTime ? timesTampChange(exp.endTime, 'Y-M') : '至今' : ''
+          exp.year = exp.endTimeStr ? getTimeDifferenceInChinese(exp.startTime, exp.endTime) : ''
+        })
+      }
+      return e
+    })
+		items.value = items.value.concat(list)
+		total.value = data.total
+
+		if (items.value.length === +data.total) {
+      more.value = 'noMore'
+      return
+    }
+	} catch {
+		query.value.pageNo--
+		more.value = 'more'
+	}
+}
+
+const checkValue = (obj) => {
+  return Object.values(obj).some(value => {  
+    return value !== null && value !== undefined && value !== '' && (Array.isArray(value) ? value.length > 0 : true)
+  })
+}
+
+const handleSearch = (key, value) => {
+	query.value.pageNo = 1
+	params.value[key] = value
+
+	if (!checkValue(params.value)) {
+		uni.showToast({
+			title: '请至少选择一个查询条件',
+			icon: 'none',
+			duration: 2000
+		})
+		more.value = 'noMore'
+		items.value = []
+		total.value = 0
+		return
+	}
+
+	items.value = []
+	total.value = 0
+	getTalentList()
+}
+
+// 加载更多
+const loadingMore = () => {
+	if (more.value === 'noMore') return
+
+  more.value = 'loading'
+  query.value.pageNo++
+	getTalentList()
+}
+</script>
+
+<style scoped lang="scss">
+.noJobId {
+	text-align: center;
+	line-height: 60vh;
+	color: #666;
+}
+.box {
+  height: 100vh;
+  overflow: hidden;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+}
+.scrollBox{
+	flex: 1;
+  padding-bottom: 100px;
+  box-sizing: border-box;
+	height: 0 !important;
+}
+.stickFilter {
+  z-index: 1;
+  position: sticky;
+  box-shadow: 0px 10rpx 12rpx 0px rgba(195, 195, 195, .25);
+  top: 110rpx;
+	background-color: #fff;
+}
+</style>

+ 99 - 0
pages/index/components/recommend.vue

@@ -0,0 +1,99 @@
+<template>
+	<view class="box defaultBgc">
+		<view style="padding: 10px 15px 15px 15px; background-color: #fff;">
+			<uni-data-select 
+				v-model="query.jobId" 
+				:clear="false" 
+				:localdata="jobList" 
+				@change="handleChangeJob" 
+				placeholder="招聘中职位"
+			></uni-data-select>
+		</view>
+		
+		<scroll-view class="scrollBox" :scroll-y="true" @scrolltolower="loadingMore" style="position:relative;">
+			<TalentItem v-if="query.jobId && (items?.length || more !== 'loading')" :items="items" />
+			<uni-load-more v-if="query.jobId" :status="more" />
+			<view v-else class="noJobId">请选择要推荐人才的职位</view>
+		</scroll-view>
+	</view>
+</template>
+
+<script setup>
+import { ref, nextTick } from 'vue'
+import TalentItem from './talentItem.vue'
+import { getPersonRecommendPage } from '@/api/search'
+import { dealDictArrayData } from '@/utils/position'
+
+defineProps({ jobList: Array })
+
+const query = ref({
+	pageNo: 1,
+	paeSize: 20,
+	jobId: null
+})
+const items = ref([])
+const more = ref('more')
+const total = ref(0)
+
+// 根据职位id获取推荐人才列表
+const getRecommendList = async () => {
+  try {
+		more.value = 'loading'
+		const { data } = await getPersonRecommendPage(query.value)
+
+		if (!data.list.length) {
+			more.value = 'noMore'
+			return
+		}
+
+		const list = dealDictArrayData([], data.list)
+		items.value = items.value.concat(list)
+		total.value = data.total
+
+		if (items.value.length === +data.total) {
+      more.value = 'noMore'
+      return
+    }
+	} catch {
+		query.value.pageNo--
+		more.value = 'more'
+	}
+}
+
+// 选择招聘中职位
+const handleChangeJob = (e) => {
+	query.value.pageNo = 1
+	items.value = []
+	total.value = 0
+	if (e) getRecommendList()
+}
+
+// 加载更多
+const loadingMore = () => {
+	if (more.value === 'noMore') return
+  more.value = 'loading'
+  query.value.pageNo++
+	getRecommendList()
+}
+</script>
+
+<style scoped lang="scss">
+.noJobId {
+	text-align: center;
+	line-height: 60vh;
+	color: #666;
+}
+.box {
+  height: 100vh;
+  overflow: hidden;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+}
+.scrollBox{
+	flex: 1;
+  padding-bottom: 100px;
+  box-sizing: border-box;
+	height: 0 !important;
+}
+</style>

+ 115 - 0
pages/index/components/talentItem.vue

@@ -0,0 +1,115 @@
+<template>
+	<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.avatar, val.sex)" mode="scaleToFill"></image>
+					<image class="user-avatar-sex" :src="val?.sex ? val?.sex === '1' ? '/static/img/man.png' : '/static/img/female.png' : ''" alt="" mode="scaleToFill" />
+				</view>
+				<view style="flex: 1; margin-left: 10px;">
+					<view class="font-size-18">{{ val.name }}</view>
+					<view class="ss-m-t-10">
+						<span 
+							class="color-666"
+							v-for="(key, index) in desc" 
+							:key="index"
+						>
+							{{ val[key] }}
+							<span v-if="index !== desc.length - 1 && val[key]" class="ss-m-x-10">|</span>
+						</span>
+					</view>
+				</view>
+			</view>
+			
+			<!-- 感兴趣职位 -->
+			<view v-if="val.position?.positionNames && showLastWorkExp" class="ss-m-t-15 color-666">
+				感兴趣职位:{{ val.position?.positionNames || '暂无' }}
+			</view>
+
+			<!-- 最近一份工作经验 -->
+			<view v-if="val?.lastWorkExp && showLastWorkExp" class="ss-m-t-15 color-666">
+				<view>
+					<uni-icons type="smallcircle" class="ss-m-r-10" color="#666"></uni-icons>
+					{{ formatName(val.lastWorkExp.enterpriseName) || '未填写企业名称' }}
+				</view>
+				<view class="ss-m-l-45">
+					<span>{{ formatName(val.lastWorkExp.positionName) || '未填写职位名称' }}</span>
+					<span v-if="val.lastWorkExp.startTime" class="ss-m-l-20">
+						{{ val.lastWorkExp.startTime ? timesTampChange(val.lastWorkExp.startTime, 'Y-M') : '暂无' }}
+						- 
+						{{ val.lastWorkExp.endTime ? timesTampChange(val.lastWorkExp.endTime, 'Y-M') : val.lastWorkExp.startTime ? '至今' : '' }}
+					</span>
+				</view>
+			</view>
+
+			<!-- 工作经历 -->
+			<view v-if="val?.workList && val.workList.length > 0 && !showLastWorkExp" class="ss-m-t-15 color-666">
+				<view v-for="k in val.workList" :key="k.id" class="ss-m-b-30">
+					<view>
+						<uni-icons type="smallcircle" class="ss-m-r-10" color="#666"></uni-icons>
+						{{ formatName(k.enterpriseName) || '未填写企业名称' }}
+					</view>
+					<view class="ss-m-l-45">
+						<span>{{ formatName(k.positionName) || '未填写职位名称' }}</span>
+						<span class="ss-m-l-20">
+							<span>{{ k.startTimeStr }}</span>
+							<span v-if="k.endTimeStr"> - {{ k.endTimeStr }}</span>
+							<!-- <span v-if="k.year"> ({{ k.year }})</span> -->
+						</span>
+					</view>
+				</view>
+			</view>
+		</uni-card>
+	</view>
+</template>
+
+<script setup>
+import { getUserAvatar } from '@/utils/avatar.js'
+import { timesTampChange } from '@/utils/date'
+import { formatName } from '@/utils/getText'
+
+defineProps({
+	items: Array,
+	showLastWorkExp: {
+		type: Boolean,
+		default: true
+	}
+})
+
+const desc = ['jobStatusName', 'eduName', 'expName']
+
+const handleDetail = ({ userId }) => {
+	if (!userId) {
+		uni.showToast({
+			title: '资源获取失败,请稍后重试',
+			icon: 'none',
+			duration: 2000
+		})
+		return
+	}
+	uni.navigateTo({
+		url: `/pagesB/personnelDetails/index?id=${userId}&type=1`
+	})
+}
+</script>
+
+<style scoped lang="scss">
+.user-avatar {
+	position: relative;
+	&-img {
+		width: 60px;
+		height: 60px;
+		border-radius: 50%;
+	}
+	&-sex {
+		position: absolute;
+		right: 0;
+		bottom: 2px;
+		width: 20px;
+		height: 20px;
+		background-color: #fff;
+		border-radius: 50%;
+	}
+}
+</style>

+ 80 - 11
pages/index/jobFair.vue

@@ -1,19 +1,88 @@
 <template>
-	<layout-page>招聘会</layout-page>
+	<layout-page>
+    <view style="padding-bottom: 30px; ">
+      <view v-if="items.length">
+        <uni-card v-for="val in items" :key="val.id" @tap="handleToJobFairEnterprises(val)">
+          <image v-if="val?.previewImg" class="ss-m-t-10" :src="val.previewImg" mode="widthFix" style="width: 100%; height: auto; border-radius: 6px;"></image>
+          <view class="ss-m-t-20">活动时间:{{ timesTampChange(val.startTime, 'Y-M-D') }}至{{ timesTampChange(val.endTime, 'Y-M-D') }}</view>
+          <button class="ss-m-t-20 ss-m-b-10" style="background-color: #00B760; color: #fff;" type="primary">立即加入</button>
+        </uni-card>
+      </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>
+    </view>
+  </layout-page>
 </template>
 
 <script setup>
-	import layoutPage from '@/layout'
-	import { onShow } from '@dcloudio/uni-app'
-	
+import layoutPage from '@/layout'
+import { ref, watch } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
+import { getAccessToken } from '@/utils/request'
+import { showAuthModal } from '@/hooks/useModal'
+import { getJobFairList, checkJobFairPermission } from '@/api/jobFair'
+import { timesTampChange } from '@/utils/date'
+import { userStore } from '@/store/user'
+
+const useUserStore = userStore()
+const items = ref([])
+
+// 获得招聘会列表
+const getList = async () => {
+  const res = await getJobFairList()
+  items.value = res?.data || []
+}
+
+watch(() => useUserStore.refreshToken, () => {
+	if (!useUserStore.refreshToken) return items.value = []
+	getList()
+})
+
+onShow(() => {
 	// 设置自定义tabbar选中值
-	onShow(() => {
-	    const currentPage = getCurrentPages()[0];  // 获取当前页面实例
-	    const currentTabBar = currentPage?.getTabBar?.();
-	
-	    // 设置当前tab页的下标index
-	    currentTabBar?.setData({ selected: 3 });
-	})
+  const currentPage = getCurrentPages()[0]  // 获取当前页面实例
+  const currentTabBar = currentPage?.getTabBar?.()
+
+  // 设置当前tab页的下标index
+  currentTabBar?.setData({ selected: 3 })
+
+	if (!getAccessToken()) return showAuthModal()
+
+  getList()
+})
+
+//跳转招聘会详情
+const handleToJobFairEnterprises = async (val) => {
+  if (!val?.id) {
+    uni.showToast({ title: '资源获取失败,请稍后重试', icon: 'none' })
+  }
+
+  try {
+    const { data } = await checkJobFairPermission(val.id)
+    if (data) {
+      uni.navigateTo({
+        url: '/pagesB/jobFair/details?id=' + val.id
+      })
+    }
+  } catch (error) {
+    // 权限被禁用
+    if (error?.code === 1100056008) {
+      uni.showToast({ title: error.msg, icon: 'none', duration: 2000 })
+      return
+    }
+    // 没有权限参加招聘会,购买门票
+    if (error?.code === 1100056005) {
+      // 没有设置门票金额则提示无权限参加
+      if (!val?.admissionPrice || val?.admissionPrice <= 0) {
+        uni.showToast({ title: error.msg, icon: 'none', duration: 2000 })
+        return
+      }
+      // 设置门票金额则提示购买门票
+      uni.showToast({ title: '您暂时无法参加该招聘会,请先购买门票', icon: 'none', duration: 2000 })
+    }
+  }
+}
 </script>
 
 <style scoped lang="scss">

+ 1 - 0
pages/index/position.vue

@@ -75,6 +75,7 @@ const positionListData = ref([])
 const query = ref({
   pageSize: 5, 
   pageNo: 1,
+  hire: false,
   name: '',
 })
 const more = ref('more')

+ 63 - 19
pages/index/search.vue

@@ -1,29 +1,73 @@
 <template>
 	<layout-page>
-		<view @tap="handleTo">企业-找人</view>
+		<view >
+      <view style="padding: 0 15px 0 15px;">
+				<uni-segmented-control 
+					:current="current"
+					:values="tabList"
+					@clickItem="changeControl"
+					styleType="text"
+					activeColor="#00B760"
+				></uni-segmented-control>
+			</view>
+
+			<!-- 职位推荐 -->
+			<RecommendPage v-if="current === 0" :jobList="jobList" />
+
+			<!-- 条件筛选 -->
+			<ConditionPage v-else />
+
+    </view>
 	</layout-page>
 </template>
 
 <script setup>
-	import layoutPage from '@/layout'
-	import { onShow } from '@dcloudio/uni-app'
-	
+import layoutPage from '@/layout'
+import { ref, watch } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
+import { getAccessToken } from '@/utils/request'
+import { showAuthModal } from '@/hooks/useModal'
+import { getJobAdvertised } from '@/api/search'
+import { userStore } from '@/store/user'
+import { formatName } from '@/utils/getText'
+import RecommendPage from './components/recommend.vue'
+import ConditionPage from './components/condition.vue'
+
+const current = ref(0)
+const tabList = ['职位推荐', '条件筛选']
+const useUserStore = userStore()
+
+// 职位列表
+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)}_${e.areaName ? e.area?.str : '全国'}`, value: e.id }
+    })
+  }
+}
+
+watch(() => useUserStore.refreshToken, () => {
+	if (!useUserStore.refreshToken) return jobList.value = []
+	getJobList()
+}, { immediate: true })
+
+onShow(() => {
 	// 设置自定义tabbar选中值
-	onShow(() => {
-	    const currentPage = getCurrentPages()[0];  // 获取当前页面实例
-	    const currentTabBar = currentPage?.getTabBar?.();
-	
-	    // 设置当前tab页的下标index
-	    currentTabBar?.setData({ selected: 0 });
-	})
-
-	// uni.switchTab({ url: '/pages/index/position' }) // 测试数据
-
-	const handleTo = () => {
-		uni.navigateTo({
-			url: '/pagesB/personnelDetails/index?id=1'
-		})
-	}
+  const currentPage = getCurrentPages()[0]  // 获取当前页面实例
+  const currentTabBar = currentPage?.getTabBar?.()
+
+  // 设置当前tab页的下标index
+  currentTabBar?.setData({ selected: 0 })
+
+	if (!getAccessToken()) return showAuthModal()
+
+})
+
+const changeControl = (e) =>{
+  current.value = e.currentIndex
+}
 </script>
 
 <style scoped lang="scss">

+ 68 - 24
pagesA/chart/index.vue

@@ -100,10 +100,9 @@
                     我想要一份您的简历,您是否同意
                   </text>
                 </view>
-                <!-- <view class="btn-actions">
+                <view class="btn-actions">
                   <text class="btn" v-if="val.payload.content?.type === 1" @tap="handlePreview(val.payload)">点击预览附件简历</text>
-                  <text class="btn" v-if="val.payload.content?.type === 2" @tap="handleFindResume">点击发送附件简历</text>
-                </view> -->
+                </view>
               </view>
             </view>
             <view v-else-if="val.payload.type === -1" class="message-text" :class="{ active: val.from_uid === IM.uid}">
@@ -117,9 +116,10 @@
       </scroll-view>
       
     <view class="box-bottom" v-if="channelItem?.channelID !== 'system'">
-      <view class="box-bottom-tool" style="display: flex; justify-content: space-between;">
-        <!-- <text class="toolBtn" :class="{ disabled: !isSendResume }" @tap="handleFindResume">{{ isSendResume ? '简历已投递' : '发送简历' }}</text> -->
-      </view>
+      <!-- <view class="box-bottom-tool" style="display: flex; justify-content: start;">
+        <text class="toolBtn" @tap="handleFindResume">求简历</text>
+        <text class="toolBtn ss-m-l-10" @tap="handleInviteInterview">面试邀约</text>
+      </view> -->
       
       <view class="d-flex align-end textBox" v-if="channelItem?.channelID !== 'system'">
         <textarea
@@ -134,6 +134,11 @@
         <text class="submitBtn" @tap="handleSend">发 送</text>
       </view>
     </view>
+
+    <!-- <uni-popup ref="invitePopup" background-color="#fff" type="bottom">
+			<view style="background-color: #fff; height: 70vh;">
+      </view>
+		</uni-popup> -->
   </view>
 </template>
 
@@ -143,11 +148,14 @@ import { onLoad } from '@dcloudio/uni-app'
 import { useIMStore } from '@/store/im'
 import { userStore } from '@/store/user'
 import { initConnect, send, initChart, getMoreMessages, toChannel } from '@/hooks/useIM'
-import { getDict } from '@/hooks/useDictionaries'
 import { timesTampChange } from '@/utils/date'
 import { getUserAvatar } from '@/utils/avatar'
 import { formatName } from '@/utils/getText'
 import { preview } from '@/utils/preview'
+import { getAccessToken } from '@/utils/request'
+import { showAuthModal } from '@/hooks/useModal'
+import { getJobAdvertisedList } from '@/api/new/position'
+
 const useUserStore = userStore()
 const userInfo = computed(() => useUserStore?.userInfo)
 
@@ -160,16 +168,11 @@ const hasMore = ref(false)
 
 const pageSize = ref(1)
 
-// 求职者面试列表
-// 求职端-获取求职者与当前邀请人的面试记录
-const statusList = ref([])
 const inputValue = ref('')
 
 const newsId = ref('') // newsId 需要和聊天列表里面的id对应 永远取最后列表中的最后一个就可以做到发送消息即时滚动到底部
 const scrollAnimation = ref(false)
 
-const isSendResume = ref(false)
-const positionInfo = ref({})
 const isEmployment = ref('-1')
 
 onMounted(() => {
@@ -233,17 +236,6 @@ const setScrollBottom = () => {
   }
 }
 
-const getStatusList = async () => {
-  try {
-    const { data } = await getDict('menduner_interview_invite_status')
-    if (data.data.length) {
-      statusList.value = data.data
-    }
-  } catch (error) {
-
-  }
-}
-
 function handleSend () {
   if (!inputValue.value) {
     uni.showToast({ title: '不能发送空白信息', icon: 'none' })
@@ -252,6 +244,57 @@ function handleSend () {
   send(inputValue.value, channelItem.value)
 }
 
+// 职位列表
+const jobNum = ref(0)
+const jobList = ref([])
+const getJobList = async () => {
+  const { data } = await getJobAdvertisedList({ pageNo: 1, pageSize: 10, hasExpiredData: false, status: 0 })
+  jobNum.value = data?.total || 0
+  jobList.value = data?.list ? data?.list.map(e => {
+    return { text: formatName(e.name), value: e.id, ...e }
+  }) : []
+}
+
+// 求简历
+const handleFindResume = async () => {
+  if (!getAccessToken()) {
+    showAuthModal()
+    return
+  }
+  // 企业必须有招聘中的职位才能发起面试邀请
+  if (jobNum.value === 0) {
+		uni.showToast({
+			title: '请先发布招聘职位',
+			icon: 'none',
+			duration: 2000
+		})
+		return
+	}
+}
+
+// 邀约面试
+const invitePopup = ref()
+const handleInviteInterview = () => {
+  if (!getAccessToken()) {
+    showAuthModal()
+    return
+  }
+  // 企业必须有招聘中的职位才能发起面试邀请
+  if (jobNum.value === 0) {
+		uni.showToast({
+			title: '请先发布招聘职位',
+			icon: 'none',
+			duration: 2000
+		})
+		return
+	}
+  console.log(info.value, '邀请面试', jobList.value, invitePopup.value)
+  invitePopup.value.open()
+	// uni.navigateTo({
+	//   url: `/pagesB/InviteInterview/index?id=${info.value.id}`
+	// })
+}
+
 // 预览简历
 function handlePreview (payload) {
   if (!payload?.content?.query?.src) {
@@ -296,7 +339,8 @@ onLoad(async (options) => {
   }
   await init(info.value.id, info.value.enterpriseId)
 
-  await getStatusList()
+  // await getJobList() // 获取招聘中的职位
+
   // 清除未读消息
   resetUnread(channelItem.value, userInfo.value?.enterpriseId)
   // 更新未读消息

+ 114 - 0
pagesB/InviteInterview/index.vue

@@ -0,0 +1,114 @@
+<template>
+	<view style="padding: 30rpx;">
+		<uni-forms ref="formRef" :modelValue="formData" :rules="formRules" validateTrigger="bind" label-width="80px" label-align="right" label-position="left">
+			<uni-forms-item name="time" label="面试时间" required>
+				<uni-datetime-picker v-model="formData.time" type="datetime" :border="true" returnType="timestamp" :hide-second="true" />
+			</uni-forms-item>
+			<uni-forms-item name="jobId" label="招聘职位" required>
+				<uni-data-select	v-model="formData.jobId" :localdata="jobList"	@change="handleChangeJob"	placeholder="请选择招聘职位"></uni-data-select>
+			</uni-forms-item>
+			<uni-forms-item name="address" label="面试地点" required>
+				<uni-easyinput v-model="formData.address" placeholder="请输入面试地点"></uni-easyinput>
+			</uni-forms-item>
+			<uni-forms-item name="invitePhone" label="联系电话" required>
+				<uni-easyinput v-model="formData.invitePhone" placeholder="请输入联系电话"></uni-easyinput>
+			</uni-forms-item>
+			<uni-forms-item name="remark" label="备注事项">
+				<uni-easyinput v-model="formData.remark" type="textarea" placeholder="请输入备注事项"></uni-easyinput>
+			</uni-forms-item>
+		</uni-forms>
+		<button class="send-button" @tap="handleSubmit">提 交</button>
+	</view>
+</template>
+
+<script setup>
+import { ref, unref } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { userStore } from '@/store/user'
+import { mobile } from '@/utils/validate'
+import { getJobAdvertisedList } from '@/api/new/position'
+import { formatName } from '@/utils/getText'
+import { getInterviewInviteDefaultTime } from '@/utils/date'
+import { saveInterviewInvite } from '@/api/new/interview'
+
+const formRef = ref(null)
+const useUserStore = userStore()
+const formData = ref({
+	userId: null,
+	address: null,
+	invitePhone: useUserStore?.userInfo?.phone || '',
+	time: getInterviewInviteDefaultTime().timeStamp,
+	jobId: null,
+	remark: null,
+	type: 1
+})
+const formRules = {
+	time: {
+		rules: [{ required: true, errorMessage: '请选择面试时间' }]
+	},
+  invitePhone: mobile,
+	address: {
+		rules: [{ required: true, errorMessage: '请输入面试地点' }]
+	},
+	jobId: {
+		rules: [{ required: true, errorMessage: '请选择邀请面试的职位' }]
+	}
+}
+
+// 职位列表
+const jobList = ref([])
+const getJobList = async () => {
+  const { data } = await getJobAdvertisedList({ pageNo: 1, pageSize: 10, hasExpiredData: false, status: 0 })
+  jobList.value = data?.list.map(e => {
+    return { text: formatName(e.name), value: e.id, ...e }
+  })
+}
+
+const handleChangeJob = (e) =>{
+	const job = jobList.value.find(item => item.value === e)
+	if (!job) return
+	formData.value.address = job.address
+}
+
+onLoad(async (options) => {
+	const { id } = options
+	if (!id) {
+		uni.showToast({
+			title: '缺少人员id',
+			icon: 'none'
+		})
+		setTimeout(() => {
+			uni.navigateBack({ delta: 1 })
+		}, 1000)
+		return
+	}
+
+	formData.value.userId = id
+	await getJobList()
+})
+
+// 提交
+const handleSubmit = async () => {
+	const valid = await unref(formRef).validate()
+	if (!valid) return
+
+	uni.showLoading({ title: '提交中' })
+	try {
+		await saveInterviewInvite(formData.value)
+		uni.hideLoading()
+		uni.showToast({
+			title: '提交成功',
+			icon: 'success'
+		})
+		setTimeout(() => {
+			uni.navigateBack({ delta: 1 })
+		}, 1000)
+	} catch {
+		uni.hideLoading()
+	}
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 135 - 0
pagesB/jobFair/details.vue

@@ -0,0 +1,135 @@
+<template>
+	<view class="box" :style="`background-color: ${jobFairInfo?.backgroundColour}`">
+		<view class="d-flex" style="padding: 15px 15px 0 15px;">
+			<view class="title-line"></view>
+			<rich-text class="title" :nodes="jobFairInfo?.title?.replace(/<\/?p[^>]*>/gi, '')"></rich-text>
+		</view>
+
+		<scroll-view class="scrollBox" :scroll-y="true" style="position:relative;">
+			<JobItem v-if="jobFairPosition?.length" :list="jobFairPosition" :jobFairId="id" @refresh="getJobFairPositionList" />
+			<uni-load-more v-else status="noMore" />
+		</scroll-view>
+
+		<view class="bottom-sticky">
+			<view class="bottom-content">
+				<button class="btnStyle bgButtons ss-m-l-15" type="primary" plain="true" @tap.stop="handleToShare">我的分享海报</button>
+        <button class="buttons btnStyle" type="primary" @tap.stop="handleJoinJobFair">职位加入</button>
+      </view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { onLoad, onShow } from '@dcloudio/uni-app'
+import { getJobFair, getJobFairPosition } from '@/api/jobFair.js'
+import { dealDictArrayData } from '@/utils/position.js'
+import JobItem from './jobItem.vue'
+
+const id = ref(null)
+
+// 获取招聘会信息
+const jobFairInfo = ref({})
+const getJobFairInfo = async () => {
+  const { data } = await getJobFair(id.value)
+  if (!data) return
+  jobFairInfo.value = data || {}
+}
+
+// 获取招聘会职位
+const jobFairPosition = ref([])
+const getJobFairPositionList = async () => {
+  const { data } = await getJobFairPosition(id.value)
+  if (!data || !data.length) {
+		jobFairPosition.value = []
+		return
+	}
+  jobFairPosition.value = dealDictArrayData([], data)
+}
+
+onLoad((options) => {
+	id.value = options.id
+	if (!id.value) {
+		uni.showToast({
+			title: '缺少招聘会id',
+			icon: 'none'
+		})
+		setTimeout(() => {
+			uni.navigateBack({ delta: 1 })
+		}, 1000)
+		return
+	}
+
+	getJobFairInfo()
+	getJobFairPositionList()
+})
+
+onShow(() => {
+	if (id.value) getJobFairPositionList()
+})
+
+// 加入招聘会
+const handleJoinJobFair = () => {
+  uni.navigateTo({
+		url: '/pagesB/jobFair/join?jobFairId=' + id.value
+	})
+}
+
+// 我的分享海报
+const handleToShare = () => {
+	uni.navigateTo({
+		url: '/pagesB/jobFair/jobFairEntShare?jobFairId=' + id.value
+	})
+}
+</script>
+
+<style scoped lang="scss">
+.title {
+	font-size: 18px;
+	color: #fff;
+}
+.title-line {
+	width: 12px;
+	height: 18px;
+	background-color: #fff;
+	margin-right: 10px;
+	border-radius: 6px;
+	margin-top: 3px;
+}
+.bottom-content {
+  display: flex;
+  justify-content: space-evenly;
+  align-items: center;
+  width: 100%;
+  margin: 20rpx 0;
+  .btnStyle {
+    flex: 1;
+    margin-right: 20rpx;
+		border-radius: 50rpx;
+  }
+  .bgButtons {
+    border: 2rpx solid #00B760;
+    color: #00B760;
+  }
+  &-tool {
+    width: 160rpx;
+    display: flex;
+    justify-content: center;
+    flex-direction: column;
+    align-items: center;
+  }
+}
+.box {
+  height: 100vh;
+  overflow: hidden;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+}
+.scrollBox{
+  flex: 1;
+  height: 0 !important;
+  padding-bottom: 100rpx;
+  box-sizing: border-box;
+}
+</style>

+ 0 - 354
pagesB/jobFair/enterprisesClassification.vue

@@ -1,354 +0,0 @@
-<!-- 招聘会/企业详情 -->
-<template>
-  <view class="box" :style="`background-color: ${backgroundColor}`">
-    <scroll-view class="scrollBox" :scroll-y="true" :scroll-top="scrollTop" @scrolltolower="loadingMore" @scroll="onScroll" style="position:relative;">
-      <view style="position: relative;">
-        <!-- 轮播图 -->
-        <SwiperAd v-if="swiperAdList.length" :list="swiperAdList" margin="0" borderRadius="0" @click="handleToDetails"></SwiperAd>
-        <view class="stick ss-p-y-30" :style="`background-color: ${backgroundColor}`">
-          <view style="position: relative;">
-            <uni-search-bar
-              v-model="query.keyword"
-              placeholder="输入关键字"
-              cancelButton="none"
-              :focus="false"
-              bgColor="#fff"
-              @confirm="onSearch($event.value)"
-              @clear="query.keyword = ''; onSearch()"
-            >
-            </uni-search-bar>
-            <button class="search-btn" @tap.stop="onSearch">搜索</button>
-          </view>
-          <!-- tab页签 -->
-          <scroll-view v-if="tabList?.length" scroll-x="true" class="scroll-container">
-            <view
-              class="scroll-item"
-              :style="`margin-left: ${index ? '24px' : ''};`"
-              v-for="(val, index) in tabList" :key="val.key"
-              @tap="handClickTab(index)"
-            >
-              <view>
-                <view class="text">{{ val.title }}</view>
-                <view v-if="index === tabIndex" class="choose" style="background-color: #fff;"></view>
-                <view v-else class="choose" style="background-color: #ffffff00;"></view>
-              </view>
-            </view>
-          </scroll-view>
-        </view>
-        <view v-if="listData?.length" class="listDataBox">
-          <uni-card v-for="val in listData" :key="val.id" @click="toDetail(val)" :is-shadow="true" :border='false' shadow="0px 0px 3px 1px rgba(0,0,0,0.1)">
-            <view class="d-flex align-center ss-m-30" @click="null">
-              <image class="enterAvatar" :src="val.logoUrl ? val.logoUrl : 'https://minio.citupro.com/dev/menduner/company-avatar.png'"></image>
-              <view class="ss-m-l-20" style="flex: 1;">
-                <view class="font-size-16 enterpriseName">{{ formatName(val.anotherName || val.name) }}</view>
-                <view class="ss-m-t-5">
-                  <span class="color-999">{{ val?.industryName || '' }}</span>
-                  <span class="divider tag-gap1" v-if="val?.industryName && val?.scaleName"> | </span>
-                  <span class="color-999">{{ val?.scaleName || '' }}</span>
-                </view>
-                <view class="ss-m-t-10">
-                  <uni-tag 
-                    v-for="(tag,i) in val?.lastJobTop5 || []"
-                    :key="i"
-                    class="ss-m-r-5"
-                    :text="formatName(tag.name)"
-                    inverted="false"
-                    size="default"
-                    custom-style="background-color: #00B760; color: #fff; border-color: #00B760; display: inline-block;border-radius: 15px; margin: 0 5px 5px 0;"
-                  />
-                </view>
-              </view>
-            </view>
-            <view class="jobCount">{{ val.jobCount }}个在线职位招聘中 >>></view>
-          </uni-card>
-          <uni-load-more :status="more" :color="textColor" />
-        </view>
-        <view v-else class="nodata-img-parent">
-          <uni-load-more class="ss-m-t-50" :color="textColor" status="noMore" :content-text="{'contentnomore': loading ? '加载中. . .' : tabList?.length ? '暂无数据,请切换类型查看~': '暂无数据'}" />
-        </view>
-        <!-- 招聘会分享按钮 -->
-        <view v-if="showShareBtn" class="shareButtonBox" @tap="handleShare">
-          <uni-icons type="redo-filled" size="30" color="#00B760" />
-        </view>
-      </view>
-    </scroll-view>
-  </view>
-</template>
-
-<script setup>
-import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
-import { ref, reactive, computed } from 'vue'
-import { dealDictArrayData } from '@/utils/position'
-import { getJobFairEnterprisePage, getJobFair } from '@/api/jobFair'
-import SwiperAd from '@/components/SwiperAd'
-import { formatName } from '@/utils/getText'
-import { getShareQueryById } from '@/api/jobFair.js'
-
-const loading = ref(true)
-const more = ref('more')
-const listData = ref([])
-const query = reactive({
-  pageSize: 20, 
-  pageNo: 1,
-  keyword: '',
-  jobFairId: undefined,
-})
-
-const showShareBtn = ref(false)
-
-onLoad(async (options) => {
-  // 网站二维码分享
-	if (options.scene) {
-    const scene = decodeURIComponent(options.scene)
-    const key = scene.split('=')[1]
-    if (!key) return
-    const res = await getShareQueryById({key})
-    options.jobFairId = res?.data?.jobFairId
-  }
-  if (options?.jobFairId) {
-    query.jobFairId = options.jobFairId
-    getJobFairDetail()
-	}
-  
-  // 转发朋友
-  onShareAppMessage(() => {
-    if(!jobFairTitle.value){
-      setTimeout(() => {}, 1000)
-    }
-    return {
-      title: jobFairTitle.value,
-      path: `/pagesB/jobFair/enterprisesClassification?jobFairId=${options?.jobFairId}`
-    }
-  })
-  // 转发朋圈
-  onShareTimeline(() => {
-    if(!jobFairTitle.value){
-      setTimeout(() => {}, 1000)
-    }
-    return {
-      title: jobFairTitle.value || '门墩儿 专注顶尖招聘',
-      path: `/pagesB/jobFair/enterprisesClassification?jobFairId=${options?.jobFairId}`
-    }
-  })
-})
-
-// 招聘会详情
-const tabIndex = ref(-1)
-const tabList = ref([])
-const swiperAdList = ref([])
-const backgroundColor = ref('#fff')
-const jobFairTitle = ref('')
-const textColor = computed(() => {
-  return backgroundColor.value === '#fff' ? '#777' : '#fff'
-})
-const getJobFairDetail = async () => {
-  if (!query.jobFairId) return
-  const { data } = await getJobFair(query.jobFairId)
-  // 类型 -1为不传tag参数
-  tabList.value = data?.tag || []
-  handClickTab(tabList.value?.length ? 0 : -1)
-  // 轮播图
-  if (data?.headImg?.length) {
-    swiperAdList.value = data.headImg
-  }
-  // 背景色
-  if (data?.backgroundColour) {
-    backgroundColor.value = data.backgroundColour || '#fff'
-  }
-  if (data?.title) {
-    jobFairTitle.value = data.title
-  }
-  showShareBtn.value = Boolean(data?.shareImg)
-}
-getJobFairDetail()
-
-// 切换类型
-const handClickTab = (index) => {
-  tabIndex.value = index
-  query.pageNo = 1
-  listData.value = []
-  getEnterpriseList()
-}
-
-const onSearch = () => {
-  query.pageNo = 1
-  listData.value = []
-  getEnterpriseList()
-}
-
-const getEnterpriseList = async () => {
-  if (!query.jobFairId) return
-  try {
-    const params = { ...query }
-    // tab对应的职位类型id列表
-    const idList = tabIndex.value !== -1 ? tabList.value[tabIndex.value]?.content : []
-    idList?.length && idList.forEach((value, index) => { params[`enterpriseId[${index}]`] = value.value })
-
-    const res = await getJobFairEnterprisePage(params)
-    const list = res?.data?.list || []
-    listData.value = listData.value.concat(dealDictArrayData([], list))
-    loading.value = false
-    if (listData.value?.length === +res?.data?.total) {
-      more.value = 'noMore'
-      return
-    }
-  } catch (error) {
-    query.pageNo--
-    more.value = 'more'
-  }
-}
-
-const scrollTop = ref(0)
-const old = ref({
-  scrollTop: 0
-})
-const onScroll = (e) =>{
-  old.value.scrollTop = e.detail.scrollTop
-}
-
-// 加载更多
-const loadingMore = () => {
-  more.value = 'loading'
-  query.pageNo++
-  getEnterpriseList()
-}
-
-const toDetail = (item) =>{
-  if (!item?.id || !(item?.jobFairId ?? query.jobFairId)) return
-  let url = `/pagesB/jobFair/positionClassification?jobFairId=${query.jobFairId || item.jobFairId}&enterpriseId=${item.id}&entName=${item.anotherName}`
-  url = url + `&backgroundColor=${backgroundColor.value}`
-  uni.navigateTo({ url })
-}
-// const goBack = () => {
-// 	uni.navigateTo({
-// 		url: '/pagesB/jobFair/index'
-// 	})
-// }
-
-// 分享招聘会
-const handleShare = () => {
-	uni.navigateTo({
-    url: `/pagesB/jobFair/jobFairShare?jobFairId=${query.jobFairId}`
-	})
-}
-</script>
-
-<style scoped lang="scss">
-.stick {
-  z-index: 1;
-  position: sticky;
-  top: 0;
-}
-.shareButtonBox {
-  z-index: 1;
-  position: absolute;
-  right: 20px;
-  top: 16px;
-  border-radius: 8px;
-  padding: 7px 9px;
-  background-color: #fff;
-}
-.box {
-  height: 100vh;
-  overflow: hidden;
-  // padding-bottom: 120rpx;
-  box-sizing: border-box;
-  display: flex;
-  flex-direction: column;
-}
-.listDataBox {
-  // padding: 1px 0 120rpx;
-  padding-bottom: 120rpx;
-  margin: 0 5rpx;
-  margin-top: -10px;
-  .enterpriseName {
-    color: #404040;
-    font-weight: 700;
-  }
-  .enterAvatar {
-    width: 60px;
-    height: 60px;
-    // border-radius: 50%;
-    margin: auto;
-  }
-  .jobCount {
-    height: 40px;
-    line-height: 40px;
-    color: #00B760;
-    text-align: center;
-    // padding: 0 50rpx;
-    font-size: 15px;
-    // background: linear-gradient(to right, #12ebb0, #7ec04c);
-    background: #F5F5F5;
-  }
-}
-.scrollBox{
-  flex: 1;
-  height: 0 !important;
-  padding-bottom: 24rpx;
-  box-sizing: border-box;
-}
-
-// :deep(.uni-load-more__text) {
-//   color: #fff !important;
-// }
-
-:deep(.uni-card) {
-  padding: 0 !important;
-  .uni-card__content {
-    padding: 0 !important;
-  }
-}
-
-.scroll-container {
-  width: calc(100vw - 20px);
-  padding: 0 20rpx;
-  white-space: nowrap; /* 确保子元素在一行内排列 */
-  .scroll-item {
-    display: inline-block; /* 子元素内联块显示 */
-    color: #fff;
-    font-size: 17px;
-    font-weight: 500;
-    .text {
-      padding: 20px 2px 0;
-    }
-    .choose {
-      width: 28px;
-      height: 2px;
-      border-radius: 8px;
-      margin: 8px auto;
-    }
-  }
-}
-
-:deep(.uni-searchbar) {
-  // background-color: #fff !important;
-  padding: 0;
-  margin: 0 10px;
-  border-radius: 5px;
-}
-
-:deep(.uni-searchbar__box) {
-  width: calc(100% - 100px);
-  height: 40px !important;
-  // border: 1px solid #00B760;
-  padding-right: 20px;
-  flex: none;
-  background-color: #fff;
-}
-
-.search-btn {
-  position: absolute;
-  right: 13px;
-  top: 3px;
-  width: 106px;
-  height: 34px;
-  line-height: 34px;
-  font-size: 16px;
-  // margin: 4px 4px 4px 0;
-  background-color: #00B760;
-  color: #fff;
-  border-radius: 5px;
-  z-index: 9;
-}
-</style>

+ 0 - 62
pagesB/jobFair/index.vue

@@ -1,62 +0,0 @@
-<!-- 招聘会 -->
-<template>
-  <view style="padding-bottom: 30px; ">
-    <view v-if="items.length">
-      <uni-card v-for="val in items" :key="val.id" @tap="handleToJobFairEnterprises(val)">
-        <image v-if="val?.previewImg" class="ss-m-t-10" :src="val.previewImg" mode="widthFix" style="width: 100%; height: auto; border-radius: 6px;"></image>
-        <view class="ss-m-t-20">活动时间:{{ timesTampChange(val.startTime, 'Y-M-D') }}至{{ timesTampChange(val.endTime, 'Y-M-D') }}</view>
-        <button class="ss-m-t-20 ss-m-b-10" style="background-color: #00B760; color: #fff;" type="primary">查看详情</button>
-      </uni-card>
-    </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>
-  </view>
-</template>
-<script setup>
-import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
-import { ref }  from 'vue'
-import { getJobFairList } from '@/api/jobFair'
-import { timesTampChange } from '@/utils/date'
-
-const items = ref([])
-
-// 获得招聘会列表
-const getList = async () => {
-  const res = await getJobFairList()
-  items.value = res?.data || []
-}
-getList()
-
-//招聘会
-const handleToJobFairEnterprises = (val) => {
-  if (!val?.id) {
-    uni.showToast({ title: '进去招聘会失败!', icon: 'none' })
-  }
-  let url = `/pagesB/jobFair/${Number(val?.category) ? 'positionClassification': 'enterprisesClassification'}?jobFairId=${val.id}`
-	uni.navigateTo({url})
-}
-
-
-const getShareParams = () => {
-  return {
-    title: '门墩儿-招聘会',
-    path: '/pagesB/jobFair/index',
-  }
-}
-
-// 转发朋友
-onShareAppMessage(() => {
-  return getShareParams()
-})
-// 转发朋圈
-onShareTimeline(() => {
-  return getShareParams()
-})
-
-</script>
-<style lang="scss" scoped>
-.line {
-  border-top: 1px solid #ccc;
-}
-</style>

+ 9 - 20
pagesB/jobFair/jobFairEntShare.vue

@@ -1,4 +1,3 @@
-<!-- 招聘会-分享企业 -->
 <template>
   <view style="position: relative;">
     <view v-if="shareUrl" class="d-flex align-center flex-column justify-center" style="height: 100vh;">
@@ -13,24 +12,24 @@
 import { ref, computed } from 'vue'
 import { onLoad } from '@dcloudio/uni-app'
 import { getJobAdvertisedShareQrcode } from '@/api/user'
-import { getJobFairEntJobPage, getJobFair, saveShareQuery } from '@/api/jobFair'
+import { getJobFairPosition, getJobFair, saveShareQuery } from '@/api/jobFair'
 import { formatName } from '@/utils/getText'
+import { userStore } from '@/store/user'
 
+const useUserStore = userStore()
 const shareUrl = ref('')
 const windowWidth = ref(0)
 let jobFairId = '' // 分享会ID
-let enterpriseId = '' // 企业ID
-let entName = '' // 企业名称
+let entLogoUrl = useUserStore?.userInfo?.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'
+let entName = formatName(useUserStore?.userInfo?.enterpriseAnotherName || useUserStore?.userInfo?.enterpriseName) || '门墩儿' // 企业名称
 let shareImg = '' // 分享背景图片-底图
 
 onLoad(async (options) => {
   jobFairId = options.jobFairId
-  enterpriseId = options.enterpriseId
   await getJobFairDetail()
   await getEntPositionList()
   const windowInfo = wx.getWindowInfo()
   windowWidth.value = windowInfo.windowWidth
-  console.log(windowWidth.value, '当前机型屏幕宽')
   if (shareImg) createPoster() // 生成海报
 })
 
@@ -41,24 +40,14 @@ const getJobFairDetail = async () => {
 }
 
 let positionNameList = []
-let entLogoUrl = 'https://minio.citupro.com/dev/menduner/company-avatar.png'
 const getEntPositionList = async () => {
-  if (!jobFairId || !enterpriseId) {
+  if (!jobFairId) {
     uni.showToast({ title: '获取企业岗位失败,请重试!', icon: 'none', duration: 2000 })
     return
   }
   try {
-    const params = {
-      pageSize: 3, 
-      pageNo: 1,
-      jobFairId,
-      enterpriseId,
-    }
-    const res = await getJobFairEntJobPage(params)
-    const list = res?.data?.list?.length ? res.data.list : []
-    positionNameList = list.map(e => { return formatName(e?.name) })
-    entLogoUrl = list[0]?.enterprise?.logoUrl || ''
-    entName = list[0]?.enterprise?.anotherName || ''
+    const { data } = await getJobFairPosition(jobFairId)
+    positionNameList = data.map(e => { return formatName(e?.name) }).slice(0, 3)
   } catch (error) {}
 }
 
@@ -116,7 +105,7 @@ const getToLocal = (base64data) => {
 // 生成分享二维码
 const qrCode = ref()
 const handleShareCode = async () => {
-  const result = await saveShareQuery({ jobFairId, enterpriseId, entName })
+  const result = await saveShareQuery({ jobFairId, entName, enterpriseId: useUserStore?.userInfo?.enterpriseId })
 	const query = {
 		scene: 'id=' + result.data,
     path: 'pagesB/jobFair/positionClassification',

+ 0 - 191
pagesB/jobFair/jobFairShare.vue

@@ -1,191 +0,0 @@
-<!-- 分享招聘会 -->
-<template>
-  <view style="position: relative;">
-    <view v-if="shareUrl" class="d-flex align-center flex-column justify-center" style="height: 100vh;">
-      <image v-if="!!shareUrl" :style="imgStyle" @click="handlePreviewSharePoster" :src="shareUrl" :show-menu-by-longpress="true"></image>
-      <view class="color-999 ss-m-t-20 font-size-14 ss-m-b-50">点击图片预览,长按图片保存</view>
-    </view>
-		<canvas canvas-id="posterCanvas" class="shareCanvas" :style="`width: ${canvasWidth}px; height: ${canvasHeight}px;`"></canvas>
-  </view>
-</template>
-
-<script setup>
-import { ref, computed } from 'vue'
-import { onLoad } from '@dcloudio/uni-app'
-import { getJobAdvertisedShareQrcode } from '@/api/user'
-import { getJobFair, saveShareQuery } from '@/api/jobFair'
-
-const canvasWidth = 500
-const canvasHeight = 900
-
-const shareUrl = ref('')
-const windowWidth = ref(0)
-let jobFairId = '' // 分享会ID
-let isEnt = false // 招聘会类型
-let shareImg = '' // 分享背景图片-底图
-// let shareImg = 'https://menduner.citupro.com:3443/dev/4dd7dd3c9ef350d62fcbc814b88c1ac1916f484a3ee8d8531ae7a2698a289670.jpg' // 分享背景图片-底图
-
-onLoad(async (options) => {
-  jobFairId = options.jobFairId
-  await getJobFairDetail()
-  const windowInfo = wx.getWindowInfo()
-  windowWidth.value = windowInfo.windowWidth
-  console.log(windowWidth.value, '当前机型屏幕宽')
-})
-
-const getJobFairDetail = async () => {
-  if (!jobFairId) return
-  const { data } = await getJobFair(jobFairId)
-  shareImg = data?.shareImg || ''
-  isEnt = Number(data?.category)
-  if (shareImg) createPoster()
-}
-
-const imgStyle = computed(() => {
-  if (windowWidth.value <= 320) return `width: 259px; height: ${Math.round((canvasHeight*259)/canvasWidth)}px;`
-  if (windowWidth.value > 320 && windowWidth.value <= 375) return `width: 313px; height: ${Math.round((canvasHeight*313)/canvasWidth)}px;`
-  if (windowWidth.value > 375) return `width: 328px; height: ${Math.round((canvasHeight*328)/canvasWidth)}px;`
-})
-
-// 图片预览
-const handlePreviewSharePoster = () => {
-	uni.previewImage({
-		current: 0,
-		longPressActions: {
-			itemList: ['发送给朋友', '保存图片', '收藏']
-		},
-		urls: [shareUrl.value]
-	})
-}
-
-const getImageTempRatio = (url) => {
-  return new Promise((req, rej)=>{
-    wx.getImageInfo({
-      src:url,
-      success:(res) =>{
-        req(res)
-      }
-    })
-  })
-}
-
-// const getToLocal = (base64data) => {
-//   const fsm = wx.getFileSystemManager()
-//   const FILE_BASE_NAME = 'tmp_base64src'; //自定义文件名
-//   const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64data) || []
-//   if (!format) {
-//     return (new Error('ERROR_BASE64SRC_PARSE'))
-//   }
-//   const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}.${format}`
-//   const buffer = wx.base64ToArrayBuffer(bodyData);
-//   fsm.writeFile({
-//     filePath,
-//     data: buffer,
-//     encoding: 'binary',
-//     success(r) {
-//       console.log(filePath,'filePath')
-//       qrCode.value = filePath
-//     },
-//     fail() {
-//       return (new Error('ERROR_BASE64SRC_WRITE'))
-//     }
-//   })
-// }
-const getToLocal = (base64data) => {
-  return new Promise((resolve, reject) => {
-    const fsm = wx.getFileSystemManager()
-    const FILE_BASE_NAME = `tmp_${new Date().getTime()}` // 自定义文件名
-    const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64data) || []
-    if (!format) {
-      reject(new Error('ERROR_BASE64SRC_PARSE'))
-      return
-    }
-    const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}.${format}`
-    const buffer = wx.base64ToArrayBuffer(bodyData)
-    fsm.writeFile({
-      filePath,
-      data: buffer,
-      encoding: 'binary',
-      success(r) {
-        resolve(filePath) // 成功时返回 filePath
-      },
-      fail() {
-        reject(new Error('ERROR_BASE64SRC_WRITE'))
-      },
-    })
-  })
-}
-
-// 生成分享二维码
-const qrCode = ref()
-const handleShareCode = async () => {
-  const result = await saveShareQuery({ jobFairId })
-	const query = {
-    scene: 'id=' + result.data,
-    path: `pagesB/jobFair/${!isEnt ? 'enterprisesClassification' : 'positionClassification'}`,
-		width: 200,
-		autoColor: false,
-		checkPath: true,
-		hyaline: false // 二维码背景颜色为透明
-	}
-  console.log(result.data, query.scene, '=========================')
-	const res = await getJobAdvertisedShareQrcode(query, { noAuth: true })
-	const data = res?.data ? 'data:image/png;base64,' + res.data : ''
-  const filePath = await getToLocal(data) // 等待 getToLocal 完成
-  qrCode.value = filePath
-}
-
-const createPoster = async () => {
-  uni.showLoading({ title: '生成中...', mask: true })
-  await handleShareCode() // 生产分享二维码
-
-  var context = uni.createCanvasContext('posterCanvas')
-
-  //清空画布
-  context.clearRect(0, 0, canvasWidth, canvasHeight)
-
-  //背景图片 
-  const { path: bgUrl } = await getImageTempRatio(shareImg)
-  context.drawImage(bgUrl, 0, 0, canvasWidth, canvasHeight) // 路径、x、y、宽、高
-  
-  //分享二维码
-  const qrCodeWidth = 127
-  const qrCodeHeight = 127
-  const qrCodeX = 188
-  const qrCodeY = 585
-  console.log('dddd', qrCode.value)
-  context.drawImage(qrCode.value, qrCodeX, qrCodeY, qrCodeWidth, qrCodeHeight)
-  
-  context.font = 'bold 16px Arial'
-  context.fillStyle = '#10325d'
-  const text = '诚挚邀约 共享机遇'
-  const textWidth = context.measureText(text).width
-  const textX = qrCodeX + (qrCodeWidth - textWidth) / 2
-  const textY = qrCodeY + qrCodeHeight + 30
-  context.fillText(text, textX, textY)
-  
-  context.draw(false, () =>{
-    wx.canvasToTempFilePath({ 
-      canvasId: 'posterCanvas',
-      success:(res)=>{
-        shareUrl.value = res.tempFilePath
-        console.log('canvas-success', shareUrl.value)
-        uni.hideLoading({})
-      },
-      fail:(err)=>{
-        uni.hideLoading({})
-        console.log('canvas-fail', err)
-      }
-    })
-  })
-}
-</script>
-
-<style scoped lang="scss">
-.shareCanvas {
-	position: fixed;
-	top: -99999upx;
-	left: -99999upx;
-	z-index: -99999;
-}
-</style>

+ 162 - 0
pagesB/jobFair/jobItem.vue

@@ -0,0 +1,162 @@
+<template>
+  <view>
+    <view v-if="list?.length" class="ss-p-b-30 ss-p-t-20">
+			<uni-card v-for="(item, index) in list" :key="index" class="mList" :is-shadow="true" :border='false' shadow="0px 0px 3px 1px rgba(0,0,0,0.1)">
+        <view class="list-shape" style="border-radius: 12px;">
+          <view @tap.stop="handleToDetail(item)">
+						<view class="titleBox">
+							<view style="display: flex;align-items: center;">
+								<image src="/static/svg/jobFair.svg" class=" ss-m-r-10" style="width: 20px; height: 20px;"></image>
+								<rich-text v-if="item.name?.indexOf('style') !== -1" class="job-name" :nodes="item.name"></rich-text>
+								<view v-else class="job-name">{{ formatName(item.name) }}</view>
+							</view>
+						</view>
+						<view class="d-flex align-center justify-space-between ss-m-t-20">
+							<view class="font-size-13 ellipsis" style="flex: 1;">
+								<span class="tag-gap" style="color: #808080;">
+									<span>{{item.area?.str ?? '全国' }}</span>
+									<span class="divider-mx" v-if="item.eduName">|</span>
+									<span>{{item.eduName }}</span>
+									<span class="divider-mx" v-if="item.expName">|</span>
+									<span>{{item.expName }}</span>
+									<span class="divider-mx">|</span>
+									<span>{{!item.payFrom && !item.payTo ? '面议' : `${item.payFrom}-${item.payTo}${item.payName ? '/' + item.payName : ''}` }}</span>
+								</span>
+							</view>
+						</view>
+					</view>
+          <view class="sub-li-bottom ss-m-t-20">
+						<view class="sub-li-bottom-item color-primary" @tap.stop="handleToResume(item)">{{ item.count || 0 }} 已投递简历</view>
+						<view class="sub-li-bottom-item color-error" @tap.stop="handleRemove(item)">移出招聘会</view>
+          </view>
+        </view>
+			</uni-card>
+		</view>
+
+		<uni-popup ref="removePopup" type="dialog">
+			<uni-popup-dialog 
+				type="warn" 
+				cancelText="取消" 
+				confirmText="确定" 
+				title="系统提示" 
+				content="是否确认将此职位移出招聘会?" 
+				@confirm="handleRemoveConfirm"
+				@close="handleRemoveClose"
+			></uni-popup-dialog>
+		</uni-popup>
+  </view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { formatName } from '@/utils/getText'
+import { quitJobFairPosition } from '@/api/jobFair'
+
+const emit = defineEmits(['refresh'])
+const props = defineProps({
+	list: Array,
+	jobFairId: [String, Number]
+})
+
+// 查看职位详情
+const handleToDetail = (val) => {
+	uni.navigateTo({
+		url: `/pagesB/positionDetail/index?jobId=${val.id}&jobFairId=${props.jobFairId}&isEdit=${val.edit}`
+	})
+}
+
+
+// 查看职位投递简历
+const handleToResume = (val) => {
+	console.log(val, '投递简历数')
+}
+
+// 移出招聘会
+const removePopup = ref()
+const removeParams = ref({})
+const handleRemove = (val) => {
+	removeParams.value = val
+	removePopup.value.open()
+}
+const handleRemoveClose = () => {
+	removeParams.value = {}
+  removePopup.value.close()
+}
+const handleRemoveConfirm = async () => {
+	if (!removeParams.value.id || !props.jobFairId) {
+		uni.showToast({ title: '正在维护中,请稍后再试', icon: 'none', duration: 2000 })
+		return
+	}
+	uni.showLoading({ title: '加载中' })
+	try {
+		await quitJobFairPosition({ jobFairId: props.jobFairId, jobId: removeParams.value.id })
+		uni.hideLoading()
+		uni.showToast({ title: '移出成功', icon: 'none' })
+		removeParams.value = {}
+		emit('refresh')
+	} catch {
+		removeParams.value = {}
+		uni.hideLoading()
+	}
+}
+</script>
+
+<style scoped lang="scss">
+.job-name {
+  font-size: 16px;
+  font-weight: 700;
+  color: #0E100F;
+  max-width: 80vw;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.sub-li-bottom {
+  display: flex;
+  justify-content: space-between;
+  margin-top: 10px;
+  font-size: 13px;
+  color: #666;
+	&-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;
+		}
+	}
+}
+
+.salary-text {
+	float: right;
+	color: #00B760;
+  font-weight: 700;
+}
+.list-shape {
+  background-color: #fff;
+  border-radius: 12px 12px 0 0;
+  .titleBox {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+}
+.tag-gap{
+	margin: 10rpx 10rpx 10rpx 0;
+}
+.divider-mx{
+	margin: 0 10rpx;
+}
+.divider {
+	color:#e4d4d2;
+}
+.mList {
+  margin-bottom: 20rpx;
+	padding: 10px 0;
+}
+</style>

+ 203 - 0
pagesB/jobFair/join.vue

@@ -0,0 +1,203 @@
+<template>
+	<view class="box defaultBgc">
+		<view style="background-color: #fff;">
+			<uni-search-bar
+        v-model="query.name"
+        radius="5"
+        placeholder="输入关键字"
+        cancelButton="none"
+        :focus="false"
+				@clear="handleSearch"
+        @confirm="handleSearch"
+      ></uni-search-bar>
+		</view>
+		
+		<scroll-view class="scrollBox" :scroll-y="true" @scrolltolower="loadingMore" style="position:relative;">
+			<uni-card  v-for="(item, index) in items" :key="index" class="mList" :is-shadow="true" :border='false' shadow="0px 0px 3px 1px rgba(0,0,0,0.1)">
+        <view class="list-shape" style="border-radius: 12px;">
+          <view>
+						<view class="titleBox">
+							<view style="display: flex;align-items: center;">
+								<rich-text v-if="item.name?.indexOf('style') !== -1" class="job-name" :nodes="item.name"></rich-text>
+								<view v-else class="job-name">{{ formatName(item.name) }}</view>
+							</view>
+						</view>
+						<view class="d-flex align-center justify-space-between ss-m-t-20">
+							<view class="font-size-13 ellipsis" style="flex: 1;">
+								<span class="tag-gap" style="color: #808080;">
+									<span>{{item.area?.str ?? '全国' }}</span>
+									<span class="divider-mx" v-if="item.eduName">|</span>
+									<span>{{item.eduName }}</span>
+									<span class="divider-mx" v-if="item.expName">|</span>
+									<span>{{item.expName }}</span>
+									<span class="divider-mx">|</span>
+									<span>{{!item.payFrom && !item.payTo ? '面议' : `${item.payFrom}-${item.payTo}${item.payName ? '/' + item.payName : ''}` }}</span>
+								</span>
+							</view>
+						</view>
+					</view>
+          <view class="sub-li-bottom">
+						<view class="sub-li-bottom-item" @tap.stop="handleJoin(item)">添加至招聘会</view>
+          </view>
+        </view>
+      </uni-card>
+			<uni-load-more :status="more" />
+		</scroll-view>
+	</view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { formatName } from '@/utils/getText.js'
+import { joinJobFairPosition, getJobFairPositionList } from '@/api/jobFair.js'
+import { dealDictArrayData } from '@/utils/position'
+
+const query = ref({
+	pageSize: 10,
+	pageNo: 1,
+	name: null,
+	jobFairId: null
+})
+const items = ref([])
+const more = ref('more')
+const total = ref(0)
+
+const getData = async () => {
+  try {
+		more.value = 'loading'
+		const { data } = await getJobFairPositionList(query.value)
+
+		if (!data.list.length) {
+			more.value = 'noMore'
+			items.value = []
+			total.value = 0
+			return
+		}
+
+		const list = dealDictArrayData([], data.list)
+		items.value = items.value.concat(list)
+		total.value = data.total
+
+		if (items.value.length === +data.total) {
+      more.value = 'noMore'
+      return
+    }
+	} catch {
+		query.value.pageNo--
+		more.value = 'more'
+	}
+}
+
+// 关键字搜索
+const handleSearch = () => {
+	total.value = 0
+	items.value = []
+	query.value.pageNo = 1
+	getData()
+}
+
+// 加载更多
+const loadingMore = () => {
+	if (more.value === 'noMore') return
+
+  more.value = 'loading'
+  query.value.pageNo++
+	getData()
+}
+
+onLoad(async (options) => {
+	query.value.jobFairId = options.jobFairId
+
+	query.value.pageNo = 1
+	await getData()
+})
+
+// 加入职位
+const handleJoin = async (item) => {
+	uni.showLoading({ title: '加载中' })
+	try {
+		await joinJobFairPosition({ jobId: item.id, jobFairId: query.value.jobFairId })
+		uni.hideLoading()
+		uni.showToast({ title: '加入成功', icon: 'success' })
+
+		setTimeout(() => {
+			uni.navigateBack({ delta: 1 })
+		}, 1000)
+	} catch (e) {
+		uni.hideLoading()
+		uni.showToast({ title: e.message })
+	}
+}
+</script>
+
+<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;
+}
+.stickFilter {
+  z-index: 1;
+  position: sticky;
+  box-shadow: 0px 10rpx 12rpx 0px rgba(195, 195, 195, .25);
+  top: 110rpx;
+	background-color: #fff;
+}
+.job-name {
+  font-size: 16px;
+  font-weight: 700;
+  color: #0E100F;
+  max-width: 80vw;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.sub-li-bottom {
+  display: flex;
+  justify-content: space-between;
+  margin-top: 10px;
+  padding-top: 10px;
+  font-size: 13px;
+  color: #00B760;
+	&-item {
+		width: 100%;
+		height: 35px;
+		line-height: 35px;
+		text-align: center;
+		background-color: #f7f8fa;
+		border-radius: 4px;
+	}
+}
+
+.list-shape {
+  background-color: #fff;
+  border-radius: 12px 12px 0 0;
+  .titleBox {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+}
+.tag-gap{
+	margin: 10rpx 10rpx 10rpx 0;
+}
+.divider-mx{
+	margin: 0 10rpx;
+}
+.divider {
+	color:#e4d4d2;
+}
+.mList {
+  margin-bottom: 20rpx;
+}
+</style>

+ 0 - 390
pagesB/jobFair/positionClassification.vue

@@ -1,390 +0,0 @@
-<!-- 招聘会职位列表 / 招聘会内的企业下-职位列表 -->
-<!-- 没有企业ID的就是招聘会下的职位列表。存在企业ID的就是招聘会企业下的职位列表。 -->
-<template>
-  <view class="box" :style="`background-color: ${backgroundColor}`">
-    <scroll-view class="scrollBox" :scroll-y="true" :scroll-top="scrollTop" @scrolltolower="loadingMore" @scroll="onScroll" style="position:relative;">
-      <view style="position: relative;">
-
-        <!-- 招聘会企业下的职位列表 -->
-        <template v-if="query?.enterpriseId">
-          <view class="enterpriseName" :style="`color: ${entNameColor}`">{{ entName }}</view>
-          <view class="stick ss-p-y-30" :style="`background-color: ${backgroundColor}`">
-            <!-- 搜索条 -->
-            <view style="position: relative;">
-              <uni-search-bar
-                v-model="query.keyword"
-                placeholder="输入关键字"
-                cancelButton="none"
-                :focus="false"
-                bgColor="#fff"
-                @confirm="onSearch($event.value)"
-                @clear="query.keyword = ''; onSearch()"
-              >
-              </uni-search-bar>
-              <button class="search-btn" @tap.stop="onSearch">搜索</button>
-            </view>
-          </view>
-        </template>
-
-        <!-- 招聘会职位列表 -->
-        <template v-else>
-          <SwiperAd v-if="swiperAdList.length && !query.enterpriseId" :list="swiperAdList" margin="0" borderRadius="0"></SwiperAd>
-          <view class="stick ss-p-y-30" :style="`background-color: ${backgroundColor}`">
-            <view style="position: relative;">
-              <uni-search-bar
-                v-model="query.keyword"
-                placeholder="输入关键字"
-                cancelButton="none"
-                :focus="false"
-                bgColor="#fff"
-                @confirm="onSearch($event.value)"
-                @clear="query.keyword = ''; onSearch()"
-              >
-              </uni-search-bar>
-              <button class="search-btn" @tap.stop="onSearch">搜索</button>
-            </view>
-            
-            <!-- 横向分类页签 -->
-            <scroll-view v-if="tabList?.length && !query.enterpriseId" scroll-x="true" class="scroll-container">
-              <view
-                class="scroll-item"
-                :style="`margin-left: ${index ? '24px' : ''};`"
-                v-for="(val, index) in tabList" :key="index+val"
-                @tap="handClickTab(index)"
-              >
-                <view>
-                  <view class="text">{{ val.title }}</view>
-                  <view v-if="index === tabIndex" class="choose" style="background-color: #fff;"></view>
-                  <view v-else class="choose" style="background-color: #ffffff00;"></view>
-                </view>
-              </view>
-            </scroll-view>
-          </view>
-        </template>
-        <view v-if="listData?.length" class="listDataBox">
-          <PositionList
-            :list="listData"
-            :noMore="false"
-            :jobFairId="query.jobFairId"
-            :showUpdateTime="false"
-            :noDataTextColor="textColor"
-            @entClick="entClick"
-          ></PositionList>
-          <uni-load-more :status="more" :color="textColor" />
-        </view>
-        <view v-else class="nodata-img-parent">
-          <uni-load-more class="ss-m-t-50" :color="textColor" status="noMore" :content-text="{'contentnomore': loading ? '加载中. . .' : tabList?.length ? '暂无数据,请切换类型查看~': '暂无数据'}" />
-        </view>
-        <!-- 招聘会分享按钮 -->
-        <view v-if="showShareBtn" class="shareButtonBox" @tap="handleShare">
-          <uni-icons type="redo-filled" size="30" color="#00B760" />
-        </view>
-      </view>
-    </scroll-view>
-  </view>
-</template>
-
-<script setup>
-import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
-import { ref, reactive, computed } from 'vue'
-import { dealDictObjData } from '@/utils/position'
-import { getJobFairEntJobPage, getJobFair } from '@/api/jobFair'
-import PositionList from '@/components/PositionList'
-import SwiperAd from '@/components/SwiperAd'
-import { getShareQueryById } from '@/api/jobFair.js'
-
-const loading = ref(true)
-const more = ref('more')
-const listData = ref([])
-const query = reactive({
-  pageSize: 20, 
-  pageNo: 1,
-  keyword: '',
-  jobFairId: undefined,
-})
-
-const entName = ref('')
-const showShareBtn = ref(false)
-
-onLoad(async (options) => {
-  // 海报二维码分享
-	if (options.scene) {
-    const scene = decodeURIComponent(options.scene)
-    console.log(scene, 'scene')
-    const key = scene.split('=')[1]
-    if (!key) return
-    const res = await getShareQueryById({key})
-    console.log(res, 'result')
-    options.jobFairId = res?.data?.jobFairId
-    options.enterpriseId = res?.data?.enterpriseId
-    options.backgroundColor = res?.data?.backgroundColor
-    options.entName = res?.data?.entName
-  }
-  if (options?.jobFairId) {
-    query.jobFairId = options.jobFairId
-    if (options.enterpriseId) {
-      query.enterpriseId = options.enterpriseId
-      entName.value = options.entName
-      getEntShareImg()
-      getEntPositionList()
-    } else {
-      getJobFairDetail()
-    }
-  }
-  if (options.backgroundColor) backgroundColor.value = options.backgroundColor
-  
-  // 转发朋友
-  onShareAppMessage(() => {
-    if (!options?.enterpriseId && !jobFairTitle.value) setTimeout(() => {}, 1000)
-    let path = `/pagesB/jobFair/positionClassification?jobFairId=${options.jobFairId}`
-    if (options?.enterpriseId) path += `&enterpriseId=${options.enterpriseId}&entName=${options.entName}`
-    return {
-      title: options?.enterpriseId ? options?.entName || '门墩儿 专注顶尖招聘' : jobFairTitle.value,
-      path
-    }
-  })
-  // 转发朋圈
-  onShareTimeline(() => {
-    if (!query.enterpriseId && !jobFairTitle.value) setTimeout(() => {}, 1000)
-    let path = `/pagesB/jobFair/positionClassification?jobFairId=${options.jobFairId}`
-    if (options?.enterpriseId) path += `&enterpriseId=${options.enterpriseId}&entName=${options.entName}`
-    return {
-      title: query.enterpriseId ? entName.value || '门墩儿 专注顶尖招聘' : jobFairTitle.value,
-      path
-    }
-  })
-})
-
-// 招聘会详情
-const tabIndex = ref(-1)
-const tabList = ref([])
-const swiperAdList = ref([])
-const backgroundColor = ref('#fff')
-const jobFairTitle = ref('')
-const textColor = computed(() => {
-  return backgroundColor.value === '#fff' ? '#777' : '#fff'
-})
-const entNameColor = computed(() => {
-  return backgroundColor.value === '#fff' ? '#00B760' : '#fff'
-})
-const getJobFairDetail = async () => {
-  if (!query.jobFairId) return
-  const { data } = await getJobFair(query.jobFairId)
-  // 类型 -1为不传tag参数
-  tabList.value = data?.tag || []
-  handClickTab(tabList.value?.length ? 0 : -1)
-
-  // 轮播图
-  if (data?.headImg?.length) {
-    swiperAdList.value = data.headImg
-  }
-  // 背景色
-  if (data?.backgroundColour) {
-    backgroundColor.value = data.backgroundColour || '#fff'
-  }
-  if (data?.title) {
-    jobFairTitle.value = data.title
-  }
-  showShareBtn.value = Boolean(data?.shareImg)
-}
-
-const getEntShareImg = async () => {
-  if (!query.jobFairId) return
-  const { data } = await getJobFair(query.jobFairId)
-  showShareBtn.value = Boolean(data?.contentImg)
-  backgroundColor.value = data.backgroundColour || '#fff'
-}
-
-// 切换类型
-const handClickTab = (index) => {
-  tabIndex.value = index
-  query.pageNo = 1
-  listData.value = []
-  getEntPositionList()
-}
-
-const onSearch = () => {
-  query.pageNo = 1
-  listData.value = []
-  getEntPositionList()
-}
-
-const getEntPositionList = async () => {
-  if (!query.jobFairId) {
-    uni.showToast({ title: '获取企业岗位失败,请重试!', icon: 'none', duration: 2000 })
-    return
-  }
-  try {
-    const params = { ...query }
-    // tab对应的职位类型id列表
-    const idList = tabIndex.value !== -1 ? tabList.value[tabIndex.value]?.content : []
-    idList?.length && idList.forEach((value, index) => { params[`positionId[${index}]`] = value.value })
-    //
-    const res = await getJobFairEntJobPage(params)
-    const list = res?.data?.list || []
-    list.forEach(e => {
-      e.job = dealDictObjData({}, e)
-      e.enterprise = { ...e.enterprise, ...dealDictObjData({}, e.enterprise)}
-    })
-    listData.value = listData.value.concat(list)
-    loading.value = false
-    if (listData.value?.length === +res?.data?.total) {
-      more.value = 'noMore'
-      return
-    }
-  } catch (error) {
-    query.pageNo--
-    more.value = 'more'
-  }
-}
-
-const scrollTop = ref(0)
-const old = ref({
-  scrollTop: 0
-})
-const onScroll = (e) =>{
-  old.value.scrollTop = e.detail.scrollTop
-}
-
-// 加载更多
-const loadingMore = () => {
-  more.value = 'loading'
-  query.pageNo++
-  getEntPositionList()
-}
-
-const entClick = (info) => {
-  // 点击企业信息跳转企业职位列表(招聘会内)。如果已经在了则不跳转
-  if (info?.enterpriseId && !query.enterpriseId) {
-    const url = `/pagesB/jobFair/positionClassification?jobFairId=${query.jobFairId }&enterpriseId=${info.enterpriseId}&entName=${info.anotherName}`
-    uni.navigateTo({ url })
-  }
-}
-
-const handleShare = () => {
-  // 分享招聘会
-  let url = `/pagesB/jobFair/${query.enterpriseId ? 'jobFairEntShare' : 'jobFairShare'}?jobFairId=${query.jobFairId}`
-  // 分享招聘会企业
-  if (query.enterpriseId) url = url + `&enterpriseId=${query.enterpriseId}`
-	uni.navigateTo({ url })
-}
-</script>
-
-<style scoped lang="scss">
-.stick {
-  z-index: 1;
-  position: sticky;
-  top: 0;
-}
-.shareButtonBox {
-  z-index: 1;
-  position: absolute;
-  right: 20px;
-  top: 16px;
-  border-radius: 8px;
-  padding: 7px 9px;
-  background-color: #fff;
-}
-.box {
-  height: 100vh;
-  overflow: hidden;
-  // padding-bottom: 120rpx;
-  box-sizing: border-box;
-  display: flex;
-  flex-direction: column;
-}
-.listDataBox {
-  // padding: 1px 0 120rpx;
-  padding-bottom: 120rpx;
-  margin: 0 5rpx;
-}
-.scrollBox{
-  flex: 1;
-  height: 0 !important;
-  padding-bottom: 24rpx;
-  box-sizing: border-box;
-}
-
-// :deep(.uni-load-more__text) {
-//   color: #fff !important;
-// }
-
-:deep(.uni-card) {
-  padding: 0 !important;
-  .uni-card__content {
-    padding: 0 !important;
-  }
-}
-
-.scroll-container {
-  width: calc(100vw - 20px);
-  padding: 0 20rpx;
-  white-space: nowrap; /* 确保子元素在一行内排列 */
-  .scroll-item {
-    display: inline-block; /* 子元素内联块显示 */
-    color: #fff;
-    font-size: 17px;
-    font-weight: 500;
-    .text {
-      padding: 20px 2px 0;
-    }
-    .choose {
-      width: 28px;
-      height: 2px;
-      border-radius: 8px;
-      margin: 8px auto;
-    }
-  }
-}
-
-.enterpriseName {
-  color: #fff;
-  font-size: 18px;
-  font-weight: bold;
-  line-height: 28px;
-  padding: 20px 80px 20px 30px;
-  position: relative;
-  &::before {
-    display: block;
-    content: '';
-    width: 6px;
-    height: 30px;
-    background-color: #fff;
-    position: absolute;
-    top: 20px;
-    left: 12px;
-    border-radius: 2px;
-  }
-}
-
-:deep(.uni-searchbar) {
-  // background-color: #fff !important;
-  padding: 0;
-  margin: 0 10px;
-  border-radius: 5px;
-}
-
-:deep(.uni-searchbar__box) {
-  width: calc(100% - 100px);
-  height: 40px !important;
-  // border: 1px solid #00B760;
-  padding-right: 20px;
-  flex: none;
-  background-color: #fff;
-}
-
-.search-btn {
-  position: absolute;
-  right: 13px;
-  top: 3px;
-  width: 106px;
-  height: 34px;
-  line-height: 34px;
-  font-size: 16px;
-  // margin: 4px 4px 4px 0;
-  background-color: #00B760;
-  color: #fff;
-  border-radius: 5px;
-  z-index: 9;
-}
-</style>

+ 24 - 10
pagesB/personnelDetails/components/baseInfo.vue

@@ -1,6 +1,10 @@
 <template>
 	<view>
 		<view class="d-flex">
+			<view class="user-avatar">
+				<image class="user-avatar-img" :src="getUserAvatar(data?.avatar, data?.sex)" alt="" mode="scaleToFill" />
+				<image class="user-avatar-sex" :src="data?.sex ? data?.sex === '1' ? '/static/img/man.png' : '/static/img/female.png' : ''" alt="" mode="scaleToFill" />
+			</view>
 			<view class="user-left">
 				<view class="user-name">{{ data?.name }}</view>
 				<view class="ss-m-t-10">
@@ -14,9 +18,6 @@
 					</span>
 				</view>
 			</view>
-			<view class="user-avatar">
-				<image :src="data?.avatar" alt="" mode="scaleToFill" />
-			</view>
 		</view>
 		<view class="ss-m-t-10">
       <uni-tag 
@@ -26,27 +27,40 @@
         :text="tag"
         inverted="false"
         size="medium"
-        custom-style="background-color: #ececec; color: #666; border-color: #ececec; display: inline-block;"
+        custom-style="background-color: #ececec; color: #666; border-color: #ececec; display: inline-block;margin-bottom: 10px;"
       />
     </view>
 	</view>
 </template>
 
 <script setup>
-const props = defineProps({ data: Object })
-console.log(props.data, 'props.data')
+defineProps({ data: Object })
+import { getUserAvatar } from '@/utils/avatar'
 
 const desc = ['jobStatusName', 'eduName', 'expName']
 </script>
 
 <style scoped lang="scss">
-.user-avatar image {
-	width: 65px;
-	height: 65px;
-	border-radius: 50%;
+.user-avatar {
+	position: relative;
+	&-img {
+		width: 65px;
+		height: 65px;
+		border-radius: 50%;
+	}
+	&-sex {
+		position: absolute;
+		right: 0;
+		bottom: 0;
+		width: 20px;
+		height: 20px;
+		background-color: #fff;
+		border-radius: 50%;
+	}
 }
 .user-left {
 	flex: 1;
+	margin-left: 10px;
 	.user-name {
 		font-size: 25px;
 		font-weight: 600;

+ 9 - 2
pagesB/personnelDetails/components/jobIntention.vue

@@ -2,12 +2,19 @@
 	<view>
 		<view v-for="val in data" :key="val.id" class="ss-m-b-40">
 			<view class="item">
-        <!-- <view class="item-title">{{ val.jobTypeName}}</view> -->
         <view style="display: flex;justify-content: space-between;align-items: center;">
           <text class="mr-20 item-title">{{ val.position}}</text>
           <text class="color-error font-weight-bold">{{ val.payFrom }} {{ val.payFrom  && val.payTo ? '-' : ''}} {{ val.payTo}}</text>
         </view>
-        <view>{{ val.interestedArea && val.interestedArea.length ? val.workArea + ',' + val.interestedArea.map(e => e.name).join(',') : val.workArea }}</view>
+        <view>
+          {{ 
+            val.interestedArea && val.interestedArea.length 
+            ? 
+            (val.workArea ? val.workArea + ',' : '') + val.interestedArea.map(e => e.name).join(',') 
+            : 
+            (val.workArea || '')
+          }}
+        </view>
         <view class="item-tags">
           <view v-for="industry in val.industry" :key="industry.id" class="tag">{{ industry.nameCn }}</view>
         </view>

+ 133 - 34
pagesB/personnelDetails/index.vue

@@ -1,11 +1,11 @@
 <template>
-	<view style="padding: 15px 15px 70px 15px; position: relative;">
+	<view style="padding: 15px 15px 70px 15px;">
 		<baseInfo v-if="cvData?.person" :data="cvData?.person" />
-		<view class="gap-line"></view>
 
-		<view>
+		<view v-if="skillExp?.length">
+			<view class="gap-line"></view>
 			<view class="title-line">职业技能</view>
-			<view class="tags" v-if="skillExp?.length">
+			<view class="tags">
         <view
           v-for="skill in skillExp"
           :key="skill.title"
@@ -14,46 +14,47 @@
           {{ skill.title }}
         </view>
       </view>
-			<view v-else class="color-999 text-center">暂无数据</view>
 		</view>
 
-		<view class="gap-line"></view>
-		<view>
+		<view v-if="cvData?.person?.advantage">
+			<view class="gap-line"></view>
 			<view class="title-line">个人优势</view>
-			<advantage v-if="cvData?.person?.advantage" :data="cvData?.person?.advantage" />
-			<view v-else class="color-999 text-center">暂无数据</view>
+			<advantage :data="cvData?.person?.advantage" />
 		</view>
 
-		<view class="gap-line"></view>
-		<view>
+		<view v-if="cvData?.interestedList?.length">
+			<view class="gap-line"></view>
 			<view class="title-line">求职意向</view>
-			<jobIntention v-if="cvData?.interestedList?.length" :data="dealJobData(cvData?.interestedList || [])" />
-			<view v-else class="color-999 text-center">暂无数据</view>
+			<jobIntention :data="dealJobData(cvData?.interestedList || [])" />
 		</view>
 
-		<view class="gap-line"></view>
-		<view>
+		<view v-if="cvData?.eduList?.length">
+			<view class="gap-line"></view>
 			<view class="title-line">教育经历</view>
-			<eduExp v-if="cvData?.eduList?.length" :data="cvData?.eduList" />
-			<view v-else class="color-999 text-center">暂无数据</view>
+			<eduExp :data="cvData?.eduList" />
 		</view>
 
-		<view class="gap-line"></view>
-		<view>
+		<view v-if="cvData?.workList?.length">
+			<view class="gap-line"></view>
 			<view class="title-line">工作经历</view>
-			<workExp v-if="cvData?.workList?.length" :data="cvData?.workList" />
-			<view v-else class="color-999 text-center">暂无数据</view>
+			<workExp :data="cvData?.workList" />
 		</view>
 
-		<view class="gap-line"></view>
-		<view>
+		<view v-if="cvData?.trainList?.length">
+			<view class="gap-line"></view>
 			<view class="title-line">培训经历</view>
-			<trainingExperience v-if="cvData?.trainList?.length" :data="cvData?.trainList" />
-			<view v-else class="color-999 text-center">暂无数据</view>
+			<trainingExperience :data="cvData?.trainList" />
 		</view>
 
-		<view class="bottom-actions">
-			操作按钮
+		<view class="bottom-sticky">
+			<view class="bottom-content">
+        <!-- <button class="bottom-content-tool shareButtonCss" open-type="share">
+          <uni-icons type="redo-filled" size="24" color="#00B760" style="line-height: 24px;"/>
+          <span style="color:#00B760;font-weight:bold;line-height: 22px;">分享</span>
+        </button> -->
+				<button class="btnStyle bgButtons ss-m-l-15" type="primary" plain="true" @tap="handleSend">立即沟通</button>
+        <button class="buttons btnStyle" type="primary" @click="handleInvite">邀请面试</button>
+      </view>
 		</view>
 	</view>
 </template>
@@ -72,6 +73,10 @@ import jobIntention from './components/jobIntention.vue'
 import workExp from './components/workExp.vue'
 import eduExp from './components/eduExp.vue'
 import trainingExperience from './components/trainingExperience.vue'
+import { getJobAdvertisedList } from '@/api/new/position'
+import { defaultText, talkToUser } from '@/hooks/useIM'
+import { getAccessToken } from '@/utils/request'
+import { showAuthModal } from '@/hooks/useModal'
 
 const cvData = ref({})
 
@@ -115,8 +120,17 @@ const getDetail = async (id) => {
 	}
 }
 
+// 职位列表
+const jobNum = ref(0)
+const getJobList = async () => {
+  const { data } = await getJobAdvertisedList({ pageNo: 1, pageSize: 10, hasExpiredData: false, status: 0 })
+  jobNum.value = data?.total || 0
+}
+
+
+const btnType = ref('') // 1: 人才详情 2:简历投递
 onLoad(async (options) => {
-	const { id } = options
+	const { id, type } = options
 	if (!id) {
 		uni.showToast({
 			title: '缺少人员id',
@@ -127,8 +141,70 @@ onLoad(async (options) => {
 		}, 1000)
 		return
 	}
+	btnType.value = type
 	await getDetail(id)
+
+	getJobList()
 })
+
+// 邀请面试
+const handleInvite = async () => {
+	if (!getAccessToken()) {
+    showAuthModal()
+    return
+  }
+  // 企业必须有招聘中的职位才能发起面试邀请
+  if (jobNum.value === 0) {
+		uni.showToast({
+			title: '请先发布招聘职位',
+			icon: 'none',
+			duration: 2000
+		})
+		return
+	}
+	uni.navigateTo({
+	  url: `/pagesB/InviteInterview/index?id=${cvData.value.person.userId}`
+	})
+}
+
+// 立即沟通
+const handleSend = async () => {
+	if (!getAccessToken()) {
+    showAuthModal()
+    return
+  }
+  // 企业必须有招聘中的职位才能发起沟通
+  if (jobNum.value === 0) {
+		uni.showToast({
+			title: '请先发布招聘职位',
+			icon: 'none',
+			duration: 2000
+		})
+		return
+	}
+  const userId = cvData.value.person.userId
+  if (!userId) return
+  const channel = await talkToUser({ userId, text: defaultText })
+
+	const query = {
+		id: userId,
+		name: cvData.value?.person?.name || cvData.value?.person?.phone,
+		channelID: channel.channelID,
+		channelType: channel.channelType,
+		avatar: cvData.value.person?.avatar,
+		sex: cvData.value.person?.sex,
+	}
+
+	const queryStr = Object.keys(query).reduce((r, v) => {
+		if (!query[v]) {
+			return r
+		}
+		return r += `${v}=${encodeURIComponent(query[v])}&`
+	}, '?')
+	uni.navigateTo({
+    url: `/pagesA/chart/index${queryStr.slice(0, -1)}`
+  })
+}
 </script>
 
 <style scoped lang="scss">
@@ -166,11 +242,34 @@ onLoad(async (options) => {
     font-size: 24rpx;
   }
 }
-.bottom-actions {
-	position: fixed;
-	width: 100%;
-	bottom: 0;
-	height: 50px;
-	background-color: #fff;
+.bottom-content {
+  display: flex;
+  justify-content: space-evenly;
+  align-items: center;
+  width: 100%;
+  margin: 20rpx 0;
+  .btnStyle {
+    flex: 1;
+    margin-right: 20rpx;
+		border-radius: 50rpx;
+  }
+  .bgButtons {
+    border: 2rpx solid #00B760;
+    color: #00B760;
+  }
+  .shareButtonCss {
+    font-size: 16px;
+    background: unset;
+    &::after{
+      border:none !important;
+    }
+  }
+  &-tool {
+    width: 160rpx;
+    display: flex;
+    justify-content: center;
+    flex-direction: column;
+    align-items: center;
+  }
 }
 </style>

+ 35 - 77
pagesB/positionDetail/index.vue

@@ -78,6 +78,11 @@
         </view>
       </view>
     </scroll-view>
+    <view class="bottom-sticky" v-if="!loading && jobId">
+      <view class="bottom-content">
+        <button class="buttons btnStyle ss-m-l-15" type="primary" @click="handleEdit">编辑职位</button>
+      </view>
+    </view>
   </layout-page>
 </template>
 
@@ -86,7 +91,7 @@ import { commissionCalculation } from '@/utils/position'
 import { timesTampChange } from '@/utils/date'
 import layoutPage from '@/layout'
 import { ref, watch } from 'vue';
-import { getPositionDetails } from '@/api/position'
+import { getPositionDetails, getEnterprisePubJobTypePermission } from '@/api/position'
 import { dealDictObjData } from '@/utils/position'
 import { onLoad } from '@dcloudio/uni-app'
 import { userStore } from '@/store/user'
@@ -100,7 +105,6 @@ const loadingText = ref('加载中 . . . ')
 const info = ref({})
 const positionInfo = ref({})
 const beenLogin = ref(false)
-const areaName = ref('')
 
 // 监听登录状态
 watch(() => useUserStore.refreshToken, (newVal) => {
@@ -108,11 +112,17 @@ watch(() => useUserStore.refreshToken, (newVal) => {
 }, { immediate: true }, { deep: true })
 
 let jobId = ''
-const isJobFair = ref(false) // 是否是通过招聘会进入的岗位
+const isJobFair = ref(false)
 let obj = {}
+const isEdit = ref(false)
 onLoad(async (options) => {
-  areaName.value = options?.area || ''
   jobId = options?.id || options?.jobId || obj?.jobId || ''
+  
+  // 招聘会进入的岗位
+  if (options?.jobFairId) isJobFair.value = true
+  // 职位是否可编辑
+  if (options?.isEdit && options?.isEdit === 'true') isEdit.value = true
+
   if (jobId) {
     loading.value = true
     loadingText.value = '加载中 . . . '
@@ -142,25 +152,34 @@ async function getPositionDetail () {
     info.value = data
     positionInfo.value = { ...dealDictObjData({}, info.value), ...info.value, enterprise: dealDictObjData({}, data.enterprise) }
     loading.value = false
-    areaName.value = positionInfo.value.area?.str ?? '全国'
   } finally {
   }
 }
 
+// 职位编辑
+const handleEdit = async () => {
+  if (!isEdit.value) {
+    uni.showToast({ title: '职位发布时间超过24小时的不支持编辑', icon: 'none', duration: 2000 })
+    return
+  }
+  uni.showLoading({ title: '加载中 . . . ' })
+  try {
+    const { data } = await getEnterprisePubJobTypePermission()
+    if (!data || !data.length) {
+      uni.showToast({ title: '没有该操作权限,请联系平台管理员升级后再试', icon: 'none', duration: 2000 })
+      return
+    }
+    uni.hideLoading()
+    // 跳转编辑页面
+  } catch {
+    uni.hideLoading()
+  }
+}
+
 </script>
 
 <style scoped lang="scss">
 @import '../../static/style/position/index.scss';
-.hideCanvasView{
-	position: relative;
-}
-.shareCanvas {
-	position: fixed;
-	top: -99999upx;
-	left: -99999upx;
-	z-index: -99999;
-}
-
 .bottom-content {
   display: flex;
   justify-content: space-evenly;
@@ -169,68 +188,7 @@ async function getPositionDetail () {
   margin: 20rpx 0;
   .btnStyle {
     flex: 1;
-    margin-right: 20rpx;
-  }
-  .bgButtons {
-    border: 2rpx solid #00B760;
-    color: #00B760;
-    // background: #FFF;
-    border-radius: 50rpx;
-  }
-  .shareButtonCss {
-    font-size: 16px;
-    background: unset;
-    &::after{
-      border:none !important;
-    }
-  }
-  &-tool {
-    width: 160rpx;
-    display: flex;
-    justify-content: center;
-    flex-direction: column;
-    align-items: center;
-  }
-}
-.share-pop {
-    width: 100%;
-    // height:300rpx;
-    display: flex;
-    justify-content: center;
-    .f-straight {
-      margin: 40rpx;
-      background: unset;
-      &::after{
-        border:none !important;
-      }
-    }
-    .share-round {
-      border-radius:50%;
-      height:100rpx;
-      width:100rpx;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-    }
-    .share-round-1 {
-        background-color:#22a039;
-    }
-    .share-round-2 {
-        background-color:#3693cd;
-    }
-}
-.preview {
-  position: fixed;
-  z-index: 9;
-  height: 100vh;
-  width: 100vw;
-  left: 0;
-  top: 0;
-  .image {
-    position: absolute;
-    width: 80%;
-    left: 10%;
-    top: 100rpx;
+    margin: 0 20rpx;
   }
 }
 </style>

+ 0 - 122
pagesB/positionDetail/similar.vue

@@ -1,122 +0,0 @@
-<template>
-  <view>
-    <view class="box defaultBgc">
-      <scroll-view class="scrollBox" :scroll-y="true" :scroll-top="scrollTop" @scrolltolower="loadingMore" @scroll="onScroll" style="position:relative;">
-        <PositionList :list="positionListData" :noMore="false"></PositionList>
-        <uni-load-more :status="more" />
-      </scroll-view>
-    </view>
-  </view>
-</template>
-
-<script setup>
-import { ref, reactive } from 'vue'
-import PositionList from '@/components/PositionList'
-import { dealDictArrayData } from '@/utils/position'
-import { getSimilarPosition } from '@/api/position'
-
-const props = defineProps({
-  id: String
-})
-const more = ref('more')
-
-const positionListData = ref([])
-const query = reactive({
-  pageSize: 10, 
-  pageNo: 1,
-	id: props.id
-})
-console.log(props.id, '==================')
-const getData = async () => {
-  try {
-    const res = await getSimilarPosition(query)
-    const arr = res?.data?.list || []
-    // 推荐接口返回数据需要拼接
-    const list = arr
-    positionListData.value.push(...dealDictArrayData(list))
-    if (positionListData.value.length === +res.data.total) {
-      more.value = 'noMore'
-      return
-    }
-  } catch (error) {
-    query.pageNo--
-    more.value = 'more'
-  }
-}
-
-const scrollTop = ref(0)
-const old = ref({
-  scrollTop: 0
-})
-const onScroll = (e) =>{
-  old.value.scrollTop = e.detail.scrollTop
-}
-
-// 加载更多
-const loadingMore = () => {
-  more.value = 'loading'
-  query.pageNo++
-  getData()
-}
-
-</script>
-
-<style scoped lang="scss">
-.stick {
-  z-index: 1;
-  position: sticky;
-  top: 0;
-}
-.stickFilter {
-  z-index: 1;
-  position: sticky;
-  box-shadow: 0px 10rpx 12rpx 0px rgba(195, 195, 195, .25);
-  top: 120rpx;
-}
-.px-0 { padding-left: 0 !important; padding-right: 0 !important; }
-.pb-10 {
-  padding-bottom: 10px;
-}
-.pb-20 { padding-bottom: 20px; }
-.px-5 { padding-left: 5px; padding-right: 5px; }
-.px-10 { padding-left: 10px; padding-right: 10px; }
-.mx-10 { margin-left: 10px; margin-right: 10px }
-.mx-20 { margin-left: 20px; margin-right: 20px; }
-.mb-10 { margin-bottom: 10px; }
-.box {
-  height: 100vh;
-  overflow: hidden;
-  padding-bottom: 120rpx;
-  box-sizing: border-box;
-  display: flex;
-  flex-direction: column;
-}
-.scrollBox{
-  flex: 1;
-  height: 0 !important;
-  padding-bottom: 24rpx;
-  box-sizing: border-box;
-}
-:deep(.uni-searchbar__box) {
-  width: calc(100% - 105px);
-  height: 40px !important;
-  border: 1px solid #00B760;
-  padding-right: 20px;
-  flex: none;
-}
-.search-btn {
-  position: absolute;
-  right: 11px;
-  top: 10px;
-  width: 110px;
-  height: 40px;
-  font-size: 16px;
-  background-color: #00B760;
-  color: #fff;
-  border-radius: 0 5px 5px 0;
-  z-index: 9;
-}
-:deep(.picker-view) {
-  padding-bottom: 80px !important;
-}
-</style>

二進制
static/img/copy/company-fill.png


二進制
static/img/copy/message-fill.png


二進制
static/img/copy/position-fill.png


二進制
static/img/female.png


二進制
static/img/man.png


+ 65 - 0
utils/date.js

@@ -38,6 +38,71 @@ export const convertYearMonthToTimestamp = (yearMonth) => {
   return timestamp  
 }
 
+export const getTimeDifferenceInChinese = (startTime, endTime) => {
+  // 将时间戳转换为 Date 对象(假设时间戳单位为毫秒)
+  const startDate = startTime ? new Date(startTime) : new Date();
+  const endDate = endTime ? new Date(endTime) : new Date();
+
+  // 计算年份差
+  let yearsDiff = endDate.getFullYear() - startDate.getFullYear();
+  // 计算月份差(考虑年份差后调整)
+  let monthsDiff = endDate.getMonth() - startDate.getMonth();
+  // 如果月份差为负,则从上一年借月
+  if (monthsDiff < 0) {
+    yearsDiff--;
+    monthsDiff += 12;
+  }
+  // 计算日期差(考虑月份差后调整,如果日期差为负,则从上一月借天)
+  let daysDiff = endDate.getDate() - startDate.getDate();
+  if (daysDiff < 0) {
+    monthsDiff--;
+    // 获取 startDate 所在月的最后一天
+    const lastDayOfMonth = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 0).getDate();
+    daysDiff += lastDayOfMonth; // 加上最后一天以补全月份差
+  }
+
+  // 构建结果字符串
+  let result = "";
+  if (yearsDiff > 0) {
+    result += `${yearsDiff}年`;
+  }
+  if (monthsDiff > 0) {
+    if (result) {
+      // 如果已经有年份差异,则直接添加月份数(不带单位),后面正则替换会处理
+      result += monthsDiff;
+    } else {
+      // 如果没有年份差异,则正常添加月份和单位
+      result += `${monthsDiff}个月`;
+      // 特别处理只有1个月的情况
+      if (monthsDiff === 1) {
+        result = "不到1个月"; // 直接替换为“不到1个月”,避免后续复杂的正则替换
+      }
+    }
+  } else if (!result && daysDiff >= 0) {
+    // 如果没有年份和月份差异,但天数差异存在(这里其实没处理天数,只是为完整性添加)
+    // 理论上应该处理天数,但题目要求只到月份,所以这里直接返回“不到1个月”
+    result = "不到1个月";
+  }
+ 
+  // 如果之前添加了月份数但没有年份(且不是直接处理的1个月情况),则需要去除末尾多余的数字并添加“个月”
+  if (result && !/\d年$/.test(result) && /\d$/.test(result)) {
+    result += "个月";
+  }
+  return result
+}
+
+// 设置面试邀请默认时间
+export const getInterviewInviteDefaultTime = () => {
+  const today = new Date()
+  today.setDate(today.getDate() + 1)
+
+  const time = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 10, 0, 0)
+  return {
+    time,
+    timeStamp: time.getTime()
+  }
+}
+
 export const getNextDate = (createTimestamp, format = 'YYYY-MM-DD', type, startTime) => {
   if (type === 'day') createTimestamp = createTimestamp * 24 * 60 * 60 * 1000
   if (type === 'hour') createTimestamp = createTimestamp * 60 * 60 * 1000