瀏覽代碼

企业-生成分享海报

Xiao_123 3 月之前
父節點
當前提交
f27657d079

+ 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>

+ 237 - 23
src/views/recruit/enterprise/jobFair/details.vue

@@ -1,37 +1,251 @@
 <template>
-  <v-card class="card-box pa-5">
-    <!-- <v-tabs v-model="tab" align-tabs="center" color="primary" bg-color="#f7f8fa" class="mb-3">
-      <v-tab v-for="(tab, index) in jobFairDetailsJob" :key="index" :value="index">{{ tab.title }}</v-tab>
-    </v-tabs>
-    <component :is="jobFairDetailsJob[tab].component" :id="id"></component> -->
+  <v-card class="card-box pa-4">
+    <div class="position-relative">
+      <div class="text-end mb-3">
+        <v-btn color="primary" @click="handleAdd">新增职位</v-btn>
+        <v-btn color="primary" class="mx-3" variant="outlined" @click="handleJoin">选择已发布的职位加入招聘会</v-btn>
+        <v-btn color="primary" v-if="bgImg" variant="outlined" prepend-icon="mdi-share-all" @click="handleShare">我的分享海报</v-btn>
+      </div>
+      <JobItem :items="jobList" @refresh="getJobList"></JobItem>
 
-    <jobFairDetailsJob :id="id" />
+      <v-navigation-drawer v-model="showDrawer" location="right" temporary width="600">
+        <Loading :visible="positionLoading" :contained="true"></Loading>
+        <div class="resume-box">
+          <div class="resume-header">
+            <div class="resume-title mr-5">已发布职位</div>
+          </div>
+        </div>
+        <div class="px-3">
+          <v-text-field
+            v-model="positionSearch"
+            append-inner-icon="mdi-magnify"
+            density="compact"
+            :label="t('position.positionName')"
+            variant="outlined"
+            hide-details
+            color="primary"
+            single-line
+            @click:append-inner="getPositionList"
+            @keyup.enter="getPositionList"
+          ></v-text-field>
+        </div>
+        <div class="pa-3" v-if="positionItems.length">
+          <div v-for="val in positionItems" :key="val.id" class="itemBox mb-3" style="height: 80px;">
+            <div class="d-flex justify-space-between" style="padding: 10px 20px;">
+              <div class="position">
+                <div class="d-flex align-center justify-space-between">
+                  <span class="position-name">{{ formatName(val.name) }}</span>
+                  <div>
+                    <v-btn size="small" color="primary" @click="handleTo(val)">添加至招聘会</v-btn>
+                  </div>
+                </div>
+                <div :class="['mt-3', 'other-info', 'ellipsis']">
+                  <span>{{ val.areaName ? val.area?.str : '全国' }}</span>
+                  <span class="lines" v-if="val.eduName"></span>
+                  <span>{{ val.eduName }}</span>
+                  <span class="lines"></span>
+                  <span>{{ val.expName }}</span>
+                  <span class="lines"></span>
+                  <span v-if="!val.payFrom && !val.payTo">面议</span>
+                  <span v-else>{{ val.payFrom ? val.payFrom + '-' : '' }}{{ val.payTo }}{{ val.payName ? '/' + val.payName : '' }}</span>
+                  <span class="lines" v-if="val.positionName"></span>
+                  <span>{{ val.positionName }}</span>
+                </div>
+              </div>
+            </div>
+          </div>
+          <CtPagination
+            v-if="total"
+            :total="positionTotal"
+            :page="positionPageInfo.pageNo"
+            :limit="positionPageInfo.pageSize"
+            @handleChange="handleChangePage"
+          ></CtPagination>
+        </div>
+        <Empty v-else :elevation="false"></Empty>
+      </v-navigation-drawer>
+
+      <div class="hideCanvasView">
+        <JobFairEntShare 
+          :show="showShare" 
+          :enterpriseName="enterpriseName" 
+          :logoUrl="logoUrl" 
+          :positionList="positionList" 
+          :bgImg="bgImg" 
+          @success="handlePreview"
+        ></JobFairEntShare>
+      </div>
+    </div>
   </v-card>
