瀏覽代碼

将groupJobFair分支的老师学生内容合并到jobFair分支.

lifanagju_citu 3 月之前
父節點
當前提交
1422fffe88
共有 100 個文件被更改,包括 4810 次插入1898 次删除
  1. 33 0
      src/api/recruit/enterprise/student.js
  2. 0 9
      src/api/recruit/personal/personalCenter/student.js
  3. 47 0
      src/api/recruit/personal/student.js
  4. 96 0
      src/api/school.js
  5. 0 194
      src/components/FormUI/datePicker/index copy.vue
  6. 2 7
      src/components/Position/item.vue
  7. 1 4
      src/components/Position/longCompany.vue
  8. 1 4
      src/components/Position/longStrip.vue
  9. 2 1
      src/components/Position/similarPositions.vue
  10. 1 1
      src/components/Upload/img.vue
  11. 6 6
      src/components/VerificationCode/index.vue
  12. 3 8
      src/components/headSearch/index.vue
  13. 1 1
      src/config/axios/service.js
  14. 3 1
      src/hooks/web/useDictionaries.js
  15. 56 26
      src/layout/company/navBar.vue
  16. 1 1
      src/layout/company/side.vue
  17. 13 6
      src/layout/personal/navBar.vue
  18. 161 0
      src/layout/teacher.vue
  19. 122 0
      src/layout/teacher/navBar.vue
  20. 89 0
      src/layout/teacher/side.vue
  21. 0 4
      src/main.js
  22. 2 2
      src/plugins/dialogExtend/components/infoForm.vue
  23. 1 1
      src/plugins/dialogExtend/components/positionAd.vue
  24. 2 2
      src/plugins/dialogExtend/components/studentInfoForm.vue
  25. 43 17
      src/router/modules/common.js
  26. 0 45
      src/router/modules/components/headhunting.js
  27. 62 62
      src/router/modules/components/recruit/personCenter.js
  28. 132 0
      src/router/modules/components/recruit/teacher.js
  29. 56 5
      src/router/modules/recruit.js
  30. 59 18
      src/router/modules/remaining.js
  31. 32 0
      src/store/system.js
  32. 44 7
      src/store/user.js
  33. 4 0
      src/styles/index.css
  34. 0 0
      src/styles/index.min.css
  35. 3 0
      src/styles/index.scss
  36. 0 1
      src/styles/recruit/position/index.css
  37. 1 1
      src/styles/recruit/position/index.scss
  38. 20 13
      src/utils/auth.js
  39. 1 1
      src/utils/date.js
  40. 12 0
      src/utils/dealData.js
  41. 0 11
      src/utils/loginType.js
  42. 403 0
      src/utils/tree.ts
  43. 17 0
      src/views/flame/index.vue
  44. 236 0
      src/views/login/flameLogin.vue
  45. 0 3
      src/views/login/index.vue
  46. 0 0
      src/views/mallCopy/components/table.vue
  47. 1 1
      src/views/mallCopy/exchange.vue
  48. 1 1
      src/views/mallCopy/exchangeRecords.vue
  49. 0 0
      src/views/mallCopy/index.vue
  50. 0 0
      src/views/mallCopy/purchasePackage/components/packageList.js
  51. 0 0
      src/views/mallCopy/purchasePackage/components/packageList.vue
  52. 0 0
      src/views/mallCopy/purchasePackage/index.vue
  53. 10 5
      src/views/recruit/components/message/index.vue
  54. 0 117
      src/views/recruit/entRegister/joiningEnterprise.vue
  55. 12 0
      src/views/recruit/enterprise/contactUs/index.vue
  56. 9 9
      src/views/recruit/enterprise/entInfoSetting/informationSettingsComponents/businessInformation.vue
  57. 27 1
      src/views/recruit/enterprise/hirePosition/index.vue
  58. 39 0
      src/views/recruit/enterprise/permissionPrompt/index.vue
  59. 3 22
      src/views/recruit/enterprise/staffInfoSetting/index.vue
  60. 13 3
      src/views/recruit/enterprise/student/InternshipSituation/CertificateForm.vue
  61. 108 54
      src/views/recruit/enterprise/student/InternshipSituation/index.vue
  62. 1 0
      src/views/recruit/enterprise/systemManagement/groupAccount/inviteConfirmEnt-old.vue
  63. 0 14
      src/views/recruit/enterprise/systemManagement/groupAccount/inviteConfirmEnt.vue
  64. 346 345
      src/views/recruit/enterprise/talentMap/index.vue
  65. 0 0
      src/views/recruit/enterprise/talentPool/indexCopy.vue
  66. 3 1
      src/views/recruit/personal/PersonalCenter/index.vue
  67. 1 4
      src/views/recruit/personal/PersonalCenter/jobFeedback/components/interview/item.vue
  68. 1 1
      src/views/recruit/personal/PersonalCenter/resume/analysis/components/basicInfo.vue
  69. 1 1
      src/views/recruit/personal/PersonalCenter/resume/analysis/components/educationExp.vue
  70. 4 4
      src/views/recruit/personal/PersonalCenter/resume/online/components/basicInfo.vue
  71. 1 1
      src/views/recruit/personal/PersonalCenter/resume/online/components/educationExp.vue
  72. 112 60
      src/views/recruit/personal/PersonalCenter/student/InternshipReport/index.vue
  73. 26 8
      src/views/recruit/personal/PersonalCenter/student/information/index.vue
  74. 152 54
      src/views/recruit/personal/PersonalCenter/student/intershipCompany/index.vue
  75. 51 9
      src/views/recruit/personal/PersonalCenter/student/intershipCompany/item.vue
  76. 182 0
      src/views/recruit/personal/PersonalCenter/studentInformation/index.vue
  77. 0 8
      src/views/recruit/personal/PersonalCenter/wallet/index.vue
  78. 0 1
      src/views/recruit/personal/companyDetail/components/positions.vue
  79. 0 2
      src/views/recruit/personal/position/components/conditionFilter.vue
  80. 3 1
      src/views/recruit/personal/position/components/details.vue
  81. 1 1
      src/views/recruit/personal/recommend/components/positionList.vue
  82. 0 113
      src/views/recruit/personal/shareJob/form/simpleInfo.vue
  83. 0 83
      src/views/recruit/personal/shareJob/form/upload.vue
  84. 0 311
      src/views/recruit/personal/shareJob/index.vue
  85. 0 113
      src/views/recruit/personal/shareJob/sendResume/select.vue
  86. 0 84
      src/views/recruit/personal/shareJob/sendResume/simple.vue
  87. 129 0
      src/views/recruit/teacher/internshipCompany/index.vue
  88. 115 0
      src/views/recruit/teacher/internshipReport/index copy.vue
  89. 236 0
      src/views/recruit/teacher/internshipReport/index.vue
  90. 241 0
      src/views/recruit/teacher/internshipSituation/index.vue
  91. 87 0
      src/views/recruit/teacher/studentList/components/baseInfo.vue
  92. 114 0
      src/views/recruit/teacher/studentList/components/other.vue
  93. 142 0
      src/views/recruit/teacher/studentList/index.vue
  94. 114 0
      src/views/recruit/teacher/studentList/studentDetails.vue
  95. 24 0
      src/views/recruit/teacher/teacherCertification/index.vue
  96. 118 0
      src/views/recruit/teacher/teacherCertification/schoolInfo.vue
  97. 259 0
      src/views/recruit/teacher/teacherCertification/teacherInfo.vue
  98. 15 4
      src/views/register/person.vue
  99. 42 0
      src/views/register/school.vue
  100. 298 0
      src/views/register/schoolForm.vue

+ 33 - 0
src/api/recruit/enterprise/student.js

@@ -0,0 +1,33 @@
+import request from '@/config/axios'
+
+// 获取实习学生记录分页
+export const getStudentPage = async (params) => {
+  return await request.get({
+    url: '/app-api/menduner/system/recruit/student/page',
+    params
+  })
+}
+
+// 获得学生实习记录状态统计
+export const getRecordStatusCount = async (params) => {
+  return await request.get({
+    url: '/app-api/menduner/system/recruit/student/get/record-status/count',
+    params
+  })
+}
+
+// 保存实习证书
+export const saveCertificate = async (data) => {
+  return await request.post({
+    url: '/app-api/menduner/system/recruit/student/save/evaluate',
+    data
+  })
+}
+
+// 保存推荐信
+export const saveRecommend = async (data) => {
+  return await request.post({
+    url: '/app-api/menduner/system/recruit/student/upload/recommendation-letter',
+    data
+  })
+}

+ 0 - 9
src/api/recruit/personal/personalCenter/student.js

@@ -1,9 +0,0 @@
-import request from '@/config/axios'
-
-// 学生实习企业列表
-export const practiceProcess = async (data) => {
-  return await request.post({
-		url: '/app-api/flames/student/practice/process',
-		data
-	})
-}

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

@@ -14,4 +14,51 @@ export const getCertificatePage = async (data) => {
 		url: '/app-api/flames/student/internship/certificate/list',
 		data
 	})
+}
+
+// 获取实习生记录分页
+export const getInternshipPage = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/recruit/student/page',
+		params
+	})
+}
+
+// 学生实习企业列表
+export const practiceProcess = async (data) => {
+	return await request.post({
+		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
+	})
+}
+
+// 获取学生实习报告列表
+export const getStudentReportList = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/student/get/report/list',
+		params
+	})
+}
+
+// 保存学生实习报告
+export const saveStudentReport = async (data) => {
+	return await request.post({
+		url: '/app-api/menduner/system/student/report/save',
+		data
+	})
+}
+
+// 获取学生实习的企业列表
+export const getStudentPracticeCompanyList = async () => {
+	return await request.get({
+		url: '/app-api/menduner/system/student/record-enterprise/list'
+	})
 }

+ 96 - 0
src/api/school.js

@@ -0,0 +1,96 @@
+import request from '@/config/axios'
+
+// 获取学校基本信息
+export const getSchoolInformation = async () => {
+	return await request.get({
+		url: '/app-api/menduner/system/teacher/info/get'
+	})
+}
+
+// 注册学校账号
+export const schoolRegister = async (data) => {
+	return await request.post({
+		url: '/app-api/menduner/system/teacher/info/register',
+		data
+	})
+}
+
+// 学生列表
+export const studentList = async (data) => {
+	return await request.post({
+		url: '/app-api/flames/student/list',
+		data
+	})
+}
+
+// 学生详情
+export const stuDetail = async (data) => {
+	return await request.post({
+		url: '/app-api/flames/student/detail',
+		data
+	})
+}
+
+// 实习证书
+export const certificateList = async (data) => {
+	return await request.post({
+		url: '/app-api/flames/student/internship/certificate/list',
+		data
+	})
+}
+
+// 推荐信
+export const recommendationList = async (data) => {
+	return await request.post({
+		url: '/app-api/flames/student/recommendation/list',
+		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({
+		url: '/app-api/flames/enterprise/school/practice/list',
+		data
+	})
+}
+
+// 学校列表
+export const flameSchoolList = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/school/list',
+		params
+	})
+}
+
+// 更新老师信息
+export const updateTeacherInfo = async (data) => {
+	return await request.post({
+		url: '/app-api/menduner/system/teacher/info/update',
+		data
+	})
+}
+
+// 更新学校信息
+export const updateSchoolInfo = async (data) => {
+	return await request.post({
+		url: '/app-api/menduner/system/teacher/school/update',
+		data
+	})
+}
+
+// 获取学校机构列表
+export const getSchoolOrganizationList = async (params) => {
+	return await request.get({
+		url: '/app-api/menduner/system/teacher/school/organization/list',
+		params
+	})
+}

+ 0 - 194
src/components/FormUI/datePicker/index copy.vue

@@ -1,194 +0,0 @@
-<template>
-  <div :style="{ width: item.width ? item.width + 'px' : '100%' }">
-    <div class="d-flex align-center">
-      <!-- <VueDatePicker
-        v-model="value"
-        ref="datepicker"
-        :options="item.options || {}"
-        locale="zh-CN"
-        :disabled="item.disabled || false"
-        :range="item.range || false"
-        :model-type="timestamp"
-        :month-picker="month"
-        :time-picker="time"
-        :year-picker="year"
-        auto-apply
-        text-input
-        :time-picker-inline="true"
-        :show-now-button="item.showToday"
-        now-button-label="今天"
-        :disabled-dates="item.disabledDates ? disabledDates : []"
-        :enable-time-picker="item.enableTimePicker ?? false"
-        :clearable="item.clearable ?? true"
-        :day-names="['一', '二', '三', '四', '五', '六', '七']"
-        select-text="确认"
-        cancel-text="取消"
-        v-bind="$attrs"
-        :class="{'detailMargin': detailMargin}"
-        style="flex: 1"
-        @open="handleOpen"
-        @closed="handleClosed"
-        @update:modelValue="modelValueUpDate"
-      >
-        <template #trigger>
-          <v-text-field
-            v-model="formatText"
-            variant="outlined"
-            :density="item.dense || 'compact'"
-            type="text"
-            :rules="rules"
-            :disabled="item.disabled"
-            :style="{width: item.width}"
-            :color="item.color || 'primary'"
-            :label="item.label"
-            :placeholder="item.placeholder || item.label"
-            :autofocus="item.autofocus"
-            :required="item.required"
-            :suffix="item.suffix"
-            :append-icon="item.appendIcon"
-            :append-inner-icon="item.appendInnerIcon"
-            :clearable="item.clearable"
-            :readonly="true"
-            :counter="item.counter"
-            :prepend-inner-icon="item.prependInnerIcon"
-            hide-spin-buttons
-            :class="item.class"
-            :hide-details="hideDetails || false"
-            @click:clear="handleClear"
-            @click="inputClick"
-            @blur="inputBlur"
-          ></v-text-field>
-        </template>
-      </VueDatePicker> -->
-      
-      <div class="form-label" :style="{'width': item.labelWidth + 'px'}">{{ item.label }}</div>
-      <el-config-provider :locale="zhCn">
-        <el-date-picker
-          style="flex: 1;"
-          v-model="value"
-          size="large"
-          :type="item.mode || 'date'"
-          :placeholder="item.placeholder || '请选择'"
-          :start-placeholder="item.startPlaceholder || '开始日期'"
-          :end-placeholder="item.endPlaceholder || '结束日期'"
-          :format="item.format"
-          :value-format="item.valueFormat || 'x'"
-          :disabled="item.disabled"
-          :disabled-date="disabledDates"
-          :date-format="item.dateFormat"
-          :time-format="item.timeFormat"
-          :default-value="item.defaultValue"
-          @change="modelValueUpDate"
-          @clear="handleClear"
-          @blur="handleOpen"
-        >
-        </el-date-picker>
-      </el-config-provider>
-    </div>
-  </div>
-</template>
-
-<script setup>
-defineOptions({ name:'FormUI-v-text-field'})
-import { timesTampChange } from '@/utils/date'
-import { computed, ref, watch } from 'vue'
-import zhCn from 'element-plus/es/locale/lang/zh-cn'
-// import { ElDatePicker } from 'element-plus'
-
-const props = defineProps({item: Object, modelValue: [String, Number, Boolean, Array], changeFn: Function})
-const emit = defineEmits(['update:modelValue', 'change'])
-const item = props.item
-
-const value = ref(props.modelValue)
-
-watch(() => props.modelValue, 
-  (newVal) => {
-    modelValueUpDate(newVal)
-  },
-  // { immediate: true },
-  // { deep: true }
-)
-
-// 过去的日期不可选
-const disabledDates = (date) => {
-  const currentDate = new Date()
-  if (!props.item.disabledDate) return false
-  currentDate.setDate(currentDate.getDate() - 1)
-  return date.getTime() < currentDate.getTime()
-}
-
-// const timestamp = 'timestamp' // 固定不能变
-const formatText = ref('')
-
-const modelValueUpDate = (val) => {
-  value.value = val
-  getFormatText()
-  emit('update:modelValue', value.value)
-  emit('change', value.value)
-}
-
-const getFormatText = () => {
-  const format = item.format || 'Y-M-D'
-  formatText.value = timesTampChange(value.value, format)
-}
-
-const handleClear = () => {
-  value.value = null
-  emit('change', value.value)
-}
-
-
-const rules = ref(item.rules)
-watch(() => item.rules, 
-  (newVal) => {
-    rules.value = newVal
-  },
-  { immediate: true },
-  { deep: true }
-)
-const handleOpen = () => {
-  rules.value = []
-}
-// const handleClosed = () => {
-//   rules.value = item.rules
-// }
-
-// const hideDetails = ref(item.hideDetails || false)
-// const detailMargin = ref(false)
-// const inputClick = () => {
-//   if (item.hideDetails) return
-//   hideDetails.value = true
-//   detailMargin.value = true
-// }
-// const inputBlur = () => {
-//   if (item.hideDetails) return
-//   hideDetails.value = item.hideDetails || false
-//   detailMargin.value = false
-// }
-
-// dateType: 默认 date, 即年月日
-const year = computed(() => {
-  return item.dateType === 'year'
-})
-const month = computed(() => {
-  return item.dateType === 'month'
-})
-const time = computed(() => {
-  return item.dateType === 'time'
-})
-
-if (!item.format) item.format = year.value ? 'Y' : month.value ? 'Y-M' : time.value ? 'Y-M-D h:m:s' : 'Y-M-D'
-if (item.value) value.value = item.value; getFormatText()
-
-</script>
-
-<style lang="scss" scoped>
-// .removeDetailHeight {}
-:deep(.dp--menu-wrapper) {
-  // top: 50px !important;
-  left: 0 !important;
-}
-.detailMargin {
-  margin-bottom: 22px;
-}
-</style>

+ 2 - 7
src/components/Position/item.vue

@@ -11,7 +11,7 @@
         <div class="job-info">
           <div class="sub-li-top">
             <div class="sub-li-info">
-              <p :class="['name', {'default-active': item.active }]">{{ formatName(item.name) }}</p>
+              <p v-ellipse-tooltip :class="['name', {'default-active': item.active }]">{{ formatName(item.name) }}</p>
               <svg-icon v-if="tab === 3 && item.hire" name="pin" size="30"></svg-icon>
             </div>
           </div>
