فهرست منبع

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

lifanagju_citu 2 ماه پیش
والد
کامیت
ef47c94de6

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

@@ -61,4 +61,28 @@ export const getStudentPracticeCompanyList = async () => {
 	return await request.get({
 		url: '/app-api/menduner/system/student/record-enterprise/list'
 	})
+}
+
+// 学校列表
+export const getSchoolList = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/school/info/list',
+		params
+	})
+}
+
+// 根据学校id获取院系列表
+export const getDepartmentListBySchoolId = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/school/organization/list',
+		params
+	})
+}
+
+// 获取专业列表
+export const getMajorList = async (params) => {
+  return await request.get({
+    url: '/app-api/menduner/system/major/list',
+    params
+  })
 }

+ 107 - 12
src/api/school.js

@@ -16,10 +16,18 @@ export const schoolRegister = async (data) => {
 }
 
 // 学生列表
-export const studentList = async (data) => {
-	return await request.post({
-		url: '/app-api/flames/student/list',
-		data
+export const studentList = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/teacher/school/student/page',
+		params
+	})
+}
+
+// 学生详情
+export const getStudentDetailsById = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/teacher/school/student/get',
+		params
 	})
 }
 
@@ -47,14 +55,6 @@ export const recommendationList = async (data) => {
 	})
 }
 
-// 学生实习情况
-export const studentPracticeStatistics = async (data) => {
-	return await request.post({
-		url: '/app-api/flames/school/student/internship/statistics',
-		data
-	})
-}
-
 // 实习企业列表
 export const internshipCompanyList = async (data) => {
 	return await request.post({
@@ -94,3 +94,98 @@ export const getSchoolOrganizationList = async (params) => {
 		params
 	})
 }
+
+// 获取机构分页
+export const getOrganizationPage = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/teacher/school/organization/page',
+		params
+	})
+}
+
+// 获取机构列表
+export const getOrganizationList = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/teacher/school/organization/list',
+		params
+	})
+}
+
+// 获取机构树状结构
+export const getOrganizationTree = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/teacher/school/organization/get/tree',
+		params
+	})
+}
+
+// 创建机构
+export const createOrganization = async (data) => {
+	return await request.post({
+		url: '/app-api/menduner/system/teacher/school/organization/create',
+		data
+	})
+}
+
+// 更新机构
+export const updateOrganization = async (data) => {
+	return await request.post({
+		url: '/app-api/menduner/system/teacher/school/organization/update',
+		data
+	})
+}
+
+// 删除机构
+export const deleteOrganization = async (id) => {
+	return await request.post({
+		url: '/app-api/menduner/system/teacher/school/delete?id=' + id
+	})
+}
+
+// 根据id获取学生实习情况
+export const getInternshipById = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/teacher/school/student/practice/record/get',
+		params
+	})
+}
+
+// 根据id获取学生实习报告
+export const getStudentPracticeReportById = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/teacher/school/student/report/get',
+		params
+	})
+}
+
+// 获取学生实习的企业列表
+export const getStudentPracticeCompanyList = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/teacher/school/student/practice/record-enterprise/list',
+		params
+	})
+}
+
+// 实习情况-数值统计
+export const studentPracticeRecordCount = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/teacher/school/student/practice/record-status/count',
+		params
+	})
+}
+
+// 实习情况-列表状态数值钻取
+export const studentPracticeRecordPage = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/teacher/school/student/practice/record/page',
+		params
+	})
+}
+
+// 实习情况-分页列表
+export const studentPracticePage = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/teacher/school/student/practice/record/enterprise/page',
+		params
+	})
+}

+ 1 - 1
src/components/CtFilter/index.vue

@@ -1,6 +1,6 @@
 <template>
 	<v-card style="width:100%; min-height: 80px;" elevation="5">
-		<v-form v-model="valid" ref="formRef">
+		<v-form v-model="valid" ref="formRef" @submit.prevent>
 			<div class="flex-horizon d-flex pa-2 flex-wrap">
 				<div v-for="(item, index) in items.options" :key="`filter_${item.label}_${index}`" class="pa-3" v-show="!item.hidden">
 					<textUI

+ 62 - 35
src/plugins/dialogExtend/components/studentInfoForm.vue

@@ -27,12 +27,12 @@
 import { getDict } from '@/hooks/web/useDictionaries'
 defineOptions({name: 'dialogExtend-InfoForm'})
 import { reactive, ref } from 'vue'
-import { schoolList, departmentList } from '@/api/recruit/personal/resume'
 import { getUserAvatar } from '@/utils/avatar'
 import { uploadFile } from '@/api/common'
 import Snackbar from '@/plugins/snackbar'
 import { useI18n } from '@/hooks/web/useI18n'; const { t } = useI18n()
 import { isValidIdCard18 } from '@/utils/validate'
