ソースを参照

求职者-招聘会页

Xiao_123 2 ヶ月 前
コミット
2ea9e45270

+ 48 - 0
src/api/recruit/personal/jobFair.js

@@ -0,0 +1,48 @@
+import request from '@/config/axios'
+
+// 招聘会企业分页查询
+export const getJobFairEnterprisePage = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/job-fair/enterprise/page',
+		params
+	})
+}
+
+// 获得招聘会
+export const getJobFair = async (id) => {
+	return await request.get({
+		url: '/app-api/menduner/system/job-fair/get',
+		params: { id }
+	})
+}
+
+// 招聘会列表
+export const getJobFairList = async () => {
+	return await request.get({
+		url: '/app-api/menduner/system/job-fair/list'
+	})
+}
+
+// 根据企业id查询招聘会职位列表
+export const getJobFairEntJobPage = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/job-fair/detail/page',
+		params
+	})
+}
+
+// 效验招聘会职位是否投递
+export const jobFairPositionDeliveryCheck = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/job-cv-rel/job-fair/send/check',
+		params
+	})
+}
+
+// 保存分享参数
+export const saveShareQuery = async (data) => {
+	return await request.post({
+		url: '/app-api/menduner/system/share/share',
+		data
+	})
+}

ファイルの差分が大きいため隠しています
+ 0 - 0
src/assets/svg/jobFair.svg


+ 1 - 0
src/components/Position/longStrip.vue

@@ -20,6 +20,7 @@
       <div class="info-content" >
         <div class="job-info">
           <div class="job-name ellipsis" :class="{'cursor-pointer': val.job.status === '0'}" v-ellipse-tooltip>
+            <svg-icon v-if="val.job.bizId" name="jobFair" size="18" class="mr-1"></svg-icon>
             <span class="mr-3" :class="{'info-name': val.job.status === '0'}" @click.stop="handleToPositionDetails(val)">{{ formatName(val.job.name) }}</span>
             <span>
               [{{ !val.job.areaId ? '全国' : val.job.area?.str }}]

+ 34 - 0
src/router/modules/components/headhunting.js

