Procházet zdrojové kódy

实习记录:实习证书&企业推荐信

Xiao_123 před 2 měsíci
rodič
revize
02b72fc44c

+ 8 - 0
src/api/recruit/personal/student.js

@@ -30,4 +30,12 @@ export const practiceProcess = async (data) => {
 		url: '/app-api/flames/student/practice/process',
 		data
 	})
+}
+
+// 获得学生实习记录分页
+export const getStudentPracticePage = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/student/page',
+		params
+	})
 }

+ 17 - 17
src/router/modules/components/recruit/personCenter.js

@@ -200,7 +200,7 @@ const personCenter = [
                 path: '/recruit/personal/personalCenter/student/internshipCompany',
                 component: () => import('@/views/recruit/personal/PersonalCenter/student/intershipCompany/index.vue'),
                 meta: {
-                  title: '实习企业',
+                  title: '实习记录',
                   enName: 'Internship Company'
                 }
               },
@@ -212,22 +212,22 @@ const personCenter = [
                   enName: 'Internship Report'
                 }
               },
-              {
-                path: '/recruit/personal/personalCenter/student/internshipCertificate',
-                component: () => import('@/views/recruit/personal/PersonalCenter/student/InternshipCertificate/index.vue'),
-                meta: {
-                  title: '实习证书',
-                  enName: 'Internship Certificate'
-                }
-              },
-              {
-                path: '/recruit/personal/personalCenter/student/enterpriseRecommendationLetter',
-                component: () => import('@/views/recruit/personal/PersonalCenter/student/EnterpriseRecommendationLetter/index.vue'),
-                meta: {
-                  title: '企业推荐信',
-                  enName: 'Recommendation Letter'
-                }
-              },
+              // {
+              //   path: '/recruit/personal/personalCenter/student/internshipCertificate',
+              //   component: () => import('@/views/recruit/personal/PersonalCenter/student/InternshipCertificate/index.vue'),
+              //   meta: {
+              //     title: '实习证书',
+              //     enName: 'Internship Certificate'
+              //   }
+              // },
+              // {
+              //   path: '/recruit/personal/personalCenter/student/enterpriseRecommendationLetter',
+              //   component: () => import('@/views/recruit/personal/PersonalCenter/student/EnterpriseRecommendationLetter/index.vue'),
+              //   meta: {
+              //     title: '企业推荐信',
+              //     enName: 'Recommendation Letter'
+              //   }
+              // },
               {
                 path: '/recruit/personal/personalCenter/student/internshipButler',
                 component: () => import('@/views/recruit/personal/PersonalCenter/student/InternshipButler/index.vue'),

+ 114 - 16
src/views/recruit/personal/PersonalCenter/student/intershipCompany/index.vue

@@ -1,43 +1,75 @@
 <template>
-	<v-tabs v-model="tab" align-tabs="start" color="primary" bg-color="#f7f8fa" @update:modelValue="handleChangeTab">
-    <v-tab v-for="(k, index) in tabList" :key="index" :value="k.value">{{ k.label }}</v-tab>
-  </v-tabs>
+	<div class="position-relative">
+		<v-tabs v-model="tab" align-tabs="start" color="primary" bg-color="#f7f8fa" @update:modelValue="handleChangeTab">
+			<v-tab v-for="(k, index) in tabList" :key="index" :value="k.value">{{ k.label }}</v-tab>
+		</v-tabs>
 
-	<div v-if="items?.length">
-		<ItemPage class="mt-3" :items="items" />
-		<CtPagination
-      v-if="total > 0"
-      :total="total"
-      :page="query.pageNo"
-      :limit="query.pageSize"
-      @handleChange="handleChangePage"
-    ></CtPagination>
+		<div v-if="items?.length">
+			<ItemPage class="mt-3" :items="items" @preview="handlePreview" />
+			<CtPagination
+				v-if="total > 0"
+				:total="total"
+				:page="query.pageNo"
+				:limit="query.pageSize"
+				@handleChange="handleChangePage"
+			></CtPagination>
+		</div>
+		<Empty v-else :elevation="false" />
+
+		<!-- 生成实习证书 -->
+		<div class="position-absolute position-relative" style="left: -9999px; bottom: 0;" ref="share">
+			<img src="https://minio.citupro.com/dev/static/bgc.jpg" width="500" height="700" cover />
+			<div class="cer-introduce">
+				兹有
+				<span class="cer-text">{{ itemData?.student?.schoolName }}</span>
+        <span class="cer-text">{{ itemData?.student?.majorName }}</span>
+        专业<span class="cer-text">{{ itemData?.person?.name }}</span>
+        同学于<span class="cer-text">{{ itemData?.startTime ? timesTampChange(itemData?.startTime, 'Y-M-D') : '' }}</span>
+        至<span class="cer-text">{{ itemData?.endTime ? timesTampChange(itemData?.endTime, 'Y-M-D') : '' }}</span>
+        在<span class="cer-text">{{ formatName(itemData?.enterprise?.anotherName || itemData?.enterprise?.name) }}</span>
+        实习。
+			</div>
+			<div class="cer-comment">{{ itemData?.evaluate }}</div>
+			<div class="cer-prove">特此证明。</div>
+			<div class="cer-end">
+        <div>{{ itemData?.createTime ? timesTampChange(itemData?.createTime, 'Y-M-D') : '' }}</div>
+      </div>
+		</div>
 	</div>
-	<Empty v-else :elevation="false" />
+
+	<Loading :visible="showLoading"></Loading>
 </template>
 
 <script setup>
 // 实习企业
 defineOptions({ name: 'PersonalCenterStudentInternshipCompany'})
 import { ref, onMounted } from 'vue'
-import { getInternshipPage } from '@/api/recruit/personal/student.js'
+import { getStudentPracticePage } from '@/api/recruit/personal/student.js'
 import ItemPage from './item.vue'
 import { getDict } from '@/hooks/web/useDictionaries'
 import { dealDictObjData } from '@/utils/position'
+import { usePersonCenterStore } from '@/store/personCenter'
+import { formatName } from '@/utils/getText'
+import { timesTampChange } from '@/utils/date'
+import Snackbar from '@/plugins/snackbar'
+import html2canvas from 'html2canvas'
+import { DPR } from '@/utils'
 
 const tab = ref(0)
 const tabList = ref([])
 const items = ref([])
+const share = ref(null)
 const total = ref(0)
 const query = ref({
 	pageNo: 1,
 	pageSize: 10,
 	status: ''
 })
+const itemData = ref({})
 
 const getList = async () => {
 	query.value.status = tabList.value[tab.value]?.value
-	const result = await getInternshipPage(query.value)
+	const result = await getStudentPracticePage(query.value)
 	items.value = result?.list.map(e => {
 		e.enterprise = dealDictObjData({}, e.enterprise)
 		e.job = dealDictObjData({}, e.job)
@@ -62,8 +94,74 @@ const handleChangePage = (page) => {
   query.value.pageNo = page
 	getList()
 }
+
+// 生成实习证书图片
+const showLoading = ref(false)
+const personCenterStore = usePersonCenterStore()
+const generateAndDownloadImage = async () => {
+  try {  
+    const canvas = await html2canvas(share.value, { scale: DPR(), useCORS: true })
+    const image = canvas.toDataURL().replace(/^data:image\/(png|jpg);base64,/, '')
+
+		const fileName = `实习证书 - ${formatName(itemData.value?.enterpriseName)}`
+		personCenterStore.setPreviewData([`data:image/png;base64,${image}`], 0, fileName)
+    showLoading.value = false
+  } catch (error) {
+    console.error('图片生成失败', error)
+		Snackbar.warning('加载失败,请稍后重试')
+		showLoading.value = false
+  }
+}
+
+// 实习证书预览
+const handlePreview = (item) => {
+	itemData.value = item
+	if (!share.value) return
+	showLoading.value = true
+	setTimeout(() => {
+		generateAndDownloadImage()
+	}, 1000)
+}
 </script>
 
 <style scoped lang="scss">
-
+.cer-text{
+  text-decoration: underline;
+  margin: 0 3px;
+	font-weight: 700;
+}
+.cer-introduce{
+  width: 70%;
+  position: absolute;
+  top: 51%;
+  left: 50%;
+  transform: translate(-50%,-50%);
+  text-indent: 2em;
+}
+.cer-comment{
+	width: 70%;
+  position: absolute;
+  top: 68%;
+  left: 50%;
+  transform: translate(-50%,-50%);
+	text-indent: 2em;
+	display: -webkit-box;
+	-webkit-box-orient: vertical;
+	-webkit-line-clamp: 3;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+.cer-prove{
+  width: 70%;
+  position: absolute;
+  top: 82%;
+  left: 50%;
+  transform: translate(-50%,-50%);
+  text-indent: 2em;
+}
+.cer-end{
+  position: absolute;
+  top: 87%;
+  right: 16%;
+}
 </style>

+ 44 - 2
src/views/recruit/personal/PersonalCenter/student/intershipCompany/item.vue

@@ -42,8 +42,23 @@
 				<div class="color-666 font-size-15">
 					<p>实习时间:{{ timesTampChange(val.startTime, 'Y-M-D') }} 至 {{ timesTampChange(val.endTime, 'Y-M-D') }}</p>
 				</div>
-				<div class="text-end" v-if="val.status === '1'">
-					<v-btn size="small" color="primary" @click="handleToReport(val)">实习报告</v-btn>
+				<div class="text-end">
+					<v-btn v-if="val.status === '1'" size="small" color="warning" @click="handleToReport(val)">实习报告</v-btn>
+					<v-btn v-if="val.evaluate && !val.certificate" size="small" class="ml-3" color="primary" @click.stop="handlePreview(val)">实习证书</v-btn>
+          <v-menu v-else open-on-hover>
+            <template v-slot:activator="{ props }">
+              <v-btn color="primary" size="small" class="ml-3" v-bind="props">实习证书</v-btn>
+            </template>
+            <v-list>
+              <v-list-item v-for="(item, index) in menuList" :key="index" @click="item.change(val)">
+                <template v-slot:prepend>
+                  <v-icon :icon="item.icon"></v-icon>
+                </template>
+                <v-list-item-title>{{ item.title }}</v-list-item-title>
+              </v-list-item>
+            </v-list>
+          </v-menu>
+					<v-btn v-if="val.recommendationLetter" @click.stop="handleDownLoadRecommendationLetter(val)" size="small" class="ml-3" color="#00897B" prepend-icon="mdi-download">企业推荐信</v-btn>
 				</div>
 			</div>
     </div>
@@ -52,11 +67,14 @@
 
 <script setup>
 defineOptions({ name: 'PersonalCenterStudentInternshipCompanyItem' })
+import { ref } from 'vue'
 import { useRouter } from 'vue-router'
 import { formatName } from '@/utils/getText'
 import { jumpToEnterpriseDetail } from '@/utils/position'
 import { timesTampChange } from '@/utils/date'
+import { getBlob, saveAs } from '@/utils'
 
+const emit = defineEmits(['preview'])
 const props = defineProps({
   items: {
     type: Array,
@@ -68,6 +86,23 @@ const router = useRouter()
 
 const desc = ['industryName', 'scaleName']
 
+// 实习证书预览
+const handlePreview = (item) => {
+  emit('preview', item)
+}
+
+// 实习证书附件下载
+const handleDownLoadCertificate = (val) => {
+  getBlob(val.certificate).then(blob => {
+    saveAs(blob, `${formatName(val.enterprise.anotherName || val.enterprise.name)} - 实习证书附件`)
+  })
+}
+
+const menuList = ref([
+  { title: '证书预览', icon: 'mdi-eye-outline', change: handlePreview },
+  { title: '附件下载', icon: 'mdi-download', change: handleDownLoadCertificate }
+])
+
 // 职位详情
 const handleToPositionDetails = (item) => {
   if (item.job.status === '1') return
@@ -81,6 +116,13 @@ const handleToReport = (val) => {
 	console.log(val, 'report')
 	router.push(`/recruit/personal/personalCenter/student/internshipReport?id=${val.enterprise.id}`)
 }
+
+// 企业推荐信下载
+const handleDownLoadRecommendationLetter = (val) => {
+  getBlob(val.recommendationLetter).then(blob => {
+    saveAs(blob, `${formatName(val.enterprise.anotherName || val.enterprise.name)} - 推荐信`)
+  })
+}
 </script>
 
 <style scoped lang="scss">