+
+  <PreviewImage v-if="showPreview" :urlList="[previewSrc]" :fileName="enterpriseName" @close="showPreview = !showPreview, showShare = false" />
 </template>
 
 <script setup>
-defineOptions({ name: 'jobFairDetails' })
-// import { shallowRef, ref } from 'vue'
-import { useRouter } from 'vue-router'
-import JobFairDetailsJob from './components/job.vue'
-// import JobFairDetailsResume from './components/resume.vue'
+defineOptions({ name: 'jobFairJob'})
+import { ref } from 'vue'
+import { getJobFairPosition, getJobFair } from '@/api/recruit/enterprise/jobFair'
+import { dealDictArrayData } from '@/utils/position.js'
+import JobItem from './job/item.vue'
+import { useRouter, useRoute } from 'vue-router'
+import { useI18n } from '@/hooks/web/useI18n'
+import Snackbar from '@/plugins/snackbar'
+import { getEnterprisePubJobTypePermission } from '@/api/recruit/enterprise/position'
+import { getJobAdvertisedList } from '@/api/position'
+import { formatName } from '@/utils/getText'
+import JobFairEntShare from '@/views/recruit/components/jobFairEntShare'
+
 const router = useRouter()
+const route = useRoute()
+const { id } = route.params
+const { t } = useI18n()
+
+// 职位列表
+const jobList = ref([])
+
+const positionItems = ref([])
+const positionTotal = ref(0)
+const positionLoading = ref(false)
+const total = ref(0)
+const positionPageInfo = ref({
+  pageSize: 10,
+  pageNo: 1,
+})
+const positionSearch = ref('')
+
+// 分享海报
+const entBaseInfo = ref(localStorage.getItem('entBaseInfo') ? JSON.parse(localStorage.getItem('entBaseInfo')) : {})
+const showShare = ref(false)
+const showPreview = ref(false)
+const enterpriseName = ref(formatName(entBaseInfo.value.enterpriseAnotherName || entBaseInfo.value.enterpriseName))
+const logoUrl = ref(entBaseInfo.value.logoUrl)
+const previewSrc  = ref('')
+const positionList = ref([])
+const bgImg = ref('')
+
+// 职位列表
+const getJobList = async () => {
+  const data = await getJobFairPosition(id)
+  if (!data || !data.length) return jobList.value = []
+  jobList.value = dealDictArrayData([], data)
+}
 
-// const jobFairDetailsJob = shallowRef([
-//   {
-//     title: '职位',
-//     component: JobFairDetailsJob
-//   },
-//   {
-//     title: '投递简历',
-//     component: JobFairDetailsResume
-//   }
-// ])
+const handleAdd = async () => {
+  const data = await getEnterprisePubJobTypePermission()
+  if (!data || !data.length) return Snackbar.warning('没有该操作权限,请联系平台管理员升级后再试')
+  router.push(`/recruit/enterprise/jobFair/details/${id}/edit`)
+}
 
-const id = router.currentRoute.value.params.id
-// const tab = ref(0)
+const showDrawer = ref(false)
+const handleJoin = async () => {
+  getPositionList()
+  showDrawer.value = true
+}
 