+import { getSchoolList, getDepartmentListBySchoolId, getMajorList } from '@/api/recruit/personal/student'
 
 const props = defineProps({
   option: {
@@ -50,6 +50,20 @@ const isShowCopper = ref(false)
 
 const male = ref('1')
 const showIcon = ref(false)
+
+// 专业名称下拉列表
+let majorName = null
+const getMajorData = async (name) => {
+  const item = items.value.options.find(e => e.key === 'majorId')
+  if (!item) return
+  if (item.items?.length && (majorName === name)) return // 防抖
+  majorName = name
+  if (name) {
+    const data = await getMajorList({ name })
+    item.items = data
+  }
+}
+
 const items = ref({
   options: [
     {
@@ -137,41 +151,50 @@ const items = ref({
       default: null,
       label: '就读学校 *',
       outlined: true,
-      itemText: 'schoolName',
+      itemText: 'name',
       itemValue: 'schoolId',
       rules: [v => !!v || '请选择就读学校'],
       items: [],
-      change: e => getDepartmentList(e),
+      change: e => getDepartmentList(e, 'schoolDeptId', 0),
     },
     {
       type: 'autocomplete',
-      key: 'schoolDepartmentName',
+      key: 'schoolDeptId',
       value: null,
       default: null,
       label: '所在院系 *',
       outlined: true,
-      itemText: 'departmentTitle',
-      itemValue: 'departmentTitle',
+      itemText: 'name',
+      itemValue: 'id',
       rules: [v => !!v || '请选择所在院系'],
+      change: e => getDepartmentList(e, 'schoolClassId', 2),
       items: []
     },
     {
-      type: 'text',
-      key: 'majorName',
-      value: '',
-      default: null,
-      label: '所学专业 *',
+      type: 'combobox',
+      key: 'schoolClassId',
+      value: null,
+      label: '所在班级 *',
       outlined: true,
-      rules: [v => !!v || '请输入所学专业']
+      clearable: true,
+      canBeInputted: true,
+      itemTextName: 'schoolClassName',
+      itemText: 'name',
+      itemValue: 'id',
+      rules: [v => !!v || '请选择所在班级'],
+      items: []
     },
     {
-      type: 'text',
-      key: 'schoolClassName',
-      value: '',
-      default: null,
-      label: '所在班级 *',
+      type: 'autocomplete',
+      key: 'majorId',
+      value: null,
+      label: '所学专业 *',
       outlined: true,
-      rules: [v => !!v || '请填写所在班级']
+      itemText: 'nameCn',
+      itemValue: 'id',
+      rules: [v => !!v || '请选择所学专业'],
+      search: e => getMajorData(e),
+      items: []
     },
     {
       type: 'text',
@@ -202,29 +225,27 @@ const items = ref({
   ]
 })
 
-if (import.meta.env.VITE_NODE_ENV === 'production') {
-  items.value.options = items.value.options.filter(e => e.slotName !== 'analysis')
-}
-
-// // 学校下拉列表
+// 学校下拉列表
 const getSchoolListData = async () => {
   const item = items.value.options.find(e => e.key === 'schoolId')
   if (!item) return
-  const { records } = await schoolList({current: 1,size: 9999})
-  item.items = records || []
+  const data = await getSchoolList()
+  item.items = data || []
 }
 getSchoolListData()
 
-const getDepartmentList = async (e) => {
-  const item = items.value.options.find(e => e.key === 'schoolDepartmentName')
+// 根据学校id获取院系、班级列表
+const getDepartmentList = async (id, key, type) => {
+  const item = items.value.options.find(e => e.key === key)
   if (!item) return
-  const query = {
-    page: { size: 9999,	current: 1	},
-    entity: { schoolId: e }
-  }
-  const res = await departmentList(query)
-  const list = res?.records?.length ? res.records : []
-  item.items = list.map(e => e.entity)
+
+  let params = { type } // type: 0院系|1专业|2班级
+  // 查院系用schoolId,查班级用parentId
+  if (key === 'schoolDeptId') params.schoolId = id
+  else params.parentId = id
+
+  const data = await getDepartmentListBySchoolId(params)
+  item.items = data || []
 }
 
 // 选择文件
@@ -306,7 +327,13 @@ const getQuery = async () => {
   const obj = {}
   items.value.options.forEach(e => {
     if (Object.prototype.hasOwnProperty.call(e, 'data')) return obj[e.key] = e.data
-    obj[e.key] = e.value === '' ? null : e.value
+    if (e.key === 'schoolClassId') {
+      const classObj = e.items.find(k => k[e.itemValue] === e.value)
+      if (!classObj) {
+        obj[e.key] = null
+        obj[e.itemTextName] = e.value
+      } else obj[e.key] = e.value
+    } else obj[e.key] = e.value === '' ? null : e.value
   })
   if (!obj.avatar) obj.avatar = getUserAvatar(null, obj.sex)
   query = Object.assign(query, obj)

+ 21 - 0
src/router/modules/components/recruit/enterprise.js

@@ -112,6 +112,27 @@ const enterprise = [
       }
     ]
   },
+  {
+    component: Layout,
+    path: '/recruit/enterprise/student',
+    redirect: '/recruit/enterprise/student/InternshipSituation',
+    name: 'enterpriseStudent',
+    meta: {
+      title: '学生专区',
+      enName: 'Student Zone',
+      icon: 'mdi-card-account-details-outline'
+    },
+    children: [
+      {
+        path: '/recruit/enterprise/student/InternshipSituation',
+        meta: {
+          title:'实习情况',
+          enName: 'Internship Situation'
+        },
+        component: () => import('@/views/recruit/enterprise/student/InternshipSituation/index.vue')
+      }
+    ]
+  },
   {
     path: '/recruit/enterprise/jobFair',
     component: Layout,

+ 14 - 35
src/router/modules/components/recruit/teacher.js

@@ -57,63 +57,42 @@ const teacher = [
     ]
   },
   {
-    path: '/recruit/teacher/internshipReport',
+    path: '/recruit/teacher/teacherCertification',
     component: Layout,
-    name: '实习报告',
-    icon: 'mdi-file-document-multiple-outline',
+    name: '账号信息',
+    icon: 'mdi-human-male-board',
     meta: {
-      enName: 'Internship Report'
+      enName: 'Teacher Certification'
     },
     children: [
       {
         path: 'index',
-        name: '实习报告',
+        name: '账号信息',
         meta: {
-          enName: 'Internship Report',
+          enName: 'Teacher Certification',
         },
         show: true,
-        component: () => import('@/views/recruit/teacher/internshipReport/index.vue')
+        component: () => import('@/views/recruit/teacher/teacherCertification/index.vue')
       },
     ]
   },
-  // {
-  //   path: '/recruit/teacher/internshipCompany',
-  //   component: Layout,
-  //   name: 'internshipCompany',
-  //   meta: {
-  //     title: '实习企业',
-  //     enName: 'Internship Company',
-  //     icon: 'mdi-home-city-outline'
-  //   },
-  //   children: [
-  //     {
-  //       path: 'index',
-  //       meta: {
-  //         title: '实习企业',
-  //         enName: 'Internship Company',
-  //       },
-  //       show: true,
-  //       component: () => import('@/views/recruit/teacher/internshipCompany/index.vue')
-  //     },
-  //   ]
-  // },
   {
-    path: '/recruit/teacher/teacherCertification',
+    path: '/recruit/teacher/organization',
     component: Layout,
-    name: '账号信息',
-    icon: 'mdi-human-male-board',
+    name: '院系管理',
+    icon: 'mdi-file-tree-outline',
     meta: {
-      enName: 'Teacher Certification'
+      enName: 'Department management'
     },
     children: [
       {
         path: 'index',
-        name: '账号信息',
+        name: '院系管理',
         meta: {
-          enName: 'Teacher Certification',
+          enName: 'Department management',
         },
         show: true,
-        component: () => import('@/views/recruit/teacher/teacherCertification/index.vue')
+        component: () => import('@/views/recruit/teacher/organization/index.vue')
       },
     ]
   },

+ 3 - 7
src/store/user.js

@@ -265,13 +265,9 @@ export const useUserStore = defineStore('user',
 
       // 获取学生信息
       async getStudentInformation () {
-        try {
-          const data = await getStudentInfo()
-          this.studentInfo = data
-          localStorage.setItem('studentInfo', data ? JSON.stringify(data) : '{}')
-        } catch (error) {
-          Snackbar.error(error.msg)
-        }
+        const data = await getStudentInfo()
+        this.studentInfo = data
+        localStorage.setItem('studentInfo', data ? JSON.stringify(data) : '{}')
       },
 
       // 获取学校基本信息

+ 2 - 2
src/views/flame/index.vue

@@ -1,8 +1,8 @@
 <template>
 	<div class="pa-3 default-width">
-		<!-- <div class="text-end mb-5">
+		<div class="text-end mb-5">
 			<v-btn color="primary" to="/flameLogin">老师登录/注册</v-btn>
-		</div> -->
+		</div>
 		<v-img src="https://minio.menduner.com/dev/108ff7690ab0773374138568fd0a3a166ee45df1f39b78ff0b2cdaeddbf71261.png"></v-img>
 		<v-img class="mt-3" src="https://minio.menduner.com/dev/95502df43c2155c457c485e869066536566d5f58ae80313567f81e7215f7eb77.png"></v-img>
 	</div>

+ 9 - 1
src/views/recruit/personal/PersonalCenter/index.vue

@@ -55,7 +55,9 @@ import { getCurrentLocaleLang } from '@/utils/lang.js'
 import personCenterRoute from '@/router/modules/components/recruit/personCenter'
 import { useUserStore } from '@/store/user'
 import { usePersonCenterStore } from '@/store/personCenter'
+import { useRoute } from 'vue-router'
 
+const route = useRoute()
 const userStore = useUserStore()
 
 const info = localStorage.getItem('baseInfo') ? JSON.parse(localStorage.getItem('baseInfo')) : {}
@@ -114,10 +116,16 @@ const handleClosePreview = () => {
 }
 
 // 更新账户信息
+const refreshPath = [
+  '/recruit/personal/personalCenter/wallet',
+  '/recruit/personal/personalCenter/tradeOrder',
+  '/recruit/personal/personalCenter/inviteRecord',
+  '/recruit/personal/personalCenter/memberBenefits/taskCenter'
+]
 const updateAccountInfo = async () => {
   await userStore.getUserAccountInfo()
 }
-updateAccountInfo()
+if (refreshPath.includes(route.path)) updateAccountInfo()
 </script>
 
 <style scoped lang="scss">

+ 82 - 54
src/views/recruit/personal/PersonalCenter/student/information/index.vue

@@ -14,64 +14,86 @@
 
 <script setup>
 defineOptions({name: 'personal-personCenter-studentInformation-index'})
-import { ref } from 'vue'
+import { ref, onMounted } from 'vue'
 import { saveStudentSimpleInfo } from '@/api/recruit/personal/shareJob'
-import { schoolList, departmentList } from '@/api/recruit/personal/resume'
 import { useI18n } from '@/hooks/web/useI18n'
 import Snackbar from '@/plugins/snackbar'
 import { isValidIdCard18 } from '@/utils/validate'
 import { useUserStore } from '@/store/user'
+import { getSchoolList, getDepartmentListBySchoolId, getMajorList } from '@/api/recruit/personal/student'
 
 const { t } = useI18n()
 const userStore = useUserStore()
 const overlay = ref(false)
 
 const CtFormRef = ref()
+
+// 专业名称下拉列表
+let majorName = null
+const getMajorData = async (name) => {
+  const item = items.value.options.find(e => e.key === 'majorId')
+  if (!item) return
+  if (item.items?.length && (majorName === name)) return // 防抖
+  majorName = name
+  if (name) {
+    const data = await getMajorList({ name })
+    item.items = data
+  }
+}
+
 const items = ref({
   options: [
     {
       type: 'autocomplete',
       key: 'schoolId',
-      value: {},
+      value: null,
       default: null,
       label: '就读学校 *',
       outlined: true,
-      returnObject: true,
-      itemText: 'schoolName',
+      itemText: 'name',
       itemValue: 'schoolId',
       rules: [v => !!v || '请选择就读学校'],
       items: [],
-      change: e => getDepartmentList(e),
+      change: e => getDepartmentList(e, 'schoolDeptId', 0, true),
     },
     {
       type: 'autocomplete',
-      key: 'schoolDepartmentName',
+      key: 'schoolDeptId',
       value: null,
       default: null,
       label: '所在院系 *',
       outlined: true,
-      itemText: 'departmentTitle',
-      itemValue: 'departmentTitle',
+      itemText: 'name',
+      itemValue: 'id',
       rules: [v => !!v || '请选择所在院系'],
+      change: e => getDepartmentList(e, 'schoolClassId', 2, true),
       items: []
     },
     {
-      type: 'text',
-      key: 'majorName',
-      value: '',
-      default: null,
-      label: '所学专业 *',
+      type: 'combobox',
+      key: 'schoolClassId',
+      value: null,
+      label: '所在班级 *',
       outlined: true,
-      rules: [v => !!v || '请输入所学专业']
+      clearable: true,
+      canBeInputted: true,
+      itemTextName: 'schoolClassName',
+      itemText: 'name',
+      itemValue: 'id',
+      rules: [v => !!v || '请选择所在班级'],
+      items: []
     },
     {
-      type: 'text',
-      key: 'schoolClassName',
-      value: '',
-      default: null,
-      label: '所在班级 *',
+      type: 'autocomplete',
+      key: 'majorId',
+      value: null,
+      label: '所学专业 *',
       outlined: true,
-      rules: [v => !!v || '请填写所在班级']
+      itemText: 'nameCn',
+      itemValue: 'id',
+      rules: [v => !!v || '请选择所学专业'],
+      search: e => getMajorData(e),
+      items: []
     },
     {
       type: 'text',
@@ -115,10 +137,9 @@ const items = ref({
       type: 'phoneNumber',
       key: 'emergencyContactPhone',
       value: '',
-      clearable: true,
       label: '紧急联系人手机号 *',
       rules: [v => !!v || '请填写紧急联系人手机号']
-    },
+    }
   ]
 })
 
@@ -129,48 +150,53 @@ items.value.options.forEach((e, index) => {
 })
 
 // 学校下拉列表
-const getSchoolListData = async (schoolId) => {
+const getSchoolListData = async () => {
   const item = items.value.options.find(e => e.key === 'schoolId')
   if (!item) return
-  const { records } = await schoolList({ current: 1,size: 9999 })
-  item.items = records || []
-
-  if (schoolId) {
-    item.value = records.find(e => e.schoolId === schoolId)
-  }
+  const data = await getSchoolList()
+  item.items = data || []
 }
-getSchoolListData()
 
-const getDepartmentList = async (e) => {
-  const item = items.value.options.find(e => e.key === 'schoolDepartmentName')
+
+// 根据学校id获取院系、班级列表
+const getDepartmentList = async (id, key, type, isRefreshValue = false) => {
+  const item = items.value.options.find(e => e.key === key)
   if (!item) return
-  const query = {
-    page: { size: 9999,	current: 1	},
-    entity: { schoolId: typeof e === 'object' ? e.schoolId : e }
-  }
-  const res = await departmentList(query)
-  const list = res?.records?.length ? res.records : []
-  item.items = list.map(e => e.entity)
-  if (typeof e === 'object') item.value = null
+
+  let params = { type } // type: 0院系|1专业|2班级
+  // 查院系用schoolId,查班级用parentId
+  if (key === 'schoolDeptId') params.schoolId = id
+  else params.parentId = id
+
+  const data = await getDepartmentListBySchoolId(params)
+  item.items = data || []
+
+  // 下拉框选择时需清空下级value
+  if (isRefreshValue) item.value = null
 }
 
 // 获取学生基本信息
 const studentInfoFun = async () => {
   await userStore.getStudentInformation()
   const data = JSON.parse(localStorage.getItem('studentInfo') || '{}')
-  if (data.schoolId) getDepartmentList(data.schoolId)
+  if (data.schoolId) getDepartmentList(data.schoolId, 'schoolDeptId', 0)
+  if (data?.schoolClassId) getDepartmentList(data.schoolDeptId, 'schoolClassId', 2)
+  if (data?.majorId) {
+    getMajorData(data?.major?.nameCn)
+  }
   // 回显
   items.value.options.forEach(e => {
     if (data[e.key]) {
-      if (e.key === 'schoolId') {
-        getSchoolListData(data[e.key])
-      }
-      else e.value = data[e.key]
+      e.value = data[e.key]
     }
   })
 }
-studentInfoFun()
-
+onMounted(() => {
+  // 获取学校列表
+  getSchoolListData()
+  // 获取学生基本信息
+  studentInfoFun()
+})
 
 // 提交
 const handleSubmit = async () => {
@@ -179,17 +205,19 @@ const handleSubmit = async () => {
   overlay.value = true
   const params = {}
   items.value.options.forEach(item => {
-    params[item.key] = item.returnObject ? '' : item.value
-    if (item.key === 'schoolId') {
-      params.schoolName = item.value.schoolName
-      params.schoolId = item.value.schoolId
+    // 班级有下拉选择的,需要根据选择的值赋值
+    if (item.key === 'schoolClassId') {
+      const classObj = item.items.find(e => e[item.itemValue] === item.value)
+      if (!classObj) {
+        params[item.key] = null
+        params[item.itemTextName] = item.value
+      } else params[item.key] = item.value
     } else params[item.key] = item.value
   })
 
-
   await saveStudentSimpleInfo(params)
   setTimeout(async () => {
-    await userStore.getStudentInformation()
+    studentInfoFun()
     Snackbar.success(t('common.submittedSuccessfully'))
     overlay.value = false
   }, 1000)

+ 26 - 37
src/views/recruit/teacher/internshipReport/index.vue

@@ -1,26 +1,30 @@
 <template>
   <v-card class="card-box d-flex pa-3">
     <div style="width: 20%; border-right: 1px solid #ccc;">
-      <v-treeview
-        :items="treeData"
-        activatable
-        color="primary"
-        item-value="id"
-        open-all
-        open-strategy="single"
-        density="compact"
-        @update:activated="handleClick"
-        @update:opened="handleClick"
-      >
-        <template v-slot:title="{ item }">
-          <div class="treeTitle font-size-15" v-ellipse-tooltip style="max-width: 100%;">{{ formatName(item.title) }}</div>
-        </template>
-      </v-treeview>
+      <div v-if="treeData.length > 0">
+        <v-treeview
+          :items="treeData"
+          activatable
+          class="mr-3"
+          color="primary"
+          item-value="id"
+          open-all
+          open-strategy="single"
+          density="compact"
+          @update:activated="handleClick"
+          @update:opened="handleClick"
+        >
+          <template v-slot:title="{ item }">
+            <div class="treeTitle font-size-15" v-ellipse-tooltip style="max-width: 100%;">{{ formatName(item.name) }}</div>
+          </template>
+        </v-treeview>
+      </div>
+      <div v-else class="font-size-15 color-999 text-center" style="line-height: 60vh;">暂无院系</div>
     </div>
     <div style="width: 80%" class="ml-3">
-      <div class="d-flex justify-space-between px-3">
+      <div class="d-flex justify-space-between">
         <TextInput v-model="query.name" :item="textItem" @change="getUserList"></TextInput>
-        <v-btn class="mr-3" width="100" color="primary" prepend-icon="mdi-refresh" variant="outlined" @click="getTreeData">刷 新</v-btn>
+        <v-btn width="100" color="primary" prepend-icon="mdi-refresh" variant="outlined" @click="getTreeData">刷 新</v-btn>
       </div>
       <CtTable
         :items="tableData"
@@ -74,7 +78,7 @@
 defineOptions({ name: 'group-account'})
 import { ref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
-import { getEnterpriseTree } from '@/api/recruit/enterprise/system/group'
+import { getOrganizationTree } from '@/api/school'
 import { getUserAvatar } from '@/utils/avatar'
 import { formatName } from '@/utils/getText';
 
@@ -122,16 +126,7 @@ const tableData = ref([
   "createTime": 1741681159370
 }
 ])
-const treeData = ref([
-  {
-    title: '农学',
-    children: [{ title: '农学1' }, { title: '农学2' }]
-  },
-  {
-    title: '文学',
-    children: [{ title: '文学1' }, { title: '文学2' }]
-  }
-])
+const treeData = ref([])
 const headers = [
   { title: '学生姓名', key: 'studentName', sortable: false },
   { title: '实习企业', key: 'enterpriseName', sortable: false },
@@ -166,15 +161,11 @@ const getUserList = async () => {
 const getTreeData = async () => {
   treeData.value = []
   try {
-    const data = await getEnterpriseTree()
-    if (!data) return
-    treeData.value[0] = data
-    query.value.enterpriseId = data.id
-    // 获取用户列表
-    getUserList()
+    const data = await getOrganizationTree({ schoolId: schoolInfo.value.schoolId })
+    treeData.value = data || []
   } catch {}
 }
-// getTreeData()
+getTreeData()
 
 // 分页
 const handleChangePage = (e) => {
@@ -185,8 +176,6 @@ const handleChangePage = (e) => {
 // 树形click
 const handleClick = (e) => {
   if (!e.length) return
-  query.value.enterpriseId = e[0]
-  getUserList()
 }
 
 // 查看实习报告

+ 76 - 87
src/views/recruit/teacher/internshipSituation/index.vue

@@ -6,7 +6,7 @@
 				<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.value }}</span>
+						<span class="value font-weight-bold color-primary">{{ val.number }}</span>
 						<span class="color-999 font-size-14">人</span>
 					</div>
 				</div>
@@ -32,14 +32,14 @@
 					<span class="ml-3 color-primary cursor-pointer" @click="handleEnterprise(item.id)">{{ formatName(item.anotherName || item.name) }}</span>
 				</div>
 			</template>
-			<template #internshipNumber="{ item }">
-				<span class="color-primary cursor-pointer" @click="handleDetail(item, '实习中')">{{ item.internshipNumber || 0 }}人</span>
+			<template #inProgressNum="{ item }">
+				<span class="color-primary cursor-pointer" @click="handleDetail(item, '实习中', '1', item.inProgressNum)">{{ item.inProgressNum || 0 }}人</span>
 			</template>
-			<template #waitInternshipNumber="{ item }">
-				<span class="color-primary cursor-pointer" @click="handleDetail(item, '等待实习')">{{ item.waitInternshipNumber || 0 }}人</span>
+			<template #waitingNum="{ item }">
+				<span class="color-primary cursor-pointer" @click="handleDetail(item, '等待实习', '0', item.waitingNum)">{{ item.waitingNum || 0 }}人</span>
 			</template>
-			<template #internshipSuccessNumber="{ item }">
-				<span class="color-primary cursor-pointer" @click="handleDetail(item, '结束实习')">{{ item.internshipSuccessNumber || 0 }}人</span>
+			<template #endNum="{ item }">
+				<span class="color-primary cursor-pointer" @click="handleDetail(item, '结束实习', '2', item.endNum)">{{ item.endNum || 0 }}人</span>
 			</template>
       <template #actions="{ item }">
         <v-btn v-if="!item?.recommendationLetter" color="primary" variant="text" @click="handleUploadLetter(item.id)">上传推荐信</v-btn>
@@ -61,79 +61,65 @@
       itemKey="id"
       @pageHandleChange="handleChangeDrillPage"
     >
+			<template #enterpriseName="{ item }">
+				{{ formatName(item.enterprise.anotherName || item.enterprise.name) }}
+			</template>
+			<template #jobName="{ item }">
+				{{ formatName(item.job.name) }}
+			</template>
 			<template #studentName="{ item }">
 				<div class="d-flex align-center">
-					<v-avatar size="40" :image="getUserAvatar(item.headImg, item.teacherSex)"></v-avatar>
-					<span class="ml-3 color-primary cursor-pointer" @click="handleToStudentDetail(item.studentId)">{{ item.studentName || item.phone }}</span>
+					<v-avatar size="40" :image="getUserAvatar(item.person.avatar, item.person.sex)"></v-avatar>
+					<span class="ml-3 color-primary cursor-pointer" @click="handleToStudentDetail(item.studentId)">{{ item.person?.name || item.person?.phone }}</span>
 				</div>
 			</template>
     </CtTable>
   </CtDialog>
-
-	<v-navigation-drawer v-model="showDetail" absolute location="right" rounded temporary width="700" class="pa-5">
-		111
-	</v-navigation-drawer>
 </template>
 
 <script setup>
 defineOptions({name: 'studentList-internship-situation'})
 import { ref, onMounted } from 'vue'
 import { getUserAvatar } from '@/utils/avatar'
-import { getStudentPage } from '@/api/recruit/enterprise/student'
-import { dealDictObjData } from '@/utils/position'
+import { dealDictArrayData } from '@/utils/position'
 import { formatName } from '@/utils/getText'
 import Snackbar from '@/plugins/snackbar'
-import { studentPracticeStatistics } from '@/api/school'
+import { getDict } from '@/hooks/web/useDictionaries'
+import { timesTampChange } from '@/utils/date'
+import { studentPracticeRecordCount, studentPracticePage, studentPracticeRecordPage } from '@/api/school'
 
-const statistics = ref([
-	{ label: '等待实习', value: 0, key: 'waitInternshipNumber' },
-	{ label: '实习中', value: 1, key: 'internshipNumber' },
-	{ label: '实习结束', value: 0, key: 'internshipSuccessNumber' }
-])
+const statistics = ref([])
 
 const loading = ref(false)
 const total = ref(0)
+const schoolInfo = ref(localStorage.getItem('schoolInfo') ? JSON.parse(localStorage.getItem('schoolInfo')) : {})
 const query = ref({
 	pageNo: 1,
 	pageSize: 10,
-	startTime: null
+	schoolId: schoolInfo.value.schoolId
 })
-const tableData = ref([
-	{
-    "id": 1,
-    "name": "门墩儿信息科技有限公司",
-    "anotherName": "门墩儿",
-    "industryId": "1829087620475494402",
-    "industryName": '互联网',
-    "scale": "0",
-    "scaleName": '0-20人',
-    "logoUrl": "https://minio.menduner.com/dev/1e6893918ef378ca280360078dfe74ade10b27101c89865261824b46de7d34a6.png",
-    internshipNumber: 2,
-    internshipSuccessNumber: 0,
-    waitInternshipNumber: 0
-  }
-])
-const schoolInfo = ref(localStorage.getItem('schoolInfo') ? JSON.parse(localStorage.getItem('schoolInfo')) : {})
+const tableData = ref([])
 
 const headers = [
 	{ title: '实习企业', key: 'enterpriseName', sortable: false },
 	{ title: '所在行业', key: 'industryName', sortable: false },
 	{ title: '企业规模', key: 'scaleName', sortable: false },
-	{ title: '实习中', key: 'internshipNumber', sortable: false },
-	{ title: '实习结束', key: 'internshipSuccessNumber', sortable: false },
-	{ title: '等待实习', key: 'waitInternshipNumber', sortable: false },
+	{ title: '等待实习', key: 'waitingNum', sortable: false },
+	{ title: '实习中', key: 'inProgressNum', sortable: false },
+	{ title: '实习结束', key: 'endNum', 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
-		})
+		const result = await studentPracticePage(query.value)
+		if (!result?.list || result?.list.length === 0) {
+			tableData.value = []
+			total.value = 0
+			return
+		}
+		tableData.value = dealDictArrayData([], result.list)
 		total.value = result?.total || 0
 	} finally {
 		loading.value = false
@@ -143,20 +129,19 @@ const getList = async () => {
 // 数值统计
 const getStatistics = async () => {
 	try {
-		const data = await studentPracticeStatistics({ schoolId: schoolInfo.value?.school?.schoolId })
-		console.log(data, 'data')
-		// statistics.value.forEach(e => {
-		// 	const obj = data.find(val => val.key === e.value)
-		// 	e.count = obj ? obj.value : 0
-		// })
+		const data = await studentPracticeRecordCount({ schoolId: schoolInfo.value?.schoolId })
+		statistics.value.forEach(e => {
+			const obj = data.find(val => val.key === e.value)
+			e.number = obj.value
+		})
 	} catch {}
 }
 
 onMounted(async () => {
-	// const { data } = await getDict('student_practice_status')
-	// statistics.value = data
-	// getStatistics()
-	// getList()
+	const { data } = await getDict('student_practice_status')
+	statistics.value = data
+	getStatistics()
+	getList()
 })
 
 const handleChangePage = (val) => {
@@ -174,51 +159,55 @@ const handleEnterprise = (id) => {
 const drill = ref({
   total: 0,
   query: {
-    size: 10,
-    current: 1
+    pageSize: 10,
+    pageNo: 1,
+		schoolId: schoolInfo.value.schoolId
   },
 	title: '学生列表',
   show: false,
-  list: [{
-		studentName: '张三',
-		enterpriseName: '北京字节跳动科技有限公司',
-		phone: '12345678901',
-		schoolDepartmentName: '计算机科学与技术',
-		majorName: '计算机科学与技术',
-		schoolClassName: '2019级',
-		studentNo: '2019111111',
-		studentId: 1,
-		teacherSex: '1',
-		headImg: '',
-		phone: '12345678901',
-		studentPracticeStatus: '实习中'
-	}],
+  list: [],
   headers: [
-    { title: '状态', key: 'studentPracticeStatus', sortable: false },
     { title: '学生姓名', key: 'studentName', sortable: false },
 		{ title: '实习企业', key: 'enterpriseName', sortable: false },
-    { title: '联系电话', key: 'phone', sortable: false },
-    { title: '所属院系', key: 'schoolDepartmentName', sortable: false },
-    { title: '所属专业', key: 'majorName', sortable: false },
-    { title: '所在班级', key: 'schoolClassName', sortable: false },
-    { title: '学号', key: 'studentNo', sortable: false },
+		{ title: '实习职位', key: 'jobName', sortable: false },
+    { 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: 'person.phone', sortable: false },
   ]
 })
-// 学生列表
-const handleDetail = (item, label) => {
+
+const getDrillData = async () => {
+	try {
+		const data = await studentPracticeRecordPage(drill.value.query)
+		drill.value.list = data.list
+		drill.value.total = data.total
+		drill.value.show = true
+	} catch {}
+}
+
+// 钻取-学生列表
+const handleDetail = (item, label, status, value) => {
+	if (!value) return Snackbar.warning('暂无数据')
 	drill.value.title = `${item.anotherName} - 状态[${label}] - 学生列表`
-	drill.value.query.current = 1
-  drill.value.show = true
+	drill.value.query.pageNo = 1
+	drill.value.query.status = status
+	drill.value.query.enterpriseId = item.id
+
+	getDrillData()
 }
-const handleChangeDrillPage = (page) => {}
+const handleChangeDrillPage = (page) => {
+	drill.value.query.pageNo = page
+	getDrillData()
+}
+
 const handleClose = () => {
   drill.value.show = false
-  // drill.value.list = []
+  drill.value.list = []
 }
 
-const showDetail = ref(false)
+// 学生详情
 const handleToStudentDetail = (id) => {
-	showDetail.value = true
+	if (id) window.open(`/recruit/teacher/studentList/detail/${id}`)
 }
 </script>
 

+ 69 - 0
src/views/recruit/teacher/organization/FormPage.vue

@@ -0,0 +1,69 @@
+<template>
+  <CtForm ref="CtFormRef" :items="formItems"></CtForm>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { updateOrganization, createOrganization } from '@/api/school'
+import Snackbar from '@/plugins/snackbar'
+
+const props = defineProps({
+  editItem: {
+    type: Object,
+    default: null
+  }
+})
+
+const CtFormRef = ref()
+const formItems = ref({
+  options: [
+    {
+      type: 'text',
+      key: 'name',
+      value: null,
+      label: '院系名称 *',
+      rules: [v => !!v || '请输入院系名称']
+    }
+  ]
+})
+
+if (props.editItem) {
+  formItems.value.options.forEach(e => {
+    e.value = props.editItem[e.key]
+  })
+}
+
+const submit = async () => {
+	const { valid } = await CtFormRef.value.formRef.validate()
+	if (!valid) return
+
+	let obj = {
+		schoolId: JSON.parse(localStorage.getItem('schoolInfo'))?.schoolId,
+		type: '0', // 0院系|1专业|2班级
+		parentId: 0
+	}
+	formItems.value.options.forEach(e => obj[e.key] = e.value)
+
+  if (props.editItem) {
+    obj.id = props.editItem.id
+  }
+
+  const subApi = props.editItem ? updateOrganization : createOrganization
+  return new Promise((resolve, reject) => {
+    subApi(obj).then(data => {
+      resolve(data)
+      Snackbar.success(props.editItem ? '保存成功' : '提交成功' )
+    }).catch(err => {
+      Snackbar.error(err.msg)
+    })
+  })
+}
+
+defineExpose({
+  submit
+})
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 148 - 0
src/views/recruit/teacher/organization/index.vue

@@ -0,0 +1,148 @@
+<template>
+  <CtFilter :items="formItems" @reset="handleReset" @search="handleSearch">
+    <template #appendBtn>
+      <v-btn class="ml-3 elevation-5" color="#00897B" @click="handleAdd">
+        <v-icon left>mdi-plus</v-icon>
+        {{ t('common.add') }} 
+      </v-btn>
+    </template>
+  </CtFilter>
+
+  <v-card class="pa-5 card-box mt-3">
+    <CtTable
+      :items="items"
+      :headers="headers"
+      :loading="loading"
+      :showPage="true"
+      :total="total"
+      :page-info="pageInfo"
+      itemKey="id"
+      :isTools="false"
+      @pageHandleChange="handleChangePage"
+    >
+      <template #status="{ item }">
+        <v-chip :color="item.status === '0' ? 'success' : 'error'" size="small" label>{{ item.statusName }}</v-chip>
+      </template>
+      <template #actions="{ item }">
+        <v-btn variant="text" color="primary" @click="handleEdit(item)">编辑</v-btn>
+        <v-btn variant="text" color="error" @click="handleDel(item)">删除</v-btn>
+      </template>
+    </CtTable>
+  </v-card>
+
+  <CtDialog :visible="show" :widthType="2" :title="editItem ? '编辑院系' : '新增院系'" @close="show = false" @submit="handleSubmit">
+    <FormPage ref="formPageRef" :editItem="editItem" />
+  </CtDialog>
+</template>
+
+<script setup>
+defineOptions({ name: 'organization' })
+import { ref } from 'vue'
+
+import { useI18n } from '@/hooks/web/useI18n'
+import Confirm from '@/plugins/confirm'
+import Snackbar from '@/plugins/snackbar'
+import { formatDate } from '@/utils/date'
+import { deleteOrganization, getOrganizationPage } from '@/api/school'
+import FormPage from './FormPage.vue'
+
+
+const { t } = useI18n()
+const total = ref(10)
+const items = ref([])
+const show = ref(false)
+const loading = ref(false)
+const formPageRef = ref()
+const editItem = ref(null)
+
+// 检索框
+const formItems = ref({
+  options: [
+		{
+      type: 'text',
+      key: 'name',
+      value: '',
+      label: '院系名称',
+			clearable: true,
+			hideDetails: true,
+			width: 200
+    }
+  ]
+})
+
+const query = ref({
+  name: null
+})
+const pageInfo = ref({
+  pageNo: 1,
+  pageSize: 10,
+  type: 0
+})
+
+const headers = [
+  { title: '院系名称', key: 'name', align: 'left', sortable: false },
+  { title: '创建时间', key: 'createTime', value: (e) => formatDate(e.createTime), sortable: false },
+  { title: t('common.actions'), key: 'actions', sortable: false }
+]
+// 列表
+const getPage = async () => {
+  loading.value = true
+  try {
+    const res = await getOrganizationPage({ ...pageInfo.value, ...query.value })
+    items.value = res.list || []
+    total.value = res.total
+  } finally {
+    loading.value = false
+  }
+}
+
+// 检索
+const handleSearch = (obj) => {
+  pageInfo.value.pageNo = 1
+  query.value = obj
+  getPage()
+}
+
+// 重置
+const handleReset = (obj) => {
+  pageInfo.value.pageNo = 1
+  query.value = obj
+  getPage()
+}
+
+// 新增
+const handleAdd = () => {
+  editItem.value = null
+  show.value = true
+}
+
+// 编辑
+const handleEdit = (item) => {
+  editItem.value = item
+  show.value = true
+}
+
+// 新增、编辑提交
+const handleSubmit = async () => {
+	await formPageRef.value.submit()
+	show.value = false
+	getPage()
+}
+
+// 删除
+const handleDel = async (item) => {
+  Confirm(t('common.confirmTitle'), '是否确定删除?').then(async () => {
+    await deleteOrganization(item.id)
+    Snackbar.success('删除成功')
+    getPage()
+  })
+}
+
+// 分页
+const handleChangePage = (index) => {
+  pageInfo.value.pageNo = index
+  getPage()
+}
+
+getPage()
+</script>

+ 11 - 11
src/views/recruit/teacher/studentList/components/baseInfo.vue

@@ -4,25 +4,25 @@
     <!-- 头像 -->
     <div class="avatarsBox">
       <v-badge
-        v-if="info?.sex === '1' || info?.sex === '2'"
+        v-if="info?.person?.sex === '1' || info?.person?.sex === '2'"
         bordered 
         offset-x="-25" 
         offset-y="33" 
-        :color="info?.sex ? (info?.sex === '1' ? '#1867c0' : 'error') : 'error'" 
-        :icon="info?.sex ? (info?.sex === '1' ? 'mdi-gender-male' : 'mdi-gender-female') : 'mdi-gender-female'">
-        <v-avatar size=80 :image="getUserAvatar(info?.avatar, info?.sex)"></v-avatar>
+        :color="info?.person?.sex ? (info?.person?.sex === '1' ? '#1867c0' : 'error') : 'error'" 
+        :icon="info?.person?.sex ? (info?.person?.sex === '1' ? 'mdi-gender-male' : 'mdi-gender-female') : 'mdi-gender-female'">
+        <v-avatar size=80 :image="getUserAvatar(info?.person?.avatar, info?.person?.sex)"></v-avatar>
       </v-badge>
-      <v-avatar v-else size=80 :image="getUserAvatar(info?.student?.studentHeadImg, info?.sex)"></v-avatar>
+      <v-avatar v-else size="80" :image="getUserAvatar(info?.person?.avatar, info?.person?.sex)"></v-avatar>
     </div>
     <!-- 信息 -->
     <div style="flex: 1;">
-      <span style="font-size: 20px; font-weight: 600;color: var(--color-666);">{{ info?.student?.studentName }}</span>
+      <span style="font-size: 20px; font-weight: 600;color: var(--color-666);">{{ info?.person?.name }}</span>
       <div class="d-flex mt-2 listBox">
-        <span>{{ info?.student?.schoolDepartmentName }}</span>
-        <span v-if="info?.student?.schoolDepartmentName && info?.student?.majorName" class="mx-3">|</span>
-        <span>{{ info?.student?.majorName }}</span>
+        <span>{{ info?.schoolDept?.name }}</span>
+        <span v-if="info?.schoolDept?.name && info?.major?.nameCn" class="mx-3">|</span>
+        <span>{{ info?.major?.nameCn }}</span>
       </div>
-      <view class="mt-2 listBox">{{ info?.student?.schoolClassName }}</view>
+      <view class="mt-2 listBox">{{ info?.schoolClass?.name }}</view>
     </div>
   </div>
 </template>
@@ -38,7 +38,7 @@ const props = defineProps({
 const info = ref({})
 if (props.data && Object.keys(props.data).length) {
   info.value = props.data
-  info.value.sex = info.value?.student?.studentSex === '男' ? '1' : info.value?.student?.studentSex === '女' ? '2' : info.value?.student?.sex || null
+  info.value.sex = info.value?.person?.sex === '男' ? '1' : info.value?.person?.sex === '女' ? '2' : info.value?.person?.sex || null
 }
 </script>
 

+ 9 - 59
src/views/recruit/teacher/studentList/components/other.vue

@@ -4,84 +4,34 @@
     <div class="boxMy">
       <div class="title-text">基本信息</div>
       <div class="my">
-        <span>出生年月:</span>
-        <span class="ml">{{ info?.student?.studentBirthday ? info?.student?.studentBirthday.slice(0, 10) : '' }}</span>
-      </div>
-      <div class="my">
-        <span>联系电话:</span>
-        <span class="ml">{{ info?.student?.phone }}</span>
-      </div>
-      <div class="my">
-        <span>就读院系:</span>
-        <span class="ml">{{ info?.schoolDepartment?.departmentTitle }}</span>
-      </div>
-      <div class="my">
-        <span>就读专业:</span>
-        <span class="ml">{{ info?.major?.majorName }}</span>
+        <span>学号:</span>
+        <span class="ml">{{ info?.studentNo }}</span>
       </div>
       <div class="my">
-        <span>所在班级:</span>
-        <span class="ml">{{ info?.schoolClass?.title }}</span>
+        <span>出生年月:</span>
+        <span class="ml">{{ info?.person?.birthday ? timesTampChange(info?.person?.birthday, 'Y-M-D') : '' }}</span>
       </div>
       <div class="my">
-        <span>学号:</span>
-        <span class="ml">{{ info?.student?.studentNo }}</span>
+        <span>联系电话:</span>
+        <span class="ml">{{ info?.person?.phone }}</span>
       </div>
       <div class="my">
         <span>紧急联系人:</span>
-        <span class="ml">{{ info?.student?.emergencyContactName }}</span>
+        <span class="ml">{{ info?.emergencyContactName }}</span>
       </div>
       <div class="my">
         <span>紧急联系人电话:</span>
-        <span class="ml">{{ info?.student?.emergencyContactPhone }}</span>
-      </div>
-      <div class="my" style="display: flex;align-items: center;">
-        <span>学生简历:</span>
-        <img v-if="checkIsImage(info?.studentBiographicalNotes?.fileUrl)" :src="info?.studentBiographicalNotes?.fileUrl" style="height: 100px;" />
-      </div>
-      <div class="my">
-        <span>录用企业:</span>
-        <span class="ml">{{ info?.student?.enterpeiseName }}</span>
-      </div>
-      <div class="my">
-        <span>录用部门:</span>
-        <span class="ml">{{ info?.student?.jobDept }}</span>
-      </div>
-      <div class="my">
-        <span>录用岗位:</span>
-        <span class="ml">{{ info?.student?.enterpriseRecruitJobName }}</span>
+        <span class="ml">{{ info?.emergencyContactPhone }}</span>
       </div>
     </div>
 
-    <div class="boxMy">
-      <div class="title-text">实习证书</div>
-      <div class="my" style="display: flex;">
-        <span>点评:</span>
-        <span class="ml">{{ info?.certificate?.comment }}</span>
-      </div>
-      <div class="my">
-        <span>证书:</span>
-        <span v-if="info?.certificate?.comment" class="ml link-text" @click="viewCertificate">点击查看</span>
-      </div>
-      <div class="my">
-        <span>附件:</span>
-        <span v-if="info?.certificate?.fileUrl" class="ml link-text" @click="handlePreview(info?.certificate?.fileUrl)">在线预览</span>
-      </div>
-    </div>
-    <div class="boxMy">
-      <div class="title-text">企业推荐信</div>
-      <div class="my">
-        <span>附件:</span>
-        <span v-if="info?.commendation?.fileUrl" class="ml link-text" @click="handlePreview(info?.commendation?.fileUrl)">在线预览</span>
-      </div>
-    </div>
   </div>
 </template>
 
 <script setup>
 defineOptions({name: 'studentList-student-details-baseInfoOther'})
 import { ref } from 'vue'
-import { checkIsImage } from '@/utils'
+import { timesTampChange } from '@/utils/date'
 
 const props = defineProps({
   data: Object

+ 108 - 40
src/views/recruit/teacher/studentList/index.vue

@@ -4,13 +4,12 @@
     <!-- 筛选条件 -->
     <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.schoolDepartmentName" :item="schoolDepartmentItem"></Autocomplete>
-        <TextInput class="mr-3" v-model="query.studentName" :item="studentNameItem" @enter="handleSearch()"></TextInput>
+        <Autocomplete class="mr-3" v-model="query.schoolDeptId" :item="schoolDepartmentItem"></Autocomplete>
+        <TextInput class="mr-3" v-model="query.name" :item="studentNameItem" @enter="handleSearch()"></TextInput>
         <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>
+      <!-- <v-btn :loading="exportLoading" prepend-icon="mdi-export-variant" color="primary" variant="tonal" class="ml-3" @click="null">导出</v-btn> -->
     </div>
     
     <!-- 列表 -->
@@ -34,31 +33,54 @@
           </div>
         </template>
         <template #actions="{ item }">
-          <v-btn v-if="!item?.recommendationLetter" color="primary" variant="text" @click="previewFile(item.recommendationLetter)">推荐信</v-btn>
-          <v-btn v-if="!item?.evaluate" color="#00897B" variant="text" @click="previewFile(item.evaluate)">实习证书</v-btn>
+          <v-btn color="primary" variant="text" @click="studentDetails(item.id)">详情</v-btn>
+          <v-btn color="primary" variant="text" @click="handleReport(item)">实习报告</v-btn>
         </template>
       </CtTable>
-      <!-- <Loading :visible="loading"></Loading> -->
     </div>
-  </v-card >
+
+    <v-navigation-drawer v-model="showDetail" absolute location="right" rounded temporary width="700" class="pa-5">
+    <div class="resume-header" style="height: 50px;">
+      <div class="resume-title">{{ itemData?.person?.name }} - 实习报告</div>
+      <Autocomplete v-model="enterpriseId" :item="enterpriseItem" @change="handleEnterprise"></Autocomplete>
+    </div>
+    <div v-if="report && report.length > 0" class="mt-5">
+      <div v-for="item in report" :key="item.date" class="mb-3">
+        <div class="color-666">日期:{{ item.date }}</div>
+        <div class="d-flex flex-wrap">
+          <img 
+            v-for="(src, index) in item.arr" 
+            :key="index" 
+            :src="src" 
+            @click="handlePreview(item.arr, index)" 
+            class="cursor-pointer" 
+            style="width: 200px; height: 250px;"
+          />
+        </div>
+      </div>
+    </div>
+    <Empty v-else :elevation="false" :message="!enterpriseId ? '请选择要查看的实习企业' : '暂无实习报告'" />
+	</v-navigation-drawer>
+  </v-card>
+
+  <PreviewImage v-if="showPreview" :initialIndex="initialIndex" :urlList="urlsList" @close="handleClosePreview" />
 </template>
 
 <script setup>
 defineOptions({name: 'studentList-index'})
-import { ref } from 'vue'
+import { ref, onMounted } from 'vue'
 import Snackbar from '@/plugins/snackbar'
-import { formatName } from '@/utils/getText'
 import { getUserAvatar } from '@/utils/avatar'
-import { studentList, getSchoolOrganizationList } from '@/api/school'
-// import { useRouter } from 'vue-router'; const router = useRouter()
-import { previewFile } from '@/utils'
+import { studentList, getSchoolOrganizationList, getStudentPracticeReportById, getStudentPracticeCompanyList } from '@/api/school'
+import { formatName } from '@/utils/getText'
 
 const loading = ref(false)
 const query = ref({
-  pageSize: 20,
+  pageSize: 10,
   pageNo: 1,
-  schoolDepartmentName: null,
-  studentName: null,
+  schoolId: JSON.parse(localStorage.getItem('schoolInfo'))?.schoolId,
+  name: null,
+  schoolDeptId: null
 })
 
 const studentNameItem = ref({
@@ -68,27 +90,35 @@ const studentNameItem = ref({
   clearable: true,
   hideDetails: true
 })
+const enterpriseId = ref(null)
+const enterpriseItem = ref({ 
+  width: 300, 
+  items: [], 
+  clearable: true,
+  hideDetails: true, 
+  label: '请选择实习企业', 
+  itemText: 'name', 
+  itemValue: 'id' 
+})
 
 const headers = [
   { title: '学生姓名', key: 'studentName', sortable: false },
-  { 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: 'schoolDept.name', sortable: false },
+  { title: '所属专业', key: 'major.nameCn', sortable: false },
+  { title: '所在班级', key: 'schoolClass.name', sortable: false },
+  { title: '学号', key: 'studentNo', sortable: false },
+  { title: '紧急联系人', key: 'emergencyContactName', sortable: false },
+  { title: '紧急联系人电话', key: 'emergencyContactPhone', sortable: false },
   { title: '操作', key: 'actions', sortable: false }
 ]
-// 数据列表
+
+// 学生列表
 const tableData = ref([]); const total = ref(0)
 const getData = async (isRefresh = false) => {
-  if (!query.value?.schoolDepartmentName) return
-
-  const { data, total: number } = await studentList(query.value)
-  tableData.value = data?.records?.length && data.records.map(item=>{
-    const { enterpeiseName, enterpriseRecruitJobName, jobDept } = item
-    return { ...item.student, enterpeiseName, enterpriseRecruitJobName, jobDept }
-  })
-  total.value = number
+  const result = await studentList(query.value)
+  tableData.value = result.list || []
+  total.value = result.total
+
   if (isRefresh) Snackbar.success('刷新成功')
 }
 
@@ -107,29 +137,67 @@ const handleSearch = (refresh = false) => {
 const schoolInfo = ref(localStorage.getItem('schoolInfo') ? JSON.parse(localStorage.getItem('schoolInfo')) : {})
 const schoolDepartmentItem = ref({ width: 300, items: [], clearable: false, hideDetails: true, label: '请选择院系', itemText: 'name', itemValue: 'id' })
 
-// 列表
-const getYuanXiItem = async () => {
+// 院系列表
+const getSchoolDepartment = async () => {
   const schoolId = schoolInfo.value?.schoolId || null
   if (!schoolId) return Snackbar.warning('获取学校信息失败!')
   
   const data = await getSchoolOrganizationList({ schoolId, type: 0 })
   schoolDepartmentItem.value.items = data || []
-
-  // if (schoolDepartmentItem.value.items?.length) {
-  //   query.value.schoolDepartmentName = schoolDepartmentItem.value.items[0].value
-  //   getData()
-  // }
 }
-getYuanXiItem()
+
+onMounted(() => {
+  getSchoolDepartment()
+  getData()
+})
 
 const studentDetails = (id) => {
   if (id) window.open(`/recruit/teacher/studentList/detail/${id}`)
 }
 
-// 导出
-const exportLoading = ref(false)
+// 实习报告
+const showDetail = ref(false)
+const report = ref([])
+const itemData = ref({})
+const handleReport = async (item) => {
+  enterpriseId.value = null
+  report.value = []
+  itemData.value = item
+  enterpriseItem.value.items = []
+  const data = await getStudentPracticeCompanyList({ userId: item.userId })
+  enterpriseItem.value.items = data ? data.map(e => {
+    return { name: formatName(e.anotherName || e.name), id: e.id }
+  }) : []
+  showDetail.value = true
+}
+
+const handleEnterprise = async (id) => {
+  report.value = []
+  if (!id) return
+  const data = await getStudentPracticeReportById({ enterpriseId: id, userId: itemData.value.userId })
+  if (!data || !Object.keys(data).length) return
+	for (let item in data) {
+		report.value.push({ date: item, arr: data[item].map(e => e.url) })
+	}
+}
+
+// 图片预览
+const showPreview = ref(false)
+const initialIndex = ref(0)
+const urlsList = ref([])
+const handlePreview = (arr, index) => {
+  urlsList.value = arr
+  initialIndex.value = index
+  showPreview.value = true
+}
 
+const handleClosePreview = () => {
+  showPreview.value = false
+  initialIndex.value = 0
+  urlsList.value = []
+}
 </script>
+
 <style lang="scss" scoped>
 .title {
   color: var(--color-333);

+ 77 - 77
src/views/recruit/teacher/studentList/studentDetails.vue

@@ -1,26 +1,42 @@
 <!-- 学生详情 -->
 <template>
-  <div v-if="Object.keys(info).length" class="d-flex justify-center mb-8">
-    <div style="width: 940px;background: #fff;" class="px-8 pb-12 pt-3 my-n3 mr-3">
-      <!-- 基本信息 -->
+  <div v-if="Object.keys(info).length" class="d-flex justify-center flex-column align-center mb-8">
+    <v-card class="pa-8">
+      <!-- 头像院系 -->
       <baseInfo class="mt-5" :data="info"></baseInfo>
       <!-- 基本信息 -->
       <other :data="info"></other>
-    </div>
-    <div class="operate pa-3">
-      <v-list>
-        <v-list-subheader class="title">操作</v-list-subheader>
-        <v-list-item
-          v-for="(item, i) in operateItems" :key="'操作' + i"
-          color="primary"
-          :prepend-icon="item.icon"
-          :title="item.text"
-          @click="handleClick(item)"
-        >
-        </v-list-item>
-      </v-list>
-    </div>
+    </v-card>
+
+    <!-- 实习情况 -->
+    <v-card class="my-3 pa-5">
+      <div class="resume-header">
+        <div class="resume-title">实习情况</div>
+      </div>
+      <CtTable
+        class="mt-3"
+        :items="practice"
+        :headers="headers"
+        :elevation="0"
+        :loading="false"
+        :is-tools="false"
+        :items-per-page="-1"
+        :showPage="false"
+        itemKey="id"
+      >
+        <template #enterpriseName="{ item }">
+          <div class="d-flex align-center defaultLink" @click="handleEnterprise(item.enterpriseId)">
+            <v-avatar size="40" :image="item.enterprise.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'"></v-avatar>
+            <span class="ml-3">{{ formatName(item.enterprise.anotherName || item.enterprise.name) }}</span>
+          </div>
+        </template>
+        <template #status="{ item }">
+          {{ statusList.find(e => e.value === item.status)?.label }}
+        </template>
+      </CtTable>
+    </v-card>
   </div>
+
   <Loading :visible="loading"></Loading>
 </template>
 
@@ -28,35 +44,38 @@
 defineOptions({name: 'studentList-student-details'})
 import baseInfo from './components/baseInfo.vue'
 import other from './components/other.vue'
-import { ref } from 'vue'
-import { stuDetail, certificateList, recommendationList } from '@/api/school'
+import { ref, onMounted } from 'vue'
+import { getStudentDetailsById, getInternshipById } from '@/api/school'
 import Snackbar from '@/plugins/snackbar'
-import { useRoute } from 'vue-router'; const route = useRoute()
+import { useRoute } from 'vue-router';
+import { formatName } from '@/utils/getText'
+import { timesTampChange } from '@/utils/date'
+import { getDict } from '@/hooks/web/useDictionaries'
 
-const operateItems = [
-  { text: '上传推荐信', key:'letter', icon: 'mdi-circle-medium' },
-  { text: '颁发实习证书', key:'certificate', icon: 'mdi-circle-medium' },
-]
+const route = useRoute()
 
 // 获取人才详情
-const info = ref({
-  student: {
-    studentName: '莫秋妮',
-    studentSex: '男',
-    studentBirthday: '2000-01-01 00:00:00',
-    schoolDepartmentName: '上海交通大学',
-    majorName: '野生动物与自然保护区管理',
-    schoolClassName: '1班',
-  },
-  studentBiographicalNotes: {
-    fileUrl: 'https://minio.huomiaoer.com/dev/data/a613722d-f0cc-404b-88b7-4f92dbed5264/ba9e680f-6bba-4fb9-8df6-1fd723b286ed.jpg',
-  }
-})
+const info = ref({})
 const loading = ref(false)
-const { id: studentId } = route.params
+const { id } = route.params
+
+const headers = [
+  { title: '实习企业', key: 'enterpriseName', sortable: false },
+  { title: '投递职位', key: 'jobName', sortable: false, value: item =>  formatName(item?.job?.name) },
+  { title: '实习状态', key: 'status', sortable: false },
+  { title: '开始时间', key: 'startTime', sortable: false, value: item => timesTampChange(item.startTime, 'Y-M-D') },
+  { title: '结束时间', key: 'endTime', sortable: false, value: item => timesTampChange(item.startTime, 'Y-M-D') },
+]
+
+// 获取学生实习情况
+const practice = ref([])
+const getPracticeRecord = async (userId) => {
+  const data = await getInternshipById({ userId })
+  practice.value = data || []
+}
 
 const getCvDetail = async () => {
-  if (!studentId) {
+  if (!id) {
     Snackbar.warning('缺少学生id')
     setTimeout(() => {
       window.close()
@@ -64,51 +83,32 @@ const getCvDetail = async () => {
     return
   }
   loading.value = true
-  const data = await stuDetail({ studentId })
-  info.value = data
-  await getInternshipCertificate()
-  await getRecommendation()
+  const data = await getStudentDetailsById({ id })
+  info.value = data || {}
   loading.value = false
-}
-getCvDetail()
 
-// 实习证书
-const getInternshipCertificate = async () => {
-  const query = {
-    size: 999,
-    current: 1,
-    studentId
-  }
-  const data = await certificateList(query)
-  info.value.certificate = data.records.length > 0 ? data.records.reverse()[0].studentInternshipCertificate : {}
-  loading.value = false
+  // 实习情况
+  getPracticeRecord(data.userId)
 }
 
-// 学生推荐信
-const getRecommendation = async () => {
-  const query = {
-    page: {
-      size: 9999,
-      current: 1
-    },
-    entity: {
-      studentId
-    }
-  }
-  const data = await recommendationList(query)
-  info.value.commendation = data.records.length > 0 ? data.records.reverse()[0].entity : {}
-}
+const statusList = ref([])
+onMounted(async () => {
+  // 状态字典
+  const { data } = await getDict('student_practice_status')
+  statusList.value = data || []
 
-const handleClick = (item) => {
-  console.log('handleClick->item:', item)
-}
+  getCvDetail()
+})
 
+// 跳转企业详情
+const handleEnterprise = (enterpriseId) => {
+  if (!enterpriseId) return
+  window.open(`/recruit/personal/company/details/${enterpriseId}?key=briefIntroduction`)
+}
 </script>
+
 <style lang="scss" scoped>
-.operate {
-  width: 240px;
-  height: 500px; // 272px
-  position: sticky;
-  top: 60px;
+.v-card {
+  width: 940px;
 }
 </style>

+ 18 - 46
src/views/recruit/teacher/teacherCertification/teacherInfo.vue

@@ -17,19 +17,6 @@
         </div>
         <div style="font-size: 14px; color: var(--color-999);">只支持JPG、JPEG、PNG类型的图片,大小不超过20M</div>
       </template>
-			<template #organizationList>
-				<div class="pa-5 mb-3" style="width: 100%; border: 1px dashed #ccc; border-radius: 4px;">
-					<p class="color-999 font-size-14 mb-3">
-						<span class="color-error">*</span>
-						负责院系
-					</p>
-					<div v-for="(k, index) in departmentList" :key="index" class="d-flex align-center mb-5">
-						<TextInput v-model="k.name" :item="textItem" />
-						<v-icon v-if="index > 0" class="ml-3 cursor-pointer" @click="handleDeleteDepartment(index)" color="error">mdi-close-circle</v-icon>
-					</div>
-					<v-btn class="mt-3" color="primary" prepend-icon="mdi-plus" size="small" @click="handleAddDepartment">添加院系</v-btn>
-				</div>
-			</template>
 		</CtForm>
 		<v-btn class="buttons my-10" color="primary" @click.stop="handleSubmit">{{ $t('common.save') }}</v-btn>
 	</div>
@@ -47,19 +34,10 @@ import { useI18n } from '@/hooks/web/useI18n'
 import { useUserStore } from '@/store/user'
 import { uploadFile } from '@/api/common'
 import Snackbar from '@/plugins/snackbar'
-import { updateTeacherInfo } from '@/api/school'
+import { updateTeacherInfo, getOrganizationList } from '@/api/school'
 
 const showIcon = ref(false)
 const CtFormRef = ref()
-const departmentList = ref([{ name: '' }])
-const textItem = {
-	type: 'text',
-  key: 'name',
-	width: 450,
-  label: '院系名称 *',
-	hideDetails: true,
-  rules: [v => !!v || '请输入您负责的院系名称']
-}
 const formItems = ref({
   options: [
 		{
@@ -100,11 +78,16 @@ const formItems = ref({
       label: '电子邮箱'
     },
     {
-			slotName: 'organizationList',
+      type: 'autocomplete',
       key: 'organizationList',
-			noParam: true,
+      value: [],
       label: '负责院系 *',
-      rules: [v => !!v || '请填写您在学校负责的院系']
+      itemText: 'name',
+      itemValue: 'id',
+      multiple: true,
+      returnObject: true,
+      rules: [v => !!v || '请选择您在学校负责的院系'],
+      items: []
     }
   ]
 })
@@ -117,8 +100,15 @@ userStore.$subscribe((mutation, state) => {
   schoolInfo.value = state.schoolInfo
 })
 
+// 院系列表
+const getSchoolOrganizationList = async () => {
+  const data = await getOrganizationList({ schoolId: schoolInfo.value.schoolId })
+  formItems.value.options.find(e => e.key === 'organizationList').items = data || []
+}
+
 onMounted(async () => {
 	await userStore.getSchoolInfo()
+  await getSchoolOrganizationList()
 
 	// 获取性别字典数据
 	const sexItem = formItems.value.options.find(e => e.key === 'sex')
@@ -127,21 +117,10 @@ onMounted(async () => {
 	sexItem.items = data || []
 
 	formItems.value.options.forEach(item => {
-		if (!item.noParam) item.value = schoolInfo.value[item.key] || item.defaultValue
-		else {
-			departmentList.value = schoolInfo.value?.organizationList && schoolInfo.value.organizationList.length ? schoolInfo.value.organizationList : [{ name: '' }]
-		}
+		item.value = schoolInfo.value[item.key] || item.defaultValue
 	})
 })
 
-// 添加院系
-const handleAddDepartment = () => {
-	departmentList.value.push({ name: '' })
-}
-// 删除院系
-const handleDeleteDepartment = (index) => {
-	departmentList.value.splice(index, 1)
-}
 
 // 图片裁剪
 const overlay = ref(false)
@@ -203,18 +182,11 @@ const handleHideCopper = (data) => {
 const handleSubmit = async () => {
   const { valid } = await CtFormRef.value.formRef.validate()
   if (!valid) return
-
-	const isCheck = departmentList.value.every(item => item.name)
-	if (!isCheck) return Snackbar.warning('请将院系信息填写完整')
-
   overlay.value = true
-
   let obj = {
-    id: schoolInfo.value.id,
-    organizationList: departmentList.value?.length ? departmentList.value.map(e => { return { name: e.name } }) : []
+    id: schoolInfo.value.id
   }
 	formItems.value.options.forEach(item => {
-		if (item.noParam) return
 		obj[item.key] = item.value
 	})