Przeglądaj źródła

Merge branch 'groupJobFair' of https://git.citupro.com/zhengnaiwen_citu/menduner into groupJobFair

Xiao_123 2 miesięcy temu
rodzic
commit
b3ea9e6bda

+ 106 - 2
src/views/recruit/teacher/internshipReport/index.vue

@@ -1,10 +1,114 @@
-<!--  -->
+<!-- 实习报告 -->
 <template>
-  <div>实习报告</div>
+  <v-card class="px-3">
+    <!-- 筛选条件 -->
+    <div class="d-flex justify-space-between mt-8 mb-10">
+      <div class="d-flex align-center">
+        <!-- <span class="mx-3 color-666 font-size-14">院系</span> -->
+        <Autocomplete class="mr-3" v-model="query.collegeId" :item="yuanXi"></Autocomplete>
+        <v-btn color="primary" class="half-button ml-3" @click="handleSearch()">查 询</v-btn>
+        <v-btn class="half-button ml-3" prepend-icon="mdi-refresh" variant="outlined" color="primary" @click="handleSearch(true)">刷 新</v-btn>
+      </div>
+      <v-btn :loading="exportLoading" prepend-icon="mdi-export-variant" color="primary" variant="tonal" class="ml-3" @click="null">导出</v-btn>
+    </div>
+    
+    <!-- 列表 -->
+    <div class="mt-5" style="min-height: 500px;">
+      <CtTable
+        :items="tableData"
+        :headers="headers"
+        :loading="loading"
+        :elevation="0"
+        :is-tools="false"
+        :showPage="true"
+        :total="total"
+        :page-info="query"
+        itemKey="id"
+        @pageHandleChange="handleChangePage"
+      >
+        <template #studentName="{ item }">
+          <div class="d-flex align-center">
+            <v-avatar size="40" :image="getUserAvatar(item?.person?.avatar, item?.person?.sex)"></v-avatar>
+            <span class="ml-3">{{ item?.person?.name }}</span>
+          </div>
+        </template>
+        <template #actions="{ item }">
+          <v-btn v-if="!item?.recommendationLetter" color="primary" variant="text" @click="handleUploadLetter(item.id)">上传推荐信</v-btn>
+          <v-btn v-if="!item?.evaluate" color="#00897B" variant="text" @click="handleIssueCertificate(item.id)">颁发实习证书</v-btn>
+        </template>
+      </CtTable>
+      <!-- <Loading :visible="loading"></Loading> -->
+    </div>
+  </v-card >
 </template>
 
 <script setup>
 defineOptions({name: 'internship-report'})
+import { ref } from 'vue'
+import Snackbar from '@/plugins/snackbar'
+import { formatName } from '@/utils/getText'
+
+const loading = ref(false)
+const query = ref({
+  pageSize: 20,
+  pageNo: 1,
+  collegeId: null,
+})
+
+const headers = [
+  { title: '学生姓名', key: 'studentName', sortable: false },
+  { title: '学生学号', key: 'studentName', sortable: false },
+  { title: '所属专业', key: 'studentName', sortable: false },
+  { title: '录用企业', key: 'studentName', sortable: false, value: item => formatName(item.studentName) },
+  { title: '录用部门', key: 'studentName', sortable: false, value: item => formatName(item.studentName) },
+  { title: '录用岗位', key: 'studentName', sortable: false, value: item => formatName(item.studentName) },
+  { title: '操作', key: 'actions', sortable: false }
+]
+
+const tableData = ref([{ studentName: '123'}])
+const total = ref(0)
+// 列表
+const getData = async (isRefresh = false) => {
+  // const { list, total: number } = await getInterviewInvitePage(query.value)
+  // tableData.value = list
+  // total.value = number
+  if (isRefresh) Snackbar.success('刷新成功')
+}
+
+const handleChangePage = (val) => {
+	query.value.pageNo = val
+	getData()
+}
+
+const handleSearch = (refresh = false) => {
+  query.value.pageNo = 1
+  getData(refresh)
+}
+
+const yuanXi = ref({ width: 300, items: [], clearable: false, hideDetails: true, label: '请选择院系' })
+// 列表
+const getYuanXiItem = async () => {
+  // const { list } = await getInterviewInvitePage(query.value)
+  yuanXi.value.items = [
+    { label: '中文系', value: '中文系' },
+    { label: '人文学院', value: '人文学院' },
+  ]
+  if (yuanXi.value.items?.length) {
+    query.value.collegeId = yuanXi.value.items[0].value
+    getData()
+  }
+}
+getYuanXiItem()
+
+const exportLoading = ref(false)
 </script>
 <style lang="scss" scoped>
+.title {
+  color: var(--color-333);
+  font-weight: 600;
+  font-size: 16px;
+}
+.left {
+  min-width: 200px;
+}
 </style>

+ 73 - 0
src/views/recruit/teacher/internshipSituation/CertificateForm.vue

@@ -0,0 +1,73 @@
+<template>
+	<div>
+		<CtForm ref="CtFormRef" class="mt-3" :items="formItems">
+			<template #uploadFile="{ item }">
+				<TextInput v-model="item.value" :item="item" @click="openFileInput"></TextInput>
+				<File ref="uploadFile" @success="handleUploadResume"></File>
+			</template>
+		</CtForm>
+		<div class="color-666" style="font-size: 13px;">* 仅支持DOC、DOCX、PDF文件且大小不能超过20MB</div>
+	</div>
+</template>
+
+<script setup>
+defineOptions({ name: 'CertificateForm'})
+import { ref } from 'vue'
+
+const CtFormRef = ref()
+const formItems = ref({
+  options: [
+    {
+      type: 'textarea',
+      key: 'evaluate',
+      value: null,
+      rows: 5,
+      counter: 60,
+      label: '点评内容 *',
+      outlined: true,
+      rules: [v => !!v || '点评不能为空']
+    },
+    {
+      slotName: 'uploadFile',
+      key: 'certificate',
+      value: '',
+      truthValue: '',
+      label: '点击上传附件 *',
+      outline: true,
+      accept: '.doc, .docx, .pdf',
+      prependInnerIcon: 'mdi-file-document-outline',
+      rules: [v => !!v || '附件不能为空']
+    }
+  ]
+})
+
+// 选择文件
+const uploadFile = ref()
+const openFileInput = () => {
+  uploadFile.value.trigger()
+}
+
+// 上传附件
+const handleUploadResume = async (url, title, filename) => {
+  const obj = formItems.value.options.find(e => e.key === 'certificate')
+  obj.value = filename
+  obj.truthValue = url
+}
+
+const getQuery = () => {
+	let obj = {}
+	formItems.value.options.forEach(e => {
+		obj[e.key] = e.truthValue || e.value
+	})
+	return obj
+}
+
+defineExpose({
+	getQuery,
+	CtFormRef
+})
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 61 - 0
src/views/recruit/teacher/internshipSituation/RecommendationLetterForm.vue

