Procházet zdrojové kódy

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

lifanagju_citu před 9 měsíci
rodič
revize
431b4361bd

+ 1 - 0
components.d.ts

@@ -25,6 +25,7 @@ declare module 'vue' {
     Details: typeof import('./src/components/Enterprise/details.vue')['default']
     Echarts: typeof import('./src/components/Echarts/index.vue')['default']
     Empty: typeof import('./src/components/Empty/index.vue')['default']
+    File: typeof import('./src/components/Upload/file.vue')['default']
     HeadSearch: typeof import('./src/components/headSearch/index.vue')['default']
     HotPromoted: typeof import('./src/components/Enterprise/hotPromoted.vue')['default']
     Img: typeof import('./src/components/Upload/img.vue')['default']

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

@@ -45,4 +45,11 @@ export const feedbackInterviewInvite = async (data) => {
     url: '/app-admin-api/menduner/system/interview-invite/feedback',
     data
   })
+}
+
+// 获取当前企业有面试的日期列表
+export const getEnterpriseInterviewCountByTime = async () => {
+  return await request.get({
+    url: '/app-admin-api/menduner/system/interview-invite/get/count/by/time'
+  })
 }

+ 3 - 6
src/components/Enterprise/details.vue

@@ -47,7 +47,7 @@
           <div class="content-right">
             <div class="welfare">
               <h4>工作时间及福利</h4>
-              <div class="my-3" style="color: var(--color-666);font-size: 14px;">
+              <div v-if="info.enterprise?.workTime" class="my-3" style="color: var(--color-666);font-size: 14px;">
                 <v-icon size="17" color="#ccc" class="mr-2">mdi-clock</v-icon>{{ info.enterprise.workTime }}
               </div>
               <div class="welfare-tags">
@@ -56,13 +56,10 @@
             </div>
             <div class="welfare my-3">
               <h4>工商信息</h4>
-              <div :class="['mt-2', 'business-item']" v-for="(val, index) in businessList" :key="val.value">
+              <div :class="['mt-2', 'business-item']" v-for="val in businessList" :key="val.value">
                 <div>{{ val.label }}</div>
                 <div class="business-value ellipsis">{{ info.business[val.value] }}</div>
-                <div :class="['my-3', {'border-bottom-dashed': index === businessList.length - 1 }]"></div>
-              </div>
-              <div class="business-source">
-                数据来源:企查查
+                <div :class="['my-3']"></div>
               </div>
             </div>
           </div>

+ 1 - 0
src/components/FormUI/autocomplete/index.vue

@@ -20,6 +20,7 @@
       :hide-no-data="item.hideNoData"
       :no-data-text="item.noDataText || 'No data available'"
       :hide-selected="item.hideSelected"
+      :hide-details="item.hireDetails || false"
       @update:search="v => item.search ? debouncedCallbackSearch(v) : null"
       @update:modelValue="modelValueUpDate"
     ></v-autocomplete>

+ 1 - 1
src/components/FormUI/datePicker/index.vue

@@ -135,7 +135,7 @@ if (item.value) value.value = item.value - 0; getFormatText()
 </script>
 <style lang="scss" scoped>
 // .removeDetailHeight {}