@@ -1,6 +1,40 @@
 import Layout from '@/layout'
 
 const headhunting = [
+  {
+    path: '/recruit/personal/jobFair',
+    component: Layout,
+    name: 'jobFair',
+    meta: {
+      title: '招聘会'
+    },
+    children: [
+      {
+        path: '/recruit/personal/jobFair',
+        component: () => import('@/views/recruit/personal/jobFair/index.vue')
+      },
+    ]
+  },
+  {
+    path: '/recruit/personal/jobFair/:id',
+    component: () => import('@/views/recruit/personal/jobFair/details/index.vue'),
+    name: 'jobFairDetails'
+  },
+  {
+    path: '/recruit/personal/jobFair/enterprises/:id',
+    component: () => import('@/views/recruit/personal/jobFair/details/enterprises.vue'),
+    name: 'jobFairEnterprises'
+  },
+  {
+    path: '/recruit/personal/jobFair/position/:id',
+    component: () => import('@/views/recruit/personal/jobFair/details/position.vue'),
+    name: 'jobFairPosition'
+  },
+  {
+    path: '/recruit/personal/jobFair/entPosition/:id',
+    component: () => import('@/views/recruit/personal/jobFair/details/entJobCard.vue'),
+    name: 'jobFairEntPosition'
+  },
   {
     path: '/headhunting',
     component: Layout,

+ 167 - 0
src/views/recruit/components/jobFairEntShare/index.vue

@@ -0,0 +1,167 @@
+<template>
+	<canvas ref="shareCanvas" :width="canvasWidth" :height="canvasHeight"></canvas>
+
+	<Loading :visible="loading" />
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+import { getJobAdvertisedShareQrcode } from '@/api/position'
+import { saveShareQuery } from '@/api/recruit/personal/jobFair'
+
+const emit = defineEmits(['success'])
+const props = defineProps({
+  canvasWidth: {
+    type: Number,
+    default: 540
+  },
+  canvasHeight: {
+    type: Number,
+    default: 788
+  },
+	show: Boolean,
+	enterpriseName: String,
+	logoUrl: String,
+	positionList: Array,
+	jobFairId: String,
+	enterpriseId: String,
+	bgImg: String
+})
+
+const loading = ref(false)
+const shareCanvas = ref(null)
+const imgSrc = ref('')
+
+const drawCanvas = () => {
+  loading.value = true
+
+  const canvas = shareCanvas.value
+  const ctx = canvas.getContext('2d')
+  const img = new Image()
+  img.crossOrigin = 'anonymous'
+  img.onload = async () => {
+    //清空画布
+    ctx.clearRect(0, 0, props.canvasWidth, props.canvasHeight)
+
+    // 设置背景图片
+    ctx.drawImage(img, 0, 0, props.canvasWidth, props.canvasHeight)
+
+    // 分享二维码
+    const secondImg = new Image()
+    secondImg.crossOrigin = 'anonymous'
+    secondImg.onload = () => {
+      // 分享二维码
+      ctx.drawImage(secondImg, 80, 550, 110, 110)
+
+      ctx.font = 'bold 18px Arial'
+      ctx.fillStyle = '#000'
+      ctx.fillText('海量职位火热招聘中', 280, 605)
+
+      ctx.font = '15px Arial'
+      ctx.fillStyle = '#999'
+      ctx.fillText('长按图片识别二维码', 290, 625)
+
+      const thirdImg = new Image()
+      thirdImg.crossOrigin = 'anonymous'
+      thirdImg.onload = () => {
+        // 企业头像
+        ctx.save()
+        const secondImgWidth = 80
+        const secondImgHeight = 80
+        const x = (canvas.width - secondImgWidth) / 2
+        const y = canvas.height - secondImgHeight - 460
+        
+        ctx.beginPath()
+        ctx.arc(x + secondImgWidth / 2, y + secondImgHeight / 2, secondImgWidth / 2, 0, Math.PI * 2, true)
+        ctx.clip()
+        ctx.drawImage(thirdImg, x, y, secondImgWidth, secondImgHeight)
+        ctx.restore()
+
+        // 企业名称
+        const maxTextWidth = 400
+        const text = props.enterpriseName
+        const fontStyle = 'bold 18px Arial'
+        ctx.font = fontStyle
+        let truncatedText = text
+        while (ctx.measureText(truncatedText + '...').width > maxTextWidth && truncatedText.length > 0) {
+          truncatedText = truncatedText.slice(0, -1)
+        }
+        if (truncatedText !== text) truncatedText += '...'
+        const textX = x + (secondImgWidth - ctx.measureText(truncatedText).width) / 2
+        const textY = y + secondImgHeight + 30
+        ctx.fillStyle = '#000'
+        ctx.fillText(truncatedText, textX, textY)
+
+        // 职位标签
+        const tagPaddingLeftRight = 20
+        const tagPaddingTopBottom = 10
+        const tagRadius = 8
+        const tagSpacing = 30
+        let tagY = textY + tagSpacing
+        props.positionList.forEach((tag) => {
+          let truncatedTag = tag
+          while (ctx.measureText(truncatedTag + '...').width > maxTextWidth - 2 * tagPaddingLeftRight && truncatedTag.length > 0) {
+            truncatedTag = truncatedTag.slice(0, -1)
+          }
+          if (truncatedTag !== tag) truncatedTag += '...'
+
+          const tagWidth = ctx.measureText(truncatedTag).width + 2 * tagPaddingLeftRight
+          const tagX = x + (secondImgWidth - tagWidth) / 2
+
+          ctx.fillStyle = '#246a6c'
+          ctx.beginPath()
+          ctx.moveTo(tagX + tagRadius, tagY)
+          ctx.lineTo(tagX + tagWidth - tagRadius, tagY)
+          ctx.quadraticCurveTo(tagX + tagWidth, tagY, tagX + tagWidth, tagY + tagRadius)
+          ctx.lineTo(tagX + tagWidth, tagY + tagPaddingTopBottom * 2)
+          ctx.quadraticCurveTo(tagX + tagWidth, tagY + tagPaddingTopBottom * 2 + tagRadius, tagX + tagWidth - tagRadius, tagY + tagPaddingTopBottom * 2 + tagRadius)
+          ctx.lineTo(tagX + tagRadius, tagY + tagPaddingTopBottom * 2 + tagRadius)
+          ctx.quadraticCurveTo(tagX, tagY + tagPaddingTopBottom * 2 + tagRadius, tagX, tagY + tagPaddingTopBottom * 2)
+          ctx.lineTo(tagX, tagY + tagRadius);
+          ctx.quadraticCurveTo(tagX, tagY, tagX + tagRadius, tagY)
+          ctx.closePath()
+          ctx.fill()
+
+          ctx.font = '16px Arial'
+          ctx.fillStyle = '#fff'
+          ctx.fillText(truncatedTag, tagX + tagPaddingLeftRight, tagY + tagPaddingTopBottom + 10)
+
+          tagY += tagPaddingTopBottom * 2 + tagSpacing
+        })
+
+        imgSrc.value = canvas.toDataURL('image/png')
+        loading.value = false
+				emit('success', imgSrc.value)
+      }
+      thirdImg.src = props.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'
+    }
+    
+    // 保存分享参数
+    const result = await saveShareQuery({ id: '14304' })
+    const query = {
+      scene: 'id=' + result,
+      path: 'pagesB/positionDetail/index',
+      width: 200,
+      autoColor: false,
+      checkPath: true,
+      hyaline: false
+    }
+    const data = await getJobAdvertisedShareQrcode(query)
+    secondImg.src = 'data:image/png;base64,' + data
+  }
+
+  img.onerror = (error) => {
+    console.error('Failed to load image:', error)
+    loading.value = false
+  }
+
+  img.src = props.bgImg
+}
+
+watch(
+	() => props.show,
+	(val) => {
+		if (val) drawCanvas()
+	}
+)
+</script>

+ 1 - 3
src/views/recruit/personal/PersonalCenter/jobFeedback/components/interview/item.vue

@@ -9,9 +9,6 @@
         <v-btn class="ml-3" color="error" size="small" @click.stop="handleRefuse(val)">拒绝</v-btn>
       </div>
       <div v-if="val.job.status === '1'" class="font-size-14 header-btn color-error">职位已关闭</div>
-      <!-- <div v-if="tab === '1' || tab === '98'" class="float-right font-size-13" :style="{'padding': '12px 12px 0 0', 'color': tab === '1' ? 'var(--v-primary-base)' : 'var(--v-error-base)'}">
-        您已于{{ timesTampChange(val.updateTime, 'Y-M-D h:m') }}{{ tab === '1' ? '接受' : '拒绝'}}了此面试邀请
-      </div> -->
       <div class="img-box">
           <v-avatar :image="getUserAvatar(val.contact.avatar, val.contact.sex)" size="x-small"></v-avatar>
           <span class="name">
@@ -30,6 +27,7 @@
         </div>
         <div class="job-info color-666">
           <div class="job-name ellipsis" style="max-width: 410px;">
+            <svg-icon v-if="val.jobFairId" name="jobFair" size="18" class="mr-1"></svg-icon>
             <span class="mr-3" :class="{'cursor-pointer': val.job.status === '0', 'position-name': val.job.status === '0'}" @click.stop="handleToPositionDetails(val)">{{ formatName(val.job.name) }}</span>
             <span v-if="!val.job.payFrom && !val.job.payTo">面议</span>
             <span v-else>{{ val.job.payFrom ? val.job.payFrom + '-' : '' }}{{ val.job.payTo }}{{ val.job.payName ? '/' + val.job.payName : '' }}</span>

+ 2 - 2
src/views/recruit/personal/components/buttons.vue

@@ -1,4 +1,3 @@
-<!--  -->
 <template>
   <v-tabs v-model="tab" align-tabs="start" color="primary" bg-color="#f7f8fa">
     <v-tab v-for="(k, i) in tagList" :key="k.value" :value="k.value" @click="handleUpdate(i)">{{ k.title }}</v-tab>
@@ -22,7 +21,8 @@ const tab = ref(props.current)
 const tagList = [
   { title: '推荐', path:'/recruit/personal/recommend', value: 0 },
   { title: '职位', path:'/recruit/personal/position', value: 1 },
-  { title: '公司', path:'/recruit/personal/company', value: 2 }
+  { title: '公司', path:'/recruit/personal/company', value: 2 },
+  { title: '招聘会', path:'/recruit/personal/jobFair', value: 3 },
 ]
 
 const handleUpdate = (e) => {

+ 61 - 0
src/views/recruit/personal/jobFair/details/components/entCard.vue

@@ -0,0 +1,61 @@
+<template>
+	<v-hover v-slot="{ isHovering, props }" v-for="(val, index) in list" :key="val.id">
+		<v-card v-bind="props" :elevation="isHovering ? 10 : 5" class="cursor-pointer mb-3" :class="{'active': chosenIndex === index}" width="500" @click="handleClickEnterprise(val, index)">
+			<div class="d-flex pa-4 pb-2">
+				<img :src="val.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'" style="width: 80px; height: 80px; border-radius: 4px;" />
+				<div style="max-width: 390px;">
+					<h3 v-ellipse-tooltip class="enterprise-name ml-3">{{ formatName(val.anotherName || val.name) }}</h3>
+					<p class="font-size-14 color-666 mt-1 mb-2 ml-3">
+						<span>{{ val.industryName }}</span>
+						<span class="septal-line" v-if="val.industryName && val.scaleName"></span>
+						<span>{{ val.scaleName }}</span>
+					</p>
+					<div class="flex-nowrap overflow-hidden pl-3" style="height: 35px;">
+						<v-chip v-for="(welfare, index) in val.welfareList" :key="index" class="mr-2 mb-4 display-inline-block" variant="flat" color="primary" size="small">
+							{{ welfare }}
+						</v-chip>
+					</div>
+				</div>
+			</div>
+			<div class="card-bottom">{{ val.jobCount }}个在线职位招聘中</div>
+		</v-card>
+	</v-hover>
+</template>
+
+<script setup>
+defineOptions({ name: 'EntCard' })
+import { formatName } from '@/utils/getText'
+import { ref } from 'vue'
+
+const emit = defineEmits(['selectChange'])
+defineProps({
+	list: Array
+})
+
+const chosenIndex = ref(0)
+
+const handleClickEnterprise = (val, index) => {
+	chosenIndex.value = index
+	emit('selectChange', val)
+}
+</script>
+
+<style scoped lang="scss">
+.card-bottom {
+	height: 40px;
+	line-height: 40px;
+	color: #fff;
+	text-align: center;
+	font-size: 15px;
+	background: linear-gradient(to right, #12ebb0, #427daa);
+}
+
+.active {
+	border: 1px solid var(--v-primary-base);
+}
+.v-card:hover {
+	h3 {
+		color: var(--v-primary-base);
+	}
+}
+</style>

+ 65 - 0
src/views/recruit/personal/jobFair/details/components/entCard1.vue

@@ -0,0 +1,65 @@
+<template>
+	<div class="content" :class="{'px-3': isMobile}" :style="{'grid-template-columns': `repeat(${isMobile ? 1 : 2}, 1fr)`}">
+		<v-hover v-slot="{ isHovering, props }" v-for="val in list" :key="val.id">
+			<v-card v-bind="props" :elevation="isHovering ? 10 : 5" class="cursor-pointer" @click="handleClickEnterprise(val.id)">
+				<div class="d-flex pa-4 pb-2">
+					<img :src="val.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'" style="width: 100px; height: 100px; border-radius: 4px;border: 1px solid #ccc;" />
+					<div style="max-width: 370px;">
+						<h3 v-ellipse-tooltip class="enterprise-name ml-3 font-weight-medium">{{ formatName(val.anotherName || val.name) }}</h3>
+						<p class="font-size-15 color-999 mt-1 mb-2 ml-3">
+							<span>{{ val.industryName }}</span>
+							<span class="septal-line" v-if="val.industryName && val.scaleName"></span>
+							<span>{{ val.scaleName }}</span>
+						</p>
+						<div class="flex-nowrap overflow-hidden pl-3" style="height: 35px;">
+							<v-chip v-for="(welfare, index) in val.welfareList" :key="index" class="mr-2 mb-4 display-inline-block" color="primary" size="small">
+								{{ welfare }}
+							</v-chip>
+						</div>
+					</div>
+				</div>
+				<div class="card-bottom px-5" @click="handleClickEnterprise(val.id)">
+					{{ val.jobCount }}个在线职位招聘中
+					<v-icon>mdi-arrow-right-drop-circle-outline</v-icon>
+				</div>
+			</v-card>
+		</v-hover>
+	</div>
+</template>
+
+<script setup>
+defineOptions({ name: 'EntCard' })
+import { formatName } from '@/utils/getText'
+
+const props = defineProps({
+	list: Array,
+	jobFairId: String,
+	isMobile: Boolean
+})
+
+const handleClickEnterprise = (id) => {
+	window.open(`/recruit/personal/jobFair/entPosition/${props.jobFairId}?enterpriseId=${id}`)
+}
+</script>
+
+<style scoped lang="scss">
+.content {
+  display: grid;
+  gap: 24px;
+  min-height: auto;
+}
+.card-bottom {
+	height: 40px;
+	line-height: 40px;
+	text-align: end;
+	font-size: 15px;
+	color: var(--v-primary-base);
+	background-color: #f5f5f5;
+}
+
+.v-card:hover {
+	h3 {
+		color: var(--v-primary-base);
+	}
+}
+</style>

+ 149 - 0
src/views/recruit/personal/jobFair/details/components/jobCard.vue

@@ -0,0 +1,149 @@
+<template>
+	<!-- <div class="resume-header mb-4 mt-1">
+    <div class="resume-title" v-ellipse-tooltip style="max-width: 400px;">{{ enterpriseName }}</div>
+		<TextInput :item="textItem" @enter="handleSearch" @appendInnerClick="handleSearch" />
+  </div> -->
+	<div v-if="items.length > 0" v-loading="loading">
+		<v-hover v-slot="{ isHovering, props }" v-for="val in items" :key="val.id">
+			<v-card  class="cursor-pointer mb-3 pa-3" v-bind="props" :elevation="isHovering ? 10 : 5" @click="handleClick(val.id)">
+				<div class="d-flex justify-space-between">
+					<div class="d-flex align-center">
+						<svg-icon name="jobFair" size="20" class="mr-1"></svg-icon>
+						<p class="job-name" v-ellipse-tooltip>{{ formatName(val.name) }}</p>
+					</div>
+					<p v-if="!val.payFrom && !val.payTo" class="salary">面议</p>
+					<p v-else class="salary ml-3">{{ val.payFrom ? val.payFrom + '-' : '' }}{{ val.payTo }}{{ val.payName ? '/' + val.payName : '' }}</p>
+				</div>
+				<div class="d-flex justify-space-between mt-3">
+					<div class="color-666 font-size-14">
+						<span v-for="k in desc" :key="k.mdi">
+							<span v-if="val[k.value] || k.value === 'areaName'" class="mr-5">
+								<v-icon color="var(--color-666)" size="15">{{ k.mdi }}</v-icon>
+								<span class="ml-1 tag-text">
+									{{ k.value === 'areaName' ? !val.areaId ? '全国' : val.area?.str : val[k.value] }}
+								</span>
+							</span>
+						</span>
+					</div>
+					<div class="font-size-14 color-999">{{ timesTampChange(val.updateTime) }} 刷新</div>
+				</div>
+			</v-card>
+		</v-hover>
+		<div :class="['loading', {'defaultLink-i': !loadingType}]" @click="handleChangePage">{{ loadingText[loadingType] }}</div>
+	</div>
+	<Empty v-else :elevation="false" message="该企业暂无在招职位,换个企业试试吧"></Empty>
+</template>
+
+<script setup>
+defineOptions({ name: 'jobCard' })
+import { ref, watch, reactive } from 'vue'
+import { formatName } from '@/utils/getText'
+import { getJobFairEntJobPage } from '@/api/recruit/personal/jobFair'
+import { dealDictArrayData } from '@/utils/position'
+import { timesTampChange } from '@/utils/date'
+
+const props = defineProps({
+	enterpriseId: [String, Number],
+	enterpriseName: String,
+	jobFairId: [String, Number]
+})
+
+const loading = ref(false)
+const items = ref([])
+const query = reactive({
+	pageNo: 1,
+	pageSize: 10,
+	keyword: '',
+	jobFairId: props.jobFairId
+})
+// 城市、学历、工作经验
+const desc = [
+  { mdi: 'mdi-map-marker-outline', value: 'areaName' },
+  { mdi: 'mdi-school-outline', value: 'eduName' },
+  { mdi: 'mdi-clock-time-ten-outline', value: 'expName' }
+]
+const textItem = ref({
+  type: 'text',
+  width: 220,
+  value: '',
+  label: '职位关键字',
+	placeholder: '请输入职位关键字',
+  clearable: true,
+	hideDetails: true,
+  appendInnerIcon: 'mdi-magnify'
+})
+const loadingText = ['加载更多', '加载中...', '没有更多数据了']
+const loadingType = ref(0)
+
+// 职位列表
+const getPositionList = async () => {
+	loading.value = true
+	loadingType.value = 1
+	try {
+		const { list, total } = await getJobFairEntJobPage(query)
+		if (!list || !list.length) {
+			items.value = []
+			total.value = 0
+			loadingType.value = 2
+			return
+		}
+		items.value = items.value.concat(dealDictArrayData([], list))
+		loadingType.value = items.value.length === total ? 2 : 0
+		loading.value = false
+	} catch {}
+}
+
+// 职位关键字检索
+const handleSearch = (val) => {
+	query.keyword = val
+	query.pageNo = 1
+	getPositionList()
+}
+
+// 加载更多
+const handleChangePage = () => {
+  if (loadingType.value) return // 没有更多数据了
+  query.pageNo++
+  getPositionList()
+}
+
+watch(
+	() => props.enterpriseId,
+	(val) => {
+		query.enterpriseId = val
+		query.keyword = ''
+		query.pageNo = 1
+		items.value = []
+		getPositionList()
+	},
+	{ immediate: true }
+)
+
+// 跳转职位详情
+const handleClick = (id) => {
+	window.open(`/recruit/personal/position/details/${id}?jobFairId=${props.jobFairId}`)
+}
+</script>
+
+<style scoped lang="scss">
+.salary {
+	color: #cec149;
+	font-weight: 700;
+}
+.job-name {
+	color: #345768;
+	font-weight: 700;
+	max-width: 420px;
+}
+.v-card:hover {
+	.job-name {
+		color: var(--v-primary-base)
+	}
+}
+.loading {
+  margin-top: 8px;
+  text-align: center;
+  font-size: 13px;
+  color: gray;
+}
+</style>

+ 100 - 0
src/views/recruit/personal/jobFair/details/components/jobCard1.vue

@@ -0,0 +1,100 @@
+<template>
+	<div class="content" :class="{'px-3': isMobile}" :style="{'grid-template-columns': `repeat(${isMobile ? 1 : 2}, 1fr)`}">
+		<v-hover v-slot="{ isHovering, props }" v-for="val in list" :key="val.id">
+			<v-card  class="cursor-pointer pa-4" v-bind="props" :elevation="isHovering ? 10 : 5" >
+				<div @click="handleClick(val.id)">
+					<div class="d-flex justify-space-between">
+						<div class="d-flex align-center">
+							<p class="job-name font-weight-medium" v-ellipse-tooltip>{{ formatName(val.name) }}</p>
+						</div>
+						<p v-if="!val.payFrom && !val.payTo" class="salary">面议</p>
+						<p v-else class="salary ml-3">{{ val.payFrom ? val.payFrom + '-' : '' }}{{ val.payTo }}{{ val.payName ? '/' + val.payName : '' }}</p>
+					</div>
+					<div class="d-flex justify-space-between mt-3">
+						<div class="color-999 font-size-15">
+							<span v-for="k in desc" :key="k.mdi">
+								<span v-if="val[k.value] || k.value === 'areaName'" class="mr-5">
+									<v-icon color="var(--color-666)" size="15">{{ k.mdi }}</v-icon>
+									<span class="ml-1 tag-text">
+										{{ k.value === 'areaName' ? !val.areaId ? '全国' : val.area?.str : val[k.value] }}
+									</span>
+								</span>
+							</span>
+						</div>
+					</div>
+				</div>
+				<v-divider class="mt-3" />
+				<!-- 企业信息 -->
+				<div class="d-flex align-center pt-4" @click="handleClickEnterprise(val.enterprise.id)">
+					<img :src="val.enterprise.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'" style="width: 60px; height: 60px; border-radius: 4px;border: 1px solid #ccc;" />
+					<div class="ml-3" style="flex: 1;">
+						<h3 v-ellipse-tooltip class="font-weight-medium enterprise-name" style="max-width: 480px;">{{ formatName(val.enterprise.anotherName || val.enterprise.name) }}</h3>
+						<p class="font-size-15 color-999 mt-1">
+							<span>{{ val.enterprise.industryName }}</span>
+							<span class="septal-line" v-if="val.enterprise.industryName && val.enterprise.scaleName"></span>
+							<span>{{ val.enterprise.scaleName }}</span>
+						</p>
+					</div>
+				</div>
+			</v-card>
+		</v-hover>
+	</div>
+</template>
+
+<script setup>
+defineOptions({ name: 'jobCard' })
+import { formatName } from '@/utils/getText'
+import Snackbar from '@/plugins/snackbar'
+
+const props = defineProps({
+	list: Array,
+	jobFairId: [String, Number],
+	isMobile: Boolean
+})
+
+// 城市、学历、工作经验
+const desc = [
+  { mdi: 'mdi-map-marker-outline', value: 'areaName' },
+  { mdi: 'mdi-school-outline', value: 'eduName' },
+  { mdi: 'mdi-clock-time-ten-outline', value: 'expName' }
+]
+
+// 跳转职位详情
+const handleClick = (id) => {
+	if (props.isMobile) return Snackbar.warning('请复制链接后在电脑端查看职位详情')
+	window.open(`/recruit/personal/position/details/${id}?jobFairId=${props.jobFairId}`)
+}
+
+const handleClickEnterprise = (id) => {
+	if (props.isMobile) return Snackbar.warning('请复制链接后在电脑端查看企业招聘职位')
+	window.open(`/recruit/personal/jobFair/entPosition/${props.jobFairId}?enterpriseId=${id}`)
+}
+</script>
+
+<style scoped lang="scss">
+.content {
+  display: grid;
+  gap: 24px;
+  min-height: auto;
+}
+.salary {
+	color: #fe574a;
+	font-size: 20px;
+}
+.job-name {
+	max-width: 400px;
+	font-size: 20px;
+	&:hover {
+		color: var(--v-primary-base)
+	}
+}
+.enterprise-name:hover {
+	color: var(--v-primary-base)
+}
+.loading {
+  margin-top: 8px;
+  text-align: center;
+  font-size: 13px;
+  color: gray;
+}
+</style>

+ 176 - 0
src/views/recruit/personal/jobFair/details/entJobCard.vue

@@ -0,0 +1,176 @@
+<!-- 企业 -->
+<template>
+  <div class="position-relative">
+    <div v-if="jobFair?.contentImg" class="position-fixed" style="right: 24px; top: 20px; z-index: 999">
+      <v-btn icon="mdi-share-all" color="primary" size="x-large" @click.stop="handleShare"></v-btn>
+    </div>
+
+    <div :style="{'background-color': jobFair.backgroundColour || '#fff', 'min-height': '100vh'}" class="pt-10">
+      <div :class="{'default-width': !isMobile}">
+        <h2 class="color-white pt-5 enterpriseName" :class="{'ml-3': isMobile}">{{ enterpriseName }}</h2>
+        <div v-if="!items.length" class="emptyText">{{ loadingType === 1 ? loadingText[loadingType] : '暂无数据,去看看其他吧~' }}</div>
+        <template v-else>
+          <JobCard :jobFairId="jobFair?.id" :list="items" :isMobile="isMobile" class="pt-5" />
+          <div :class="['loading', {'loadMoreText': !loadingType}]" class="pb-5" @click="handleChangePage">{{ loadingText[loadingType] }}</div>
+        </template>
+      </div>
+    </div>
+
+    <div class="hideCanvasView">
+      <JobFairEntShare 
+        :show="showShare" 
+        :enterpriseName="enterpriseName" 
+        :logoUrl="logoUrl" 
+        :positionList="positionList" 
+        :bgImg="jobFair?.contentImg" 
+        @success="handlePreview"
+      ></JobFairEntShare>
+    </div>
+  </div>
+
+  <PreviewImage v-if="showPreview" :urlList="[previewSrc]" :fileName="enterpriseName" @close="showPreview = !showPreview, showShare = false" />
+</template>
+
+<script setup>
+defineOptions({name: 'jobFair-enterprises-job-card'})
+import { ref, reactive, onMounted } from 'vue'
+import { getJobFair, getJobFairEntJobPage } from '@/api/recruit/personal/jobFair'
+import JobCard from './components/jobCard1.vue'
+import { useRoute } from 'vue-router'; const route = useRoute();
+import { dealDictObjData } from '@/utils/position'
+import { getEnterpriseDetails } from '@/api/enterprise'
+import { formatName } from '@/utils/getText'
+import JobFairEntShare from '@/views/recruit/components/jobFairEntShare'
+
+const query = reactive({
+  pageNo: 1,
+  pageSize: 20,
+	jobFairId: route.params.id,
+  enterpriseId: route.query.enterpriseId
+})
+
+const logoUrl = ref('')
+const showPreview = ref(false)
+const showShare = ref(false)
+const previewSrc = ref('')
+const positionList = ref([])
+
+const isMobile = ref(false)
+onMounted(() => {
+  const userAgent = navigator.userAgent
+  isMobile.value = /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i.test(userAgent)
+})
+
+const items = ref([])
+const loadingText = ['点击加载更多', '加载中...', '']
+const loadingType = ref(0)
+
+// 参与招聘会的企业
+const getList = async () => {
+	loadingType.value = 1
+
+	try {
+		const result = await getJobFairEntJobPage(query)
+		const list = result?.list || []
+    if (list.length) {
+      items.value.push(...list.map(e => {
+        e.enterprise = dealDictObjData({}, e.enterprise)
+        e = dealDictObjData({}, e)
+        return e
+      }))
+
+			loadingType.value = items.value.length === result.total ? 2 : 0
+		} else {
+			loadingType.value = 2
+		}
+	} catch {}
+}
+
+// 招聘会详情
+const jobFair = ref([])
+const enterpriseName = ref('')
+const getJobFairDetail = async () => {
+  const data = await getJobFair(route?.params?.id)
+  if (!data) return
+  jobFair.value = data
+
+  const { enterprise } = await getEnterpriseDetails({ id: route.query.enterpriseId })
+  enterpriseName.value = formatName(enterprise.anotherName || enterprise.name)
+  logoUrl.value = enterprise.logoUrl
+  document.title = enterpriseName.value + ' - 招聘会职位'
+
+  getList()
+}
+getJobFairDetail()
+
+const handleChangePage = () => {
+  if (loadingType.value) return // 没有更多数据了
+  // 加载更多
+  query.pageNo++
+  getList()
+}
+
+// 海报预览
+const handlePreview = (src) => {
+  if (!src) return
+  previewSrc.value = src
+  showPreview.value = true
+}
+
+const handleShare = () => {
+  positionList.value = items.value && items.value.length > 0 ? items.value.map(e => formatName(e.name)).slice(0, 2) : []
+  showShare.value = true
+}
+</script>
+
+<style scoped lang="scss">
+.hideCanvasView {
+	position: fixed;
+	top: -99999px;
+	left: -99999px;
+	z-index: -99999;
+}
+.enterpriseName {
+  position: relative;
+  padding-left: 20px;
+  &::before {
+    display: block;
+    content: '';
+    width: 6px;
+    height: 30px;
+    background-color: #fff;
+    position: absolute;
+    top: 25px;
+    left: 0;
+    border-radius: 2px;
+  }
+}
+.emptyText {
+  color: #fff;
+  height: calc(100vh - 574px);
+  line-height: calc(100vh - 574px);
+  text-align: center;
+}
+.loading {
+  margin-top: 8px;
+  text-align: center;
+  font-size: 18px;
+  color: #fff;
+}
+.loadMoreText {
+  color: #fff;
+  cursor: pointer;
+}
+::-webkit-scrollbar {
+  width: 0;
+  height: 0;
+}
+::-webkit-scrollbar-thumb, .temporaryAdd ::-webkit-scrollbar-thumb, .details_edit ::-webkit-scrollbar-thumb {
+  // 滚动条-颜色
+  background: #c3c3c379;
+}
+::-webkit-scrollbar-track, .temporaryAdd ::-webkit-scrollbar-track, .details_edit ::-webkit-scrollbar-track {
+  // 滚动条-底色
+  background: #e5e5e58f;
+}
+</style>

+ 290 - 0
src/views/recruit/personal/jobFair/details/enterprises.vue

@@ -0,0 +1,290 @@
+<!-- 企业 -->
+<template>
+  <div class="position-relative">
+    <!-- 轮播 -->
+    <v-carousel 
+      v-if="jobFair?.pcHeadImg && jobFair?.pcHeadImg.length > 0" 
+      :show-arrows="jobFair?.pcHeadImg.length > 1 ? 'hover' : false" 
+      cycle :hide-delimiters="true" 
+      :style="{'height': isMobile ? '200px' : '500px'}"
+    >
+			<v-carousel-item v-for="(k, i) in jobFair?.pcHeadImg" :key="i">
+				<img :src="k" :lazy-src="k" style="width: 100%; height:100%;">
+			</v-carousel-item>
+		</v-carousel>
+
+    <!-- 招聘会分享按钮 -->
+    <div v-if="jobFair?.shareImg" class="position-fixed" style="right: 24px; top: 20px; z-index: 999;">
+      <v-btn icon="mdi-share-all" color="primary" size="x-large" @click.stop="handleShare"></v-btn>
+    </div>
+
+    <div :style="{'background-color': jobFair.backgroundColour || '#fff', 'min-height': jobFair?.pcHeadImg && jobFair?.pcHeadImg.length > 0 ? `calc(100vh - ${isMobile ? '200px' : '500px'})` : '100vh'}">
+      <div :class="{'default-width': !isMobile}">
+        <!-- 类别展示 -->
+        <div class="d-flex align-center" v-if="jobFair?.tag && jobFair.tag.length" style="overflow-x: auto;">
+          <div 
+            v-for="(val, index) in jobFair.tag" 
+            :key="index" 
+            class="tag-item"
+            :class="[{'tag-active-bg': index === tab && jobFair.backgroundColour}, {'mx-5': isMobile}, {'tag-active': index === tab && !jobFair.backgroundColour}]" 
+            :style="{'width': `calc(100% / ${jobFair.tag.length} )`, 'color': jobFair.backgroundColour ? '#fff' : 'var(--v-primary-base)'}"
+            @click.stop="handleTabClick(index)"
+          >
+            {{ val.title }}
+          </div>
+        </div>
+
+        <div 
+          v-if="!items.length" 
+          class="text-center" 
+          :style="{'color': jobFair.backgroundColour ? '#fff' : 'var(--v-primary-base)', 'height': `calc(100vh - ${jobFair?.tag && jobFair.tag.length ? '574px': '500px'})`, 'line-height': `calc(100vh - ${jobFair?.tag && jobFair.tag.length ? '574px': '500px'})`}"
+        >
+          {{ loadingType === 1 ? loadingText[loadingType] : '暂无数据,去看看其他吧~' }}
+        </div>
+        <template v-else>
+          <EntCard v-if="jobFair?.category === '0'" :jobFairId="jobFair?.id" :list="items" :isMobile="isMobile" class="mt-5" />
+          <JobCard v-if="jobFair?.category === '1'" :jobFairId="jobFair?.id" :list="items" :isMobile="isMobile" class="mt-5" />
+          <div 
+            :class="['loading', {'cursor-pointer': !loadingType}]" 
+            :style="{'color': !jobFair?.tag || !jobFair.tag.length ? 'var(--v-primary-base)' : '#fff'}" 
+            class="py-5" 
+            @click="handleChangePage"
+          >
+            {{ loadingText[loadingType] }}
+          </div>
+        </template>
+      </div>
+    </div>
+
+    <div class="hideCanvasView">
+      <canvas ref="shareCanvas" :width="canvasWidth" :height="canvasHeight"></canvas>
+    </div>
+  </div>
+
+  <Loading :visible="loading" />
+
+  <PreviewImage v-if="showPreview" :urlList="[previewSrc]" :fileName="jobFair?.title?.replace(/<\/?p[^>]*>/gi, '')" @close="showPreview = !showPreview" />
+
+</template>
+
+<script setup>
+defineOptions({name: 'jobFair-enterprises'})
+import { ref, reactive, onMounted } from 'vue'
+import { getJobFairEnterprisePage, getJobFair, getJobFairEntJobPage, saveShareQuery } from '@/api/recruit/personal/jobFair'
+import EntCard from './components/entCard1.vue'
+import JobCard from './components/jobCard1.vue'
+import { useRoute } from 'vue-router'; const route = useRoute();
+import { dealDictArrayData, dealDictObjData } from '@/utils/position'
+import { getJobAdvertisedShareQrcode } from '@/api/position'
+
+const tab = ref(0)
+const loading = ref(false)
+const query = reactive({
+  pageNo: 1,
+  pageSize: 20,
+	jobFairId: route.params.id
+})
+
+const showPreview = ref(false)
+const shareCanvas = ref(null)
+const previewSrc = ref('')
+const canvasWidth = 500
+const canvasHeight = 900
+
+const isMobile = ref(false)
+onMounted(() => {
+  const userAgent = navigator.userAgent
+  isMobile.value = /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i.test(userAgent)
+})
+
+const items = ref([])
+const loadingText = ['点击加载更多', '加载中...', '']
+const loadingType = ref(0)
+
+// 参与招聘会的企业
+const getList = async () => {
+	loadingType.value = 1
+
+  // 有类别的添加筛选条件
+  if (jobFair.value?.tag && jobFair.value?.tag.length) {
+    const key = jobFair.value.tag[tab.value].key
+    const value = jobFair.value.tag[tab.value].value
+    query[key] = value
+  }
+
+	try {
+		const result = jobFair.value?.category === '0' ? await getJobFairEnterprisePage(query) : await getJobFairEntJobPage(query)
+		const list = result?.list || []
+    if (list.length) {
+      if (jobFair.value?.category === '1') {
+        items.value.push(...list.map(e => {
+          e.enterprise = dealDictObjData({}, e.enterprise)
+          e = dealDictObjData({}, e)
+          return e
+        }))
+      }
+      else items.value = items.value.concat(dealDictArrayData([], list))
+
+			loadingType.value = items.value.length === result.total ? 2 : 0
+		} else {
+			loadingType.value = 2
+		}
+	} catch {}
+}
+
+// 招聘会详情
+const jobFair = ref([])
+const getJobFairDetail = async () => {
+  const data = await getJobFair(route?.params?.id)
+  if (!data) return
+  jobFair.value = data
+  document.title = data.title.replace(/<\/?p[^>]*>/gi, '')
+  getList()
+}
+getJobFairDetail()
+
+// tab项点击
+const handleTabClick = (index) => {
+  items.value = []
+  query.pageNo = 1
+  tab.value = index
+  getList()
+}
+
+const handleChangePage = () => {
+  if (loadingType.value) return // 没有更多数据了
+  // 加载更多
+  query.pageNo++
+  getList()
+}
+
+const handleShare = () => {
+  loading.value = true
+
+  const canvas = shareCanvas.value
+  const ctx = canvas.getContext('2d')
+  const img = new Image()
+  img.crossOrigin = 'anonymous'
+  img.onload = async () => {
+    //清空画布
+    ctx.clearRect(0, 0, canvasWidth, canvasHeight)
+
+    // 设置背景图片
+    ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight)
+
+    // 分享二维码
+    const secondImg = new Image()
+    secondImg.crossOrigin = 'anonymous'
+    secondImg.onload = () => {
+      // 计算第二张图片的位置和大小(这里以示例为准,您可以根据需要调整)
+      const secondImgWidth = Math.min(secondImg.width, canvas.width / 4)
+      const secondImgHeight = (secondImgWidth / secondImg.width) * secondImg.height // 保持宽高比
+      const x = (canvas.width - secondImgWidth) / 2 // 水平居中
+      const y = canvas.height - secondImgHeight - 190 // 垂直放置在 Canvas 底部上方 170 像素处
+  
+      // 绘制第二张图片到 Canvas 上的指定位置和大小
+      ctx.drawImage(secondImg, x, y, secondImgWidth, secondImgHeight)
+
+      ctx.font = 'bold 16px Arial'
+      ctx.fillStyle = '#10325d'
+      const text = '诚挚邀约 共享机遇'
+      const textWidth = ctx.measureText(text).width
+      const textX = x + (secondImgWidth - textWidth) / 2
+      const textY = y + secondImgHeight + 30
+      ctx.fillText(text, textX, textY)
+
+      previewSrc.value = canvas.toDataURL()
+      loading.value = false
+      showPreview.value = true
+    }
+    
+    // 保存分享参数
+    const result = await saveShareQuery({ jobFairId: jobFair.value.id })
+    const query = {
+      scene: 'id=' + result,
+      path: 'pagesB/jobFair/positionClassification',
+      width: 200,
+      autoColor: false,
+      checkPath: true,
+      hyaline: false
+    }
+    const data = await getJobAdvertisedShareQrcode(query)
+    secondImg.src = 'data:image/png;base64,' + data
+  }
+
+  img.onerror = (error) => {
+    console.error('Failed to load image:', error)
+    loading.value = false
+  }
+
+  img.src = jobFair.value?.shareImg
+}
+</script>
+
+<style scoped lang="scss">
+.tag-item {
+  font-weight: 700;
+  font-size: 24px;
+  text-align: center;
+  height: 74px;
+  line-height: 74px;
+  opacity: .8;
+  cursor: pointer;
+  text-wrap: nowrap;
+}
+.tag-active-bg {
+  opacity: 1;
+  position: relative;
+  &::before {
+    display: block;
+    content: '';
+    width: 34px;
+    height: 4px;
+    background-color: #fff;
+    position: absolute;
+    top: 62px;
+    left: 50%;
+    border-radius: 2px;
+    transform: translateX(-50%);
+  }
+}
+.tag-active {
+  opacity: 1;
+  position: relative;
+  &::before {
+    display: block;
+    content: '';
+    width: 34px;
+    height: 4px;
+    background-color: var(--v-primary-base);
+    position: absolute;
+    top: 62px;
+    left: 50%;
+    border-radius: 2px;
+    transform: translateX(-50%);
+  }
+}
+.loading {
+  margin-top: 8px;
+  text-align: center;
+  font-size: 18px;
+}
+.hideCanvasView {
+	position: fixed;
+	top: -99999px;
+	left: -99999px;
+	z-index: -99999;
+}
+::-webkit-scrollbar {
+  width: 0;
+  height: 0;
+}
+::-webkit-scrollbar-thumb, .temporaryAdd ::-webkit-scrollbar-thumb, .details_edit ::-webkit-scrollbar-thumb {
+  // 滚动条-颜色
+  background: #c3c3c379;
+}
+::-webkit-scrollbar-track, .temporaryAdd ::-webkit-scrollbar-track, .details_edit ::-webkit-scrollbar-track {
+  // 滚动条-底色
+  background: #e5e5e58f;
+}
+</style>

+ 180 - 0
src/views/recruit/personal/jobFair/details/index.vue

@@ -0,0 +1,180 @@
+<template>
+  <div>
+    <!-- <div style="background-color: #fff; position: sticky;"> -->
+      <!-- <buttons :current="3" class="mb-3"></buttons> -->
+			<!-- <v-breadcrumbs v-if="breadcrumbs?.length" :items="breadcrumbs">
+				<template v-slot:item="{ item }">
+					<span class="breadcrumbsText" :class="{ active: !item.disabled }" @click="handleClick(item)">{{ item.text }}</span>
+				</template>
+			</v-breadcrumbs> -->
+
+			<!-- <headSearch v-model="query.keyword" :class="{'mt-3': !breadcrumbs?.length}" placeholder="搜索公司关键字" @handleSearch="handleSearch"></headSearch> -->
+    <!-- </div> -->
+
+    <!-- <v-carousel :show-arrows="cursor.length > 1 ? 'hover' : false" :hide-delimiters="true" cycle style="height: 300px;">
+      <v-carousel-item v-for="(k, i) in cursor" :key="i">
+				<v-img :src="k" :lazy-src="k" width="1184" height="300" cover>
+					<template v-slot:placeholder>
+						<v-row align="center" class="fill-height ma-0" justify="center">
+							<v-progress-circular color="grey-lighten-5" indeterminate></v-progress-circular>
+						</v-row>
+					</template>
+				</v-img>
+			</v-carousel-item>
+    </v-carousel> -->
+    <v-carousel :show-arrows="cursor.length > 1 ? 'hover' : false" cycle :hide-delimiters="true" style="height: 500px;">
+			<v-carousel-item v-for="(k, i) in cursor" :key="i">
+				<img :src="k" :lazy-src="k" style="width: 100%; height:100%;">
+			</v-carousel-item>
+		</v-carousel>
+    <div style="background-color: #7ec04c">
+      <div class="default-width">
+        <Empty v-if="!items.length && !query.keyword" :message="loadingType === 1 ? loadingText[loadingType] : '该招聘会暂无企业参与,前往其他招聘会看看吧~'" class="mt-3 py-15"></Empty>
+        <template v-else>
+          <div class="d-flex">
+            <div class="mt-3">
+              <EntCard :list="items" @selectChange="selectChange" />
+              <div :class="['loading', {'defaultLink-i': !loadingType}]" @click="handleChangePage">{{ loadingText[loadingType] }}</div>
+            </div>
+            <div class="position-details ml-3" style="flex: 1; overflow: hidden;">
+              <div class="position-content px-3">
+                <JobCard :enterpriseId="enterpriseId" :enterpriseName="enterpriseName" :jobFairId="route.params.id" />
+              </div>
+            </div>
+          </div>
+        </template>
+      </div>
+    </div>
+
+  </div>
+
+</template>
+
+<script setup>
+defineOptions({ name: 'jobFairPosition' })
+import buttons from '@/views/recruit/personal/components/buttons.vue'
+import { ref, reactive } from 'vue'
+import { getJobFairEnterprisePage, getJobFair } from '@/api/recruit/personal/jobFair'
+import EntCard from './components/entCard.vue'
+import JobCard from './components/jobCard.vue'
+import { useRoute, useRouter } from 'vue-router'; const route = useRoute(); const router = useRouter()
+import { formatName } from '@/utils/getText'
+import { dealDictArrayData } from '@/utils/position'
+
+const cursor = ref([
+  // "https://menduner.citupro.com:3443/dev/9de969f4723dd8819794fcd2d91b47a2f2b6f16993908712a53ee6a21f6735f1.jpg",
+  "https://minio.menduner.com/dev/fed8685fb4fec65347c2e3756db230ddd9c8f3538998c5678efe5acb51fb74e1.jpg"
+])
+const breadcrumbs = ref([
+  { text: '招聘会', path: '/recruit/personal/jobFair' },
+  { text: '', path: '', disabled: true }
+])
+const handleClick = (item) => {
+  if (!item.path || item.disabled) return
+  router.push(item.path)
+}
+// 招聘会详情-面包屑标题设置
+const getJobFairDetail = async () => {
+  const data = await getJobFair(route?.params?.id)
+  if (!data) return
+  // breadcrumbs.value[breadcrumbs.value.length - 1].text = data.title.replace(/<\/?p[^>]*>/gi, '')
+  document.title = data.title.replace(/<\/?p[^>]*>/gi, '')
+}
+getJobFairDetail()
+
+// 切换企业选中
+const enterpriseId = ref('')
+const enterpriseName = ref('')
+const selectChange = (val) => {
+	enterpriseId.value = val.id
+	enterpriseName.value = formatName(val.anotherName || val.name)
+}
+
+const query = reactive({
+  pageNo: 1,
+  pageSize: 10,
+	jobFairId: route.params.id,
+	keyword: ''
+})
+
+const items = ref([])
+const loadingText = ['加载更多', '加载中...', '没有更多数据了']
+const loadingType = ref(0)
+
+// 参与招聘会的企业
+const getList = async () => {
+	loadingType.value = 1
+	try {
+		const result = await getJobFairEnterprisePage(query)
+		const list = result?.list || []
+		if (list.length) {
+			items.value = items.value.concat(dealDictArrayData([], list))
+			loadingType.value = items.value.length === result.total ? 2 : 0
+			enterpriseId.value = items.value[0].id
+			enterpriseName.value = formatName(items.value[0].anotherName || items.value[0].name)
+		} else {
+			loadingType.value = 2
+		}
+	} catch {}
+}
+getList()
+
+const handleSearch = (val) => {
+	query.keyword = val
+	query.pageNo = 1
+	items.value = []
+	getList()
+}
+
+const handleChangePage = () => {
+  if (loadingType.value) return // 没有更多数据了
+  // 加载更多
+  query.pageNo++
+  getList()
+}
+</script>
+
+<style scoped lang="scss">
+.position-details {
+  position: sticky;
+  top: 62px;
+  border-radius: 12px;
+  // background-color: #fff;
+  margin-top: 12px;
+  height: calc(100vh - 127px);
+  widows: 100%;
+  overflow: hidden;
+  .position-content {
+    height: 100%;
+    width: 100%;
+    padding-right: 4px;
+    overflow-y: auto;
+  }
+}
+.loading {
+  margin-top: 8px;
+  text-align: center;
+  font-size: 13px;
+  color: gray;
+}
+.breadcrumbsText {
+  color: var(--color-999);
+  font-size: 15px;
+  &.active {
+    color: var(--v-primary-base);
+    cursor: pointer;
+  }
+}
+::-webkit-scrollbar {
+  width: 0;
+  height: 0;
+}
+::-webkit-scrollbar-thumb, .temporaryAdd ::-webkit-scrollbar-thumb, .details_edit ::-webkit-scrollbar-thumb {
+  // 滚动条-颜色
+  background: #c3c3c379;
+}
+::-webkit-scrollbar-track, .temporaryAdd ::-webkit-scrollbar-track, .details_edit ::-webkit-scrollbar-track {
+  // 滚动条-底色
+  background: #e5e5e58f;
+}
+</style>

+ 10 - 0
src/views/recruit/personal/jobFair/details/position.vue

@@ -0,0 +1,10 @@
+<!-- 职位 -->
+<template>
+  <div>vue3PageInit</div>
+</template>
+
+<script setup>
+defineOptions({name: 'jobFair-position'})
+</script>
+<style lang="scss" scoped>
+</style>

+ 51 - 0
src/views/recruit/personal/jobFair/index.vue

@@ -0,0 +1,51 @@
+<template>
+	<div class="default-width">
+		<buttons :current="3" style="position: sticky;" class="mx-4 mb-3"></buttons>
+		<div v-if="list.length > 0">
+			<div class="px-3 content">
+				<v-card elevation="5" v-for="val in list" :key="val.id">
+					<img :src="val.pcPreviewImg" alt="" style="object-fit: contain; width: 100%;">
+					<div class="pa-3">
+						<div class="color-666">活动时间:{{ timesTampChange(val.startTime, 'Y-M-D') }}至{{ timesTampChange(val.endTime, 'Y-M-D') }}</div>
+						<div class="text-end">
+							<v-btn color="primary" variant="outlined" @click.stop="handleJoin(val)">查看详情</v-btn>
+						</div>
+					</div>
+				</v-card>
+			</div>
+		</div>
+		<Empty v-else message="暂无进行中的招聘会,去看看其他吧~" />
+	</div>
+</template>
+
+<script setup>
+defineOptions({ name: 'jobFair' })
+import { ref } from 'vue'
+import { timesTampChange } from '@/utils/date'
+import { getJobFairList } from '@/api/recruit/personal/jobFair'
+import buttons from '@/views/recruit/personal/components/buttons.vue'
+import { useRouter } from 'vue-router'; const router = useRouter()
+
+const list = ref([])
+
+// 招聘会列表
+const getList = async () => {
+	const data = await getJobFairList()
+	list.value = data || []
+}
+getList()
+
+const handleJoin = (val) => {
+	if (!val?.id) return
+	window.open('/recruit/personal/jobFair/enterprises/' + val.id)
+}
+</script>
+
+<style scoped lang="scss">
+.content {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 12px;
+  min-height: auto;
+}
+</style>

+ 37 - 7
src/views/recruit/personal/position/components/details.vue

@@ -3,10 +3,11 @@
     <div class="banner px-6" id="share" :class="{'default-width': defaultWidth}">
       <div class="banner-title d-flex justify-space-between align-center">
         <div class="d-flex align-center justify-between">
-          <h1>{{ formatName(info.name) }}</h1>
           <svg-icon v-if="info?.hire" class="ml-5" name="pin" size="50"></svg-icon>
+          <svg-icon v-if="(info.source === '2' && info.bizId) || jobFairId" name="jobFair" class="mr-1" size="35"></svg-icon>
+          <h1>{{ formatName(info.name) }}</h1>
         </div>
-        <v-btn v-if="showContentRight" color="primary" variant="text" size="large" @click.stop="handleReturn" prepend-icon="mdi-chevron-triple-left">返回上一页</v-btn>
+        <v-btn v-if="showContentRight && !jobFairId" color="primary" variant="text" size="large" @click.stop="handleReturn" prepend-icon="mdi-chevron-triple-left">返回上一页</v-btn>
       </div>
       <div class="d-flex mt-1 justify-space-between align-center">
         <div class="banner-tags">
@@ -133,7 +134,7 @@
 
     <Loading :visible="loading"></Loading>
     <div v-if="Object.keys(info).length && Object.keys(positionInfo).length" style="position: absolute; left: -9999px; bottom: 0">
-      <PosterPage :id="id" :info="info" :positionInfo="positionInfo" ref="share"></PosterPage>
+      <PosterPage :id="id" :jobFairId="jobFairId" :info="info" :positionInfo="positionInfo" ref="share"></PosterPage>
     </div>
 
     <!-- 快速登录 -->
@@ -144,7 +145,7 @@
 <script setup>
 defineOptions({ name: 'position-details' })
 import { ref, computed } from 'vue'
-import { useRouter } from 'vue-router'
+import { useRouter, useRoute } from 'vue-router'
 import Snackbar from '@/plugins/snackbar'
 import html2canvas from 'html2canvas'
 import { useI18n } from '@/hooks/web/useI18n'
@@ -173,6 +174,7 @@ import { getUserAvatar } from '@/utils/avatar'
 import { checkPersonBaseInfo } from '@/utils/check'
 import dialogExtend from '@/plugins/dialogExtend'
 import { formatName } from '@/utils/getText'
+import { jobFairPositionDeliveryCheck } from '@/api/recruit/personal/jobFair'
 
 const emit = defineEmits(['preview'])
 const props = defineProps({
@@ -201,6 +203,7 @@ const props = defineProps({
 
 const { t } = useI18n()
 const router = useRouter()
+const route = useRoute()
 let { id } = props.propJobId ? { id: props.propJobId } : router.currentRoute.value.params
 if (id) id = id.toString()
 const delivery = ref(false) // 是否已投递简历
@@ -234,6 +237,9 @@ const formItems = ref({
   ]
 })
 
+// 招聘会id
+const jobFairId = ref(route?.query?.jobFairId)
+
 const nextFunc = ref(null) // 登录成功或强制填写个人信息成功后回调
 let loginCloseWarningWord = ''
 // 快速登录
@@ -316,7 +322,11 @@ getPositionDetail()
 
 // 效验是否有投递过简历
 const deliveryCheck = async () => {
-  const data = await jobCvRelCheckSend({ jobId: id })
+  // 区分招聘会职位与普通职位
+  const api = jobFairId.value ? jobFairPositionDeliveryCheck : jobCvRelCheckSend
+  const params = jobFairId.value ? { jobFairId: jobFairId.value, jobId: id } : { jobId: id }
+
+  const data = await api(params)
   if (data) delivery.value = true
 }
 if (getToken()) deliveryCheck()
@@ -400,7 +410,17 @@ const handleUploadSubmit = async () => {
   if (!obj.title || !obj.url) return
   loading.value = true
   await savePersonResumeCv(obj)
-  await jobCvRelSend({ jobId: id, title: obj.title, url: obj.url, type: info.value.hire ? 1 : 0 })
+
+  const params = {
+    jobId: id,
+    title: obj.title,
+    url: obj.url,
+    type: info.value.hire ? 1 : 0
+  }
+  // 如果是参与招聘会的职位,则传招聘会id
+  if (jobFairId.value) params.jobFairId = jobFairId.value
+  await jobCvRelSend(params)
+
   showUploadDialog.value = false
   setTimeout(() => {
     Snackbar.success(t('resume.deliverySuccess'))
@@ -453,7 +473,17 @@ const handleSubmit = async (val) =>{
   if (!obj) return Snackbar.warning(t('resume.selectedResumeNotExist'))
   handleClose()
   loading.value = true
-  await jobCvRelSend({ jobId: id, title: obj.title, url: obj.url, type: info.value.hire ? 1 : 0 })
+
+  const params = {
+    jobId: id,
+    title: obj.title,
+    url: obj.url,
+    type: info.value.hire ? 1 : 0
+  }
+  // 如果是参与招聘会的职位,则传招聘会id
+  if (jobFairId.value) params.jobFairId = jobFairId.value
+  await jobCvRelSend(params)
+
   setTimeout(async () => {
     Snackbar.success(t('resume.deliverySuccess'))
     await deliveryCheck()

+ 15 - 5
src/views/recruit/personal/position/components/poster.vue

@@ -13,7 +13,7 @@
           <div class="banner-tags">
             <div v-for="(k, i) in desc" :key="k.mdi">
               <span>
-                {{ k.value === 'areaName' ? !positionInfo.areaId ? '全国' : positionInfo.area?.str : positionInfo[k.value] }}
+                {{ k.value === 'areaName' ? !positionInfo.areaId ? '全国' : positionInfo.area?.str : positionInfo[k.value]}}
               </span>
               <span v-if="i !== desc.length - 1 && (positionInfo[k.value] || k.value === 'areaName')" class="septal-line"></span>
             </div>
@@ -74,16 +74,18 @@
 <script setup>
 defineOptions({name: 'recruit-personal-shareJob-index'})
 import { ref } from 'vue'
-import { getJobAdvertisedShareQrcode, getJobAdvertisedShare } from '@/api/position'
+import { getJobAdvertisedShareQrcode } from '@/api/position'
+import { saveShareQuery } from '@/api/recruit/personal/jobFair'
 import { formatName } from '@/utils/getText'
-// import { timesTampChange } from '@/utils/date'
 
 // 职位详情
 const props = defineProps({
   info: Object,
   positionInfo: Object,
-  id: String
+  id: String,
+  jobFairId: [String, Number] // 招聘会id
 })
+
 // 富文本内容处理,去除多余的换行空格等
 const cleanedHtml = (text) => {
   let cleaned = text.replace(/\n/g, '</br>')
@@ -99,7 +101,15 @@ const url = ref('')
 // 获取二维码
 const getQrCode = async () => {
   // 先将需要的参数传递给后端,后端返回一个id,再根据id获取二维码,小程序端根据scene中的id获取分享的职位id与推荐人id
-  const result = await getJobAdvertisedShare({ jobId: props.id, sharedById: userInfo?.id })
+  
+  const params = {
+    jobId: props.id,
+    sharedById: userInfo?.id
+  }
+  // 参与招聘会的职位则需要传递招聘会id
+  if (props.jobFairId) params.jobFairId = props.jobFairId
+
+  const result = await saveShareQuery(params)
   const query = {
     scene: 'id=' + result,
     path: 'pagesB/positionDetail/index',

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません