浏览代码

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

zhengnaiwen_citu 9 月之前
父节点
当前提交
8686892ab2
共有 46 个文件被更改,包括 864 次插入415 次删除
  1. 16 0
      src/api/recruit/enterprise/information/index.js
  2. 33 0
      src/api/recruit/enterprise/statistics/index.js
  3. 1 0
      src/components/DatePicker/index.vue
  4. 6 5
      src/components/Echarts/index.vue
  5. 1 1
      src/components/Enterprise/details.vue
  6. 5 5
      src/components/Enterprise/hotPromoted.vue
  7. 7 1
      src/components/PositionLongStrip/item.vue
  8. 4 0
      src/components/Upload/img.vue
  9. 1 1
      src/components/VerificationCode/index.vue
  10. 15 8
      src/config/axios/service.js
  11. 2 2
      src/layout/company/navBar.vue
  12. 6 3
      src/layout/enterprise.vue
  13. 1 1
      src/layout/personal/navBar.vue
  14. 41 0
      src/plugins/curtain/components/point.vue
  15. 31 0
      src/plugins/curtain/index.js
  16. 3 0
      src/plugins/snackbar/index.js
  17. 18 0
      src/plugins/snackbar/message.vue
  18. 25 0
      src/utils/date.js
  19. 1 0
      src/utils/position.js
  20. 22 15
      src/utils/prefixUrl.js
  21. 51 0
      src/utils/validate.js
  22. 19 1
      src/views/recruit/enterprise/elite/components/invite.vue
  23. 113 0
      src/views/recruit/enterprise/elite/components/public.vue
  24. 30 25
      src/views/recruit/enterprise/elite/components/screen.vue
  25. 26 7
      src/views/recruit/enterprise/elite/components/table.vue
  26. 2 2
      src/views/recruit/enterprise/elite/index.vue
  27. 6 3
      src/views/recruit/enterprise/informationManagement/informationSettings.vue
  28. 83 16
      src/views/recruit/enterprise/informationManagement/informationSettingsComponents/authentication.vue
  29. 24 3
      src/views/recruit/enterprise/informationManagement/informationSettingsComponents/basicInfo.vue
  30. 5 1
      src/views/recruit/enterprise/informationManagement/informationSettingsComponents/enterpriseLogo.vue
  31. 4 4
      src/views/recruit/enterprise/positionManagement/components/add.vue
  32. 7 3
      src/views/recruit/enterprise/positionManagement/index.vue
  33. 2 1
      src/views/recruit/enterprise/register/register.vue
  34. 0 171
      src/views/recruit/enterprise/statistics/components/data.js
  35. 8 19
      src/views/recruit/enterprise/statistics/components/overview.vue
  36. 188 9
      src/views/recruit/enterprise/statistics/components/resume.vue
  37. 0 11
      src/views/recruit/enterprise/statistics/index.vue
  38. 36 7
      src/views/recruit/enterprise/statistics/overallAnalysis.vue
  39. 5 3
      src/views/recruit/personal/PersonalCenter/components/interview/item.vue
  40. 4 7
      src/views/recruit/personal/PersonalCenter/dynamic/right.vue
  41. 8 50
      src/views/recruit/personal/home/components/popularEnterprises.vue
  42. 0 1
      src/views/recruit/personal/home/index.vue
  43. 1 27
      src/views/recruit/personal/position/components/details.vue
  44. 0 1
      src/views/recruit/personal/position/index.vue
  45. 2 1
      src/views/recruit/personal/remuse/components/basicInfo.vue
  46. 1 0
      src/views/recruit/personal/remuse/components/educationExp.vue

+ 16 - 0
src/api/recruit/enterprise/information/index.js

@@ -0,0 +1,16 @@
+import request from '@/config/axios'
+
+// 获取企业实名认证信息
+export const getEnterpriseAuth = async () => {
+  return await request.get({
+    url: '/app-admin-api/menduner/system/enterprise/get/auth'
+  })
+}
+
+// 实名认证
+export const saveEnterpriseAuth = async (data) => {
+  return await request.post({
+    url: '/app-admin-api/menduner/system/enterprise/auth/save',
+    data
+  })
+}

+ 33 - 0
src/api/recruit/enterprise/statistics/index.js

@@ -0,0 +1,33 @@
+import request from '@/config/axios'
+
+// 获取投递简历的年龄分布
+export const getJobCvAgeCount = async (params) => {
+  return await request.get({
+    url: '/app-admin-api/menduner/system/analysis/get/job/cv/age/count',
+    params
+  })
+}
+
+// 获取投递简历的学历分布
+export const getJobCvEduCount = async (params) => {
+  return await request.get({
+    url: '/app-admin-api/menduner/system/analysis/get/job/cv/edu/count',
+    params
+  })
+}
+
+// 获取投递简历的工作经验分布
+export const getJobCvExpCount = async (params) => {
+  return await request.get({
+    url: '/app-admin-api/menduner/system/analysis/get/job/cv/exp/count',
+    params
+  })
+}
+
+// 获取投递简历的性别分布
+export const getJobCvSexCount = async (params) => {
+  return await request.get({
+    url: '/app-admin-api/menduner/system/analysis/get/job/cv/sex/count',
+    params
+  })
+}

+ 1 - 0
src/components/DatePicker/index.vue

@@ -17,6 +17,7 @@
       :clearable="options.clearable ?? true"
       :day-names="['一', '二', '三', '四', '五', '六', '七']"
       v-bind="$attrs"
+      @update:model-value="options?.change"
     ></VueDatePicker>
   </div>
 </template>

+ 6 - 5
src/components/Echarts/index.vue