@@ -0,0 +1,61 @@
+<template>
+	<CtForm ref="CtFormRef" class="mt-3" :items="formItems">
+    <template #uploadFile="{ item }">
+      <TextInput v-model="item.value" :item="item" @click="openFileInput"></TextInput>
+      <File ref="uploadFile" @success="handleUploadResume"></File>
+    </template>
+  </CtForm>
+  <div class="color-666" style="font-size: 13px;">* 仅支持DOC、DOCX、PDF文件且大小不能超过20MB</div>
+</template>
+
+<script setup>
+defineOptions({ name: 'RecommendationLetterForm'})
+import { ref } from 'vue'
+
+const CtFormRef = ref()
+const formItems = ref({
+  options: [
+    {
+      slotName: 'uploadFile',
+      key: 'url',
+      value: '',
+      truthValue: '',
+      label: '点击上传推荐信 *',
+      outline: true,
+      accept: '.doc, .docx, .pdf',
+      prependInnerIcon: 'mdi-file-document-outline',
+      rules: [v => !!v || '推荐信不能为空']
+    }
+  ]
+})
+
+// 选择文件
+const uploadFile = ref()
+const openFileInput = () => {
+  uploadFile.value.trigger()
+}
+
+// 上传附件
+const handleUploadResume = async (url, title, filename) => {
+  const obj = formItems.value.options.find(e => e.key === 'url')
+  obj.value = filename
+  obj.truthValue = url
+}
+
+const getQuery = () => {
+	let obj = {}
+	formItems.value.options.forEach(e => {
+		obj[e.key] = e.truthValue
+	})
+	return obj
+}
+
+defineExpose({
+	getQuery,
+	CtFormRef
+})
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 225 - 3
src/views/recruit/teacher/internshipSituation/index.vue

@@ -1,10 +1,232 @@
-<!--  -->
+<!-- 实习情况 -->
 <template>
-  <div>实习情况</div>
+	<v-card class="card-box pa-3">
+		<div class="d-flex justify-space-between align-center">
+			<!-- 统计 -->
+			<div class="d-flex align-center statistics">
+				<div v-for="(val, index) in statistics" :key="index" class="statistics-card pa-5">
+					<div class="color-666">{{ val.label }}</div>
+					<div class="">
+						<span class="value font-weight-bold color-primary">{{ val.count }}</span>
+						<span class="color-999 font-size-14">人</span>
+					</div>
+				</div>
+			</div>
+			<!-- 时间范围筛选 -->
+			<div class="d-flex align-center ml-7">
+        <span class="color-666">实习日期</span>
+        <div class="ml-5">
+          <date-picker 
+						v-model="date"
+						:item="{
+							mode: 'daterange',
+							clearable: true,
+							placeholder: '请选择要查看的时间范围',
+							format: 'YYYY/MM/DD',
+							width: 250
+						}"
+						@change="handleChangeDate"
+					/>
+        </div>
+      </div>
+		</div>
+
+		<div class="mt-5">
+			<CtTable
+        :items="tableData"
+        :headers="headers"
+        :loading="loading"
+        :elevation="0"
+        :is-tools="false"
+        :showPage="true"
+        :total="total"
+        :page-info="query"
+        itemKey="id"
+        @pageHandleChange="handleChangePage"
+      >
+        <template #studentName="{ item }">
+          <div class="d-flex align-center">
+            <v-avatar size="40" :image="getUserAvatar(item?.person?.avatar, item?.person?.sex)"></v-avatar>
+            <span class="ml-3">{{ item?.person?.name }}</span>
+          </div>
+        </template>
+        <template #actions="{ item }">
+          <v-btn v-if="!item?.recommendationLetter" color="primary" variant="text" @click="handleUploadLetter(item.id)">上传推荐信</v-btn>
+          <v-btn v-if="!item?.evaluate" color="#00897B" variant="text" @click="handleIssueCertificate(item.id)">颁发实习证书</v-btn>
+        </template>
+      </CtTable>
+		</div>
+	</v-card>
+
+	<!-- 上传推荐信 -->
+	<CtDialog :visible="showLitterDialog" :widthType="2" titleClass="text-h6" title="上传推荐信" @close="handleLetterClose" @submit="handleSubmitLetter">
+    <UploadRecommendationLetterForm ref="RecommendationLetterRef" />
+  </CtDialog>
+
+	<!-- 颁发实习证书 -->
+	<CtDialog :visible="showCertificateDialog" :widthType="2" titleClass="text-h6" title="颁发实习证书" @close="handleCertificateClose" @submit="handleSubmitCertificate">
+    <IssueCertificateForm ref="IssueCertificateFormRef" />
+  </CtDialog>
 </template>
 
 <script setup>
 defineOptions({name: 'studentList-internship-situation'})