-::v-deep .dp--menu-wrapper {
+:deep(.dp--menu-wrapper) {
   // top: 50px !important;
   left: 0 !important;
 }

+ 56 - 0
src/components/Upload/file.vue

@@ -0,0 +1,56 @@
+<template>
+  <input type="file" ref="fileInput" :accept="accept" style="display: none;" @change="handleUploadFile"/>
+</template>
+
+<script setup>
+defineOptions({ name: 'upload-file'})
+import { ref } from 'vue'
+import Snackbar from '@/plugins/snackbar'
+import { useI18n } from '@/hooks/web/useI18n'
+import { uploadFile } from '@/api/common'
+
+const emits = defineEmits(['success'])
+defineProps({
+  accept: {
+    type: String,
+    default: '.pdf, .doc, .docx'
+  }
+})
+
+const { t } = useI18n()
+
+const clicked = ref(false)
+const fileInput = ref()
+
+const trigger = () => {
+  if (clicked.value) return
+  clicked.value = true
+  fileInput.value.click()
+  clicked.value = false
+}
+
+const handleUploadFile = async (e) => {
+  if (!e.target.files.length) return
+
+  const file = e.target.files[0]
+  const size = file.size
+  if (size / (1024*1024) > 10) {
+    Snackbar.warning(t('common.fileSizeExceed'))
+    return
+  }
+  const arr = file.name.split('.')
+  const formData = new FormData()
+  formData.append('file', file)
+  const { data } = await uploadFile(formData)
+  if (!data) return
+  emits('success', data, arr[0])
+}
+
+defineExpose({
+  trigger
+})
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 14 - 12
src/hooks/web/useIM.js

@@ -27,15 +27,24 @@ export const defaultText = '您好,关注到您发布该职位信息,请问
 // 企业默认招呼语
 export const defaultTextEnt = '您好,我们正在寻找充满激情、勇于挑战的您,快来和我聊一聊吧~'
 
-const ObjType = [ 101, 102, 103, 104 ]
 
 const { ObjectContent } = initRegister(101)
 const { ObjectContent: ObjectContent2 } = initRegister(102)
 const { ObjectContent: ObjectContent3 } = initRegister(103)
 const { ObjectContent: ObjectContent4 } = initRegister(104)
 const { ObjectContent: ObjectContent5 } = initRegister(105) // 发送简历
+
+
+const contentType = {
+  101: ObjectContent,
+  102: ObjectContent2,
+  103: ObjectContent3,
+  104: ObjectContent4,
+  105: ObjectContent5, // 发送简历
+}
+
 // 注册消息体
-function initRegister (contentType) {
+function initRegister (type) {
   class ObjectContent extends MessageContent {
     constructor(text) {
       super();
@@ -47,7 +56,7 @@ function initRegister (contentType) {
     }
     get contentType() {
         // 这里需要实现具体的逻辑
-        return contentType; // 示例实现
+        return type; // 示例实现
     }
     decodeJSON(content) {
         this.content = content.text;
@@ -59,7 +68,7 @@ function initRegister (contentType) {
     }
   }
   // 注册101类型为面试
-  WKSDK.shared().register(contentType, () => new ObjectContent())
+  WKSDK.shared().register(type, () => new ObjectContent())
   return {
     ObjectContent
   }
@@ -133,7 +142,7 @@ export function useDataSource () {
         // const message = Convert.toMessage(msg);
         // msg.channel = new Channel(msg.channel_id, msg.channel_type)
         msg.payload = JSON.parse(Base64.decode(msg.payload))
-        if (ObjType.includes(msg.payload.type)) {
+        if (contentType[msg.payload.type]) {
           msg.payload.content = JSON.parse(msg.payload.content ?? '{}')
         }
         resultMessages.push(msg)
@@ -359,13 +368,6 @@ export async function getMoreMessages (pageSize, channel) {
  * @returns 
  */
   // 发送职位使用101
-const contentType = {
-  101: ObjectContent,
-  102: ObjectContent2,
-  103: ObjectContent3, // 求职者拒绝面试邀请
-  104: ObjectContent4, // 求职者接受面试邀请
-  105: ObjectContent5, // 发送简历
-}
 export function send (text, _channel, type) {
   let _text
   if (contentType[type]) {

+ 1 - 0
src/locales/en.js

@@ -245,6 +245,7 @@ export default {
     uploadUpToFiveCopies: 'Upload up to 5 copies',
     accountWithdrawal: 'Account withdrawal',
     goldCoins: 'Remaining gold coins',
+    requestResume: 'I would like a copy of your resume, if you agree?'
   },
   setting: {
     accountSettings: 'Account Settings',

+ 1 - 0
src/locales/zh-CN.js

@@ -245,6 +245,7 @@ export default {
     uploadUpToFiveCopies: '最多上传5份',
     accountWithdrawal: '账户取现',
     goldCoins: '剩余金币',
+    requestResume: '我想要一份您的简历,您是否同意'
   },
   setting: {
     accountSettings: '账号设置',

+ 8 - 17
src/utils/date.js

@@ -41,25 +41,16 @@ export const getDayBounds = (timestamp) => {
 }
 
 // 传入 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)
+export const getStartAndEndOfDay = (dateTimeStr) => {
+  const date = new Date(dateTimeStr)
 
-  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
-  }
+  const year = date.getFullYear()
+  const month = String(date.getMonth() + 1).padStart(2, '0')
+  const day = String(date.getDate()).padStart(2, '0')
 
-  return [formatDate(startDate), formatDate(endDate)]
+  const startTime = `${year}-${month}-${day} 00:00:00`
+  const endTime = `${year}-${month}-${day} 23:59:59`
+  return [startTime, endTime]
 }
 
 // 传入一组时间戳,返回 [最早时间点,最晚时间点]

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

@@ -17,7 +17,7 @@
           <span class="name">{{ info.name }}</span>
           <template v-if="info.enterpriseId">
             <span>{{ info.postNameCn }}</span>
-            <span class="septal-line"></span>
+            <span v-if="info.postNameCn && info.enterpriseName" class="septal-line"></span>
             <span>{{ info.enterpriseName }}</span>
           </template>
         </p>
@@ -101,7 +101,7 @@
             <div style="width: 40px; height: 40px;">
               <v-avatar>
                 <v-img
-                  :src="val.from_uid === IM.uid ? mAvatar : getUserAvatar(info.avatar, info.sex)"
+                  :src="(val.from_uid === IM.uid ? mAvatar : getUserAvatar(info.avatar, info.sex)) || 'https://minio.citupro.com/dev/menduner/7.png'"
                   :width="40"
                   height="40"
                   rounded
@@ -133,6 +133,23 @@
                 接受了面试邀请
               </v-chip>
             </div>
+            <div v-else-if="val.payload.type === 105">
+              <v-chip class="ma-2" label color="primary" v-if="val.from_uid === IM.uid">
+                <v-icon icon="mdi-check" start></v-icon>
+                {{ val.payload.content.type === 1 ? '附件简历已发送' : '简历请求已发送' }}
+              </v-chip>
+              <v-card v-else width="300" class="pa-3 ma-2" color="teal" variant="tonal" :elevation="3">
+                <v-card-text class="d-flex">
+                  <p v-if="val.payload.content.type === 1">{{ val.payload.content.query.title || t('resume.attachmentResume') }}</p>
+                  <p v-if="val.payload.content.type === 2">{{ t('resume.requestResume') }}</p>
+                </v-card-text>
+                <v-card-actions class="justify-center">
+                  <!-- <v-btn variant="tonal" flat size="small" color="error" @click="handleRejectReceive(val.payload)">拒绝</v-btn> -->
+                  <v-btn v-if="val.payload.content.type === 1" block  variant="tonal" flat size="small" color="success" @click="handlePreview(val.payload)">点击预览附件简历</v-btn>
+                  <v-btn v-if="val.payload.content.type === 2" block  variant="tonal" flat size="small" color="success" @click="handleSendResume(val.payload)">点击发送附件简历</v-btn>
+                </v-card-actions>
+              </v-card>
+            </div>
             <div v-else class="message-text" :class="{ active: val.from_uid === IM.uid}">
               {{ val.payload?.content }}
             </div>
@@ -169,6 +186,9 @@
               </v-card-item>
             </v-card>
           </div>
+          <!-- <div class="d-flex justify-center" v-if="val.payload.type === 105">
+            <v-chip>已成功发送简历</v-chip>
+          </div> -->
         </div>
       </div>
     </div>
@@ -204,6 +224,7 @@ defineOptions({ name: 'message-chatting'})
 import { ref, nextTick, onMounted, inject } from 'vue'
 import { timesTampChange } from '@/utils/date'
 import { useIMStore } from '@/store/im'
+import { useI18n } from '@/hooks/web/useI18n'
 import { useRouter } from 'vue-router';
 import { getDict } from '@/hooks/web/useDictionaries'
 // import { getDictValueWithLabel } from '@/utils/position'
@@ -211,6 +232,7 @@ import { getUserAvatar } from '@/utils/avatar'
 
 import { useUserStore } from '@/store/user'
 const isEnterprise = inject('isEnterprise')
+const { t } = useI18n()
 
 const emits = defineEmits(['handleMore', 'handleSend', 'handleAgree', 'handleRefuse'])
 
@@ -334,6 +356,23 @@ const handleToCenter = () => {
   router.push({ path: '/recruit/personal/personalCenter', query: { showInterviewScheduleMore: true } })
 }
 
+// 简历预览
+const handlePreview = (val) => {
+  emits('handlePreview', val)
+}
+// 发送简历
+const handleSendResume = (val) => {
+  emits('handleSendResume', val)
+}
+// 拒绝接收简历
+// const handleRejectReceive = (val) => {
+//   emits('handleRejectReceive', val)
+// }
+// // 同意接收简历
+// const handleAccessReceive = (val) => {
+//   emits('handleAccessReceive', val)
+// }
+
 defineExpose({
   reset,
   changeLoading,

+ 140 - 21
src/views/recruit/components/message/index.vue

@@ -81,17 +81,27 @@
         @handleMore="handleGetMore"
         @handleAgree="handleAgree"
         @handleRefuse="handleRefuse"
+        @handlePreview="handlePreview"
+        @handleSendResume="handleSendResume"
       >
         <template #tools>
           <v-btn
             v-for="tool in tools"
             :key="tool.name"
             size="small"
-            :prepend-icon="tool.icon"
             class="mr-3"
             :color="tool.color"
             @click="tool.handle(tool)"
           >
+            <v-progress-circular
+              v-if="tool.loading"
+              :width="2"
+              :size="16"
+              color="white"
+              class="mr-2"
+              indeterminate
+            ></v-progress-circular>
+            <v-icon v-else class="mr-2">{{ tool.icon }}</v-icon>
             {{ tool.name }}
           </v-btn>
         </template>
@@ -99,9 +109,21 @@
     </div>
   </div>
 
+  <File ref="uploadFile" @success="handleUploadResume"></File>
+
   <CtDialog :visible="showInvite" :widthType="2" titleClass="text-h6" title="邀请面试" @close="showInvite = false" @submit="handleSubmit">
     <InvitePage v-if="showInvite" ref="inviteRef" :item-data="itemData" :position="positionList"></InvitePage>
   </CtDialog>
+
+  <CtDialog :visible="showResume" :widthType="2" titleClass="text-h6" title="发送简历" @close="showResume = false; selectResume = null " @submit="handleSubmitResume">
+    
+    <div style="position: relative; min-height: 200px">
+      <v-radio-group v-model="selectResume">
+        <v-radio v-for="val in resumeList" :key="val.id" :value="val.id" :label="val.title" color="primary"></v-radio>
+      </v-radio-group>
+    </div>
+    
+  </CtDialog>
 </template>
 
 <script setup>
@@ -123,8 +145,11 @@ import { getUserAvatar } from '@/utils/avatar'
 import { getJobAdvertised } from '@/api/enterprise'
 import { dealDictArrayData } from '@/utils/position'
 import { saveInterviewInvite } from '@/api/recruit/enterprise/interview'
+import { savePersonResumeCv } from '@/api/recruit/personal/resume'
 import { useI18n } from '@/hooks/web/useI18n'
 import { userInterviewInviteReject, userInterviewInviteConsent } from '@/api/recruit/personal/personalCenter'
+import { getPersonResumeCv } from '@/api/recruit/personal/resume'
+import { previewFile } from '@/utils'
 import Confirm from '@/plugins/confirm'
 
 const { t } = useI18n()
@@ -152,6 +177,30 @@ const itemData = ref({})
 
 const inviteRef = ref()
 
+// 发送简历
+const uploadFile = ref()
+const showResume = ref(false)
+const resumeList = ref([])
+const selectResume = ref(null)
+
+// 求职者面试列表
+const interview = ref([])
+
+const showRightNoData = ref(false)
+
+const info = ref({})
+
+const enterpriseTools = ref([
+  { name: '求简历', icon: 'mdi-email', color:"primary", loading: false, handle: handleRequest },
+  { name: '面试邀约', icon: 'mdi-email', color:"primary", loading: false, handle: handleInvite }
+])
+
+const userTools = ref([
+  { name: '发送简历', icon: 'mdi-email', color:"primary", loading: false, handle: handleSendResume }
+])
+
+const tools = isEnterprise ? enterpriseTools.value : userTools.value
+
 if (!IM) {
   console.log('IM is disconnected')
 }
@@ -195,8 +244,7 @@ const {
   chatRef.value.scrollBottom()
 })
 
-// 求职者面试列表
-const interview = ref([])
+
 const getInterviewInviteList = async () => {
   if (!info.value.userId) return
   const data = await getInterviewInviteListByInviteUserId(info.value.userId)
@@ -223,24 +271,6 @@ watch(
 )
 
 
-const showRightNoData = ref(false)
-
-const info = ref({})
-
-const handleUpdate = (val) => {
-  send(val.value, channelItem.value)
-}
-
-
-const enterpriseTools = ref([
-  { name: '面试邀约', icon: 'mdi-email', color:"primary", loading: false, handle: handleInvite }
-])
-
-const userTools = ref([
-])
-
-const tools = isEnterprise ? enterpriseTools.value : userTools.value
-
 async function handleChange (items) {
   // console.log([...items])
   try {
@@ -253,6 +283,7 @@ async function handleChange (items) {
     const userId = userInfoVo.userInfoResp.userId
     const enterpriseId = userInfoVo.userInfoResp.enterpriseId || undefined
     const { channel, list, more } = await initChart(userId, enterpriseId)
+    // console.log('--------',list)
     channelItem.value = channel.value
     messageItems.value = list.value
     hasMore.value = more
@@ -269,6 +300,94 @@ async function handleChange (items) {
   }
 }
 
+// 普通消息
+const handleUpdate = (val) => {
+  send(val.value, channelItem.value)
+}
+
+// 获取简历
+async function handleSendResume (item) {
+  try {
+    item.loading = true
+    // showResume.value = true
+    // 获取简历列表
+    const result = await getPersonResumeCv()
+    if (result.length === 0) {
+      Snackbar.error(t('resume.resumeYetSubmit'))
+      uploadFile.value.trigger()
+      return
+    }
+    resumeList.value = result
+    showResume.value = true
+  } finally {
+    item.loading = false
+  }
+}
+
+/**
+ * 发送简历
+ * text param
+ * {
+ *  remark: 备注
+ *  query: {} 自定义参数 access -1 未确定 0 拒绝 1 同意
+ *  type: 1 => 发送简历
+ *        2 => 索要简历
+ *        3 => 信息描述
+ * }
+ */
+
+// 没有上传过简历的弹窗上传并发送给对方
+const handleUploadResume = async (url, title) => {
+  if (!url || !title) return
+  await savePersonResumeCv({ title, url })
+  const text = {
+    remark: '发送简历',
+    query: {
+      src: url,
+      title
+    },
+    type: 1
+  }
+  send (JSON.stringify(text), channelItem.value, 105)
+}
+
+function handleSubmitResume () {
+  if (!selectResume.value) {
+    Snackbar.error(t('resume.selectResumeToSubmit'))
+    return
+  }
+  const _info = resumeList.value.find((item) => item.id === selectResume.value)
+  const text = {
+    remark: '发送简历',
+    query: {
+      src: _info.url,
+      title: _info.title,
+      id: _info.id,
+    },
+    type: 1
+  }
+  send (JSON.stringify(text), channelItem.value, 105)
+  showResume.value = false
+}
+// 求简历
+function handleRequest () {
+  const text = {
+    remark: '求简历',
+    query: {
+      src: '',
+      title: '',
+      id: '',
+    },
+    type: 2
+  }
+  send (JSON.stringify(text), channelItem.value, 105)
+}
+
+// 简历预览
+const handlePreview = (val) => {
+  previewFile(val.content.query.src)
+}
+
 const handleGetMore = async () => {
   try {
     chatRef.value.changeLoading(true)

+ 71 - 44
src/views/recruit/enterprise/interview/index.vue

@@ -1,42 +1,15 @@
-<!-- 面试 -->
 <template>
-  <v-card class="pa-5" style="height: 100%;font-size: 14px;">
+  <v-card class="pa-5" style="height: calc(100vh - 130px);font-size: 14px;">
     <div class="d-flex justify-space-between">
       <div class="d-flex mb-3">
-        <!-- 职位 -->
-        <v-autocomplete 
-          v-model="query.jobId" 
-          :items="positionItems" 
-          density="compact" 
-          variant="outlined" 
-          item-title="label" 
-          item-value="value"
-          clearable
-          hide-details
-          label="职位"
-          color="primary"
-          style="width: 300px;"
-          class="mr-3"
-        ></v-autocomplete>
-        <v-select 
-          v-model="query.status" 
-          :items="statusList" 
-          density="compact" 
-          variant="outlined" 
-          item-title="label" 
-          item-value="value"
-          clearable
-          hide-details
-          label="面试状态"
-          color="primary"
-          style="width: 300px;"
-        ></v-select>
+        <Autocomplete class="mr-3" v-model="query.jobId" :item="jobItem"></Autocomplete>
+        <Autocomplete v-model="query.status" :item="statusItem"></Autocomplete>
         <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>
     <v-divider class="mb-3"></v-divider>
-    <div class="d-flex">
+    <div class="d-flex" style="height: calc(100vh - 228px);">
       <div>
         <div class="d-flex justify-space-between px-5">
           <div v-if="selectDateValue">
@@ -44,21 +17,30 @@
             <span class="ml-2" style="cursor: pointer;" @click="handleClear">{{ $t('common.cleanUp') }}</span>
           </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>
+          <v-btn color="primary" variant="text" size="small" @click="selectDateValue = new Date(); handleChangeDate()">{{ $t('interview.today') }}</v-btn>
         </div>
-        <v-date-picker
+        <VueDatePicker 
+          class="mr-5"
           v-model="selectDateValue"
-          color="primary"
-          :hide-header="true"
-          @update:modelValue="handleChangeDate"
-          class="mr-3"
+          inline auto-apply
+          locale="zh-CN"
+          :enable-time-picker="false"
+          :day-names="['一', '二', '三', '四', '五', '六', '七']"
+          :markers="markers"
+          hide-offset-dates
+          @update:model-value="handleChangeDate"
         >
-        </v-date-picker>
+          <template #marker>
+            <span class="custom-marker"></span>
+          </template>
+      </VueDatePicker>
       </div>
       <v-divider style="height: auto;" class="mr-5" vertical></v-divider>
-      <div style="flex: 1;overflow: hidden;">
+      <div style="flex: 1;">
         <div v-if="items.length">
-          <itemPage :items="items" :statusList="statusList" :positionItems="positionItems" @refresh="handleRefresh"></itemPage>
+          <div style="height: calc(100vh - 318px);overflow: auto;padding-right: 12px;">
+            <itemPage :items="items" :statusList="statusList" :positionItems="positionItems" @refresh="handleRefresh"></itemPage>
+          </div>
           <CtPagination
             v-if="total > 0"
             :total="total"
@@ -76,7 +58,7 @@
 <script setup>
 defineOptions({ name: 'enterprise-interview'})
 import { ref } from 'vue'
-import { getInterviewInvitePage } from '@/api/recruit/enterprise/interview'
+import { getInterviewInvitePage, getEnterpriseInterviewCountByTime } from '@/api/recruit/enterprise/interview'
 import { getDict } from '@/hooks/web/useDictionaries'
 import { getJobAdvertised } from '@/api/enterprise'
 import { dealDictArrayData } from '@/utils/position'
@@ -87,12 +69,27 @@ const items = ref([])
 const statusList = ref()
 const total = ref(0)
 const query = ref({
-  pageSize: 10,
+  pageSize: 20,
   pageNo: 1,
   status: null,
   jobId: null,
   time: []
 })
+const positionItems = ref([])
+
+const jobItem = ref({ width: 300, items: positionItems, clearable: true, hireDetails: true, label: '职位' })
+const statusItem = ref({ width: 300, items: statusList, clearable: true, hireDetails: true, label: '面试状态' })
+
+// 获取有面试的日期列表
+const markers = ref([])
+const getCountByTime = async () => {
+  const data = await getEnterpriseInterviewCountByTime()
+  if (!data || !data.length) return
+  markers.value = data.map(e => {
+    return { date: e.key, type: 'dot' }
+  })
+}
+getCountByTime()
 
 // 状态字典
 const getStatusList = async () => {
@@ -155,9 +152,8 @@ const handleReset = () => {
 }
 
 // 职位
-const positionItems = ref([])
 const getPositionList = async () => {
-  const data = await getJobAdvertised({ hire: false })
+  const data = await getJobAdvertised()
   if (!data.length) return
   const list = dealDictArrayData([], data)
   positionItems.value = list.map(e => {
@@ -168,4 +164,35 @@ getPositionList()
 </script>
 
 <style scoped lang="scss">
+:deep(.dp__menu) {
+  border: none !important;
+}
+.custom-marker {
+  position: absolute;
+  bottom: 0;
+  right: 50%;
+  transform: translateX(50%);
+  height: 8px;
+  width: 8px;
+  border-radius: 100%;
+  background-color: var(--v-primary-base);
+}
+/* 滚动条样式 */
+::-webkit-scrollbar {
+  -webkit-appearance: none;
+  width: 4px;
+  height: 0px;
+}
+/* 滚动条内的轨道 */
+::-webkit-scrollbar-track {
+  background: rgba(0, 0, 0, 0.1);
+  border-radius: 0;
+}
+/* 滚动条内的滑块 */
+::-webkit-scrollbar-thumb {
+  cursor: pointer;
+  border-radius: 5px;
+  background: rgba(0, 0, 0, 0.15);
+  transition: color 0.2s ease;
+}
 </style>

+ 6 - 34
src/views/recruit/personal/PersonalCenter/dynamic/right.vue

@@ -32,13 +32,7 @@
         <span class="title">{{ $t('resume.attachmentResume') }}</span>
         <span class="upload--text cursor-pointer" @click="openFileInput">
           {{ $t('common.upload') }}
-          <input
-            type="file"
-            ref="fileInput"
-            accept=".pdf, .doc, .docx"
-            style="display: none;"
-            @change="handleUploadFile"
-          />
+          <File ref="uploadFile" @success="handleUploadResume"></File>
         </span>
       </div>
       <span class="more-text">{{ $t('resume.uploadUpToFiveCopies') }}</span>
@@ -68,7 +62,6 @@
 <script setup>
 defineOptions({ name: 'personal-center-right'})
 import { ref } from 'vue'
-import { uploadFile } from '@/api/common'
 import { previewFile } from '@/utils'
 import { useRouter } from 'vue-router'
 import { useRoute } from 'vue-router'; const route = useRoute()
@@ -95,7 +88,6 @@ userStore.$subscribe((mutation, state) => {
 })
 
 const resumeList = ref([
-  // { icon: 'mdi-upload', title: t('resume.topResume'), desc: t('resume.increaseMoreExposure') },
   { name: 'refresh', icon: 'mdi-refresh', title: t('resume.refreshResume'), desc: t('resume.enhanceResumeActivity') },
   { name: 'interview', icon: 'mdi-account-multiple-check-outline', title: t('resume.interviewSchedule'), desc: '' },
 ])
@@ -116,37 +108,17 @@ const getList = async () => {
 getList()
 
 // 选择文件
-const fileInput = ref()
-const clicked = ref(false)
+const uploadFile = ref()
 const openFileInput = () => {
   if (attachmentList.value.length >= 5) return Snackbar.warning(t('resume.uploadFiveCopies'))
-  if (clicked.value) return
-  clicked.value = true
-  fileInput.value.click()
-  clicked.value = false
+  uploadFile.value.trigger()
 }
 
 // 上传附件
-const typeList = ['pdf', 'doc', 'docx']
-const handleUploadFile = async (e) => {
-  if (!e.target.files.length) return
-  const file = e.target.files[0]
-  const size = file.size
-  if (size / (1024*1024) > 10) {
-    Snackbar.warning(t('common.fileSizeExceed'))
-    return
-  }
-  const arr = file.name.split('.')
-  if (typeList.indexOf(arr[arr.length - 1]) < 0) {
-    Snackbar.warning(t('common.fileFormatIncorrect'))
-    return
-  }
-  const formData = new FormData()
-  formData.append('file', file)
-  const { data } = await uploadFile(formData)
-  if (!data) return
+const handleUploadResume = async (url, title) => {
+  if (!url || !title) return
   Snackbar.success(t('common.uploadSucMsg'))
-  await savePersonResumeCv({ title: file.name, url: data })
+  await savePersonResumeCv({ title, url })
   getList()
 }
 

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

@@ -14,7 +14,7 @@
     </div>
   </div>
   <!-- 快速填写简易人才信息-弹窗 -->
-  <simplePage v-if="showSimplePage" :closeable="true" closeText="暂时跳过"></simplePage>
+  <simplePage v-if="showSimplePage" :closeable="true" closeText="暂时跳过" @simpleInfoReady="handleUpdateInfo"></simplePage>
 </template>
 
 <script setup>
@@ -48,6 +48,11 @@ nextTick(() => {
 const handleSearch = (val) => {
   if (val) router.push(`/recruit/personal/position?content=${val}`)
 }
+
+// 更新用户基本信息
+const handleUpdateInfo = async () => {
+  await store.getUserBaseInfos(null)
+}
 </script>
 
 <style lang="scss" scoped>

+ 10 - 32
src/views/recruit/personal/position/components/details.vue

@@ -79,9 +79,6 @@
                     {{ info.contact.postNameCn }}</div>
                 </div>
               </div>
-              <!-- <div class="float-right">
-                <v-chip color="primary" label>{{ $t('position.currentOnline') }}</v-chip>
-              </div> -->
             </div>
             <v-divider class="my-3"></v-divider>
             <div>
@@ -113,7 +110,7 @@
     </div>
 
     <!-- 简历上传 -->
-    <input type="file" ref="fileInput" accept=".pdf, .doc, .docx" style="display: none;" @change="handleUploadFile"/>
+    <File ref="uploadFile" @success="handleUploadResume"></File>
 
     <!-- 选择简历 -->
     <selectResumeDialog v-model="showResume" :list="resumeList" @submit="handleSubmit" @close="handleClose"></selectResumeDialog>
@@ -150,8 +147,7 @@ defineOptions({ name: 'position-details' })
 import { computed, ref } from 'vue'
 import { useRouter } from 'vue-router'
 import { timesTampChange } from '@/utils/date'
-import { uploadFile } from '@/api/common'
-import { getPersonResumeCv } from '@/api/recruit/personal/resume'
+import { getPersonResumeCv, savePersonResumeCv } from '@/api/recruit/personal/resume'
 import { useI18n } from '@/hooks/web/useI18n'
 import { getPositionDetails, getSimilarPosition, getJobFavoriteCheck, getPersonJobFavorite, getPersonJobUnfavorite, jobCvRelCheckSend, jobCvRelSend } from '@/api/position'
 import { dealDictObjData, dealDictArrayData } from '@/utils/position'
@@ -247,26 +243,13 @@ const handleCollection = async () => {
 }
 
 // 投递简历时,若当前用户没有简历列表则弹窗上传简历以及投递
-const typeList = ['pdf', 'doc', 'docx']
-const handleUploadFile = async (e) => {
-  if (!e.target.files.length) return
-  const file = e.target.files[0]
-  const size = file.size
-  if (size / (1024*1024) > 10) {
-    Snackbar.warning(t('common.fileSizeExceed'))
-    return
-  }
-  const arr = file.name.split('.')
-  if (typeList.indexOf(arr[arr.length - 1]) < 0) {
-    Snackbar.warning(t('common.fileFormatIncorrect'))
-    return
-  }
-  const formData = new FormData()
-  formData.append('file', file)
-  const { data } = await uploadFile(formData)
-  if (!data) return
-
-  await jobCvRelSend({ jobId: id, title: arr[0], url: data, type: info.value.hire ? 1 : 0 })
+const uploadFile = ref()
+const handleUploadResume = async (url, title) => {
+  if (!url || !title) return
+  // 简历上传
+  await savePersonResumeCv({ title, url })
+  // 简历投递
+  await jobCvRelSend({ jobId: id, title, url, type: info.value.hire ? 1 : 0 })
   setTimeout(() => {
     Snackbar.success(t('resume.deliverySuccess'))
     deliveryCheck()
@@ -274,8 +257,6 @@ const handleUploadFile = async (e) => {
 }
 
 const showResume = ref(false)
-const clicked = ref(false)
-const fileInput = ref()
 
 // 效验是否有投递简历
 const resumeList = ref([])
@@ -287,10 +268,7 @@ const handleDelivery = async () => {
   // 没有上传过简历的先去上传
   if (!result.length) {
     Snackbar.warning('您还未上传过简历,请先上传简历')
-    if (clicked.value) return
-    clicked.value = true
-    fileInput.value.click()
-    clicked.value = false
+    uploadFile.value.trigger()
     return
   }
   showResume.value = true

+ 11 - 7
src/views/recruit/personal/position/components/rightRecommend.vue

@@ -1,15 +1,18 @@
 <template>
   <v-card class="position-box">
     <h4 class="h4 mb-3">{{ $t('position.recommend') }}</h4>
-    <div v-for="(item, index) in items" :key="index" class="mb-2 cursor-pointer" @click="handlePosition(item)">
-      <p class="recruit-name">{{ item.name }}</p>
-      <span class="recruit-salary">{{ item.payFrom }}-{{ item.payTo }}/{{ item.payName }}</span>
-      <div :class="['enterprise', {'border-bottom-dashed': index !== items.length - 1}]" @click="handleEnterprise(item)">
-        <v-img class="float-left" :src="item.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'" :width="30" :height="30"></v-img>
-        <span class="float-left enterprise-name">{{ item.anotherName }}</span>
-        <span class="float-right enterprise-address">{{ item.areaName }}</span>
+    <div v-if="items.length">
+      <div v-for="(item, index) in items" :key="index" class="mb-2 cursor-pointer" @click="handlePosition(item)">
+        <p class="recruit-name">{{ item.name }}</p>
+        <span class="recruit-salary">{{ item.payFrom }}-{{ item.payTo }}/{{ item.payName }}</span>
+        <div :class="['enterprise', {'border-bottom-dashed': index !== items.length - 1}]" @click="handleEnterprise(item)">
+          <v-img class="float-left" :src="item.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'" :width="30" :height="30"></v-img>
+          <span class="float-left enterprise-name">{{ item.anotherName }}</span>
+          <span class="float-right enterprise-address">{{ item.areaName }}</span>
+        </div>
       </div>
     </div>
+    <Empty v-else :elevation="false" message="暂无推荐职位"></Empty>
   </v-card>
 </template>
 
@@ -23,6 +26,7 @@ const items = ref([])
 
 const getList = async () => {
   const { list } = await getPromotedPosition({ pageSize: 10, pageNo: 1 })
+  if (!list.length) return items.value = []
   items.value = dealDictArrayData([], list)
 }
 getList()