@@ -123,9 +123,6 @@ const handlePosition = (item) => {
   &:nth-child(3n) {
     margin-right: 0;
   }
-  &:hover {
-    box-shadow: 0 16px 40px 0 rgba(153, 153, 153, .3);
-  }
 }
 .job-info {
   position: relative;
@@ -195,9 +192,7 @@ const handlePosition = (item) => {
 }
 .user-info {
   // display: flex;
-  padding: 12px 20px;
-  // align-items: center;
-  // justify-content: space-between;
+  padding: 10px 20px 12px;
 }
 .names {
   font-weight: 500;

+ 1 - 4
src/components/Position/longCompany.vue

@@ -1,6 +1,6 @@
 <template>
   <div>
-    <div class="sub-li mb-3 elevation-2" v-for="item in list" :key="item.id" @mouseenter="item.active = true" @mouseleave="item.active = false">
+    <div class="sub-li mb-3" :class="item.active ? 'elevation-8' : 'elevation-3'" v-for="item in list" :key="item.id" @mouseenter="item.active = true" @mouseleave="item.active = false">
       <div class="company-info-top">
         <div class="company-info">
           <div class="float-left mr-5">
@@ -68,9 +68,6 @@ const handleCancel = async (item) => {
   &:nth-child(4n) {
     margin-right: 0;
   }
-  &:hover {
-    box-shadow: 0px 3px 5px -1px var(--v-shadow-key-umbra-opacity, rgba(0, 0, 0, 0.2)), 0px 5px 8px 0px var(--v-shadow-key-penumbra-opacity, rgba(0, 0, 0, 0.14)), 0px 1px 14px 0px var(--v-shadow-key-ambient-opacity, rgba(0, 0, 0, 0.12)) !important;
-  }
 }
 .company-info {
   width: 100%;

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

@@ -1,6 +1,6 @@
 <template>
   <div>
-    <div class="position-item mb-3 job-closed elevation-2" style="position: relative;" 
+    <div class="position-item mb-3 job-closed" style="position: relative;" :class="val.active ? 'elevation-8' : 'elevation-3'"
       v-for="(val, i) in props.items" :key="i" @mouseenter="val.active = true" @mouseleave="val.active = false"
     >
       <div class="info-header">
@@ -165,9 +165,6 @@ const loginClose = () => {
   height: 144px;
   background-color: #fff;
   border-radius: 12px;
-  &:hover {
-    box-shadow: 0px 3px 5px -1px var(--v-shadow-key-umbra-opacity, rgba(0, 0, 0, 0.2)), 0px 5px 8px 0px var(--v-shadow-key-penumbra-opacity, rgba(0, 0, 0, 0.14)), 0px 1px 14px 0px var(--v-shadow-key-ambient-opacity, rgba(0, 0, 0, 0.12)) !important;
-  }
   .info-header {
     height: 48px;
     background: linear-gradient(90deg,#f5fcfc,#fcfbfa);

+ 2 - 1
src/components/Position/similarPositions.vue

@@ -7,7 +7,7 @@
       <span v-else class="recruit-salary">{{ item.payFrom ? item.payFrom + '-' : '' }}{{ item.payTo }}{{ item.payName ? '/' + item.payName :'' }}</span>
       <div :class="['enterprise', {'border-bottom-dashed': index !== list.length - 1}]">
         <v-img class="float-left" :src="item.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'" :width="30" :height="30"></v-img>
-        <span class="float-left enterprise-name" v-ellipse-tooltip>{{ formatName(item.anotherName) }}</span>
+        <span class="float-left enterprise-name" v-ellipse-tooltip>{{ formatName(item.anotherName || item.enterpriseName) }}</span>
         <span class="float-right enterprise-address">{{ !item.areaId ? '全国' : item.area?.str }}</span>
       </div>
     </div>
@@ -27,6 +27,7 @@ const props = defineProps({
     default: () => {}
   }
 })
+
 const handlePosition = (item) => {
   window.open(`/recruit/personal/position/details/${item.id}`)
 }

+ 1 - 1
src/components/Upload/img.vue

@@ -13,7 +13,7 @@
   <div v-else class="" style="position: relative;">
     <v-icon color="error" class="close" @click="handleClose">mdi-close-circle</v-icon>
     <v-img :src="src" width="100" height="100" rounded class="imgBox" :class="{'cursor-pointer': showCursor}" @click="emit('imgClick')"></v-img>
-    <div @click="emit('imgClick')" class="color-primary cursor-pointer text-center text-decoration-underline">点击预览</div>
+    <div @click="emit('imgClick', src)" class="color-primary cursor-pointer text-center text-decoration-underline mt-1">点击预览</div>
   </div>
 </template>
 

+ 6 - 6
src/components/VerificationCode/index.vue

@@ -168,12 +168,12 @@ const getSmsCode = async () => {
     phone: loginData.phone,
     scene: props.scene ? props.scene-0 : 30
   }
-  // try {
-  await sendSmsCode(query)
-  Snackbar.success(t('login.sendCode'))
-  // } catch (error) {
-  //   Snackbar.error(error.msg)
-  // }
+  try {
+    await sendSmsCode(query)
+    Snackbar.success(t('login.sendCode'))
+  } catch (error) {
+    Snackbar.error(error.msg)
+  }
 }
 const setTime = () => {
   showCode.value = false

+ 3 - 8
src/components/headSearch/index.vue

@@ -67,12 +67,11 @@ const defineProps = defineProps({
     default: ''
   }
 })
-
 // const value = ref('')
 const value = ref(defineProps.modelValue)
 let drawer = ref(false)
 
-if (route.query && route.query?.content) value.value = route.query.content
+if (route.query && route.query?.content) value.value = route.query.content.includes('%') ? decodeURIComponent(route.query.content) : route.query.content
 
 // 点击外部关闭职位下拉
 const sharedState = useSharedState()
@@ -82,12 +81,8 @@ watch(() => sharedState.layoutClickCount, () => {
 });
 
 const handleSearch = () => {
-  // // 职位搜索页传参,其它的跳转到职位搜索页
-  // if (route.path !== '/recruit/personal/position') {
-  //   if (value.value) router.push(`/recruit/personal/position?content=${value.value}`)
-  //   else router.push('/recruit/personal/position')
-  // } else emits('handleSearch', value.value)
-  emits('handleSearch', value.value)
+  const name = value.value ? value.value.includes('&') ? encodeURIComponent(value.value) : value.value : ''
+  emits('handleSearch', name)
 }
 
 const handleClickJob = (val) => {

+ 1 - 1
src/config/axios/service.js

@@ -67,7 +67,7 @@ service.interceptors.request.use(
     let isToken = (config.headers || {}).isToken === false
     // token类型. api》function中设置tokenIndex(优先)
     const tokenIndex = config.tokenIndex ? config.tokenIndex : getIsEnterprise() ? 1 : 2
-    // console.log('令牌类型', tokenIndex === 1 ? '企业:' : '个人:', getToken(tokenIndex))
+    console.log('令牌类型', tokenIndex === 1 ? '企业:' : '个人:', getToken(tokenIndex))
     whiteList.some((v) => {
       if (config.url) {
         config.url.indexOf(v) > -1

+ 3 - 1
src/hooks/web/useDictionaries.js

@@ -9,6 +9,7 @@ import {
   getAreaTreeData,
   getPositionData
 } from '@/api/common/index'
+import { flameSchoolList } from '@/api/school'
 import { getSecondNodes } from '@/utils/dealData'
 
 // const setDict = (type, val, cacheTime = 7200) => {
@@ -47,7 +48,8 @@ export const getDict = (type, params, apiType = 'dict') => {
         areaList: getAreaListData,
         areaMap: getAreaMapData,
         positionData: getPositionData,
-        positionSecondData: getPositionTreeData
+        positionSecondData: getPositionTreeData,
+        schoolList: flameSchoolList
       }
       apiFn[apiType](query).then(data => {
         if (type === 'areaTreeData') {

+ 56 - 26
src/layout/company/navBar.vue

@@ -17,7 +17,7 @@
           <div class="cursor-pointer mx-3 commonHover" @click="handleLogout(false)">我要求职</div>
 
           <div class="enterprise-septal-line"></div>
-          <div class="d-flex align-center mx-3 cursor-pointer commonHover" @click="router.push('/recruit/enterprise/tradingOrder?key=tab_recharge')">
+          <div class="d-flex align-center mx-3 cursor-pointer commonHover" @click="router.push('/recruit/enterprise/membershipPackage/index?key=1')">
             <div>剩余M豆:{{ enterpriseUserAccount?.balance ? enterpriseUserAccount?.balance / 100 : 0 }}个</div>
           </div>
 
@@ -76,7 +76,7 @@
           </v-menu> -->
 
           <!-- 消息通知 -->
-          <MessageNotification class="commonHover2" path="/recruit/enterprise/chatTools"></MessageNotification>
+          <MessageNotification class="commonHover2" path="/recruit/enterprise/invite/chatTools"></MessageNotification>
         </div>
       </div>
     </v-toolbar>
@@ -84,11 +84,8 @@
 </template>
 
 <script setup>
-import {
-  getUserBindEnterpriseList,
-  // getUserRegisterEnterpriseApply
-} from '@/api/personal/user'
-import { computed, ref, onMounted } from 'vue'
+defineOptions({ name: 'personal-navbar' })
+import { ref, onMounted, computed } from 'vue'
 import { getToken } from '@/utils/auth'
 import { useUserStore } from '@/store/user'; const userStore = useUserStore()
 // import { useLocaleStore } from '@/store/locale'; const localeStore = useLocaleStore()
@@ -96,8 +93,7 @@ import { useRouter } from 'vue-router'; const router = useRouter()
 import { useI18n } from '@/hooks/web/useI18n'; const { t } = useI18n()
 import MessageNotification from '../message.vue'
 import { getUserAvatar } from '@/utils/avatar'
-import { formatName } from '@/utils/getText';
-defineOptions({ name: 'personal-navbar' })
+import { formatName } from '@/utils/getText'
 
 defineProps({
   sticky: {
@@ -108,16 +104,10 @@ defineProps({
 
 const showBall = ref(false)
 
-onMounted(() => {
-  if (getToken(1)) {
-    showBall.value = true
-  }
-})
-
 const handleLogoClick = () => { window.open('/recruitHome') } // 点击logo
 
 const enterpriseClick = (tabKey = 1) => {
-  const path = '/recruit/enterprise/entInfoSetting'
+  const path = '/recruit/enterprise/systemManagement/entInfoSetting'
   router.push({ path, query: { tabKey } })
 }
 
@@ -126,16 +116,64 @@ const handleLogout = async (exit = true) => {
   if (exit) await userStore.userLogout(2)
   router.push({ path: '/recruitHome' })
 }
-const enterpriseList = ref([])
 
 const menuList = ref([
-  { title: t('setting.editPassword'), icon: 'mdi-shield-lock-open-outline', change: () => router.push({ path: '/recruit/enterprise/staffChangePassword' }) },
+  { title: t('setting.editPassword'), icon: 'mdi-shield-lock-open-outline', key: 'editPassword', change: () => router.push({ path: '/recruit/enterprise/systemManagement/staffChangePassword' }) },
   { title: t('setting.logOut'), icon: 'mdi-logout', change: handleLogout }
 ])
 const items = computed(() => {
   return menuList.value.filter(item => !item.hidden)
 })
 
+/**
+ * @param {Array} routes
+ * @returns {Array}
+ * 路由扁平化 抽离children字段
+ */
+ function routeFlattening (routes) {
+  return routes.reduce((prev, cur) => {
+    prev.push(cur.path)
+    if (cur.children && cur.children.length) {
+      prev.push(...routeFlattening(cur.children))
+    }
+    return prev
+  }, [])
+}
+
+const hasRoute = (path) => {
+  const routes = router.getRoutes()
+  return routeFlattening(routes).some(_path => {
+    if (_path.includes(':')) {
+      const change = path.split('/')
+      const _change = _path.split('/')
+      if (change.length !== _change.length) {
+        return false
+      }
+      const res = _change.reduce((e, v, i) => {
+        if (v.includes(':')) {
+          e.push(true)
+          return e
+        }
+        e.push(change[i] === v)
+        return e
+      }, [])
+      return res.every(e => e)
+    }
+    return _path === path
+  })
+}
+// 没有权限访问时,隐藏菜单
+if (!hasRoute('/recruit/enterprise/systemManagement/staffChangePassword')) {
+  menuList.value.find(e => e.key === 'editPassword').hidden = true
+}
+
+onMounted(() => {
+  if (getToken(1)) {
+    showBall.value = true
+    localStorage.setItem('showEditPassword', hasRoute('/recruit/enterprise/systemManagement/staffChangePassword'))
+  }
+})
+
 // 企业logo、用户基本信息
 let baseInfo = ref(JSON.parse(localStorage.getItem('entBaseInfo')) || {})
 let enterpriseUserAccount = ref(JSON.parse(localStorage.getItem('enterpriseUserAccount')) || {}) // 账户信息
@@ -151,14 +189,6 @@ userStore.$subscribe((mutation, state) => {
 //   location.reload()
 // }
 
-const test = ref(false)
-// 企业列表
-const getEnterpriseListData = async () => {
-  if (!test.value) return
-  const data = await getUserBindEnterpriseList() // 申请通过才有数据,否则空数组
-  enterpriseList.value = data || []
-}
-getEnterpriseListData()
 </script>
 
 <style lang="scss" scoped>

+ 1 - 1
src/layout/company/side.vue

@@ -50,7 +50,7 @@ import enterpriseRoute from '@/router/modules/components/recruit/enterprise'
 const list = computed(() => {
   return getList(enterpriseRoute)
 })
-console.log(import.meta.env.VITE_NODE_ENV, '当前环境变量============')
+// console.log(import.meta.env.VITE_NODE_ENV, '当前环境变量============')
 
 const info = localStorage.getItem('entBaseInfo') ? JSON.parse(localStorage.getItem('entBaseInfo')) : {}
 const getList = (arr, obj = []) => {

+ 13 - 6
src/layout/personal/navBar.vue

@@ -38,7 +38,9 @@
         
         <div class="d-flex" style="height: 50px;">
           <div class="btns d-flex align-center" v-if="!isLogin && showLoginBtn">
-            <v-btn color="primary" style="color: #fff !important; background-color: #00B760;" @click="handleLogin">{{ $t('login.loginOrRegister') }}</v-btn>
+            <v-btn color="primary" style="color: #fff !important; background-color: #00B760;" @click="handleLogin">
+              {{ route.path === '/flame' ? '老师登录/注册' : '登录/注册' }}
+            </v-btn>
           </div>
           
           <!-- 头像用户名 -->
@@ -176,7 +178,7 @@ const navList = ref([
   { title: '门墩儿招聘', path: '/recruit/personal/recommend', dealActive: true },
   { title: '门墩儿猎头', path: '/headhunting' },
   { title: '门墩儿商城', path: '/pointsExchange', isEdit: true },
-  { title: '火苗儿校企' },
+  { title: '火苗儿校企', path: '/flame' },
   { title: '了解门墩儿', path: '/about' }
 ])
 
@@ -207,9 +209,13 @@ const position = [
   '/recruit/personal/company/details',
   '/recruit/personal/position/details'
 ]
-const menuActive = computed(() => (val)=> {
-  return val.dealActive ? position.indexOf(route.path) !== -1 : (route.path === val.path || route.path.includes(val.path))
-})
+const menuActive = (val) => {
+  let path
+  position.forEach(e => {
+    if (route.path.indexOf(e) !== -1) path = e
+  })
+  return val.dealActive ? position.indexOf(path) !== -1 : (route.path === val.path || route.path.includes(val.path))
+}
 
 const vip = computed(() => {
   return new Date().getTime() < userStore.userInfo?.vipExpireDate
@@ -300,7 +306,8 @@ userStore.$subscribe((mutation, state) => {
 })
 
 const handleLogin = () => {
-  router.push({ path: '/login' })
+  const path = route.path === '/flame' ? '/flameLogin' : '/login'
+  router.push(path)
 }
 
 // 语言切换

+ 161 - 0
src/layout/teacher.vue

@@ -0,0 +1,161 @@
+<template>
+  <div class="parent d-flex flex-column">
+    <Headers class="headers"></Headers>
+    <div class="content d-flex">
+      <side class="content-sticky" v-if="!isInWhiteList(route.path)"></side>
+      <div class="content-box d-flex flex-column" :style="`width: ${ !isInWhiteList(route.path) ? 'calc(100% - 230px)' : '100%'}`">
+        <div v-if="!isInWhiteList(route.path)" class="breadcrumbs_sticky">
+          <div class=" d-flex align-center justify-space-between">
+            <v-breadcrumbs :items="system.teacherBreadcrumbs" elevation="3">
+              <template v-slot:item="{ item }">
+                <span class="text" :class="{active: !item.disabled}" @click="toPath(item)">{{ item.text }}</span>
+              </template>
+            </v-breadcrumbs>
+          </div>
+          <v-divider></v-divider>
+        </div>
+        <div class="box pa-3">
+          <div v-if="!isInWhiteList(route.path)" class="box-content">
+            <router-view :key="key"></router-view>
+          </div>
+          <div v-else class="full">
+            <router-view :key="key"></router-view>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+defineOptions({ name: 'teacher-layout-index' })
+import Headers from './teacher/navBar.vue'
+import side from './teacher/side.vue'
+import { useRouter, useRoute } from 'vue-router'
+import { watch, computed } from 'vue'
+import { useSystem } from '@/store/system'
+// import { useUserStore } from '@/store/user'; const user = useUserStore()
+
+const router = useRouter()
+const route = useRoute()
+const system = useSystem()
+const key = computed(() => {
+  return route.path + Math.random()
+})
+
+const whiteList = [
+  '/recruit/teacher/studentList/detail',
+]
+// 查询是否在白名单内,在则不展示面包屑
+const isInWhiteList = (url)=> {
+  const path = url.split('?')[0]
+  for (const item of whiteList) {
+    if (path.startsWith(item)) {
+      return true
+    }
+  }
+  return false
+}
+
+watch(
+  () => route.matched,
+  async (val) => {
+    system.setTeacherBreadcrumbs(val, route.fullPath)
+    // await user.getSchoolInfo(false)
+  },
+  { immediate: true },
+  { deep: true }
+)
+
+const toPath = (item) => {
+  const { disabled, to } = item
+  if (disabled) {
+    event.preventDefault()
+    return
+  }
+  router.push(to)
+}
+</script>
+
+<style lang="scss" scoped>
+$top: 50px;
+.parent {
+  background-color: var(--default-bgc);
+  min-width: 1200px;
+}
+.headers {
+  position: sticky;
+  top: 0;
+  z-index: 999;
+}
+.box {
+  height: 0;
+  flex: 1;
+  height: calc(100vh - 50px);
+  &-content {
+    width: 100%;
+    min-height: 100%;
+    position: relative;
+    overflow-y: auto;
+    overflow-x: hidden;
+  }
+}
+.full {
+  height: calc(100vh - $top - 25px);
+  width: 100%;
+  flex: 1;
+  position: relative;
+  overflow-y: auto;
+  overflow-x: hidden;
+}
+.slider {
+  position: fixed;
+  bottom: 50%;
+  right: 24px;
+  translate: 0 50%;
+  z-index: 999;
+}
+.content {
+  height: 0;
+  flex: 1;
+
+  &-sticky {
+    position: sticky;
+    top: $top;
+    height: calc(100vh - $top);
+  }
+}
+.breadcrumbs_sticky {
+  position: sticky;
+  top: $top;
+  background: #FFF;
+  z-index: var(--zIndex-breadcrumbs);
+  border-left: 1px solid #eaecef;
+}
+.text {
+  color: var(--color-999);
+  font-size: 14px;
+  &.active {
+    color: var(--v-primary-base);
+    cursor: pointer;
+  }
+}
+/* 滚动条样式 */
+::-webkit-scrollbar {
+  -webkit-appearance: none;
+  width: 0px;
+  height: 0px;
+}
+/* 滚动条内的轨道 */
+::-webkit-scrollbar-track {
+  background: rgba(0, 0, 0, 0.1);
+  border-radius: 0;
+}
+/* 滚动条内的滑块 */
+::-webkit-scrollbar-thumb {
+  cursor: pointer;
+  border-radius: 5px;
+  background: rgba(0, 0, 0, 0.15);
+  transition: color 0.2s ease;
+}
+</style>

+ 122 - 0
src/layout/teacher/navBar.vue

@@ -0,0 +1,122 @@
+<template>
+  <div>
+    <v-toolbar class="banner font-size-14 pl-0" density="compact" style="height: 50px;">
+      <div class="innerBox d-flex justify-space-between">
+        <div class="nav-logo" style="cursor: pointer;" @click="handleLogoClick">
+          <v-img src="../../assets/logo.png"  aspect-ratio="16/9" contain :width="97" style="height: 40px"></v-img>
+        </div>
+        
+        <div class="d-flex user-nav align-center">
+          
+          <!-- 头像用户名 -->
+          <div class="d-flex align-center" v-if="showBall">
+            <v-menu open-on-hover>
+              <template v-slot:activator="{ props }">
+                <div class="d-flex ml-3 pl-2 align-center cursor-pointer" v-bind="props">
+                  <v-avatar>
+                    <v-img alt="" :src="getUserAvatar(schoolInfo?.avatar, schoolInfo?.sex)"></v-img>
+                  </v-avatar>
+                  <div class="ml-2 commonHover">
+                    {{ formatName(schoolInfo?.school?.name) + ' - ' + schoolInfo?.name ?? schoolInfo?.phone }}
+                  </div>
+                </div>
+              </template>
+
+              <v-list>
+                <v-list-item v-for="(item, index) in menuList" :key="index" @click="item.change">
+                  <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>
+          </div>
+        </div>
+      </div>
+    </v-toolbar>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getToken } from '@/utils/auth'
+import { useUserStore } from '@/store/user'; const userStore = useUserStore()
+import { useRouter } from 'vue-router'; const router = useRouter()
+import { useI18n } from '@/hooks/web/useI18n'; const { t } = useI18n()
+import { getUserAvatar } from '@/utils/avatar'
+import { formatName } from '@/utils/getText';
+defineOptions({ name: 'teacher-navbar' })
+
+defineProps({
+  sticky: {
+    type: Boolean,
+    default: true
+  }
+})
+
+const showBall = ref(false)
+
+onMounted(() => {
+  if (getToken()) {
+    showBall.value = true
+  }
+})
+
+const handleLogoClick = () => { window.open('/recruitHome') } // 点击logo
+
+// 退出登录
+const handleLogout = async () => {
+  await userStore.userLogout(1)
+  router.push({ path: '/flame' })
+}
+
+const menuList = ref([
+  { title: t('setting.logOut'), icon: 'mdi-logout', change: handleLogout }
+])
+
+// 学校信息
+let schoolInfo = ref(JSON.parse(localStorage.getItem('schoolInfo')) || {})
+
+userStore.$subscribe((mutation, state) => {
+  if (Object.keys(state.schoolInfo).length) schoolInfo.value = state.schoolInfo
+})
+
+</script>
+
+<style lang="scss" scoped>
+.banner {
+  width: 100%;
+  height: 50px;
+  z-index: var(--zIndex-nav) !important;
+  color: #fff;
+  background-color: var(--color-d5e6e8);
+  padding-left: 0px;
+  height: 50px;
+  font-size: 15px;
+}
+.hover:hover {
+  cursor: pointer;
+  background: rgba(0, 0, 0, 0.03);
+}
+.innerBox {
+  position: relative;
+  width: 100%;
+  align-items: center;
+  padding: 0 30px;
+}
+.nav-logo {
+  float: left;
+}
+.nav {
+  font-size: 0;
+  float: left;
+  margin-left: 50px;
+  height: 49px;
+  line-height: 49px;
+}
+.user-nav {
+  color: var(--color-333);
+  font-size: 15px;
+}
+</style>

+ 89 - 0
src/layout/teacher/side.vue

@@ -0,0 +1,89 @@
+<template>
+  <div>
+    <v-list class="side-box" color="primary">
+      <template v-for="item in list">
+        <template v-if="!item.children.length">
+          <v-list-item
+            :key="item.key"
+            active-class="active"
+            color="primary"
+            :href="item.path"
+            :to="item.path"
+            rounded="shaped"
+            :prepend-icon="item.icon"
+            :title="getCurrentLocaleLang() === 'zh_CN' ? item.title : item.enName"
+          >
+          </v-list-item>
+        </template>
+        <v-list-group
+          v-else
+          color="primary"
+          rounded="shaped"
+          :key="`${item.path}_${item.title}`"
+          :prepend-icon="item.icon"
+        >
+          <template v-slot:activator="{ props }">
+            <v-list-item v-bind="props" :title="getCurrentLocaleLang() === 'zh_CN' ? item.title : item.enName"></v-list-item>
+          </template>
+          <v-list-item
+            v-for="(val, i) in item.children"
+            :key="i"
+            color="primary"
+            :href="val.path"
+            style="padding-left: 40px;"
+            :to="val.path"
+            :title="getCurrentLocaleLang() === 'zh_CN' ? val.title : val.enName"
+            rounded="shaped"
+          ></v-list-item>
+        </v-list-group>
+      </template>
+    </v-list>
+  </div>
+</template>
+
+<script setup>
+defineOptions({ name: 'teacher-side'})
+import { computed } from 'vue'
+import { getCurrentLocaleLang } from '@/utils/lang.js'
+import routeList from '@/router/modules/components/recruit/teacher'
+
+const list = computed(() => {
+  return getList(routeList)
+})
+
+const MENU_TYPE = {
+  CATALOGUE: 1,
+  MENU: 2,
+  BUTTON: 3
+}
+
+const getList = (arr, obj = [], root = '') => {
+  arr.forEach((element, index) => {
+    if (element.show) return
+    let data = {}
+    const path = root + element.path
+    data = {
+      key: element?.name + index,
+      title: element?.name,
+      enName: element?.meta?.enName,
+      icon: element?.icon,
+      path: MENU_TYPE.CATALOGUE === element.type ? path + '/index' : path,
+      children: []
+    }
+    if (element?.children) {
+      getList(element.children, data.children, path + '/')
+    }
+    obj.push(data)
+  })
+
+  return obj
+}
+
+</script>
+
+<style scoped lang="scss">
+.side-box {
+  width: 230px;
+  height: 100%;
+}
+</style>

+ 0 - 4
src/main.js

@@ -32,7 +32,6 @@ import VueLuckyCanvas from '@lucky-canvas/vue'
 import router from './router'
 
 import { ellipsisTooltip } from '@/directives/ellipsisTooltip/index.js'
-// import imageDirective from '@/directives/previewImageDirective'
 
 import './permission'
 
@@ -48,10 +47,7 @@ app.use(pinia)
 app.use(router)
 app.use(Clipboard)
 app.use(VueLuckyCanvas)
-// imageDirective(app)
 
-
-// app.config.globalProperties.$echarts = echarts
 registerPlugins(app)
 app.component('VueDatePicker', VueDatePicker)
 

+ 2 - 2
src/plugins/dialogExtend/components/infoForm.vue

@@ -30,7 +30,7 @@
     </CtForm>
   </div>
   <ImgCropper :visible="isShowCopper" :image="selectPic" :cropBoxResizable="true" @submit="handleHideCopper" :aspectRatio="1 / 1" @close="isShowCopper = false"></ImgCropper>
-  
+
   <!-- 选择本地简历 -->
   <CtDialog
     :visible="openUploadDialog"
@@ -457,4 +457,4 @@ defineExpose({
     border-radius: 50%;
   }
 }
-</style>
+</style>

+ 1 - 1
src/plugins/dialogExtend/components/positionAd.vue

@@ -1,4 +1,4 @@
-<!-- 完善个人信息 -->
+<!-- 职位广告 -->
 <template>
   <v-app>
     <v-dialog

+ 2 - 2
src/plugins/dialogExtend/components/studentInfoForm.vue

@@ -151,7 +151,7 @@ const items = ref({
       label: '所在院系 *',
       outlined: true,
       itemText: 'departmentTitle',
-      itemValue: 'schoolDepartmentId',
+      itemValue: 'departmentTitle',
       rules: [v => !!v || '请选择所在院系'],
       items: []
     },
@@ -342,4 +342,4 @@ defineExpose({
     border-radius: 50%;
   }
 }
-</style>
+</style>

+ 43 - 17
src/router/modules/common.js

@@ -1,6 +1,7 @@
 // 公共路由(任何身份都可以访问的路由 如:商城)
 
-import headhunting from './components/headhunting'
+import Layout from '@/layout'
+import { setCommonPage } from '@/utils/dealData'
 
 const common = [
   {
@@ -17,14 +18,6 @@ const common = [
       // }
     ]
   },
-  // { // 暂停维护
-  //   path: '/shareJob',
-  //   name: 'shareJob',
-  //   meta: {
-  //     title: '分享职位'
-  //   },
-  //   component: () => import('@/views/recruit/personal/shareJob/index.vue')
-  // },
   // 邀请页
   {
     path: '/invite',
@@ -55,13 +48,6 @@ const common = [
           title: '注册新企业'
         },
       },
-      {
-        path: '/recruit/entRegister/joiningEnterprise',
-        component: () => import('@/views/recruit/entRegister/joiningEnterprise'),
-        meta: {
-          title: '加入企业'
-        }
-      },
       {
         path: '/recruit/entRegister/inReview',
         component: () => import('@/views/recruit/entRegister/inReview.vue'),
@@ -102,7 +88,47 @@ const common = [
       hideSide: true
     }
   },
-  ...headhunting
+  {
+    path: '/headhunting',
+    component: Layout,
+    meta: {
+      title: '门墩儿猎寻服务'
+    },
+    children: [
+      {
+        path:'/headhunting',
+        component: () => import('@/views/headhunting/index.vue')
+      }
+    ]
+  },
+  {
+    path: '/headhunting/service',
+    component: Layout,
+    meta: {
+      title: '我们的服务'
+    },
+    children: [
+      {
+        path: '/headhunting/service',
+        component: () => import('@/views/headhunting/service.vue')
+      }
+    ]
+  },
+  {
+    path: '/headhunting/service/details',
+    component: Layout,
+    meta: {
+      title: '门墩儿猎寻服务'
+    },
+    children: [
+      {
+        path: '/headhunting/service/details',
+        component: () => import('@/views/headhunting/details.vue')
+      }
+    ]
+  }
 ]
 
+setCommonPage(common) // 公共访问路由
+
 export default common

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

@@ -1,45 +0,0 @@
-import Layout from '@/layout'
-
-const headhunting = [
-  {
-    path: '/headhunting',
-    component: Layout,
-    meta: {
-      title: '门墩儿猎寻服务'
-    },
-    children: [
-      {
-        path:'/headhunting',
-        component: () => import('@/views/headhunting/index.vue')
-      }
-    ]
-  },
-  {
-    path: '/headhunting/service',
-    component: Layout,
-    meta: {
-      title: '我们的服务'
-    },
-    children: [
-      {
-        path: '/headhunting/service',
-        component: () => import('@/views/headhunting/service.vue')
-      }
-    ]
-  },
-  {
-    path: '/headhunting/service/details',
-    component: Layout,
-    meta: {
-      title: '门墩儿猎寻服务'
-    },
-    children: [
-      {
-        path: '/headhunting/service/details',
-        component: () => import('@/views/headhunting/details.vue')
-      }
-    ]
-  }
-]
-
-export default headhunting

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

@@ -71,6 +71,68 @@ const personCenter = [
               }
             ]
           },
+          // 学生专区
+          {
+            path: '/recruit/personal/personalCenter/student',
+            redirect: '/recruit/personal/personalCenter/student/information',
+            name: 'Student',
+            permissionName: 'studentInformation',
+            meta: {
+              title: '学生专区',
+              enName: 'Student Information',
+              icon: 'mdi-account-school-outline'
+            },
+            children: [
+              {
+                path: '/recruit/personal/personalCenter/student/information',
+                component: () => import('@/views/recruit/personal/PersonalCenter/student/information/index.vue'),
+                meta: {
+                  title: '学生信息',
+                  enName: 'Student Information'
+                }
+              },
+              {
+                path: '/recruit/personal/personalCenter/student/internshipCompany',
+                component: () => import('@/views/recruit/personal/PersonalCenter/student/intershipCompany/index.vue'),
+                meta: {
+                  title: '实习记录',
+                  enName: 'Internship Company'
+                }
+              },
+              {
+                path: '/recruit/personal/personalCenter/student/internshipReport',
+                component: () => import('@/views/recruit/personal/PersonalCenter/student/InternshipReport/index.vue'),
+                meta: {
+                  title: '实习报告',
+                  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/internshipButler',
+                component: () => import('@/views/recruit/personal/PersonalCenter/student/InternshipButler/index.vue'),
+                meta: {
+                  title: '实习管家',
+                  enName: 'Internship Butler'
+                }
+              }
+            ]
+          },
           // 赏金奖励
           {
             path: '/recruit/personal/personalCenter/bountyRewards',
@@ -175,68 +237,6 @@ const personCenter = [
               enName: 'Shipping Address',
               icon: 'mdi-map-marker-outline'
             }
-          },
-          // 学生专区
-          {
-            path: '/recruit/personal/personalCenter/student',
-            redirect: '/recruit/personal/personalCenter/student/information',
-            name: 'Student',
-            permissionName: 'studentInformation',
-            meta: {
-              title: '学生专区',
-              enName: 'Student Information',
-              icon: 'mdi-account-school-outline'
-            },
-            children: [
-              {
-                path: '/recruit/personal/personalCenter/student/information',
-                component: () => import('@/views/recruit/personal/PersonalCenter/student/information/index.vue'),
-                meta: {
-                  title: '学生信息',
-                  enName: 'Student Information'
-                }
-              },
-              {
-                path: '/recruit/personal/personalCenter/student/internshipCompany',
-                component: () => import('@/views/recruit/personal/PersonalCenter/student/intershipCompany/index.vue'),
-                meta: {
-                  title: '实习企业',
-                  enName: 'Internship Company'
-                }
-              },
-              {
-                path: '/recruit/personal/personalCenter/student/internshipReport',
-                component: () => import('@/views/recruit/personal/PersonalCenter/student/InternshipReport/index.vue'),
-                meta: {
-                  title: '实习报告',
-                  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/internshipButler',
-                component: () => import('@/views/recruit/personal/PersonalCenter/student/InternshipButler/index.vue'),
-                meta: {
-                  title: '实习管家',
-                  enName: 'Internship Butler'
-                }
-              }
-            ]
           }
         ]
       }

+ 132 - 0
src/router/modules/components/recruit/teacher.js

@@ -0,0 +1,132 @@
+// 教师路由信息
+import Layout from '@/layout/teacher.vue'
+
+const teacher = [
+  {
+    path: '/recruit/teacher',
+    show: true,
+    redirect: '/recruit/teacher/studentList/index',
+  },
+  {
+    path: '/recruit/teacher/studentList',
+    component: Layout,
+    name: '学生列表',
+    icon: 'mdi-account-school-outline',
+    meta: {
+      enName: 'Student List'
+    },
+    children: [
+      {
+        path: 'index',
+        name: '学生列表',
+        meta: {
+          enName: 'Student List',
+        },
+        show: true,
+        component: () => import('@/views/recruit/teacher/studentList/index.vue')
+      },
+      {
+        path: 'detail/:id',
+        show: true,
+        name: '学生详情',
+        meta: {
+          enName: 'Student Details'
+        },
+        component: () => import('@/views/recruit/teacher/studentList/studentDetails.vue')
+      },
+    ]
+  },
+  {
+    path: '/recruit/teacher/internshipSituation',
+    component: Layout,
+    name: '实习情况',
+    icon: 'mdi-town-hall',
+    meta: {
+      enName: 'Internship Situation'
+    },
+    children: [
+      {
+        path: 'index',
+        name: '实习情况',
+        meta: {
+          enName: 'Internship Situation',
+        },
+        show: true,
+        component: () => import('@/views/recruit/teacher/internshipSituation/index.vue')
+      },
+    ]
+  },
+  {
+    path: '/recruit/teacher/internshipReport',
+    component: Layout,
+    name: '实习报告',
+    icon: 'mdi-file-document-multiple-outline',
+    meta: {
+      enName: 'Internship Report'
+    },
+    children: [
+      {
+        path: 'index',
+        name: '实习报告',
+        meta: {
+          enName: 'Internship Report',
+        },
+        show: true,
+        component: () => import('@/views/recruit/teacher/internshipReport/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',
+    component: Layout,
+    name: '账号信息',
+    icon: 'mdi-human-male-board',
+    meta: {
+      enName: 'Teacher Certification'
+    },
+    children: [
+      {
+        path: 'index',
+        name: '账号信息',
+        meta: {
+          enName: 'Teacher Certification',
+        },
+        show: true,
+        component: () => import('@/views/recruit/teacher/teacherCertification/index.vue')
+      },
+    ]
+  },
+]
+
+function traverse(list, type) {
+  list.forEach(e => {
+    if (!e.type) e.type = type
+    if (!e.meta) e.meta = {}
+    if (!e.meta.title) e.meta.title = e.name
+    if (e.children?.length) traverse(e.children, 2)
+  })
+}
+traverse(teacher, 1)
+
+export default teacher

+ 56 - 5
src/router/modules/recruit.js

@@ -1,9 +1,11 @@
 
 // 门墩儿招聘
+import teacher from './components/recruit/teacher'
 import enterprise from './components/recruit/enterprise'
 import personal from './components/recruit/personal'
 import Layout from '@/layout'
-import { setLoginType } from '@/utils/loginType'
+import { setCommonPage } from '@/utils/dealData'
+
 const recruit = [
   {
     path: '/recruit',
@@ -101,7 +103,7 @@ const recruit = [
     children: [
       {
         path: '/pointsExchange',
-        component: () => import('@/views/mall copy/index.vue'),
+        component: () => import('@/views/mallCopy/index.vue'),
         name: 'pointsExchange',
         meta: {
           title: '门墩儿商城'
@@ -123,6 +125,20 @@ const recruit = [
       }
     ]
   },
+  {
+    path: '/flame',
+    component: Layout,
+    children: [
+      {
+        path: '/flame',
+        component: () => import('@/views/flame/index'),
+        name: 'flame',
+        meta: {
+          title: '火苗儿校企'
+        }
+      }
+    ]
+  },
   {
     path: '/recruit/personal/position',
     component: Layout,
@@ -162,6 +178,40 @@ const recruit = [
       }
     ]
   },
+  {
+    path: '/recruit/personal/jobFair',
+    component: Layout,
+    name: 'jobFair',
+    meta: {
+      title: '招聘会'
+    },
+    children: [
+      {
+        path: '/recruit/personal/jobFair',
+        component: () => import('@/views/recruit/personal/jobFair/index.vue')
+      },
+    ]
+  },
+  {
+    path: '/recruit/personal/jobFair/:id',
+    component: () => import('@/views/recruit/personal/jobFair/details/index.vue'),
+    name: 'jobFairDetails'
+  },
+  {
+    path: '/recruit/personal/jobFair/enterprises/:id',
+    component: () => import('@/views/recruit/personal/jobFair/details/enterprises.vue'),
+    name: 'jobFairEnterprises'
+  },
+  {
+    path: '/recruit/personal/jobFair/position/:id',
+    component: () => import('@/views/recruit/personal/jobFair/details/position.vue'),
+    name: 'jobFairPosition'
+  },
+  {
+    path: '/recruit/personal/jobFair/entPosition/:id',
+    component: () => import('@/views/recruit/personal/jobFair/details/entJobCard.vue'),
+    name: 'jobFairEntPosition'
+  },
   {
     path: '/recruit/personal/company/details/:id',
     component: Layout,
@@ -235,13 +285,14 @@ const recruit = [
     },
 ]
 
-setLoginType(recruit, 'personalCommon'),
-setLoginType(enterprise, 'enterprise'),
-setLoginType(personal, 'personal')
+
+setCommonPage(recruit) // 公共访问路由
+setCommonPage(recruit) // 公共访问路由
 
 const routeArray = [
   ...recruit,
   ...enterprise,
+  ...teacher,
   ...personal
 ]
 export default routeArray

+ 59 - 18
src/router/modules/remaining.js

@@ -1,7 +1,8 @@
 import common from './common'
 import recruit from './recruit'
 // import Layout from '@/layout'
-import { setLoginType } from '@/utils/loginType'
+import { setCommonPage } from '@/utils/dealData'
+
 
 
 const remainingRouter = [
@@ -10,10 +11,20 @@ const remainingRouter = [
     component: () => import('@/views/login/index'),
     name: 'login',
     meta: {
+      commonPage: true,
       hidden: true,
       title: '登录/注册'
     }
   },
+  {
+    path: '/flameLogin',
+    component: () => import('@/views/login/flameLogin'),
+    name: 'flameLogin',
+    meta: {
+      hidden: true,
+      title: '老师登录/注册'
+    }
+  },
   {
     path: '/register/selected',
     component: () => import('@/views/register/select.vue'),
@@ -32,6 +43,33 @@ const remainingRouter = [
       title: '个人用户选择角色'
     }
   },
+  {
+    path: '/register/school',
+    component: () => import('@/views/register/school.vue'),
+    name: 'registerShool',
+    meta: {
+      hidden: true,
+      title: '老师注册'
+    }
+  },
+  {
+    path: '/register/schoolIndex',
+    component: () => import('@/views/register/schoolForm.vue'),
+    name: 'registerShoolForm',
+    meta: {
+      hidden: true,
+      title: '老师注册'
+    }
+  },
+  {
+    path: '/register/school/inReview',
+    component: () => import('@/views/register/schoolInReview.vue'),
+    name: 'registerShoolInReview',
+    meta: {
+      hidden: true,
+      title: '老师注册详情'
+    }
+  },
   {
     path: '/register/person',
     component: () => import('@/views/register/person.vue'),
@@ -133,25 +171,28 @@ const remainingRouter = [
       }
     ]
   },
-  // {
-  //   path: '/404',
-  //   name: '404Page',
-  //   component: () => import('@/views/404/index.vue'),
-  //   meta: {
-  //     title: '404'
-  //   }
-  // },
-  // {
-  //   path: '/:catchAll(.*)',
-  //   redirect: '/404'
-  // }
+  {
+    path: '/404',
+    name: '404Page',
+    component: () => import('@/views/404/index.vue'),
+    meta: {
+      title: '404'
+    }
+  },
+  {
+    path: '/permissionPrompt',
+    name: 'permissionPrompt',
+    component: () => import('@/views/recruit/enterprise/permissionPrompt/index.vue'),
+    meta: {
+      title: '无权限提示'
+    }
+  }
 ]
-setLoginType(remainingRouter, 'personalCommon') // 暂定:登录企业端不能访问personalCommon路由
-setLoginType(common, 'common') // common 没有身份,任何情况都能访问
+setCommonPage(remainingRouter) // 公共访问路由
+
 const routeArray = [
-  ...remainingRouter,
   ...recruit,
-  ...common
+  ...common,
+  ...remainingRouter
 ]
-
 export default routeArray

+ 32 - 0
src/store/system.js

@@ -51,10 +51,42 @@ export const useSystem = defineStore('system',
       }
       breadcrumbs.value =  arr
     }
+
+    // 教师
+    const teacherBreadcrumbs = ref([])
+    const setTeacherBreadcrumbs = (matched, fullPath) => {
+      const _fullPath = fullPath.split('/')
+      const arr = matched.map((item, index) => {
+        // 重组路径
+        if (item.path === matched[index - 1]?.path) return false
+        const text = item.meta.title
+        const _path = item.path.split('/')
+        const obj = {
+          text,
+          to: _path.map((e, i) => _fullPath[i]).join('/')
+        }
+        if (item.children && item.children.length) {
+          if (!item.children.find(e => e.path === 'index')) {
+            obj.to += `/${item.children[0].path}`
+            return obj
+          }
+          obj.to += '/index'
+          return obj
+        }
+        return obj
+      }).filter(e => e) || []
+      if (arr?.length) {
+        arr[arr.length - 1].disabled = true
+        arr[arr.length - 1].link = true
+      }
+      teacherBreadcrumbs.value =  arr
+    }
     return {
       systemInfo,
       setTimeDifference,
       getTimeDifference,
+      setTeacherBreadcrumbs,
+      teacherBreadcrumbs,
       setBreadcrumbs,
       breadcrumbs
     }

+ 44 - 7
src/store/user.js

@@ -19,6 +19,8 @@ import { updateEventList } from '@/utils/eventList'
 import { getBaseInfoDictOfName } from '@/utils/getText'
 import { checkPersonBaseInfo } from '@/utils/check'
 import { getStudentInfo } from '@/api/recruit/personal/resume'
+import { getSchoolInformation } from '@/api/school'
+import router from '@/router'
 // import Confirm from '@/plugins/confirm'
 
 // import { useIMStore } from './im'
@@ -37,6 +39,7 @@ export const useUserStore = defineStore('user',
       userAccount: {}, // 用户账户信息
       enterpriseUserAccount: {}, // 企业账户信息
       studentInfo: localStorage.getItem('studentInfo') ? JSON.parse(localStorage.getItem('studentInfo')) : {}, // 学生信息
+      schoolInfo: localStorage.getItem('schoolInfo') ? JSON.parse(localStorage.getItem('schoolInfo')) : {}, // 学校信息
     }),
     actions: {
       // 个人用户注册并登录
@@ -68,6 +71,7 @@ export const useUserStore = defineStore('user',
             updateEventList(true) // 获取规则配置跟踪列表
             await this.getUserInfos()
             await this.getUserBaseInfos('', { chooseRole: data.chooseRole })
+            if (data?.schoolRegister) await this.getSchoolInfo(true)
             resolve(res)
           }).catch(err => { reject(err) })
         })
@@ -115,11 +119,12 @@ export const useUserStore = defineStore('user',
           data = data || {}
           this.baseInfo = await this.getFieldText(data)
           localStorage.setItem('baseInfo', JSON.stringify(this.baseInfo))
-          localStorage.setItem('necessaryInfoReady', checkPersonBaseInfo(this.baseInfo) ? 'ready' : 'fddeaddc47868b') // 校验是否完善人才必填信息
-          // if (option?.chooseRole && import.meta.env.VITE_NODE_ENV !== 'production') {
+          localStorage.setItem('necessaryInfoReady', !option?.chooseRole || checkPersonBaseInfo(this.baseInfo) ? 'ready' : 'fddeaddc47868b')
+
+          if (option?.chooseRole && import.meta.env.VITE_NODE_ENV !== 'production') {
           //   // 刚注册时让用户选择学生用户还是求职者用户,角色不同填写的基本信息不同。
-          //   localStorage.setItem('chooseRole', 'showChooseRole')
-          // }
+            localStorage.setItem('chooseRole', 'showChooseRole')
+          }
           // 当前角色若为学生则获取学生信息
           if (data?.type && Number(data.type) === 1) this.getStudentInformation()
         } catch (error) {
@@ -164,6 +169,10 @@ export const useUserStore = defineStore('user',
       },
       // 切换为招聘者
       async changeRole (res) {
+        // 切换企业时需将个人的账户信息另外储存起来,以防企业账户有角色无菜单权限返回首页清除企业信息时个人账户信息丢失
+        const perAccountData = JSON.parse(localStorage.getItem('accountInfo'))
+        localStorage.setItem('perAccountInfo', JSON.stringify(perAccountData))
+
         let data
         if (res?.type === 'emailLogin') {
           data = res
@@ -180,7 +189,7 @@ export const useUserStore = defineStore('user',
 
         // 企业受邀加入企业,只保存token等操作。
         if (res?.onlySetToken) return
-
+        
         await this.updatePasswordCheck() // 检查密码是否需要修改
         await this.getEnterpriseInfo()
         await this.getEnterpriseUserAccountInfo()
@@ -213,7 +222,6 @@ export const useUserStore = defineStore('user',
         const data = await getEnterpriseUserAccount()
         if (!data) return
         this.enterpriseUserAccount = data
-        // this.getUserAccountBalance()
         localStorage.setItem('enterpriseUserAccount', JSON.stringify(data))
         return data // 方便直接获取
       },
@@ -238,7 +246,6 @@ export const useUserStore = defineStore('user',
         if (!data) return
         this.userAccount = data
         this.getUserAccountBalance()
-        // localStorage.setItem('userAccount', JSON.stringify(data))
       },
       // 获取账户余额
       async getUserAccountBalance () {
@@ -265,6 +272,36 @@ export const useUserStore = defineStore('user',
         } catch (error) {
           Snackbar.error(error.msg)
         }
+      },
+
+      // 获取学校基本信息
+      async getSchoolInfo (isRegister = false) {
+        const data = await getSchoolInformation()
+        this.schoolInfo = data || {}
+        localStorage.setItem('schoolInfo', data ? JSON.stringify(data) : '{}')
+
+        // 注册时执行下方内容
+        if (!isRegister) return
+
+        if (!data || !Object.keys(data).length) {
+          console.log('没有注册过,直接跳转到学校注册页面')
+          router.push({ path: '/register/schoolIndex' })
+        }
+        else if (data?.authStatus === '0') {
+          console.log('审核中,等待审核')
+          localStorage.setItem('registerSchoolInfo', JSON.stringify(data))
+          router.push({ path: '/register/school/inReview' })
+        }
+        else if (data?.authStatus === '1') {
+          localStorage.setItem('schoolInfo', JSON.stringify(data))
+          console.log('审核通过直接进入老师页面')
+          router.push('/recruit/teacher') 
+        }
+        else if (data?.authStatus === '2') {
+          console.log('审核不通过,重新填写信息提交')
+          localStorage.setItem('registerSchoolInfo', JSON.stringify(data))
+          router.push({ path: '/register/school/inReview' })
+        }
       }
     }
   },

+ 4 - 0
src/styles/index.css

@@ -185,6 +185,10 @@
   background-color: #fff;
 }
 
+.default-bgc {
+  background-color: #f2f4f7;
+}
+
 .ellipsis {
   white-space: nowrap;
   text-overflow: ellipsis;

文件差異過大導致無法顯示
+ 0 - 0
src/styles/index.min.css


+ 3 - 0
src/styles/index.scss

@@ -184,6 +184,9 @@
 .white-bgc {
   background-color: #fff;
 }
+.default-bgc {
+  background-color: #f2f4f7;
+}
 
 .ellipsis {
   white-space: nowrap;

+ 0 - 1
src/styles/recruit/position/index.css

@@ -13,7 +13,6 @@
   display: inline-block;
   color: #0E100F;
   font-size: 28px;
-  margin-right: 30px;
   margin-top: 1px;
   vertical-align: middle;
 }

+ 1 - 1
src/styles/recruit/position/index.scss

@@ -11,7 +11,7 @@
   display: inline-block;
   color: #0E100F;
   font-size: 28px;
-  margin-right: 30px;
+  // margin-right: 30px;
   margin-top: 1px;
   // max-width: 360px;
   vertical-align: middle;

+ 20 - 13
src/utils/auth.js

@@ -1,5 +1,4 @@
 const ENTERPRISE_PATH = '/recruit/enterprise'
-
 // 是否是企业路由
 export const getIsEnterprise = () => {
   const PATH_NAME = window.location.pathname
@@ -9,32 +8,40 @@ export const getIsEnterprise = () => {
   return PATH_NAME.includes(ENTERPRISE_PATH)
 }
 
+const TEACHER_PATH = '/recruit/teacher'
+// 是否是教师路由
+export const getIsTeacher = () => {
+  const PATH_NAME = window.location.pathname
+  return PATH_NAME.includes(TEACHER_PATH)
+}
+
+
+const ACCESS_TOKEN_NAME = ['ENT_ACCESS_TOKEN', 'PER_ACCESS_TOKEN']
+const REFRESH_TOKEN_NAME = ['ENT_REFRESH_TOKEN', 'PER_REFRESH_TOKEN']
+
 // 获取token
-export const getToken = (index = 2) => { // index=1: 使用招聘token; index=2: 使用求职token
-  const arr = ['ENT_ACCESS_TOKEN', 'PER_ACCESS_TOKEN']
-  return localStorage.getItem(arr[index-1])
+export const getToken = (index = 2) => { // index: 1-招聘; 2-求职;
+  return localStorage.getItem(ACCESS_TOKEN_NAME[index-1])
 }
 
 // 设置token
-export const setToken = (token, index = 2) => { // index=1: 招聘token; index=2: 求职token
-  return localStorage.setItem((index === 1 ? 'ENT_ACCESS_TOKEN' : 'PER_ACCESS_TOKEN'), token)
+export const setToken = (token, index = 2) => { // index: 1-招聘; 2-求职;
+  return localStorage.setItem(ACCESS_TOKEN_NAME[index-1], token)
 }
 
 // 清除token
 export const removeToken = () => {
-  localStorage.removeItem('ENT_ACCESS_TOKEN')
-  localStorage.removeItem('PER_ACCESS_TOKEN')
-  localStorage.removeItem('ENT_REFRESH_TOKEN')
-  localStorage.removeItem('PER_REFRESH_TOKEN')
+  ACCESS_TOKEN_NAME.forEach(item => localStorage.removeItem(item))
+  REFRESH_TOKEN_NAME.forEach(item => localStorage.removeItem(item))
 }
 // 获取刷新token
 export const getRefreshToken = (index) => {
-  return localStorage.getItem(index === 1 ? 'ENT_REFRESH_TOKEN' : 'PER_REFRESH_TOKEN')
+  return localStorage.getItem(ACCESS_TOKEN_NAME[index-1])
 }
 
 // 设置刷新token
-export const setRefreshToken = (refreshToken, index = 2) => { // index=1: 招聘token; index=2: 求职token
-  return localStorage.setItem((index === 1 ? 'ENT_REFRESH_TOKEN' : 'PER_REFRESH_TOKEN'), refreshToken)
+export const setRefreshToken = (refreshToken, index = 2) => { // index: 1-招聘; 2-求职;
+  return localStorage.setItem((REFRESH_TOKEN_NAME[index-1]), refreshToken)
 }
 
 // 租户ID

+ 1 - 1
src/utils/date.js

@@ -168,4 +168,4 @@ export const getTimeDifferenceInChinese = (startTime, endTime) => {
     result += "个月";
   }
   return result
-}
+}

+ 12 - 0
src/utils/dealData.js

@@ -47,3 +47,15 @@ export const findFirstDuplicateWithIndices = (arr) => {
   // 如果没有找到重复元素,返回 null 或其他表示未找到的值
   return null;
 }
+
+export const setCommonPage = (arr) => {
+  traverse(arr)
+
+  function traverse(list) {
+    list.forEach(e => {
+      if (!e.meta) e.meta = {}
+      if (e.meta.commonPage !== false) e.meta.commonPage = true
+      if (e.children?.length) traverse(e.children)
+    })
+  }
+}

+ 0 - 11
src/utils/loginType.js

@@ -1,11 +0,0 @@
-export const setLoginType = (arr, type) => {
-  traverse(arr)
-
-  function traverse(list) {
-    list.forEach(e => {
-      if (!e.meta) e.meta = {}
-      if (!e.meta.loginType) e.meta.loginType = type // 存在的话取e.meta.loginType,meta.loginType优先级高于这里默认的type
-      if (e.children?.length) traverse(e.children)
-    })
-  }
-}

+ 403 - 0
src/utils/tree.ts

@@ -0,0 +1,403 @@
+interface TreeHelperConfig {
+  id: string
+  children: string
+  pid: string
+}
+
+type Fn = Function
+
+const DEFAULT_CONFIG: TreeHelperConfig = {
+  id: 'id',
+  children: 'children',
+  pid: 'pid'
+}
+export const defaultProps = {
+  children: 'children',
+  label: 'name',
+  value: 'id',
+  isLeaf: 'leaf',
+  emitPath: false // 用于 cascader 组件:在选中节点改变时,是否返回由该节点所在的各级菜单的值所组成的数组,若设置 false,则只返回该节点的值
+}
+
+const getConfig = (config: Partial<TreeHelperConfig>) => Object.assign({}, DEFAULT_CONFIG, config)
+
+// tree from list
+export const listToTree = <T = any>(list: any[], config: Partial<TreeHelperConfig> = {}): T[] => {
+  const conf = getConfig(config) as TreeHelperConfig
+  const nodeMap = new Map()
+  const result: T[] = []
+  const { id, children, pid } = conf
+
+  for (const node of list) {
+    node[children] = node[children] || []
+    nodeMap.set(node[id], node)
+  }
+  for (const node of list) {
+    const parent = nodeMap.get(node[pid])
+    ;(parent ? parent.children : result).push(node)
+  }
+  return result
+}
+
+export const treeToList = <T = any>(tree: any, config: Partial<TreeHelperConfig> = {}): T => {
+  config = getConfig(config)
+  const { children } = config
+  const result: any = [...tree]
+  for (let i = 0; i < result.length; i++) {
+    if (!result[i][children!]) continue
+    result.splice(i + 1, 0, ...result[i][children!])
+  }
+  return result
+}
+
+export const findNode = <T = any>(
+  tree: any,
+  func: Fn,
+  config: Partial<TreeHelperConfig> = {}
+): T | null => {
+  config = getConfig(config)
+  const { children } = config
+  const list = [...tree]
+  for (const node of list) {
+    if (func(node)) return node
+    node[children!] && list.push(...node[children!])
+  }
+  return null
+}
+
+export const findNodeAll = <T = any>(
+  tree: any,
+  func: Fn,
+  config: Partial<TreeHelperConfig> = {}
+): T[] => {
+  config = getConfig(config)
+  const { children } = config
+  const list = [...tree]
+  const result: T[] = []
+  for (const node of list) {
+    func(node) && result.push(node)
+    node[children!] && list.push(...node[children!])
+  }
+  return result
+}
+
+export const findPath = <T = any>(
+  tree: any,
+  func: Fn,
+  config: Partial<TreeHelperConfig> = {}
+): T | T[] | null => {
+  config = getConfig(config)
+  const path: T[] = []
+  const list = [...tree]
+  const visitedSet = new Set()
+  const { children } = config
+  while (list.length) {
+    const node = list[0]
+    if (visitedSet.has(node)) {
+      path.pop()
+      list.shift()
+    } else {
+      visitedSet.add(node)
+      node[children!] && list.unshift(...node[children!])
+      path.push(node)
+      if (func(node)) {
+        return path
+      }
+    }
+  }
+  return null
+}
+
+export const findPathAll = (tree: any, func: Fn, config: Partial<TreeHelperConfig> = {}) => {
+  config = getConfig(config)
+  const path: any[] = []
+  const list = [...tree]
+  const result: any[] = []
+  const visitedSet = new Set(),
+    { children } = config
+  while (list.length) {
+    const node = list[0]
+    if (visitedSet.has(node)) {
+      path.pop()
+      list.shift()
+    } else {
+      visitedSet.add(node)
+      node[children!] && list.unshift(...node[children!])
+      path.push(node)
+      func(node) && result.push([...path])
+    }
+  }
+  return result
+}
+
+export const filter = <T = any>(
+  tree: T[],
+  func: (n: T) => boolean,
+  config: Partial<TreeHelperConfig> = {}
+): T[] => {
+  config = getConfig(config)
+  const children = config.children as string
+
+  function listFilter(list: T[]) {
+    return list
+      .map((node: any) => ({ ...node }))
+      .filter((node) => {
+        node[children] = node[children] && listFilter(node[children])
+        return func(node) || (node[children] && node[children].length)
+      })
+  }
+
+  return listFilter(tree)
+}
+
+export const forEach = <T = any>(
+  tree: T[],
+  func: (n: T) => any,
+  config: Partial<TreeHelperConfig> = {}
+): void => {
+  config = getConfig(config)
+  const list: any[] = [...tree]
+  const { children } = config
+  for (let i = 0; i < list.length; i++) {
+    // func 返回true就终止遍历,避免大量节点场景下无意义循环,引起浏览器卡顿
+    if (func(list[i])) {
+      return
+    }
+    children && list[i][children] && list.splice(i + 1, 0, ...list[i][children])
+  }
+}
+
+/**
+ * @description: Extract tree specified structure
+ */
+export const treeMap = <T = any>(
+  treeData: T[],
+  opt: { children?: string; conversion: Fn }
+): T[] => {
+  return treeData.map((item) => treeMapEach(item, opt))
+}
+
+/**
+ * @description: Extract tree specified structure
+ */
+export const treeMapEach = (
+  data: any,
+  { children = 'children', conversion }: { children?: string; conversion: Fn }
+) => {
+  const haveChildren = Array.isArray(data[children]) && data[children].length > 0
+  const conversionData = conversion(data) || {}
+  if (haveChildren) {
+    return {
+      ...conversionData,
+      [children]: data[children].map((i: number) =>
+        treeMapEach(i, {
+          children,
+          conversion
+        })
+      )
+    }
+  } else {
+    return {
+      ...conversionData
+    }
+  }
+}
+
+/**
+ * 递归遍历树结构
+ * @param treeDatas 树
+ * @param callBack 回调
+ * @param parentNode 父节点
+ */
+export const eachTree = (treeDatas: any[], callBack: Fn, parentNode = {}) => {
+  treeDatas.forEach((element) => {
+    const newNode = callBack(element, parentNode) || element
+    if (element.children) {
+      eachTree(element.children, callBack, newNode)
+    }
+  })
+}
+
+/**
+ * 构造树型结构数据
+ * @param {*} data 数据源
+ * @param {*} id id字段 默认 'id'
+ * @param {*} parentId 父节点字段 默认 'parentId'
+ * @param {*} children 孩子节点字段 默认 'children'
+ */
+export const handleTree = (data: any[], id?: string, parentId?: string, children?: string) => {
+  if (!Array.isArray(data)) {
+    console.warn('data must be an array')
+    return []
+  }
+  const config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenList: children || 'children'
+  }
+
+  const childrenListMap = {}
+  const nodeIds = {}
+  const tree: any[] = []
+
+  for (const d of data) {
+    const parentId = d[config.parentId]
+    if (childrenListMap[parentId] == null) {
+      childrenListMap[parentId] = []
+    }
+    nodeIds[d[config.id]] = d
+    childrenListMap[parentId].push(d)
+  }
+
+  for (const d of data) {
+    const parentId = d[config.parentId]
+    if (nodeIds[parentId] == null) {
+      const { id, name, children } = d
+      tree.push({ id, name, children })
+    }
+  }
+
+  for (const t of tree) {
+    adaptToChildrenList(t)
+  }
+
+  function adaptToChildrenList(o) {
+    if (childrenListMap[o[config.id]] !== null) {
+      o[config.childrenList] = childrenListMap[o[config.id]]
+    }
+    if (o[config.childrenList]) {
+      for (const c of o[config.childrenList]) {
+        adaptToChildrenList(c)
+      }
+    }
+  }
+
+  return tree
+}
+
+/**
+ * 构造树型结构数据
+ * @param {*} data 数据源
+ * @param {*} id id字段 默认 'id'
+ * @param {*} parentId 父节点字段 默认 'parentId'
+ * @param {*} children 孩子节点字段 默认 'children'
+ * @param {*} rootId 根Id 默认 0
+ */
+// @ts-ignore
+export const handleTree2 = (data, id, parentId, children, rootId) => {
+  id = id || 'id'
+  parentId = parentId || 'parentId'
+  // children = children || 'children'
+  rootId =
+    rootId ||
+    Math.min(
+      ...data.map((item) => {
+        return item[parentId]
+      })
+    ) ||
+    0
+  // 对源数据深度克隆
+  const cloneData = JSON.parse(JSON.stringify(data))
+  // 循环所有项
+  const treeData = cloneData.filter((father) => {
+    const branchArr = cloneData.filter((child) => {
+      // 返回每一项的子级数组
+      return father[id] === child[parentId]
+    })
+    branchArr.length > 0 ? (father.children = branchArr) : ''
+    // 返回第一层
+    return father[parentId] === rootId
+  })
+  return treeData !== '' ? treeData : data
+}
+
+/**
+ * 校验选中的节点,是否为指定 level
+ *
+ * @param tree 要操作的树结构数据
+ * @param nodeId 需要判断在什么层级的数据
+ * @param level 检查的级别, 默认检查到二级
+ * @return true 是;false 否
+ */
+export const checkSelectedNode = (tree: any[], nodeId: any, level = 2): boolean => {
+  if (typeof tree === 'undefined' || !Array.isArray(tree) || tree.length === 0) {
+    console.warn('tree must be an array')
+    return false
+  }
+
+  // 校验是否是一级节点
+  if (tree.some((item) => item.id === nodeId)) {
+    return false
+  }
+
+  // 递归计数
+  let count = 1
+
+  // 深层次校验
+  function performAThoroughValidation(arr: any[]): boolean {
+    count += 1
+    for (const item of arr) {
+      if (item.id === nodeId) {
+        return true
+      } else if (typeof item.children !== 'undefined' && item.children.length !== 0) {
+        if (performAThoroughValidation(item.children)) {
+          return true
+        }
+      }
+    }
+    return false
+  }
+
+  for (const item of tree) {
+    count = 1
+    if (performAThoroughValidation(item.children)) {
+      // 找到后对比是否是期望的层级
+      if (count >= level) {
+        return true
+      }
+    }
+  }
+
+  return false
+}
+
+/**
+ * 获取节点的完整结构
+ * @param tree 树数据
+ * @param nodeId 节点 id
+ */
+export const treeToString = (tree: any[], nodeId) => {
+  if (typeof tree === 'undefined' || !Array.isArray(tree) || tree.length === 0) {
+    console.warn('tree must be an array')
+    return ''
+  }
+  // 校验是否是一级节点
+  const node = tree.find((item) => item.id === nodeId)
+  if (typeof node !== 'undefined') {
+    return node.name
+  }
+  let str = ''
+
+  function performAThoroughValidation(arr) {
+    for (const item of arr) {
+      if (item.id === nodeId) {
+        str += ` / ${item.name}`
+        return true
+      } else if (typeof item.children !== 'undefined' && item.children.length !== 0) {
+        str += ` / ${item.name}`
+        if (performAThoroughValidation(item.children)) {
+          return true
+        }
+      }
+    }
+    return false
+  }
+
+  for (const item of tree) {
+    str = `${item.name}`
+    if (performAThoroughValidation(item.children)) {
+      break
+    }
+  }
+  return str
+}

+ 17 - 0
src/views/flame/index.vue

@@ -0,0 +1,17 @@
+<template>
+	<div class="pa-3 default-width">
+		<!-- <div class="text-end mb-5">
+			<v-btn color="primary" to="/flameLogin">老师登录/注册</v-btn>
+		</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>
+</template>
+
+<script setup>
+defineOptions({ name: 'Flame'})
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 236 - 0
src/views/login/flameLogin.vue

@@ -0,0 +1,236 @@
+<template>
+  <div class="box" :style="{'background-image': 'url(' + logoBgUrl + ')'}">
+    <div class="content">
+      <div class="login-content">
+        <v-card height="392px" class="carousel mr-3" style="width: 792px; border-radius: 8px;">
+          <v-carousel show-arrows="hover" cycle :model-value="0">
+            <v-carousel-item v-for="(item, i) in carouselList" :key="i" @click="handleClick(item)">
+              <div style="height: 392px; overflow: hidden;" :class="{'cursor-pointer': item.link}">
+                <v-img :src="item.img" :lazy-src="item.src" cover style="height: 100%; overflow: hidden;">
+                  <template v-slot:placeholder>
+                    <v-row align="center" class="fill-height ma-0" justify="center">
+                      <v-progress-circular color="grey-lighten-5" indeterminate></v-progress-circular>
+                    </v-row>
+                  </template>
+                </v-img>
+              </div>
+            </v-carousel-item>
+          </v-carousel>
+        </v-card>
+        <div class="login-card">
+          <div class="login-tab">
+            <v-tabs v-model="tab" align-tabs="center" color="primary" class="mb-10">
+              <v-tab :value="0">老师手机验证码登录</v-tab>
+            </v-tabs>
+            <phoneFrom ref="phoneRef" openVerify @handleEnter="handleLogin"></phoneFrom>
+          </div>
+          <div class="font-size-14">
+            <span class="float-right color-error cursor-pointer border-bottom-error" @click="router.push('/register/school')">还没有登录账户?去注册</span>
+          </div>
+          <v-btn :loading="loginLoading" color="primary" class="white--text mt-5" min-width="350" @click.stop="handleLogin">
+            {{ $t('login.login') }}
+          </v-btn>
+          <div class="login-tips mt-3">
+              <v-icon v-if="isAgree" size="25" color="primary" class="mr-1" @click="isAgree = !isAgree">mdi-check-circle</v-icon>
+              <v-icon v-else size="25" color="grey" class="mr-1" @click="isAgree = !isAgree">mdi-circle-outline</v-icon>
+              {{ $t('login.agreeLogin') }}
+              <span class="color" style="cursor: pointer;" @click="windowOpen('/userAgreement')"> [{{ $t('login.userAgreement') }}] </span>和
+              <span class="color" style="cursor: pointer;" @click="windowOpen('/privacyPolicy')">[{{ $t('login.privacyPolicy') }}]</span>
+          </div>
+        </div>
+      </div>
+      <div class="aboutBox">
+        <about :showBanner="false"></about>
+      </div>
+    </div>
+    <navBar :showLoginBtn="false" class="navBar"></navBar>
+  </div>
+  <Verify
+    ref="verify"
+    captchaType="blockPuzzle"
+    :imgSize="{ width: '400px', height: '200px' }"
+    mode="pop"
+    @success="verifySuccess"
+  />
+</template>
+
+<script setup>
+defineOptions({ name: 'login-index' }) 
+import { ref } from 'vue'
+import phoneFrom from '@/components/VerificationCode'
+import { useUserStore } from '@/store/user'
+import { useRouter } from 'vue-router'
+import { useI18n } from '@/hooks/web/useI18n'
+import { getWebContent } from '@/api/common'
+import Snackbar from '@/plugins/snackbar'
+import Confirm from '@/plugins/confirm'
+import navBar from '@/layout/personal/navBar.vue'
+import about from '@/views/about/index.vue'
+import Verify from '@/components/Verifition'
+
+// 获取轮播图、背景图
+const logoBgUrl = ref('')
+const preferred = ref({})
+const carouselList = ref([])
+const getSystemWebContent = async () => {
+  const data = await getWebContent()
+  logoBgUrl.value = data.pcLoginBackground && data.pcLoginBackground.length ? data.pcLoginBackground[0].img : 'https://minio.menduner.com/dev/menduner/login-bgc.jpg'
+  carouselList.value = data.pcLoginCarousel || []
+  preferred.value = data.appPreferredGroup || {}
+}
+getSystemWebContent()
+
+const handleClick = (item) => {
+  if (!item.link) return
+  if (item.link.includes('http')) return window.open(item.link)
+
+  // 优选集团
+  if (preferred.value && Object.keys(preferred.value).length > 0 && preferred.value[item.link]) window.open(`/recruit/personal/advertisement/${item.link}`)
+  else window.open(`/recruit/personal/company/details/${item.link}?key=briefIntroduction`)
+}
+
+const { t } = useI18n()
+const router = useRouter()
+const tab = ref(0)
+const isAgree = ref(false)
+
+
+// 验证码登录
+const phoneRef = ref()
+const loginLoading = ref(false)
+const userStore = useUserStore()
+
+
+const handleLogin = async () => {
+  const { valid } = await phoneRef.value.phoneForm.validate()
+  if (!valid) return
+  if (!isAgree.value) return Snackbar.warning('请阅读并勾选底部协议')
+
+  const params = {
+    ...phoneRef.value.loginData,
+    chooseRole: false,
+    schoolRegister: true
+  }
+  
+  loginLoading.value = true
+  try {
+    if (!params.captchaVerification && captchaStr.value) params.captchaVerification = captchaStr.value
+    if (!params.captchaVerification) {
+      getCode() // 验证码组件
+      return
+    }
+
+    await userStore.handleSmsLogin(params)
+    // Snackbar.success(t('login.loginSuccess'))
+  } catch (err) {
+    console.log(err)
+    captchaStr.value = '' // 清空人机验证
+
+    if (!err.code || (err?.message && err.message.includes('timeout'))) return
+
+    // 登录未注册过的账号跳转注册
+    const text = err.code === 1100016002 ? '您的手机号还未注册过' : '您的邮箱还未注册过'
+    Confirm('系统提示',  `${text},去注册?`, {
+      cancelCallback: true
+    }).then(() => {
+      localStorage.setItem('loginAccount', phoneRef.value.loginData.phone)
+      router.push('/register/school')
+    }).catch(() => {})
+  } finally {
+    loginLoading.value = false
+  }
+}
+
+
+// 获取验证码
+const verify = ref()
+const getCode = async () => {
+  // 弹出验证码 // 已开启:则展示验证码;只有完成验证码的情况,才进行登录
+  verify.value.show()
+}
+
+const captchaStr = ref('')
+const verifySuccess = (params) => {
+  captchaStr.value = params.captchaVerification
+  handleLogin()
+}
+
+const windowOpen = (url) => {
+  if (url) window.open(url)
+}
+
+</script>
+
+<style lang="scss" scoped>
+.box {
+  position: relative;
+  width: 100%;
+  height: 100vh;
+  background-size: cover;
+  overflow: hidden;
+  .navBar {
+    position: absolute;
+    top: 0;
+  }
+}
+.content {
+  width: 100%;
+  height: 100%;
+  overflow-y: auto;
+}
+.login-content {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 78vh;
+  // height: calc(100vh - 50px);
+  margin-top: 50px;
+}
+.login-change {
+  position: absolute;
+  top: 0;
+  right: 0;
+  margin: 15px 44px;
+  border-bottom: 1px solid orange;
+  color: orange; 
+  cursor: pointer;
+  font-weight: 400;
+  &:hover {
+    color: #fbb93e; 
+  }
+}
+.login-card {
+  position: relative;
+  width: 450px;
+  background-color: #fff;
+  border-radius: 8px;
+  padding: 36px 50px;
+}
+.left {
+  display: flex;
+}
+.login-tips {
+  width: 100%;
+  font-size: 12px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+.tips:hover {
+  border-bottom: 1px solid #666;
+}
+.color {
+  color: var(--v-primary-base); 
+}
+
+.aboutBox {
+  width: 100%;
+  background-color: #fff;
+}
+.carousel {
+  :deep(.v-window) {
+    height: 392px !important;
+  }
+}
+</style>

+ 0 - 3
src/views/login/index.vue

@@ -173,8 +173,6 @@ const handleLogin = async () => {
   loginLoading.value = true
   try {
     let params, api = {}
-    // if (tab.value === 1) { params = { ...phoneRef.value.loginData }; api = 'handleSmsLogin'}
-    // else { params = { ...passRef.value.loginData }; api = 'handlePasswordLogin'}
     if (isEnterpriseLogin.value) { params = { ...entPassRef.value.loginData }; api = 'handlePasswordLogin'}
     else { params = tab.value === 1 ? { ...phoneRef.value.loginData } : { ...passRef.value.loginData }; api = tab.value === 1 ? 'handleSmsLogin' : 'handlePasswordLogin'}
     if (!params.captchaVerification && captchaStr.value) params.captchaVerification = captchaStr.value
@@ -257,7 +255,6 @@ const verifySuccess = (params) => {
 }
 
 const windowOpen = (url) => {
-  // router.push(url)
   if (url) window.open(url)
 }
 

+ 0 - 0
src/views/mall copy/components/table.vue → src/views/mallCopy/components/table.vue


+ 1 - 1
src/views/mall copy/exchange.vue → src/views/mallCopy/exchange.vue

@@ -33,7 +33,7 @@ import { ref } from 'vue'
 import { getToken } from '@/utils/auth'
 import Dialog from '@/components/CtDialog'
 import Snackbar from '@/plugins/snackbar'
-import { redeemSubmit } from '@/api/mall copy'
+import { redeemSubmit } from '@/api/mallCopy'
 import { useUserStore } from '@/store/user'
 import { getDict } from '@/hooks/web/useDictionaries'
 import { checkPersonBaseInfo } from '@/utils/check'

+ 1 - 1
src/views/mall copy/exchangeRecords.vue → src/views/mallCopy/exchangeRecords.vue

@@ -21,7 +21,7 @@
 <script setup>
 defineOptions({name: 'mall-exchangeRecords'})
 import { ref } from 'vue'
-import { getRedeemPage } from '@/api/mall copy'
+import { getRedeemPage } from '@/api/mallCopy'
 import { getToken } from '@/utils/auth'
 
 const total = ref(0)

+ 0 - 0
src/views/mall copy/index.vue → src/views/mallCopy/index.vue


+ 0 - 0
src/views/mall copy/purchasePackage/components/packageList.js → src/views/mallCopy/purchasePackage/components/packageList.js


+ 0 - 0
src/views/mall copy/purchasePackage/components/packageList.vue → src/views/mallCopy/purchasePackage/components/packageList.vue


+ 0 - 0
src/views/mall copy/purchasePackage/index.vue → src/views/mallCopy/purchasePackage/index.vue


+ 10 - 5
src/views/recruit/components/message/index.vue

@@ -225,6 +225,7 @@ import { previewFile } from '@/utils'
 import { timesTampChange } from '@/utils/date'
 import { useRouter } from 'vue-router'
 import studentDeliveryForm from '@/views/recruit/personal/components/studentDeliveryForm.vue'
+import { getIsEnterprise } from '@/utils/auth'
 
 const { t } = useI18n()
 const chatRef = ref()
@@ -345,6 +346,10 @@ if (!IM) {
   console.log('IM is disconnected')
 }
 
+// 参与招聘会的职位进入需传递招聘会id
+// const jobFairId = ref('')
+// if (route.query?.jobFairId) jobFairId.value = route.query.jobFairId
+
 // 职位进入
 if (route.query.id) {
   const api = route.query.enterprise ? getPositionDetails : getBaseInfo // getBaseInfo  getUserInfo
@@ -433,7 +438,7 @@ async function getMessageTypeSync () {
       ]
     }
   })
-  if (!data.records || !data.records.length) { 
+  if (!data.records || !data.records.length) {
     positionInfo.value = {}
     handleChangeSendResumeStatus(false)
     return
@@ -562,7 +567,7 @@ const getRecruitPositionList = async () => {
       data: e
     }
   }).filter(Boolean)
-  
+
   setTimeout(() => { pageLoading.value = false }, 300)
 }
 
@@ -722,7 +727,7 @@ async function handleSubmitResume () {
     }
     // 如果是学生则需要带上实习信息
     if (practice && Object.keys(practice).length > 0) params = Object.assign(params, practice)
-    
+
     await jobCvRelSend(params)
   }
   handleChangeSendResumeStatus(true)
@@ -739,7 +744,7 @@ async function handleSubmitResume () {
   }
   if (enRequestPositionInfo.value) text.query.positionInfo = enRequestPositionInfo.value
   send (JSON.stringify(text), channelItem.value, 105)
-  
+
   enRequestPositionInfo.value = {}
 }
 
@@ -861,7 +866,7 @@ const handleAgree = (val) => {
   const query = {
     id: val.id
   }
-  const type = route?.meta?.loginType === 'enterprise' ? 'entBaseInfo' : 'baseInfo'
+  const type = getIsEnterprise() ? 'entBaseInfo' : 'baseInfo'
   const baseInfo = localStorage.getItem(type)
   if (baseInfo) {
     const { phone } = JSON.parse(baseInfo)

+ 0 - 117
src/views/recruit/entRegister/joiningEnterprise.vue

@@ -1,117 +0,0 @@
-<template>
-  <div class="pt-5">
-    <v-card class="default-width pa-5">
-      <!-- 标题 -->
-      <div class="resume-header">
-        <div class="resume-title">{{ $t('enterprise.joiningEnterprise') }}</div>
-      </div>
-      <!-- 表单 -->
-      <div class="CtFormClass" style="width: 600px;">
-        <CtForm ref="CtFormRef" :items="formItems" style="width: 100%;"></CtForm>
-      </div>
-      <div class="text-center">
-        <!-- 完成 -->
-        <v-btn
-          :loading="loginLoading"
-          color="primary" class="white--text mt-8" min-width="350"
-          @click="handleCommit"
-        >
-        {{ $t('common.complete') }}
-        </v-btn>
-      </div>
-      <!-- 底部 -->
-      <div class="text-center mt-5">
-        <v-btn color="primary" variant="text" @click="router.push({ path: '/recruit/entRegister' })">{{ $t('enterprise.registeringNewEnterprise') }}</v-btn>
-      </div>
-    </v-card>
-  </div>
-</template>
-
-<script setup>
-import CtForm from '@/components/CtForm'
-import { enterpriseSearchByName } from '@/api/recruit/personal/resume'
-import { useRouter } from 'vue-router'
-import Snackbar from '@/plugins/snackbar'
-import { useI18n } from '@/hooks/web/useI18n'
-import { ref } from 'vue'
-
-defineOptions({name: 'enterprise-enterpriseRegister-joiningEnterprise'})
-const router = useRouter()
-const loginLoading = ref(false)
-const { t } = useI18n()
-
-// 企业名称下拉列表
-const getSchoolListData = async (name) => {
-  const item = formItems.value.options.find(e => e.key === 'enterpriseId')
-  if (!item) return
-  const data = await enterpriseSearchByName({ name })
-  item.items = data
-}
-
-const formItems = ref({
-  options: [
-    {
-      type: 'autocomplete',
-      key: 'enterpriseId',
-      value: null,
-      default: null,
-      label: '企业名称 *',
-      outlined: true,
-      clearable: true,
-      itemText: 'value',
-      itemValue: 'key',
-      rules: [v => !!v || '请选择企业名称'],
-      search: getSchoolListData,
-      items: []
-    },
-    {
-      type: 'text',
-      key: 'email',
-      value: '',
-      label: '职务 *',
-      rules: [v => !!v || '请输入职务']
-    },
-    {
-      type: 'text',
-      key: 'name',
-      value: '',
-      label: '姓名 *',
-      counter: 15,
-      rules: [v => !!v || '请输入姓名']
-    },
-  ]
-})
-
-// 提交
-const handleCommit = () => {
-  Snackbar.success(t('common.submittedSuccessfully'))
-  setTimeout(() => {
-    router.push({ path: '/recruit/enterprise' })
-  }, 3000);
-}
-</script>
-<style lang="scss" scoped>
-.CtFormClass {
-  margin: 0 auto;
-}
-.note {
-  color: var(--color-666);
-  font-size: 14px;
-  line-height: 32px;
-}
-.file-input-box {
-  position: relative;
-  height: 80px;
-  width: 100px;
-  border: 1px solid rgb(188, 188, 188);
-  border-radius: 5px;
-  cursor: pointer;
-  .icon {
-    position: absolute;
-    top: 45%;
-    left: 50%;
-    transform: translate(-50%, -50%);
-    color: var(--color-999);
-  }
-}
-</style>

+ 12 - 0
src/views/recruit/enterprise/contactUs/index.vue

@@ -0,0 +1,12 @@
+<template>
+	<ContactUsPage />
+</template>
+
+<script setup>
+defineOptions({ name: 'contactUs'})
+import ContactUsPage from '@/views/recruit/components/contactUs/index.vue'
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 9 - 9
src/views/recruit/enterprise/entInfoSetting/informationSettingsComponents/businessInformation.vue

@@ -11,14 +11,14 @@
             <span>支持jpg、jpeg、png格式,图片大小不得超过20M</span>
           </div>
           <div class="file-box">
-            <Img 
-              class="mt-3" 
-              tips="上传图片" 
+            <Img
+              class="mt-3"
+              tips="上传图片"
               :value="licenseUrl"
-              :showSnackbar="false" 
-              @imgClick="emit('preview', [licenseUrl])" 
-              :showCursor="true" 
-              @success="handleUploadImg" 
+              :showSnackbar="false"
+              @imgClick="emit('preview', [licenseUrl])"
+              :showCursor="true"
+              @success="handleUploadImg"
               @delete="handleDeleteImg"
             ></Img>
           </div>
@@ -216,7 +216,7 @@ const getOcr = async () => {
   loading.value = true
   try {
     const data = await getBusinessLicenseOCR(licenseUrl.value)
-    
+
     if (data && Object.keys(data).length) {
       Confirm(t('common.confirmTitle'), '是否根据营业执照内容替换以下相关信息').then(() => {
         formItems.value.options.forEach(e => {
@@ -228,7 +228,7 @@ const getOcr = async () => {
         })
         business.value = data
       })
-      
+
     } else {
       licenseUrl.value = ''
       Confirm(t('common.confirmTitle'), '营业执照图片识别失败,请重新上传清晰合法图片', { hideCancelBtn: true })

+ 27 - 1
src/views/recruit/enterprise/hirePosition/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div>
+  <div :class="{'disable': info && info?.entitlement && !info?.entitlement?.hireJob}">
     <v-card class="card-box pa-5">
       <div class="d-flex justify-center mt-3">
         <TextUI :item="textItem" @enter="handleEnter" @appendInnerClick="handleEnter"></TextUI>
@@ -78,6 +78,12 @@ const textItem = ref({
   appendInnerIcon: 'mdi-magnify'
 })
 
+// 获取企业权益信息
+const info = ref(localStorage.getItem('entBaseInfo') ? JSON.parse(localStorage.getItem('entBaseInfo')) : {})
+store.$subscribe((mutation, state) => {
+  if (Object.keys(state.entBaseInfo).length) info.value = state.entBaseInfo
+})
+
 const handleAdd = async () => {
   const data = await getEnterprisePubJobTypePermission()
   if (!data || !data.length) return Snackbar.warning('没有该操作权限,请联系平台管理员升级后再试')
@@ -143,4 +149,24 @@ const handleEnter = (e) => {
 </script>
 
 <style scoped lang="scss">
+.disable {
+  position: relative;
+  overflow: hidden;
+  &::after {
+    content: '很抱歉,您当前没有权限使用全员猎寻模块';
+    position: absolute;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 1.5em;
+    font-weight: bold;
+    color: #fff;
+    top: 0;
+    border-radius: 12px;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.35);
+  }
+}
 </style>

+ 39 - 0
src/views/recruit/enterprise/permissionPrompt/index.vue

@@ -0,0 +1,39 @@
+<template>
+  <div class="d-flex align-center flex-column">
+    <svg-icon name="permissionPrompt" size="500"></svg-icon>
+    <div class="color-999 mb-5">抱歉,您当前登录的企业账号未查询到可以访问的企业菜单权限,请联系企业管理员分配菜单权限后再登录。</div>
+    <v-btn class="mt-5" color="primary" width="250" @click="handleToHome">回到首页</v-btn>
+  </div>
+</template>
+
+<script setup>
+defineOptions({ name: 'PermissionPrompt'})
+import { useRouter } from 'vue-router'
+import { logoutToken } from '@/api/common'
+import { getToken } from '@/utils/auth'
+
+const router = useRouter()
+
+const handleToHome = async () => {
+	// 退出企业登录
+	await logoutToken(getToken(1))
+	
+	// 有企业id则为求职者切换招聘者
+	// 切换招聘者时已将企业账户信息赋值给accountInfo,现退出企业登录后需将个人账户信息赋值给accountInfo
+	const isChangeRole = localStorage.getItem('enterpriseId')
+	if (isChangeRole) {
+		const perAccountData = JSON.parse(localStorage.getItem('perAccountInfo'))
+		localStorage.setItem('accountInfo', JSON.stringify(perAccountData))
+	} else localStorage.removeItem('accountInfo')
+	
+	// 清除企业相关缓存信息
+	const enterpriseLocalStorage = ['ENT_REFRESH_TOKEN', 'ENT_ACCESS_TOKEN', 'entBaseInfo', 'isAdmin', 'enterpriseUserAccount', 'entUpdatePassword', 'emailLoginInfo', 'enterpriseStore', 'enterpriseId']
+	enterpriseLocalStorage.forEach(e => localStorage.removeItem(e))
+
+	router.push('/')
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 3 - 22
src/views/recruit/enterprise/staffInfoSetting/index.vue

@@ -19,7 +19,7 @@
       </template>
     </CtForm>
     <v-btn class="buttons mt-5" color="primary" @click.stop="handleSubmit">{{ $t('common.save') }}</v-btn>
-    <v-btn class="mt-3" color="primary" variant="text" to="/recruit/enterprise/staffChangePassword">修改登录密码</v-btn>
+    <v-btn v-if="showEditPassword" class="mt-3" color="primary" variant="text" to="/recruit/enterprise/systemManagement/staffChangePassword">修改登录密码</v-btn>
   </v-card>
 
   <Loading :visible="overlay"></Loading>
@@ -47,6 +47,8 @@ const overlay = ref(false)
 const selectPic = ref('')
 const isShowCopper = ref(false)
 
+const showEditPassword = localStorage.getItem('showEditPassword') === 'true'
+
 const CtFormRef = ref()
 const formItems = ref({
   options: [
@@ -56,15 +58,6 @@ const formItems = ref({
       value: '',
       flexStyle: 'align-center'
     },
-    // {
-    //   type: 'ifRadio',
-    //   key: 'sex',
-    //   value: '2',
-    //   label: '性别',
-    //   width: 90,
-    //   dictTypeName: 'menduner_sex',
-    //   items: []
-    // },
     {
       type: 'text',
       key: 'name',
@@ -85,16 +78,6 @@ const formItems = ref({
       value: '',
       label: '电子邮箱',
       disabled: true,
-      // rules: [
-      //   value => {
-      //     if (value) return true
-      //     return '请输入联系邮箱'
-      //   },
-      //   value => {
-      //     if (checkEmail(value)) return true
-      //     return '请输入正确的电子邮箱'
-      //   }
-      // ]
     },
     {
       type: 'text',
@@ -106,7 +89,6 @@ const formItems = ref({
 })
 
 // 用户基本信息
-// let emailChange = false
 const baseInfo = ref(JSON.parse(localStorage.getItem('entBaseInfo')) || {})
 const query = ref({})
 // 获取字典数据以及字段回显
@@ -180,7 +162,6 @@ const handleSubmit = async () => {
     query.value[item.key] = item.value
   })
   await saveUserInfo(query.value)
-  // if (query.value?.email && emailChange) await entUpdateEmail({ email: query.value.email })
   setTimeout(async () => {
     await userStore.getEnterpriseInfo()
     Snackbar.success(t('common.submittedSuccessfully'))

+ 13 - 3
src/views/recruit/enterprise/student/InternshipSituation/CertificateForm.vue

@@ -17,9 +17,19 @@ 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: 'url',
+      key: 'certificate',
       value: '',
       truthValue: '',
       label: '点击上传附件 *',
@@ -39,7 +49,7 @@ const openFileInput = () => {
 
 // 上传附件
 const handleUploadResume = async (url, title, filename) => {
-  const obj = formItems.value.options.find(e => e.key === 'url')
+  const obj = formItems.value.options.find(e => e.key === 'certificate')
   obj.value = filename
   obj.truthValue = url
 }
@@ -47,7 +57,7 @@ const handleUploadResume = async (url, title, filename) => {
 const getQuery = () => {
 	let obj = {}
 	formItems.value.options.forEach(e => {
-		obj[e.key] = e.truthValue
+		obj[e.key] = e.truthValue || e.value
 	})
 	return obj
 }

+ 108 - 54
src/views/recruit/enterprise/student/InternshipSituation/index.vue

@@ -1,19 +1,19 @@
 <template>
 	<v-card class="card-box pa-3">
-		<div class="d-flex">
+		<div class="d-flex justify-space-between align-center">
 			<!-- 统计 -->
 			<div class="d-flex align-center statistics">
-				<div v-for="val in statistics" :key="val.key" class="statistics-card pa-5">
+				<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.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>
+        <span class="color-666">实习日期</span>
         <div class="ml-5">
           <date-picker 
 						v-model="date"
@@ -30,10 +30,8 @@
       </div>
 		</div>
 
-		<div class="d-flex mt-5">
-			<v-treeview :items="items" color="primary" style="width: 25%; border-right: 1px solid #eee;" class="pr-5"></v-treeview>
+		<div class="mt-5">
 			<CtTable
-				class="ml-5"
         :items="tableData"
         :headers="headers"
         :loading="loading"
@@ -44,116 +42,172 @@
         :page-info="query"
         itemKey="id"
         @pageHandleChange="handleChangePage"
-				style="flex: 1;"
       >
         <template #studentName="{ item }">
           <div class="d-flex align-center">
-            <v-avatar size="40" :image="getUserAvatar(item.studentHeadImg, item.sex)"></v-avatar>
-            <span class="ml-3">{{ item?.studentName }}</span>
+            <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 color="primary" variant="text" @click="handleUploadLetter(item)">上传推荐信</v-btn>
-          <v-btn color="#00897B" variant="text" @click="handleIssueCertificate(item)">颁发实习证书</v-btn>
+          <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="showLitterDialog = false;" @submit="handleSubmitLetter">
+	<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="showCertificateDialog = false;" @submit="handleSubmitCertificate">
+	<CtDialog :visible="showCertificateDialog" :widthType="2" titleClass="text-h6" title="颁发实习证书" @close="handleCertificateClose" @submit="handleSubmitCertificate">
     <IssueCertificateForm ref="IssueCertificateFormRef" />
   </CtDialog>
 </template>
 
 <script setup>
 defineOptions({ name: 'enterpriseStudentInternshipSituation' })
-import { ref } from 'vue'
+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([
-	{ label: '实习中', value: 2, key: 'studentPracticeApplyCount' },
-	{ label: '实习结束', value: 6, key: 'studentPracticeSuccessCount' },
-	{ label: '等待实习', value: 10, key: 'studentPracticeWaitCount' }
-])
-
-const items = ref([
-  { id: 1, title: '运营部' },
-  { id: 2, title: '开发部' },
-  { id: 3, title: '行政部' },
-  { id: 4, title: '市场部' }
-])
+const statistics = ref([])
+
 const loading = ref(false)
 const total = ref(0)
 const query = ref({
 	pageNo: 1,
-	pageSize: 10
+	pageSize: 10,
+	startTime: null
 })
-const tableData = ref([
-	{
-		id: 1,
-		studentHeadImg: 'https://minio.menduner.com/dev/4cd324c459e055c34f8467089bd8c3013e2a95dee6f3883cbdacf100b9a50d52.jpeg',
-		studentName: '张三',
-		sex: '1',
-		schoolDepartmentName: '计算机学院',
-		majorName: '计算机科学与技术'
-	},
-	{
-		id: 2,
-		studentHeadImg: 'https://minio.menduner.com/dev/4cd324c459e055c34f8467089bd8c3013e2a95dee6f3883cbdacf100b9a50d52.jpeg',
-		studentName: '张三',
-		sex: '2',
-		schoolDepartmentName: '计算机学院',
-		majorName: '计算机科学与技术'
-	}
-])
+const tableData = ref([])
+
 const headers = [
   { title: '学生姓名', key: 'studentName', sortable: false },
-  { title: '所属院系', key: 'schoolDepartmentName', sortable: false },
-  { title: '所属专业', key: 'majorName', 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) => {
-	console.log(time, 'range')
+	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 = () => {
+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()
-	console.log(query, '推荐信-form')
+
+	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 = () => {
+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>
 

+ 1 - 0
src/views/recruit/enterprise/systemManagement/groupAccount/inviteConfirmEnt-old.vue

@@ -1,3 +1,4 @@
+<!-- 备份-已废弃 -->
 <template>
   <div style="background-color: #f0f0f0;">
     <div class="inviteView text-center" :style="{'width': isMobile ? '100%' : '750px'}">

+ 0 - 14
src/views/recruit/enterprise/systemManagement/groupAccount/inviteConfirmEnt.vue

@@ -101,20 +101,6 @@ const verifySuccess = (params) => {
   handleLogin()
 }
 
-// const entBaseInfo = ref()
-// // 获取当前登录的企业用户信息
-// const getLoginEnterpriseInfo = async () => {
-//   try {
-//     const result = await getEnterprisingUserInfo()
-//     entBaseInfo.value = result
-    
-//     // 是否为企业账号管理员
-//     // const isAdmin = result.userType === '1'
-//   } catch (error) {
-//     console.error('error', error)
-//   }
-// }
-
 const loginLoading = ref(false)
 const handleLogin = async () => {
   const { valid } = await entPassRef.value.passwordForm.validate()

+ 346 - 345
src/views/recruit/enterprise/talentMap/index.vue

@@ -1,355 +1,356 @@
 <template>
-<div>
-  <v-card class="card-box pa-5" style="height: 100%;">
-    <div class="d-flex flex-column align-center" :class="{'v-center': init}">
-      <TextUI
-        v-model="content"
-        :item="textItem"
-        @keyup.enter="handleConfirm"
-        @appendInnerClick="handleConfirm"
-      ></TextUI>
-      <div class="align-center">
-        <template v-if="init && defaultLabelsShow?.length">
-          <v-btn
-            v-for="(val, index) in defaultLabelsShow" :key="val + index"
-            class="mr-3 my-2 py-0 px-2"
-            density="comfortable"
-            color="primary" variant="tonal"
-            @click="clickChip(index)"
-          >
-            {{ val }}
-          </v-btn>
-          <!-- <v-btn class="ml-0 my-1 py-0 px-2" density="comfortable" variant="tonal" color="error" @click="moreLabels">
-            更多人才标签...
-          </v-btn> -->
-        </template>
-      </div>
-    </div>
-    <template v-if="!init">
-      <span class="mr-2 color-666" style="width: 68px; min-width: 68px;">人才标签:</span>
-      <v-btn
-        v-for="(val, index) in chosenLabels" :key="val + index"
-        class="mr-3 my-2 py-0 px-2"
-        density="comfortable"
-        color="primary" variant="tonal"
-      >
-        {{ val }}
-        <v-icon class="ml-1" style="margin-top: 1px;" @click="deleteChip(index)">mdi-close</v-icon>
-      </v-btn>
-      <!-- <v-btn
-        class="mr-3 my-2 py-0 px-0"
-        density="comfortable"
-        color="primary" variant="tonal"
-        @click="moreLabels"
-      >
-        <v-icon>mdi-plus-box</v-icon>
-      </v-btn> -->
-      <v-btn icon="mdi-plus" class="mr-8" variant="outlined" size="x-small" @click="moreLabels"></v-btn>
-      <span v-if="chosenLabels?.length" class="my-2" style="font-size: 14px;cursor: pointer;color: var(--color-999);" @click="resetLabel">清空标签</span>
-    </template>
-    <!-- 人员信息表单 -->
-    <CtTable
-      v-if="dataList?.length && (chosenLabels?.length || content)"
-      class="mt-3"
-      :items="dataList"
-      :headers="headers"
-      :loading="loading"
-      :elevation="0"
-      height="calc(100vh - 380px)"
-      :isTools="false"
-      :showPage="true"
-      :total="total"
-      :page-info="pageInfo"
-      itemKey="id"
-      @pageHandleChange="handleChangePage"
-    >
-      <template #name="{ item }">
-        <div class="d-flex align-center cursor-pointer" @click="talentPoolDetails(item)">
-          <v-badge
-            v-if="item?.sex === '1' || item?.sex === '2'"
-            bordered
-            offset-y="6"
-            :color="badgeColor(item)"
-            :icon="badgeIcon(item)">
-            <v-avatar size="40" :image="getUserAvatar(item.avatar, item.sex)"></v-avatar>
-          </v-badge>
-          <v-avatar v-else size="40" :image="getUserAvatar(item.avatar, item.sex)"></v-avatar>
-          <span class="defaultLink ml-3">{{ item?.name }}</span>
+  <div>
+    <v-card class="card-box pa-5" style="height: 100%;">
+      <div class="d-flex flex-column align-center" :class="{'v-center': init}">
+        <TextUI
+          v-model="content"
+          :item="textItem"
+          @keyup.enter="handleConfirm"
+          @appendInnerClick="handleConfirm"
+        ></TextUI>
+        <div class="align-center">
+          <template v-if="init && defaultLabelsShow?.length">
+            <v-btn
+              v-for="(val, index) in defaultLabelsShow" :key="val + index"
+              class="mr-3 my-2 py-0 px-2"
+              density="comfortable"
+              color="primary" variant="tonal"
+              @click="clickChip(index)"
+            >
+              {{ val }}
+            </v-btn>
+            <!-- <v-btn class="ml-0 my-1 py-0 px-2" density="comfortable" variant="tonal" color="error" @click="moreLabels">
+              更多人才标签...
+            </v-btn> -->
+          </template>
         </div>
+      </div>
+      <template v-if="!init">
+        <span class="mr-2 color-666" style="width: 68px; min-width: 68px;">人才标签:</span>
+        <v-btn
+          v-for="(val, index) in chosenLabels" :key="val + index"
+          class="mr-3 my-2 py-0 px-2"
+          density="comfortable"
+          color="primary" variant="tonal"
+        >
+          {{ val }}
+          <v-icon class="ml-1" style="margin-top: 1px;" @click="deleteChip(index)">mdi-close</v-icon>
+        </v-btn>
+        <!-- <v-btn
+          class="mr-3 my-2 py-0 px-0"
+          density="comfortable"
+          color="primary" variant="tonal"
+          @click="moreLabels"
+        >
+          <v-icon>mdi-plus-box</v-icon>
+        </v-btn> -->
+        <v-btn icon="mdi-plus" class="mr-8" variant="outlined" size="x-small" @click="moreLabels"></v-btn>
+        <span v-if="chosenLabels?.length" class="my-2" style="font-size: 14px;cursor: pointer;color: var(--color-999);" @click="resetLabel">清空标签</span>
       </template>
-      <template #actions="{ item }">
-        <v-btn color="primary" variant="text" @click="talentPoolDetails(item)">查看</v-btn>
-      </template>
-    </CtTable>
-    <Empty v-if="!init && !total" :message="loading ? '加载中...' : '没有找到对应人才信息,请换个条件搜索'" :elevation="false" class="mt-15"></Empty>
-  </v-card>
-  <CtDialog :visible="showMore" titleClass="text-h6" title="选择人才标签" @close="showMore = false" @submit="handleSubmit">
-    <div style="width: 80%; margin: 0 auto; min-height: 400px;">
-      <CtForm :items="formItems" style="width: 100%;"></CtForm>
-    </div>
-  </CtDialog>
-</div>
-</template>
-
-<script setup>
-defineOptions({ name: 'enterprise-talent-map'})
-import { getRecruitPersonMapPage } from '@/api/recruit/enterprise/resumeManagement/talentMap'
-import { getRocketLabelList } from '@/api/recruit/enterprise/resumeManagement/talentMap'
-import { dealDictArrayData } from '@/utils/position'
-import { getUserAvatar } from '@/utils/avatar'
-import { timesTampChange } from '@/utils/date'
-import TextUI from '@/components/FormUI/TextInput'
-import Snackbar from '@/plugins/snackbar'
-
-import { computed, reactive, ref } from 'vue'
-
-const loading = ref(false)
-
-const content = ref('')
-const pageInfo = reactive({ pageNo: 1, pageSize: 10 })
-const dataList = ref([])
-const total = ref(0)
-const headers = [
-  { title: '姓名', key: 'name', sortable: false },
-  { title: '求职状态', key: 'jobStatusName', sortable: false },
-  // { title: '求职类型', key: 'jobName', sortable: false },
-  { title: '电话号码', key: 'phone', sortable: false },
-  // { title: '常用邮箱', key: 'email', sortable: false },
-  // { title: '微信二维码', key: 'wxCode', sortable: false },
-  { title: '出生日期', key: 'birthday', sortable: false, value: item =>  timesTampChange(item.birthday, 'Y-M-D') },
-  { title: '婚姻状况', key: 'maritalStatusName', sortable: false },
-  { title: '所在城市', key: 'areaName', sortable: false },
-  // { title: '户籍地', key: 'regName', sortable: false },
-  { title: '首次工作时间', key: 'firstWorkTime', sortable: false, value: item =>  timesTampChange(item.firstWorkTime, 'Y-M-D') },
-  // { title: '个人优势', key: 'advantage', sortable: false },
-  { title: '工作年限', key: 'expName', sortable: false },
-  { title: '最高学历', key: 'eduName', sortable: false },
-  { title: '操作', value: 'actions', sortable: false }
-]
-
-const init = ref(true)
-const labelList = ref([])
-const chosenLabels = ref([])
-const defaultLabelsShow = ref([])
-// 人才标签
-const getLabelData = async () => {
-  const res = await getRocketLabelList({ current: 1, size:9999, type: 'person' }) //type: job  enterprise person
-  labelList.value = res?.records?.length ? res.records : []
-  defaultLabelsShow.value = labelList.value?.length ? labelList.value.slice(0, 15) : [] // 默认展示且没有删除按钮
-  //
-  const labelsItem = formItems.value.options.find(f => f.key === 'labels')
-  if (labelsItem) {
-    labelsItem.items = labelList.value?.length ? labelList.value.map(e => ({ label: e, value: e })) : []
+      <!-- 人员信息表单 -->
+      <CtTable
+        v-if="dataList?.length && (chosenLabels?.length || content)"
+        class="mt-3"
+        :items="dataList"
+        :headers="headers"
+        :loading="loading"
+        :elevation="0"
+        height="calc(100vh - 380px)"
+        :isTools="false"
+        :showPage="true"
+        :total="total"
+        :page-info="pageInfo"
+        itemKey="id"
+        @pageHandleChange="handleChangePage"
+      >
+        <template #name="{ item }">
+          <div class="d-flex align-center cursor-pointer" @click="talentPoolDetails(item)">
+            <v-badge
+              v-if="item?.sex === '1' || item?.sex === '2'"
+              bordered
+              offset-y="6"
+              :color="badgeColor(item)"
+              :icon="badgeIcon(item)">
+              <v-avatar size="40" :image="getUserAvatar(item.avatar, item.sex)"></v-avatar>
+            </v-badge>
+            <v-avatar v-else size="40" :image="getUserAvatar(item.avatar, item.sex)"></v-avatar>
+            <span class="defaultLink ml-3">{{ item?.name }}</span>
+          </div>
+        </template>
+        <template #actions="{ item }">
+          <v-btn color="primary" variant="text" @click="talentPoolDetails(item)">查看</v-btn>
+        </template>
+      </CtTable>
+      <Empty v-if="!init && !total" :message="loading ? '加载中...' : '没有找到对应人才信息,请换个条件搜索'" :elevation="false" class="mt-15"></Empty>
+    </v-card>
+    <CtDialog :visible="showMore" titleClass="text-h6" title="选择人才标签" @close="showMore = false" @submit="handleSubmit">
+      <div style="width: 80%; margin: 0 auto; min-height: 400px;">
+        <CtForm :items="formItems" style="width: 100%;"></CtForm>
+      </div>
+    </CtDialog>
+  </div>
+  </template>
+  
+  <script setup>
+  defineOptions({ name: 'enterprise-talent-map'})
+  import { getRecruitPersonMapPage } from '@/api/recruit/enterprise/resumeManagement/talentMap'
+  import { getRocketLabelList } from '@/api/recruit/enterprise/resumeManagement/talentMap'
+  import { dealDictArrayData } from '@/utils/position'
+  import { getUserAvatar } from '@/utils/avatar'
+  import { timesTampChange } from '@/utils/date'
+  import TextUI from '@/components/FormUI/TextInput'
+  import Snackbar from '@/plugins/snackbar'
+  
+  import { computed, reactive, ref } from 'vue'
+  
+  const loading = ref(false)
+  
+  const content = ref('')
+  const pageInfo = reactive({ pageNo: 1, pageSize: 10 })
+  const dataList = ref([])
+  const total = ref(0)
+  const headers = [
+    { title: '姓名', key: 'name', sortable: false },
+    { title: '求职状态', key: 'jobStatusName', sortable: false },
+    // { title: '求职类型', key: 'jobName', sortable: false },
+    { title: '电话号码', key: 'phone', sortable: false },
+    // { title: '常用邮箱', key: 'email', sortable: false },
+    // { title: '微信二维码', key: 'wxCode', sortable: false },
+    { title: '出生日期', key: 'birthday', sortable: false, value: item =>  timesTampChange(item.birthday, 'Y-M-D') },
+    { title: '婚姻状况', key: 'maritalStatusName', sortable: false },
+    { title: '所在城市', key: 'areaName', sortable: false },
+    // { title: '户籍地', key: 'regName', sortable: false },
+    { title: '首次工作时间', key: 'firstWorkTime', sortable: false, value: item =>  timesTampChange(item.firstWorkTime, 'Y-M-D') },
+    // { title: '个人优势', key: 'advantage', sortable: false },
+    { title: '工作年限', key: 'expName', sortable: false },
+    { title: '最高学历', key: 'eduName', sortable: false },
+    { title: '操作', value: 'actions', sortable: false }
+  ]
+  
+  const init = ref(true)
+  const labelList = ref([])
+  const chosenLabels = ref([])
+  const defaultLabelsShow = ref([])
+  // 人才标签
+  const getLabelData = async () => {
+    const res = await getRocketLabelList({ current: 1, size:9999, type: 'person' }) //type: job  enterprise person
+    labelList.value = res?.records?.length ? res.records : []
+    defaultLabelsShow.value = labelList.value?.length ? labelList.value.slice(0, 15) : [] // 默认展示且没有删除按钮
+    //
+    const labelsItem = formItems.value.options.find(f => f.key === 'labels')
+    if (labelsItem) {
+      labelsItem.items = labelList.value?.length ? labelList.value.map(e => ({ label: e, value: e })) : []
+    }
   }
-}
-getLabelData()
-
-const showMore = ref(false)
-const moreLabels = () => {
-  const labelsItem = formItems.value.options.find(f => f.key === 'labels')
-  if (labelsItem) {
-    labelsItem.value = chosenLabels.value
+  getLabelData()
+  
+  const showMore = ref(false)
+  const moreLabels = () => {
+    const labelsItem = formItems.value.options.find(f => f.key === 'labels')
+    if (labelsItem) {
+      labelsItem.value = chosenLabels.value
+    }
+    showMore.value = true
   }
-  showMore.value = true
-}
-
-const clickChip = (index) => {
-  chosenLabels.value = [labelList.value[index]]
-  handleConfirm()
-}
-
-const deleteChip = (index) => {
-  chosenLabels.value.splice(index, 1)
-  handleConfirm()
-}
-
-const resetLabel = () => {
-  chosenLabels.value = []
-  handleConfirm()
-}
-
-const formItems = ref({
-  options: [
-    {
-      type: 'autocomplete',
-      key: 'labels',
-      value: null,
-      label: '人才标签 ',
-      multiple: true,
-      outlined: true,
-      itemText: 'label',
-      itemValue: 'value',
-      clearable: true,
-      items: [
-        // { label: '标签', value: '0' },
-      ]
-    },
-  ]
-})
-const handleSubmit = () => {
-  const labelsItem = formItems.value.options.find(f => f.key === 'labels')
-  chosenLabels.value = labelsItem?.value?.length ? labelsItem.value : []
-  showMore.value = false
-  handleConfirm()
-}
-
-// 获取数据
-const getData = async () => {
-  loading.value = true
-  init.value = false
-  const obj = { ...pageInfo, content: content.value }
-  if (chosenLabels.value?.length) obj.labels = chosenLabels.value
-  //
-  const res = await getRecruitPersonMapPage(obj)
-  total.value = res?.total ? res.total-0 : 0
-  dataList.value = res?.list?.length ? dealDictArrayData([], res?.list).map(e => {
-    e.areaName = e.area?.str ?? ''
-    return e
-  }) : []
-  loading.value = false
-}
-// getData()
-
-const handleChangePage = (e) => {
-  pageInfo.pageNo = e
-  getData()
-}
-
-// 筛选
-const handleConfirm = () => {
-  if (!chosenLabels.value?.length && !content.value) { // && init.value
-    Snackbar.warning('请先输入内容或选择人才标签!')
-    init.value = true
-    dataList.value = []; total.value = 0
-    return
+  
+  const clickChip = (index) => {
+    chosenLabels.value = [labelList.value[index]]
+    handleConfirm()
+  }
+  
+  const deleteChip = (index) => {
+    chosenLabels.value.splice(index, 1)
+    handleConfirm()
+  }
+  
+  const resetLabel = () => {
+    chosenLabels.value = []
+    handleConfirm()
+  }
+  
+  const formItems = ref({
+    options: [
+      {
+        type: 'autocomplete',
+        key: 'labels',
+        value: null,
+        label: '人才标签 ',
+        multiple: true,
+        outlined: true,
+        itemText: 'label',
+        itemValue: 'value',
+        clearable: true,
+        items: [
+          // { label: '标签', value: '0' },
+        ]
+      },
+    ]
+  })
+  const handleSubmit = () => {
+    const labelsItem = formItems.value.options.find(f => f.key === 'labels')
+    chosenLabels.value = labelsItem?.value?.length ? labelsItem.value : []
+    showMore.value = false
+    handleConfirm()
+  }
+  
+  // 获取数据
+  const getData = async () => {
+    loading.value = true
+    init.value = false
+    const obj = { ...pageInfo, content: content.value }
+    if (chosenLabels.value?.length) obj.labels = chosenLabels.value
+    //
+    const res = await getRecruitPersonMapPage(obj)
+    total.value = res?.total ? res.total-0 : 0
+    dataList.value = res?.list?.length ? dealDictArrayData([], res?.list).map(e => {
+      e.areaName = e.area?.str ?? ''
+      return e
+    }) : []
+    loading.value = false
+  }
+  // getData()
+  
+  const handleChangePage = (e) => {
+    pageInfo.pageNo = e
+    getData()
+  }
+  
+  // 筛选
+  const handleConfirm = () => {
+    if (!chosenLabels.value?.length && !content.value) { // && init.value
+      Snackbar.warning('请先输入内容或选择人才标签!')
+      init.value = true
+      dataList.value = []; total.value = 0
+      return
+    }
+    pageInfo.pageNo = 1
+    getData()
+  }
+  
+  const badgeColor = computed(() => (item) => {
+    return (item && item.sex) ? (item.sex === '1' ? '#1867c0' : 'error') : 'error'
+  })
+  
+  const badgeIcon = computed(() => (item) => {
+    return (item && item.sex) ? (item.sex === '1' ? 'mdi-gender-male' : 'mdi-gender-female') : 'mdi-gender-female'
+  })
+  
+  const textItem = ref({
+    type: 'text',
+    width: 1000,
+    value: '',
+    // label: '职位匹配',
+    label: '请输入职位匹配相关内容',
+    placeholder: '回车开始人才匹配',
+    clearable: false,
+    // readonly: true,
+    appendInnerIcon: 'mdi-magnify'
+  })
+  
+  // 人才详情
+  const talentPoolDetails = ({ userId }) => {
+    if (!userId) return
+    window.open(`/recruit/enterprise/talentPool/details/${userId}`)
+  }
+  
+  </script>
+  
+  <style scoped lang="scss">
+  :deep(.v-table > .v-table__wrapper > table > thead) {
+    background-color: #f7f8fa !important;
+  }
+  :deep(.v-selection-control__input) {
+    color: var(--v-primary-base) !important;
+  }
+  .card-box {
+    position: relative;
+    .v-center {
+      position: absolute;
+      top: 40%;
+      left: 50%;
+      translate: -50% -50%;
+    }
+  }
+  .requirementBox {
+    width: 150px;
+    height: 28px;
+    line-height: 28px;
+    // overflow: hidden;
+  }
+  .requirement {
+    white-space: pre-wrap;
+    word-break: break-all;
+    line-height: 28px;
+    color: var(--color-333);
+    font-size: 15px;
+    text-align: justify;
+    letter-spacing: 0;
+    // width: 60%;
+  }
+  .list-item {
+    border: 1px solid #e5e6eb;
+  }
+  .top {
+    display: flex;
+    background-color: #f7f8fa;
+    height: 50px;
+    line-height: 50px;
+    font-size: 14px;
+    color: var(--color-666);
+    padding: 0 20px;
+  }
+  .user-name {
+    font-size: 18px;
+    font-weight: 700;
+    color: #555;
+  }
+  .user-info {
+    color: var(--color-666);
+    font-size: 14px;
+    font-weight: 500;
+  }
+  :deep(.v-timeline-divider__dot--size-small) {
+    width: 10px !important;
+    height: 10px !important;
+    margin-top: 10px !important;
+  }
+  :deep(.v-timeline-divider__inner-dot) {
+    width: 10px !important;
+    height: 10px !important;
+  }
+  .bottom {
+    display: flex;
+    justify-content: space-between;
+    padding-bottom: 12px;
+    .experience {
+      width: 54%;
+      height: 100%;
+    }
+    .edu {
+      width: 40%;
+      height: 100%;
+    }
   }
-  pageInfo.pageNo = 1
-  getData()
-}
-
-const badgeColor = computed(() => (item) => {
-  return (item && item.sex) ? (item.sex === '1' ? '#1867c0' : 'error') : 'error'
-})
-
-const badgeIcon = computed(() => (item) => {
-  return (item && item.sex) ? (item.sex === '1' ? 'mdi-gender-male' : 'mdi-gender-female') : 'mdi-gender-female'
-})
-
-const textItem = ref({
-  type: 'text',
-  width: 1000,
-  value: '',
-  // label: '职位匹配',
-  label: '请输入职位匹配相关内容',
-  placeholder: '回车开始人才匹配',
-  clearable: false,
-  // readonly: true,
-  appendInnerIcon: 'mdi-magnify'
-})
-
-// 人才详情
-const talentPoolDetails = ({ userId }) => {
-  if (!userId) return
-  window.open(`/recruit/enterprise/talentPool/details/${userId}`)
-}
-
-</script>
-
-<style scoped lang="scss">
-:deep(.v-table > .v-table__wrapper > table > thead) {
-  background-color: #f7f8fa !important;
-}
-:deep(.v-selection-control__input) {
-  color: var(--v-primary-base) !important;
-}
-.card-box {
-  position: relative;
-  .v-center {
-    position: absolute;
-    top: 40%;
-    left: 50%;
-    translate: -50% -50%;
+  .second-title {
+    color: var(--color-666);
+    font-size: 15px;
   }
-}
-.requirementBox {
-  width: 150px;
-  height: 28px;
-  line-height: 28px;
-  // overflow: hidden;
-}
-.requirement {
-  white-space: pre-wrap;
-  word-break: break-all;
-  line-height: 28px;
-  color: var(--color-333);
-  font-size: 15px;
-  text-align: justify;
-  letter-spacing: 0;
-  // width: 60%;
-}
-.list-item {
-  border: 1px solid #e5e6eb;
-}
-.top {
-  display: flex;
-  background-color: #f7f8fa;
-  height: 50px;
-  line-height: 50px;
-  font-size: 14px;
-  color: var(--color-666);
-  padding: 0 20px;
-}
-.user-name {
-  font-size: 18px;
-  font-weight: 700;
-  color: #555;
-}
-.user-info {
-  color: var(--color-666);
-  font-size: 14px;
-  font-weight: 500;
-}
-:deep(.v-timeline-divider__dot--size-small) {
-  width: 10px !important;
-  height: 10px !important;
-  margin-top: 10px !important;
-}
-:deep(.v-timeline-divider__inner-dot) {
-  width: 10px !important;
-  height: 10px !important;
-}
-.bottom {
-  display: flex;
-  justify-content: space-between;
-  padding-bottom: 12px;
-  .experience {
-    width: 54%;
-    height: 100%;
+  .timeline-item {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 100%;
+    color: var(--color-666);
+    font-size: 13px;
+    .timeline-item-name {
+      width: 26%;
+    }
   }
-  .edu {
-    width: 40%;
-    height: 100%;
+  :deep(.v-timeline-item__body) {
+    width: 100%;
   }
-}
-.second-title {
-  color: var(--color-666);
-  font-size: 15px;
-}
-.timeline-item {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  width: 100%;
-  color: var(--color-666);
-  font-size: 13px;
-  .timeline-item-name {
-    width: 26%;
+  :deep(.v-timeline--vertical.v-timeline) {
+    row-gap: 0;
   }
-}
-:deep(.v-timeline-item__body) {
-  width: 100%;
-}
-:deep(.v-timeline--vertical.v-timeline) {
-  row-gap: 0;
-}
-</style>
+  </style>
+  

+ 0 - 0
src/views/recruit/enterprise/talentPool/index copy.vue → src/views/recruit/enterprise/talentPool/indexCopy.vue


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

@@ -57,10 +57,11 @@ import { useUserStore } from '@/store/user'
 import { usePersonCenterStore } from '@/store/personCenter'
 
 const userStore = useUserStore()
+
 const info = localStorage.getItem('baseInfo') ? JSON.parse(localStorage.getItem('baseInfo')) : {}
 
 const menuHide = { // 是否隐藏
-  studentInformation: (info?.type !== '1') // 学生信息管理。 type:'0'是求职者,'1'是学生
+  studentInformation: (info?.type && Number(info.type) !== 1) // 学生信息管理。 type:0是求职者,1是学生
 }
 
 // 左侧菜单列表
@@ -69,6 +70,7 @@ const list = computed(() => {
 })
 console.log(import.meta.env.VITE_NODE_ENV, '当前环境变量===========')
 
+
 const getList = (arr, obj = []) => {
   arr.forEach(element => {
     if (element.show) return

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

@@ -1,5 +1,5 @@
 <template>
-  <div class="position-item mb-3 job-closed elevation-2"
+  <div class="position-item mb-3 job-closed" :class="val.active ? 'elevation-8' : 'elevation-3'"
     style="position: relative;"
     v-for="(val, i) in props.items" :key="i" @mouseenter="val.active = true" @mouseleave="val.active = false"
   >
@@ -122,9 +122,6 @@ const handleRefuse = (val) => {
   height: 160px;
   background-color: #fff;
   border-radius: 12px;
-  &:hover {
-    box-shadow: 0px 3px 5px -1px var(--v-shadow-key-umbra-opacity, rgba(0, 0, 0, 0.2)), 0px 5px 8px 0px var(--v-shadow-key-penumbra-opacity, rgba(0, 0, 0, 0.14)), 0px 1px 14px 0px var(--v-shadow-key-ambient-opacity, rgba(0, 0, 0, 0.12)) !important;
-  }
   .info-header {
     height: 48px;
     background: linear-gradient(90deg,#f5fcfc,#fcfbfa);

+ 1 - 1
src/views/recruit/personal/PersonalCenter/resume/analysis/components/basicInfo.vue

@@ -223,7 +223,7 @@ const items = ref({
       col: 6,
       items: [],
     },
-    
+
   ]
 })
 

+ 1 - 1
src/views/recruit/personal/PersonalCenter/resume/analysis/components/educationExp.vue

@@ -59,7 +59,7 @@ const getMajorListData = async (name) => {
   }
   if (item.items?.length && (majorNameInput.value === name)) return // 防抖
   item[item.itemTextName] = majorNameInput.value = name
-  
+
   if (name === null || name === '') { item.items = [] }
   else {
     const data = await schoolMajorByName({ name })

+ 4 - 4
src/views/recruit/personal/PersonalCenter/resume/online/components/basicInfo.vue

@@ -9,8 +9,8 @@
       <div class="avatarsBox" @mouseover="showIcon = true" @mouseleave="showIcon = false">
         <v-badge
           v-if="baseInfo?.sex === '1' || baseInfo?.sex === '2'"
-          bordered 
-          :color="baseInfo?.sex ? (baseInfo?.sex === '1' ? '#1867c0' : 'error') : 'error'" 
+          bordered
+          :color="baseInfo?.sex ? (baseInfo?.sex === '1' ? '#1867c0' : 'error') : 'error'"
           :icon="baseInfo?.sex ? (baseInfo?.sex === '1' ? 'mdi-gender-male' : 'mdi-gender-female') : 'mdi-gender-female'">
           <v-img :src="getUserAvatar(baseInfo?.avatar, baseInfo?.sex)" width="130" height="130" style="border-radius: 6px;"></v-img>
           <div v-show="showIcon" @click.self="openFileInput" class="mdi mdi-camera-outline camera">
@@ -108,7 +108,7 @@
       </div>
     </div>
   </div>
-  
+
   <Loading :visible="overlay"></Loading>
   <!-- 图片裁剪 -->
   <ImgCropper :visible="isShowCopper" :image="selectPic" :cropBoxResizable="true" @submit="handleHideCopper" :aspectRatio="1 / 1" @close="isShowCopper = false, selectPic = ''"></ImgCropper>
@@ -405,7 +405,7 @@ const items = ref({
       col: 6,
       items: [],
     },
-    
+
   ]
 })
 

+ 1 - 1
src/views/recruit/personal/PersonalCenter/resume/online/components/educationExp.vue

@@ -92,7 +92,7 @@ const getMajorListData = async (name) => {
   }
   if (item.items?.length && (majorNameInput.value === name)) return // 防抖
   item[item.itemTextName] = majorNameInput.value = name
-  
+
   if (name === null || name === '') { item.items = [] }
   else {
     const data = await schoolMajorByName({ name })

+ 112 - 60
src/views/recruit/personal/PersonalCenter/student/InternshipReport/index.vue

@@ -1,104 +1,156 @@
 <template>
 	<!-- 筛选 -->
 	<div class="pa-3 d-flex justify-space-between align-center">
-		<Autocomplete v-model="query.enterpriseId" :item="selectItems" style="width: 300px;" />
+		<Autocomplete v-model="enterpriseId" @change="getList" :item="selectItems" style="width: 300px;" />
 		<div>
-			<v-btn color="primary" elevation="5" prepend-icon="mdi-plus" @click="handleAddReport">新增报告</v-btn>
+			<v-btn color="primary" elevation="5" prepend-icon="mdi-refresh" @click="getList">刷 新</v-btn>
+			<v-btn color="#00897B" class="ml-5" elevation="5" prepend-icon="mdi-plus" @click="handleAddReport">新增报告</v-btn>
 		</div>
 	</div>
 	<v-divider class="ma-3"></v-divider>
 	<!-- 实习报告 -->
 	<div class="pa-3">
-		<div v-for="item in items" :key="item.date" class="mb-3">
-			<div class="title-date">{{ 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 v-if="items && items.length > 0">
+			<div v-for="item in items" :key="item.date" class="mb-3">
+				<div class="title-date">{{ 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" />
 	</div>
 
 	<CtDialog :visible="showDialog" :widthType="2" titleClass="text-h6" :footer="true" title="新增实习报告" @close="handleClose" @submit="handleSubmit">
-		<div>
-			<Imgs v-model="fileUrls" :showTips="false" limit="9"></Imgs>
-			<div class="color-999 font-size-14 mt-3 text-left">
-				<p>*最多可上传9张图片</p>
-				<p>*只支持JPG、JPEG、PNG类型的图片</p>
-			</div>
-		</div>
+		<CtForm ref="CtFormRef" :items="formItems">
+			<template #urlList="{ item }">
+				<div>
+					<p class="color-primary">*请上传实习报告图片(最多可上传9张图片)</p>
+					<p class="mb-3 color-primary">*只支持JPG、JPEG、PNG类型的图片</p>
+					<Imgs v-model="item.value" :showTips="false" limit="9"></Imgs>
+				</div>
+			</template>
+		</CtForm>
 	</CtDialog>
 </template>
 
 <script setup>
 defineOptions({ name: 'InternshipReport' })
-import { ref } from 'vue'
+import { ref, onMounted } from 'vue'
 import { usePersonCenterStore } from '@/store/personCenter'
+import { useRoute } from 'vue-router'
+import { getStudentReportList, saveStudentReport, getStudentPracticeCompanyList } from '@/api/recruit/personal/student'
+import Snackbar from '@/plugins/snackbar'
+import { formatName } from '@/utils/getText'
 
-const query = ref({
-	enterpriseId: null
+const route = useRoute()
+const enterpriseId = ref(null)
+const CtFormRef = ref(null)
+const items = ref([])
+const formItems = ref({
+  options: [
+    {
+      type: 'autocomplete',
+      key: 'enterpriseId',
+      value: null,
+      defaultValue: null,
+      label: '实习企业 *',
+      outlined: true,
+      itemText: 'enterpriseName',
+      itemValue: 'id',
+      rules: [v => !!v || '请选择实习企业'],
+      items: []
+    },
+    {
+			slotName: 'urlList',
+			value: [],
+			defaultValue: [],
+			key: 'urlList',
+			label: '实习报告 *'
+    },
+  ]
 })
-const items = ref([
-	{
-		date: '2025-02-21',
-		arr: [
-			'https://minio.citupro.com/dev/huomiao/report.png',
-			'https://minio.citupro.com/dev/huomiao/report1.png',
-			'https://minio.citupro.com/dev/huomiao/report2.png'
-		]
-	},
-	{
-		date: '2025-02-25',
-		arr: [
-			'https://minio.citupro.com/dev/huomiao/report.png',
-			'https://minio.citupro.com/dev/huomiao/report1.png',
-			'https://minio.citupro.com/dev/huomiao/report2.png'
-		]
-	},
-	{
-		date: '2025-03-01',
-		arr: [
-			'https://minio.citupro.com/dev/huomiao/report.png',
-			'https://minio.citupro.com/dev/huomiao/report1.png',
-			'https://minio.citupro.com/dev/huomiao/report2.png'
-		]
-	}
-])
-
-const handleChangeEnterprise = (val) => {
-  console.log(val, 'enterpriseId')
-}
 
 const selectItems = ref({
   label: '请选择要查看的企业',
-  itemText: 'name',
+  itemText: 'enterpriseName',
   itemValue: 'id',
   clearable: true,
   hideDetails: true,
-	change: handleChangeEnterprise,
   items: []
 })
 
+// 获取实习企业列表
+const getCompanyList = async () => {
+  try {
+    const data = await getStudentPracticeCompanyList()
+		data.forEach(e => {
+			e.id = e.id.toString()
+			e.enterpriseName = formatName(e.anotherName || e.name)
+		})
+		selectItems.value.items = data
+		formItems.value.options.find(e => e.key === 'enterpriseId').items = data
+	} catch {}
+}
+
+// 实习报告列表
+const getList = async () => {
+	items.value = []
+	try {
+		const data = await getStudentReportList(enterpriseId.value ? { enterpriseId: enterpriseId.value } : {})
+		if (!data || !Object.keys(data).length) return
+		for (let item in data) {
+			items.value.push({ date: item, arr: data[item].map(e => e.url) })
+		}
+	} catch {}
+}
+
+onMounted(() => {
+	getCompanyList()
+	const { id } = route.query
+	if (id) {
+		enterpriseId.value = id
+	}
+	getList()
+})
+
+
 // 新增实习报告
-const fileUrls = ref([])
 const showDialog = ref(false)
 const handleAddReport = () => {
-	fileUrls.value = []
+	formItems.value.options.find(e => e.key === 'urlList').value = []
 	showDialog.value = true
 }
 
 const handleClose = () => {
+	formItems.value.options.forEach(e => e.value = e.defaultValue)
 	showDialog.value = false
 }
 
-const handleSubmit = () => {
-  console.log('handleSubmit', fileUrls.value)
-	showDialog.value = false
+const handleSubmit = async () => {
+	const { valid } = await CtFormRef.value.formRef.validate()
+	if (!valid) return
+	let obj = {}
+	formItems.value.options.forEach(e => {
+		obj[e.key] = e.value
+	})
+	if (!obj.urlList || !obj.urlList.length) return Snackbar.warning('请上传实习报告')
+
+	try {
+		await saveStudentReport(obj)
+		Snackbar.success('保存成功')
+		handleClose()
+		getList()
+	} catch {
+		handleClose()
+	}
 }
 
 // 预览

+ 26 - 8
src/views/recruit/personal/PersonalCenter/student/information/index.vue

@@ -32,10 +32,11 @@ const items = ref({
     {
       type: 'autocomplete',
       key: 'schoolId',
-      value: null,
+      value: {},
       default: null,
       label: '就读学校 *',
       outlined: true,
+      returnObject: true,
       itemText: 'schoolName',
       itemValue: 'schoolId',
       rules: [v => !!v || '请选择就读学校'],
@@ -50,7 +51,7 @@ const items = ref({
       label: '所在院系 *',
       outlined: true,
       itemText: 'departmentTitle',
-      itemValue: 'schoolDepartmentId',
+      itemValue: 'departmentTitle',
       rules: [v => !!v || '请选择所在院系'],
       items: []
     },
@@ -127,12 +128,16 @@ items.value.options.forEach((e, index) => {
   if ((index + 2) % 2 === 0) e.flexStyle = 'mr-3'
 })
 
-// // 学校下拉列表
-const getSchoolListData = async () => {
+// 学校下拉列表
+const getSchoolListData = async (schoolId) => {
   const item = items.value.options.find(e => e.key === 'schoolId')
   if (!item) return
-  const { records } = await schoolList({current: 1,size: 9999})
+  const { records } = await schoolList({ current: 1,size: 9999 })
   item.items = records || []
+
+  if (schoolId) {
+    item.value = records.find(e => e.schoolId === schoolId)
+  }
 }
 getSchoolListData()
 
@@ -141,20 +146,27 @@ const getDepartmentList = async (e) => {
   if (!item) return
   const query = {
     page: { size: 9999,	current: 1	},
-    entity: { schoolId: e }
+    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
 }
 
 // 获取学生基本信息
 const studentInfoFun = async () => {
+  await userStore.getStudentInformation()
   const data = JSON.parse(localStorage.getItem('studentInfo') || '{}')
   if (data.schoolId) getDepartmentList(data.schoolId)
   // 回显
   items.value.options.forEach(e => {
-    if (data[e.key]) e.value = data[e.key]
+    if (data[e.key]) {
+      if (e.key === 'schoolId') {
+        getSchoolListData(data[e.key])
+      }
+      else e.value = data[e.key]
+    }
   })
 }
 studentInfoFun()
@@ -167,8 +179,14 @@ const handleSubmit = async () => {
   overlay.value = true
   const params = {}
   items.value.options.forEach(item => {
-    params[item.key] = item.value
+    params[item.key] = item.returnObject ? '' : item.value
+    if (item.key === 'schoolId') {
+      params.schoolName = item.value.schoolName
+      params.schoolId = item.value.schoolId
+    } else params[item.key] = item.value
   })
+
+
   await saveStudentSimpleInfo(params)
   setTimeout(async () => {
     await userStore.getStudentInformation()

+ 152 - 54
src/views/recruit/personal/PersonalCenter/student/intershipCompany/index.vue

@@ -1,69 +1,167 @@
 <template>
-	<v-tabs v-model="tab" align-tabs="start" color="primary" bg-color="#f7f8fa">
-    <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>
 
-	<ItemPage v-if="data[tabList[tab].key]?.length" :tab="tab" class="mt-3" :items="data[tabList[tab].key]" />
-	<Empty v-else :elevation="false" />
+		<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>
+
+	<Loading :visible="showLoading"></Loading>
 </template>
 
 <script setup>
+// 实习企业
 defineOptions({ name: 'PersonalCenterStudentInternshipCompany'})
-import { ref } from 'vue'
-import { practiceProcess } from '@/api/recruit/personal/personalCenter/student.js'
+import { ref, onMounted } from 'vue'
+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([
-	{ label: '等待中', value: 0, key: 'waitProcess' },
-	{ label: '进行中', value: 1, key: 'process' },
-	{ label: '已结束', value: 2, key: 'endProcess' },
-])
-const studentInfo = ref(localStorage.getItem('studentInfo') ? JSON.parse(localStorage.getItem('studentInfo')) : {})
-
-const data = ref({
-	waitProcess: [
-		{
-			job: {
-				name: '前端开发',
-				payFrom: 10000,
-				payTo: 20000,
-				payUnit: '月',
-				area: {
-					"str": "呼和浩特市-和林格尔县"
-				},
-				id: '1864521193404727297',
-				areaId: 150123,
-				eduType: null,
-				expType: null,
-				status: '0',
-				updateTime: 1740972578585
-			},
-			enterprise: {
-				id: '1',
-				anotherName: '门墩儿',
-				name: '门墩儿科技有限公司',
-				logoUrl: 'https://minio.citupro.com/dev/menduner/company-avatar.png',
-				industryName: '互联网',
-				scaleName: '10000-50000人',
-			},
-			entity: {
-				jobJoinDate: 1740972578585,
-				internshipEndDate: 1740972578585,
-				percentage: '50',
-			}
-		}
-	],
-	process: [],
-	endProcess: []
+const tab = ref(1)
+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 () => {
-	data = await practiceProcess({ studentId: studentInfo.value.userId })
-	// console.log(data, 'list')
+	query.value.status = tabList.value[tab.value]?.value
+	const result = await getStudentPracticePage(query.value)
+	items.value = result?.list.map(e => {
+		e.enterprise = dealDictObjData({}, e.enterprise)
+		e.job = dealDictObjData({}, e.job)
+		return e
+	})
+	total.value = result?.total || 0
+}
+
+onMounted(async () => {
+	const { data } = await getDict('student_practice_status')
+	tabList.value = data || []
+
+	await getList()
+})
+
+const handleChangeTab = () => {
+	query.value.pageNo = 1
+	getList()
+}
+
+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)
 }
-// getList()
 </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>

+ 51 - 9
src/views/recruit/personal/PersonalCenter/student/intershipCompany/item.vue

@@ -8,13 +8,14 @@
         <div class="job-info">
           <div class="job-name ellipsis" :class="{'cursor-pointer': val.job.status === '0'}" v-ellipse-tooltip>
             <span class="mr-3" :class="{'info-name': val.job.status === '0'}" @click.stop="handleToPositionDetails(val)">{{ formatName(val.job.name) }}</span>
-            <span>
-              [{{ !val.job.areaId ? '全国' : val.job.area?.str }}]
+            <span v-if="val.job.status === '1'" class="font-size-14 color-error">[职位已关闭]</span>
+            <span v-else>
+              <span v-if="!val.job?.areaId || val.job.areaName">[{{ !val.job.areaId ? '全国' : val.job.areaName }}]</span>
             </span>
           </div>
           <div class="job-other">
             <span v-if="!val.job.payFrom && !val.job.payTo" class="salary color-primary">面议</span>
-            <span v-else class="salary color-primary">{{ val.job.payFrom ? val.job.payFrom + '-' : ''}}{{ val.job.payTo }}{{ val.job.payUnit ? '/' + val.job.payUnit : '' }}</span>
+            <span v-else class="salary color-primary">{{ val.job.payFrom ? val.job.payFrom + '-' : ''}}{{ val.job.payTo }}{{ val.job.payName ? '/' + val.job.payName : '' }}</span>
             <v-chip v-if="val.job?.expName" class="mx-3" color="primary" label size="small">{{ val.job.expName }}</v-chip>
             <v-chip v-if="val.job?.eduName" color="primary" label size="small">{{ val.job.eduName }}</v-chip>
           </div>
@@ -35,13 +36,29 @@
         </div>
       </div>
 			<div class="mx-5" style="border-bottom: 1px dashed #e0e0e0"></div>
+
 			<!-- 实习范围 -->
 			<div class="d-flex justify-space-between px-5 py-3">
 				<div class="color-666 font-size-15">
-					<p>实习时间:{{ timesTampChange(val.entity.jobJoinDate, 'Y-M-D') }} 至 {{ timesTampChange(val.entity.internshipEndDate, 'Y-M-D') }}</p>
+					<p>实习时间:{{ timesTampChange(val.startTime, 'Y-M-D') }} 至 {{ timesTampChange(val.endTime, 'Y-M-D') }}</p>
 				</div>
-				<div class="text-end" v-if="tab === 0">
-					<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-if="val.evaluate && val.certificate" 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>
@@ -50,23 +67,42 @@
 
 <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,
     default: () => []
-  },
-	tab: Number
+  }
 })
 
 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
@@ -80,11 +116,17 @@ 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">
 .position-item {
-  height: 144px;
   background-color: #fff;
   border-radius: 12px;
   &:hover {

+ 182 - 0
src/views/recruit/personal/PersonalCenter/studentInformation/index.vue

@@ -0,0 +1,182 @@
+<template>
+  <div style="padding: 20px 30px">
+    <div class="resume-header mb-3">
+      <div class="resume-title">学生信息认证</div>
+    </div>
+    <div class="d-flex flex-column align-center pt-5">
+      <CtForm ref="CtFormRef" :items="items" style="width: 900px;"></CtForm>
+      <v-btn class="buttons mt-5" color="primary" @click.stop="handleSubmit">{{ $t('common.save') }}</v-btn>
+    </div>
+  </div>
+
+  <Loading :visible="overlay"></Loading>
+</template>
+
+<script setup>
+defineOptions({name: 'personal-personCenter-studentInformation-index'})
+import { ref } from 'vue'
+import { saveStudentSimpleInfo } from '@/api/recruit/personal/shareJob'
+import { schoolList, departmentList, getStudentInfo } from '@/api/recruit/personal/resume'
+import { useI18n } from '@/hooks/web/useI18n'
+import Snackbar from '@/plugins/snackbar'
+import { isValidIdCard18 } from '@/utils/validate'
+
+const { t } = useI18n()
+
+const overlay = ref(false)
+
+const CtFormRef = ref()
+const items = ref({
+  options: [
+    {
+      type: 'autocomplete',
+      key: 'schoolId',
+      value: null,
+      default: null,
+      label: '就读学校 *',
+      outlined: true,
+      itemText: 'schoolName',
+      itemValue: 'schoolId',
+      rules: [v => !!v || '请选择就读学校'],
+      items: [],
+      change: e => getDepartmentList(e),
+    },
+    {
+      type: 'autocomplete',
+      key: 'schoolDepartmentName',
+      value: null,
+      default: null,
+      label: '所在院系 *',
+      outlined: true,
+      itemText: 'departmentTitle',
+      itemValue: 'schoolDepartmentId',
+      rules: [v => !!v || '请选择所在院系'],
+      items: []
+    },
+    {
+      type: 'text',
+      key: 'majorName',
+      value: '',
+      default: null,
+      label: '所学专业 *',
+      outlined: true,
+      rules: [v => !!v || '请输入所学专业']
+    },
+    {
+      type: 'text',
+      key: 'schoolClassName',
+      value: '',
+      default: null,
+      label: '所在班级 *',
+      outlined: true,
+      rules: [v => !!v || '请填写所在班级']
+    },
+    {
+      type: 'text',
+      key: 'studentNo',
+      value: '',
+      default: null,
+      label: '学号 *',
+      outlined: true,
+      rules: [v => !!v || '请填写学号']
+    },
+    {
+      type: 'text',
+      key: 'idCardNo',
+      value: '',
+      label: '身份证号码 *',
+      rules: [
+        value => {
+          if (!value) {
+            return '请输入您的身份证号码'
+          }
+          return true
+        },
+        value => {
+          if (!isValidIdCard18(value)) {
+            return '请输入正确的身份证号码'
+          }
+          return true
+        }
+      ]
+    },
+    {
+      type: 'text',
+      key: 'emergencyContactName',
+      value: '',
+      default: null,
+      label: '紧急联系人姓名 *',
+      outlined: true,
+      rules: [v => !!v || '请填写紧急联系人姓名']
+    },
+    {
+      type: 'phoneNumber',
+      key: 'emergencyContactPhone',
+      value: '',
+      clearable: true,
+      label: '紧急联系人手机号 *',
+      rules: [v => !!v || '请填写紧急联系人手机号']
+    },
+  ]
+})
+
+// 左侧加mr
+items.value.options.forEach((e, index) => {
+  e.col = 6
+  if ((index + 2) % 2 === 0) e.flexStyle = 'mr-3'
+})
+
+// // 学校下拉列表
+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 || []
+}
+getSchoolListData()
+
+const getDepartmentList = async (e) => {
+  const item = items.value.options.find(e => e.key === 'schoolDepartmentName')
+  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)
+}
+
+// 获取学生基本信息
+const getStudentInfoFun = async () => {
+  const data = await getStudentInfo()
+  if (data.schoolId) getDepartmentList(data.schoolId)
+  // 回显
+  items.value.options.forEach(e => {
+    if (data[e.key]) e.value = data[e.key]
+  })
+}
+getStudentInfoFun()
+
+
+// 提交
+const handleSubmit = async () => {
+  const { valid } = await CtFormRef.value.formRef.validate()
+  if (!valid) return
+  overlay.value = true
+  const params = {}
+  items.value.options.forEach(item => {
+    params[item.key] = item.value
+  })
+  await saveStudentSimpleInfo(params)
+  // getStudentInfoFun()
+  setTimeout(async () => {
+    Snackbar.success(t('common.submittedSuccessfully'))
+    overlay.value = false
+  }, 1000)
+}
+
+</script>
+
+<style scoped lang="scss">
+</style>

+ 0 - 8
src/views/recruit/personal/PersonalCenter/wallet/index.vue

@@ -15,7 +15,6 @@
 defineOptions({ name: 'person-center-wallet'})
 import myBalance from './myBalance'
 import IntegralPage from '@/views/integral/pointsManagement'
-// import { useUserStore } from '@/store/user'
 import { ref } from 'vue'
 import { useRouter } from 'vue-router'
 
@@ -23,13 +22,6 @@ const tab = ref(0)
 const router = useRouter()
 
 if (router.currentRoute.value.query.key) tab.value = Number(router.currentRoute.value.query.key)
-
-// const store = useUserStore()
-// const updateAccountInfo = async () => {
-//   await store.getUserAccountInfo()
-// }
-// updateAccountInfo()
-
 </script>
 
 <style scoped lang="scss">

+ 0 - 1
src/views/recruit/personal/companyDetail/components/positions.vue

@@ -47,7 +47,6 @@
                 <v-icon color="var(--color-666)" size="15">{{ k.mdi }}</v-icon>
                 <span class="ml-1 tag-text">
                   {{ k.value === 'areaName' ? !val.job.areaId ? '全国' : val.job.area?.str : val.job[k.value] }}
-                  <!-- {{ (k.value === 'areaName' && !val.job.areaId) ? '全国' : val.job[k.value] }} -->
                 </span>
               </span>
             </span>

+ 0 - 2
src/views/recruit/personal/position/components/conditionFilter.vue

@@ -96,8 +96,6 @@ const assembleList = async ({ key, idsStr }) => {
 watch(
   () => route.query, 
   (newVal = {}, oldVal = {}) => {
-    // console.log('1oldVal', oldVal)
-    // console.log('2newVal', newVal)
     const newKeyList = Object.keys(newVal).length ? [...Object.keys(newVal)] : null
 
     // 回显已选筛选-标签

+ 3 - 1
src/views/recruit/personal/position/components/details.vue

@@ -165,6 +165,7 @@ import {
   jobCvRelCheckSend,
   jobCvRelSend
 } from '@/api/position'
+import { jobFairPositionDeliveryCheck } from '@/api/recruit/personal/jobFair'
 import { getPersonResumeCv, savePersonResumeCv } from '@/api/recruit/personal/resume'
 
 import { DPR } from '@/utils'
@@ -175,7 +176,6 @@ import { getUserAvatar } from '@/utils/avatar'
 import { checkPersonBaseInfo } from '@/utils/check'
 import dialogExtend from '@/plugins/dialogExtend'
 import { formatName } from '@/utils/getText'
-import { jobFairPositionDeliveryCheck } from '@/api/recruit/personal/jobFair'
 import studentDeliveryForm from '@/views/recruit/personal/components/studentDeliveryForm.vue'
 
 const emit = defineEmits(['preview'])
@@ -537,6 +537,8 @@ const toDetails = async (info) => {
     if (info.contact.enterpriseId) {
       url += `&enterprise=${info.contact.enterpriseId}`
     }
+    // 参与招聘会的职位需传递招聘会id
+    // if (jobFairId.value) url+= `&jobFairId=${jobFairId.value}`
 
     window.open(url)
   } catch (error) {

+ 1 - 1
src/views/recruit/personal/recommend/components/positionList.vue

@@ -96,7 +96,7 @@ const handleClick = (item, index) => {
   width: 100%;
   height: 100%;
 }
-.chosen { border: 1px solid var(--v-primary-lighten2) !important; }
+.chosen { border: 1px solid var(--v-primary-base) !important; }
 .sub-li {
   position: relative;
   width: 384px;

+ 0 - 113
src/views/recruit/personal/shareJob/form/simpleInfo.vue

@@ -1,113 +0,0 @@
-<template>
-  <div style="width: 100%;">
-    <CtForm ref="formPageRef" :items="items"></CtForm>
-  </div>
-</template>
-
-<script setup>
-import { getDict } from '@/hooks/web/useDictionaries'
-defineOptions({name: 'shareJob-form-baseInfo'})
-import { reactive, ref } from 'vue'
-
-const formPageRef = ref()
-let query = reactive({})
-
-const items = ref({
-  options: [
-    {
-      type: 'text',
-      key: 'name',
-      value: '',
-      default: null,
-      label: '姓名 *',
-      outlined: true,
-      rules: [v => !!v || '请输入姓名']
-    },
-    {
-      type: 'phoneNumber',
-      key: 'phone',
-      value: '',
-      clearable: true,
-      label: '联系手机号 *',
-      rules: [v => !!v || '请填写联系手机号']
-    },
-    {
-      type: 'autocomplete',
-      key: 'jobStatus',
-      value: '',
-      default: null,
-      label: '求职状态 *',
-      outlined: true,
-      itemText: 'label',
-      itemValue: 'value',
-      dictTypeName: 'menduner_job_seek_status',
-      rules: [v => !!v || '请选择求职状态'],
-      items: []
-    },
-    {
-      type: 'autocomplete',
-      key: 'expType',
-      value: '',
-      default: null,
-      label: '工作经验 *',
-      outlined: true,
-      itemText: 'label',
-      itemValue: 'value',
-      dictTypeName: 'menduner_exp_type',
-      rules: [v => !!v || '请选择工作经验'],
-      items: []
-    },
-    {
-      type: 'autocomplete',
-      key: 'eduType',
-      value: '',
-      default: null,
-      label: '最高学历 *',
-      outlined: true,
-      itemText: 'label',
-      itemValue: 'value',
-      dictTypeName: 'menduner_education_type',
-      rules: [v => !!v || '请选择最高学历'],
-      items: []
-    },
-  ]
-})
-
-// 获取字典内容
-const getDictData = async (dictTypeName) => {
-  const item = items.value.options.find(e => e.dictTypeName === dictTypeName)
-  if (item) {
-    const { data } = await getDict(dictTypeName)
-    item.items = data
-  }
-}
-
-const userInfo = ref(localStorage.getItem('userInfo') ? JSON.parse(localStorage.getItem('userInfo')) : {})
-items.value.options.forEach((e) => {
-  if (e.dictTypeName) getDictData(e.dictTypeName) // 查字典set options
-  e.value = userInfo.value[e.key] // 回显
-  // formItems回显
-  // const infoExist = baseInfo.value && Object.keys(baseInfo.value).length
-  // if (infoExist && baseInfo.value[e.key]) e.value = baseInfo.value[e.key]
-  // // 日期相关
-  // if (e.type === 'datepicker') e.value = timesTampChange(e.value).slice(0, 10)
-  // // 所在城市回显
-  // if (infoExist && e.nameKey) e[e.nameKey] = baseInfo.value[e.nameKey]
-})
-
-const getQuery = async () => {
-  const { valid } = await formPageRef.value.formRef.validate()
-  if (!valid) return false
-  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
-  })
-  query = Object.assign(query, obj)
-  return query
-}
-
-defineExpose({
-  getQuery
-})
-</script>

+ 0 - 83
src/views/recruit/personal/shareJob/form/upload.vue

@@ -1,83 +0,0 @@
-<template>
-  <div style="width: 100%;">
-    <CtForm ref="formPageRef" :items="items"></CtForm>
-    <div class="color-666 mb-3" style="font-size: 13px;">* 仅支持.doc, .docx, .pdf文件且大小不能超过20MB</div>
-  </div>
-</template>
-
-<script setup>
-defineOptions({name: 'shareJob-form-upload'})
-import { reactive, ref } from 'vue'
-import { useI18n } from '@/hooks/web/useI18n'
-import { uploadFile } from '@/api/common'
-import Snackbar from '@/plugins/snackbar'
-
-const { t }  = useI18n()
-const formPageRef = ref()
-let query = reactive({})
-
-// 上传简历
-const typeList = ['pdf', 'doc', 'docx']
-const handleUpload = async (e) => {
-  const file = e
-  const size = file.size
-  if (size / (1024*1024) > 20) {
-    Snackbar.warning(t('common.fileSizeExceed'))
-    return
-  }
-  const arr = file.name.split('.')
-  if (typeList.indexOf(arr[arr.length - 1]) < 0) {
-    Snackbar.warning(t('common.fileFormatIncorrect'))
-    return
-  }
-  const formData = new FormData()
-  formData.append('file', file)
-  formData.append('path', 'attachment')
-  const { data } = await uploadFile(formData)
-  if (!data) return
-  const urlItem = items.value.options.find(e => e.key === 'url')
-  if (urlItem) urlItem.data = data
-  query.fileName = file.name
-}
-
-const items = ref({
-  options: [
-    {
-      type: 'text',
-      key: 'title',
-      value: null,
-      label: '附件简历名称 *',
-      rules: [v => !!v || '请填写附件简历名称']
-    },
-    {
-      type: 'upload',
-      key: 'url',
-      value: null,
-      data: '',
-      label: '附件简历 *',
-      placeholder: '请上传附件简历',
-      accept: '.doc, .docx, .pdf',
-      prependInnerIcon: 'mdi-file-document-outline',
-      rules: [v => !!v || '请上传附件简历'],
-      change: handleUpload
-    }
-  ]
-})
-
-
-const getQuery = async () => {
-  const { valid } = await formPageRef.value.formRef.validate()
-  if (!valid) return
-  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
-  })
-  query = Object.assign(query, obj)
-  return query
-}
-
-defineExpose({
-  getQuery
-})
-</script>

+ 0 - 311
src/views/recruit/personal/shareJob/index.vue

@@ -1,311 +0,0 @@
-<!-- 分享职位 -->
-<template>
-  <div style="background-color: #f0f0f0;" :style="{'padding': isMobile ? '0' : '10px'}">
-    <v-card
-      class="py-3 px-5"
-      style="min-height: calc(100vh - 20px); box-sizing: border-box; margin: 0 auto;"
-      :style="{'width': isMobile ? '100%' : '750px'}"
-    >
-      <div v-if="!Object.keys(info).length">加载失败</div>
-      <div v-else>
-        <div class="d-flex justify-space-between">
-          <h2 class="JobName ellipsis">{{ info.name }}</h2>
-          <span v-if="!info.payFrom && !info.payTo" class="salary">面议</span>
-          <span v-else class="salary">{{ info.payFrom ? info.payFrom + '-' : '' }}{{ info.payTo }}{{ positionInfo.payName ? '/' + positionInfo.payName : '' }}</span>
-        </div>
-        <div class="d-flex justify-space-between mt-4">
-          <div class="banner-tags">
-            <div v-for="k in desc" :key="k.mdi" class="mr-3">
-              <v-icon color="var(--color-666)" size="20">{{ k.mdi }}</v-icon>
-              <span class="f-w-600 ml-1">{{ positionInfo[k.value] }}</span>
-            </div>
-          </div>
-          <div class="mt-4" v-if="info?.tagList">
-            <v-chip size="small" class="mr-1 mb-1" color="primary" label v-for="(k, i) in info.tagList" :key="i">{{ k }}</v-chip>
-          </div>
-        </div>
-        <div class="text-end d-flex align-center justify-space-between mt-3">
-          <v-btn
-            class="button-item radius"
-            color="warning" 
-            variant="outlined" 
-            :prepend-icon="isCollection ? 'mdi-heart' : 'mdi-heart-outline'"
-            @click="handleCollection"
-          >{{ isCollection ? $t('position.cancelFavorite') : $t('position.collection') }}</v-btn>
-          <svg-icon v-if="info.hire" name="pin" size="50"></svg-icon>
-        </div>
-        <div v-if="info.hire" class="mt-3">
-          <v-chip v-if="info.hirePrice" label color="primary">赏金:{{ commissionCalculation(info.hirePrice / 100, 1) }}元</v-chip>
-          <v-chip v-if="info.hirePoint" label color="primary">积分:{{ commissionCalculation(info.hirePoint / 100, 1) }}点</v-chip>
-        </div>
-        <v-divider class="mt-3"></v-divider>
-        <div class="mt-3 mb-1 f-w-600">{{ $t('position.jobResponsibilities') }}</div>
-        <div v-if="info.content" class="requirement" v-html="info.content?.replace(/\n/g, '</br>')"></div>
-        <div v-else>暂无</div>
-        <div class="mt-3 mb-1 f-w-600">{{ $t('position.jobRequirements') }}</div>
-        <div v-if="info.requirement" class="requirement" v-html="info.requirement?.replace(/\n/g, '</br>')"></div>
-        <div v-else>暂无</div>
-        <v-divider class="my-3"></v-divider>
-        <div class="contact">
-          <div class="float-left d-flex align-center">
-            <v-img :src="info.contact?.avatar || 'https://minio.citupro.com/dev/menduner/7.png'" :width="45" style="height: 45px;"></v-img>
-            <div class="ml-2">
-              <div class="contact-name">{{ info.contact?.name }}</div>
-              <div class="contact-info">{{ info.enterprise?.name }} · {{ info.contact?.postNameCn }}</div>
-            </div>
-          </div>
-        </div>
-        <v-divider class="my-3"></v-divider>
-        <div>
-          <h4>{{ $t('position.address') }}</h4>
-          <div class="mt-1">
-            <v-icon size="25" color="primary">mdi-map-marker</v-icon>
-            <span style="color: var(--color-666);font-size: 15px;">{{ info.address }}</span>
-          </div>
-        </div>
-        <div class="text-center my-10">
-          <v-btn v-if="!isMobile" class="mr-2 radius button-item" color="success" variant="outlined" target="_blank" to="/recruit/personal/position">{{ $t('position.moreBtn') }}</v-btn>
-          <v-btn class="radius button-item" color="primary" :disabled="delivery" @click="sendResumeProcessVerify">{{ delivery ? $t('position.delivered') : $t('position.submitResume') }}</v-btn>
-          <span v-if="showSwitchAccount" class="ml-2 cursor-pointer" style="font-size: 14px; color: #666; text-decoration: underline;" @click="handleSwitchAccount">切换账号</span>
-        </div>
-      </div>
-    </v-card>
-
-    <!-- 快速登录/注册 -->
-    <loginPage
-      v-if="sendResume.showLogin"
-      :jobId="jobId"
-      :hasLogout="hasLogout"
-      @loginSuccess="loginSuccess"
-      @close="handleClose('showLogin')"
-    ></loginPage>
-
-    <!-- 快速填写简易人才信息 -->
-    <simplePage
-      v-if="sendResume.showSimpleInfo"
-      @simpleInfoReady="simpleInfoReadyFun"
-      @close="handleClose('showSimpleInfo')"
-    ></simplePage>
-
-    <!-- 选择简历 -->
-    <selectPage
-      v-if="sendResume.showSelect"
-      :hire="info?.hire"
-      :jobId="jobId"
-      :userId="sharedById"
-      @refresh="handleCheckJobDelivery"
-      @close="handleClose('showSelect')"
-      ref="selectRef"
-    ></selectPage>
-  </div>
-</template>
-
-<script setup>
-import { commissionCalculation } from '@/utils/position'
-defineOptions({name: 'recruit-personal-shareJob-index'})
-import loginPage from '@/views/common/loginDialog.vue'
-import simplePage from './sendResume/simple.vue'
-import selectPage from './sendResume/select.vue'
-import { onMounted, reactive, ref } from 'vue';
-import { getPositionDetails, jobCvRelCheckSend, getPersonJobUnfavorite, getPersonJobFavorite, getJobFavoriteCheck } from '@/api/position'
-import { dealDictObjData } from '@/utils/position'
-import { getToken } from '@/utils/auth'
-import Snackbar from '@/plugins/snackbar'
-import { useI18n } from '@/hooks/web/useI18n'; const { t } = useI18n()
-
-// 组件挂载后添加事件监听器  
-const isMobile = ref(false)
-onMounted(() => {
-  const userAgent = navigator.userAgent
-  isMobile.value = /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i.test(userAgent)
-})
-// 获取路由参数
-const queryParams = new URLSearchParams(window.location.search)
-const jobId = queryParams.get('jobId') || ''
-const sharedById = queryParams.get('sharedById') || ''
-
-// 职位详情
-const info = ref({})
-const positionInfo = ref({})
-const getPositionDetail = async () => {
-  const data = await getPositionDetails({ id: jobId })
-  info.value = data
-  positionInfo.value = { ...dealDictObjData({}, info.value), ...info.value }
-}
-
-// 效验是否已投递
-const delivery = ref(false)
-const handleCheckJobDelivery = async () => {
-  delivery.value = await jobCvRelCheckSend({ jobId })
-  return delivery.value
-}
-
-// 效验求职者是否有收藏该职位
-const actions = ref(false)
-const isCollection = ref(false)
-const getCollectionStatus = async () => {
-  if (!jobId) return
-  const data = await getJobFavoriteCheck({ jobId })
-  isCollection.value = data
-}
-
-// 收藏&取消收藏职位
-const handleCollection = async () => {
-  // 效验登录状态
-  if (!getToken()) {
-    actions.value = true
-    sendResume.showLogin = true
-    return
-  }
-  if (!jobId) return
-  const api = isCollection.value ? getPersonJobUnfavorite : getPersonJobFavorite
-  await api(isCollection.value ? jobId : { jobId })
-  await getCollectionStatus()
-}
-
-// 判断有没有jobId跟sharedById,没有的话关闭当前窗口
-if (!jobId) { // !sharedById
-  Snackbar.warning('当前打开的链接无效')
-  setTimeout(() => {
-    window.close()
-  }, 2000)
-} else {
-  getPositionDetail()
-  if (getToken()) {
-    handleCheckJobDelivery()
-    getCollectionStatus()
-  }
-}
-
-const desc = [
-  { mdi: 'mdi-map-marker-outline', value: 'areaName' },
-  { mdi: 'mdi-school-outline', value: 'eduName' },
-  { mdi: 'mdi-clock-time-ten-outline', value: 'expName' }
-]
-
-// 快速登录成功
-const loginSuccess = async () => {
-  simpleInfoReady.value = false
-  sendResume.showLogin = false // 关闭快速登录弹窗
-  showSwitchAccountFun(true)
-  // actions为true则是收藏时的登录,不需要弹窗选择简历
-  if (actions.value) {
-    actions.value = false
-    Snackbar.success('登录成功')
-    await handleCollection()
-    return 
-  }
-  const bool = await handleCheckJobDelivery()
-  if (bool) return Snackbar.warning(t('resume.alreadyResume'))
-  sendResumeProcessVerify()
-}
-
-// 简易人才信息
-const simpleInfoReady = ref(false)
-const simpleInfoReadyFun = () => {
-  sendResume.showSimpleInfo = false
-  simpleInfoReady.value = true
-  sendResumeProcessVerify()
-}
-const sendResume = reactive({
-  showLogin: false,
-  showSimpleInfo: false,
-  showSelect: false,
-})
-
-// 流程校准
-const sendResumeProcessVerify = async () => {
-  try { // 1.登录 2.具备人才信息 3.有无附件简历,无则上传
-    // 未登录
-    if (!getToken()) {
-      sendResume.showLogin = true
-      return
-    }
-    // 已登录
-    // * 必填人才信息不完全 -> 不符合快速投递,进入完善人才信息流程
-    if (!simpleInfoReady.value) {
-      sendResume.showSimpleInfo = true
-      return
-    }
-    // * 简历列表
-    sendResume.showSelect = hasLogout.value ? false : true
-    hasLogout.value = false
-  } catch (error) {
-    console.error('error', error)
-  }
-}
-
-const handleClose = (key) => {
-  sendResume[key] = false // 弹窗关闭,重置绑定数据
-  hasLogout.value = false
-}
-
-const showSwitchAccount = ref(false)
-const showSwitchAccountFun = (bool) => {
-  showSwitchAccount.value = bool
-}
-
-onMounted(() => {
-  showSwitchAccountFun(getToken()) // (isMobile.value && getToken())
-})
-
-// 切换账号
-const hasLogout = ref(false)
-const handleSwitchAccount = async () => {
-  hasLogout.value = true
-  sendResume.showLogin = true
-}
-</script>
-
-<style lang="scss" scoped>
-.f-w-600 { font-weight: 600; }
-.radius { border-radius: 8px; }
-.salary {
-  color: var(--v-error-base);
-  line-height: 41px;
-  font-weight: 600;
-  height: auto;
-  display: inline-block;
-  vertical-align: sub;
-}
-.JobName {
-  color: #37576c;
-  font-size: 28px;
-  margin-right: 30px;
-  margin-top: 1px;
-  // max-width: 45%;
-  vertical-align: middle;
-  flex: 1;
-}
-.banner-tags { display: flex; flex-wrap: wrap; }
-.requirement {
-  white-space: pre-wrap;
-  word-break: break-all;
-  line-height: 28px;
-  color: var(--color-333);
-  font-size: 15px;
-  text-align: justify;
-  letter-spacing: 0;
-}
-.contact {
-  height: 60px;
-  line-height: 60px;
-  padding: 0 10px;
-}
-.contact-name {
-  font-size: 20px;
-  font-weight: 500;
-  color: var(--color-222);
-  line-height: 28px;
-}
-.contact-info {
-  font-size: 15px;
-  color: var(--color-666);
-  line-height: 21px;
-  margin-top: 8px;
-}
-.button-item {
-  min-width: 110px;
-  height: 36px
-}
-</style>

+ 0 - 113
src/views/recruit/personal/shareJob/sendResume/select.vue

@@ -1,113 +0,0 @@
-<!-- 选择简历 -->
-<template>
-  <!-- 已上传的简历列表 -->
-  <selectResumeDialog
-    v-model="showResume"
-    :list="resumeList"
-    :selectLocalFile="true"
-    @handleLocalFileClick="selectLocalFileClick"
-    @submit="handleSubmit"
-    @close="handleClose"
-  ></selectResumeDialog>
-  <!-- 选择本地简历 -->
-  <CtDialog
-    :visible="openUploadDialog"
-    :widthType="2"
-    titleClass="text-h6"
-    submitText="立即投递"
-    @close="openUploadDialog = false"
-    title="附件简历上传"
-    @submit="uploadFileSubmit"
-  >
-  <uploadForm ref="uploadFormRef"></uploadForm>
-  <div class="color-warning" style="font-size: 13px;">提示:立即投递会将已上传的简历进行投递</div>
-  </CtDialog>
-</template>
-
-<script setup>
-// 上传附件
-import uploadForm from '../form/upload.vue'
-import { savePersonResumeCv } from '@/api/recruit/personal/resume'
-// 选择简历
-import selectResumeDialog from '@/views/recruit/personal/position/components/jobDetails/selectResumeDialog'
-import { hireJobCvDelivery } from '@/api/recruit/personal/shareJob'
-import { jobCvRelSend } from '@/api/position'
-import { getPersonResumeCv } from '@/api/recruit/personal/resume'
-import { useI18n } from '@/hooks/web/useI18n'; const { t } = useI18n()
-import Snackbar from '@/plugins/snackbar'
-import { ref } from 'vue'
-defineOptions({name: 'shareJob-sendResume-select'})
-const emit = defineEmits(['refresh', 'close'])
-const props = defineProps({
-  jobId: {
-    type: String,
-    default: ''
-  },
-  hire: {
-    type: Boolean,
-    default: false
-  },
-  userId: {
-    type: String,
-    default: ''
-  },
-})
-
-const resumeList = ref([])
-const getResumeList = async () => {
-  resumeList.value = []
-  const data = await getPersonResumeCv()
-  if (!data?.length) {
-    // 没有上传过简历
-    openUploadDialog.value = true
-    return
-  }
-  // 可以选择之前上传的简历
-  resumeList.value = data
-  showResume.value = true
-}
-getResumeList()
-
-const uploadFormRef = ref()
-const openUploadDialog = ref(false)
-// 上传附件-提交
-const uploadFileSubmit = async () => {
-  const obj = await uploadFormRef.value.getQuery()
-  if (!obj?.url || !obj?.title) return Snackbar.warning(t('resume.selectResumeToSubmit'))
-  const query = { title: obj.title, url: obj.url }
-  await savePersonResumeCv(query)
-  handleSubmit(query, '上传的文件提交_直接投递')
-}
-
-const selectLocalFileClick = () => {
-  openUploadDialog.value = true
-}
-
-const showResume = ref(false)
-const selectResume = ref()
-const handleSubmit = async (val, type = '') =>{
-  selectResume.value = val
-  if (!selectResume.value) return Snackbar.warning(t('resume.selectResumeToSubmit'))
-  const obj = type ? val : resumeList.value.find(e => e.id === selectResume.value)
-  if (!obj && !type) return Snackbar.warning(t('resume.selectedResumeNotExist'))
-
-  // 区分普通职位跟众聘职位投递
-  if (props.hire) await hireJobCvDelivery({ jobId: props.jobId, recommendUserId: props.userId, url: obj.url })
-  else await jobCvRelSend({ jobId: props.jobId, title: obj.title, url: obj.url, type: props.hire ? 1 : 0  })
-  Snackbar.success(t('resume.deliverySuccess'))
-  // setTimeout(() => {
-  // }, 3000)
-  emit('refresh')
-  handleClose()
-}
-
-const handleClose = () => {
-  showResume.value = false
-  openUploadDialog.value = false
-  selectResume.value = null
-  emit('close')
-}
-
-</script>
-<style lang="scss" scoped>
-</style>

+ 0 - 84
src/views/recruit/personal/shareJob/sendResume/simple.vue

@@ -1,84 +0,0 @@
-<!-- 快速填写-简易人才信息 -->
-<template>
-  <CtDialog
-    :visible="openDialog"
-    :widthType="2"
-    :closeText="closeText"
-    titleClass="text-h6"
-    title="补充基本信息"
-    :closeable="props.closeable"
-    @close="handleClose"
-    @submit="simpleInfoSubmit"
-  >
-    <simpleInfoForm ref="formRef"></simpleInfoForm>
-  </CtDialog>
-</template>
-
-<script setup>
-import { getToken } from '@/utils/auth'
-import simpleInfoForm from '../form/simpleInfo.vue'
-import { savePersonSimpleInfo } from '@/api/recruit/personal/shareJob'
-import { useI18n } from '@/hooks/web/useI18n'; const { t } = useI18n()
-import Snackbar from '@/plugins/snackbar'
-import { ref } from 'vue'
-defineOptions({name: 'shareJob-sendResume-simple'})
-
-const emit = defineEmits(['simpleInfoReady', 'close'])
-const props = defineProps({
-  closeable: {
-    type: Boolean,
-    default: true
-  },
-  closeText: {
-    type: String,
-    default: '取消'
-  }
-})
-
-const openDialog = ref(false) // 默认不打开弹窗,先检验simpleInfoReady
-
-const handleClose = () => {
-  emit('close')
-  openDialog.value = props.closeable ? false : true
-}
-
-let verifyCount = 0
-const getUserInfoVerify = () => {
-  // console.log('获取人才信息->')
-  const bInfo = JSON.parse(localStorage.getItem('baseInfo'))
-  if (bInfo && Object.keys(bInfo).length) { // 校验必填人才信息
-    // console.log('获取人才信息成功->')
-    const keyArr = ['name', 'phone', 'jobStatus', 'expType', 'eduType'] // 必填人才信息
-    const simpleInfoReady = keyArr.every(e => bInfo[e] && bInfo[e] !== 0) // 校验必填人才信息
-    if (simpleInfoReady) {
-      emit('simpleInfoReady') // 存在
-    } else {
-      openDialog.value = true // 不存在
-      Snackbar.warning('请先完善个人基本信息')
-    }
-  } else {
-    if (verifyCount > 2) Snackbar.error(t('login.getUserInfoFailed')+','+t('login.loginAgain'))  // 获取人才信息失败 
-    else {
-      verifyCount++
-      setTimeout(() => { getUserInfoVerify() }, 4000) // 获取人才信息
-    }
-  }
-}
-if (getToken()) setTimeout(() => { getUserInfoVerify() }, 2000) // 获取人才信息
-
-const formRef = ref()
-const simpleInfoSubmit = async () => {
-  try {
-    const obj = await formRef.value.getQuery()
-    if (!obj) return
-    await savePersonSimpleInfo(obj)
-    localStorage.setItem('baseInfo', JSON.stringify(obj))
-    openDialog.value = false
-    emit('simpleInfoReady')
-  } catch (error) {
-    console.error('error', error)
-  }
-}
-</script>
-<style lang="scss" scoped>
-</style>

+ 129 - 0
src/views/recruit/teacher/internshipCompany/index.vue

@@ -0,0 +1,129 @@
+<template>
+  <v-card class="card-box pa-3">
+    <CtTable
+      :items="tableData"
+      :headers="headers"
+      :loading="loading"
+      :elevation="0"
+      :is-tools="false"
+      :showPage="true"
+      :total="total"
+      :page-info="query"
+      itemKey="id"
+      @pageHandleChange="handleChangePage"
+    >
+      <template #enterpriseName="{ item }">
+        <div class="d-flex align-center">
+          <v-avatar size="40" :image="item.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'"></v-avatar>
+          <span class="ml-3 color-primary cursor-pointer" @click="handleDetail(item.id)">{{ formatName(item.anotherName || item.name) }}</span>
+        </div>
+      </template>
+      <template #studentCount="{ item }">
+        <span class="color-primary cursor-pointer" @click="handleStudent(item.id)">{{ item.studentCount || 0 }}人</span>
+      </template>
+    </CtTable>
+  </v-card>
+
+  <CtDialog :visible="drill.show" :widthType="1" titleClass="text-h6" :footer="false" :title="title" @close="handleClose">
+    <CtTable
+      :items="drill.list"
+      :headers="drill.headers"
+      :loading="false"
+      :elevation="0"
+      :isTools="false"
+      :showPage="true"
+      :total="drill.total"
+      :page-info="drill.query"
+      itemKey="id"
+      @pageHandleChange="handleChangeDrillPage"
+    >
+    </CtTable>
+  </CtDialog>
+</template>
+
+<script setup>
+defineOptions({name: 'internship-company'})
+import { ref, onMounted } from 'vue'
+import { internshipCompanyList } from '@/api/school'
+import { formatName } from '@/utils/getText'
+
+const total = ref(0)
+const loading = ref(false)
+const query = ref({
+  size: 10,
+  current: 10,
+  schoolId: JSON.parse(localStorage.getItem('schoolInfo'))?.schoolId
+})
+const tableData = ref([
+  {
+    "id": 1,
+    "name": "门墩儿信息科技有限公司",
+    "anotherName": "门墩儿",
+    "industryId": "1829087620475494402",
+    "industryName": '互联网',
+    "scale": "0",
+    "scaleName": '0-20人',
+    "logoUrl": "https://minio.menduner.com/dev/1e6893918ef378ca280360078dfe74ade10b27101c89865261824b46de7d34a6.png",
+    studentCount: 2
+  }
+])
+const headers = [
+  { title: '企业名称', key: 'enterpriseName', sortable: false },
+  { title: '在岗实习学生', key: 'studentCount', sortable: false },
+  { title: '所在行业', key: 'industryName', sortable: false },
+  { title: '企业规模', key: 'scaleName', sortable: false }
+]
+
+const getList = async () => {
+  try {
+    const data = await internshipCompanyList(query.value)
+    console.log(data, 'data')
+  } catch {}
+}
+
+const handleChangePage = (page) => {
+  query.value.current = page
+  getList()
+}
+
+// 跳转企业详情
+const handleDetail = (id) => {
+  if (!id) return
+  window.open(`/recruit/personal/company/details/${id}?key=briefIntroduction`)
+}
+
+const drill = ref({
+  total: 0,
+  query: {
+    size: 10,
+    current: 1
+  },
+  show: false,
+  list: [],
+  headers: [
+    { title: '学生姓名', key: 'studentName', sortable: false },
+  ]
+})
+
+// 在岗实习学生列表
+const handleStudent = (id) => {
+  drill.value.query.current = 1
+  drill.value.show = true
+
+}
+
+const handleChangeDrillPage = (page) => {
+  drill.value.query.current = page
+}
+const handleClose = () => {
+  drill.value.show = false
+  drill.value.list = []
+}
+
+onMounted(async () => {
+  // await getList()
+})
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 115 - 0
src/views/recruit/teacher/internshipReport/index copy.vue

@@ -0,0 +1,115 @@
+<!-- 实习报告 -->
+<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> -->
+        <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'
+import { getUserAvatar } from '@/utils/avatar'
+
+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>

+ 236 - 0
src/views/recruit/teacher/internshipReport/index.vue

@@ -0,0 +1,236 @@
+<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>
+    <div style="width: 80%" class="ml-3">
+      <div class="d-flex justify-space-between px-3">
+        <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>
+      </div>
+      <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.headImg, item.teacherSex)"></v-avatar>
+            <span class="ml-3">{{ item?.studentName }}</span>
+          </div>
+        </template>
+        <template #actions="{ item }">
+          <v-btn color="primary" variant="text" @click="handleReport(item)">实习报告</v-btn>
+        </template>
+      </CtTable>
+    </div>
+
+    <v-navigation-drawer v-model="showDetail" absolute location="right" rounded temporary width="700" class="pa-5">
+      <p class="text-center color-primary font-weight-bold mb-5 font-size-20">{{ detailTitle }}</p>
+      <div v-if="reportList && reportList.length > 0">
+        <div v-for="item in reportList" :key="item.date" class="mb-3">
+          <div class="title-date">{{ 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" />
+	  </v-navigation-drawer>
+  </v-card>
+
+  <PreviewImage v-if="showPreview" :initialIndex="initialIndex" :urlList="urlsList" @close="handleClosePreview" />
+</template>
+
+<script setup>
+defineOptions({ name: 'group-account'})
+import { ref } from 'vue'
+import { useI18n } from '@/hooks/web/useI18n'
+import { getEnterpriseTree } from '@/api/recruit/enterprise/system/group'
+import { getUserAvatar } from '@/utils/avatar'
+import { formatName } from '@/utils/getText';
+
+const { t } = useI18n()
+const total = ref(0)
+const loading = ref(false)
+const query = ref({
+  pageSize: 10,
+  pageNo: 1,
+  enterpriseId: '',
+  name: null
+})
+
+// 企业基本信息
+const schoolInfo = ref(localStorage.getItem('schoolInfo') ? JSON.parse(localStorage.getItem('schoolInfo')) : {})
+
+const tableData = ref([
+  {
+  "id": "1899374563922436098",
+  "userId": "1899373349776244738",
+  "personId": "1899374563842744322",
+  "graduationStatus": null,
+  "status": null,
+  "enterpriseId": null,
+  "studentNo": "200004512",
+  "idCardName": null,
+  studentName: '张三',
+  "idCardNo": "440104199908129861",
+  "idCardImg1": null,
+  "idCardImg2": null,
+  "authStatus": "0",
+  "schoolId": "1899055558628085760",
+  "schoolName": "辞图科技",
+  "schoolClassId": null,
+  "schoolDepartmentName": "外语系",
+  "schoolClassName": "德语一班",
+  "majorId": null,
+  "majorName": "德语",
+  "majorCode": null,
+  teacherSex: '1',
+  headImg: '',
+  enterpriseName: '企业1',
+  "emergencyContactName": "吴轩",
+  "emergencyContactPhone": "13229740017",
+  "createTime": 1741681159370
+}
+])
+const treeData = ref([
+  {
+    title: '农学',
+    children: [{ title: '农学1' }, { title: '农学2' }]
+  },
+  {
+    title: '文学',
+    children: [{ title: '文学1' }, { title: '文学2' }]
+  }
+])
+const headers = [
+  { title: '学生姓名', key: 'studentName', sortable: false },
+  { title: '实习企业', key: 'enterpriseName', sortable: false },
+  { title: '所属院系', key: 'schoolDepartmentName', sortable: false },
+  { title: '所属专业', key: 'majorName', sortable: false },
+  { title: '所在班级', key: 'schoolClassName', sortable: false },
+  { title: '学号', key: 'studentNo', sortable: false },
+  { title: t('common.actions'), key: 'actions', sortable: false }
+]
+const textItem = ref({
+  type: 'text',
+  value: null,
+  width: 250,
+  clearable: true,
+  label: '请输入学生名称搜索'
+})
+
+
+// 获取用户列表
+const getUserList = async () => {
+  // loading.value = true
+  // try {
+  //   const { list, total: number } = await getEnterpriseUserList(query.value)
+  //   tableData.value = list
+  //   total.value = number
+  // } finally {
+  //   loading.value = false
+  // }
+}
+
+// 获取树形列表
+const getTreeData = async () => {
+  treeData.value = []
+  try {
+    const data = await getEnterpriseTree()
+    if (!data) return
+    treeData.value[0] = data
+    query.value.enterpriseId = data.id
+    // 获取用户列表
+    getUserList()
+  } catch {}
+}
+// getTreeData()
+
+// 分页
+const handleChangePage = (e) => {
+  query.value.pageNo = e
+  getUserList()
+}
+
+// 树形click
+const handleClick = (e) => {
+  if (!e.length) return
+  query.value.enterpriseId = e[0]
+  getUserList()
+}
+
+// 查看实习报告
+const reportList = ref([
+  {
+    date: '2023-01-01',
+    arr: ['https://menduner.citupro.com:3443/dev/person/1899373349776244738/img/c1e6d4d93ef29bf62e07eec698b8a44a1cf690aa1f5c0d36065f0f2fb1144956.png']
+  }
+])
+const detailTitle = ref('实习报告')
+const showDetail = ref(false)
+const handleReport = (item) => {
+  detailTitle.value = item.studentName + ' - 实习报告'
+  showDetail.value = true
+}
+
+// 图片预览
+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 scoped lang="scss">
+.title-date {
+	border-left: 5px solid var(--v-primary-base);
+	padding-left: 12px;
+	line-height: 17px;
+}
+.treeTitle {
+  width: 100%;
+  max-width: 100%;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  overflow: hidden;
+}
+</style>

+ 241 - 0
src/views/recruit/teacher/internshipSituation/index.vue

@@ -0,0 +1,241 @@
+<!-- 实习情况 -->
+<template>
+	<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.value }}</span>
+						<span class="color-999 font-size-14">人</span>
+					</div>
+				</div>
+			</div>
+		</div>
+
+		<CtTable
+			class="mt-5"
+      :items="tableData"
+      :headers="headers"
+      :loading="loading"
+      :elevation="0"
+      :is-tools="false"
+      :showPage="true"
+      :total="total"
+      :page-info="query"
+      itemKey="id"
+      @pageHandleChange="handleChangePage"
+    >
+			<template #enterpriseName="{ item }">
+				<div class="d-flex align-center">
+					<v-avatar size="40" :image="item.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'"></v-avatar>
+					<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>
+			<template #waitInternshipNumber="{ item }">
+				<span class="color-primary cursor-pointer" @click="handleDetail(item, '等待实习')">{{ item.waitInternshipNumber || 0 }}人</span>
+			</template>
+			<template #internshipSuccessNumber="{ item }">
+				<span class="color-primary cursor-pointer" @click="handleDetail(item, '结束实习')">{{ item.internshipSuccessNumber || 0 }}人</span>
+			</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>
+	</v-card>
+
+	<CtDialog :visible="drill.show" :widthType="1" titleClass="text-h6" :footer="false" :title="drill.title" @close="handleClose">
+    <CtTable
+      :items="drill.list"
+      :headers="drill.headers"
+      :loading="false"
+      :elevation="0"
+      :isTools="false"
+      :showPage="true"
+      :total="drill.total"
+      :page-info="drill.query"
+      itemKey="id"
+      @pageHandleChange="handleChangeDrillPage"
+    >
+			<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>
+				</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 { formatName } from '@/utils/getText'
+import Snackbar from '@/plugins/snackbar'
+import { studentPracticeStatistics } from '@/api/school'
+
+const statistics = ref([
+	{ label: '等待实习', value: 0, key: 'waitInternshipNumber' },
+	{ label: '实习中', value: 1, key: 'internshipNumber' },
+	{ label: '实习结束', value: 0, key: 'internshipSuccessNumber' }
+])
+
+const loading = ref(false)
+const total = ref(0)
+const query = ref({
+	pageNo: 1,
+	pageSize: 10,
+	startTime: null
+})
+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 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 },
+]
+
+// 学生列表
+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 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
+		// })
+	} catch {}
+}
+
+onMounted(async () => {
+	// const { data } = await getDict('student_practice_status')
+	// statistics.value = data
+	// getStatistics()
+	// getList()
+})
+
+const handleChangePage = (val) => {
+	query.value.pageNo = val
+	getList()
+}
+
+// 跳转企业详情
+const handleEnterprise = (id) => {
+  if (!id) return
+  window.open(`/recruit/personal/company/details/${id}?key=briefIntroduction`)
+}
+
+// 实习学生
+const drill = ref({
+  total: 0,
+  query: {
+    size: 10,
+    current: 1
+  },
+	title: '学生列表',
+  show: false,
+  list: [{
+		studentName: '张三',
+		enterpriseName: '北京字节跳动科技有限公司',
+		phone: '12345678901',
+		schoolDepartmentName: '计算机科学与技术',
+		majorName: '计算机科学与技术',
+		schoolClassName: '2019级',
+		studentNo: '2019111111',
+		studentId: 1,
+		teacherSex: '1',
+		headImg: '',
+		phone: '12345678901',
+		studentPracticeStatus: '实习中'
+	}],
+  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 },
+  ]
+})
+// 学生列表
+const handleDetail = (item, label) => {
+	drill.value.title = `${item.anotherName} - 状态[${label}] - 学生列表`
+	drill.value.query.current = 1
+  drill.value.show = true
+}
+const handleChangeDrillPage = (page) => {}
+const handleClose = () => {
+  drill.value.show = false
+  // drill.value.list = []
+}
+
+const showDetail = ref(false)
+const handleToStudentDetail = (id) => {
+	showDetail.value = true
+}
+</script>
+
+<style scoped lang="scss">
+.statistics {
+	width: 70%;
+	&-card {
+		width: 33.33%;
+		margin-right: 12px;
+		background-color: #f7f8fa;
+		border-radius: 10px;
+		&:nth-child(3) {
+			margin-right: 0;
+		}
+		.value {
+			font-size: 44px;
+		}
+	}
+}
+</style>

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

@@ -0,0 +1,87 @@
+<!-- 基本信息 -->
+<template>
+  <div class="d-flex">
+    <!-- 头像 -->
+    <div class="avatarsBox">
+      <v-badge
+        v-if="info?.sex === '1' || info?.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>
+      </v-badge>
+      <v-avatar v-else size=80 :image="getUserAvatar(info?.student?.studentHeadImg, info?.sex)"></v-avatar>
+    </div>
+    <!-- 信息 -->
+    <div style="flex: 1;">
+      <span style="font-size: 20px; font-weight: 600;color: var(--color-666);">{{ info?.student?.studentName }}</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>
+      </div>
+      <view class="mt-2 listBox">{{ info?.student?.schoolClassName }}</view>
+    </div>
+  </div>
+</template>
+
+<script setup>
+defineOptions({name: 'studentList-student-details-baseInfo'})
+import { ref } from 'vue'
+import { getUserAvatar } from '@/utils/avatar'
+
+const props = defineProps({
+  data: Object
+})
+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
+}
+</script>
+
+<style lang="scss" scoped>
+.avatarsBox {
+  height: 80px;
+  width: 80px;
+  position: relative;
+  // margin: 32px;
+  margin-right: 40px;
+  .img {
+    width: 100%;
+    height: 100%;
+  }
+  .mdi {
+    font-size: 42px;
+    color: #fff;
+  }
+  div {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    border-radius: 50%;
+  }
+}
+.listBox {
+  display: flex;
+  flex-wrap: wrap; /* 允许换行 */
+  width: 100%; /* 设置容器宽度 */
+  // height: 68px;
+  overflow: hidden;
+  color: var(--color-777);
+  div {
+    margin-right: 50px;
+    span {
+      height: 32px;
+      line-height: 32px;
+    }
+    .mdi {
+      font-size: 22px;
+      margin-right: 8px;
+    }
+  }
+}
+</style>

+ 114 - 0
src/views/recruit/teacher/studentList/components/other.vue

@@ -0,0 +1,114 @@
+<!-- 基本信息 -->
+<template>
+  <div>
+    <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>
+      </div>
+      <div class="my">
+        <span>所在班级:</span>
+        <span class="ml">{{ info?.schoolClass?.title }}</span>
+      </div>
+      <div class="my">
+        <span>学号:</span>
+        <span class="ml">{{ info?.student?.studentNo }}</span>
+      </div>
+      <div class="my">
+        <span>紧急联系人:</span>
+        <span class="ml">{{ info?.student?.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>
+      </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'
+
+const props = defineProps({
+  data: Object
+})
+const info = ref({})
+if (props.data && Object.keys(props.data).length) {
+  info.value = props.data
+}
+</script>
+
+<style lang="scss" scoped>
+.boxMy{
+  margin-top: 32px;
+  .title-text{
+    font-size: 16px;
+    font-weight: 600;
+    color: var(--color-333);
+  }
+  .my{
+    margin-top: 20px;
+    span {
+      margin-right: 20px;
+      color: var(--color-777);
+    }
+  }
+  .ml{
+    color: var(--color-666);
+  }
+}
+</style>

+ 142 - 0
src/views/recruit/teacher/studentList/index.vue

@@ -0,0 +1,142 @@
+<!-- 学生列表 -->
+<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> -->
+        <Autocomplete class="mr-3" v-model="query.schoolDepartmentName" :item="schoolDepartmentItem"></Autocomplete>
+        <TextInput class="mr-3" v-model="query.studentName" :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>
+    </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 defaultLink" @click="studentDetails(item.id)">
+            <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="previewFile(item.recommendationLetter)">推荐信</v-btn>
+          <v-btn v-if="!item?.evaluate" color="#00897B" variant="text" @click="previewFile(item.evaluate)">实习证书</v-btn>
+        </template>
+      </CtTable>
+      <!-- <Loading :visible="loading"></Loading> -->
+    </div>
+  </v-card >
+</template>
+
+<script setup>
+defineOptions({name: 'studentList-index'})
+import { ref } 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'
+
+const loading = ref(false)
+const query = ref({
+  pageSize: 20,
+  pageNo: 1,
+  schoolDepartmentName: null,
+  studentName: null,
+})
+
+const studentNameItem = ref({
+  type: 'text',
+  width: 300,
+  label: '请输入学生姓名搜索',
+  clearable: true,
+  hideDetails: true
+})
+
+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: '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
+  if (isRefresh) Snackbar.success('刷新成功')
+}
+
+// 分页
+const handleChangePage = (val) => {
+	query.value.pageNo = val
+	getData()
+}
+
+// 查询
+const handleSearch = (refresh = false) => {
+  query.value.pageNo = 1
+  getData(refresh)
+}
+
+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 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()
+
+const studentDetails = (id) => {
+  if (id) window.open(`/recruit/teacher/studentList/detail/${id}`)
+}
+
+// 导出
+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>

+ 114 - 0
src/views/recruit/teacher/studentList/studentDetails.vue

@@ -0,0 +1,114 @@
+<!-- 学生详情 -->
+<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">
+      <!-- 基本信息 -->
+      <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>
+  </div>
+  <Loading :visible="loading"></Loading>
+</template>
+
+<script setup>
+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 Snackbar from '@/plugins/snackbar'
+import { useRoute } from 'vue-router'; const route = useRoute()
+
+const operateItems = [
+  { text: '上传推荐信', key:'letter', icon: 'mdi-circle-medium' },
+  { text: '颁发实习证书', key:'certificate', icon: 'mdi-circle-medium' },
+]
+
+// 获取人才详情
+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 loading = ref(false)
+const { id: studentId } = route.params
+
+const getCvDetail = async () => {
+  if (!studentId) {
+    Snackbar.warning('缺少学生id')
+    setTimeout(() => {
+      window.close()
+    }, 2000)
+    return
+  }
+  loading.value = true
+  const data = await stuDetail({ studentId })
+  info.value = data
+  await getInternshipCertificate()
+  await getRecommendation()
+  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
+}
+
+// 学生推荐信
+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 handleClick = (item) => {
+  console.log('handleClick->item:', item)
+}
+
+</script>
+<style lang="scss" scoped>
+.operate {
+  width: 240px;
+  height: 500px; // 272px
+  position: sticky;
+  top: 60px;
+}
+</style>

+ 24 - 0
src/views/recruit/teacher/teacherCertification/index.vue

@@ -0,0 +1,24 @@
+<template>
+  <v-card class="card-box pa-3">
+    <v-tabs v-model="tab" align-tabs="start" color="primary" bg-color="#f7f8fa" class="mb-10">
+      <v-tab :value="0">老师基本信息</v-tab>
+      <v-tab :value="1">学校基本信息</v-tab>
+    </v-tabs>
+
+    <SchoolInfo v-if="tab === 1" />
+    <TeacherInfo v-else />
+
+  </v-card>
+</template>
+
+<script setup>
+defineOptions({name: 'teacher-certification'})
+import { ref } from 'vue'
+import SchoolInfo from './schoolInfo.vue'
+import TeacherInfo from './teacherInfo.vue'
+
+const tab = ref(0)
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 118 - 0
src/views/recruit/teacher/teacherCertification/schoolInfo.vue

@@ -0,0 +1,118 @@
+<template>
+	<div class="d-flex flex-column align-center">
+		<CtForm ref="CtFormRef" :items="formItems" style="width: 900px;margin: 0 auto">
+      <template #photos="{ item }">
+        <div>
+					<p class="color-primary">*请上传学校环境图片(最多可上传9张图片)</p>
+					<p class="mb-3 color-primary">*只支持JPG、JPEG、PNG类型的图片</p>
+					<Imgs v-model="item.value" :showTips="false" limit="9"></Imgs>
+				</div>
+      </template>
+    </CtForm>
+    <v-btn class="buttons my-10" color="primary" @click.stop="handleSubmit">{{ $t('common.save') }}</v-btn>
+	</div>
+
+  <Loading :visible="overlay"></Loading>
+</template>
+
+<script setup>
+defineOptions({ name: 'schoolInfo' })
+import { ref, onMounted } from 'vue'
+import { useI18n } from '@/hooks/web/useI18n'
+import { useUserStore } from '@/store/user'
+import Snackbar from '@/plugins/snackbar'
+import { updateSchoolInfo } from '@/api/school'
+
+const overlay = ref(false)
+const CtFormRef = ref()
+const formItems = ref({
+  options: [
+    {
+      type: 'text',
+      key: 'name',
+      value: '',
+      col: 6,
+      disabled: true,
+      label: '学校名称 *',
+      flexStyle: 'mr-3',
+      rules: [v => !!v || '请输入您所在的学校名称']
+    },
+    {
+      type: 'text',
+      key: 'email',
+      value: '',
+      col: 6,
+      label: '学校邮箱'
+    },
+    {
+      type: 'text',
+      key: 'address',
+      value: '',
+      label: '学校地址'
+    },
+    {
+      type: 'textarea',
+      key: 'introduce',
+      value: null,
+      counter: 2000,
+      rows: 6,
+      label: '学校简介',
+      outlined: true
+    },
+    {
+      slotName: 'photos',
+      key: 'photos',
+      value: [],
+      defaultValue: [],
+    }
+  ]
+})
+
+const { t } = useI18n()
+const userStore = useUserStore()
+const schoolInfo = ref(localStorage.getItem('schoolInfo') ? JSON.parse(localStorage.getItem('schoolInfo')) : {})
+// 监听store变化
+userStore.$subscribe((mutation, state) => {
+  schoolInfo.value = state.schoolInfo
+})
+
+onMounted(async () => {
+	await userStore.getSchoolInfo()
+
+	formItems.value.options.forEach(item => {
+		item.value = schoolInfo.value?.school[item.key] || item?.defaultValue
+	})
+})
+
+// 保存
+const handleSubmit = async () => {
+  const { valid } = await CtFormRef.value.formRef.validate()
+  if (!valid) return
+
+  overlay.value = true
+
+  let obj = {
+    id: schoolInfo.value?.school.id,
+    schoolId: schoolInfo.value.schoolId
+  }
+	formItems.value.options.forEach(item => {
+		obj[item.key] = item.value
+	})
+
+  try {
+		await updateSchoolInfo(obj)
+
+		setTimeout(async () => {
+			await userStore.getSchoolInfo()
+			Snackbar.success(t('common.saveMsg'))
+			overlay.value = false
+		}, 1000)
+	} catch {
+		overlay.value = false
+	}
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 259 - 0
src/views/recruit/teacher/teacherCertification/teacherInfo.vue

@@ -0,0 +1,259 @@
+<template>
+	<div class="d-flex flex-column align-center">
+		<CtForm ref="CtFormRef" :items="formItems" style="width: 700px;">
+			<template #avatar="{ item }">
+        <div style="color: #7a7a7a;">头像</div>
+        <div class="avatarsBox" @mouseover="showIcon = true" @mouseleave="showIcon = false">
+          <v-avatar class="elevation-5" size=80 :image="getUserAvatar(item.value, schoolInfo?.sex)"></v-avatar>
+          <div v-show="showIcon" @click="openFileInput" v-bind="$attrs" class="mdi mdi-camera-outline">
+            <input
+              type="file"
+              ref="fileInput"
+              accept="image/png, image/jpg, image/jpeg"
+              style="display: none;"
+              @change="handleUploadFile"
+            />
+          </div>
+        </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>
+
+	<Loading :visible="overlay"></Loading>
+  <ImgCropper :visible="isShowCopper" :image="selectPic" :cropBoxResizable="true" @submit="handleHideCopper" :aspectRatio="1 / 1" @close="isShowCopper = false"></ImgCropper>
+</template>
+
+<script setup>
+defineOptions({ name: 'teacherInfo'})
+import { ref, onMounted } from 'vue'
+import { getUserAvatar } from '@/utils/avatar'
+import { getDict } from '@/hooks/web/useDictionaries'
+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'
+
+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: [
+		{
+      slotName: 'avatar',
+      key: 'avatar',
+      value: '',
+      flexStyle: 'align-center'
+    },
+		{
+      type: 'ifRadio',
+      key: 'sex',
+      value: '1',
+			defaultValue: '1',
+      label: '性别 *',
+      width: 50,
+      dictTypeName: 'menduner_sex',
+			rules: [v => !!v || '请选择您的性别'],
+      items: []
+    },
+    {
+      type: 'text',
+      key: 'name',
+      value: '',
+      label: '昵称 *',
+      rules: [v => !!v || '请输入您的昵称']
+    },
+		{
+      type: 'phoneNumber',
+      key: 'phone',
+      value: '',
+      label: '联系电话 *',
+      rules: [v => !!v || '请输入您的联系电话']
+    },
+    {
+      type: 'text',
+      key: 'email',
+      value: '',
+      label: '电子邮箱'
+    },
+    {
+			slotName: 'organizationList',
+      key: 'organizationList',
+			noParam: true,
+      label: '负责院系 *',
+      rules: [v => !!v || '请填写您在学校负责的院系']
+    }
+  ]
+})
+
+const { t } = useI18n()
+const userStore = useUserStore()
+const schoolInfo = ref(localStorage.getItem('schoolInfo') ? JSON.parse(localStorage.getItem('schoolInfo')) : {})
+// 监听store变化
+userStore.$subscribe((mutation, state) => {
+  schoolInfo.value = state.schoolInfo
+})
+
+onMounted(async () => {
+	await userStore.getSchoolInfo()
+
+	// 获取性别字典数据
+	const sexItem = formItems.value.options.find(e => e.key === 'sex')
+	if (!sexItem || !Object.keys(sexItem).length) return
+	const { data } = await getDict(sexItem.dictTypeName)
+	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: '' }]
+		}
+	})
+})
+
+// 添加院系
+const handleAddDepartment = () => {
+	departmentList.value.push({ name: '' })
+}
+// 删除院系
+const handleDeleteDepartment = (index) => {
+	departmentList.value.splice(index, 1)
+}
+
+// 图片裁剪
+const overlay = ref(false)
+const selectPic = ref('')
+const isShowCopper = ref(false)
+
+// 选择文件
+const fileInput = ref()
+const clicked = ref(false)
+const openFileInput = () => {
+  if (clicked.value) return
+  clicked.value = true
+  fileInput.value.click()
+  clicked.value = false
+}
+
+// 上传头像
+const accept = ['jpg', 'png', 'jpeg']
+const handleUploadFile = async (e) => {
+  const file = e.target.files[0]
+  if (!file) return
+
+  const arr = file.name.split('.')
+  const fileType = arr?.length ? arr[arr.length-1] : ''
+  if (!accept.includes(fileType)) return Snackbar.warning('请上传图片格式文件')
+
+  const size = file.size
+  if (size / (1024*1024) > 20) {
+    Snackbar.warning(t('common.fileSizeExceed'))
+    return
+  }
+
+  const reader = new FileReader()
+  reader.readAsDataURL(file)
+  reader.onload = () => {
+    selectPic.value = String(reader.result)
+    isShowCopper.value = true
+  }
+}
+
+// 头像裁剪
+const handleHideCopper = (data) => {
+  isShowCopper.value = false
+  if (data) {
+    const { file } = data
+    if (!file) return
+
+    const formData = new FormData()
+    formData.append('file', file)
+    formData.append('path', 'img')
+    uploadFile(formData).then(async ({ data }) => {
+      if (!data) return
+      formItems.value.options.find(e => e.key === 'avatar').value = 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 } }) : []
+  }
+	formItems.value.options.forEach(item => {
+		if (item.noParam) return
+		obj[item.key] = item.value
+	})
+
+	try {
+		await updateTeacherInfo(obj)
+
+		setTimeout(async () => {
+			await userStore.getSchoolInfo()
+			Snackbar.success(t('common.saveMsg'))
+			overlay.value = false
+		}, 1000)
+	} catch {
+		overlay.value = false
+	}
+}
+</script>
+
+<style scoped lang="scss">
+.avatarsBox {
+  height: 80px;
+  width: 80px;
+  position: relative;
+  cursor: pointer;
+  margin: 32px;
+  margin-right: 40px;
+  .img {
+    width: 100%;
+    height: 100%;
+  }
+  .mdi {
+    font-size: 42px;
+    color: #fff;
+  }
+  div {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    border-radius: 50%;
+  }
+}
+</style>

+ 15 - 4
src/views/register/person.vue

@@ -7,7 +7,7 @@
         <phoneFrom ref="phoneRef" openVerify @handleEnter="handleRegister" :phone="phone"></phoneFrom>
       </div>
       <div v-if="!isMobile" class="font-size-14 text-end">
-        <span class="tips color-primary cursor-pointer" @click="router.push('/login')">已有账号?去登录</span>
+        <span class="tips color-primary cursor-pointer" @click="router.push(isSchool ? '/flameLogin' : '/login')">已有账号?去登录</span>
       </div>
       <v-btn :loading="loading" color="primary" class="white--text mt-5" min-width="370" @click="handleRegister">{{ isCompany ? '下一步' : '注册' }}</v-btn>
       <div class="login-tips mt-3" v-if="!isCompany">
@@ -49,6 +49,10 @@ const props = defineProps({
   isLogin: {
     type: Boolean,
     default: false
+  },
+  isSchool: {
+    type: Boolean,
+    default: false
   }
 })
 
@@ -66,9 +70,16 @@ const handleRegister = async () => {
   if (!props.isCompany && !isAgree.value) return Snackbar.warning('请阅读并勾选底部协议')
   loading.value = true
   try {
-    // isLogin:企业注册申请被驳回后,再次提交时需先登录个人账号
-    // props.isLogin ? await userStore.handleSmsLogin({ ...phoneRef.value.loginData, autoRegister: true }) : await userStore.handleUserRegister({ ...phoneRef.value.loginData })
-    await userStore.handleSmsLogin({ ...phoneRef.value.loginData, autoRegister: true, chooseRole: true }) // 自动注册登录
+    await userStore.handleSmsLogin({ 
+      ...phoneRef.value.loginData,
+      autoRegister: true,
+      chooseRole: props.isSchool ? false : true,
+      schoolRegister: props.isSchool
+    })
+    // autoRegister 自动注册登录
+
+    if (props.isSchool) localStorage.setItem('schoolLoginAccount', JSON.stringify({ phone: phoneRef.value.loginData.phone }))
+
     Snackbar.success(props.isCompany ? '手机号验证成功' : '注册成功')
     if (!props.isCompany) {
       localStorage.setItem('simpleCompleteDialogHaveBeenShow', true) // 个人登录简易基本信息填写弹窗open-status

+ 42 - 0
src/views/register/school.vue

@@ -0,0 +1,42 @@
+<template>
+  <div class="box" style="overflow-x: hidden;" :style="{'background-image': 'url(' + webContent.loginBgUrl + ')'}">
+    <PhonePage :isCompany="true" :isSchool="true" @success="handleValidate"></PhonePage>
+  </div>
+</template>
+
+<script setup>
+defineOptions({ name: 'register-school'})
+import { ref, onMounted } from 'vue'
+import PhonePage from './person.vue'
+import { webContentStore } from '@/store/webContent'
+import { useUserStore } from '@/store/user'
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+const webContent = webContentStore()
+const userStore = useUserStore()
+const isMobile = ref(false)
+onMounted(async () => {
+  await webContent.getSystemWebContent()
+  const userAgent = navigator.userAgent
+  isMobile.value = /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i.test(userAgent)
+})
+
+// 手机验证码效验通过
+const handleValidate= async () => {
+	// 验证通过,跳转到学校信息填写页面
+	localStorage.setItem('necessaryInfoReady', 'ready')
+	// await userStore.handleSmsLogin()
+}
+</script>
+
+<style scoped lang="scss">
+.box {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: center center;
+}
+</style>

+ 298 - 0
src/views/register/schoolForm.vue

@@ -0,0 +1,298 @@
+<template>
+  <div class="box" style="overflow-x: hidden;" :style="{'background-image': 'url(' + webContent.loginBgUrl + ')'}">
+		<navBar :showLoginBtn="false" class="navBar"></navBar>
+		<div class="content pa-5">
+			<div class="mb-10 mt-5" style="font-size: 22px; font-weight: bold; text-align: center;">
+				{{ isUpdate ? '注册信息修改' : '老师注册'}}
+			</div>
+			<CtForm class="mt-5" ref="CtFormRef" :items="formItems">
+				<template #authDept>
+					<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>
+				<template #tips>
+					<p class="font-size-14 color-warning mb-3">图片上传提示:支持jpg、jpeg、png格式,图片大小不得超过20M</p>
+				</template>
+				<template #employmentCertificate="{ item }">
+					<div class="d-flex flex-column">
+						<p class="color-999 font-size-14 mb-3">
+							<span class="color-error">*</span>
+							在岗证明图片
+						</p>
+						<Img
+							class="upload-box"
+							tips="上传图片"
+							:value="item.value"
+							:showSnackbar="false"
+							@imgClick="handlePreview"
+							:showCursor="true"
+							@success="url => item.value = url"
+							@delete="item.value = null"
+						/>
+					</div>
+				</template>
+				<template #idCardFront="{ item }">
+					<div class="d-flex flex-column">
+						<p class="color-999 font-size-14 mb-3">
+							<span class="color-error">*</span>
+							身份证正面图片
+						</p>
+						<Img
+							class="upload-box"
+							tips="上传图片"
+							:value="item.value"
+							:showSnackbar="false"
+							@imgClick="handlePreview"
+							:showCursor="true"
+							@success="url => item.value = url"
+							@delete="item.value = null"
+						/>
+					</div>
+				</template>
+				<template #idCardBack="{ item }">
+					<div class="d-flex flex-column">
+						<p class="color-999 font-size-14 mb-3">
+							<span class="color-error">*</span>
+							身份证背面图片
+						</p>
+						<Img
+							class="upload-box"
+							tips="上传图片"
+							:value="item.value"
+							:showSnackbar="false"
+							@imgClick="handlePreview"
+							:showCursor="true"
+							@success="url => item.value = url"
+							@delete="item.value = null"
+						/>
+					</div>
+				</template>
+			</CtForm>
+			<div class="text-center my-10">
+				<v-btn color="primary" width="250" @click.stop="handleSubmit">提 交</v-btn>
+			</div>
+		</div>
+  </div>
+
+	<PreviewImage v-if="showPreview" :initialIndex="0" :urlList="[previewUrl]" @close="showPreview = !showPreview, previewUrl = ''" />
+</template>
+
+<script setup>
+defineOptions({ name: 'register-schoolForm'})
+import { ref, onMounted } from 'vue'
+import { webContentStore } from '@/store/webContent'
+import { getDict } from '@/hooks/web/useDictionaries'
+import Snackbar from '@/plugins/snackbar'
+import navBar from '@/layout/personal/navBar.vue'
+import { schoolRegister } from '@/api/school'
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+const webContent = webContentStore()
+
+const previewUrl = ref('')
+const showPreview = ref(false)
+const departmentList = ref([{ name: '' }])
+const textItem = {
+	type: 'text',
+  key: 'name',
+	width: 450,
+  label: '院系名称 *',
+	hideDetails: true,
+  rules: [v => !!v || '请输入您负责的院系名称']
+}
+
+const isUpdate = ref(false)
+const CtFormRef = ref()
+const formItems = ref({
+  options: [
+    {
+      type: 'text',
+      key: 'name',
+      value: '',
+      label: '昵称 *',
+			col: 6,
+      outlined: true,
+      rules: [v => !!v || '请输入您的昵称']
+    },
+    {
+      type: 'ifRadio',
+      key: 'sex',
+      value: '1',
+			defaultValue: '1',
+      label: '性别 *',
+      col: 6,
+			flexStyle: 'ml-5',
+      width: 50,
+      dictTypeName: 'menduner_sex',
+			rules: [v => !!v || '请选择您的性别'],
+      items: []
+    },
+    {
+      type: 'phoneNumber',
+      key: 'phone',
+      value: localStorage.getItem('schoolLoginAccount') ? JSON.parse(localStorage.getItem('schoolLoginAccount')).phone : '',
+      label: '联系电话 *',
+			col: 6,
+      outlined: true,
+      rules: [v => !!v || '请填写您的联系电话']
+    },
+    {
+      type: 'autocomplete',
+      key: 'schoolId',
+      value: null,
+      label: '所在学校 *',
+			col: 6,
+			itemText: 'name',
+			itemValue: 'id',
+			flexStyle: 'ml-5',
+      outlined: true,
+			items: [],
+      rules: [v => !!v || '请选择您所在的学校名称']
+    },
+    {
+			slotName: 'authDept',
+      key: 'authDept',
+			noParam: true,
+      label: '负责院系 *',
+      rules: [v => !!v || '请填写您在学校负责的院系']
+    },
+		{
+			slotName: 'tips',
+			noParam: true
+		},
+		{
+			slotName: 'employmentCertificate',
+			key: 'employmentCertificate',
+			value: '',
+			col: 4,
+			rules: [v => !!v || '请上传您的在岗证明图片']
+		},
+		{
+			slotName: 'idCardFront',
+			key: 'idCardFront',
+			value: '',
+			col: 4,
+			rules: [v => !!v || '请上传您的身份证正面图片']
+		},
+		{
+			slotName: 'idCardBack',
+			key: 'idCardBack',
+			value: '',
+			col: 4,
+			rules: [v => !!v || '请上传您的身份证背面图片']
+		}
+  ]
+})
+
+const formData = ref({})
+onMounted(async () => {
+  await webContent.getSystemWebContent()
+
+	// 获取性别字典数据
+	const sexItem = formItems.value.options.find(e => e.key === 'sex')
+	if (!sexItem || !Object.keys(sexItem).length) return
+	const { data } = await getDict(sexItem.dictTypeName)
+	sexItem.items = data || []
+
+	// 获取学校列表
+	const schoolItem = formItems.value.options.find(e => e.key === 'schoolId')
+	if (!schoolItem || !Object.keys(schoolItem).length) return
+	getDict('schoolList', {}, 'schoolList').then(({ data }) => {
+		schoolItem.items = data || []
+	})
+
+	// 重新提交,数据回显
+	formData.value = localStorage.getItem('registerSchoolInfo') ? JSON.parse(localStorage.getItem('registerSchoolInfo')) : {}
+	if (formData.value && formData.value && formData.value?.authStatus === '2') {
+		isUpdate.value = true
+		departmentList.value = formData.value?.authDept || [{ name: '' }]
+		formItems.value.options.forEach(item => {
+			if (item.key !== 'authDept' && !item.noParam) item.value = formData.value[item.key] || item?.defaultValue
+		})
+	}
+})
+
+// 图片预览
+const handlePreview = (url) => {
+	previewUrl.value = url
+	showPreview.value = true
+}
+
+// 添加院系
+const handleAddDepartment = () => {
+	departmentList.value.push({ name: '' })
+}
+// 删除院系
+const handleDeleteDepartment = (index) => {
+	departmentList.value.splice(index, 1)
+}
+
+// 提交注册
+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('请将院系信息填写完整')
+
+	let obj = {
+		authDept: departmentList.value
+	}
+	formItems.value.options.forEach(item => {
+		if (item.noParam) return
+		obj[item.key] = item.value
+	})
+
+	// 修改信息需提交原始数据
+	if (isUpdate.value) obj = Object.assign(formData.value, obj)
+
+	try {
+		await schoolRegister(obj)
+		console.log(obj, 'submit-data提交成功,等待系统管理员审核')
+
+		Snackbar.success('提交成功,等待系统管理员审核!')
+		// 重新提交审核的需将authStatus改为0(待审核状态)
+		localStorage.setItem('registerSchoolInfo', JSON.stringify(isUpdate.value ? { ...obj, authStatus: '0' } : obj))
+		isUpdate.value = false
+		router.push('/register/school/inReview')
+	} catch {}
+}
+</script>
+
+<style scoped lang="scss">
+.navBar {
+  position: absolute;
+  top: 0;
+}
+.box {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: center center;
+}
+.content {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  translate: -50% -50%;
+  width: 600px;
+  height: 80%;
+  overflow: auto;
+  background-color: #fff;
+  border-radius: 10px;
+}
+.upload-box {
+	width: 100px;
+}
+</style>

部分文件因文件數量過多而無法顯示