@@ -16,7 +16,6 @@ const props = defineProps({
     type: [Number, String],
     default: 485
   },
-  // 附加数据
   option: {
     type: [Object, Array],
     default: () => {}
@@ -32,13 +31,15 @@ const initMap = () => {
   chart.value.on('click', (param) => {
     emit('click', param)
   })
-  window.addEventListener('resize',()=>{
-    chart.value.resize()
-  })
+  // window.addEventListener('resize',()=>{
+  //   chart.value.resize()
+  // })
 }
 
 onMounted(() => {
-  initMap()
+  nextTick(()=>{
+    initMap()
+  })
 })
 
 watch(

+ 1 - 1
src/components/Enterprise/details.vue

@@ -44,7 +44,7 @@
                 <v-icon size="17" color="#ccc" class="mr-2">mdi-clock</v-icon>{{ info.enterprise.workTime }}
               </div>
               <div class="welfare-tags">
-                <v-chip size="small" label v-for="(k, i) in info.enterprise.welfareList.slice(0, 6)" :key="i" class="mb-2 welfare-tags-item ellipsis" color="primary">{{ k }}</v-chip>
+                <v-chip size="small" label v-for="(k, i) in info?.enterprise?.welfareList?.slice(0, 6)" :key="i" class="mb-2 welfare-tags-item ellipsis" color="primary">{{ k }}</v-chip>
               </div>
             </div>
             <div class="welfare my-3">

+ 5 - 5
src/components/Enterprise/hotPromoted.vue

@@ -10,11 +10,11 @@
           <div class="company-info">
             <h3>{{ item.enterprise.anotherName }}</h3>
             <p>
-              {{ item.financingName }}
-              <span class="septal-line" v-if="item.financingName && item.scaleName"></span>
-              {{ item.scaleName }}
-              <span class="septal-line" v-if="item.industryName"></span>
-              {{ item.industryName }}
+              {{ item?.enterprise.financingName }}
+              <span class="septal-line" v-if="item.enterprise.financingName && item.enterprise.scaleName"></span>
+              {{ item?.enterprise.scaleName }}
+              <span class="septal-line" v-if="item.enterprise.industryName"></span>
+              {{ item?.enterprise.industryName }}
             </p>
           </div>
         </div>

+ 7 - 1
src/components/PositionLongStrip/item.vue

@@ -33,7 +33,13 @@
               <h3 v-bind="props" :class="{'default-active': isHovering }" class="title1">{{ item.enterprise.anotherName }}</h3>
             </template>
           </v-hover>
-          <p class="mt-2">{{ item.enterprise.financingName }}<span class="mx-2">|</span>{{ item.enterprise.scaleName }}<span class="mx-2">|</span>{{ item.enterprise.industryName }}</p>
+          <p class="mt-2">
+            {{ item.enterprise.financingName }}
+            <span class="mx-2" v-if="item.enterprise.financingName && item.enterprise.industryName">|</span>
+            {{ item.enterprise.scaleName }}
+            <span class="mx-2" v-if="item.enterprise.scaleName && item.enterprise.industryName">|</span>
+            {{ item.enterprise.industryName }}
+          </p>
         </div>
       </div>
     </div>

+ 4 - 0
src/components/Upload/img.vue

@@ -24,9 +24,13 @@ import { useI18n } from '@/hooks/web/useI18n'
 import Snackbar from '@/plugins/snackbar'
 
 const emit = defineEmits(['success', 'delete'])
+const props = defineProps({
+  value: String
+})
 
 const { t } = useI18n()
 const src = ref('')
+if (props.value) src.value = props.value
 
 // 选择文件
 const fileInput = ref()

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

@@ -116,7 +116,7 @@ const autoTimer = () => {
 autoTimer()
 const loginUserPhone = localStorage.getItem('loginUserPhone') || '13229740091'
 const loginData = reactive({
-  phone: loginUserPhone, // 13229740091 小梅 // 15775026250 瑞森
+  phone: loginUserPhone,
   code: '123456'
 })
 

+ 15 - 8
src/config/axios/service.js

@@ -201,14 +201,21 @@ service.interceptors.response.use(
     }
     
     // 请求成功后触发获取积分
-    if (!requestCompletionTrigger.length) {
-      return data
-    }
-    const _list = requestCompletionTrigger.filter(_e => {
-      return response.config.url.includes(_e.url)
-    })
-    if (_list.length) {
-      getIntegral(_list.pop().url, userStore)
+    // if (!requestCompletionTrigger.length) {
+    //   return data
+    // }
+    // const _list = requestCompletionTrigger.filter(_e => {
+    //   return response.config.url.includes(_e.url)
+    // })
+    // if (_list.length) {
+    //   getIntegral(_list.pop().url, userStore)
+    // }
+
+    const url = getSuffixAfterPrefix(response.config.url)
+    if (!url || !requestCompletionTrigger.length) return data
+    const _obj = requestCompletionTrigger.find(e => e.url === url)
+    if (_obj) {
+      getIntegral(_obj.url, userStore)
     }
     return data
   },

+ 2 - 2
src/layout/company/navBar.vue

@@ -14,8 +14,8 @@
         
         <div class="d-flex user-nav align-center">
           <div class="d-flex align-center cursor-pointer">
-            <v-img @click="enterpriseClick(2)" rounded width="40" height="40" :src="baseInfo?.logoUrl || 'https://minio.citupro.com/dev/menduner/7.png'" ></v-img>
-            <span @click="enterpriseClick(1)" class="ml-3">{{ baseInfo?.enterpriseAnotherName || $t('sys.tourist') }}</span>
+            <v-img @click="enterpriseClick(2)" rounded width="40" height="40" :src="baseInfo?.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'" ></v-img>
+            <span @click="enterpriseClick(1)" class="ml-3">{{ baseInfo?.enterpriseAnotherName || baseInfo?.enterpriseName || '--' }}</span>
           </div>
           <div class="line"></div>
           <div class="ml-3 cursor-pointer" @click="handleLogout">我要求职</div>

+ 6 - 3
src/layout/enterprise.vue

@@ -16,10 +16,10 @@
         </div>
         <div class="box pa-3">
           <div v-if="!isInWhiteList(route.path, whiteList)" class="box-content">
-            <router-view></router-view>
+            <router-view :key="key"></router-view>
           </div>
           <div v-else class="full">
-            <router-view></router-view>
+            <router-view :key="key"></router-view>
           </div>
         </div>
       </div>
@@ -34,10 +34,13 @@ import Headers from './company/navBar.vue'
 import Slider from './company/slider.vue'
 import side from './company/side.vue'
 import { useRouter, useRoute } from 'vue-router'
-import { watch, ref } from 'vue'
+import { watch, ref, computed } from 'vue'
 
 const router = useRouter()
 const route = useRoute()
+const key = computed(() => {
+  return route.path + Math.random()
+})
 
 const whiteList = [
   '/recruit/enterprise/talentPool/details',

+ 1 - 1
src/layout/personal/navBar.vue

@@ -185,7 +185,7 @@ const getApplyInfo = async () => {
   const data = await getUserRegisterEnterpriseApply()
   const bool = data && Object.keys(data).length // 已经有数据说明已经申请过了
   const path = bool ? '/recruit/enterprise/register/inReview' : '/recruit/enterprise/register'
-  router.push({ path })
+  router.replace({ path })
 }
 
 const handleToPersonalCenter = () => {

+ 41 - 0
src/plugins/curtain/components/point.vue

@@ -0,0 +1,41 @@
+<!-- 展示积分 -->
+<template>
+  <v-app>
+    <v-dialog
+      v-model="dialog"
+      max-width="400"
+      :persistent="persistent || false"
+    >
+      <div class="white-bgc pa-5" style="border-radius: 2px;">
+        <template v-if="list?.length">
+          <div class="d-flex align-center" v-for="(item, index) in list" :key="'curtainPoint' + index">
+            <span style="color: darkorange; font-size: 44px;" class="mdi mdi-database-check-outline ml-2 mr-2"></span>
+            <div style="color: darkorange;" class="mt-4">{{ item }}</div>
+          </div>
+        </template>
+        <div v-else class="d-flex align-center">
+          <span style="color: darkorange; font-size: 44px;" class="mdi mdi-database-check-outline ml-2 mr-2"></span>
+          <div style="color: darkorange;" class="mt-4">{{message}}</div>
+        </div>
+      </div>
+      <div class="text-center mt-3">
+        <span style="color: white; font-size: 28px;" class="mdi mdi-close-circle-outline cursor-pointer" @click="dialog = false"></span>
+      </div>
+    </v-dialog>
+  </v-app>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+defineOptions({name: 'curtain-point'})
+defineProps({
+  message: String, // 单条数据,也可以用list['']
+  list: Array, // 多条数据一起展示传list
+  persistent: Boolean, // false: 点击遮罩层关闭dialog
+})
+
+const dialog = ref(true)
+
+</script>
+<style lang="scss" scoped>
+</style>

+ 31 - 0
src/plugins/curtain/index.js

@@ -0,0 +1,31 @@
+import { createApp } from 'vue'
+import point from './components/point.vue'
+import vuetify from '@/plugins/vuetify'
+// Curtain('point', { message: '暂未开发,敬请期待!' })
+
+const toastMessage  = (type, options)  => {
+  const componentName = type === 'point' ? point : null
+  const rootNode = document.createElement("div")
+  document.querySelector('.v-application').appendChild(rootNode)
+  const app = createApp(componentName, options)
+  app.use(vuetify)
+  app.mount(rootNode)
+  const { timeout } = options || {}
+  if ((timeout - 0)) {
+    setTimeout(() => {
+        app.unmount()
+        rootNode.remove()
+    }, (timeout-0))
+  }
+}
+
+// toastMessage.point = (options) => {
+//   toastMessage('point', options)
+// }
+
+// 注册插件app.use()会自动执行install函数
+toastMessage.install = (app) => {
+  app.config.globalProperties.Curtain = toastMessage
+}
+
+export default toastMessage

+ 3 - 0
src/plugins/snackbar/index.js

@@ -28,6 +28,9 @@ toastMessage.info = (message, variant, timeout) => {
 toastMessage.warning = (message, variant, timeout) => {
   toastMessage({ message, color: 'warning', variant, timeout })
 }
+toastMessage.point = (message, variant, timeout) => {
+  toastMessage({ message, color: 'point', variant, timeout })
+}
 // 注册插件app.use()会自动执行install函数
 toastMessage.install = (app) => {
   app.config.globalProperties.Snackbar = toastMessage

+ 18 - 0
src/plugins/snackbar/message.vue

@@ -1,6 +1,24 @@
 <template>
   <v-app :full-height="false">
     <v-snackbar
+      v-if="color === 'point'"
+      style="padding-top: 40vh;"
+      timeout="-1"
+      location="top"
+      color="#f9eddce3"
+      :variant="variant || 'flat'"
+      v-model="snackbar"
+    >
+    <div class="d-flex align-end">
+      <span style="color: darkorange; font-size: 48px;" class="mdi mdi-database-check-outline ml-5 mr-2"></span>
+      <div style="color: darkorange;" class="mb-3">{{message}}</div>
+    </div>
+    <template v-slot:actions>
+          <span style="color: darkorange; font-size: 22px;" class="mdi mdi-close-circle-outline mr-5" @click="snackbar = false"></span>
+        </template>
+    </v-snackbar>
+    <v-snackbar
+      v-else
       timeout="-1"
       location="top"
       :variant="variant || 'flat'"

+ 25 - 0
src/utils/date.js

@@ -60,4 +60,29 @@ export const getStartAndEndOfDay = (dateString) => {
   }
 
   return [formatDate(startDate), formatDate(endDate)]
+}
+
+// 传入一组时间戳,返回 [最早时间点,最晚时间点]
+export const  convertTimestampsToDayRange = (timestamps) => {
+  if (timestamps.length < 2) {
+    throw new Error('Timestamps array must contain at least two elements')
+  }
+  function formatDate(date) {
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0')
+    const day = String(date.getDate()).padStart(2, '0')
+    const hours = String(date.getHours()).padStart(2, '0')
+    const minutes = String(date.getMinutes()).padStart(2, '0')
+    const seconds = String(date.getSeconds()).padStart(2, '0')
+    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+  }
+
+  const startDate = new Date(timestamps[0])
+  const endDate = new Date(timestamps[1])
+
+  const startOfDay = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())
+
+  const endOfDay = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate() + 1, 0, 0, 0, -1)
+
+  return [formatDate(startOfDay), formatDate(endOfDay)]
 }

+ 1 - 0
src/utils/position.js

@@ -56,6 +56,7 @@ export const dealDictArrayData = (res, list) => {
 }
 
 export const dealDictObjData = (res, obj) => {
+  res = obj
   Object.keys(obj).map(e => {
     const data = dictList.value.find(k => k.key === e)
     if (!data) return

+ 22 - 15
src/utils/prefixUrl.js

@@ -1,4 +1,6 @@
-import Snackbar from '@/plugins/snackbar'
+// import Snackbar from '@/plugins/snackbar'
+import Curtain from '@/plugins/curtain'
+
 
 const prefixList = ['/app-api', '/admin-api']
 export const getSuffixAfterPrefix = (str) => {
@@ -18,18 +20,23 @@ export const getSuffixAfterPrefix = (str) => {
 
 
 // 展示积分
-export function showNextAction (list, currentIndex = 0) {
-  if (currentIndex < list.length) {
-    const action = list[currentIndex]
-    if (action.match) {
-      Snackbar.success(`恭喜您${action.title}获得${action.point}积分`)
-      setTimeout(() => {
-        showNextAction(list, currentIndex + 1)
-      }, 3000)
-    } else {
-      setTimeout(() => {
-        showNextAction(list, currentIndex + 1)
-      }, 0)
-    }
-  }
+export function showNextAction (list) { // , currentIndex = 0
+  const arr = list.reduce((newArr, action) => {
+    if (action.match) newArr.push(`+${action.point}  恭喜您【${action.title}】获得${action.point}积分`)
+    return newArr
+  }, [])
+  if (arr?.length) Curtain('point', { list: arr })
+  // if (currentIndex < list.length) {
+  //   const action = list[currentIndex]
+  //   if (action.match) {
+  //     Snackbar.point(`+${action.point}  恭喜您${action.title}获得${action.point}积分`)
+  //     setTimeout(() => {
+  //       showNextAction(list, currentIndex + 1)
+  //     }, 3000)
+  //   } else {
+  //     setTimeout(() => {
+  //       showNextAction(list, currentIndex + 1)
+  //     }, 0)
+  //   }
+  // }
 }

+ 51 - 0
src/utils/validate.js

@@ -0,0 +1,51 @@
+// 效验身份证号码
+export const isValidIdCard18 = (idCard) => {
+  // 身份证号码正则表达式(18位)
+  var regex = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(10|11|12))(([012]\d)|3[01])\d{3}(\d|X|x)$/
+
+  // 校验长度,类型
+  if (!regex.test(idCard)) {
+    return false
+  }
+
+  // 提取出生年月日,并验证是否为有效日期
+  var year = idCard.substring(6, 10)
+  var month = idCard.substring(10, 12)
+  var day = idCard.substring(12, 14)
+  var date = new Date(year, month - 1, day)
+
+  if (date.getFullYear() !== parseInt(year, 10) ||
+    date.getMonth() !== parseInt(month, 10) - 1 ||
+    date.getDate() !== parseInt(day, 10)) {
+    return false
+  }
+
+  // 校验码部分
+  var idCardBase = idCard.substring(0, 17)
+  var weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
+  var checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
+  var sum = 0
+  for (var i = 0; i < idCardBase.length; i++) {
+    sum += idCardBase[i] * weights[i]
+  }
+  var index = sum % 11
+  var last = checkCodes[index]
+
+  return last.toUpperCase() === idCard.substring(17).toUpperCase()
+}
+
+// 身份证号码转换*号
+export const  maskNumber = (numberStr) => {
+  if (numberStr.length < 2) {
+    return numberStr
+  }
+  
+  // 提取首尾字符
+  const firstChar = numberStr[0]
+  const lastChar = numberStr[numberStr.length - 1]
+
+  const middleLength = numberStr.length - 2
+  const middleStars = '*'.repeat(middleLength)
+  
+  return firstChar + middleStars + lastChar
+}

+ 19 - 1
src/views/recruit/enterprise/elite/components/invite.vue

@@ -1,5 +1,5 @@
 <template>
-  <CtForm ref="CtFormRef" :items="formItems" style="height: 420px;">
+  <CtForm ref="CtFormRef" :items="formItems">
     <template #time="{ item }">
       <VueDatePicker 
         v-model="item.value"
@@ -25,6 +25,18 @@ const props = defineProps({
 const CtFormRef = ref()
 const formItems = ref({
   options: [
+    {
+      type: 'ifRadio',
+      key: 'type',
+      value: '0',
+      label: '发送方式 *',
+      width: 90,
+      noParam: true,
+      items: [
+        { label: '短信', value: '0' },
+        { label: '邮箱', value: '1' }
+      ]
+    },
     {
       slotName: 'time',
       key: 'time',
@@ -74,6 +86,12 @@ if (Object.keys(props.itemData).length) {
   const obj = formItems.value.options.find(e => e.key === 'position')
   obj.value = `${props.itemData?.job?.name}${props.itemData?.job?.areaName ? '_' + props.itemData?.job?.areaName : ''} ${props.itemData?.job?.payFrom}-${props.itemData?.job?.payTo}/${props.itemData?.job?.payName}`
   formItems.value.options.find(e => e.key === 'address').value = props.itemData.job?.address
+
+  const baseInfo = localStorage.getItem('baseInfo')
+  if (baseInfo) {
+    const { phone } = JSON.parse(baseInfo)
+    formItems.value.options.find(e => e.key === 'invitePhone').value = phone
+  }
 }
 
 const getQuery = () => {

+ 113 - 0
src/views/recruit/enterprise/elite/components/public.vue

@@ -0,0 +1,113 @@
+<template>
+  <CtForm ref="CtFormRef" :items="formItems">
+    <template #time="{ item }">
+      <VueDatePicker 
+        v-model="item.value"
+        placeholder="面试时间 *"
+        class="mb-4"
+        model-type="timestamp"
+        :text-input="{ format: 'MM.dd.yyyy HH:mm' }" />
+    </template>
+  </CtForm>
+</template>
+
+<script setup>
+defineOptions({ name: 'invite-public'})
+import { ref } from 'vue'
+
+const props = defineProps({
+  itemData: {
+    type: Object,
+    default: () => {}
+  }
+})
+
+const CtFormRef = ref()
+const formItems = ref({
+  options: [
+    {
+      type: 'ifRadio',
+      key: 'type',
+      value: '0',
+      label: '发送方式 *',
+      width: 90,
+      items: [
+        { label: '短信', value: '0' },
+        { label: '邮箱', value: '1' }
+      ]
+    },
+    {
+      slotName: 'time',
+      key: 'time',
+      value: null,
+      rules: [v => !!v || '请选择面试时间'],
+    },
+    {
+      type: 'text',
+      key: 'position',
+      value: '',
+      noParam: true,
+      disabled: true,
+      label: '面试岗位'
+    },
+    {
+      type: 'text',
+      key: 'address',
+      value: '',
+      label: '面试地点 *',
+      rules: [v => !!v || '请输入面试地点'],
+    },
+    {
+      type: 'text',
+      key: 'invitePhone',
+      value: null,
+      label: '联系电话 *',
+      outlined: true,
+      rules: [v => !!v || '请填写联系电话']
+    },
+    {
+      type: 'textarea',
+      key: 'remark',
+      value: '',
+      label: '备注事项',
+      counter: 140,
+      rules: [
+        value => {
+          if (value?.length <= 140) return true
+          return '请输入备注事项,最多140字'
+        }
+      ]
+    }
+  ]
+})
+
+if (Object.keys(props.itemData).length) {
+  const obj = formItems.value.options.find(e => e.key === 'position')
+  obj.value = `${props.itemData?.job?.name}${props.itemData?.job?.areaName ? '_' + props.itemData?.job?.areaName : ''} ${props.itemData?.job?.payFrom}-${props.itemData?.job?.payTo}/${props.itemData?.job?.payName}`
+  formItems.value.options.find(e => e.key === 'address').value = props.itemData.job?.address
+}
+
+const getQuery = () => {
+  const obj = {
+    type: 1,
+    jobId: props.itemData.job.id,
+    userId: props.itemData.userId,
+    latitude: props.itemData.job?.latitude,
+    longitude: props.itemData.job?.longitude
+  }
+  formItems.value.options.forEach(item => {
+    if (item.noParam) return
+    obj[item.key] = item.value
+  })
+  return obj
+}
+
+defineExpose({
+  CtFormRef,
+  getQuery
+})
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 30 - 25
src/views/recruit/enterprise/elite/components/screen.vue

@@ -1,27 +1,32 @@
 <template>
-  <div class="d-flex align-center">
-    <CommonStyle v-for="(val, i) in list" :key="i" :btnTitle="val.title">
-      <v-list>
-        <v-list-item
-          v-for="(item, index) in val.items"
-          :key="index"
-          :active="val.selected.includes(item.value)"
-          color="primary"
-          :value="item.value"
-          @click="handleClick(item, val)"
-        >
-          <v-list-item-title>{{ item.label }}</v-list-item-title>
-        </v-list-item>
-      </v-list>
-    </CommonStyle>
-    <div class="mr-5 d-flex align-center" v-if="props.tab === 0">
-      <v-radio-group v-model="selected" inline style="height: 28px;" @update:modelValue="handleChangeSelected">
-        <v-radio v-model="selected" label="新投递" value="0" color="primary" hide-details density="compact" class="mr-3"></v-radio>
-        <v-radio v-model="selected" label="已查看" value="1" color="primary" hide-details density="compact"></v-radio>
-      </v-radio-group>
-      <v-checkbox class="ml-3" v-model="bounty" label="赏金职位" color="primary" hide-details density="compact" @update:model-value="handleChangeBounty"></v-checkbox>
+  <div class="d-flex align-center justify-space-between">
+    <div class="d-flex align-center">
+      <CommonStyle v-for="(val, i) in list" :key="i" :btnTitle="val.title">
+        <v-list>
+          <v-list-item
+            v-for="(item, index) in val.items"
+            :key="index"
+            :active="val.selected.includes(item.value)"
+            color="primary"
+            :value="item.value"
+            @click="handleClick(item, val)"
+          >
+            <v-list-item-title>{{ item.label }}</v-list-item-title>
+          </v-list-item>
+        </v-list>
+      </CommonStyle>
+      <div class="mr-5 d-flex align-center" v-if="props.tab === 0">
+        <v-radio-group v-model="bounty" inline style="height: 28px;" @update:modelValue="handleChangeBounty">
+          <v-radio v-model="bounty" label="普通职位" :value="false" color="primary" hide-details density="compact" class="mr-3"></v-radio>
+          <v-radio v-model="bounty" label="赏金职位" :value="true" color="primary" hide-details density="compact"></v-radio>
+        </v-radio-group>
+        <v-radio-group class="ml-5" v-model="selected" inline style="height: 28px;" @update:modelValue="handleChangeSelected">
+          <v-radio v-model="selected" label="新投递" value="0" color="primary" hide-details density="compact" class="mr-3"></v-radio>
+          <v-radio v-model="selected" label="已查看" value="1" color="primary" hide-details density="compact"></v-radio>
+        </v-radio-group>
+      </div>
     </div>
-    <span class="reset-text cursor-pointer ml-3" @click="handleReset">重置</span>
+    <div class="reset-text cursor-pointer ml-3" @click="handleReset">重置</div>
   </div>
 </template>
 
@@ -39,7 +44,7 @@ const props = defineProps({
 })
 
 const selected = ref()
-const bounty = ref(false)
+const bounty = ref(null)
 
 const list = ref([
   {
@@ -85,7 +90,7 @@ list.value.forEach(k => {
     })
   }
   if (k.api) {
-    k.api({ hire: false }).then(data => {
+    k.api({}).then(data => {
       if (data.length) {
         const list = dealDictArrayData([], data)
         k.items = list.map(e => {
@@ -112,7 +117,7 @@ const handleReset = () => {
     return e
   })
   selected.value = ''
-  bounty.value = false
+  bounty.value = null
   emit('reset')
 }
 

+ 26 - 7
src/views/recruit/enterprise/elite/components/table.vue

@@ -27,21 +27,19 @@
         <span v-else>{{ item.status ? props.statusList.find(i => i.value === item.status).label : '' }}</span>
       </template>
       <template v-slot:item.actions="{ item }">
-        <div v-if="tab === 0">
-          <v-btn color="primary" variant="text" @click="handlePreviewResume(item)">查看附件</v-btn>
-          <v-btn color="primary" variant="text" @click="handleInterviewInvite(item)">邀请面试</v-btn>
-        </div>
+        <v-btn v-if="tab === 0" color="primary" variant="text" @click="handlePreviewResume(item)">查看附件</v-btn>
+        <v-btn v-if="tab === 0" color="primary" variant="text" @click="handleInterviewInvite(item)">邀请面试</v-btn>
         <v-btn v-if="tab === 0 || tab === 1" color="primary" variant="text" @click="handleEliminate(item)">不合适</v-btn>
-        <div v-if="tab === 1">
-          <v-btn color="primary" variant="text" @click="handleEnterByEnterprise(item)">入职</v-btn>
-        </div>
+        <v-btn v-if="tab === 1 && (item.status === '3' || item.status === '4')" color="primary" variant="text" @click="handleEnterByEnterprise(item)">入职</v-btn>
         <v-btn v-if="tab === 4" color="primary" variant="text" @click="handleCancelEliminate(item)">取消不合适</v-btn>
+        <v-btn v-if="tab === 2 && item?.job?.hire" color="primary" variant="text" @click="handleSettlement(item)">结算</v-btn>
       </template>
     </v-data-table>
 
     <!-- 邀请面试 -->
     <CtDialog :visible="showInvite" :widthType="2" titleClass="text-h6" title="面试信息" @close="handleEditClose" @submit="handleEditSubmit">
       <InvitePage v-if="showInvite" ref="inviteRef" :itemData="itemData"></InvitePage>
+      <!-- <PublicPage v-if="showInvite && inviteType" ref="publicRef" :item-data="itemData"></PublicPage> -->
     </CtDialog>
   </div>
 </template>
@@ -52,9 +50,12 @@ import { ref, computed, watch } from 'vue'
 import { previewFile } from '@/utils'
 import { personJobCvLook, joinEliminate, personEntryByEnterprise, personCvUnfitCancel } from '@/api/recruit/enterprise/personnel'
 import { saveInterviewInvite } from '@/api/recruit/enterprise/interview'
+import { hireJobCvRelSettlement } from '@/api/recruit/public/delivery'
 import { useI18n } from '@/hooks/web/useI18n'
+import { useUserStore } from '@/store/user'
 import Snackbar from '@/plugins/snackbar'
 import InvitePage from './invite.vue'
+// import PublicPage from './public.vue'
 
 const { t } = useI18n()
 const emit = defineEmits(['refresh'])
@@ -71,7 +72,9 @@ const badgeIcon = computed(() => (item) => {
   return (item.person && item.person.sex) ? (item.person.sex === '1' ? 'mdi-gender-male' : 'mdi-gender-female') : 'mdi-gender-female'
 })
 
+const userStore = useUserStore()
 const inviteRef = ref()
+const publicRef = ref()
 const showInvite = ref(false)
 const headers = ref([
   { title: '姓名', value: 'name', sortable: false },
@@ -151,17 +154,21 @@ const handlePreviewResume = async ({ url, id }) => {
 
 // 邀请面试
 const itemData = ref({})
+// const inviteType = ref(false)
 const handleInterviewInvite = (item) => {
+  // if (item?.job?.hire) inviteType.value = true
   itemData.value = item
   showInvite.value = true
 }
 
 const handleEditClose = () => {
   showInvite.value = false
+  // inviteType.value = false
   itemData.value = {}
 }
 
 const handleEditSubmit = async () => {
+  // if (inviteType.value) return
   const { valid } = await inviteRef.value.CtFormRef.formRef.validate()
   if (!valid) return
   const query = inviteRef.value.getQuery()
@@ -171,6 +178,18 @@ const handleEditSubmit = async () => {
   handleEditClose()
   emit('refresh')
 }
+
+// 结算
+const handleSettlement = async (item) => {
+  if (!item.id) return
+  await hireJobCvRelSettlement(item.id)
+  Snackbar.success(t('common.operationSuccessful'))
+  emit('refresh')
+  // 更新账户信息
+  setTimeout(async () => {
+    await userStore.getEnterpriseUserAccountInfo()
+  }, 2000)
+}
 </script>
 
 <style scoped lang="scss">

+ 2 - 2
src/views/recruit/enterprise/elite/index.vue

@@ -40,7 +40,7 @@ const query = ref({
   pageNo: 1,
   pageSize: 10,
   status: null,
-  type: 0
+  type: null
 })
 const tab = ref(0)
 const tabList = ref([
@@ -121,7 +121,7 @@ const handleScreenReset = () => {
   }
   if (tab.value === 0) {
     query.value.status = null
-    query.value.type = 0
+    query.value.type = null
   }
   if (textItems.value.value) query.value.name = textItems.value.value
   getList()

+ 6 - 3
src/views/recruit/enterprise/informationManagement/informationSettings.vue

@@ -23,11 +23,14 @@ import welfareLabel from './informationSettingsComponents/welfareLabel.vue'
 import businessInformation from './informationSettingsComponents/businessInformation.vue'
 import authentication from './informationSettingsComponents/authentication.vue'
 import { ref, watch } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
+import { 
+  useRoute, 
+  // useRouter
+} from 'vue-router'
 import { useI18n } from '@/hooks/web/useI18n'
 
 const route = useRoute()
-const router = useRouter()
+// const router = useRouter()
 const { t } = useI18n()
 // tab
 const tab = ref(1)
@@ -43,7 +46,7 @@ const tabList = [
 
 watch(() => route?.query?.tabKey, (newVal) => { if (newVal) tab.value = newVal - 0 })
 const handleTabClick = () => {
-  router.push(`${route.path}?tabKey=${tab.value.toString()}`)
+  // router.push(`${route.path}?tabKey=${tab.value.toString()}`)
 }
 </script>
 

+ 83 - 16
src/views/recruit/enterprise/informationManagement/informationSettingsComponents/authentication.vue

@@ -1,31 +1,48 @@
 <template>
   <div v-if="authentication" class="ml-3">
-    <div>
+    <div class="topTip" v-if="info.status === '0'">审核中,请耐心等待</div>
+    <div v-if="info.status === '1'">
       <v-icon color="primary">mdi-check-circle</v-icon>
       已通过实名认证
     </div>
+    <div class="topTip" v-if="info.status === '2'">
+      认证已被驳回,原因:{{ info.reason }}
+    </div>
     <div class="box mt-5">
-      <div>姓名:史迪奇</div>
-      <div class="mt-5">身份证号:4******************8</div>
+      <div>姓名:{{ info.name }}</div>
+      <div class="my-5">身份证号:{{ maskNumber(info.identityNo) }}</div>
+      <div class="d-flex" v-if="info.status !== '1'">
+        <span>国徽照</span>
+        <div class="ml-10" style="width: 120px; height: 120px;">
+          <v-img :src="info.backUrl" width="120" height="120" rounded alt=""/>
+        </div>
+      </div>
+      <div class="d-flex mt-5" v-if="info.status !== '1'">
+        <span>人像照</span>
+        <div class="ml-10" style="width: 120px; height: 120px;">
+          <v-img :src="info.frontUrl" width="120" height="120" rounded alt=""/>
+        </div>
+      </div>
     </div>
-    <v-btn color="primary" class="half-button mt-5" @click="authentication = !authentication">解绑</v-btn>
+    <v-btn v-if="info.status === '2'" class="buttons mt-5" color="primary" @click="handleAgain">重新认证</v-btn>
   </div>
+
   <div v-else>
-    <div class="topTip">为了您在平台有更好的操作体验,请进行实名认证</div>
-    <div class="d-flex align-center justify-center flex-column">
+    <div class="topTip" v-if="info.status !== '2'">为了您在平台有更好的操作体验,请进行实名认证</div>
+    <div class="d-flex align-center justify-center flex-column mt-5">
       <CtForm ref="CtFormRef" :items="formItems" style="width: 300px;">
-        <template #idCardImg1="{ item }">
+        <template #backUrl="{ item }">
           <div class="color-666 font-size-14 mr-5">{{ item.label }}</div>
-          <Img @success="val => item.value = val" @delete="item.value = ''"></Img>
+          <Img :value="item.value" @success="val => item.value = val" @delete="item.value = ''"></Img>
         </template>
-        <template #idCardImg2="{ item }">
+        <template #frontUrl="{ item }">
           <div class="mt-5 d-flex">
             <div class="color-666 font-size-14 mr-5">{{ item.label }}</div>
-            <Img @success="val => item.value = val" @delete="item.value = ''"></Img>
+            <Img :value="item.value" @success="val => item.value = val" @delete="item.value = ''"></Img>
           </div>
         </template>
       </CtForm>
-      <v-btn class="buttons mt-5" color="primary" @click="authentication = !authentication">{{ $t('common.submit') }}</v-btn>
+      <v-btn class="buttons mt-5" color="primary" @click="handleSave">{{ $t('common.submit') }}</v-btn>
     </div>
   </div>
 </template>
@@ -33,10 +50,15 @@
 <script setup>
 defineOptions({ name: 'authentication-page'})
 import { ref } from 'vue'
+import { isValidIdCard18, maskNumber } from '@/utils/validate'
+import { getEnterpriseAuth, saveEnterpriseAuth } from '@/api/recruit/enterprise/information'
+import Snackbar from '@/plugins/snackbar'
 
 // 是否已实名
-const authentication = ref(true)
+const info = ref({})
+const authentication = ref(false)
 const CtFormRef = ref()
+const query = ref({})
 
 const formItems = ref({
   options: [
@@ -49,19 +71,34 @@ const formItems = ref({
     },
     {
       type: 'text',
-      key: 'idCardNo',
+      key: 'identityNo',
       value: '',
       label: '身份证号码 *',
-      rules: [v => !!v || '请输入您的身份证号码']
+      rules: [
+        value => {
+          if (!value) {
+            return '请输入您的身份证号码'
+          }
+          return true
+        },
+        value => {
+          if (!isValidIdCard18(value)) {
+            return '请输入正确的身份证号码'
+          }
+          return true
+        }
+      ]
     },
     {
-      slotName: 'idCardImg1',
+      slotName: 'backUrl',
+      key: 'backUrl',
       value: '',
       label: '身份证-国徽照 *',
       rules: [v => !!v || '请上传']
     },
     {
-      slotName: 'idCardImg2',
+      key: 'frontUrl',
+      slotName: 'frontUrl',
       value: '',
       label: '身份证-人像照 *',
       rules: [v => !!v || '请上传']
@@ -69,6 +106,36 @@ const formItems = ref({
   ]
 })
 
+const getData = async () => {
+  const data = await getEnterpriseAuth()
+  if (!data) return authentication.value = false
+  authentication.value = true
+  info.value = data
+}
+getData()
+
+const handleAgain = () => {
+  formItems.value.options.forEach(item => {
+    item.value = info.value[item.key]
+  })
+  query.value.id = info.value.id
+  authentication.value = false
+}
+
+// 提交实名认证
+const handleSave = async () => {
+  const { valid } = await CtFormRef.value.formRef.validate()
+  if (!valid) return
+  formItems.value.options.forEach(item => {
+    query.value[item.key] = item.value
+  })
+  if (!query.value.backUrl || !query.value.frontUrl) return Snackbar.warning('请上传您的身份证照片')
+
+  await saveEnterpriseAuth(query.value)
+  Snackbar.success('提交成功')
+  query.value = {}
+  getData()
+}
 </script>
 
 <style scoped lang="scss">

+ 24 - 3
src/views/recruit/enterprise/informationManagement/informationSettingsComponents/basicInfo.vue

@@ -5,8 +5,8 @@
     <CtForm ref="CtFormRef" :items="formItems" style="width: 900px;margin: 0 auto">
       <template #name="{ item }">
         <div v-show="!item.show" class="text-right" style="width: 80px; line-height: 40px;">
-          <v-icon color="primary" size="20">mdi-shield-check</v-icon> <!-- mdi-shield-remove -->
-          <span style="color: var(--v-primary-base);font-size: 14px;">已认证</span>
+          <v-icon :color="statusInfo.color" size="20">{{ statusInfo.mdi }}</v-icon>
+          <span :style="{'color': statusInfo.color,'font-size': '14px'}">{{ statusInfo.label }}</span>
         </div>
       </template>
       <template #industryId="{ item }">
@@ -33,8 +33,9 @@
 
 <script setup>
 defineOptions({name: 'informationSettingsComponents-basicInfo'})
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed } from 'vue'
 import { getEnterpriseBaseInfo, updateEnterpriseBaseInfo } from '@/api/enterprise'
+import { getEnterpriseAuth } from '@/api/recruit/enterprise/information'
 import { getDict } from '@/hooks/web/useDictionaries'
 import { useI18n } from '@/hooks/web/useI18n'
 import industryTypeCard from '@/components/industryTypeCard'
@@ -181,6 +182,26 @@ getDict('menduner_industry_type', {}, 'industryList').then(({ data }) => {
   industryList.value = data
 })
 
+// 获取企业实名信息
+const authInfo = ref({})
+const statusList = [
+  { label: '未认证', color: '#fb8c00', value: null, mdi: 'mdi-shield-remove' },
+  { label: '审核中', color: '#fb8c00', value: '0', mdi: 'mdi-shield-half-full' },
+  { label: '已认证', color: '#00897B', value: '1', mdi: 'mdi-shield-check' },
+  { label: '已驳回', color: '#fe574a', value: '2', mdi: 'mdi-shield-off' }
+]
+const getAuthInfo = async () => {
+  const data = await getEnterpriseAuth()
+  if (!data) return
+  authInfo.value = data
+}
+getAuthInfo()
+
+const statusInfo = computed(() => {
+  const obj = (authInfo.value && Object.keys(authInfo.value).length) ? statusList.find(e => e.value === authInfo.value.status) : statusList[0]
+  return obj
+})
+
 // 获取基本信息
 const getBaseInfo = async () => {
   const data = await getEnterpriseBaseInfo()

+ 5 - 1
src/views/recruit/enterprise/informationManagement/informationSettingsComponents/enterpriseLogo.vue

@@ -34,6 +34,7 @@
 
 <script setup>
 defineOptions({name: 'informationSettingsComponents-enterpriseLogo'})
+import { useUserStore } from '@/store/user'; const userStore = useUserStore()
 import { ref } from 'vue'
 import { uploadFile } from '@/api/common'
 import { useI18n } from '@/hooks/web/useI18n'
@@ -44,7 +45,10 @@ const { t } = useI18n()
 let squareImageUrl = ref('')
 const getInfo = async () => {
   const data = await getEnterpriseBaseInfo()
-  if (data && data?.logoUrl) squareImageUrl.value = data.logoUrl
+  if (data && data?.logoUrl) {
+    squareImageUrl.value = data.logoUrl
+    await userStore.getEnterpriseInfo() // 更新当前登录的企业用户信息
+  }
 }
 getInfo()
 

+ 4 - 4
src/views/recruit/enterprise/positionManagement/components/add.vue

@@ -16,7 +16,7 @@
         </v-timeline-item>
       </v-timeline>
       <div class="text-center mb">
-        <v-btn class="half-button mr-3" color="primary" variant="outlined" @click="handleCancel">{{ $t('common.cancel') }}</v-btn>
+        <v-btn class="half-button mr-3" color="primary" variant="outlined" @click="handleCancel(itemData.hire)">{{ $t('common.cancel') }}</v-btn>
         <v-btn class="half-button" color="primary" @click="handleSave">{{ $t('common.release') }}</v-btn>
       </div>
     </v-card>
@@ -82,9 +82,9 @@ const list = [
 const recharge = ref(false)
 
 // 取消
-const handleCancel = () => {
+const handleCancel = (hire) => { //  hire:是否是众聘岗位
   itemData.value = {}
-  router.push('/recruit/enterprise/position')
+  const query = hire ? { hire } : {}; router.push({ path: '/recruit/enterprise/position', query })
   // 新增职位发布需更新账户信息
   if (route.query && !route.query?.id) {
     setTimeout(async () => {
@@ -111,7 +111,7 @@ const handleSave = async () => {
   try {
     await saveJobAdvertised(query)
     Snackbar.success(route.query.id ? t('common.editSuccessMsg') : t('common.publishSuccessMsg'))
-    handleCancel()
+    handleCancel(baseInfo.hire)
   } catch (error) {
     // 余额不足展示充值窗口
     if (error === '用户余额不足') recharge.value = true

+ 7 - 3
src/views/recruit/enterprise/positionManagement/index.vue

@@ -35,14 +35,13 @@ defineOptions({ name: 'enterprise-position-list'})
 import { ref } from 'vue'
 import TextUI from '@/components/FormUI/TextInput'
 import PositionItem from './components/item.vue'
-import { useRouter } from 'vue-router'
+import { useRoute } from 'vue-router'; const route = useRoute()
+import { useRouter } from 'vue-router'; const router = useRouter()
 import { getJobAdvertisedList } from '@/api/position'
 import { dealDictArrayData } from '@/utils/position'
 import { useI18n } from '@/hooks/web/useI18n'
 
 const { t } = useI18n()
-const router = useRouter()
-const tab = ref(1)
 const total = ref(0)
 const tipsText = ref(t('common.noData'))
 const query = ref({
@@ -52,6 +51,10 @@ const query = ref({
   hasExpiredData: false, // true 到期职位
   hire: false // true 众聘岗位
 })
+
+const tab = ref(route.query?.hire ? 4: 1)
+if (route.query?.hire) router.replace({ path: route.path, query: { ...route.query, hire: false } })
+
 const tabList = [
   { label: t('position.recruitmentInProgress'), value: 1 },
   { label: t('position.closed'), value: 2 },
@@ -72,6 +75,7 @@ const handleAdd = () => {
   router.push('/recruit/enterprise/position/add')
 }
 
+
 // 获取职位列表
 const getPositionList = async () => {
   query.value.hasExpiredData = tab.value === 3 ? true : false

+ 2 - 1
src/views/recruit/enterprise/register/register.vue

@@ -97,11 +97,12 @@ const formItems = ref({
       type: 'text',
       key: 'code',
       value: '',
+      counter: 18,
       label: '企业统一社会信用代码 *',
       rules: [v => !!v || '请输入企业统一社会信用代码']
     },
     {
-      type: 'text',
+      type: 'number',
       key: 'phone',
       value: '',
       label: '联系电话 *',

+ 0 - 171
src/views/recruit/enterprise/statistics/components/data.js

@@ -1,171 +0,0 @@
-export const list = [
-  {
-    col: 6,
-    option: {
-      title: {
-        text: '性别比例'
-      },
-      tooltip: {
-        trigger: 'item'
-      },
-      legend: {
-        top: '5%',
-        left: 'center'
-      },
-      series: [
-        {
-          name: 'Access From',
-          type: 'pie',
-          radius: ['40%', '70%'],
-          avoidLabelOverlap: false,
-          itemStyle: {
-            borderRadius: 10,
-            borderColor: '#fff',
-            borderWidth: 2
-          },
-          label: {
-            show: true,
-            formatter: e => {
-              return e.name + ': ' + e.value + '%'
-            }
-          },
-          labelLine: {
-            show: true
-          },
-          data: [
-            { value: 65, name: '男' },
-            { value: 35, name: '女' }
-          ]
-        }
-      ]
-    }
-  },
-  {
-    col: 6,
-    option: {
-      title: {
-        text: '年龄分布'
-      },
-      xAxis: {
-        type: 'category',
-        data: ['18-22岁', '22-30岁', '30-39岁', '40-49岁', '50-59岁']
-      },
-      yAxis: {
-        type: 'value'
-      },
-      grid: {
-        left: '0',
-        top: '50',
-        right: '0',
-        bottom: 0,
-        containLabel: true
-      },
-      series: [
-        {
-          data: [120, 200, 150, 80, 70],
-          type: 'bar',
-          barWidth: 40,
-          label: {
-            show: true
-          }
-        }
-      ]
-    }
-  },
-  {
-    col: 6,
-    option: {
-      title: {
-        text: '工作年限分布'
-      },
-      xAxis: {
-        type: 'category',
-        data: ['应届毕业生', '1年以上', '2年以上', '3年以上', '5年以上', '8年以上', '10年以上']
-      },
-      yAxis: {
-        type: 'value'
-      },
-      grid: {
-        left: '0',
-        top: '50',
-        right: '0',
-        bottom: 0,
-        containLabel: true
-      },
-      series: [
-        {
-          data: [120, 200, 150, 80, 70, 110, 130],
-          type: 'bar',
-          barWidth: 40,
-          label: {
-            show: true
-          }
-        }
-      ]
-    }
-  },
-  {
-    col: 6,
-    option: {
-      title: {
-        text: '学历分布'
-      },
-      xAxis: {
-        type: 'category',
-        data: ['本科以上', '大专', '中专', '中技', '高中', '初中']
-      },
-      yAxis: {
-        type: 'value'
-      },
-      grid: {
-        left: '0',
-        top: '50',
-        right: '0',
-        bottom: 0,
-        containLabel: true
-      },
-      series: [
-        {
-          data: [120, 200, 150, 80, 70, 110],
-          type: 'bar',
-          barWidth: 40,
-          label: {
-            show: true
-          }
-        }
-      ]
-    }
-  },
-  {
-    col: 12,
-    option: {
-      title: {
-        text: '期望月薪'
-      },
-      xAxis: {
-        type: 'category',
-        data: ['3-5k', '5-8k', '8-12k', '12-15k', '15-20k', '20-30k', '面议']
-      },
-      yAxis: {
-        type: 'value'
-      },
-      grid: {
-        left: '0',
-        top: '50',
-        right: '0',
-        bottom: 0,
-        containLabel: true
-      },
-      series: [
-        {
-          data: [120, 200, 150, 80, 70, 110, 130],
-          type: 'bar',
-          barWidth: 40,
-          label: {
-            show: true
-          }
-        }
-      ]
-    }
-  }
-]

+ 8 - 19
src/views/recruit/enterprise/statistics/components/overview.vue

@@ -133,23 +133,25 @@ onMounted(() => {
 <style scoped lang="scss">
 .overview {
   display: flex;
-  // flex-wrap: wrap;
-  // width: 100%;
-  overflow-x: auto;
+  width: 100%;
+  flex-wrap: wrap; // 换行
 }
 .overview-item {
   // width: calc((100% - 84px) / 8);
   // min-width: calc((100% - 84px) / 8);
   // max-width: calc((100% - 84px) / 8);
-  width: 180px;
-  min-width: 180px;
+  min-width: 200px;
   margin: 0 12px 12px 0;
   height: 175px;
   border-radius: 12px;
   overflow: hidden;
   transition: all .2s linear;
   background-color: #f7f8fa;
-  &:nth-child(8n) {
+  div {
+    white-space: nowrap; /* 防止子级文本换行 */
+    flex-grow: 1; /* 允许子级根据内容撑开,但保持最小宽度限制 */
+  }
+  &:last-child {
     margin-right: 0;
   }
 }
@@ -158,17 +160,4 @@ onMounted(() => {
   font-weight: 700;
   font-size: 44px;
 }
-::-webkit-scrollbar {
-  width: 10px;
-  height: 6px;
-  // display: none;
-}
-::-webkit-scrollbar-thumb, .temporaryAdd ::-webkit-scrollbar-thumb, .details_edit ::-webkit-scrollbar-thumb {
-  // 滚动条-颜色
-  background: #c3c3c379;
-}
-::-webkit-scrollbar-track, .temporaryAdd ::-webkit-scrollbar-track, .details_edit ::-webkit-scrollbar-track {
-  // 滚动条-底色
-  background: #e5e5e58f;
-}
 </style>

+ 188 - 9
src/views/recruit/enterprise/statistics/components/resume.vue

@@ -1,22 +1,201 @@
 <template>
-  <v-container>
-    <v-row>
-      <v-col class="bgc" v-for="(val, i) in list" :key="i" :md="val.col">
-        <Echarts :height="400" :option="val.option"></Echarts>
-      </v-col>
-    </v-row>
-  </v-container>
+  <div class="chart-box">
+    <div class="chart-item" v-for="(val, i) in list" :key="i">
+      <Echarts :height="400" :option="val.option"></Echarts>
+    </div>
+    <!-- <div class="fullChart">
+      <Echarts :height="400" :option="option"></Echarts>
+    </div> -->
+  </div>
 </template>
 
 <script setup>
 defineOptions({ name: 'resume-analysis'})
-import { list } from './data.js'
+import { ref, onMounted, watch, nextTick } from 'vue'
+import cloneDeep from 'lodash/cloneDeep'
+import { getJobCvAgeCount, getJobCvEduCount, getJobCvSexCount, getJobCvExpCount } from '@/api/recruit/enterprise/statistics'
 
+const props = defineProps({
+  query: Object
+})
+
+// 柱状图公共option
+const barCommonOption = {
+  title: {
+    text: ''
+  },
+  xAxis: {
+    type: 'category',
+    name: '范围',
+    axisLabel: {
+      rotate: 30
+    },
+    data: []
+  },
+  yAxis: {
+    type: 'value',
+    name: '数量(人)'
+  },
+  grid: {
+    left: '20',
+    top: '70',
+    right: '50',
+    bottom: '10',
+    containLabel: true
+  },
+  series: [
+    {
+      data: [],
+      type: 'bar',
+      barWidth: 40,
+      label: {
+        show: true
+      }
+    }
+  ]
+}
+
+// 期望月薪柱状图
+// const option = {
+//   title: {
+//     text: '期望月薪'
+//   },
+//   xAxis: {
+//     type: 'category',
+//     name: '范围',
+//     data: ['3-5k', '5-8k', '8-12k', '12-15k', '15-20k', '20-30k', '面议']
+//   },
+//   yAxis: {
+//     type: 'value'
+//   },
+//   grid: {
+//     left: '0',
+//     top: '60',
+//     right: '50',
+//     bottom: '10',
+//     containLabel: true
+//   },
+//   series: [
+//     {
+//       data: [120, 200, 150, 80, 70, 110, 130],
+//       type: 'bar',
+//       barWidth: 40,
+//       label: {
+//         show: true
+//       }
+//     }
+//   ]
+// }
+
+const list = ref([
+  {
+    api: getJobCvSexCount,
+    isPie: true,
+    option: {
+      title: {
+        text: '性别分布'
+      },
+      tooltip: {
+        trigger: 'item'
+      },
+      legend: {
+        top: '5%',
+        left: 'center'
+      },
+      series: [
+        {
+          name: '性别分布',
+          type: 'pie',
+          radius: ['40%', '70%'],
+          avoidLabelOverlap: false,
+          itemStyle: {
+            borderRadius: 10,
+            borderColor: '#fff',
+            borderWidth: 2
+          },
+          label: {
+            show: true,
+            formatter: e => {
+              return e.data.key + ': ' + e.value + '人'
+            }
+          },
+          labelLine: {
+            show: true
+          },
+          data: []
+        }
+      ]
+    }
+  },
+  {
+    api: getJobCvAgeCount,
+    title: '年龄分布',
+    option: cloneDeep(barCommonOption)
+  },
+  {
+    api: getJobCvExpCount,
+    title: '工作年限分布',
+    option: cloneDeep(barCommonOption)
+  },
+  {
+    api: getJobCvEduCount,
+    title: '学历分布',
+    option: cloneDeep(barCommonOption)
+  }
+])
+
+const getStatistics = () => {
+  list.value.forEach(async (e) => {
+    const data = await e.api(props.query)
+    if (e.isPie) {
+      e.option.series[0].data = data
+    } else {
+      e.option.title.text = e.title
+      e.option.xAxis.data = data.x
+      e.option.series[0].data = data.y
+    }
+  })
+}
+
+onMounted(() => {
+  nextTick(() => {
+    getStatistics()
+  })
+})
+
+watch(
+  () => props.query,
+  (val) => {
+    if (val) getStatistics()
+  },
+  { deep: true }
+)
 </script>
 
 <style scoped lang="scss">
-.bgc {
+.chart-box {
+  width: 100%;
+  display: flex;
+  flex-wrap: wrap;
+  .chart-item {
+    width: calc((100% - 24px) / 2);
+    min-width: calc((100% - 24px) / 2);
+    max-width: calc((100% - 24px) / 2);
+    overflow: hidden;
+    transition: all .2s linear;
+    background-color: #f7f8fa;
+    border-radius: 8px;
+    margin: 0 12px 12px 0;
+    padding: 12px;
+    &:nth-child(2n) {
+      margin-right: 0;
+    }
+  }
+}
+.fullChart {
+  width: 100%;
   background-color: #f7f8fa;
   border-radius: 8px;
+  padding: 12px;
 }
 </style>

+ 0 - 11
src/views/recruit/enterprise/statistics/index.vue

@@ -1,11 +0,0 @@
-<template>
-  <div>统计分析</div>
-</template>
-
-<script setup>
-defineOptions({ name: 'enterprise-statistics'})
-</script>
-
-<style scoped lang="scss">
-
-</style>

+ 36 - 7
src/views/recruit/enterprise/statistics/overallAnalysis.vue

@@ -4,13 +4,13 @@
       <div class="d-flex align-center">
         <span>选择时间</span>
         <div class="ml-5 after">
-          <span v-for="(k, i) in list" :key="i" :class="['item', { 'active': current === (i + 1) }]" @click="current = i + 1 ">{{ k }}</span>
+          <span v-for="(k, i) in list" :key="i" :class="['item', { 'active': current === i }]" @click="handleClickType(i)">{{ k.label }}</span>
         </div>
       </div>
       <div class="d-flex align-center ml-15">
         <span>自定义日期</span>
         <div class="ml-5">
-          <date-picker v-model="date" :options="{ range: true, clearable: true, placeholder: '请选择要查看的时间范围' }"></date-picker>
+          <date-picker v-model="date" :options="{ range: true, clearable: true, placeholder: '请选择要查看的时间范围', change: handleChangeDate, format: 'timestamp' }"></date-picker>
         </div>
       </div>
     </div>
@@ -21,10 +21,10 @@
       <Overview class="mt-5"></Overview>
     </div>
     <div class="my-10">
-      <v-tabs v-model="tab" align-tabs="start" color="primary" bg-color="#f7f8fa">
+      <v-tabs class="mb-5" v-model="tab" align-tabs="start" color="primary" bg-color="#f7f8fa">
         <v-tab :value="1">应聘简历分析</v-tab>
       </v-tabs>
-      <ResumeAnalysis class="mt-5"></ResumeAnalysis>
+      <ResumeAnalysis :query="query"></ResumeAnalysis>
     </div>
     <div>
       <v-tabs v-model="tab" align-tabs="start" color="primary" bg-color="#f7f8fa">
@@ -37,15 +37,44 @@
 
 <script setup>
 defineOptions({ name: 'overallAnalysis'})
-import { ref } from 'vue'
+import { reactive, ref } from 'vue'
 import Overview from './components/overview.vue'
 import DailyPage from './components/daily.vue'
 import ResumeAnalysis from './components/resume.vue'
+import { convertTimestampsToDayRange } from '@/utils/date'
 
 const tab = ref(1)
 const date = ref(null)
-const current = ref(1)
-const list = ['最近7天', '上个月', '上季度']
+const current = ref(0)
+const query = reactive({
+  type: 0,
+  time: null
+})
+const list = [
+  { label: '最近7天', value: 0 },
+  { label: '上个月', value: 1 },
+  { label: '上季度', value: 2 }
+]
+
+// 类型选择
+const handleClickType = (i) => {
+  current.value = i
+  query.time = null
+  date.value = null
+  query.type = list[i].value
+}
+
+// 时间范围选择
+const handleChangeDate = (time) => {
+  if (!time) {
+    query.type = 0
+    query.time = null
+    return
+  }
+  current.value = null
+  query.type = 99
+  query.time = convertTimestampsToDayRange(time)
+}
 
 </script>
 

+ 5 - 3
src/views/recruit/personal/PersonalCenter/components/interview/item.vue

@@ -85,9 +85,11 @@ const handleToEnterprise = (item) => {
 // 同意
 const handleAgree = (val) => {
   if (!val.id) return
-  query.value = {
-    id: val.id,
-    phone: val.phone
+  query.value.id = val.id
+  const baseInfo = localStorage.getItem('baseInfo')
+  if (baseInfo) {
+    const { phone } = JSON.parse(baseInfo)
+    query.value.phone = phone
   }
   show.value = true
 }

+ 4 - 7
src/views/recruit/personal/PersonalCenter/dynamic/right.vue

@@ -43,12 +43,12 @@
       </div>
       <span class="more-text">{{ $t('resume.uploadUpToFiveCopies') }}</span>
       <div v-if="attachmentList.length">
-        <div class="d-flex attachment-item my-2 cursor-pointer" v-for="k in attachmentList" :key="k.id">
+        <div class="d-flex attachment-item my-2" v-for="k in attachmentList" :key="k.id">
           <v-icon color="primary">mdi-file-account</v-icon>
           <div class="file-name ellipsis ml-2">{{ k.title }}</div>
-          <v-icon color="primary" @click="previewFile(k.url)">mdi-eye-outline</v-icon>
-          <v-icon class="mx-2" color="primary" @click="handleDownload(k)">mdi-download-box-outline</v-icon>
-          <v-icon color="error" @click="handleDelete(k)">mdi-trash-can-outline</v-icon>
+          <v-icon class="cursor-pointer" color="primary" @click="previewFile(k.url)">mdi-eye-outline</v-icon>
+          <v-icon class="cursor-pointer mx-2" color="primary" @click="handleDownload(k)">mdi-download-box-outline</v-icon>
+          <v-icon class="cursor-pointer" color="error" @click="handleDelete(k)">mdi-trash-can-outline</v-icon>
         </div>
       </div>
       <div v-else class="more-text d-flex justify-center">暂无简历,请先上传</div>
@@ -281,9 +281,6 @@ const interviewScheduleMore = () => {
     font-size: 14px;
     .file-name {
       width: 230px;
-      &:hover {
-        color: var(--v-primary-base);
-      }
     }
   }
 }

+ 8 - 50
src/views/recruit/personal/home/components/popularEnterprises.vue

@@ -9,63 +9,21 @@
 
 <script setup name="popularEnterprises">
 import HotPromoted from '@/components/Enterprise/hotPromoted.vue'
-import { ref, reactive } from 'vue'
+import { ref } from 'vue'
 import { getHotEnterprise } from '@/api/enterprise'
-import { getDict } from '@/hooks/web/useDictionaries'
-import { dealDictArrayData } from '@/utils/position'
+import { dealDictArrayData, dealDictObjData } from '@/utils/position'
 
 const items = ref([])
-const dictObj = reactive({
-  payUnit: [], // 薪资单位
-  scale: [], // 规模
-  industry: [], // 行业
-  edu: [], // 学历
-  exp: [], // 工作经验
-  area: [], // 地区
-  financing: [] // 融资类型
-})
-const dictList = ref([
-  { type: 'menduner_pay_unit', value: 'payUnit', key: 'payUnit', label: 'payName' },
-  { type: 'menduner_education_type', value: 'edu', key: 'eduType', label: 'eduName' },
-  { type: 'menduner_exp_type', value: 'exp', key: 'expType', label: 'expName' },
-  { type: 'menduner_area_type', value: 'area', key: 'areaId', label: 'areaName', params: {}, apiType: 'areaList', nameKey: 'name', valueKey: 'id' },
-  { type: 'menduner_financing_status', value: 'financing', key: 'financingStatus', label: 'financingName', isEnter: true },
-  { type: 'menduner_scale', value: 'scale', key: 'scale', label: 'scaleName', isEnter: true },
-  { type: 'menduner_industry_type', value: 'industry', key: 'industryId', label: 'industryName', params: {}, apiType: 'industryList', nameKey: 'nameCn', valueKey: 'id', isEnter: true }
-])
 
 // 热门企业
 const getHotEnterpriseList = async () => {
   const { list } = await getHotEnterprise({ pageNo: 1, pageSize: 9 })
-  dictList.value.forEach(item => {
-    items.value = list.map(e => {
-      if (item.isEnter) {
-        const valueKey = item.nameKey ? item.nameKey : 'label'
-        const idKey = item.valueKey ? item.valueKey : 'value'
-        const obj = dictObj[item.value].find(k => k[idKey] === e.enterprise[item.key])
-        if (!obj) return
-        e[item.label] = obj[valueKey]
-      }
-      const list = e.jobList
-      if (!item.isEnter) {
-        // 职位列表
-        e.jobList = dealDictArrayData([], list).slice(0, 3)
-      }
-      return e
-    })
+  items.value = list.map(e => {
+    let jobList = []
+    const enterprise = dealDictObjData({}, e.enterprise)
+    if (e.jobList && e.jobList.length) jobList = dealDictArrayData([], e.jobList).slice(0, 3)
+    return { enterprise, jobList }
   })
 }
-
-// 字典
-const getDictList = async () => {
-  dictList.value.forEach(async (val) => {
-    const { data } = await getDict(val.type, val.params, val.apiType)
-    dictObj[val.value] = data
-  })
-}
-const getData = async () => {
-  await getDictList()
-  getHotEnterpriseList()
-}
-getData()
+getHotEnterpriseList()
 </script>

+ 0 - 1
src/views/recruit/personal/home/index.vue

@@ -32,7 +32,6 @@ import { useRouter } from 'vue-router'
 import { nextTick, ref } from 'vue'
 
 const router = useRouter()
-console.log('1', localStorage.getItem('simpleCompleteDialogHaveBeenShow'))
 const simple = localStorage.getItem('simpleCompleteDialogHaveBeenShow')
 const showSimplePage = ref(simple? false : true) // 只提示一次
 nextTick(() => {

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

@@ -207,6 +207,7 @@ const getCollectionStatus = async () => {
   if (!getToken()) return isCollection.value = false
   const data = await getJobFavoriteCheck({ jobId: id })
   isCollection.value = data
+  console.log('isCollection', data)
 }
 getCollectionStatus()
 
@@ -230,33 +231,6 @@ const shareUrlTxt = computed(() => {
   return 'http://menduner.citupro.com:7878' + shareUrl.value
 })
 const copyText = async () => {
-  // try {
-  //   const txt = shareUrlTxt.value
-  //   if (navigator.clipboard && navigator.clipboard.writeText) {
-  //     await navigator.clipboard.writeText(txt)
-  //     Snackbar.success('复制成功')
-  //   } else {
-  //     const textArea = document.createElement("textarea")
-  //     textArea.value = txt
-  //     textArea.style.position = "fixed" // 避免在页面上滚动  
-  //     textArea.style.top = 0
-  //     textArea.style.left = 0
-  //     textArea.style.width = "2em"
-  //     textArea.style.height = "2em"
-  //     textArea.style.padding = 0
-  //     textArea.style.border = "none"
-  //     textArea.style.outline = "none"
-  //     textArea.style.boxShadow = "none"
-  //     textArea.style.background = "transparent"
-  //     document.body.appendChild(textArea)
-  //     textArea.focus()
-  //     textArea.select()
-  //     const successful = document.execCommand('copy')
-  //     Snackbar.success(successful ? '复制成功' : '复制失败,请手动复制。')
-  //   }
-  // } catch (err) {
-  //   Snackbar.error('复制失败,请手动复制。')
-  // }
   try {
     await navigator.clipboard.writeText(shareUrlTxt.value)
     Snackbar.success('复制成功')

+ 0 - 1
src/views/recruit/personal/position/index.vue

@@ -5,7 +5,6 @@
       <div class="stickyBox">
         <headSearch
           v-model="headSearchText"
-          text="中国"
           @handleSearch="val => handleQueryChange('content', val)"
         ></headSearch>
       </div>

+ 2 - 1
src/views/recruit/personal/remuse/components/basicInfo.vue

@@ -154,13 +154,14 @@ const openFileInput = () => {
 // 上传头像
 const handleUploadFile = async (e) => {
   const file = e.target.files[0]
+  if (!file) return
   const formData = new FormData()
   formData.append('file', file)
   const { data } = await uploadFile(formData)
   if (!data) return
   Snackbar.success(t('common.uploadSucMsg'))
   await updatePersonAvatar(data)
-  if (baseInfo.value.userId) await userStore.getUserBaseInfos(baseInfo.value.userId)
+  await userStore.getUserBaseInfos(baseInfo.value.userId)
   getBasicInfo()
 }
 

+ 1 - 0
src/views/recruit/personal/remuse/components/educationExp.vue

@@ -72,6 +72,7 @@ const getSchoolListData = async (name, init = '') => {
   item.items = data
 }
 const debouncedCallbackSchool = debounce(newValue => {
+  if (!newValue) return
   getSchoolListData(newValue)
 }, 500)