+const handleChangePage = (index) => {
+  positionPageInfo.value.pageNo = index
+  getPositionList()
+}
+
+const handleTo = (val) => {
+  router.push(`/recruit/enterprise/jobFair/details/${id}/edit?id=${val.id}`)
+}
+
+// 获取职位列表
+const getPositionList = async () => {
+  positionLoading.value = true
+  const query = {
+    ...positionPageInfo.value,
+    status: 0,
+    hasExpiredData: false,
+    hire: false
+  }
+  if ( positionSearch.value) {
+    Object.assign(query, {
+      name: positionSearch.value,
+    })
+  }
+  try {
+    const { list, total } = await getJobAdvertisedList(query)
+    positionTotal.value = total
+    positionItems.value = list.length ? dealDictArrayData([], list) : []
+  } finally {
+    positionLoading.value = false
+  }
+}
+getJobList()
+
+const getJob = async () => {
+  const data = await getJobFair(id)
+  if (!data) return
+  bgImg.value = data.contentImg
+}
+getJob()
+
+// 分享海报预览
+const handlePreview = (val) => {
+  if (!val) return
+  previewSrc.value = val
+  showPreview.value = true
+}
+
+const handleShare = () => {
+  positionList.value = jobList.value && jobList.value.length > 0 ? jobList.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;
+}
+.resume-box {
+  padding-top: 120px;
+}
+
+.itemBox {
+  position: relative;
+  border: 1px solid #e5e6eb;
+}
+.position-name {
+  color: var(--color-333);
+  font-size: 19px;
+}
+.position {
+  width: 100%;
+  position: relative;
+  .item-select {
+    position: absolute;
+    left: -8px;
+    top: -13px;
+  }
+}
+.lines {
+  display: inline-block;
+  width: 1px;
+  height: 17px;
+  vertical-align: middle;
+  background-color: #e0e0e0;
+  margin: 0 10px;
+}
+.other-info {
+  font-size: 15px;
+  color: var(--color-666);
+}
+.bottom {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  width: 100%;
+  height: 40px;
+  background-color: #f7f8fa;
+  font-size: 14px;
+  color: var(--color-888);
+}
+.actions:hover {
+  color: var(--v-primary-base);
+}
 </style>

+ 1 - 2
src/views/recruit/personal/jobFair/details/components/jobCard1.vue

@@ -44,7 +44,6 @@
 <script setup>
 defineOptions({ name: 'jobCard' })
 import { formatName } from '@/utils/getText'
-import { jumpToEnterpriseDetail } from '@/utils/position'
 import Snackbar from '@/plugins/snackbar'
 
 const props = defineProps({
@@ -68,7 +67,7 @@ const handleClick = (id) => {
 
 const handleClickEnterprise = (id) => {
 	if (props.isMobile) return Snackbar.warning('请复制链接后在电脑端查看企业招聘职位')
-	jumpToEnterpriseDetail(id, true)
+	window.open(`/recruit/personal/jobFair/entPosition/${props.jobFairId}?enterpriseId=${id}`)
 }
 </script>
 

+ 45 - 11
src/views/recruit/personal/jobFair/details/entJobCard.vue

@@ -1,15 +1,11 @@
 <!-- 企业 -->
 <template>
-  <div>
-    <!-- 轮播 -->
-    <!-- <v-carousel v-if="jobFair?.pcHeadImg && jobFair?.pcHeadImg.length > 0" :show-arrows="jobFair?.pcHeadImg.length > 1 ? 'hover' : false" cycle :hide-delimiters="true" style="height: 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 :style="{'background-color': jobFair.backgroundColour || '#fff', 'min-height': jobFair?.pcHeadImg && jobFair?.pcHeadImg.length > 0 ? 'calc(100vh - 500px)' : '100vh'}"> -->
-    <div :style="{'background-color': jobFair.backgroundColour || '#fff', 'min-height': '100vh'}">
+  <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>
@@ -19,7 +15,20 @@
         </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>
@@ -31,6 +40,7 @@ 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,
@@ -39,6 +49,12 @@ const query = reactive({
   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
@@ -80,22 +96,40 @@ const getJobFairDetail = async () => {
 
   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;

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

@@ -14,7 +14,7 @@
 		</v-carousel>
 
     <!-- 招聘会分享按钮 -->
-    <div class="position-fixed" style="right: 24px; top: 80px;">
+    <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>
 
@@ -164,7 +164,7 @@ const handleShare = () => {
   const canvas = shareCanvas.value
   const ctx = canvas.getContext('2d')
   const img = new Image()
-  img.setAttribute('crossOrigin', 'anonymous')
+  img.crossOrigin = 'anonymous'
   img.onload = async () => {
     //清空画布
     ctx.clearRect(0, 0, canvasWidth, canvasHeight)
@@ -174,7 +174,7 @@ const handleShare = () => {
 
     // 分享二维码
     const secondImg = new Image()
-    secondImg.setAttribute('crossOrigin', 'anonymous')
+    secondImg.crossOrigin = 'anonymous'
     secondImg.onload = () => {
       // 计算第二张图片的位置和大小(这里以示例为准,您可以根据需要调整)
       const secondImgWidth = Math.min(secondImg.width, canvas.width / 4)