Procházet zdrojové kódy

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

zhengnaiwen_citu před 10 měsíci
rodič
revize
47dd8596f3
37 změnil soubory, kde provedl 1090 přidání a 416 odebrání
  1. 1 0
      components.d.ts
  2. 8 0
      src/api/common/index.js
  3. 25 0
      src/api/recruit/enterprise/interview/index.js
  4. 3 2
      src/api/recruit/enterprise/personnel/index.js
  5. 5 1
      src/components/CtForm/index.vue
  6. 8 6
      src/components/Position/item.vue
  7. 3 2
      src/components/PositionLongStrip/item.vue
  8. 88 0
      src/components/Recharge/index.vue
  9. 3 1
      src/hooks/web/useDictionaries.js
  10. 7 5
      src/layout/company/navBar.vue
  11. 11 9
      src/layout/personal/navBar.vue
  12. 2 2
      src/locales/en.js
  13. 3 3
      src/locales/zh-CN.js
  14. 5 5
      src/store/dict.js
  15. 4 0
      src/styles/index.css
  16. 1 1
      src/styles/index.min.css
  17. 1 0
      src/styles/index.scss
  18. 17 0
      src/utils/areaDeal.js
  19. 37 0
      src/utils/date.js
  20. 16 0
      src/utils/position.js
  21. 23 37
      src/views/recruit/enterprise/informationManagement/informationSettings.vue
  22. 65 0
      src/views/recruit/enterprise/informationManagement/informationSettingsComponents/authentication.vue
  23. 25 0
      src/views/recruit/enterprise/informationManagement/informationSettingsComponents/basicInfo.vue
  24. 106 0
      src/views/recruit/enterprise/interview/components/invite.vue
  25. 253 115
      src/views/recruit/enterprise/interview/index.vue
  26. 2 2
      src/views/recruit/enterprise/personnelManagement/components/screen.vue
  27. 26 10
      src/views/recruit/enterprise/personnelManagement/components/table.vue
  28. 14 14
      src/views/recruit/enterprise/personnelManagement/index.vue
  29. 29 3
      src/views/recruit/enterprise/positionManagement/components/add.vue
  30. 10 12
      src/views/recruit/enterprise/positionManagement/components/baseInfo.vue
  31. 3 2
      src/views/recruit/enterprise/positionManagement/components/item.vue
  32. 82 57
      src/views/recruit/enterprise/positionManagement/components/jobRequirements.vue
  33. 40 45
      src/views/recruit/personal/message/components/chatting.vue
  34. 16 4
      src/views/recruit/personal/position/components/details.vue
  35. 100 32
      src/views/recruit/personal/remuse/components/basicInfo.vue
  36. 45 44
      src/views/recruit/personal/remuse/components/jobIntention.vue
  37. 3 2
      src/views/recruit/personal/shareJob/index.vue

+ 1 - 0
components.d.ts

@@ -42,6 +42,7 @@ declare module 'vue' {
     PreviewImg: typeof import('./src/components/PreviewImg/index.vue')['default']
     PublicRecruitment: typeof import('./src/components/publicRecruitment/index.vue')['default']
     RadioGroup: typeof import('./src/components/FormUI/radioGroup/index.vue')['default']
+    Recharge: typeof import('./src/components/Recharge/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     SimilarPositions: typeof import('./src/components/Position/similarPositions.vue')['default']

+ 8 - 0
src/api/common/index.js

@@ -159,6 +159,14 @@ export const getAreaListData = async (params) => {
   })
 }
 
