Преглед изворни кода

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

lifanagju_citu пре 1 месец
родитељ
комит
fe09349559
47 измењених фајлова са 793 додато и 752 уклоњено
  1. 38 0
      src/api/recruit/enterprise/talentMap/labeling.js
  2. 9 0
      src/api/recruit/enterprise/talentMap/search.js
  3. 0 0
      src/api/recruit/enterprise/talentMap/tag.js
  4. 7 4
      src/components/Position/longCompany.vue
  5. 26 7
      src/components/Position/longStrip.vue
  6. 3 3
      src/layout/company/navBar.vue
  7. 20 4
      src/layout/index.vue
  8. 12 4
      src/router/modules/components/recruit/enterprise.js
  9. 1 0
      src/styles/personal/navBar.css
  10. 1 1
      src/styles/personal/navBar.min.css
  11. 1 0
      src/styles/personal/navBar.scss
  12. 18 10
      src/views/headhunting/components/nav.vue
  13. 13 5
      src/views/mall/home/pointExchange/index.vue
  14. 14 5
      src/views/mall1/exchange.vue
  15. 2 2
      src/views/recruit/components/message/components/chatting.vue
  16. 74 29
      src/views/recruit/components/message/index.vue
  17. 12 5
      src/views/recruit/enterprise/entInfoSetting/informationSettingsComponents/basicInfo.vue
  18. 11 5
      src/views/recruit/enterprise/entInfoSetting/informationSettingsComponents/businessInformation.vue
  19. 33 13
      src/views/recruit/enterprise/interviewManagement/components/item.vue
  20. 14 5
      src/views/recruit/enterprise/invoiceManagement/index.vue
  21. 13 5
      src/views/recruit/enterprise/jobFair/details.vue
  22. 160 166
      src/views/recruit/enterprise/newTalentMap/labeling/index.vue
  23. 88 0
      src/views/recruit/enterprise/newTalentMap/search/index.vue
  24. 2 2
      src/views/recruit/enterprise/newTalentMap/tag/index.vue
  25. 52 32
      src/views/recruit/enterprise/resume/components/table.vue
  26. 14 5
      src/views/recruit/enterprise/search/recommend/index.vue
  27. 12 3
      src/views/recruit/enterprise/search/retrieval/index.vue
  28. 19 10
      src/views/recruit/enterprise/systemManagement/groupAccount/index.vue
  29. 6 3
      src/views/recruit/enterprise/talentPool/index.vue
  30. 0 355
      src/views/recruit/enterprise/talentPool/indexCopy.vue
  31. 1 0
      src/views/recruit/enterprise/tradingOrder/components/balance.vue
  32. 1 1
      src/views/recruit/enterprise/tradingOrder/components/public.vue
  33. 1 0
      src/views/recruit/enterprise/tradingOrder/components/recharge.vue
  34. 0 22
      src/views/recruit/enterprise/tradingOrder/components/trading.vue
  35. 7 0
      src/views/recruit/enterprise/tradingOrder/components/transaction.vue
  36. 3 3
      src/views/recruit/enterprise/tradingOrder/index.vue
  37. 13 5
      src/views/recruit/personal/PersonalCenter/resume/attachment/index.vue
  38. 12 5
      src/views/recruit/personal/PersonalCenter/resume/online/components/basicInfo.vue
  39. 12 5
      src/views/recruit/personal/PersonalCenter/resume/online/components/educationExp.vue
  40. 13 6
      src/views/recruit/personal/PersonalCenter/resume/online/components/jobIntention.vue
  41. 11 5
      src/views/recruit/personal/PersonalCenter/resume/online/components/selfEvaluation.vue
  42. 1 1
      src/views/recruit/personal/PersonalCenter/resume/online/components/trainingExperience.vue
  43. 16 7
      src/views/recruit/personal/PersonalCenter/resume/online/components/vocationalSkills.vue
  44. 11 5
      src/views/recruit/personal/PersonalCenter/resume/online/components/workExperience.vue
  45. 8 1
      src/views/recruit/personal/PersonalCenter/shippingAddress/index.vue
  46. 6 2
      src/views/recruit/personal/PersonalCenter/student/InternshipReport/index.vue
  47. 2 1
      vite.config.mjs

+ 38 - 0
src/api/recruit/enterprise/talentMap/labeling.js

@@ -0,0 +1,38 @@
+import request from '@/config/axios'
+
+// 获取所有名片列表
+export const getCardList = async () => {
+	return await request.get({
+		url: '/api/parse/get-business-cards'
+	})
+}
+
+// 根据人才id获取关联的标签列表
+export const getTalentTagById = async (talent_id) => {
+	return await request.get({
+		url: `/api/parse/talent-get-tags/${talent_id}`,
+	})
+}
+
+// 更新人才关联标签
+export const updateTalentTags = async (data) => {
+	return await request.post({
+		url: '/api/parse/talent-update-tags',
+		data
+	})
+}
+
+// 人才启用/禁用
+export const updateTalentStatus = async (talent_id, data) => {
+	return await request.put({
+		url: `/api/parse/update-business-cards/${talent_id}/status`,
+		data
+	})
+}
+
+// 获取人才名片
+export const getTalentCardByImagePath = async (image_path) => {
+	return await request.download({
+		url: `/api/parse/business-cards/image/${image_path}`,
+	})
+}

+ 9 - 0
src/api/recruit/enterprise/talentMap/search.js

@@ -0,0 +1,9 @@
+import request from '@/config/axios'
+
+// 根据条件获取人才列表
+export const getTalentList = async (data) => {
+	return await request.post({
+		url: '/api/parse/query-kg',
+		data
+	})
+}

+ 0 - 0
src/api/recruit/enterprise/talentMap/tag/index.js → src/api/recruit/enterprise/talentMap/tag.js


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

@@ -34,6 +34,7 @@
 <script setup>
 defineOptions({ name: 'long-company-card'})
 import Snackbar from '@/plugins/snackbar'
+import Confirm from '@/plugins/confirm'
 import { useI18n } from '@/hooks/web/useI18n'
 import { formatName } from '@/utils/getText'
 import { getEnterpriseUnsubscribe } from '@/api/enterprise'
@@ -48,10 +49,12 @@ const { t } = useI18n()
 
 // 取消收藏
 const handleCancel = async (item) => {
-  if (!item.id) return Snackbar.warning(t('sys.api.operationFailed'))
-  await getEnterpriseUnsubscribe(item.id)
-  emits('refresh')
-  Snackbar.success(t('common.operationSuccessful'))
+  if (!item.id) return Snackbar.warning('数据异常')
+  Confirm('系统提示', '是否确认取消收藏该企业?').then(async () => {
+    await getEnterpriseUnsubscribe(item.id)
+    emits('refresh')
+    Snackbar.success(t('common.operationSuccessful'))
+  })
 }
 
 </script>

+ 26 - 7
src/components/Position/longStrip.vue

@@ -13,9 +13,26 @@
         </div>
         <div class="d-flex align-center">
           <div v-if="val.active && val.job.status === '0'" class="header-btn">
-            <v-btn v-if="showCancelDeliveryResumeBtn && val.cvRel?.status === '0'" class="MiSans-Medium" color="warning" size="small" @click.stop="emits('cancelDeliveryResume', val.cvRel.id)">撤销投递简历</v-btn>
-            <v-btn v-if="props.showCancelBtn" class="half-button ml-3 MiSans-Medium" color="warning" size="small" @click.stop="handleCancel(val)">取消收藏</v-btn>
-            <v-btn class="half-button ml-3 MiSans-Medium" color="#008BB7" size="small" @click.stop="toDetails(val)">立即沟通</v-btn>
+            <v-btn 
+              v-if="showCancelDeliveryResumeBtn && val.cvRel?.status === '0'" 
+              class="MiSans-Medium" 
+              color="warning" 
+              size="small" 
+              @click.stop="emits('cancelDeliveryResume', val.cvRel.id)"
+            >撤销投递简历</v-btn>
+            <v-btn 
+              v-if="props.showCancelBtn" 
+              class="half-button ml-3 MiSans-Medium" 
+              color="warning" 
+              size="small" 
+              @click.stop="handleCancel(val)"
+            >取消收藏</v-btn>
+            <v-btn 
+              class="half-button ml-3 MiSans-Medium" 
+              color="#008BB7" 
+              size="small" 
+              @click.stop="toDetails(val)"
+            >立即沟通</v-btn>
           </div>
           <div v-if="val.job.status === '1'" class="font-size-14 header-btn color-error mr-3">职位已关闭</div>
           <div v-if="showReportBtn && !val.internshipEnterprise" class="header-btn">
@@ -108,10 +125,12 @@ const router = useRouter()
 const desc = ['industryName', 'scaleName']
 
 const handleCancel = async (item) => {
-  if (!item.job.id) return Snackbar.warning(t('sys.api.operationFailed'))
-  await getPersonJobUnfavorite(item.job.id)
-  emits('refresh')
-  Snackbar.success(t('common.operationSuccessful'))
+  if (!item.job.id) return Snackbar.warning('数据异常')
+  Confirm('系统提示', '是否确定取消收藏该职位?').then(async () => {
+    await getPersonJobUnfavorite(item.job.id)
+    emits('refresh')
+    Snackbar.success(t('common.operationSuccessful'))
+  })
 }
 
 // 上报为实习企业

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

@@ -16,11 +16,9 @@
             </div>
           </div>
 
-          <v-chip class="ml-4 mr-8" label color="primary" size="small" variant="flat" @click="handleLogout(false)">我要求职</v-chip>
-
           <v-chip
             color="primary"
-            class="cursor-pointer"
+            class="cursor-pointer ml-4"
             label
             size="small"
             @click="router.push('/recruit/enterprise/tradingOrder?key=tab_recharge')"
@@ -84,6 +82,8 @@
 
           <!-- 消息通知 -->
           <MessageNotification path="/recruit/enterprise/chatTools"></MessageNotification>
+
+          <v-chip class="ml-8" label color="primary" size="small" variant="flat" @click="handleLogout(false)">我要求职</v-chip>
         </div>
       </div>
     </v-toolbar>

+ 20 - 4
src/layout/index.vue

@@ -10,8 +10,14 @@
         <component :is="Component" :key="router.currentRoute.value.path" v-if="!router.currentRoute.value.meta?.keepAlive"/>
       </router-view>  -->
     </div>
-    <Footers v-if="footerWhiteList.indexOf(router.currentRoute.value.path) === -1" :class="{'mt-10': !router.currentRoute.value.path.includes('/mall')}"></Footers>
-    <Slider v-if="whiteList.indexOf(router.currentRoute.value.path) === -1" class="slider"></Slider>
+    <Footers 
+      v-if="footerWhiteList.indexOf(router.currentRoute.value.path) === -1" 
+      :class="{'mt-10': !router.currentRoute.value.path.includes('/mall')}"
+    ></Footers>
+    <Slider 
+      v-if="whiteList.indexOf(router.currentRoute.value.path) === -1" 
+      class="slider"
+    ></Slider>
 
     <BackTop />
   </div>
@@ -19,7 +25,7 @@
 
 <script setup>
 defineOptions({ name: 'personal-layout-index' })
-import { onMounted, watch } from 'vue'
+import { onMounted } from 'vue'
 import NavBar from './personal/navBar.vue'
 import Footers from './personal/footer.vue'
 import Slider from './personal/slider.vue'
@@ -29,7 +35,17 @@ import { useMallStore } from '@/store/mall'
 import BackTop from '@/components/BackTop'
 
 // 不展示侧边栏名单