+import { ref, onMounted } from 'vue'
+import { getUserAvatar } from '@/utils/avatar'
+import DatePicker from '@/components/FormUI/datePicker'
+import UploadRecommendationLetterForm from './RecommendationLetterForm'
+import IssueCertificateForm from './CertificateForm'
+import { getStudentPage, getRecordStatusCount, saveRecommend, saveCertificate } from '@/api/recruit/enterprise/student'
+import { getDict } from '@/hooks/web/useDictionaries'
+import { dealDictObjData } from '@/utils/position'
+import { formatName } from '@/utils/getText'
+import { timesTampChange } from '@/utils/date'
+import { convertTimestampsToDayRange } from '@/utils/date'
+import Snackbar from '@/plugins/snackbar'
+
+const date = ref(null)
+const statistics = ref([])
+
+const loading = ref(false)
+const total = ref(0)
+const query = ref({
+	pageNo: 1,
+	pageSize: 10,
+	startTime: null
+})
+const tableData = ref([])
+
+const headers = [
+  { title: '学生姓名', key: 'studentName', sortable: false },
+  { title: '就读学校', key: 'student.schoolName', sortable: false },
+  { title: '所属院系', key: 'student.schoolDepartmentName', sortable: false },
+  { title: '所属专业', key: 'student.majorName', sortable: false },
+  { title: '应聘职位', key: 'job.name', sortable: false, value: item => formatName(item.job.name) },
+  { title: '到岗日期', key: 'startTime', sortable: false, value: item => timesTampChange(item.startTime, 'Y-M-D') },
+  { title: '结束日期', key: 'endTime', sortable: false, value: item => timesTampChange(item.endTime, 'Y-M-D') },
+  { title: '创建日期', key: 'createTime', sortable: false, value: item => timesTampChange(item.createTime) },
+  { title: '操作', key: 'actions', sortable: false }
+]
+
+// 学生列表
+const getList = async () => {
+	loading.value = true
+	try {
+		const result = await getStudentPage(query.value)
+		tableData.value = result?.list.map(e => {
+			e.enterprise = dealDictObjData({}, e.enterprise)
+			e.job = dealDictObjData({}, e.job)
+			return e
+		})
+		total.value = result?.total || 0
+	} finally {
+		loading.value = false
+	}
+}
+
+// 数值统计
+const getStatistics = async () => {
+	try {
+		const data = await getRecordStatusCount({ type: '-1' })
+		statistics.value.forEach(e => {
+			const obj = data.find(val => val.key === e.value)
+			e.count = obj ? obj.value : 0
+		})
+	} catch {}
+}
+
+onMounted(async () => {
+	const { data } = await getDict('student_practice_status')
+	statistics.value = data
+	// getStatistics()
+	// getList()
+})
+
+const handleChangePage = (val) => {
+	query.value.pageNo = val
+	getList()
+}
+
+// 时间范围选择
+const handleChangeDate = (time) => {
+	if (time && time.length) {
+		query.value.startTime = convertTimestampsToDayRange(time)
+	} else {
+		query.value.startTime = []
+	}
+	query.value.pageNo = 1
+	getList()
+}
+
+// 上传推荐信
+const recordId = ref(null)
+const showLitterDialog = ref(false)
+const RecommendationLetterRef = ref(null)
+const handleUploadLetter = (id) => {
+	recordId.value = id
+	showLitterDialog.value = true
+}
+const handleLetterClose = () => {
+	recordId.value = null
+	showLitterDialog.value = false
+}
+const handleSubmitLetter = async () => {
+	const { valid } = await RecommendationLetterRef.value.CtFormRef.formRef.validate()
+	if (!valid) return
+	const query = RecommendationLetterRef.value.getQuery()
+
+	try {
+		await saveRecommend({ id: recordId.value, recommendationLetter: query.url })
+		Snackbar.success('上传成功')
+		getList()
+		handleLetterClose()
+	} catch {
+		handleLetterClose()
+	}
+}
+
+// 颁发实习证书
+const showCertificateDialog = ref(false)
+const IssueCertificateFormRef = ref(null)
+const handleIssueCertificate = (id) => {
+	recordId.value = id
+	showCertificateDialog.value = true
+}
+const handleCertificateClose = () => {
+	recordId.value = null
+	showCertificateDialog.value = false
+}
+const handleSubmitCertificate = async () => {
+	const { valid } = await IssueCertificateFormRef.value.CtFormRef.formRef.validate()
+	if (!valid) return
+	const query = IssueCertificateFormRef.value.getQuery()
+
+	try {
+		await saveCertificate({ id: recordId.value, ...query })
+		Snackbar.success('上传成功')
+		getList()
+		handleCertificateClose()
+	} catch {
+		handleCertificateClose()
+	}
+  
+}
 </script>