+// 获取地区获取地区map
+export const getAreaMapData = async (params) => {
+  return await request.get({
+    url: '/app-api/menduner/system/area/map',
+    params
+  })
+}
+
 // 公司检索-获取热门地区
 export const getHotArea = async () => {
   return await request.get({

+ 25 - 0
src/api/recruit/enterprise/interview/index.js

@@ -0,0 +1,25 @@
+import request from '@/config/axios'
+
+// 获取当前企业用户的面试信息分页
+export const getInterviewInvitePage = async (params) => {
+  return await request.get({
+    url: '/app-admin-api/menduner/system/interview-invite/page',
+    params
+  })
+}
+
+// 面试-保存、重新邀约
+export const saveInterviewInvite = async (data) => {
+  return await request.post({
+    url: '/app-admin-api/menduner/system/interview-invite/save',
+    data
+  })
+}
+
+// 取消面试
+export const cancelInterviewInvite = async (data) => {
+  return await request.post({
+    url: '/app-admin-api/menduner/system/interview-invite/cancellation',
+    data
+  })
+}

+ 3 - 2
src/api/recruit/enterprise/personnel/index.js

@@ -8,9 +8,10 @@ export const personJobCvLook = async (id) => {
 }
 
 // 招聘端-牛人管理-加入不合适
-export const joinEliminate = async (ids) => {
+export const joinEliminate = async (data) => {
   return await request.post({
-    url: `/app-admin-api/menduner/system/person-cv/eliminate?ids=${ids}`
+    url: '/app-admin-api/menduner/system/person-cv/unfit/eliminate',
+    data
   })
 }
 

+ 5 - 1
src/components/CtForm/index.vue

@@ -125,7 +125,11 @@ const formRef = ref()
 const handleChange = (item) => {
   // console.log('handleChange', item)
   if (item.type === 'date' && item.value) item.option.validate = false
-  if (item?.change) item.change(item.value, item)
+  let selectObj = {}
+  if (item.returnSelect && item.items?.length) {
+    selectObj = item.items.find(e => e[item.itemValue || 'value'] === item.value)
+  }
+  if (item?.change) item.change(item.value, item, selectObj)
   emit('change', false)
 }
 

+ 8 - 6
src/components/Position/item.vue

@@ -6,6 +6,7 @@
           <div class="sub-li-top">
             <div class="sub-li-info">
               <p :class="['name', {'default-active': item.active }]">{{ item.name }}</p>
+              <PublicRecruitment v-if="tab === 3 && item.hire" width="30" height="30"></PublicRecruitment>
             </div>
             <p class="salary">{{ item.payFrom }}-{{ item.payTo }}/{{ item.payName }}</p>
           </div>
@@ -18,11 +19,10 @@
                 <v-chip v-if="item[j.value]" size="x-small" label class="mr-1" color="var(--color-666)" :prepend-icon="j.mdi">{{ item[j.value] }}</v-chip>
               </span>
             </div>
-            <PublicRecruitment v-if="tab === 3 && item.hire"></PublicRecruitment>
           </div>
-          <div v-if="tab === 3 && item.hire">
-            <v-chip v-if="item.hirePrice" size="small" label class="mr-1" color="primary">赏金:{{ item.hirePrice }}元</v-chip>
-            <v-chip v-if="item.hirePoint" size="small" label class="mr-1" color="primary">积分:{{ item.hirePoint }}点</v-chip>
+          <div v-if="tab === 3 && item.hire" class="text-end mt-3">
+            <v-chip v-if="item.hirePrice" size="small" label color="primary">赏金:{{ commissionCalculation(item.hirePrice, 1) }}元</v-chip>
+            <v-chip v-if="item.hirePoint" size="small" label class="ml-1" color="primary">积分:{{ commissionCalculation(item.hirePoint, 1) }}点</v-chip>
           </div>
         </div>
         <div class="sub-li-bottom" @click="handleEnterprise(item)">
@@ -52,6 +52,8 @@ import bountyDisplay from '@/views/publicRecruitment/components/bountyDisplay.vu
 
 defineOptions({ name: 'position-card-item' })
 import { ref, watch } from 'vue'
+import { commissionCalculation } from '@/utils/position'
+
 const props = defineProps({
   items: {
     type: Array,
@@ -135,7 +137,7 @@ const height = ((210 * 2) + 12) + 'px'
   display: flex;
   align-items: center;
   flex-wrap: wrap;
-  height: 22px;
+  height: 31%;
   overflow: hidden;
   flex: 1;
 }
@@ -157,7 +159,7 @@ const height = ((210 * 2) + 12) + 'px'
 }
 .sub-li-info .name {
   position: relative;
-  max-width: 200px;
+  max-width: 140px;
   margin-right: 8px;
   overflow: hidden;
   text-overflow: ellipsis;

+ 3 - 2
src/components/PositionLongStrip/item.vue

@@ -12,8 +12,8 @@
           <p v-else :class="['title1', {'default-active': item.positionActive }]">{{ item.job.name }}{{ item.job.pos ? ' [' + item.job.pos + '] ' : '' }}</p>
           <p class="salary ml-1">{{ item.job.payFrom }}-{{ item.job.payTo }}/{{ item.job.payName }}</p>
           <div v-if="item?.job?.hire">
-            <v-chip v-if="item?.job?.hirePrice && item?.job?.hirePrice > 0" class="ml-3" label color="primary" size="small">赏金:{{ item.job.hirePrice }}元</v-chip>
-            <v-chip v-if="item?.job?.hirePoint && item?.job?.hirePoint > 0" class="ml-3" label color="primary" size="small">积分:{{ item.job.hirePoint }}点</v-chip>
+            <v-chip v-if="item?.job?.hirePrice && item?.job?.hirePrice > 0" class="ml-3" label color="primary" size="small">赏金:{{ commissionCalculation(item.job.hirePrice, 1) }}元</v-chip>
+            <v-chip v-if="item?.job?.hirePoint && item?.job?.hirePoint > 0" class="ml-3" label color="primary" size="small">积分:{{ commissionCalculation(item.job.hirePoint, 1) }}点</v-chip>
           </div>
         </div>
         <div class="mt-2">
@@ -58,6 +58,7 @@
 </template>
 
 <script setup>
+import { commissionCalculation } from '@/utils/position'
 defineOptions({ name: 'long-strip-position-card-item' })
 import { ref, watch } from 'vue'
 const props = defineProps({

+ 88 - 0
src/components/Recharge/index.vue

@@ -0,0 +1,88 @@
+<template>
+  <CtForm ref="CtFormRef" :items="formItems">
+    <template #tips>
+      <div class="my-3">
+        <v-icon color="warning">mdi-information</v-icon>
+        <span class="color-warning ml-3">当前余额不足,请先充值后再试</span>
+      </div>
+    </template>
+    <template #paymentAmount>
+      <div class="d-flex align-center justify-space-between" style="width: 100%;">
+        <div class="color-666">支付金额</div>
+        <div class="font-size-20 color-error font-weight-bold">¥{{ paymentAmount }}</div>
+      </div>
+    </template>
+  </CtForm>
+</template>
+
+<script setup>
+defineOptions({ name: 'Recharge-index'})
+import { ref, computed } from 'vue'
+import CtForm from '@/components/CtForm/index.vue'
+
+// 余额充值
+const CtFormRef = ref()
+const formItems = ref({
+  options: [
+    {
+      noParam: true,
+      slotName: 'tips'
+    },
+    {
+      type: 'ifRadio',
+      key: 'rechargeType',
+      value: true,
+      label: '充值类型 *',
+      items: [
+        { label: '金额', value: true },
+        { label: '积分', value: false }
+      ],
+      rules: [v => !!v || '请选择充值类型']
+    },
+    {
+      type: 'number',
+      key: 'count',
+      value: 100,
+      label: '充值数量 *',
+      rules: [
+        value => {
+          if (value) return true
+          return '请输入要充值的数量'
+        },
+        value => {
+          if (value && value > 0) return true
+          return '充值数量不得等于0'
+        }
+      ]
+    },
+    {
+      slotName: 'paymentAmount',
+      noParam: true
+    }
+  ]
+})
+
+const paymentAmount = computed(() => {
+  return formItems.value.options.find(e => e.key === 'count').value
+})
+
+const getQuery = () => {
+  const obj = {}
+  formItems.value.options.forEach(e => {
+    if (e.noParam) return
+    obj[e.key] = e.value
+  })
+  return obj
+}
+
+defineExpose({
+  CtFormRef,
+  getQuery,
+  formItems
+})
+
+</script>
+
+<style scoped lang="scss">
+
+</style>

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

@@ -3,6 +3,7 @@ import {
   getIndustryListData,
   getIndustryTreeData,
   getAreaListData,
+  getAreaMapData,
   getPositionTreeData,
   getAreaTreeData,
   getPositionData
@@ -21,7 +22,7 @@ const setDict = (type, val, cacheTime = 7200) => {
 
 export const getDict = (type, params, apiType = 'dict') => {
     if (!type) {
-      console.error('type不存在', type, params, apiType)
+      // console.error('type不存在', type, params, apiType)
       return []
     }
     return new Promise((resolve) => {
@@ -39,6 +40,7 @@ export const getDict = (type, params, apiType = 'dict') => {
         industryTreeData: getIndustryTreeData, // 行业tree
         industryList: getIndustryListData,
         areaList: getAreaListData,
+        areaMap: getAreaMapData,
         positionData: getPositionData
       }
       apiFn[apiType](query).then(data => {

+ 7 - 5
src/layout/company/navBar.vue

@@ -12,12 +12,14 @@
           </div>
         </div>
         
-        <div class="d-flex user-nav">
+        <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>
           </div>
           <div class="line"></div>
+          <div class="ml-3 cursor-pointer" @click="handleLogout">我要求职</div>
+          <div class="line"></div>
           <div class="d-flex align-center ml-6">
             <div class="cursor-pointer" @click="router.push({ path: '/recruit/enterprise/memberCenter/myPoints' })">{{ $t('enterprise.account.accountBalances') }}:{{ enterpriseUserAccount?.balance || 0 }}元</div>
             <div class="ml-5 cursor-pointer" @click="router.push({ path: '/recruit/enterprise/memberCenter/myPoints' })">{{ $t('enterprise.account.remainingPoints') }}:{{ enterpriseUserAccount?.point || 0 }}点</div>
@@ -48,7 +50,7 @@
           </div>
 
           <!-- 语言切换 -->
-          <v-menu>
+          <!-- <v-menu>
             <template v-slot:activator="{ props }">
               <v-btn
                 class="ml-3"
@@ -70,7 +72,7 @@
                 <v-list-item-title>{{ item.name }}</v-list-item-title>
               </v-list-item>
             </v-list>
-          </v-menu>
+          </v-menu> -->
 
           <v-btn size="small" icon="mdi-bell-outline"></v-btn>
         </div>
@@ -124,7 +126,7 @@ const menuList = ref([
   { title: t('enterprise.personalInformationSettings'), icon: 'mdi-account-cog', change: () => router.push({ path: '/recruit/enterprise/informationSettings' }) },
   { title: t('setting.switchToOtherCompany'), icon: 'mdi-home-switch', hidden: enterpriseList.value?.length < 2, change: () => handleSwitchToAnotherEnterprise },
   { title: t('enterprise.registeringNewEnterprise'), icon: 'mdi-home-plus-outline', change: () => handleRegisteringNewEnterprise },
-  { title: t('setting.switchToJobSeeker'), icon: 'mdi-swap-horizontal', change: handleLogout },
+  // { title: t('setting.switchToJobSeeker'), icon: 'mdi-swap-horizontal', change: handleLogout },
   { title: t('setting.logOut'), icon: 'mdi-logout', change: handleLogout }
 ])
 const items = computed(() => {
@@ -231,7 +233,7 @@ getEnterpriseListData()
 }
 .line {
   width: 1px;
-  height: 25px;
+  height: 20px;
   background-color: #fff;
   margin: 0 10px;
   margin: 8px 0 0 29px;

+ 11 - 9
src/layout/personal/navBar.vue

@@ -33,17 +33,18 @@
           
           <!-- 头像用户名 -->
           <div class="d-flex align-center" v-if="getToken()">
-            <span class="cursor-pointer" @click="router.push({ path: '/recruit/personal/myWallet' })">{{t('resume.accountWithdrawal')}}:{{ userAccount?.balance || 0 }}{{t('unit.rmb')}}</span>
-            <span class="mr-5 ml-5 cursor-pointer" @click="router.push({ path: '/recruit/personal/myWallet' })">{{t('resume.goldCoins')}}:{{ userAccount?.point || 0 }}{{t('unit.ge')}}</span>
+            <span class="cursor-pointer mr-5" @click="changeLoginType">我要招聘</span>
+            <span class="cursor-pointer" @click="router.push({ path: '/recruit/personal/myWallet' })">{{t('resume.accountWithdrawal')}}:{{ userAccount?.balance || 0 }}</span>
+            <span class="mr-3 ml-3 cursor-pointer" @click="router.push({ path: '/recruit/personal/myWallet' })">{{t('resume.goldCoins')}}:{{ userAccount?.point || 0 }}</span>
 
             
             <v-menu open-on-hover>
               <template v-slot:activator="{ props }">
-                <div class="d-flex ml-5 pl-2 align-center cursor-pointer" v-bind="props" @click="handleToPersonalCenter">
+                <div class="d-flex ml-3 align-center cursor-pointer" v-bind="props" @click="handleToPersonalCenter">
                   <v-avatar>
                     <v-img alt="John" :src="baseInfo?.avatar || 'https://minio.citupro.com/dev/menduner/7.png'"></v-img>
                   </v-avatar>
-                  <div class="ml-2">{{ baseInfo?.name || $t('sys.tourist') }}</div>
+                  <div class="ml-3">{{ baseInfo?.name || $t('sys.tourist') }}</div>
                 </div>
               </template>
 
@@ -59,7 +60,7 @@
           </div>
 
           <!-- 语言切换 -->
-          <v-menu>
+          <!-- <v-menu>
             <template v-slot:activator="{ props }">
               <v-btn
                 class="ml-3"
@@ -81,10 +82,11 @@
                 <v-list-item-title>{{ item.name }}</v-list-item-title>
               </v-list-item>
             </v-list>
-          </v-menu>
+          </v-menu> -->
           <div class="d-flex align-center" v-if="getToken()">
-            <span class="cursor-pointer mx-5" @click="router.push({ path: '/recruit/personal/TaskCenter' })">{{ $t('sys.signIn') }}</span>
-            <span class="cursor-pointer" @click="router.push({ path: '/recruit/personal/message' })">{{ $t('sys.news') }}</span>
+            <!-- <span class="cursor-pointer mx-5" @click="router.push({ path: '/recruit/personal/TaskCenter' })">{{ $t('sys.signIn') }}</span> -->
+            <!-- <span class="cursor-pointer" @click="router.push({ path: '/recruit/personal/message' })">{{ $t('sys.news') }}</span> -->
+            <v-btn class="ml-1" size="small" icon="mdi-bell-outline" @click="router.push({ path: '/recruit/personal/message' })"></v-btn>
           </div>
         </div>
         
@@ -178,7 +180,7 @@ const items = ref([
   { title: t('vipPackage.purchasePackage'), icon: 'mdi-gift-outline', change: () => router.push({ path: '/recruit/personal/purchasePackage' }) },
   { title: t('resume.onlineResume'), icon: 'mdi-list-box-outline', change: () => router.push({ path: '/recruit/personal/resume' }) },
   { title: t('setting.accountSettings'), icon: 'mdi-cog-outline', change: () => router.push({ path: '/recruit/personal/accountSettings/accountBinding' }) },
-  { title: t('setting.switchToRecruit'), icon: 'mdi-swap-horizontal', change: changeLoginType },
+  // { title: t('setting.switchToRecruit'), icon: 'mdi-swap-horizontal', change: changeLoginType },
   { title: t('publicRecruitment.bountyRewards'), icon: 'mdi-google-circles-extended', change: () => router.push({ path: '/publicRecruitment' }) },
   { title: t('points.wallet'), icon: ' mdi-database-outline', change: () => router.push({ path: '/recruit/personal/myWallet' }) },
   { title: t('taskCenter.taskCenter'), icon: 'mdi-calendar-check-outline', change: () => router.push({ path: '/recruit/personal/TaskCenter' }) },

+ 2 - 2
src/locales/en.js

@@ -127,7 +127,8 @@ export default {
     hire: 'Crowd hired positions',
     allBtn: 'View All Positions',
     throughCommunication: 'Through Communication',
-    delivered: 'Delivered',
+    delivered: 'I have submitted it',
+    submitResume: 'I want to deliver',
     interview: 'Interview',
     interested: 'Interested',
     interestedInMe: 'Interested In Me',
@@ -142,7 +143,6 @@ export default {
     screen: 'Screen',
     positionName: 'Please enter the position name',
     popularPosition: 'Popular positions',
-    submitResume: 'Submit resume',
     communicate: 'Communicate immediately',
     currentOnline: 'Currently online',
     jobResponsibilities: 'Job responsibilities',

+ 3 - 3
src/locales/zh-CN.js

@@ -127,7 +127,8 @@ export default {
     hire: '众聘职位',
     allBtn: '查看全部职位',
     throughCommunication: '沟通过',
-    delivered: '已投递',
+    delivered: '我已投递',
+    submitResume: '我要投递',
     interview: '面试',
     interested: '感兴趣',
     interestedInMe: '对我感兴趣',
@@ -142,7 +143,6 @@ export default {
     screen: '筛选',
     positionName: '请输入职位名称',
     popularPosition: '热门职位',
-    submitResume: '投递简历',
     communicate: '立即沟通',
     currentOnline: '当前在线',
     jobResponsibilities: '岗位职责',
@@ -175,7 +175,7 @@ export default {
       welfareLabel: '福利标签',
       businessInformation: '工商信息',
       enterpriseVideo: '企业视频',
-      recruitmentQRCode: '招聘二维码',
+      recruitmentQRCode: '招聘二维码'
     },
     account: {
       myAccount: '我的账户',

+ 5 - 5
src/store/dict.js

@@ -6,12 +6,12 @@ const list = [
   { type: 'areaTreeData', apiFn: 'areaTreeData' },
   { type: 'positionTreeData', apiFn: 'positionTreeData' },
   { type: 'positionData', params: {}, apiFn: 'positionData' },
-  { type: 'menduenr_industry_type', params: {}, apiFn: 'industryList' },
-  { type: 'menduenr_area_type', params: {}, apiFn: 'areaList' },
-  { type: 'menduenr_education_type' },
-  { type: 'menduenr_exp_type' },
+  { type: 'menduner_industry_type', params: {}, apiFn: 'industryList' },
+  { type: 'menduner_area_type', params: {}, apiFn: 'areaList' },
+  { type: 'menduner_education_type' },
+  { type: 'menduner_exp_type' },
   { type: 'system_user_sex' },
-  { type: 'menduenr_job_type' },
+  { type: 'menduner_job_type' },
   { type: 'menduner_job_status' },
   { type: 'menduner_marital_status' },
   { type: 'menduner_pay_unit' },

+ 4 - 0
src/styles/index.css

@@ -73,6 +73,10 @@
   color: #00897B;
 }
 
+.color-warning {
+  color: #fb8c00;
+}
+
 .font-size-12 {
   font-size: 12px;
 }

+ 1 - 1
src/styles/index.min.css

@@ -1 +1 @@
-:root{--zIndex-dialog:999;--default-bgc:#f2f4f7;--v-primary-base:#00897B;--v-error-base:#fe574a;--v-primary-lighten1:#26A69A;--v-primary-lighten2:#4DB6AC;--v-primary-lighten3:#80CBC4;--v-primary-lighten4:#B2DFDB;--color-222:#222;--color-333:#333;--color-666:#666;--color-777:#777;--color-999:#999;--color-ccc:#ccc;--color-f3:#f3f3f3;--color-f2f4f742:#f2f4f742;--color-f8:#f8f8f8;--color-f2f4f7:#f2f4f7;--color-d5e6e8:#d5e6e8;--zIndex-breadcrumbs:999}.color-222{color:#222}.color-333{color:#333}.color-666{color:#666}.color-777{color:#777}.color-999{color:#999}.color-ccc{color:#ccc}.color-f3f3f3{color:#f3f3f3}.color-f2f4f742{color:#f2f4f742}.color-f8f8f8{color:#f8f8f8}.color-f2f4f7{color:#f2f4f7}.color-d5e6e8{color:#d5e6e8}.color-error{color:#fe574a}.color-primary{color:#00897B}.font-size-12{font-size:12px}.font-size-13{font-size:13px}.font-size-14{font-size:14px}.font-size-15{font-size:15px}.font-size-16{font-size:16px}.font-size-17{font-size:17px}.font-size-18{font-size:18px}.font-size-19{font-size:19px}.font-size-20{font-size:20px}.buttons{height:36px;width:224px}.half-button{height:36px;width:88px}.default-width{width:1184px;min-width:1184px;max-width:1184px;margin:0 auto}.defaultLink{color:#008978;cursor:pointer}.default-active{color:var(--v-primary-base) !important}.border-bottom-dashed{border-bottom:1px dashed var(--color-ccc)}.white-bgc{background-color:#fff}.ellipsis{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.septal-line{display:inline-block;width:1px;height:10px;vertical-align:middle;background-color:#e0e0e0;margin:0 10px}.resume-box{border-radius:5px;padding:20px 30px;background-color:#fff}.resume-header{display:flex;justify-content:space-between;align-items:center;height:36px}.resume-title{font-weight:700;font-size:18px;border-left:5px solid #00897B;padding-left:12px;line-height:17px}.resumeNoDataText{color:var(--color-666);font-size:14px}.card-box{width:100%;height:100%;min-height:70vh}
+:root{--zIndex-dialog:999;--default-bgc:#f2f4f7;--v-primary-base:#00897B;--v-error-base:#fe574a;--v-primary-lighten1:#26A69A;--v-primary-lighten2:#4DB6AC;--v-primary-lighten3:#80CBC4;--v-primary-lighten4:#B2DFDB;--color-222:#222;--color-333:#333;--color-666:#666;--color-777:#777;--color-999:#999;--color-ccc:#ccc;--color-f3:#f3f3f3;--color-f2f4f742:#f2f4f742;--color-f8:#f8f8f8;--color-f2f4f7:#f2f4f7;--color-d5e6e8:#d5e6e8;--zIndex-breadcrumbs:999}.color-222{color:#222}.color-333{color:#333}.color-666{color:#666}.color-777{color:#777}.color-999{color:#999}.color-ccc{color:#ccc}.color-f3f3f3{color:#f3f3f3}.color-f2f4f742{color:#f2f4f742}.color-f8f8f8{color:#f8f8f8}.color-f2f4f7{color:#f2f4f7}.color-d5e6e8{color:#d5e6e8}.color-error{color:#fe574a}.color-primary{color:#00897B}.color-warning{color:#fb8c00}.font-size-12{font-size:12px}.font-size-13{font-size:13px}.font-size-14{font-size:14px}.font-size-15{font-size:15px}.font-size-16{font-size:16px}.font-size-17{font-size:17px}.font-size-18{font-size:18px}.font-size-19{font-size:19px}.font-size-20{font-size:20px}.buttons{height:36px;width:224px}.half-button{height:36px;width:88px}.default-width{width:1184px;min-width:1184px;max-width:1184px;margin:0 auto}.defaultLink{color:#008978;cursor:pointer}.default-active{color:var(--v-primary-base) !important}.border-bottom-dashed{border-bottom:1px dashed var(--color-ccc)}.white-bgc{background-color:#fff}.ellipsis{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.septal-line{display:inline-block;width:1px;height:10px;vertical-align:middle;background-color:#e0e0e0;margin:0 10px}.resume-box{border-radius:5px;padding:20px 30px;background-color:#fff}.resume-header{display:flex;justify-content:space-between;align-items:center;height:36px}.resume-title{font-weight:700;font-size:18px;border-left:5px solid #00897B;padding-left:12px;line-height:17px}.resumeNoDataText{color:var(--color-666);font-size:14px}.card-box{width:100%;height:100%;min-height:70vh}

+ 1 - 0
src/styles/index.scss

@@ -34,6 +34,7 @@
 .color-d5e6e8 { color: #d5e6e8; }
 .color-error { color: #fe574a; }
 .color-primary { color: #00897B; }
+.color-warning { color: #fb8c00; }
 
 .font-size-12 { font-size: 12px; }
 .font-size-13 { font-size: 13px; }

+ 17 - 0
src/utils/areaDeal.js

@@ -0,0 +1,17 @@
+import { getDict } from '@/hooks/web/useDictionaries'
+
+// 根据市获取省份 
+export const cityToProvince = async (cityId, query, parentList = []) => {
+  if (!cityId) return cityId
+  query = { pid: true, cityList: true, ...query }
+  const obj = {}
+  if (query.cityList || query.pid) {
+    const areaMap = await getDict('areaMap', {}, 'areaMap')
+    obj.pid = areaMap?.data && areaMap?.data[cityId]?.parentId
+    if (query.cityList && obj.pid) {
+      const parent = parentList.find(pv => pv.id === obj.pid)
+      obj.cityList = parent?.children.length ? parent.children : []
+    }
+  }
+  return obj
+}

+ 37 - 0
src/utils/date.js

@@ -16,4 +16,41 @@ export const timesTampChange = (timestamp) => {
 export const getTimeStamp = (str) => {
   const date = new Date(str)
   return date.getTime()
+}
+
+// 传入一个时间戳返回这个日期的最早时间点以及最晚时间点 输出:[1721232000000, 1721318399999]
+export const getDayBounds = (timestamp) => {
+  const date = new Date(timestamp)
+  date.setHours(0, 0, 0, 0)
+  const startOfDay = date.getTime()
+  const endOfDay = new Date(timestamp)
+  endOfDay.setHours(23, 59, 59, 999)
+  if (endOfDay.getDate() !== date.getDate()) {
+    endOfDay.setDate(endOfDay.getDate() - 1)
+    endOfDay.setHours(23, 59, 59, 999)
+  }
+  // 返回包含最早和最晚时间点的时间戳的数组
+  return [startOfDay, endOfDay.getTime()]
+}
+
+// 传入 Wed May 01 2024 00:00:00 GMT+0800 (中国标准时间) 输出 [2024-07-18 00:00:00, 2024-07-18 23:59:59]
+export const getStartAndEndOfDay = (dateString) => {
+  const date = new Date(dateString + ' UTC')
+  if (isNaN(date.getTime())) {
+    throw new Error('Invalid date string')
+  }
+
+  const startDate = new Date(date.getFullYear(), date.getMonth(), date.getDate())
+  const endDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59)
+
+  function formatDate(dateObj) {
+    let month = ('0' + (dateObj.getMonth() + 1)).slice(-2)
+    let day = ('0' + dateObj.getDate()).slice(-2)
+    let hours = ('0' + dateObj.getHours()).slice(-2)
+    let minutes = ('0' + dateObj.getMinutes()).slice(-2)
+    let seconds = ('0' + dateObj.getSeconds()).slice(-2)
+    return dateObj.getFullYear() + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds
+  }
+
+  return [formatDate(startDate), formatDate(endDate)]
 }

+ 16 - 0
src/utils/position.js

@@ -1,5 +1,6 @@
 import { reactive, ref } from 'vue'
 import { getDict } from '@/hooks/web/useDictionaries'
+import { getPublicRatio } from '@/api/recruit/enterprise/position'
 
 const dictObj = reactive({
   payUnit: [], // 薪资单位
@@ -66,4 +67,19 @@ export const dealDictObjData = (res, obj) => {
     res = { ...obj, ...res }
   })
   return res
+}
+
+// 计算众聘佣金
+let data
+const list = ['headhuntRate', 'recommendRate', 'cvRate'] // 平台、推荐人、投递人
+const getRation = async () => {
+  data = await getPublicRatio()
+}
+getRation()
+
+export const commissionCalculation = (count, type) => {
+  if (!data || !Object.keys(data).length) return
+  const ratio = parseFloat(data[list[type]]) / 100
+  const value = count * ratio
+  return value % 1 === 0 ? Math.floor(value) : value.toFixed(2)
 }

+ 23 - 37
src/views/recruit/enterprise/informationManagement/informationSettings.vue

@@ -2,40 +2,14 @@
 <template>
   <div>
     <v-card class="card-box pa-5" style="min-height: 500px">
-      <div>
-        <v-tabs v-model="tab" @update:model-value="handleTabClick" align-tabs="start" color="primary" bg-color="#fff">
-          <v-tab :value="1">{{ $t('enterprise.infoSetting.basicInfo') }}</v-tab>
-          <v-tab :value="2">{{ $t('enterprise.infoSetting.enterpriseLogo') }}</v-tab>
-          <v-tab :value="3">{{ $t('enterprise.infoSetting.enterpriseAlbum') }}</v-tab>
-          <v-tab :value="4">{{ $t('enterprise.infoSetting.welfareLabel') }}</v-tab>
-          <v-tab :value="5">{{ $t('enterprise.infoSetting.businessInformation') }}</v-tab>
-          <!-- <v-tab :value="5">{{ $t('enterprise.infoSetting.enterpriseVideo') }}</v-tab>
-          <v-tab :value="6">{{ $t('enterprise.infoSetting.recruitmentQRCode') }}</v-tab> -->
-        </v-tabs>
-        <v-window v-model="tab" class="mt-3">
-          <v-window-item :value="1">
-            <basicInfo></basicInfo>
-          </v-window-item>
-          <v-window-item :value="2">
-            <enterpriseLogo></enterpriseLogo>
-          </v-window-item>
-          <v-window-item :value="3">
-            <enterpriseAlbum></enterpriseAlbum>
-          </v-window-item>
-          <v-window-item :value="4">
-            <welfareLabel></welfareLabel>
-          </v-window-item>
-          <v-window-item :value="5">
-            <businessInformation></businessInformation>
-          </v-window-item>
-          <!-- <v-window-item :value="5">
-            <enterpriseVideo></enterpriseVideo>
-          </v-window-item>
-          <v-window-item :value="6">
-            <recruitmentQRCode></recruitmentQRCode>
-          </v-window-item> -->
-        </v-window>
-      </div>
+      <v-tabs v-model="tab" @update:model-value="handleTabClick" align-tabs="start" color="primary" bg-color="#fff">
+        <v-tab v-for="val in tabList" :key="val.value" :value="val.value">{{ val.label }}</v-tab>
+      </v-tabs>
+      <v-window v-model="tab" class="mt-3">
+        <v-window-item :value="val.value" v-for="val in tabList" :key="val.value">
+          <component :is="val.path"></component>
+        </v-window-item>
+      </v-window>
     </v-card>
   </div>
 </template>
@@ -47,14 +21,26 @@ import enterpriseLogo from './informationSettingsComponents/enterpriseLogo.vue'
 import enterpriseAlbum from './informationSettingsComponents/enterpriseAlbum.vue'
 import welfareLabel from './informationSettingsComponents/welfareLabel.vue'
 import businessInformation from './informationSettingsComponents/businessInformation.vue'
-// import enterpriseVideo from './informationSettingsComponents/enterpriseVideo.vue'
-// import recruitmentQRCode from './informationSettingsComponents/recruitmentQRCode.vue'
+import authentication from './informationSettingsComponents/authentication.vue'
 import { ref, watch } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
+import { useI18n } from '@/hooks/web/useI18n'
 
-const route = useRoute(); const router = useRouter()
+const route = useRoute()
+const router = useRouter()
+const { t } = useI18n()
 // tab
 const tab = ref(1)
+const tabList = [
+  { label: t('enterprise.infoSetting.basicInfo'), value: 1, path: basicInfo },
+  { label: t('enterprise.infoSetting.enterpriseLogo'), value: 2, path: enterpriseLogo },
+  { label: t('enterprise.infoSetting.enterpriseAlbum'), value: 3, path: enterpriseAlbum },
+  { label: t('enterprise.infoSetting.welfareLabel'), value: 4, path: welfareLabel },
+  { label: t('enterprise.infoSetting.businessInformation'), value: 5, path: businessInformation },
+  { label: t('setting.realNameAuthentication'), value: 6, path: authentication },
+]
+
+
 watch(() => route?.query?.tabKey, (newVal) => { if (newVal) tab.value = newVal - 0 })
 const handleTabClick = () => {
   router.push(`${route.path}?tabKey=${tab.value.toString()}`)

+ 65 - 0
src/views/recruit/enterprise/informationManagement/informationSettingsComponents/authentication.vue

@@ -0,0 +1,65 @@
+<template>
+  <div v-if="authentication" class="ml-3">
+    <div>
+      <v-icon color="primary">mdi-check-circle</v-icon>
+      已通过实名认证
+    </div>
+    <div class="box mt-5">
+      <div>姓名:史迪奇</div>
+      <div class="mt-5">身份证号:4******************8</div>
+    </div>
+    <v-btn color="primary" class="half-button mt-5" @click="authentication = !authentication">解绑</v-btn>
+  </div>
+  <div v-else>
+    <div class="topTip">为了您在平台有更好的操作体验,请进行实名认证</div>
+    <div class="d-flex align-center justify-center flex-column">
+      <CtForm ref="CtFormRef" :items="formItems" style="width: 300px;"></CtForm>
+      <v-btn class="buttons" color="primary" @click="authentication = !authentication">{{ $t('common.submit') }}</v-btn>
+    </div>
+  </div>
+</template>
+
+<script setup>
+defineOptions({ name: 'authentication-page'})
+import { ref } from 'vue'
+
+// 是否已实名
+const authentication = ref(true)
+const CtFormRef = ref()
+
+const formItems = ref({
+  options: [
+    {
+      type: 'text',
+      key: 'name',
+      value: '',
+      label: '真实姓名 *',
+      rules: [v => !!v || '请输入您的真实姓名']
+    },
+    {
+      type: 'text',
+      key: 'idCardNo',
+      value: '',
+      label: '身份证号码 *',
+      rules: [v => !!v || '请输入您的身份证号码']
+    }
+  ]
+})
+</script>
+
+<style scoped lang="scss">
+.topTip {
+  background-color: #fff6ec;
+  color: var(--v-error-base);
+  padding: 12px 20px;
+  margin: 10px 0 40px;
+  font-size: 14px;
+}
+.box {
+  background-color: #f7f8fa;
+  border-radius: 6px;
+  color: var(--color-666);
+  font-size: 14px;
+  padding: 25px;
+}
+</style>

+ 25 - 0
src/views/recruit/enterprise/informationManagement/informationSettingsComponents/basicInfo.vue

@@ -21,6 +21,9 @@
           <industryTypeCard :limit="1" :select="[query.industryId].filter(Boolean)" @handleClickIndustry="handleIndustry"></industryTypeCard>
         </v-menu>
       </template>
+      <template #prepare="{ item }">
+        <v-checkbox v-model="item.value" label="筹建中(如果贵企业正在筹建,请勾选)" color="primary"></v-checkbox>
+      </template>
     </CtForm>
     <div class="text-center">
       <v-btn color="primary" class="buttons mt-3 mb-10" @click.stop="handleSave">{{ $t('common.save') }}</v-btn>
@@ -133,6 +136,27 @@ const formItems = ref({
       label: '上班时间(示例:上午09:00 - 下午17:00) *',
       rules: [v => !!v || '请填写上班时间']
     },
+    {
+      type: 'datePicker',
+      key: 'openTime',
+      value: null,
+      col: 6,
+      class: 'mb-3',
+      flexStyle: 'mr-3',
+      rules: [v => !!v || '请选择开业时间'],
+      options: {
+        type: 'date',
+        format: 'timestamp',
+        placeholder: '开业时间 *',
+        clearable: false
+      },
+    },
+    {
+      slotName: 'prepare',
+      key: 'prepare',
+      value: true,
+      col: 6
+    },
     {
       type: 'textarea',
       key: 'introduce',
@@ -197,6 +221,7 @@ const handleSave = async () => {
   getBaseInfo()
 }
 </script>
+
 <style lang="scss" scoped>
 .topTip {
   background-color: #f7f8fa;

+ 106 - 0
src/views/recruit/enterprise/interview/components/invite.vue

@@ -0,0 +1,106 @@
+<template>
+  <CtForm ref="CtFormRef" :items="formItems" style="height: 420px;">
+    <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: 'formPage'})
+import { ref } from 'vue'
+
+const props = defineProps({
+  itemData: {
+    type: Object,
+    default: () => {}
+  },
+  position: {
+    type: Array,
+    default: () => []
+  }
+})
+
+const CtFormRef = ref()
+const formItems = ref({
+  options: [
+    {
+      slotName: 'time',
+      key: 'time',
+      value: ref(),
+      rules: [v => !!v || '请选择面试时间'],
+    },
+    {
+      type: 'autocomplete',
+      key: 'jobId',
+      value: null,
+      label: '招聘职位 *',
+      outlined: true,
+      clearable: false,
+      itemText: 'label',
+      itemValue: 'value',
+      rules: [v => !!v || '请选择招聘职位'],
+      items: props.position
+    },
+    {
+      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) {
+  formItems.value.options.forEach(item => {
+    item.value = props.itemData[item.key]
+  })
+}
+
+const getQuery = () => {
+  const obj = {}
+  formItems.value.options.forEach(item => {
+    obj[item.key] = item.value
+  })
+  obj.type = 1
+  obj.id = props.itemData.id
+  obj.userId = props.itemData.userId
+  return obj
+}
+
+defineExpose({
+  CtFormRef,
+  getQuery
+})
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 253 - 115
src/views/recruit/enterprise/interview/index.vue

@@ -4,38 +4,36 @@
     <div class="d-flex justify-space-between">
       <div class="d-flex mb-3">
         <!-- 职位 -->
-        <v-select 
-          v-model="positionValue" 
+        <v-autocomplete 
+          v-model="query.jobId" 
           :items="positionItems" 
           density="compact" 
           variant="outlined" 
           item-title="label" 
-          item-value="value" 
-          hide-details 
+          item-value="value"
+          clearable
+          hide-details
+          label="职位"
           color="primary"
-          style="width: 150px;"
+          style="width: 300px;"
           class="mr-3"
-          @update:model-value="handlePositionChange"
-        ></v-select>
-        <!-- 状态 -->
+        ></v-autocomplete>
         <v-select 
-          v-model="stateValue" 
-          :items="stateItems" 
+          v-model="query.status" 
+          :items="statusList" 
           density="compact" 
           variant="outlined" 
           item-title="label" 
-          item-value="value" 
-          hide-details 
+          item-value="value"
+          clearable
+          hide-details
+          label="面试状态"
           color="primary"
-          style="width: 150px;"
-          @update:model-value="handleStateChange"
+          style="width: 300px;"
         ></v-select>
+        <v-btn color="primary" class="half-button ml-3" @click="handleSearch">查 询</v-btn>
+        <v-btn class="half-button ml-3" variant="outlined" color="primary" @click="handleReset">重 置</v-btn>
       </div>
-      <!-- <div class="mr-3 mb-3 px-2 py-1" style="background-color: var(--color-999); color: #fff; border-radius: 5px;">
-        <v-icon size="30">mdi mdi-view-list-outline</v-icon>
-        <v-icon size="30">mdi mdi-circle-small</v-icon>
-      </div> -->
-      <div></div>
     </div>
     <v-divider class="mb-3"></v-divider>
     <div class="d-flex">
@@ -43,139 +41,279 @@
         <div class="d-flex justify-space-between px-5">
           <div v-if="selectDateValue">
             <span>{{ timesTampChange(selectDateValue).slice(0, 10) }}</span>
-            <span class="ml-2" style="cursor: pointer;" @click="selectDateValue = null">{{ $t('common.cleanUp') }}</span>
+            <span class="ml-2" style="cursor: pointer;" @click="handleClear">{{ $t('common.cleanUp') }}</span>
           </div>
-          <div v-else class="color999">{{ $t('interview.noDateSelected') }}</div>
+          <div v-else class="color-999">{{ $t('interview.noDateSelected') }}</div>
           <v-btn color="primary" variant="text" size="small" @click="selectDateValue = new Date()">{{ $t('interview.today') }}</v-btn>
         </div>
         <v-date-picker
           v-model="selectDateValue"
           color="primary"
-          show-adjacent-months
           :hide-header="true"
-          @update:modelValue="handleCurrentChange"
+          @update:modelValue="handleChangeDate"
           class="mr-3"
-        ></v-date-picker>
+        >
+        </v-date-picker>
       </div>
       <v-divider style="height: auto;" class="mr-5" vertical></v-divider>
       <div style="flex: 1;overflow: hidden;">
-        <div
-          class="listItem d-flex align-center justify-space-between pa-3 mb-3"
-          style="width: 100%;min-width: 900px;overflow: auto;height: 76px;border: 1px solid #e5e6eb;border-radius: 5px;"
-          v-for="(item, index) in dataList" :key="'item_' + index"
-        >
-          <div class="d-flex align-center">
-            <span class="mr-2">{{ item.date }}</span>
-            <span class="mr-5 fz16" style="color: orange;">{{ item.time }}</span>
-            <v-avatar class="mr-2" size=50 :image="item?.avatar || 'https://minio.citupro.com/dev/menduner/7.png'"></v-avatar>
-            <div class="d-flex flex-column mr-3" style="width: 110px;">
-              <span class="ellipsis mb-1">{{ item?.name }}</span>
-              <span class="ellipsis" style="color: var(--color-999);">{{ item?.job }}</span>
+        <div v-if="items.length">
+          <div
+            class="listItem d-flex align-center pa-3 mb-3"
+            v-for="(item, index) in items" :key="'item_' + index"
+          >
+            <div class="d-flex align-center">
+              <span class="mr-5 font-size-16" style="color: orange;">{{ timesTampChange(item.time) }}</span>
+              <v-avatar class="mr-2" size=40 :image="item?.person?.avatar || 'https://minio.citupro.com/dev/menduner/7.png'"></v-avatar>
+              <div class="d-flex flex-column mr-3" style="width: 110px;">
+                <span class="ellipsis mb-1">{{ item?.person?.name }}</span>
+                <span class="ellipsis" style="color: var(--color-999);">{{ item?.job?.name }}</span>
+              </div>
+            </div>
+            <div class="d-flex align-center right-item">
+              <div style="min-width: 80px;text-align: center;">
+                <v-icon v-if="item?.phone" class="mx-1" size="20" color="primary">mdi-phone-outline</v-icon>
+                <span>{{ item?.phone || '-' }}</span>
+              </div>
+              <div>
+                <!-- 面试类型: 线下面试 -->
+                <span v-if="item.type === '1'">
+                  <v-icon class="mx-3" size="20" color="primary">mdi-account-multiple-outline</v-icon>
+                  <span>{{ $t('interview.offlineInterview') }}</span>
+                </span>
+                <!-- 面试类型: 线上面试 -->
+                <span v-else class="d-flex">
+                  <v-icon class="mx-3 mt-2" size="20" color="primary">mdi mdi-video-account</v-icon>
+                  <span class="d-flex flex-column">
+                    <span>{{ $t('interview.onlineInterview') }}</span>
+                    <span style="color: var(--color-999);">腾讯会议</span>
+                  </span>
+                </span>
+              </div>
+              <!-- 面试状态: '待接受'/'已取消' -->
+              <div :style="{ 'color': item.status !== '99' ? 'orange' :'var(--color-999)'}">
+                <v-icon size="30">mdi mdi-circle-small</v-icon>
+                <span>{{ statusList.find(e => e.value === item.status)?.label }}</span>
+              </div>
+              <div>
+                <span v-if="editStatus.indexOf(item.status)" class="font-size-15 color-primary" @click="handleActionClick(2, item)">修改面试</span>
+                <v-menu>
+                  <template v-slot:activator="{ props }">
+                    <v-icon v-bind="props" class="mx-3" size="20" color="primary">mdi-dots-horizontal</v-icon>
+                  </template>
+                  <v-list>
+                    <v-list-item
+                      v-for="(k, index) in actionItems"
+                      :key="index"
+                      :value="index"
+                      color="primary"
+                      @click="handleActionClick(k.value, item)"
+                    >
+                      <v-list-item-title>{{ k.title }}</v-list-item-title>
+                    </v-list-item>
+                  </v-list>
+                </v-menu>
+              </div>
             </div>
-            <span style="min-width: 100px;">离职-随时到岗</span>
           </div>
-          <span style="min-width: 120px;text-align: center;">
-            <v-icon v-if="item?.phone" class="mx-1" size="20" color="primary">mdi-phone-outline</v-icon>
-            <span>{{ item?.phone || '-' }}</span>
-          </span>
-          <!-- 面试类型: 线下面试 -->
-          <span v-if="item.interviewType === 1">
-            <v-icon class="mx-3" size="20" color="primary">mdi-account-multiple-outline</v-icon>
-            <span>{{ $t('interview.offlineInterview') }}</span>
-          </span>
-          <!-- 面试类型: 线上面试 -->
-          <span class="d-flex" v-else>
-            <v-icon class="mx-3 mt-2" size="20" color="primary">mdi mdi-video-account</v-icon>
-            <span class="d-flex flex-column">
-              <span>{{ $t('interview.onlineInterview') }}</span>
-              <span style="color: var(--color-999);">腾讯会议</span>
-            </span>
-          </span>
-          <!-- 面试状态: '待接受'/'已取消' -->
-           <span :style="{ 'color': item.interviewStatus ? 'orange' :'var(--color-999)'}">
-            <v-icon size="30">mdi mdi-circle-small</v-icon>
-            <span>{{ $t(item.interviewStatus ? 'interview.waitingForAcceptance' :'interview.canceled') }}</span>
-           </span>
-          <span>
-            <span class="fz15 primaryColor">{{ item.interviewStatus ? '修改面试' :'重新邀请' }}</span>
-            <v-icon class="mx-3" size="20" color="primary">mdi-dots-horizontal</v-icon>
-          </span>
+          <CtPagination
+            v-if="total > 0"
+            :total="total"
+            :page="query.pageNo"
+            :limit="query.pageSize"
+            @handleChange="handleChangePage"
+          ></CtPagination>
         </div>
+        <Empty v-else :elevation="false"></Empty>
       </div>
     </div>
   </v-card>
+
+  <!-- 修改面试 -->
+  <CtDialog :visible="showInvite" :widthType="2" titleClass="text-h6" title="面试邀请" @close="handleClose" @submit="handleSubmit">
+    <InvitePage v-if="showInvite" ref="inviteRef" :itemData="itemData" :position="positionItems"></InvitePage>
+  </CtDialog>
+
+  <CtDialog :visible="cancelInvite" :widthType="2" titleClass="text-h6" title="取消面试" @close="handleCancelClose" @submit="handleCancelSubmit">
+    <TextInput v-model="cancelQuery.reason" :item="textItems"></TextInput>
+  </CtDialog>
 </template>
 
 <script setup>
-import { timesTampChange } from '@/utils/date'
-import { ref } from 'vue'
 defineOptions({ name: 'enterprise-interview'})
+import { ref } from 'vue'
+import { getInterviewInvitePage, saveInterviewInvite, cancelInterviewInvite } from '@/api/recruit/enterprise/interview'
+import InvitePage from './components/invite.vue'
+import { getDict } from '@/hooks/web/useDictionaries'
+import Snackbar from '@/plugins/snackbar'
+import { getJobAdvertised } from '@/api/enterprise'
+import { dealDictArrayData } from '@/utils/position'
+import { timesTampChange, getStartAndEndOfDay } from '@/utils/date'
 
-const dataList = ref([
-  {
-    avatar: 'http://menduner.citupro.com:6868/admin-api/infra/file/24/get/241e594d4473872eabb312673f42241a2e9598298cb7d9d791cc9c8cb65fb058.jpg',
-    name: '王瑶',
-    job: '软件测试',
-    phone: '18406571583',
-    date: '2024-06-14',
-    time: '11:00',
-    interviewType: 1,
-    interviewStatus: 1,
-  },
-  {
-    name: '黄小姐',
-    job: '软件测试-实习岗位',
-    phone: '',
-    date: '2024-06-14',
-    time: '15:00',
-    interviewType: 2,
-    interviewStatus: 0,
-  },
+const cancelInvite = ref(false)
+const showInvite = ref(false)
+const inviteRef = ref()
+const items = ref([])
+const cancelQuery = ref({
+  id: null,
+  reason: null
+})
+const editStatus = ['99', '1', '0']
+const statusList = ref()
+const itemData = ref({})
+// 状态
+const actionItems = ref([
+  // { title: '沟通', value: 1 },
+  // { title: '修改面试', value: 2 },
+  { title: '取消面试', value: 3 },
+  // { title: '面试记录', value: 4 }
 ])
+const total = ref(0)
+const query = ref({
+  pageSize: 10,
+  pageNo: 1,
+  status: null,
+  jobId: null,
+  time: []
+})
+const textItems = ref({
+  type: 'text',
+  label: '取消原因 *',
+  clearable: true
+})
+
+// 状态字典
+const getStatusList = async () => {
+  const { data } = await getDict('menduner_interview_invite_status')
+  statusList.value = data
+}
+getStatusList()
+
+// 列表
+const getData = async () => {
+  const { list, total: number } = await getInterviewInvitePage(query.value)
+  items.value = list
+  total.value = number
+}
+getData()
+
+// 分页
+const handleChangePage = (e) => {
+  query.value.pageNo = e
+  getData()
+}
+
+// 日期选择
+const selectDateValue = ref(null)
+const handleChangeDate = () => {
+  const time = getStartAndEndOfDay(selectDateValue.value)
+  if (!time || !time.length) return delete query.value.time
+  query.value.time = time
+  query.value.pageNo = 1
+  getData()
+}
+// 清除
+const handleClear = () => {
+  query.value.pageNo = 1
+  selectDateValue.value = null
+  delete query.value.time 
+  getData()
+}
 
-const selectDateValue = ref(null) // new Date(); new Date('2018-03-02')
-const handleCurrentChange = (val, val1) => {
-  console.log('1', val, val1)
+const handleSearch = () => {
+  query.value.pageNo = 1
+  getData()
+}
+
+const handleReset = () => {
+  query.value = {
+    pageSize: 10,
+    pageNo: 1,
+    status: null,
+    jobId: null,
+    time: []
+  }
+  selectDateValue.value = null
+  getData()
 }
 
 // 职位
-const positionValue = ref('0')
-const positionItems = ref([
-  { label: '全部职位', value: '0' },
-  { label: '软件测试(10-11K)', value: '1' },
-])
-const handlePositionChange = (val) => {
-  console.log('1', val)
+const positionItems = ref([])
+const getPositionList = async () => {
+  const data = await getJobAdvertised({ hire: false })
+  if (!data.length) return
+  const list = dealDictArrayData([], data)
+  positionItems.value = list.map(e => {
+    return { label: `${e.name}${e.areaName ? '_' + e.areaName : ''} ${e.payFrom}-${e.payTo}/${e.payName}`, value: e.id }
+  })
 }
-// 状态
-const stateValue = ref('0')
-const stateItems = ref([
-  { label: '全部状态', value: '0' },
-  { label: '待接受', value: '1' },
-  { label: '待面试', value: '1' },
-  { label: '即将面试', value: '1' },
-  { label: '面试时间到', value: '1' },
-  { label: '已完成', value: '1' },
-  { label: '待反馈', value: '1' },
-  { label: '已反馈', value: '1' },
-])
-const handleStateChange = (val) => {
-  console.log('1', val)
+getPositionList()
+
+// 操作按钮
+const handleActionClick = (value, item) => {
+  // 修改
+  if (value === 2) {
+    itemData.value = item
+    showInvite.value = true
+  }
+  // 取消
+  if (value === 3) {
+    cancelQuery.value.id = item.id
+    cancelInvite.value = true
+  }
+}
+
+// 修改面试
+const handleClose = () => {
+  itemData.value = {}
+  showInvite.value = false
+}
+
+const handleSubmit = async () => {
+  const query = inviteRef.value.getQuery()
+  if (!Object.keys(query).length) return
+  await saveInterviewInvite(query)
+  Snackbar.success('操作成功')
+  handleClose()
+  getData()
+}
+
+// 取消面试
+const handleCancelClose = () => {
+  cancelInvite.value = false
+  cancelQuery.value = {
+    id: null,
+    reason: null
+  }
+}
+
+const handleCancelSubmit = async () => {
+  if (!cancelQuery.value.reason) return Snackbar.warning('请填写取消原因')
+  await cancelInterviewInvite(cancelQuery.value)
+  Snackbar.success('操作成功')
+  handleCancelClose()
+  getData()
 }
 </script>
 
 <style scoped lang="scss">
-.fz14 { font-size: 14px; }
-.fz15 { font-size: 15px; }
-.fz16 { font-size: 16px; }
-.color666 { color: var(--color-666); }
-.color999 { color: var(--color-999); }
-.primaryColor { color: var(--v-primary-base); }
 .listItem {
   cursor: pointer;
+  width: 100%;
+  min-width: 600px;
+  overflow: auto;
+  height: 76px;
+  border: 1px solid #e5e6eb;
+  border-radius: 5px;
   
   &:hover {
     background-color: var(--color-f8);
   }
+  .right-item {
+    width: 100%;
+    div {
+      width: 25%;
+    }
+  }
 }
 </style>

+ 2 - 2
src/views/recruit/enterprise/personnelManagement/components/screen.vue

@@ -28,7 +28,7 @@ import CommonStyle from './commonStyle.vue'
 
 const emit = defineEmits(['search', 'reset'])
 const props = defineProps({
-  tab: String
+  tab: Number
 })
 
 const list = ref([
@@ -79,7 +79,7 @@ list.value.forEach(k => {
       if (data.length) {
         const list = dealDictArrayData([], data)
         k.items = list.map(e => {
-          return { label: `${e.name}_${e.areaName} ${e.payFrom}-${e.payTo}/${e.payName}`, value: e.id }
+          return { label: `${e.name}${e.areaName ? '_' + e.areaName : ''} ${e.payFrom}-${e.payTo}/${e.payName}`, value: e.id }
         })
       }
     })

+ 26 - 10
src/views/recruit/enterprise/personnelManagement/components/table.vue

@@ -1,8 +1,7 @@
 <template>
   <div>
     <div class="text-end">
-      <v-btn v-if="tab === '0'" color="primary" :disabled="selected.length ? false : true" variant="tonal" @click="handleAction('all', 0)">不合适</v-btn>
-      <v-btn v-if="tab === '1'" color="primary" :disabled="selected.length ? false : true" variant="tonal" @click="handleAction('all', 1)">入职</v-btn>
+      <v-btn v-if="tab === 1" color="primary" :disabled="selected.length ? false : true" variant="tonal" @click="handleAction('all', 0)">入职</v-btn>
     </div>
     <v-data-table
       class="mt-3"
@@ -29,12 +28,13 @@
         </div>
       </template>
       <template v-slot:item.actions="{ item }">
-        <div v-if="tab === '0'">
+        <div v-if="tab === 0">
           <v-btn color="primary" variant="text" @click="handlePreviewResume(item)">查看简历</v-btn>
-          <v-btn color="primary" variant="text" @click="handleAction('', 0, item)">不合适</v-btn>
         </div>
-        <div v-if="tab === '1'">
-          <v-btn color="primary" variant="text" @click="handleAction('', 1, 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="handleInterviewInvite(item)">邀请面试</v-btn>
+          <v-btn color="primary" variant="text" @click="handleAction('', 0, item)">入职</v-btn>
         </div>
       </template>
     </v-data-table>
@@ -51,8 +51,8 @@ import Snackbar from '@/plugins/snackbar'
 
 const { t } = useI18n()
 const emit = defineEmits(['refresh'])
-defineProps({
-  tab: String,
+const props = defineProps({
+  tab: Number,
   items: Array
 })
 const badgeColor = computed(() => (item) => {
@@ -81,11 +81,10 @@ const handleToPersonDetail = ({ userId, id }) => {
 }
 
 const apiList = [
-  joinEliminate, // 不合适
   personEntryByEnterprise // 入职
 ]
 
-// 不合适、入职
+// 入职
 const handleAction = async (type, index, item) => {
   const ids = type ? selected.value : [item?.id]
   if (!ids) return
@@ -94,12 +93,29 @@ const handleAction = async (type, index, item) => {
   emit('refresh')
 }
 
+// 不合适
+const handleEliminate = async (item) => {
+  if (!item.id || !item?.job?.id) return
+  const query = {
+    bizId: item.id,
+    jobId: item.job.id,
+    type: props.tab === 0 ? '0' : '1' // 投递简历0 已邀约1
+  }
+  await joinEliminate(query)
+  Snackbar.success(t('common.operationSuccessful'))
+  emit('refresh')
+}
+
 // 查看简历
 const handlePreviewResume = async ({ url, id }) => {
   if (!url || !id) return
   await personJobCvLook(id)
   previewFile(url)
 }
+
+const handleInterviewInvite = (item) => {
+  console.log(item, 'item')
+}
 </script>
 
 <style scoped lang="scss">

+ 14 - 14
src/views/recruit/enterprise/personnelManagement/index.vue

@@ -28,8 +28,8 @@
 defineOptions({ name: 'enterprise-personnelManagement-management'})
 import { ref } from 'vue'
 import { getPersonCvPage } from '@/api/enterprise'
-import { getDict } from '@/hooks/web/useDictionaries'
 import { dealDictObjData } from '@/utils/position'
+import { getInterviewInvitePage } from '@/api/recruit/enterprise/interview'
 import TablePage from './components/table.vue'
 import Screen from './components/screen.vue'
 
@@ -37,11 +37,17 @@ const total = ref(0)
 const query = ref({
   pageNo: 1,
   pageSize: 10,
-  status: 0,
+  status: null,
   type: 0
 })
-const tab = ref('0')
-const tabList = ref([])
+const tab = ref(0)
+const tabList = ref([
+  { label: '投递简历', value: 0, api: getPersonCvPage, status: null },
+  { label: '已邀约', value: 1, api: getInterviewInvitePage, status: '0' },
+  { label: '已发offer', value: 2, api: getInterviewInvitePage, status: '1' },
+  { label: '已入职', value: 3, api: getInterviewInvitePage, status: '2' },
+  { label: '不合适', value: 4 },
+])
 const textItems = ref({
   type: 'text',
   value: '',
@@ -51,19 +57,13 @@ const textItems = ref({
   appendInnerIcon: 'mdi-magnify'
 })
 
-// 获取tab列表项
-const getTabData = () => {
-  getDict('menduner_job_cv_status').then(({ data }) => {
-    data = data?.length && data || []
-    tabList.value = data
-  })
-}
-getTabData()
-
 // 获取牛人列表
 const items = ref([])
 const getList = async () => {
-  const { list, total: number } = await getPersonCvPage(query.value)
+  if (tab.value === 4) return
+  const api = tabList.value[tab.value].api
+  query.value.status = tab.value === 0 ? null : tabList.value[tab.value].status
+  const { list, total: number } = await api(query.value)
   if (!list.length) {
     items.value = []
     total.value = 0

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

@@ -20,6 +20,10 @@
         <v-btn class="half-button" color="primary" @click="handleSave">{{ $t('common.release') }}</v-btn>
       </div>
     </v-card>
+
+    <CtDialog :visible="recharge" :widthType="2" titleClass="text-h6" title="确认支付" @close="handleClose" @submit="handleSubmit">
+      <Recharge ref="rechargeRef"></Recharge>
+    </CtDialog>
   </div>
 </template>
 
@@ -42,6 +46,19 @@ const userStore = useUserStore()
 const baseInfoRef = ref()
 const jobRequirementsRef = ref()
 const itemData = ref({})
+
+// 充值
+const rechargeRef = ref()
+const handleClose = () => {
+  recharge.value = false
+  rechargeRef.value.formItems.options.find(e => e.key === 'count').value = 100
+}
+
+const handleSubmit = () => {
+  recharge.value = false
+  Snackbar.warning('此功能还在开发中')
+}
+
 const list = [
   {
     color: '#00897B',
@@ -61,6 +78,9 @@ const list = [
   }
 ]
 
+// 余额充值
+const recharge = ref(false)
+
 // 取消
 const handleCancel = () => {
   itemData.value = {}
@@ -81,9 +101,15 @@ const handleSave = async () => {
   const query = Object.assign(baseInfo, requirement)
   // 有id则为编辑
   if (route.query && route.query.id) query.id = route.query.id
-  await saveJobAdvertised(query)
-  Snackbar.success(route.query.id ? t('common.editSuccessMsg') : t('common.publishSuccessMsg'))
-  handleCancel()
+  try {
+    await saveJobAdvertised(query)
+    Snackbar.success(route.query.id ? t('common.editSuccessMsg') : t('common.publishSuccessMsg'))
+    handleCancel()
+  } catch (error) {
+    // 余额不足展示充值窗口
+    if (error === '用户余额不足') recharge.value = true
+  }
+  
 }
 
 // 获取编辑的职位详情

+ 10 - 12
src/views/recruit/enterprise/positionManagement/components/baseInfo.vue

@@ -8,7 +8,7 @@
         </span>
       </template>
       <template #ratio>
-        <div class="color-666 mb-3">
+        <div class="font-size-13 mb-3" style="color: red;">
           众聘岗位分配比例:平台占比{{ ratio.headhuntRate }}%&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 推荐人占比{{ ratio.recommendRate }}%&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 投递人占比{{ ratio.cvRate }}%
         </div>
       </template>
@@ -91,10 +91,6 @@ const handleChangePublic = (val) => {
   })
 }
 
-const handleBlur = (item, val) => {
-  console.log(item, val, 'blur')
-}
-
 const formPageRef = ref()
 let query = reactive({})
 
@@ -131,12 +127,19 @@ const items = ref({
       hide: true,
       disabled: false,
       noParam: true,
+      hideDetails: true,
       items: [
         { label: '赏金', value: true },
         { label: '积分', value: false }
       ],
       change: handleChangeHireType
     },
+    {
+      slotName: 'ratio',
+      noParam: true,
+      show: true,
+      hide: true
+    },
     {
       type: 'number',
       key: 'hirePrice',
@@ -154,8 +157,7 @@ const items = ref({
           if (value >= 1) return true
           return '赏金金额不得小于1'
         }
-      ],
-      blur: handleBlur
+      ]
     },
     {
       type: 'number',
@@ -177,11 +179,7 @@ const items = ref({
         }
       ]
     },
-    {
-      slotName: 'ratio',
-      noParam: true,
-      hide: false
-    },
+    
     {
       type: 'text',
       key: 'name',

+ 3 - 2
src/views/recruit/enterprise/positionManagement/components/item.vue

@@ -32,8 +32,8 @@
             <span>{{ val.positionName }}</span>
           </div>
           <div v-if="val?.hire" class="ml-10 mt-2">
-            <v-chip v-if="val?.hirePrice && val.hirePrice > 0" class="mr-3" label color="primary" size="small">赏金:{{ val.hirePrice }}元</v-chip>
-            <v-chip v-if="val?.hirePoint && val.hirePoint > 0" label color="primary" size="small">积分:{{ val.hirePoint }}点</v-chip>
+            <v-chip v-if="val?.hirePrice && val.hirePrice > 0" class="mr-3" label color="primary" size="small">赏金:{{ commissionCalculation(val.hirePrice, 1) }}元</v-chip>
+            <v-chip v-if="val?.hirePoint && val.hirePoint > 0" label color="primary" size="small">积分:{{ commissionCalculation(val.hirePoint, 1) }}点</v-chip>
           </div>
         </div>
         <div class="d-flex align-center">
@@ -73,6 +73,7 @@
 </template>
 
 <script setup>
+import { commissionCalculation } from '@/utils/position'
 defineOptions({ name: 'enterprise-position-item'})
 import { defineEmits, ref, watch } from 'vue'
 import { useRouter } from 'vue-router'

+ 82 - 57
src/views/recruit/enterprise/positionManagement/components/jobRequirements.vue

@@ -1,30 +1,15 @@
 <template>
   <div style="width: 100%;">
-    <CtForm ref="formPageRef" :items="items" style="width: 600px;">
-      <template #areaId="{ item }">
-        <v-menu :close-delay="1" :open-delay="0" v-bind="$attrs">
-            <template v-slot:activator="{  props }">
-              <textUI
-                v-model="item.value"
-                :item="item"
-                v-bind="props"
-                style="position: relative;"
-              ></textUI>
-            </template>
-            <areaType :select="[query.areaId].filter(Boolean)" @handleAreaClick="handleArea" class="jobTypeCardBox" isSingle></areaType>
-          </v-menu>
-      </template>
-    </CtForm>
+    <CtForm ref="formPageRef" :items="items" style="width: 600px;"></CtForm>
   </div>
 </template>
 
 <script setup>
 defineOptions({ name: 'position-add-job-requirements'})
 import CtForm from '@/components/CtForm'
-import areaType from '@/components/AreaSelect'
-import textUI from '@/components/FormUI/TextInput'
 import { reactive, ref, defineExpose, watch } from 'vue'
 import { getDict } from '@/hooks/web/useDictionaries'
+import { cityToProvince } from '@/utils/areaDeal'
 
 const props = defineProps({
   itemData: Object
@@ -45,31 +30,6 @@ const items = ref({
       rules: [v => !!v || '招聘类型'],
       items: []
     },
-    {
-      type: 'autocomplete',
-      key: 'eduType',
-      value: null,
-      label: '最高学历 *',
-      itemText: 'label',
-      itemValue: 'value',
-      col: 6,
-      flexStyle: 'mr-3',
-      dictTypeName: 'menduner_education_type',
-      rules: [v => !!v || '请选择最高学历'],
-      items: []
-    },
-    {
-      type: 'autocomplete',
-      key: 'expType',
-      value: null,
-      label: '工作经验 *',
-      itemText: 'label',
-      itemValue: 'value',
-      col: 6,
-      dictTypeName: 'menduner_exp_type',
-      rules: [v => !!v || '请选择工作经验'],
-      items: []
-    },
     {
       type: 'text',
       key: 'payFrom',
@@ -120,13 +80,58 @@ const items = ref({
       items: []
     },
     {
-      slotName: 'areaId',
-      key: 'areaId',
+      type: 'autocomplete',
+      key: 'eduType',
       value: null,
+      label: '最高学历 *',
+      itemText: 'label',
+      itemValue: 'value',
+      col: 6,
+      flexStyle: 'mr-3',
+      dictTypeName: 'menduner_education_type',
+      rules: [v => !!v || '请选择最高学历'],
+      items: []
+    },
+    {
+      type: 'autocomplete',
+      key: 'expType',
+      value: null,
+      label: '工作经验 *',
+      itemText: 'label',
+      itemValue: 'value',
+      col: 6,
+      dictTypeName: 'menduner_exp_type',
+      rules: [v => !!v || '请选择工作经验'],
+      items: []
+    },
+    {
+      type: 'autocomplete',
+      key: 'workAreaProvinceId',
+      value: null,
+      label: '工作城市:省 *',
+      outlined: true,
+      itemText: 'name',
+      itemValue: 'id',
+      returnSelect: true,
       noParam: true,
-      labelKey: 'areaName',
-      label: '工作城市 *',
-      rules: [v => !!v || '请选择工作城市']
+      col: 6,
+      flexStyle: 'mr-3',
+      rules: [v => !!v || '请选择工作城市:省'],
+      items: [],
+      change: null
+    },
+    {
+      type: 'autocomplete',
+      key: 'areaId',
+      value: null,
+      label: '工作城市:市 *',
+      outlined: true,
+      itemText: 'name',
+      itemValue: 'id',
+      col: 6,
+      rules: [v => !!v || '请选择工作城市:市'],
+      items: [],
+      change: null
     },
     {
       type: 'text',
@@ -148,17 +153,26 @@ const getDictData = async () => {
   })
 }
 
-// 工作城市
-const handleArea = (list, name) => {
-  if (!list.length) {
-    delete query.areaId
-    items.value.options.find(e => e.key === 'areaId').value = ''
-    return
-  }
-  query.areaId = list[0]
-  const obj = items.value.options.find(e => e.key === 'areaId')
-  obj.value = name
+const provinceChange = (value, val, obj) => {
+  const item = items.value.options.find(e => e.key === 'areaId')
+  if (!item) return
+  item.items = obj.children || []
+  item.value = null
 }
+getDict('areaTreeData', null, 'areaTreeData').then(({ data }) => {
+  data = data?.length && data || []
+  if (!data?.length) return console.error('areaTreeData获取失败!')
+  //
+  const china = data.find(e => e.id === '1')
+  const chinaTreeData = china?.children?.length ? china.children : []
+  //
+  if (!chinaTreeData?.length) return console.error('chinaTreeData获取失败!')
+  const item = items.value.options.find(e => e.key === 'workAreaProvinceId')
+  if (item?.items) {
+    item.items = chinaTreeData
+    item.change = provinceChange
+  }
+})
 
 const getQuery = async () => {
   const { valid } = await formPageRef.value.formRef.validate()
@@ -179,6 +193,7 @@ watch(
     await getDictData()
     if (!Object.keys(val).length) return
     // 编辑
+    let workAreaId = ''
     items.value.options.forEach(e => {
       if (e.labelKey) {
         query[e.key] = val[e.key]
@@ -187,7 +202,17 @@ watch(
       }
       if (e.noParam) return
       e.value = val[e.key]
+      if (e.key === 'areaId' && val[e.key]) workAreaId = val[e.key]
     })
+    if (workAreaId) { // 省份回显
+      const province = items.value.options.find(pv => pv.key === 'workAreaProvinceId')
+      if (province) {
+        const dealReturnObj = await cityToProvince(workAreaId, {}, province.items || [])
+        const city = items.value.options.find(pv => pv.key === 'areaId')
+        if (city) city.items = dealReturnObj.cityList || []
+        province.value = dealReturnObj.pid || ''
+      }
+    }
   },
   { immediate: true },
   { deep: true }

+ 40 - 45
src/views/recruit/personal/message/components/chatting.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="chatting">
+  <div class="chatting d-flex flex-column">
     <div class="top-info">
       <div class="user-info d-flex align-center">
         <p class="d-flex align-center float-left">
@@ -15,8 +15,8 @@
         <span class="color-333">广州</span>
       </div>
     </div>
-    <div class="chat-record mt-3">
-      <div class="message-box" @scroll="handleScroll" ref="chatRef">
+    <div class="mt-3 message-box" @scroll="handleScroll" ref="chatRef">
+      <div>
         <div v-for="(val, i) in items" :key="i" :id="val.id">
           <div class="time-box">{{ val.time }}</div>
           <div :class="['message-view_item', val.userId === myUserId ? 'is-self' : 'is-other']">
@@ -168,56 +168,51 @@ const handleScroll = (e) => {
     }
   }
 }
-.chat-record {
-  
-  padding-bottom: 30px;
-  .message-box {
-    height: 525px;
-    padding: 0 30px 20px;
-    overflow-y: auto;
-    .time-box {
-      user-select: none;
-      position: relative;
-      top: 8px;
-      margin: 20px 0;
-      max-height: 20px;
-      text-align: center;
-      font-weight: 400;
-      font-size: 12px;
-      color: var(--color-time-divider);
-    }
-    .message-view_item {
-      display: flex;
-      flex-direction: row;
-      align-items: center;
-      margin: 8px 0;
-      position: relative;
-      .message-text {
-        background-color: #f0f2f5;
-        border-radius: 6px;
-        max-width: 85%;
-        padding: 10px;
-      }
+.message-box {
+  flex: 1;
+  padding: 0 30px 30px;
+  overflow-y: auto;
+  .time-box {
+    user-select: none;
+    position: relative;
+    top: 8px;
+    margin: 20px 0;
+    max-height: 20px;
+    text-align: center;
+    font-weight: 400;
+    font-size: 12px;
+    color: var(--color-time-divider);
+  }
+  .message-view_item {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    margin: 8px 0;
+    position: relative;
+    .message-text {
+      background-color: #f0f2f5;
+      border-radius: 6px;
+      max-width: 85%;
+      padding: 10px;
     }
-    .is-self {
-      flex-direction: row-reverse;
-      display: flex;
-      .message-text {
-        margin-right: 10px;
-      }
+  }
+  .is-self {
+    flex-direction: row-reverse;
+    display: flex;
+    .message-text {
+      margin-right: 10px;
     }
-    .is-other {
-      .message-text {
-        margin-left: 10px;
-      }
+  }
+  .is-other {
+    .message-text {
+      margin-left: 10px;
     }
   }
 }
 .bottom-info {
-  position: absolute;
-  bottom: 0;
   width: 100%;
   height: 160px;
+  background-color: #fff;
 }
 input {
   outline: none;

+ 16 - 4
src/views/recruit/personal/position/components/details.vue

@@ -11,8 +11,10 @@
       </div>
       <div class="banner-tags mt-4">
         <span v-for="k in desc" :key="k.mdi" class="mr-10">
-          <v-icon color="var(--color-666)" size="20">{{ k.mdi }}</v-icon>
-          <span class="ml-1">{{ positionInfo[k.value] }}</span>
+          <span v-if="positionInfo[k.value]">
+            <v-icon color="var(--color-666)" size="20">{{ k.mdi }}</v-icon>
+            <span class="ml-1">{{ positionInfo[k.value] }}</span>
+          </span>
         </span>
       </div>
       <div class="banner-tools my-4">
@@ -20,10 +22,19 @@
       </div>
       <div class="d-flex justify-space-between mb-5">
         <div>
-          <v-chip v-if="info.hire && info.hirePrice && info.hirePrice > 0" label color="primary">赏金:{{ info.hirePrice }}元</v-chip>
-          <v-chip v-if="info.hire && info.hirePoint && info.hirePoint > 0" label color="primary">积分:{{ info.hirePoint }}点</v-chip>
+          <v-chip v-if="info.hire && info.hirePrice && info.hirePrice > 0" label color="primary">赏金:{{ commissionCalculation(info.hirePrice, 1) }}元</v-chip>
+          <v-chip v-if="info.hire && info.hirePoint && info.hirePoint > 0" label color="primary">积分:{{ commissionCalculation(info.hirePoint, 1) }}点</v-chip>
         </div>
         <div class="banner-tools-btns">
+          <v-btn
+            class="radius mr-2 button-item"
+            variant="outlined"
+            color="error"
+            prepend-icon="mdi-share-outline"
+            style="height: 36px;"
+            v-if="info?.hire && info?.hirePoint && info?.hirePoint > 0"
+            @click="handleShare"
+          >我要赏金</v-btn>
           <v-btn
             class="radius mr-2 button-item"
             variant="outlined"
@@ -122,6 +133,7 @@
 </template>
 
 <script setup>
+import { commissionCalculation } from '@/utils/position'
 defineOptions({ name: 'position-details' })
 import { computed, ref } from 'vue'
 import { useRouter } from 'vue-router'

+ 100 - 32
src/views/recruit/personal/remuse/components/basicInfo.vue

@@ -30,11 +30,11 @@
       <div style="flex: 1;" class="mr-8 mt-5">
         <!-- 编辑 -->
         <div v-if="isEdit">
-          <CtForm ref="CtFormRef" :items="formItems" style="width: 100%;">
+          <CtForm ref="CtFormRef" :items="items" style="width: 100%;">
             <template v-slot:phone>
               <v-btn variant="text" class="ml-2" color="primary">{{ $t('common.change') }}</v-btn>
             </template>
-            <template #areaType="{ item }">
+            <!-- <template #areaType="{ item }">
               <v-menu :close-delay="1" :open-delay="0" v-bind="$attrs">
                 <template v-slot:activator="{  props }">
                   <textUI
@@ -46,7 +46,7 @@
                 </template>
                 <areaType :isIntType="false" :select="[baseInfo?.areaId].filter(Boolean)" @handleAreaClick="handleArea" class="jobTypeCardBox" isSingle></areaType>
               </v-menu>
-            </template>
+            </template> -->
           </CtForm>
           <div class="text-end">
             <v-btn class="half-button mr-3" variant="tonal" @click="isEdit = false">{{ $t('common.cancel') }}</v-btn>
@@ -117,9 +117,10 @@
 <script setup>
 import CtForm from '@/components/CtForm'
 import Snackbar from '@/plugins/snackbar'
-import areaType from '@/components/AreaSelect'
-import textUI from '@/components/FormUI/TextInput'
+// import areaType from '@/components/AreaSelect'
+// import textUI from '@/components/FormUI/TextInput'
 import { getDict } from '@/hooks/web/useDictionaries'
+import { cityToProvince } from '@/utils/areaDeal'
 import { getTimeStamp, timesTampChange } from '@/utils/date'
 import { updatePersonAvatar, saveResumeBasicInfo } from '@/api/recruit/personal/resume'
 import { useUserStore } from '@/store/user'
@@ -163,7 +164,7 @@ const handleUploadFile = async (e) => {
   getBasicInfo()
 }
 
-const formItems = ref({
+const items = ref({
   options: [
     {
       type: 'text',
@@ -214,16 +215,16 @@ const formItems = ref({
       // disabled: true,
       // slotName: 'phone',
     },
-    {
-      slotName: 'areaType',
-      key: 'areaId',
-      value: null,
-      label: '所在城市 *',
-      nameKey: 'areaName', // 展示出来id对应的内容
-      col: 6,
-      flexStyle: 'mr-3',
-      rules: [v => !!v || '请选择所在城市']
-    },
+    // {
+    //   slotName: 'areaType',
+    //   key: 'areaId',
+    //   value: null,
+    //   label: '所在城市 *',
+    //   nameKey: 'areaName', // 展示出来id对应的内容
+    //   col: 6,
+    //   flexStyle: 'mr-3',
+    //   rules: [v => !!v || '请选择所在城市']
+    // },
     {
       type: 'text',
       key: 'email',
@@ -304,6 +305,35 @@ const formItems = ref({
       rules: [v => !!v || '请选择婚姻状况'],
       items: []
     },
+    {
+      type: 'autocomplete',
+      key: 'workAreaProvinceId',
+      value: null,
+      label: '所在城市:省 *',
+      outlined: true,
+      itemText: 'name',
+      itemValue: 'id',
+      returnSelect: true,
+      noParam: true,
+      col: 6,
+      flexStyle: 'mr-3',
+      rules: [v => !!v || '请选择所在城市:省'],
+      items: [],
+      change: null
+    },
+    {
+      type: 'autocomplete',
+      key: 'areaId',
+      value: null,
+      label: '所在城市:市 *',
+      outlined: true,
+      itemText: 'name',
+      itemValue: 'id',
+      col: 6,
+      rules: [v => !!v || '请选择所在城市:市'],
+      items: [],
+      change: null
+    },
     {
       type: 'datePicker',
       key: 'firstWorkTime',
@@ -326,7 +356,7 @@ const handleSave = async () => {
   const { valid } = await CtFormRef.value.formRef.validate()
   if (!valid) return
   const obj = {}
-  formItems.value.options.forEach(e => {
+  items.value.options.forEach(e => {
     if (e.type === 'datepicker') obj[e.key] = getTimeStamp(e.value)
     else obj[e.key] = e.value
   })
@@ -339,28 +369,41 @@ const handleSave = async () => {
 }
 
 // 城市
-const setValue = (key, id, name) => {
-  const item =  formItems.value.options.find(e => e.key === key)
-  if (item) {
-    item.value = id
-    item[item.nameKey] = name
-  }
-}
-const handleArea = (list, name) => {
-  if (!list.length) return
-  const id = list[0]
-  setValue('areaId', id, name)
-}
+// const setValue = (key, id, name) => {
+//   const item =  items.value.options.find(e => e.key === key)
+//   if (item) {
+//     item.value = id
+//     item[item.nameKey] = name
+//   }
+// }
+// const handleArea = (list, name) => {
+//   if (!list.length) return
+//   const id = list[0]
+//   setValue('areaId', id, name)
+// }
 
 // 获取字典内容
 const getDictData = async (dictTypeName) => {
-  const item = formItems.value.options.find(e => e.dictTypeName === dictTypeName)
+  const item = items.value.options.find(e => e.dictTypeName === dictTypeName)
   if (item) {
     const { data } = await getDict(dictTypeName)
     item.items = data
   }
 }
-formItems.value.options.forEach((e, index) => {
+let workAreaId = ''
+const deal = async () => {
+  if (workAreaId) { // 省份回显
+    const province = items.value.options.find(pv => pv.key === 'workAreaProvinceId')
+    if (province) {
+      const dealReturnObj = await cityToProvince(workAreaId, {}, province.items || [])
+      const city = items.value.options.find(pv => pv.key === 'areaId')
+      if (city) city.items = dealReturnObj.cityList || []
+      province.value = dealReturnObj.pid || ''
+      console.log(dealReturnObj, 'dealReturnObj', province)
+    }
+  }
+}
+items.value.options.forEach((e, index) => {
   if ((index + 2) % 2 === 0) e.flexStyle = 'mr-3'
   if (e.dictTypeName) getDictData(e.dictTypeName) // 查字典set options
   // formItems回显
@@ -369,7 +412,32 @@ formItems.value.options.forEach((e, index) => {
   // 日期相关
   if (e.type === 'datepicker') e.value = timesTampChange(e.value).slice(0, 10)
   // 所在城市回显
-  if (infoExist && e.nameKey) e[e.nameKey] = baseInfo.value[e.nameKey]
+  // if (infoExist && e.nameKey) e[e.nameKey] = baseInfo.value[e.nameKey]
+  if (infoExist && e.key === 'areaId' && baseInfo.value[e.key]) {
+    workAreaId = baseInfo.value[e.key]
+    deal()
+  }
+})
+
+const provinceChange = (value, val, obj) => {
+  const item = items.value.options.find(e => e.key === 'areaId')
+  if (!item) return
+  item.items = obj.children || []
+  item.value = null
+}
+getDict('areaTreeData', null, 'areaTreeData').then(({ data }) => {
+  data = data?.length && data || []
+  if (!data?.length) return console.error('areaTreeData获取失败!')
+  //
+  const china = data.find(e => e.id === '1')
+  const chinaTreeData = china?.children?.length ? china.children : []
+  //
+  if (!chinaTreeData?.length) return console.error('chinaTreeData获取失败!')
+  const item = items.value.options.find(e => e.key === 'workAreaProvinceId')
+  if (item?.items) {
+    item.items = chinaTreeData
+    item.change = provinceChange
+  }
 })
 </script>
 

+ 45 - 44
src/views/recruit/personal/remuse/components/jobIntention.vue

@@ -61,19 +61,6 @@
             <industryTypeCard :select="query.industryIdList" :currentData="currentSelect" showSelect @handleClickIndustry="handleIndustry"></industryTypeCard>
           </v-menu>
         </template>
-        <!-- <template #workAreaId_old="{ item }">
-          <v-menu :close-delay="1" :open-delay="0" v-bind="$attrs">
-            <template v-slot:activator="{  props }">
-              <textUI
-                v-model="item.value"
-                :item="item"
-                v-bind="props"
-                style="position: relative;"
-              ></textUI>
-            </template>
-            <areaType :select="[query.workAreaId_old].filter(Boolean)" @handleAreaClick="handleArea" class="jobTypeCardBox" isSingle></areaType>
-          </v-menu>
-        </template> -->
       </CtForm>
       <div class="text-end">
         <v-btn class="half-button mr-3" variant="tonal" @click="isAdd = false; resetForm()">{{ $t('common.cancel') }}</v-btn>
@@ -89,12 +76,12 @@ import CtForm from '@/components/CtForm'
 import textUI from '@/components/FormUI/TextInput'
 import jobTypeCard from '@/components/jobTypeCard'
 import industryTypeCard from '@/components/industryTypeCard'
-// import areaType from '@/components/AreaSelect'
 import Snackbar from '@/plugins/snackbar'
 import Confirm from '@/plugins/confirm'
 import { saveResumeJobInterested, getResumeJobInterested, deleteResumeJobInterested } from '@/api/recruit/personal/resume'
 import { dealJobData } from './dict'
 import { getDict } from '@/hooks/web/useDictionaries'
+import { cityToProvince } from '@/utils/areaDeal'
 
 const isAdd = ref(false)
 const formPageRef = ref()
@@ -142,31 +129,33 @@ const items = ref({
       outlined: true,
       rules: [v => !!v || '请输入薪资最高要求']
     },
-    // {
-    //   slotName: 'workAreaId_old',
-    //   key: 'workAreaId_old',
-    //   value: null,
-    //   label: '工作城市 *',
-    //   valueKey: 'workArea',
-    //   col: 6,
-    //   flexStyle: 'mr-3',
-    //   rules: [v => !!v || '请选择工作城市']
-    // },
     {
-      // slotName: 'workAreaId',
-      // type: 'citySelect',
-      type: 'nestedListGroup',
-      key: 'workAreaId',
+      type: 'autocomplete',
+      key: 'workAreaProvinceId',
       value: null,
-      label: '工作城市 *',
+      label: '工作城市:省 *',
+      outlined: true,
       itemText: 'name',
       itemValue: 'id',
+      returnSelect: true,
       col: 6,
       flexStyle: 'mr-3',
-      selectLevel: 2,
+      rules: [v => !!v || '请选择工作城市:省'],
       items: [],
-      rules: [v => !!v || '请选择工作城市'],
-      // valueKey: 'workArea',
+      change: null
+    },
+    {
+      type: 'autocomplete',
+      key: 'workAreaId',
+      value: null,
+      label: '工作城市:市 *',
+      outlined: true,
+      itemText: 'name',
+      itemValue: 'id',
+      col: 6,
+      rules: [v => !!v || '请选择工作城市:市'],
+      items: [],
+      change: null
     },
     {
       type: 'autocomplete',
@@ -175,7 +164,7 @@ const items = ref({
       label: '求职类型 *',
       outlined: true,
       itemText: 'label',
-      col: 6,
+      col: 12,
       itemValue: 'value',
       rules: [v => !!v || '请选择求职类型'],
       items: [
@@ -188,6 +177,12 @@ const items = ref({
   ]
 })
 
+const provinceChange = (value, val, obj) => {
+  const item = items.value.options.find(e => e.key === 'workAreaId')
+  if (!item) return
+  item.items = obj.children || []
+  item.value = null
+}
 getDict('areaTreeData', null, 'areaTreeData').then(({ data }) => {
   data = data?.length && data || []
   if (!data?.length) return console.error('areaTreeData获取失败!')
@@ -196,10 +191,12 @@ getDict('areaTreeData', null, 'areaTreeData').then(({ data }) => {
   const chinaTreeData = china?.children?.length ? china.children : []
   //
   if (!chinaTreeData?.length) return console.error('chinaTreeData获取失败!')
-  const item = items.value.options.find(e => e.key === 'workAreaId')
-  if (item?.items) item.items = chinaTreeData
+  const item = items.value.options.find(e => e.key === 'workAreaProvinceId')
+  if (item?.items) {
+    item.items = chinaTreeData
+    item.change = provinceChange
+  }
 })
-getDict()
 
 // 获取求职意向
 const interestList = ref([])
@@ -232,13 +229,6 @@ const handleIndustry = (list, arr) => {
   setValue('industryIdList', str)
 }
 
-// // 工作城市
-// const handleArea = (list, name) => {
-//   if (!list.length) return
-//   query.workAreaId_old = list[0]
-//   setValue('workAreaId_old', name)
-// }
-
 const resetForm = () => {
   items.value.options.forEach(e => {
     if (e.key === 'industryIdList') e.value = ''
@@ -264,15 +254,26 @@ const handleSave = async () => {
   getJobInterested()
 }
 
-const handleEdit = (item) => {
+const handleEdit = async (item) => {
   editId.value = item.id
+  let workAreaId = ''
   items.value.options.forEach(e => {
     query[e.key] = item[e.key] 
     if (e.valueKey) {
       e.value = item[e.valueKey]
     } else e.value = item[e.key]
     if (e.key === 'industryIdList') e.value = item.industry.map(e => e.nameCn).join('、')
+    if (e.key === 'workAreaId' && item[e.key]) workAreaId = item[e.key]
   })
+  if (workAreaId) { // 省份回显
+    const province = items.value.options.find(pv => pv.key === 'workAreaProvinceId')
+    if (province) {
+      const dealReturnObj = await cityToProvince(workAreaId, {}, province.items || [])
+      const city = items.value.options.find(pv => pv.key === 'workAreaId')
+      if (city) city.items = dealReturnObj.cityList || []
+      province.value = dealReturnObj.pid || ''
+    }
+  }
   currentSelect = item.industry
   isAdd.value = true
 }

+ 3 - 2
src/views/recruit/personal/shareJob/index.vue

@@ -34,8 +34,8 @@
           <publicRecruitment v-if="info.hire" width="50" height="60"></publicRecruitment>
         </div>
         <div v-if="info.hire" class="mt-3">
-          <v-chip v-if="info.hirePrice" label color="primary">赏金:{{ info.hirePrice }}元</v-chip>
-          <v-chip v-if="info.hirePoint" label color="primary">积分:{{ info.hirePoint }}点</v-chip>
+          <v-chip v-if="info.hirePrice" label color="primary">赏金:{{ commissionCalculation(info.hirePrice, 1) }}元</v-chip>
+          <v-chip v-if="info.hirePoint" label color="primary">积分:{{ commissionCalculation(info.hirePoint, 1) }}点</v-chip>
         </div>
         <v-divider class="mt-3"></v-divider>
         <div class="mt-3 mb-1 f-w-600">{{ $t('position.jobResponsibilities') }}</div>
@@ -96,6 +96,7 @@
 </template>
 
 <script setup>
+import { commissionCalculation } from '@/utils/position'
 defineOptions({name: 'recruit-personal-shareJob-index'})
 import loginPage from './components/login.vue'
 import simplePage from './sendResume/simple.vue'