-const whiteList = ['/login', '/privacyPolicy', '/userAgreement', '/register', '/recruit/personal/advertisement/introduce', '/contactService']
+const whiteList = [
+  '/login', 
+  '/privacyPolicy', 
+  '/userAgreement', 
+  '/register', 
+  '/recruit/personal/advertisement/introduce', 
+  '/contactService',
+  '/headhunting',
+  '/headhunting/service',
+  '/headhunting/service/details'
+]
 // 不展示页脚白名单
 const footerWhiteList = [
   '/recruit/personal/message',

+ 12 - 4
src/router/modules/components/recruit/enterprise.js

@@ -429,13 +429,21 @@ const enterprise = [
         component: () => import('@/views/recruit/enterprise/newTalentMap/newlyAppointed/index.vue')
       },
       {
-        path: '/recruit/enterprise/talentMap/talentMatching',
+        path: '/recruit/enterprise/talentMap/search',
         meta: {
-          title:'骐骥之才',
-          enName: 'Talent matching'
+          title: '人才搜索',
+          enName: 'Talent Search'
         },
-        component: () => import('@/views/recruit/enterprise/newTalentMap/talentMatching/index.vue')
+        component: () => import('@/views/recruit/enterprise/newTalentMap/search/index.vue')
       },
+      // {
+      //   path: '/recruit/enterprise/talentMap/talentMatching',
+      //   meta: {
+      //     title:'骐骥之才',
+      //     enName: 'Talent matching'
+      //   },
+      //   component: () => import('@/views/recruit/enterprise/newTalentMap/talentMatching/index.vue')
+      // },
       {
         path: '/recruit/enterprise/talentMap/tagManagement',
         meta: {

+ 1 - 0
src/styles/personal/navBar.css

@@ -55,6 +55,7 @@
   position: absolute;
   left: 20px;
   margin-bottom: 1px;
+  cursor: pointer;
 }
 
 .active-route {

+ 1 - 1
src/styles/personal/navBar.min.css

@@ -1 +1 @@
-@media (max-width: 1425px){.menuList{font-size:13px}.menuList>div{width:85px !important}}@media (max-width: 1480px){.menuList{margin-left:120px}}.vipBox .avatar{border:1px solid #b29701}.vipBox .avatar:hover{border:2px solid var(--v-primary-base)}.vipBox .userName{font-family:'MiSans-Bold';background:-webkit-linear-gradient(45deg, #333, #b29701);background:linear-gradient(45deg, #333, #b29701);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}.banner{width:100%;height:50px;z-index:var(--zIndex-nav) !important;color:#333;background-color:#fff;border-bottom:1px solid #e8e6e6;padding-left:0px;font-size:15px}.innerBox{position:relative;width:100vw;min-width:1180px;margin:0 auto;align-items:center;height:50px;line-height:50px}.nav-logo{position:absolute;left:20px;margin-bottom:1px}.active-route{color:#00B760;font-weight:700;border-bottom:4px solid #00B760;background-color:#e5f8ef;height:50px}.menuList{display:flex;align-items:center;height:50px}.menuList div{width:110px;text-align:center}.menuList-first-title:hover{color:#00B760;font-weight:700}:deep(.v-toolbar__content){height:50px !important}:deep(.v-chip.v-chip--size-small){height:30px !important}
+@media (max-width: 1425px){.menuList{font-size:13px}.menuList>div{width:85px !important}}@media (max-width: 1480px){.menuList{margin-left:120px}}.vipBox .avatar{border:1px solid #b29701}.vipBox .avatar:hover{border:2px solid var(--v-primary-base)}.vipBox .userName{font-family:'MiSans-Bold';background:-webkit-linear-gradient(45deg, #333, #b29701);background:linear-gradient(45deg, #333, #b29701);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}.banner{width:100%;height:50px;z-index:var(--zIndex-nav) !important;color:#333;background-color:#fff;border-bottom:1px solid #e8e6e6;padding-left:0px;font-size:15px}.innerBox{position:relative;width:100vw;min-width:1180px;margin:0 auto;align-items:center;height:50px;line-height:50px}.nav-logo{position:absolute;left:20px;margin-bottom:1px;cursor:pointer}.active-route{color:#00B760;font-weight:700;border-bottom:4px solid #00B760;background-color:#e5f8ef;height:50px}.menuList{display:flex;align-items:center;height:50px}.menuList div{width:110px;text-align:center}.menuList-first-title:hover{color:#00B760;font-weight:700}:deep(.v-toolbar__content){height:50px !important}:deep(.v-chip.v-chip--size-small){height:30px !important}

+ 1 - 0
src/styles/personal/navBar.scss

@@ -54,6 +54,7 @@
   position: absolute;
   left: 20px;
   margin-bottom: 1px;
+  cursor: pointer;
 }
 .active-route {
   color: #00B760;

+ 18 - 10
src/views/headhunting/components/nav.vue

@@ -19,6 +19,8 @@
   <CtDialog :visible="showDialog" titleClass="text-h6" :footer="true" :widthType="2" title="联系我们" @submit="handleSubmit" @close="handleClose">
     <CtForm ref="formPageRef" :items="formItems"></CtForm>
   </CtDialog>
+
+  <Loading :visible="loading" />
 </template>
 
 <script setup>
@@ -78,19 +80,25 @@ const handleClose = () => {
   formItems.value.options.forEach(e => e.value = '')
 }
 
+const loading = ref(false)
 const handleSubmit = async () => {
   const { valid } = await formPageRef.value.formRef.validate()
   if (!valid) return
-  const obj = {}
-  formItems.value.options.forEach(e => obj[e.key] = e.value)
-  await huntSubmit(obj)
-  handleClose()
-  Curtain('message', {
-    message: '提交成功,我们会尽快与您联系',
-    name: 'submit',
-    color: '#00B760',
-    iconFontSize: 300
-  })
+  loading.value = true
+  try {
+    const obj = {}
+    formItems.value.options.forEach(e => obj[e.key] = e.value)
+    await huntSubmit(obj)
+    handleClose()
+    Curtain('message', {
+      message: '提交成功,我们会尽快与您联系',
+      name: 'submit',
+      color: '#00B760',
+      iconFontSize: 300
+    })
+  } finally {
+    loading.value = false
+  }
 }
 </script>
 

+ 13 - 5
src/views/mall/home/pointExchange/index.vue

@@ -40,6 +40,8 @@
       积分不足,快去赚取积分吧
     </div>
   </CtDialog>
+
+  <Loading :visible="loading" />
 </template>
 
 <script setup>
@@ -147,6 +149,7 @@ const handleShowDetail = (item) =>{
 }
 
 // 兑换提交
+const loading = ref(false)
 const handleSubmit = async () =>{
   const { valid } = await CtFormRef.value.formRef.validate()
   if (!valid) return
@@ -161,11 +164,16 @@ const handleSubmit = async () =>{
   delete obj.address 
   if (!obj.contactName) obj.contactName = localStorage.getItem('baseInfo') ? JSON.parse(localStorage.getItem('baseInfo')).name : '--'
 
-  await redeemSubmit(obj)
-  Snackbar.success('提交成功')
-  showDetail.value = false
-  detailItem.value = {}
-  await useUserStore().getUserAccountInfo()
+  loading.value = true
+  try {
+    await redeemSubmit(obj)
+    Snackbar.success('提交成功')
+    showDetail.value = false
+    detailItem.value = {}
+    await useUserStore().getUserAccountInfo()
+  } finally {
+    loading.value = false
+  }
 }
 
 // 积分兑换记录、积分明细

+ 14 - 5
src/views/mall1/exchange.vue

@@ -25,6 +25,8 @@
     </div>
     <div v-if="point < detailItem.point" class="text-end color-error font-size-14 cursor-pointer text-decoration-underline" @click="emit('toTaskCenter')">积分不足,快去赚取积分吧</div>
   </Dialog>
+
+  <Loading :visible="loading" />
 </template>
 
 <script setup>
@@ -127,6 +129,7 @@ const handleShowDetail = (item) =>{
 }
 
 // 兑换提交
+const loading = ref(false)
 const handleSubmit = async () =>{
   const { valid } = await CtFormRef.value.formRef.validate()
   if (!valid) return
@@ -140,11 +143,17 @@ const handleSubmit = async () =>{
   if (obj.type) obj.contactAddress = obj.address.join('') + obj.contactAddress
   delete obj.address 
   if (!obj.contactName) obj.contactName = localStorage.getItem('baseInfo') ? JSON.parse(localStorage.getItem('baseInfo')).name : '--'
-  await redeemSubmit(obj)
-  Snackbar.success('提交成功')
-  showDetail.value = false
-  detailItem.value = {}
-  await useUserStore().getUserAccountInfo()
+
+  loading.value = true
+  try {
+    await redeemSubmit(obj)
+    Snackbar.success('提交成功')
+    showDetail.value = false
+    detailItem.value = {}
+    await useUserStore().getUserAccountInfo()
+  } finally {
+    loading.value = false
+  }
 }
 </script>
 

+ 2 - 2
src/views/recruit/components/message/components/chatting.vue

@@ -246,7 +246,7 @@ import { useUserStore } from '@/store/user'
 const isEnterprise = inject('isEnterprise')
 const { t } = useI18n()
 
-const emits = defineEmits(['handleMore', 'handleSend', 'handleAgree', 'handleRefuse'])
+const emits = defineEmits(['handleMore', 'handleSend', 'handleAgree', 'handleRefuse', 'handleSendResume', 'handlePreview'])
 
 const props = defineProps({
   items: {
@@ -389,7 +389,7 @@ const handleRefuse = (val) => {
 
 // 跳转个人中心-面试
 const handleToCenter = () => {
-  router.push({ path: '/recruit/personal/personalCenter', query: { showInterviewScheduleMore: true } })
+  router.push({ path: '/recruit/personal/personalCenter' })
 }
 
 // 简历预览

+ 74 - 29
src/views/recruit/components/message/index.vue

@@ -159,7 +159,7 @@
     <CtForm v-if="showSelectPosition" ref="requestFromRef" :items="requestFormItems"></CtForm>
   </CtDialog>
   <!-- 发送简历-选择要发送的职位 -->
-  <CtDialog :visible="openPositionSelectDialog" :widthType="2" titleClass="text-h6" title="请选择要投递的职位" @close="openPositionSelectDialog = false" @submit="selectPositionSubmit">
+  <CtDialog :visible="openPositionSelectDialog" :widthType="2" titleClass="text-h6" submitText="确认" title="请选择要投递的职位" @close="openPositionSelectDialog = false" @submit="selectPositionSubmit">
     <div style="position: relative; min-height: 200px">
       <v-radio-group v-model="selectJobId">
         <div v-for="val in entPositionList" :key="val.value" class="d-flex align-center radioBox" >
@@ -648,8 +648,15 @@ const handleSubmitAttachment = async () => {
   }
   if (isStudent.value && (!practice?.practiceStartTime || !practice?.practiceEndTime)) return Snackbar.warning('请完善实习到岗信息')
 
+  pageLoading.value = true
+
   // 保存附件
-  await savePersonResumeCv(obj)
+  try {
+    await savePersonResumeCv(obj)
+  } catch {
+    pageLoading.value = false
+    return
+  }
 
   // 简历投递至简历库
   if (isEmployment.value !== '-1') {
@@ -660,7 +667,12 @@ const handleSubmitAttachment = async () => {
     }
     // 如果是学生则需要带上实习信息
     if (practice && Object.keys(practice).length > 0) params = Object.assign(params, practice)
-    await jobCvRelHireSend(params)
+    try {
+      await jobCvRelHireSend(params)
+    } catch {
+      pageLoading.value = false
+      return
+    }
   } else {
     const jobId = enRequestPositionInfo.value && enRequestPositionInfo.value?.id ? enRequestPositionInfo.value?.id : positionInfo.value.id || selectJobId.value
     const type = (enRequestPositionInfo.value && Object.keys(enRequestPositionInfo.value).length ? enRequestPositionInfo.value.hire : positionInfo.value.hire) ? 1 : 0
@@ -672,10 +684,16 @@ const handleSubmitAttachment = async () => {
     }
     // 如果是学生则需要带上实习信息
     if (practice && Object.keys(practice).length > 0) params = Object.assign(params, practice)
-    await jobCvRelSend(params)
+    try {
+      await jobCvRelSend(params)
+    } catch {
+      pageLoading.value = false
+      return
+    }
   }
   handleChangeSendResumeStatus(true)
   showUploadDialog.value = false
+  pageLoading.value = false
 
   const text = {
     remark: '发送简历',
@@ -706,6 +724,8 @@ async function handleSubmitResume () {
   }
   if (isStudent.value && (!practice?.practiceStartTime || !practice?.practiceEndTime)) return Snackbar.warning('请完善实习到岗信息')
 
+  pageLoading.value = true
+
   // 简历投递至简历库
   if (isEmployment.value !== '-1') {
     let params = {
@@ -715,7 +735,12 @@ async function handleSubmitResume () {
     }
     // 如果是学生则需要带上实习信息
     if (practice && Object.keys(practice).length > 0) params = Object.assign(params, practice)
-    await jobCvRelHireSend(params)
+    try {
+      await jobCvRelHireSend(params)
+    } catch {
+      pageLoading.value = false
+      return
+    }
   } else {
     const jobId = enRequestPositionInfo.value && enRequestPositionInfo.value?.id ? enRequestPositionInfo.value?.id : positionInfo.value.id || selectJobId.value
     const type = (enRequestPositionInfo.value && Object.keys(enRequestPositionInfo.value).length ? enRequestPositionInfo.value.hire : positionInfo.value.hire) ? 1 : 0
@@ -728,10 +753,16 @@ async function handleSubmitResume () {
     // 如果是学生则需要带上实习信息
     if (practice && Object.keys(practice).length > 0) params = Object.assign(params, practice)
 
-    await jobCvRelSend(params)
+    try {
+      await jobCvRelSend(params)
+    } catch {
+      pageLoading.value = false
+      return
+    }
   }
   handleChangeSendResumeStatus(true)
   showResume.value = false
+  pageLoading.value = false
 
   const text = {
     remark: '发送简历',
@@ -774,6 +805,7 @@ const handleGetMore = async () => {
   }
 }
 
+// 删除最近联系人
 const handleDelete = async ({ channel }) => {
   await deleteConversations(channel, entBaseInfo?.enterpriseId)
   await updateConversation()
@@ -824,15 +856,21 @@ const handleSubmit = async () => {
     Snackbar.warning('时间不能为空')
     return
   }
-  query.userId = info.value.userId
-  query.positionInfo = positionList.value.find(e => e.value === query.jobId)
-  // 需要id
-  const data = await saveInterviewInvite(query)
-  // 保留邀请id
-  query.id = data.id
-  showTip.value = true
-  send(JSON.stringify(query), channelItem.value, 101)
-  showInvite.value = false
+
+  pageLoading.value = true
+  try {
+    query.userId = info.value.userId
+    query.positionInfo = positionList.value.find(e => e.value === query.jobId)
+    // 需要id
+    const data = await saveInterviewInvite(query)
+    // 保留邀请id
+    query.id = data.id
+    showTip.value = true
+    send(JSON.stringify(query), channelItem.value, 101)
+    showInvite.value = false
+  } finally {
+    pageLoading.value = false
+  }
 }
 
 const router = useRouter()
@@ -844,23 +882,30 @@ const handleToInterviewManagement = () => {
 const handleRequestResumeSubmit = async () => {
   const { valid } = await requestFromRef.value.formRef.validate()
   if (!valid) return
-  const jobId = requestFormItems.value.options.find(e => e.key === 'jobId').value
-  const positionInfo = positionList.value.find(e => e.value === jobId)
 
-  const text = {
-    remark: '求简历',
-    query: {
-      src: '',
-      title: '',
-      id: '',
-      positionInfo
-    },
-    type: 2
+  pageLoading.value = true
+  try {
+    const jobId = requestFormItems.value.options.find(e => e.key === 'jobId').value
+    const positionInfo = positionList.value.find(e => e.value === jobId)
+
+    const text = {
+      remark: '求简历',
+      query: {
+        src: '',
+        title: '',
+        id: '',
+        positionInfo
+      },
+      type: 2
+    }
+    send (JSON.stringify(text), channelItem.value, 105)
+  } finally {
+    showSelectPosition.value = false
+    pageLoading.value = false
   }
-  send (JSON.stringify(text), channelItem.value, 105)
-  showSelectPosition.value = false
 }
 
+// 接受面试邀请
 const handleAgree = (val) => {
   if (!val.id) return
   const query = {
@@ -884,7 +929,7 @@ const handleAgree = (val) => {
 // 拒绝面试邀请
 const handleRefuse = (val) => {
   if (!val.id) return
-  Confirm(t('common.confirmTitle'), '是否确定拒绝此面试邀请?').then(async () => {
+  Confirm(t('common.confirmTitle'), '是否确定拒绝此面试邀请?').then(async () => {
     await userInterviewInviteReject(val.id)
     Snackbar.success(t('common.operationSuccessful'))
     getInterviewInviteList()

+ 12 - 5
src/views/recruit/enterprise/entInfoSetting/informationSettingsComponents/basicInfo.vue

@@ -20,7 +20,7 @@
       </template>
     </CtForm>
     <div class="text-center">
-      <v-btn color="primary" class="buttons mt-3 mb-10" @click.stop="handleSave">{{ $t('common.save') }}</v-btn>
+      <v-btn color="primary" class="buttons mt-3 mb-10" :loading="loading" @click.stop="handleSave">{{ $t('common.save') }}</v-btn>
     </div>
   </div>
 </template>
@@ -37,6 +37,7 @@ import { useUserStore } from '@/store/user'
 const emit = defineEmits(['complete', 'preview'])
 
 const { t } = useI18n()
+const loading = ref(false)
 const CtFormRef = ref()
 const query = reactive({})
 const user = useUserStore()
@@ -190,14 +191,20 @@ const handleIndustry = (list, arr) => {
 const handleSave = async () => {
   const { valid } = await CtFormRef.value.formRef.validate()
   if (!valid) return
+  loading.value = true
   formItems.value.options.forEach(e => {
     if (e.noParam) return
     query[e.key] = e.value
   })
-  await updateEnterpriseBaseInfo(query)
-  Snackbar.success(t('common.saveMsg'))
-  await user.getEnterpriseInfo()
-  getBaseInfo()
+
+  try {
+    await updateEnterpriseBaseInfo(query)
+    Snackbar.success(t('common.saveMsg'))
+    await user.getEnterpriseInfo()
+    getBaseInfo()
+  } finally {
+    loading.value = false
+  }
 }
 </script>
 

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

@@ -27,7 +27,7 @@
     </CtForm>
     <Loading :visible="loading"></Loading>
     <div class="text-center">
-      <v-btn color="primary" class="buttons mt-3 mb-10" @click="handleSave">{{ $t('common.save') }}</v-btn>
+      <v-btn color="primary" class="buttons mt-3 mb-10" :loading="formLoading" @click="handleSave">{{ $t('common.save') }}</v-btn>
     </div>
   </div>
 </template>
@@ -48,6 +48,7 @@ const { t } = useI18n()
 let licenseUrl = ref('')
 // 图片预览
 const loading = ref(false)
+const formLoading = ref(false)
 
 const formItems = ref({
   options: [
@@ -191,16 +192,21 @@ const query = reactive({})
 const handleSave = async () => {
   const { valid } = await CtFormRef.value.formRef.validate()
   if (!valid) return
+  formLoading.value = true
   formItems.value.options.forEach(e => {
     if (e.noParam) return
     query[e.key] = e.value
   })
   query.businessUrl = licenseUrl.value
   if (!query.establishmentTime) return Snackbar.warning('请选择成立时间')
-  await saveEnterpriseBusiness(query)
-  Snackbar.success('编辑成功')
-  isUpdate = true
-  getBaseInfo()
+  try {
+    await saveEnterpriseBusiness(query)
+    Snackbar.success('编辑成功')
+    isUpdate = true
+    getBaseInfo()
+  } finally {
+    formLoading.value = false
+  }
 }
 
 let isUpdate = false // 是否同时更新基本信息

+ 33 - 13
src/views/recruit/enterprise/interviewManagement/components/item.vue

@@ -84,6 +84,7 @@
     <TextArea v-else v-model="query.reason" :item="textItems2"></TextArea>
   </CtDialog>
 
+  <Loading :visible="loading" />
 </template>
 
 <script setup>
@@ -106,6 +107,7 @@ defineProps({
 const emit = defineEmits(['refresh', 'action'])
 
 const { t } = useI18n()
+const loading = ref(false)
 const editStatus = ['0'] // 修改面试状态
 const againStatus = ['98', '99'] // 重新邀约状态
 const actions = ref([
@@ -206,10 +208,16 @@ const handleEditClose = () => {
 const handleEditSubmit = async () => {
   const query = inviteRef.value.getQuery()
   if (!Object.keys(query).length) return
-  await saveInterviewInvite(query)
-  Snackbar.success(t('common.operationSuccessful'))
-  handleEditClose()
-  emit('refresh')
+
+  loading.value = true
+  try {
+    await saveInterviewInvite(query)
+    Snackbar.success(t('common.operationSuccessful'))
+    handleEditClose()
+    emit('refresh')
+  } finally {
+    loading.value = false
+  }
 }
 
 // 取消面试
@@ -223,10 +231,16 @@ const handleCancelClose = () => {
 
 const handleCancelSubmit = async () => {
   if (!cancelQuery.value.reason) return Snackbar.warning('请填写取消原因')
-  await cancelInterviewInvite(cancelQuery.value)
-  Snackbar.success(t('common.operationSuccessful'))
-  handleCancelClose()
-  emit('refresh')
+  
+  loading.value = true
+  try {
+    await cancelInterviewInvite(cancelQuery.value)
+    Snackbar.success(t('common.operationSuccessful'))
+    handleCancelClose()
+    emit('refresh')
+  } finally {
+    loading.value = false
+  }
 }
 
 // 爽约、反馈
@@ -238,11 +252,17 @@ const handleClose = () => {
 const handleSubmit = async () => {
   const key = currentAction.value === 'feedback' ? 'evaluate' : 'reason'
   if (!query.value[key]) return Snackbar.warning('请填写您的' + (currentAction.value === 'feedback' ? '反馈' : '爽约原因'))
-  const api = currentAction.value === 'feedback' ? feedbackInterviewInvite : noAttendInterviewInvite
-  await api(query.value)
-  Snackbar.success(t('common.operationSuccessful'))
-  emit('refresh')
-  handleClose()
+  
+  loading.value = true
+  try {
+    const api = currentAction.value === 'feedback' ? feedbackInterviewInvite : noAttendInterviewInvite
+    await api(query.value)
+    Snackbar.success(t('common.operationSuccessful'))
+    emit('refresh')
+    handleClose()
+  } finally {
+    loading.value = false
+  }
 }
 </script>
 

+ 14 - 5
src/views/recruit/enterprise/invoiceManagement/index.vue

@@ -38,6 +38,8 @@
       </template>
     </CtForm>
   </CtDialog>
+
+  <Loading :visible="loading" />
 </template>
 
 <script setup>
@@ -278,6 +280,7 @@ const handleClose = () => {
   editId.value = null
 }
 
+const loading = ref(false)
 const { userId } = JSON.parse(localStorage.getItem('accountInfo'))
 const handleSubmit = async () => {
   const { valid } = await CtFormRef.value.formRef.validate()
@@ -290,11 +293,17 @@ const handleSubmit = async () => {
   })
   if (obj.type === undefined || obj.type === null || obj.type === '') return Snackbar.warning('请选择发票类型')
   if (editId.value) obj.id = editId.value
-  const api = editId.value ? updateInvoiceTitle : createInvoiceTitle
-  await api(obj)
-  Snackbar.success(editId.value ? t('common.editSuccessMsg') : t('common.addMsg'))
-  handleClose()
-  getList()
+
+  loading.value = true
+  try {
+    const api = editId.value ? updateInvoiceTitle : createInvoiceTitle
+    await api(obj)
+    Snackbar.success(editId.value ? t('common.editSuccessMsg') : t('common.addMsg'))
+    handleClose()
+    getList()
+  } finally {
+    loading.value = false
+  }
 }
 </script>
 

+ 13 - 5
src/views/recruit/enterprise/jobFair/details.vue

@@ -108,6 +108,8 @@
     </div>
   </v-card>
 
+  <Loading :visible="entryLoading" />
+
   <PreviewImage v-if="showPreview" :urlList="[previewSrc]" :fileName="enterpriseName" @close="showPreview = !showPreview, showShare = false" />
 </template>
 
@@ -183,12 +185,18 @@ const handleChangePage = (index) => {
 }
 
 // 添加至招聘会
+const entryLoading = ref(false)
 const handleTo = async (val) => {
-  await joinJobFairPosition({ jobFairId: id, jobId: val.id })
-  Snackbar.success('加入成功')
-  showDrawer.value = false
-  getJobNum()
-  getJobList()
+  entryLoading.value = true
+  try {
+    await joinJobFairPosition({ jobFairId: id, jobId: val.id })
+    Snackbar.success('加入成功')
+    showDrawer.value = false
+    getJobNum()
+    getJobList()
+  } finally {
+    entryLoading.value = false
+  }
 }
 
 // 获取职位列表

+ 160 - 166
src/views/recruit/enterprise/newTalentMap/labeling/index.vue

@@ -1,6 +1,6 @@
 <template>
 	<div>
-		<CtFilter :items="formItems" @reset="handleReset" @search="handleSearch" />
+		<CtFilter :items="filterItems" @reset="handleReset" @search="handleSearch" />
 
 		<v-card elevation="5" class="mt-3">
 			<CtTable
@@ -11,147 +11,123 @@
 				:disable-sort="true"
 				:elevation="0"
 				:isTools="false"
-				height="calc(100vh - 400px)"
-				:showPage="true"
+				:items-per-page="-1"
+				height="calc(100vh - 150px)"
 				:showFixedLastItem="true"
-				:total="total"
-				:pageInfo="query"
 				itemKey="id"
-				@pageHandleChange="handleChangePage"
 			>
+				<template #status="{ item }">
+					<v-chip size="small" variant="elevated" :color="item.status === 'active' ? 'primary' : 'error'">{{ item.status === 'active' ? '已启用' : '已禁用' }}</v-chip>
+				</template>
 				<template #actions="{ item }">
 					<v-btn variant="text" color="primary" @click.stop="handleAnnotation(item)">标注</v-btn>
-					<v-btn variant="text" color="error" @click.stop="handleDisabled(item)">禁用</v-btn>
-					<v-btn variant="text" color="#008970" @click.stop="handleEnable(item)">启用</v-btn>
-					<v-btn variant="text" color="error" @click.stop="handleDelete(item)">删除</v-btn>
+					<v-btn v-if="item.status === 'active'" variant="text" color="warning" @click.stop="handleAction(item.id, 'inactive')">禁用</v-btn>
+					<v-btn v-if="item.status === 'inactive'" variant="text" color="#007cd6" @click.stop="handleAction(item.id, 'active')">启用</v-btn>
+					<!-- <v-btn variant="text" color="error" @click.stop="handleDelete(item)">删除</v-btn> -->
 				</template>
 			</CtTable>
 		</v-card>
 
-		<!-- 无权限提示 -->
-		<CtDialog :visible="showDialog" :widthType="4" :footer="false" titleClass="text-h6" title="系统提示" @close="showDialog = false">
-			<div class="d-flex align-center flex-column">
-				<div class="color-warning">
-					<p>很抱歉,您当前没有权限查看门墩儿新任命的相关信息</p>
-					<p>请用微信扫描下方企业微信联系门墩儿管理员开通权限</p>
-				</div>
-				<div style="width: 150px; height: 150px;">
-					<v-img src="https://minio.menduner.com/dev/menduner/contact.png"></v-img>
-				</div>
-				<div class="text-center ml-5">潘青海先生(Peter Pan)</div>
-			</div>
-		</CtDialog>
-
 		<!-- 标注 -->
-		<CtDialog :visible="showAnnotationDialog" title="人才标注" :footer="false" widthType="1" @close="showAnnotationDialog = false">
+		<CtDialog :visible="showDialog" title="人才标注" :footer="false" widthType="1" @close="showDialog = false">
 			<v-row>
 				<v-col cols="4">
 					<v-card elevation="3" class="pa-3" style="height: 100%">
-						<p>名片</p>
-						<p>门墩儿新任命</p>
+						<p :class="{'active': previewUrl}" @click="handlePreview">
+							名片
+							<v-icon v-if="previewUrl">mdi-magnify-expand</v-icon>
+						</p>
+						<img v-if="previewUrl" width="100%" :src="previewUrl" />
+						<p class="mb-3" :class="{'mt-3': !previewUrl}">门墩儿新任命</p>
 						<p>门墩儿用户简历</p>
 					</v-card>
 				</v-col>
 				<v-col cols="8">
-					<v-card elevation="3" class="pa-3">
+					<v-card elevation="3" class="pa-3" height="100%">
 						<div class="base-info pa-3">
+							<div class="mb-6">
+								{{ talentItem.name_zh }}
+								<span v-if="talentItem.name_en">({{ talentItem.name_en }})</span>
+							</div>
 							<div class="d-flex align-center">
-								<div v-for="(val, index) in talentInfoKeys" :key="index" class="common-width">
+								<div v-for="(val, index) in talentInfoKeys" :key="index" class="common-width info-item">
 									<p v-ellipse-tooltip>{{ talentItem[val.key] }}</p>
 									<p v-ellipse-tooltip>{{ talentItem[val.value] }}</p>
 								</div>
 							</div>
 							<div class="d-flex align-center my-6">
-								<div class="common-width">{{ talentItem.phone }}</div>
-								<div class="common-width">{{ talentItem.email }}</div>
+								<div class="common-width pr-3" v-ellipse-tooltip>{{ talentItem.mobile }}</div>
+								<div style="flex: 1;">{{ talentItem.email }}</div>
 							</div>
-							<div>{{ talentItem.address }}</div>
+							<div>{{ talentItem.address_zh }}</div>
 						</div>
 						<div class="my-5">
-							<p>自动标注</p>
+							<p>人才标签</p>
 							<v-chip
-								v-for="(item, index) in talentItem.automaticDimensionTags" :key="index"
-								class="chip mx-2 mt-4"
-								label color="primary"
-							>
-								{{ item }}
-								<v-icon size="18" color="primary" style="margin-left: 6px;" @click="closeClick(index, 'automaticDimensionTags')">mdi-close-circle</v-icon>
-							</v-chip>
-						</div>
-						<div>
-							<p>人工标注</p>
-							<v-chip
-								v-for="(item, index) in talentItem.manualAnnotationTags" :key="index"
+								v-for="(item, index) in talentSelectedTags" :key="index"
 								class="chip mx-2 mt-4 cursor-pointer"
 								label color="primary"
 							>
-								{{ item }}
-								<v-icon size="18" color="primary" style="margin-left: 6px;" @click="closeClick(index, 'manualAnnotationTags')">mdi-close-circle</v-icon>
+								{{ item.name }}
+								<v-icon size="18" color="primary" style="margin-left: 6px;" @click="closeClick(item)">mdi-close-circle</v-icon>
 							</v-chip>
 
-							<div class="mt-5">
+							<div :class="{'mt-5': talentSelectedTags?.length > 0}">
 								<v-chip
 									v-for="(item, index) in tagList" :key="index"
 									class="chip mx-2 mt-4 cursor-pointer"
-									label color="primary"
-									:disabled="talentItem.manualAnnotationTags.includes(item)"
+									label color="#248dbb"
+									:disabled="talentSelectedTags.includes(item)"
 									@click="handleAdd(item)"
 								>
 									<v-icon icon="mdi-plus" start></v-icon>
-									{{ item }}
+									{{ item.name }}
 								</v-chip>
 							</div>
 						</div>
-						<v-divider class="mt-5"></v-divider>
+						<v-divider></v-divider>
 						<div class="d-flex justify-space-evenly my-5">
-							<v-btn variant="outlined" width="120" color="primary" elevation="5" @click="handleClose">取 消</v-btn>
-							<v-btn width="120" color="primary" elevation="5" @click="handleSave">保 存</v-btn>
+							<v-btn width="120" color="warning" elevation="5" @click="handleClose">取 消</v-btn>
+							<v-btn width="120" color="primary" elevation="5" @click="handleUpdate">更 新</v-btn>
 						</div>
 					</v-card>
 				</v-col>
 			</v-row>
 		</CtDialog>
 	</div>
+
+	<Loading :visible="annotationLoading"></Loading>
+
+	<PreviewImage v-if="showPreview" :initialIndex="0" :urlList="[previewUrl]" @close="showPreview = !showPreview" />
 </template>
 
 <script setup>
 defineOptions({ name: 'NewTalentMapAnnotation' })
 import { ref } from 'vue'
-import { getNewAppointmentsPage } from '@/api/recruit/enterprise/newlyAppointed'
-import { useUserStore } from '@/store/user'
+import { getCardList, updateTalentStatus, getTalentCardByImagePath, getTalentTagById, updateTalentTags } from '@/api/recruit/enterprise/talentMap/labeling'
+import { getTalentTagList } from '@/api/recruit/enterprise/talentMap/tag'
 import Snackbar from '@/plugins/snackbar'
 import Confirm from '@/plugins/confirm'
 import { useI18n } from '@/hooks/web/useI18n'
-import { timesTampChange } from '@/utils/date'
 
 const { t } = useI18n()
-const store = useUserStore()
 const loading = ref(false)
 const showDialog = ref(false)
-const showAnnotationDialog = ref(false)
-const total = ref(10)
-const query = ref({
-  pageSize: 10,
-	pageNo: 1
-})
 const items = ref([])
 const headers = [
-  { title: '姓名', key: 'nameChinese', sortable: false },
-  { title: '职位', key: 'position', sortable: false },
-  { title: '酒店', key: 'inaugurationHotel', sortable: false },
-  { title: '标签数量', key: 'tagNum', sortable: false },
-  { title: '人才状态', key: 'tagNum', sortable: false },
-  { title: '创建时间', key: 'createTime', sortable: false, value: item => timesTampChange(item.createTime) },
-  { title: '创建人', key: 'createUser', sortable: false },
-  { title: '修改时间', key: 'updateTime', sortable: false, value: item => timesTampChange(item.updateTime) },
-  { title: '修改人', key: 'updateUser', sortable: false },
+  { title: '姓名', key: 'name_zh', sortable: false },
+  // { title: '英文名', key: 'name_en', sortable: false },
+  { title: '职位', key: 'title_zh', sortable: false },
+  { title: '酒店', key: 'hotel_zh', sortable: false },
+  { title: '人才状态', key: 'status', sortable: false },
+  { title: '创建时间', key: 'created_at', sortable: false },
   { title: '操作', key: 'actions', sortable: false, align: 'center' }
 ]
-const formItems = ref({
+const filterItems = ref({
   options: [
-		{
+    {
       type: 'text',
-      key: 'position',
+      key: 'title_zh',
       value: '',
       label: '职位',
 			clearable: true,
@@ -160,7 +136,7 @@ const formItems = ref({
     },
 		{
       type: 'text',
-      key: 'inaugurationHotel',
+      key: 'hotel_zh',
       value: '',
       label: '酒店',
 			clearable: true,
@@ -169,9 +145,9 @@ const formItems = ref({
     },
 		{
       type: 'text',
-      key: 'nameChinese',
+      key: 'name_zh',
       value: '',
-      label: '名',
+      label: '中文名',
 			clearable: true,
 			hideDetails: true,
 			width: 200
@@ -179,127 +155,132 @@ const formItems = ref({
   ]
 })
 
-// 获取企业权益信息
-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 getList = async () => {
 	loading.value = true
 	try {
-		const result = await getNewAppointmentsPage(query.value)
-		items.value = result.list
-		total.value = result.total
+		const data = await getCardList()
+		items.value = data || []
 	} finally {
 		loading.value = false
 	}
 }
 getList()
 
-// 分页切换
-const handleChangePage = async (e) => {
-	await store.getEnterpriseInfo(true)
-	// 没有权限提示联系门墩儿
-	if (!info.value?.entitlement?.newAppointment) {
-		showDialog.value = true
-		return
-	}
-	query.value.pageNo = e
-	getList()
-}
-
 // 搜索
 const handleSearch = async (obj) => {
-	await store.getEnterpriseInfo(true)
-	// 没有权限提示联系门墩儿
-	if (!info.value?.entitlement?.newAppointment) {
-		showDialog.value = true
-		return
-	}
-	query.value = Object.assign(query.value, obj)
-	query.value.pageNo = 1
-	getList()
+	console.log(obj, '搜索')
+	// query.value = obj
+	// getList()
 }
-
 // 重置
 const handleReset = (obj) => {
-	query.value = Object.assign(query.value, obj)
-	query.value.pageNo = 1
-	getList()
+	console.log(obj, '重置')
+	// query.value = obj
+	// getList()
 }
 
 // 标注
-const tagList = ['富强', '民主', '文明', '和谐', '自由', '平等', '公正', '法治', '爱国', '敬业', '诚信', '友善']
-const talentItem = ref({
-	name: '张三',
-	enName: 'Tom',
-	postName: '总经理',
-	enPostName: 'General Manager',
-	inaugurationHotel: '南京洲际酒店',
-	enInaugurationHotel: 'Nanjing InterContinental Hotel',
-	groupName: '洲际集团',
-	enGroupName: 'IHG',
-	phone: '13800138000',
-	email: 'zhangsan@163.com',
-	address: '南京市人民路123号',
-	automaticDimensionTags: ['富强', '民主', '文明', '和谐'],
-	manualAnnotationTags: [],
-})
+const talentItem = ref({})
 const talentInfoKeys = [
-	{ key: 'name', value: 'enName' },
-	{ key: 'postName', value: 'enPostName' },
-	{ key: 'inaugurationHotel', value: 'enInaugurationHotel' },
-	{ key: 'groupName', value: 'enGroupName' },
+	{ key: 'title_zh', value: 'title_en' },
+	{ key: 'hotel_zh', value: 'hotel_en' },
+	{ key: 'brand_zh', value: 'brand_en' },
 ]
-const handleAnnotation = (item) => {
-	console.log(item, '标注')
-	showAnnotationDialog.value = true
+
+// 获取人才标签
+const tagList = ref([])
+const getTagList = async () => {
+	annotationLoading.value = true
+	try {
+		const data = await getTalentTagList()
+		tagList.value = data || []
+	} finally {
+		annotationLoading.value = false
+	}
 }
 
-// 删除
-const closeClick = (index, dataKey) => {
-	talentItem.value[dataKey].splice(index, 1)
+// 名片预览
+const showPreview = ref(false)
+const handlePreview = () => {
+	showPreview.value = true
 }
 
-// 手动标注添加
-const handleAdd = (item) => {
-	talentItem.value.manualAnnotationTags.push(item)
+// 标注
+const talentSelectedTags = ref([])
+const previewUrl = ref(null)
+const annotationLoading = ref(false)
+const handleAnnotation = async (item) => {
+	if (!item || !item.id) return
+	
+	talentItem.value = item
+	// 获取所有标签列表
+	await getTagList()
+
+	// 获取名片预览
+	if (item.image_path) {
+		const data = await getTalentCardByImagePath(item.image_path)
+		previewUrl.value = URL.createObjectURL(data)
+	}
+
+	// 获取人才标签
+	const tagData = await getTalentTagById(item.id)
+	console.log(tagData, '人才标签')
+
+	showDialog.value = true
 }
 
-// 禁用
-const handleDisabled = (item) => {
-	console.log(item, '禁用')
-	Confirm(t('common.confirmTitle'), '是否确定禁用此人才数据?').then(async () => {
-    Snackbar.success(t('common.operationSuccessful'))
-  })
+// 标签删除
+const closeClick = (item) => {
+	const index = talentSelectedTags.value.findIndex((i) => i === item)
+	if (index !== -1) talentSelectedTags.value.splice(index, 1)
 }
 
-// 启用
-const handleEnable = (item) => {
-	console.log(item, '启用')
-	Confirm(t('common.confirmTitle'), '是否确定启用此人才数据?').then(async () => {
-    Snackbar.success(t('common.operationSuccessful'))
-  })
+// 标签添加
+const handleAdd = (item) => {
+	talentSelectedTags.value.push(item)
 }
 
-// 删除
-const handleDelete = (item) => {
-	console.log(item, '删除')
-	Confirm(t('common.confirmTitle'), '是否确定删除?').then(async () => {
-    // await deleteInvoiceTitle(id)
-    Snackbar.success(t('common.delMsg'))
-    // getList()
+// 启用、禁用
+const handleAction = (id, status) => {
+	Confirm(t('common.confirmTitle'), `是否确定${status === 'active' ? '启用' : '禁用'}?`).then(async () => {
+		try {
+			await updateTalentStatus(id, { status })
+			Snackbar.success(t('common.operationSuccessful'))
+			getList()
+		} catch {}
   })
 }
 
+// 删除
+// const handleDelete = (item) => {
+// 	console.log(item, '删除')
+// 	Confirm(t('common.confirmTitle'), '是否确定删除?').then(async () => {
+//     // await deleteInvoiceTitle(id)
+//     Snackbar.success(t('common.delMsg'))
+//     // getList()
+//   })
+// }
+
+// 更新人才标签
 const handleClose = () => {
-	showAnnotationDialog.value = false
+	showDialog.value = false
+	talentItem.value = {}
+	talentSelectedTags.value = []
+	previewUrl.value = ''
 }
 
-const handleSave = async () => {
-	handleClose()
+const handleUpdate = async () => {
+	const tags = talentSelectedTags.value.map(e => {
+		return { talent: e.id, tag: e.name }
+	})
+
+	try {
+		await updateTalentTags(tags)
+		Snackbar.success('人才标签更新成功')
+		handleClose()
+		getList()
+	} catch {}
 }
 </script>
 
@@ -309,7 +290,20 @@ const handleSave = async () => {
 	border-radius: 6px;
 }
 .common-width {
-	width: 25%;
-	max-width: 25%;
+	width: 33.3%;
+	max-width: 33.3%;
+}
+.info-item {
+	padding-right: 12px;
+	&:nth-child(3n) {
+		padding-right: 0;
+	}
+	p {
+		height: 24px;
+	}
+}
+.active {
+	color: var(--v-primary-base);
+	cursor: pointer;
 }
 </style>

+ 88 - 0
src/views/recruit/enterprise/newTalentMap/search/index.vue

@@ -0,0 +1,88 @@
+<template>
+	<div>
+		<v-card elevation="5" class="pa-10">
+			<TextInput
+        v-model="content"
+        :item="textItem"
+        @enter="handleSearch"
+        @appendInnerClick="handleSearch"
+      ></TextInput>
+		</v-card>
+
+		<v-card elevation="5" class="mt-3">
+			<CtTable
+				:items="items"
+				class="pa-3"
+				:headers="headers"
+				:loading="loading"
+				:disable-sort="true"
+				:elevation="0"
+				:isTools="false"
+				:noDataText="!content ? '请先输入您的描述信息定位人才' : '暂无数据'"
+				:items-per-page="-1"
+				height="calc(100vh - 400px)"
+				:showFixedLastItem="true"
+				itemKey="id"
+			></CtTable>
+		</v-card>
+	</div>
+
+	<Loading :visible="annotationLoading"></Loading>
+</template>
+
+<script setup>
+defineOptions({ name: 'NewTalentMapAnnotation' })
+import { ref } from 'vue'
+import { getTalentList } from '@/api/recruit/enterprise/talentMap/search'
+import Snackbar from '@/plugins/snackbar'
+
+const loading = ref(false)
+const content = ref('')
+const items = ref([])
+const headers = [
+  { title: '姓名', key: 'chinese_name', sortable: false },
+  { title: '酒店名称', key: 'hotel_chinese_name', sortable: false },
+  { title: '职位', key: 'position_chinese', sortable: false },
+  { title: '邮箱', key: 'email', sortable: false },
+  { title: '联系电话', key: 'mobile', sortable: false },
+  { title: '更新时间', key: 'talent_updated', sortable: false },
+]
+const textItem = ref({
+  type: 'text',
+  value: '',
+  label: '请输入您的描述信息定位人才',
+  placeholder: '请输入您的描述信息,例如:查找所有在上海的五星级酒店担任总经理职位的人才',
+  clearable: true,
+	hideDetails: true,
+  appendInnerIcon: 'mdi-magnify'
+})
+
+// 人才列表
+const getList = async () => {
+	loading.value = true
+	try {
+		const data = await getTalentList({ query_requirement: content.value })
+		items.value = data || []
+	} finally {
+		loading.value = false
+	}
+}
+getList()
+
+// 搜索
+const handleSearch = async () => {
+	if (!content.value) return Snackbar.warning('请输入您的描述信息定位人才')
+	getList()
+}
+</script>
+
+<style scoped lang="scss">
+.base-info {
+	background-color: #f7f8fa;
+	border-radius: 6px;
+}
+.common-width {
+	width: 25%;
+	max-width: 25%;
+}
+</style>

+ 2 - 2
src/views/recruit/enterprise/newTalentMap/tag/index.vue

@@ -28,9 +28,9 @@
           <div v-ellipse-tooltip style="max-width: 250px;">{{ item.description }}</div>
         </template>
         <template #status="{ item }">
-          <span :class="`color-${item.status === 'active' ? 'primary' : 'error'}`">
+          <v-chip :color="item.status === 'active' ? 'primary' : 'error'" variant="elevated" size="small">
             {{ item.status === 'active' ? '启用' : '禁用' }}
-          </span>
+          </v-chip>
         </template>
 				<template #actions="{ item }">
 					<v-btn variant="text" color="primary" @click.stop="handleEdit(item)">编 辑</v-btn>

+ 52 - 32
src/views/recruit/enterprise/resume/components/table.vue

@@ -76,6 +76,8 @@
     <TipDialog :visible="showTip" icon="mdi-check-circle-outline" message="面试邀请发送成功" @close="showTip = false">
       <div class="color-primary text-decoration-underline cursor-pointer" @click="handleToInterviewManagement">点击到面试中查看。</div>
     </TipDialog>
+
+    <Loading :visible="formLoading" />
   </div>
 </template>
 
@@ -89,6 +91,7 @@ import { hireJobCvRelSettlement } from '@/api/recruit/public/delivery'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useUserStore } from '@/store/user'
 import Snackbar from '@/plugins/snackbar'
+import Confirm from '@/plugins/confirm'
 import InvitePage from './invite.vue'
 import { getUserAvatar } from '@/utils/avatar'
 import { getBlob, saveAs } from '@/utils'
@@ -114,6 +117,7 @@ const badgeIcon = computed(() => (item) => {
   return (item.person && item.person.sex) ? (item.person.sex === '1' ? 'mdi-gender-male' : 'mdi-gender-female') : 'mdi-gender-female'
 })
 
+const formLoading = ref(false)
 const userStore = useUserStore()
 const inviteRef = ref()
 const showInvite = ref(false)
@@ -167,46 +171,54 @@ const handleToPersonDetail = async ({ userId, id }) => {
 // 加入人才库 
 const handleJoinToTalentPool = async (item) => {
   if (!item.userId) return Snackbar.warning('数据异常')
-  await joinToTalentPool(item.userId)
-  Snackbar.success(t('common.operationSuccessful'))
-  emit('refresh')
+  Confirm('系统提示', '是否确认将此求职者加入人才库?').then(async () => {
+    await joinToTalentPool(item.userId)
+    Snackbar.success(t('common.operationSuccessful'))
+    emit('refresh')
+  })
 }
 
 // 入职
 const handleEnterByEnterprise = async (item) => {
   if (!item.id) return
-  await personEntryByEnterprise(item.id)
-  Snackbar.success(t('common.operationSuccessful'))
-  emit('refresh')
+  Confirm('系统提示', '是否确认将此求职者操作入职?').then(async () => {
+    await personEntryByEnterprise(item.id)
+    Snackbar.success(t('common.operationSuccessful'))
+    emit('refresh')
+  })
 }
 
 // 不合适
 const handleEliminate = async (item) => {
   if (!item.id || !item?.job?.id) return
-  const query = {
-    bizId: item.id,
-    jobId: item.job.id,
-    userId: item.userId,
-    type: props.tab === 0 ? '0' : '1' // 投递简历0 已邀约1
-  }
-  // 招聘会职位则带id
-  if (item?.jobFairId) query.jobFairId = item.jobFairId
-  await joinEliminate(query)
-  Snackbar.success(t('common.operationSuccessful'))
-  emit('refresh')
+  Confirm('系统提示', '是否确认加入不合适?').then(async () => {
+    const query = {
+      bizId: item.id,
+      jobId: item.job.id,
+      userId: item.userId,
+      type: props.tab === 0 ? '0' : '1' // 投递简历0 已邀约1
+    }
+    // 招聘会职位则带id
+    if (item?.jobFairId) query.jobFairId = item.jobFairId
+    await joinEliminate(query)
+    Snackbar.success(t('common.operationSuccessful'))
+    emit('refresh')
+  })
 }
 
 // 取消不合适
 const handleCancelEliminate = async (item) => {
   if (!item.id) return
-  await personCvUnfitCancel(item.id)
-  Snackbar.success(t('common.operationSuccessful'))
-  emit('refresh')
+  Confirm('系统提示', '是否确认取消不合适?').then(async () => {
+    await personCvUnfitCancel(item.id)
+    Snackbar.success(t('common.operationSuccessful'))
+    emit('refresh')
+  })
 }
 
 // 查看简历
 const handlePreviewResume = async ({ url, id }) => {
-  if (!url || !id) return
+  if (!url || !id) return Snackbar.warning('简历不存在')
   try {
     const res = await personJobCvLook(id)
     if (res) {
@@ -244,22 +256,30 @@ const handleEditSubmit = async () => {
   if (!valid) return
   const query = inviteRef.value.getQuery()
   if (!query?.time) return Snackbar.warning('请选择面试时间')
-  await saveInterviewInvite(query)
-  showTip.value = true
-  handleEditClose()
-  emit('refresh')
+
+  formLoading.value = true
+  try {
+    await saveInterviewInvite(query)
+    showTip.value = true
+    handleEditClose()
+    emit('refresh')
+  } finally {
+    formLoading.value = false
+  }
 }
 
 // 结算
 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)
+  Confirm('系统提示', '是否确认结算?').then(async () => {
+    await hireJobCvRelSettlement(item.id)
+    Snackbar.success(t('common.operationSuccessful'))
+    emit('refresh')
+    // 更新账户信息
+    setTimeout(async () => {
+      await userStore.getEnterpriseUserAccountInfo()
+    }, 2000)
+  })
 }
 
 const handleToInterviewManagement = () => {

+ 14 - 5
src/views/recruit/enterprise/search/recommend/index.vue

@@ -51,6 +51,8 @@
   <TipDialog :visible="showTip" icon="mdi-check-circle-outline" message="面试邀请发送成功" @close="showTip = false">
     <div class="color-primary text-decoration-underline cursor-pointer" @click="handleToInterviewManagement">点击到面试中查看。</div>
   </TipDialog>
+
+  <Loading :visible="formLoading" />
 </template>
 
 <script setup>
@@ -82,6 +84,7 @@ const selectItems = ref({
 const total = ref(0)
 const items = ref([])
 const loading = ref(false)
+const formLoading = ref(false)
 const headers = ref([
   { title: '姓名', key: 'name', sortable: false },
   { title: '求职状态', key: 'jobStatusName', sortable: false },
@@ -172,11 +175,17 @@ const handleSubmit = async () => {
     Snackbar.warning('面试时间不能为空')
     return
   }
-  await saveInterviewInvite(query)
-  showInvite.value = false
-  showTip.value = true
-  getData()
-  itemData .value = {}
+
+  formLoading.value = true
+  try {
+    await saveInterviewInvite(query)
+    showInvite.value = false
+    showTip.value = true
+    getData()
+  } finally {
+    itemData .value = {}
+    formLoading.value = false
+  }
 }
 
 // 立即沟通

+ 12 - 3
src/views/recruit/enterprise/search/retrieval/index.vue

@@ -101,6 +101,8 @@
   <TipDialog :visible="showTip" icon="mdi-check-circle-outline" message="面试邀请发送成功" @close="showTip = false">
     <div class="color-primary text-decoration-underline cursor-pointer" @click="handleToInterviewManagement">点击到面试中查看。</div>
   </TipDialog>
+
+  <Loading :visible="formLoading" />
 </template>
 
 <script setup>
@@ -121,6 +123,7 @@ import { getDict } from '@/hooks/web/useDictionaries'
 import { getTimeDifferenceInChinese } from '@/utils/date'
 import { formatName } from '@/utils/getText'
 
+const formLoading = ref(false)
 const textItem = ref({
   type: 'text',
   width: 600,
@@ -321,9 +324,15 @@ const handleSubmit = async () => {
     return
   }
   delete query.id
-  await saveInterviewInvite(query)
-  showInvite.value = false
-  showTip.value = true
+  
+  formLoading.value = true
+  try {
+    await saveInterviewInvite(query)
+    showInvite.value = false
+    showTip.value = true
+  } finally {
+    formLoading.value = false
+  }
 }
 
 const handleToInterviewManagement = () => {

+ 19 - 10
src/views/recruit/enterprise/systemManagement/groupAccount/index.vue

@@ -53,7 +53,7 @@
           <template #actions="{ item }">
             <v-btn v-if="item.userType === '0'" color="primary" variant="text" @click="handleEdit(item)">编辑</v-btn>
             <v-btn v-if="item.status === '1' && item.userType !== '1'" color="primary" variant="text" @click="handleAction('', 0, item)">{{ $t('enterprise.userManagement.enable') }}</v-btn>
-            <v-btn v-if="item.status === '0' && item.userType !== '1'" color="primary" variant="text" @click="handleAction('', 1, item)">{{ $t('enterprise.userManagement.disable') }}</v-btn>
+            <v-btn v-if="item.status === '0' && item.userType !== '1'" color="error" variant="text" @click="handleAction('', 1, item)">{{ $t('enterprise.userManagement.disable') }}</v-btn>
           </template>
         </CtTable>
       </div>
@@ -84,6 +84,8 @@
     </CtForm>
   </CtDialog>
 
+  <Loading :visible="editLoading" />
+
   <ImgCropper :visible="isShowCopper" :image="selectPic" :cropBoxResizable="true" @submit="handleHideCopper" :aspectRatio="1 / 1" @close="isShowCopper = false"></ImgCropper>
 </template>
 
@@ -301,18 +303,25 @@ const handleHideCopper = (data) => {
 }
 
 // 提交
+const editLoading = ref(false)
 const handleSubmit = async () => {
   const { valid } = await CtFormRef.value.formRef.validate()
   if (!valid) return
-  const obj = {
-    id: editId.value
-  } 
-  formItems.value.options.forEach(e => { obj[e.key] = e.value })
-  await updateGroupUserAccount(obj)
-  showEdit.value = false
-  editId.value = null
-  Snackbar.success('编辑成功')
-  getUserList()
+  
+  editLoading.value = true
+  try {
+    const obj = {
+      id: editId.value
+    } 
+    formItems.value.options.forEach(e => { obj[e.key] = e.value })
+    await updateGroupUserAccount(obj)
+    showEdit.value = false
+    editId.value = null
+    Snackbar.success('编辑成功')
+    getUserList()
+  } finally {
+    editLoading.value = false
+  }
 }
 </script>
 

+ 6 - 3
src/views/recruit/enterprise/talentPool/index.vue

@@ -59,6 +59,7 @@ defineOptions({ name: 'enterprise-talent-pool'})
 import { useI18n } from '@/hooks/web/useI18n'; const { t } = useI18n()
 import { computed, reactive, ref } from 'vue'
 import Snackbar from '@/plugins/snackbar'
+import Confirm from '@/plugins/confirm'
 import FilterPage from './components/filter.vue'
 import { dealDictArrayData } from '@/utils/position'
 import { getTalentPoolPage } from '@/api/recruit/enterprise/talentPool'
@@ -92,9 +93,11 @@ getData()
 // 移出人才库 
 const handleRemove = async (item) => {
   if (!item.userId) return Snackbar.warning('数据异常')
-  await removeFormTalentPool(item.userId)
-  Snackbar.success(t('common.operationSuccessful'))
-  getData()
+  Confirm('系统提示', `是否确认将【${item.name}】移出人才库?`).then(async () => {
+    await removeFormTalentPool(item.userId)
+    Snackbar.success(t('common.operationSuccessful'))
+    getData()
+  })
 }
 
 const handleChangePage = (e) => {

+ 0 - 355
src/views/recruit/enterprise/talentPool/indexCopy.vue

@@ -1,355 +0,0 @@
-<!-- 样式备份 -->
-<template>
-  <v-card class="card-box pa-5">
-    <div class="d-flex justify-space-between">
-      <TextUI v-if="showTextUI" :item="textItem" @enter="handleEnter" @appendInnerClick="handleEnter"></TextUI>
-      <div></div>
-      <v-btn color="primary" prependIcon="mdi-filter-multiple-outline" class="half-button" variant="tonal" @click="openDrawer">筛选</v-btn>
-    </div>
-    <div v-if="items.length && showTextUI" class="d-flex align-center" style="margin-left: 14px;">
-      <v-checkbox v-model="selectAll" :label="!selectAll ? '全选' : `已选中${selectList.length}条`" hide-details color="primary" @update:model-value="handleChangeSelectAll"></v-checkbox>
-      <v-btn class="ml-8" :disabled="!selectAll" color="primary" variant="tonal" size="small">邀请面试</v-btn>
-      <v-btn class="mx-3" :disabled="!selectAll" color="primary" variant="tonal" size="small">简历回复</v-btn>
-      <v-btn :disabled="!selectAll" color="primary" variant="tonal" size="small">批量导出</v-btn>
-      <v-btn class="mx-3" :disabled="!selectAll" color="primary" variant="tonal" size="small">移除</v-btn>
-      <v-btn :disabled="!selectAll" color="primary" variant="tonal" size="small">移动到回收站</v-btn>
-      <v-btn class="ml-3" :disabled="!selectAll" color="primary" variant="tonal" size="small">加入黑名单</v-btn>
-    </div>
-    <div v-if="items.length" class="mt-3">
-      <div v-for="val in items" :key="val.id" class="list-item mb-3">
-        <div class="top">
-          <v-checkbox class="mr-5" v-model="val.select" color="primary" density="compact" hide-details @update:model-value="handleChangeSelect"></v-checkbox>
-          <span>应聘/意向职位:{{ val.job.name }}</span>
-          <span class="mx-10">加入时间:{{ val.createTime }}</span>
-          <span>人才分类:{{ val.type }}</span>
-        </div>
-        <div @click.stop="talentPoolDetails(val)" class="px-5 py-3 d-flex justify-space-between align-center cursor-pointer">
-          <div class="d-flex">
-            <v-badge
-              bordered
-              offset-x="6"
-              offset-y="44"
-              :color="val.sex ? (val.sex === '1' ? '#1867c0' : 'error') : 'error'"
-              :icon="val.sex ? (val.sex === '1' ? 'mdi-gender-male' : 'mdi-gender-female') : 'mdi-gender-female'">
-              <v-avatar size="large" :image="val.avatar || 'https://minio.citupro.com/dev/menduner/7.png'"></v-avatar>
-            </v-badge>
-            <div class="ml-5">
-              <div class="user-name">{{ val.name }}</div>
-              <div class="mt-2 user-info">
-                <span v-for="(k, i) in dict" :key="k">
-                  {{ val[k] }}
-                  <span v-if="i !== dict.length - 1" class="mx-3">|</span>
-                </span>
-              </div>
-            </div>
-          </div>
-          <div>
-            <!-- <v-btn color="primary" variant="tonal" @click.stop="{}">和TA聊聊</v-btn>
-            <v-btn class="ml-3" color="primary" @click.stop="{}">邀请面试</v-btn> -->
-            <v-btn class="ml-3" color="primary" variant="tonal" @click.stop="handleRemove(val)">移除</v-btn>
-          </div>
-        </div>
-        <div class="d-flex mx-5 bottom cursor-pointer">
-          <div class="experience" v-if="val.exp.length">
-            <div class="second-title">工作经验</div>
-            <v-timeline density="compact" align="start" side="end" truncate-line="both">
-              <v-timeline-item v-for="(j, i) in val.exp" :key="i" dot-color="primary" size="small">
-                <div class="timeline-item mt-1">
-                  <div>{{ j.startTime }}-{{ j.endTime }} ({{ j.year }})</div>
-                  <div class="timeline-item-name ellipsis">{{ j.name }}</div>
-                  <div class="timeline-item-name ellipsis">{{ j.jobName }}</div>
-                </div>
-              </v-timeline-item>
-            </v-timeline>
-          </div>
-          <div class="edu" v-if="val.edu.length">
-            <div class="second-title">教育经历</div>
-            <v-timeline density="compact" align="start" side="end" truncate-line="both">
-              <v-timeline-item v-for="(j, i) in val.edu" :key="i" dot-color="primary" size="small">
-                <div class="timeline-item mt-1">
-                  <div>{{ j.startTime }}-{{ j.endTime }}</div>
-                  <div class="timeline-item-name ellipsis">{{ j.name }}</div>
-                  <div class="timeline-item-name ellipsis">{{ j.major }}</div>
-                </div>
-              </v-timeline-item>
-            </v-timeline>
-          </div>
-        </div>
-      </div>
-      <CtPagination
-        :total="total"
-        :page="pageInfo.pageNo"
-        :limit="pageInfo.pageSize"
-        @handleChange="handleChangePage"
-      ></CtPagination>
-    </div>
-    <Empty v-else :message="tipsText" :elevation="false" class="mt-15"></Empty>
-
-    <v-navigation-drawer v-model="screen" location="right" absolute temporary width="700">
-      <FilterPage
-        ref="FilterPageRef"
-        @confirm="handleConfirm"
-        @cancel="screen = false"
-      ></FilterPage>
-    </v-navigation-drawer>
-  </v-card>
-</template>
-
-<script setup>
-// import { useRouter } from 'vue-router'
-// const router = useRouter()
-defineOptions({ name: 'enterprise-talent-pool'})
-import { useI18n } from '@/hooks/web/useI18n'; const { t } = useI18n()
-import { reactive, ref } from 'vue'
-import Snackbar from '@/plugins/snackbar'
-import TextUI from '@/components/FormUI/TextInput'
-import FilterPage from './components/filter.vue'
-import { dealDictArrayData } from '@/utils/position'
-import { getTalentPoolPage } from '@/api/recruit/enterprise/talentPool'
-import { removeFormTalentPool } from '@/api/recruit/enterprise/personnel'
-
-
-let query = {}
-const showTextUI = ref(false)
-const screen = ref(false)
-const selectAll = ref(false)
-const selectList = ref([])
-const items = ref([
-  {
-    job: {
-      name: '客房服务员'
-    },
-    createTime: '2026-11-12',
-    type: '默认分类',
-    name: '花城',
-    age: '27岁',
-    expName: '3年经验',
-    areaName: '广州',
-    userId: '1',
-    id: '1793583467288223745',
-    sex: '2',
-    select: false,
-    eduName: '本科',
-    payName: '薪资面议',
-    avatar: 'https://cdn.vuetifyjs.com/images/john.jpg',
-    exp: [
-      {
-        startTime: '2016.05',
-        endTime: '2018.05',
-        year: '2年',
-        name: '广州辞图科技有限公司',
-        jobName: '前台'
-      },
-      {
-        startTime: '2016.05',
-        endTime: '2018.05',
-        year: '2年',
-        name: '广州辞图科技有限公司',
-        jobName: '前台'
-      }
-    ],
-    edu: [
-      {
-        startTime: '2016.05',
-        endTime: '2018.05',
-        name: '广州大学',
-        major: '酒店管理'
-      }
-    ]
-  },
-  {
-    job: {
-      name: '客房服务员'
-    },
-    createTime: '2026-11-12',
-    type: '默认分类',
-    name: '花城',
-    age: '27岁',
-    expName: '3年经验',
-    areaName: '广州',
-    userId: '1',
-    id: '1793583467288223745',
-    sex: '2',
-    select: false,
-    eduName: '本科',
-    payName: '薪资面议',
-    avatar: 'https://cdn.vuetifyjs.com/images/john.jpg',
-    exp: [
-      {
-        startTime: '2016.05',
-        endTime: '2018.05',
-        year: '2年',
-        name: '广州辞图科技有限公司',
-        jobName: '前台'
-      },
-      {
-        startTime: '2016.05',
-        endTime: '2018.05',
-        year: '2年',
-        name: '广州辞图科技有限公司',
-        jobName: '前台'
-      }
-    ],
-    edu: [
-      {
-        startTime: '2016.05',
-        endTime: '2018.05',
-        name: '广州大学',
-        major: '酒店管理'
-      }
-    ]
-  }
-])
-// items.value = [] // 暂定无数据展示
-const tipsText = ref('暂无数据')
-const total = ref(2)
-const pageInfo = reactive({ pageNo: 1, pageSize: 10 })
-
-const textItem = ref({
-  type: 'text',
-  width: 600,
-  value: '',
-  label: '请输入简历姓名/职位名称',
-  appendInnerIcon: 'mdi-magnify'
-})
-const dict = ['age', 'expName', 'areaName', 'eduName', 'payName']
-
-// 获取数据
-const getData = async () => {
-  const obj = { ...pageInfo, ...query }
-  console.log('obj', obj)
-  const { list, total: number } = await getTalentPoolPage(pageInfo)
-  total.value = number
-  if (showTextUI.value) items.value = list?.length ? dealDictArrayData([], list) : []
-}
-// getData()
-
-const handleEnter = (e) => {
-  console.log(e, 'enter')
-}
-
-// 移出人才库 
-const handleRemove = async (item) => {
-  if (!item.userId) return Snackbar.warning('数据异常')
-  await removeFormTalentPool(item.userId)
-  Snackbar.success(t('common.operationSuccessful'))
-}
-
-const dealSelect = () => {
-  selectList.value = items.value.filter(e => e.select).map(k => k.id)
-}
-
-// 全选
-const handleChangeSelectAll = () => {
-  items.value.map(k => {
-    k.select = selectAll.value
-    return k
-  })
-  dealSelect()
-}
-// 单选
-const handleChangeSelect = () => {
-  const length = items.value.filter(k => k.select).length
-  selectAll.value = length > 0 ? true : false
-  dealSelect()
-}
-
-const handleChangePage = () => {
-  // 分页获取新数据后勾选
-  selectList.value.forEach(e => {
-    const obj = items.value.find(k => k.id === e)
-    if (obj) obj.select = true
-  })
-  getData()
-}
-
-// 筛选
-const handleConfirm = (params) => {
-  screen.value = false
-  pageInfo.pageNo = 1
-  query = { ...params }
-  getData()
-}
-
-const FilterPageRef = ref()
-const openDrawer = () => {
-  screen.value = true
-  if (Object.keys(query).length) FilterPageRef.value?.setValue(query)
-  else FilterPageRef.value?.resetValue()
-}
-
-
-// 人才详情
-const talentPoolDetails = ({ userId }) => {
-  if (!userId) return
-  window.open(`/recruit/enterprise/talentPool/details/${userId}`)
-}
-</script>
-
-<style scoped lang="scss">
-.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%;
-  }
-}
-.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-item__body) {
-  width: 100%;
-}
-:deep(.v-timeline--vertical.v-timeline) {
-  row-gap: 0;
-}
-</style>

+ 1 - 0
src/views/recruit/enterprise/tradingOrder/components/trading/balance.vue → src/views/recruit/enterprise/tradingOrder/components/balance.vue

@@ -17,6 +17,7 @@
 </template>
 
 <script setup>
+// M豆交易明细
 defineOptions({ name: 'trading-order-balance'})
 import { ref } from 'vue'
 import { timesTampChange } from '@/utils/date'

+ 1 - 1
src/views/recruit/enterprise/tradingOrder/components/public.vue

@@ -30,4 +30,4 @@ const list = [
 
 <style scoped lang="scss">
 
-</style>
+</style>

+ 1 - 0
src/views/recruit/enterprise/tradingOrder/components/trading/recharge.vue → src/views/recruit/enterprise/tradingOrder/components/recharge.vue

@@ -17,6 +17,7 @@
 </template>
 
 <script setup>
+// 充值订单
 defineOptions({ name: 'trading-order-recharge'})
 import { ref } from 'vue'
 import { timesTampChange } from '@/utils/date'

+ 0 - 22
src/views/recruit/enterprise/tradingOrder/components/trading.vue

@@ -1,22 +0,0 @@
-<template>
-  <div class="white-bgc pa-3 pt-3">
-    <v-tabs v-model="tab" align-tabs="start" color="primary" bg-color="#f7f8fa">
-      <v-tab value="tab_transaction">{{ '交易订单' }}</v-tab>
-      <v-tab value="tab_recharge">{{ '充值订单' }}</v-tab>
-    </v-tabs>
-    <Transaction v-if="tab === 'tab_transaction'"></Transaction>
-    <Recharge v-if="tab === 'tab_recharge'"></Recharge>
-  </div>
-</template>
-
-<script setup>
-defineOptions({name: 'enterprise-memberCenter-tradingOrder'})
-import { ref } from 'vue'
-import Transaction from './trading/transaction.vue'
-import Recharge from './trading/recharge.vue'
-
-const tab = ref('tab_transaction')
-</script>
-
-<style lang="scss" scoped>
-</style>

+ 7 - 0
src/views/recruit/enterprise/tradingOrder/components/trading/transaction.vue → src/views/recruit/enterprise/tradingOrder/components/transaction.vue

@@ -43,9 +43,12 @@
     </div>
   </CtDialog>
+
+  <Loading :visible="loading" />
 </template>
 
 <script setup>
+// 交易订单
 defineOptions({ name: 'trading-order-transaction'})
 import { ref } from 'vue'
 import { timesTampChange } from '@/utils/date'
@@ -163,8 +166,10 @@ const handleClose = () => {
   radio.value = null
 }
 
+const loading = ref(false)
 const handleSubmit = async () => {
   if (!radio.value) return Snackbar.warning('请选择要进行开票的抬头信息')
+  loading.value = true
   const obj = invoiceTitleList.value.find(e => e.id === radio.value)
   delete obj.id
   try {
@@ -175,6 +180,8 @@ const handleSubmit = async () => {
     getData()
   } catch (error) {
     console.log(error)
+  } finally {
+    loading.value = false
   }
 }
 

+ 3 - 3
src/views/recruit/enterprise/tradingOrder/index.vue

@@ -15,9 +15,9 @@
 defineOptions({name: 'enterprise-memberCenter-myAccount'})
 import { ref } from 'vue'
 import { useUserStore } from '@/store/user'
-import Transaction from './components/trading/transaction.vue'
-import Recharge from './components/trading/recharge.vue'
-import Balance from './components/trading/balance.vue'
+import Transaction from './components/transaction.vue'
+import Recharge from './components/recharge.vue'
+import Balance from './components/balance.vue'
 import { useRouter } from 'vue-router'
 
 const router = useRouter()

+ 13 - 5
src/views/recruit/personal/PersonalCenter/resume/attachment/index.vue

@@ -50,6 +50,8 @@
     </CtForm>
     <div class="color-666" style="font-size: 13px;">* 仅支持.doc, .docx, .pdf文件且大小不能超过20MB</div>
   </CtDialog>
+
+  <Loading :visible="loading" />
 </template>
 
 <script setup>
@@ -142,6 +144,7 @@ const handleAdd = () => {
 }
 
 // 上传附件
+const loading = ref(false)
 const handleSubmit = async () => {
   const { valid } = await CtFormRef.value.formRef.validate()
   if (!valid) return
@@ -149,11 +152,16 @@ const handleSubmit = async () => {
   formItems.value.options.forEach(e => {
     obj[e.key] = e.truthValue || e.value
   })
-  if (!obj.title || !obj.url) return
-  await savePersonResumeCv(obj)
-  getList()
-  showUploadDialog.value = false
-  Snackbar.success(t('common.uploadSucMsg'))
+  if (!obj.title || !obj.url) return Snackbar.warning('上传失败,请稍后再试')
+  loading.value = true
+  try {
+    await savePersonResumeCv(obj)
+    getList()
+    showUploadDialog.value = false
+    Snackbar.success(t('common.uploadSucMsg'))
+  } finally {
+    loading.value = false
+  }
 }
 
 // 删除

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

@@ -413,6 +413,7 @@ const items = ref({
 const handleSave = async () => {
   const { valid } = await CtFormRef.value.formRef.validate()
   if (!valid) return
+
   const obj = {}
   items.value.options.forEach(e => {
     obj[e.key] = e.value
@@ -420,11 +421,17 @@ const handleSave = async () => {
   if (!obj.sex) return Snackbar.warning('请先选择您的性别')
   if (!obj.birthday) return Snackbar.warning('请选择您的出生日期')
   // if (baseInfo.value.userId) obj.userId = baseInfo.value.userId
-  await saveResumeBasicInfo(obj)
-  Snackbar.success(t('common.saveMsg'))
-  isEdit.value = false
-  await userStore.getUserBaseInfos(baseInfo.value.userId || null)
-  getBasicInfo()
+
+  overlay.value = true
+  try {
+    await saveResumeBasicInfo(obj)
+    Snackbar.success(t('common.saveMsg'))
+    isEdit.value = false
+    await userStore.getUserBaseInfos(baseInfo.value.userId || null)
+    getBasicInfo()
+  } finally {
+    overlay.value = false
+  }
 }
 
 items.value.options.forEach(async (e, index) => {

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

@@ -10,7 +10,7 @@
       <CtForm ref="CtFormRef" :items="formItems" style="width: 100%;"></CtForm>
       <div class="text-end mt-3">
         <v-btn class="half-button mr-3" variant="tonal" @click="isEdit = false">{{ $t('common.cancel') }}</v-btn>
-        <v-btn color="primary" class="half-button" @click="handleSave">{{ $t('common.save') }}</v-btn>
+        <v-btn color="primary" class="half-button" :loading="loading" @click="handleSave">{{ $t('common.save') }}</v-btn>
       </div>
     </div>
     <!-- 展示 -->
@@ -282,6 +282,7 @@ const handle = (item) => {
 }
 
 // 保存-基础信息
+const loading = ref(false)
 const handleSave = async () => {
   const { valid } = await CtFormRef.value.formRef.validate()
   if (!valid) return
@@ -297,10 +298,16 @@ const handleSave = async () => {
   if (!obj.startTime || !obj.endTime) return Snackbar.warning('请选择起始时间!')
   if (obj.startTime > obj.endTime) return Snackbar.warning('开始时间不能大于结束时间!')
   if (editId.value) obj.id = editId.value
-  await saveResumeEduExp(obj)
-  Snackbar.success('保存成功!')
-  isEdit.value = false
-  await getData()
+
+  loading.value = true
+  try {
+    await saveResumeEduExp(obj)
+    Snackbar.success('保存成功!')
+    isEdit.value = false
+    await getData()
+  } finally {
+    loading.value = false
+  }
 }
 
 // 删除数据

+ 13 - 6
src/views/recruit/personal/PersonalCenter/resume/online/components/jobIntention.vue

@@ -39,7 +39,7 @@
       <CtForm ref="formPageRef" :items="items" style="width: 100%;"></CtForm>
       <div class="text-end">
         <v-btn class="half-button mr-3" variant="tonal" @click="isAdd = false; resetForm()">{{ $t('common.cancel') }}</v-btn>
-        <v-btn color="primary" class="half-button" @click="handleSave">{{ $t('common.save') }}</v-btn>
+        <v-btn color="primary" class="half-button" :loading="loading" @click="handleSave">{{ $t('common.save') }}</v-btn>
       </div>
     </div>
   </div>
@@ -229,6 +229,7 @@ const resetForm = () => {
   query = {}
 }
 
+const loading = ref(false)
 const handleSave = async () => {
   const { valid } = await formPageRef.value.formRef.validate()
   if (!valid) return
@@ -238,11 +239,17 @@ const handleSave = async () => {
   if (editId.value) query.id = editId.value
   if (!query.workAreaId) return Snackbar.warning('请选择期望城市')
 
-  await saveResumeJobInterested(query)
-  Snackbar.success('保存成功')
-  isAdd.value = false
-  resetForm()
-  getJobInterested()
+  loading.value = true
+
+  try {
+    await saveResumeJobInterested(query)
+    Snackbar.success('保存成功')
+    isAdd.value = false
+    resetForm()
+    getJobInterested()
+  } finally {
+    loading.value = false
+  }
 }
 
 const handleEdit = async (item) => {

+ 11 - 5
src/views/recruit/personal/PersonalCenter/resume/online/components/selfEvaluation.vue

@@ -16,7 +16,7 @@
       ></v-textarea>
       <div class="text-end">
         <v-btn class="half-button mr-3" variant="tonal" @click="isEdit = false; getData()">{{ $t('common.cancel') }}</v-btn>
-        <v-btn color="primary" class="half-button" @click="handleSave">{{ $t('common.save') }}</v-btn>
+        <v-btn color="primary" class="half-button" :loading="loading" @click="handleSave">{{ $t('common.save') }}</v-btn>
       </div>
     </div>
     <div v-else class="content-list mt-3">
@@ -63,13 +63,19 @@ const advantageRules = ref([
 ])
 
 // 编辑
+const loading = ref(false)
 const handleSave = async () => {
   advantage.value = DOMPurify.sanitize(advantage.value)
   if (!advantage.value) return Snackbar.warning('请先输入个人优势!')
-  await saveResumeAdvantage({ content: advantage.value })
-  isEdit.value = false
-  Snackbar.success('编辑成功')
-  await getData()
+  loading.value = true
+  try {
+    await saveResumeAdvantage({ content: advantage.value })
+    isEdit.value = false
+    Snackbar.success('编辑成功')
+    await getData()
+  } finally {
+    loading.value = false
+  }
 }
 </script>
 

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

@@ -173,7 +173,6 @@ getResumeTrainExpData()
 const handleSave = async () => {
   const { valid } = await formPageRef.value.formRef.validate()
   if (!valid) return
-  loading.value = true
   const obj = {}
   items.value.options.forEach(e => {
     obj[e.key] = e.value
@@ -181,6 +180,7 @@ const handleSave = async () => {
   if (!obj.startTime || !obj.endTime) return Snackbar.warning('请选择培训起始时间!')
   if (obj.startTime > obj.endTime) return Snackbar.warning('开始时间不能大于结束时间!')
   if (editId.value) obj.id = editId.value
+  loading.value = true
   try {
     await saveResumeTrainExp(obj)
     Snackbar.success('保存成功!')

+ 16 - 7
src/views/recruit/personal/PersonalCenter/resume/online/components/vocationalSkills.vue

@@ -20,7 +20,7 @@
       <CtForm ref="formPageRef" :items="formItems" style="width: 100%;"></CtForm>
       <div class="text-end">
         <v-btn class="half-button mr-3" variant="tonal" @click="isEdit = false; type = ''">{{ $t('common.cancel') }}</v-btn>
-        <v-btn color="primary" class="half-button" @click="handleSave">{{ $t('common.save') }}</v-btn>
+        <v-btn color="primary" class="half-button" :loading="loading" @click="handleSave">{{ $t('common.save') }}</v-btn>
       </div>
     </div>
     <div v-else-if="!dataList?.length" class="resumeNoDataText">{{ $t('resume.dataDefaultPrompt') }}{{ $t('resume.vocationalSkills') }}...</div>
@@ -100,20 +100,29 @@ const getData = async () => {
 getData()
 
 // 保存 职业技能
+const loading = ref(false)
 const handleSave = async () => {
   const { valid } = await formPageRef.value.formRef.validate()
   if (!valid) return
+  loading.value = true
+  
   const obj = {}
   formItems.value.options.forEach(e => {
     obj[e.key] = e.value
   })
   if (editId.value) obj.id = editId.value
-  await saveResumePersonSkill(obj)
-  Snackbar.success('保存成功!')
-  isEdit.value = false
-  type.value = ''
-  editId.value = null
-  getData()
+  
+  try {
+    
+    await saveResumePersonSkill(obj)
+    Snackbar.success('保存成功!')
+    isEdit.value = false
+    type.value = ''
+    editId.value = null
+    getData()
+  } finally {
+    loading.value = false
+  }
 }
 
 const handleEdit = (item) => {

+ 11 - 5
src/views/recruit/personal/PersonalCenter/resume/online/components/workExperience.vue

@@ -28,7 +28,7 @@
       </CtForm>
       <div class="text-end mt-3">
         <v-btn class="half-button mr-3" variant="tonal" @click="isEdit = false">{{ $t('common.cancel') }}</v-btn>
-        <v-btn color="primary" class="half-button" @click="handleSave">{{ $t('common.save') }}</v-btn>
+        <v-btn color="primary" class="half-button" :loading="loading" @click="handleSave">{{ $t('common.save') }}</v-btn>
       </div>
     </div>
     <!-- 展示 -->
@@ -268,6 +268,7 @@ const handle = (item) => {
 }
 
 // 保存-基础信息
+const loading = ref(false)
 const handleSave = async () => {
   const { valid } = await CtFormRef.value.formRef.validate()
   if (!valid) return
@@ -283,10 +284,15 @@ const handleSave = async () => {
   if (obj.endTime && obj.startTime > obj.endTime) return Snackbar.warning('开始时间不能大于结束时间!')
 
   if (editId.value) obj.id = editId.value
-  await saveResumeWorkExp(obj)
-  Snackbar.success('保存成功!')
-  isEdit.value = false
-  await getData()
+  loading.value = true
+  try {
+    await saveResumeWorkExp(obj)
+    Snackbar.success('保存成功!')
+    isEdit.value = false
+    await getData()
+  } finally {
+    loading.value = false
+  }
 }
 
 // 删除数据

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

@@ -48,6 +48,8 @@
       </template>
     </CtForm>
   </CtDialog>
+
+  <Loading :visible="loading" />
 </template>
 
 <script setup>
@@ -180,6 +182,7 @@ const handleClose = () => {
   editId.value = null
 }
 
+const loading = ref(false)
 const handleSubmit = async () => {
   const { valid } = await CtFormRef.value.formRef.validate()
   if (!valid) return
@@ -191,12 +194,16 @@ const handleSubmit = async () => {
     } else obj[e.key] = e.value
   })
   if (!obj.areaId && !obj.areaName) return Snackbar.warning('请将必填项填写完整')
+
+  loading.value = true
   try {
     isAdd.value ? await createMallUserAddress(obj) : await updateMallUserAddress(obj)
     Snackbar.success(isAdd.value ? '新增成功' : '编辑成功')
     handleClose()
     getAddressList()
-  } catch {}
+  } finally {
+    loading.value = false
+  }
 }
 
 // 删除

+ 6 - 2
src/views/recruit/personal/PersonalCenter/student/InternshipReport/index.vue

@@ -38,6 +38,8 @@
 			</template>
 		</CtForm>
 	</CtDialog>
+
+	<Loading :visible="loading" />
 </template>
 
 <script setup>
@@ -135,6 +137,7 @@ const handleClose = () => {
 	showDialog.value = false
 }
 
+const loading = ref(false)
 const handleSubmit = async () => {
 	const { valid } = await CtFormRef.value.formRef.validate()
 	if (!valid) return
@@ -144,13 +147,14 @@ const handleSubmit = async () => {
 	})
 	if (!obj.urlList || !obj.urlList.length) return Snackbar.warning('请上传实习报告')
 
+	loading.value = true
 	try {
 		await saveStudentReport(obj)
 		Snackbar.success('保存成功')
-		handleClose()
 		getList()
-	} catch {
+	} finally {
 		handleClose()
+		loading.value = false
 	}
 }
 

+ 2 - 1
vite.config.mjs

@@ -91,7 +91,8 @@ export default defineConfig({
     port: 3000,
     proxy: {
       '/api': {
-        target: 'https://company.citupro.com:18183',
+        target: 'http://192.168.3.143:5500',
+        // target: 'https://company.citupro.com:18183',
         secure: false, // 是否支持 https,默认 false
         changeOrigin: true, // 是否支持跨域
         pathRewrite: {