-<style lang="scss" scoped>
+
+<style scoped lang="scss">
+.statistics {
+	width: 50%;
+	&-card {
+		width: 33.33%;
+		margin-right: 12px;
+		background-color: #f7f8fa;
+		border-radius: 10px;
+		&:nth-child(3) {
+			margin-right: 0;
+		}
+		.value {
+			font-size: 44px;
+		}
+	}
+}
 </style>

+ 9 - 7
src/views/recruit/teacher/studentList/index.vue

@@ -1,7 +1,7 @@
 <!-- 学生列表 -->
 <template>
   <v-card class="px-3">
-    <!-- 院系 -->
+    <!-- 筛选条件 -->
     <div class="d-flex justify-space-between mt-8 mb-10">
       <div class="d-flex align-center">
         <!-- <span class="mx-3 color-666 font-size-14">院系</span> -->
@@ -12,7 +12,7 @@
       <v-btn :loading="exportLoading" prepend-icon="mdi-export-variant" color="primary" variant="tonal" class="ml-3" @click="null">导出</v-btn>
     </div>
     
-    <!-- 学生列表 -->
+    <!-- 列表 -->
     <div class="mt-5" style="min-height: 500px;">
       <CtTable
         :items="tableData"
@@ -47,6 +47,7 @@ defineOptions({name: 'studentList-index'})
 import { ref } from 'vue'
 import Snackbar from '@/plugins/snackbar'
 import { formatName } from '@/utils/getText'
+import { getUserAvatar } from '@/utils/avatar'
 
 const loading = ref(false)
 const query = ref({
@@ -57,11 +58,11 @@ const query = ref({
 
 const headers = [
   { title: '学生姓名', key: 'studentName', sortable: false },
-  { title: '学生学号', key: 'studentName', sortable: false },
-  { title: '所属专业', key: 'studentName', sortable: false },
-  { title: '录用企业', key: 'studentName', sortable: false, value: item => formatName(item.job.name) },
-  { title: '录用部门', key: 'studentName', sortable: false, value: item => formatName(item.job.name) },
-  { title: '录用岗位', key: 'studentName', sortable: false, value: item => formatName(item.job.name) },
+  { title: '学生学号', key: 'test', sortable: false },
+  { title: '所属专业', key: 'test', sortable: false },
+  { title: '录用企业', key: 'test', sortable: false, value: item => formatName(item.test) },
+  { title: '录用部门', key: 'test', sortable: false, value: item => formatName(item.test) },
+  { title: '录用岗位', key: 'test', sortable: false, value: item => formatName(item.test) },
   { title: '操作', key: 'actions', sortable: false }
 ]
 
@@ -72,6 +73,7 @@ const getData = async (isRefresh = false) => {
   // const { list, total: number } = await getInterviewInvitePage(query.value)
   // tableData.value = list
   // total.value = number
+  tableData.value = [{ test: 'ces', person: { name: '123' }}]
   if (isRefresh) Snackbar.success('刷新成功')
 }