index.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987
  1. <template>
  2. <div class="message" :class="{'default-width': !isEnterprise, 'py-3': !isEnterprise}" :style="`height: calc(100vh - ${isEnterprise ? '130px' : '50px'});`">
  3. <div class="message-left d-flex flex-column">
  4. <div class="message-left-search d-flex align-center px-3 justify-space-between" >
  5. <div>
  6. <v-icon class="mr-3">mdi-history</v-icon>
  7. 最近联系人
  8. </div>
  9. <div>
  10. <v-btn
  11. density="compact"
  12. :color="showDelete ? '' : 'red'"
  13. :icon="showDelete ? 'mdi-close' : 'mdi-trash-can-outline'"
  14. variant="text"
  15. @click="showDelete = !showDelete"
  16. >
  17. </v-btn>
  18. </div>
  19. <!-- {{ connected ? '连接成功': '连接失败' }} -->
  20. </div>
  21. <div class="message-chat-box mt-5">
  22. <v-overlay
  23. :model-value="!IM.connected"
  24. contained
  25. class="align-center justify-center"
  26. >
  27. <v-progress-circular
  28. color="primary"
  29. size="64"
  30. indeterminate
  31. ></v-progress-circular>
  32. </v-overlay>
  33. <div v-if="conversationList.length">
  34. <v-list density="compact" mandatory @update:selected="handleChange">
  35. <v-list-item
  36. v-for="(val, i) in conversationList"
  37. :key="i"
  38. :value="val"
  39. color="primary"
  40. class="mb-2"
  41. :active="val.channel.channelID === info?.channel?.channelID"
  42. :title="val.userInfoVo ? (val.userInfoVo.userInfoResp?.name ? val.userInfoVo.userInfoResp.name : val.userInfoVo.userInfoResp?.phone) : '系统消息'"
  43. :subtitle="timesTampChange(+val.timestamp.padEnd(13, '0'))"
  44. >
  45. <template v-slot:title="{ title }">
  46. <div v-if="!isEnterprise" class="mt-2 d-flex align-center">
  47. {{ title }}
  48. <div class="ml-3 color-666 font-size-14 enterprise-name ellipsis" v-ellipse-tooltip :style="{'color': val.channel.channelID === info?.channel?.channelID ? '#00B760' : '#666'}">
  49. {{ formatName(val.userInfoVo?.userInfoResp?.enterpriseAnotherName) }}
  50. <span class="line" v-if="val.userInfoVo?.userInfoResp?.postNameCn && val.userInfoVo?.userInfoResp?.enterpriseAnotherName"></span>
  51. {{ val.userInfoVo?.userInfoResp?.postNameCn }}
  52. </div>
  53. </div>
  54. <div v-else class="mt-2" v-ellipse-tooltip>{{ title }}</div>
  55. </template>
  56. <template v-slot:subtitle="{ subtitle }">
  57. <div class="mt-2">{{ subtitle }}</div>
  58. </template>
  59. <template v-slot:prepend>
  60. <v-avatar :image="getUserAvatar(val?.userInfoVo?.userInfoResp?.avatar, val?.userInfoVo?.userInfoResp?.sex, !val.userInfoVo ? true : false)"></v-avatar>
  61. </template>
  62. <template v-slot:append>
  63. <v-badge
  64. v-if="val.unread > 0"
  65. color="error"
  66. :content="val.unread"
  67. inline
  68. ></v-badge>
  69. <v-btn v-show="showDelete" density="compact" icon="mdi-trash-can-outline" variant="text" color="red" @click.stop="handleDelete(val)"></v-btn>
  70. </template>
  71. </v-list-item>
  72. </v-list>
  73. <div class="message-no-more-text mt-3">没有更多了</div>
  74. </div>
  75. <div v-else class="left-noData">
  76. <Empty :elevation="false" message="暂无30天内联系人" width="300" height="150"></Empty>
  77. </div>
  78. </div>
  79. </div>
  80. <div class="message-right">
  81. <div v-if="showRightNoData" class="right-noData">
  82. <Empty :elevation="false" message="与您进行过沟通的 Boss 都会在左侧列表中显示"></Empty>
  83. </div>
  84. <Chatting
  85. ref="chatRef"
  86. :items="messageItems"
  87. :info="info"
  88. :interview="interview"
  89. :has-more="hasMore"
  90. :updateConversation="updateConversation"
  91. :updateUnreadCount="updateUnreadCount"
  92. :resetUnread="resetUnread"
  93. @handleSend="handleUpdate"
  94. @handleMore="handleGetMore"
  95. @handleAgree="handleAgree"
  96. @handleRefuse="handleRefuse"
  97. @handlePreview="handlePreview"
  98. @handleSendResume="handleSendResume"
  99. @handleBack="handleBack"
  100. >
  101. <template #tools>
  102. <v-btn
  103. v-for="tool in tools"
  104. :key="tool.name"
  105. size="small"
  106. class="mr-3"
  107. :disabled="tool.disabled"
  108. :color="tool.color"
  109. @click="tool.handle(tool)"
  110. >
  111. <v-progress-circular
  112. v-if="tool.loading"
  113. :width="2"
  114. :size="16"
  115. color="white"
  116. class="mr-2"
  117. indeterminate
  118. ></v-progress-circular>
  119. <v-icon v-else class="mr-2">{{ tool.icon }}</v-icon>
  120. {{ tool.disabled ? tool.disabledText : tool.name }}
  121. </v-btn>
  122. </template>
  123. </Chatting>
  124. </div>
  125. </div>
  126. <!-- 附件上传 -->
  127. <CtDialog
  128. :visible="showUploadDialog"
  129. :widthType="2"
  130. :footer="true"
  131. title="附件简历上传"
  132. titleClass="text-h6"
  133. @close="showUploadDialog = false"
  134. @submit="handleSubmitAttachment"
  135. >
  136. <div class="color-warning mb-3" style="font-size: 13px;">* 仅支持.doc, .docx, .pdf文件且大小不能超过20MB</div>
  137. <CtForm ref="CtFormRef" :items="formItems">
  138. <template #uploadFile="{ item }">
  139. <TextInput v-model="item.value" :item="item" @click="openFileInput"></TextInput>
  140. <File ref="uploadFile" @success="handleUploadResume"></File>
  141. </template>
  142. </CtForm>
  143. <!-- 学生-实习到岗信息 -->
  144. <studentDeliveryForm v-if="baseInfo?.type && Number(baseInfo.type) === 1" ref="studentDeliveryFormRef" />
  145. </CtDialog>
  146. <!-- 面试邀请 -->
  147. <CtDialog :visible="showInvite" :widthType="4" titleClass="text-h6" title="邀请面试" @close="showInvite = false" @submit="handleSubmit">
  148. <InvitePage v-if="showInvite" ref="inviteRef" :item-data="itemData" :position="positionList"></InvitePage>
  149. </CtDialog>
  150. <TipDialog :visible="showTip" icon="mdi-check-circle-outline" message="面试邀请发送成功" @close="showTip = false">
  151. <div class="color-primary text-decoration-underline cursor-pointer" @click="handleToInterviewManagement">点击到面试中查看。</div>
  152. </TipDialog>
  153. <!-- 求简历-选择求简历的职位 -->
  154. <CtDialog :visible="showSelectPosition" :widthType="2" titleClass="text-h6" title="选择要求简历的职位" @close="showSelectPosition = false" @submit="handleRequestResumeSubmit">
  155. <CtForm v-if="showSelectPosition" ref="requestFromRef" :items="requestFormItems"></CtForm>
  156. </CtDialog>
  157. <!-- 发送简历-选择要发送的职位 -->
  158. <CtDialog :visible="openPositionSelectDialog" :widthType="2" titleClass="text-h6" title="请选择要投递的职位" @close="openPositionSelectDialog = false" @submit="selectPositionSubmit">
  159. <div style="position: relative; min-height: 200px">
  160. <v-radio-group v-model="selectJobId">
  161. <div v-for="val in entPositionList" :key="val.value" class="d-flex align-center radioBox" >
  162. <v-radio :label="val.label" :value="val.value" color="primary"></v-radio>
  163. <span class="defaultLink mx-3" style="font-size: 14px;" @click.stop="positionDetail(val)">预览</span>
  164. </div>
  165. </v-radio-group>
  166. </div>
  167. <v-btn
  168. v-if="entPositionTotal > 5"
  169. variant="text"
  170. color="primary"
  171. @click="changePositionData"
  172. >
  173. {{ entPositionListLastData ? '没有更多职位了~ 再选一遍' : '换一批'}} <v-icon size="16">mdi-refresh</v-icon>
  174. </v-btn>
  175. </CtDialog>
  176. <!-- 选择附件简历投递 -->
  177. <CtDialog :visible="showResume" :widthType="2" titleClass="text-h6" title="发送简历" @close="showResume = false; selectResume = null; enRequestPositionInfo = {}" @submit="handleSubmitResume">
  178. <div style="position: relative;">
  179. <v-radio-group v-model="selectResume">
  180. <div v-for="val in resumeList" :key="val.id" class="d-flex align-center radioBox">
  181. <v-radio :label="val.title" :value="val.id" color="primary"></v-radio>
  182. <span class="defaultLink mx-3" style="font-size: 14px;" @click.stop="previewFile(val.url)">预览</span>
  183. </div>
  184. </v-radio-group>
  185. </div>
  186. <studentDeliveryForm v-if="isStudent" ref="studentDeliveryFormRef" />
  187. </CtDialog>
  188. <Loading :visible="pageLoading" />
  189. </template>
  190. <script setup>
  191. defineOptions({ name: 'personal-message-index'})
  192. import InvitePage from '@/views/recruit/enterprise/interviewManagement/components/invite'
  193. import { ref, inject, watch, nextTick, computed } from 'vue'
  194. import { useRoute } from 'vue-router'
  195. import Chatting from './components/chatting.vue'
  196. import { initConnect, send, initChart, getMoreMessages, checkConversation } from '@/hooks/web/useIM'
  197. import { useI18n } from '@/hooks/web/useI18n'
  198. import { getPositionDetails, jobCvRelCheckSend, jobCvRelSend, jobCvRelHireSend, getJobAdvertisedSearch } from '@/api/position'
  199. import { getInterviewInviteListByInviteUserId, getMessageType } from '@/api/common'
  200. // import { getUserInfo } from '@/api/personal/user'
  201. import { getBaseInfo } from '@/api/common'
  202. import { getJobAdvertised } from '@/api/enterprise'
  203. import { saveInterviewInvite } from '@/api/recruit/enterprise/interview'
  204. import { savePersonResumeCv } from '@/api/recruit/personal/resume'
  205. import { userInterviewInviteReject, userInterviewInviteConsent } from '@/api/recruit/personal/personalCenter'
  206. import { getPersonResumeCv } from '@/api/recruit/personal/resume'
  207. import { formatName } from '@/utils/getText'
  208. import { useIMStore } from '@/store/im'
  209. import { useUserStore } from '@/store/user'
  210. import Snackbar from '@/plugins/snackbar'
  211. import Confirm from '@/plugins/confirm'
  212. import { getUserAvatar } from '@/utils/avatar'
  213. import { dealDictArrayData } from '@/utils/position'
  214. import { previewFile } from '@/utils'
  215. import { timesTampChange } from '@/utils/date'
  216. import { useRouter } from 'vue-router'
  217. import studentDeliveryForm from '@/views/recruit/personal/components/studentDeliveryForm.vue'
  218. import { getIsEnterprise } from '@/utils/auth'
  219. const { t } = useI18n()
  220. const chatRef = ref()
  221. const IM = useIMStore()
  222. // 自己的信息
  223. const { entBaseInfo } = useUserStore()
  224. const isEnterprise = inject('isEnterprise')
  225. // 实例
  226. const route = useRoute()
  227. const channelItem = ref(null)
  228. const messageItems = ref([])
  229. const pageSize = ref(1)
  230. const hasMore = ref(false)
  231. const studentDeliveryFormRef = ref()
  232. const baseInfo = ref(localStorage.getItem('baseInfo') ? JSON.parse(localStorage.getItem('baseInfo')) : {})
  233. const isStudent = ref(baseInfo.value?.type && Number(baseInfo.value.type) === 1)
  234. const positionList = ref([])
  235. const showTip = ref(false)
  236. const showInvite = ref(false)
  237. // 企业-求简历
  238. const showSelectPosition = ref(false)
  239. const requestFromRef = ref()
  240. const requestFormItems = ref({
  241. options: [
  242. {
  243. type: 'autocomplete',
  244. key: 'jobId',
  245. value: null,
  246. label: '招聘职位 *',
  247. outlined: true,
  248. clearable: false,
  249. itemText: 'label',
  250. itemValue: 'value',
  251. rules: [v => !!v || '请选择招聘职位'],
  252. items: positionList
  253. }
  254. ]
  255. })
  256. const showDelete = ref(false)
  257. const itemData = ref({})
  258. const inviteRef = ref()
  259. // 发送简历
  260. const showResume = ref(false)
  261. const resumeList = ref([])
  262. const selectResume = ref(null)
  263. // 众聘 介绍人个人id
  264. const isEmployment = ref('-1')
  265. // 上传附件简历
  266. const CtFormRef = ref()
  267. const formItems = ref({
  268. options: [
  269. {
  270. type: 'text',
  271. key: 'title',
  272. value: '',
  273. label: '附件简历名称 *',
  274. rules: [v => !!v || '请输入附件简历名称']
  275. },
  276. {
  277. slotName: 'uploadFile',
  278. key: 'url',
  279. value: '',
  280. truthValue: '',
  281. label: '点击上传附件简历 *',
  282. outline: true,
  283. accept: '.doc, .docx, .pdf',
  284. prependInnerIcon: 'mdi-file-document-outline',
  285. rules: [v => !!v || '请上传您的附件简历']
  286. }
  287. ]
  288. })
  289. // 求职者面试列表
  290. const interview = ref([])
  291. const showRightNoData = ref(false)
  292. const info = ref({})
  293. const enterpriseTools = ref([
  294. { name: '求简历', key: 'requestResume', icon: 'mdi-email', color:"primary", loading: false, handle: handleInvite, disabled: false, disabledText: '简历已接收' },
  295. { name: '面试邀约', key: 'interviewInvite', icon: 'mdi-email', color:"primary", loading: false, handle: handleInvite, disabled: false, disabledText: '面试邀约' }
  296. ])
  297. const userTools = ref([
  298. {
  299. name: '发送简历',
  300. key: 'sendResume',
  301. icon: 'mdi-email',
  302. color:"primary",
  303. loading: false,
  304. handle: handleSendResume,
  305. disabled: false,
  306. disabledText: '简历已投递'
  307. }
  308. ])
  309. // const tools = isEnterprise ? enterpriseTools.value : userTools.value
  310. const tools = computed(() => {
  311. return isEnterprise ? enterpriseTools.value : userTools.value
  312. })
  313. const positionInfo = ref({})
  314. // const isSendResume = ref(false)
  315. if (!IM) {
  316. console.log('IM is disconnected')
  317. }
  318. // 参与招聘会的职位进入需传递招聘会id
  319. // const jobFairId = ref('')
  320. // if (route.query?.jobFairId) jobFairId.value = route.query.jobFairId
  321. // 职位进入
  322. if (route.query.id) {
  323. const api = route.query.enterprise ? getPositionDetails : getBaseInfo // getBaseInfo getUserInfo
  324. // const res = await api({ id: route.query.id })
  325. const res = await api(route.query.enterprise ? { id: route.query.id } : { userId: route.query.id })
  326. if (!res) {
  327. Snackbar.error('个人资料为空')
  328. } else {
  329. const query = route.query.enterprise ? [res.contact?.userId, res.contact?.enterpriseId] : [res?.userId]
  330. nextTick(async () => {
  331. const { channel } = await checkConversation(...query)
  332. const items = [
  333. {
  334. channel,
  335. userInfoVo: {
  336. userInfoResp: route.query.enterprise ? res.contact : { ...res, userId: res?.userId}
  337. }
  338. }
  339. ]
  340. handleChange(items)
  341. })
  342. }
  343. }
  344. const {
  345. conversationList,
  346. updateConversation,
  347. updateUnreadCount,
  348. deleteConversations,
  349. resetUnread
  350. } = initConnect(async (successful) => {
  351. if (!successful) {
  352. Snackbar.error('发送失败')
  353. return
  354. }
  355. chatRef.value.reset()
  356. // 发送成功
  357. const { list } = await getMoreMessages(1, channelItem.value)
  358. // updateConversation()
  359. messageItems.value = list.value
  360. chatRef.value.scrollBottom()
  361. })
  362. const getInterviewInviteList = async () => {
  363. if (!info.value.userId) return
  364. const data = await getInterviewInviteListByInviteUserId(info.value.userId)
  365. interview.value = data.slice(0, 1)
  366. }
  367. // 在当前频道中有新消息时更新未读消息数量
  368. const updateUnreadMessageCount = (val) => {
  369. const obj = val.find(e => e.userInfoVo.userId === info.value.userId)
  370. if (!obj?.unread || obj.unread === 0) return
  371. delete info.value.unread
  372. Object.assign(info.value, { unread: obj.unread, enterpriseId: entBaseInfo?.enterpriseId })
  373. }
  374. watch(
  375. () => conversationList.value,
  376. async (val) => {
  377. // 数据发生变化
  378. if (channelItem.value && IM.fromChannel === channelItem.value.channelID) {
  379. // 更新
  380. const { list } = await getMoreMessages(1, channelItem.value)
  381. messageItems.value = list.value
  382. if (Object.keys(info.value).length) updateUnreadMessageCount(val)
  383. chatRef.value.scrollBottom()
  384. }
  385. },
  386. {
  387. deep: true,
  388. immediate: true
  389. }
  390. )
  391. // 获取职位信息
  392. async function getMessageTypeSync () {
  393. const data = await getMessageType({
  394. fromUid: IM.uid,
  395. channelId: channelItem.value?.channelID,
  396. type: 102,
  397. page: {
  398. current: 1,
  399. size: 1,
  400. orders: [
  401. { column: 'message_seq', asc: false }
  402. ]
  403. }
  404. })
  405. if (!data.records || !data.records.length) {
  406. positionInfo.value = {}
  407. handleChangeSendResumeStatus(false)
  408. return
  409. }
  410. const _item = data.records.pop()
  411. const _itemJSON = JSON.parse(_item.payload)
  412. const _content = JSON.parse(_itemJSON.content)
  413. positionInfo.value = _content.positionInfo
  414. const check = await jobCvRelCheckSend({ jobId: _content.positionInfo.id })
  415. handleChangeSendResumeStatus(check)
  416. }
  417. // 修改发送状态
  418. function handleChangeSendResumeStatus (status) {
  419. if (!isEnterprise) {
  420. const item = userTools.value.find(e => e.key === 'sendResume')
  421. item.disabled = status
  422. }
  423. }
  424. async function handleChange (items) {
  425. try {
  426. chatRef.value.changeOverlay(true)
  427. const { userInfoVo, channel: myChannel, unread } = items.pop()
  428. info.value = userInfoVo?.userInfoResp ?? { name: '系统消息' }
  429. Object.assign(info.value, {
  430. channel: myChannel,
  431. unread
  432. })
  433. if (myChannel.channelID === 'system') {
  434. channelItem.value = myChannel
  435. const { list, more } = await getMoreMessages(1, channelItem.value)
  436. messageItems.value = list.value
  437. hasMore.value = more
  438. chatRef.value.scrollBottom()
  439. // 点开窗口消除未读数量
  440. await resetUnread(channelItem.value, entBaseInfo?.enterpriseId)
  441. await updateConversation()
  442. updateUnreadCount()
  443. return
  444. }
  445. // 个人端获取面试信息
  446. if (!isEnterprise) getInterviewInviteList()
  447. const userId = userInfoVo.userInfoResp.userId
  448. const enterpriseId = userInfoVo.userInfoResp.enterpriseId || undefined
  449. const { channel, list, more } = await initChart(userId, enterpriseId)
  450. // console.log('--------',list)
  451. channelItem.value = channel.value
  452. messageItems.value = list.value
  453. hasMore.value = more
  454. chatRef.value.scrollBottom()
  455. // 点开窗口消除未读数量
  456. await resetUnread(channel.value, entBaseInfo?.enterpriseId)
  457. await updateConversation()
  458. updateUnreadCount()
  459. // 获取最近职位记录
  460. getMessageTypeSync()
  461. } catch (error) {
  462. messageItems.value = []
  463. } finally {
  464. chatRef.value.changeOverlay(false)
  465. }
  466. }
  467. // 普通消息
  468. const handleUpdate = (val) => {
  469. send(val.value, channelItem.value)
  470. }
  471. // 选择文件
  472. const uploadFile = ref()
  473. const openFileInput = () => {
  474. uploadFile.value.trigger()
  475. }
  476. // 上传附件
  477. const handleUploadResume = async (url, title, filename) => {
  478. const obj = formItems.value.options.find(e => e.key === 'url')
  479. obj.value = filename
  480. obj.truthValue = url
  481. }
  482. const changePositionData = () => {
  483. entPositionListParams.value.pageNo = entPositionListLastData.value ? 1 : entPositionListParams.value.pageNo + 1
  484. selectJobId.value = ''
  485. getRecruitPositionList()
  486. }
  487. const positionDetail = (val) => {
  488. const id = val.value
  489. if (!id) return
  490. window.open(`/recruit/personal/position/details/${id}`)
  491. }
  492. // 选中职位并投递
  493. const selectJobId = ref('')
  494. const selectPositionSubmit = async () => {
  495. // 投递
  496. openPositionSelectDialog.value = false
  497. handleSendResume(handleSendResumeItem)
  498. }
  499. const pageLoading = ref(false)
  500. const entPositionTotal = ref(0)
  501. const entPositionList = ref([])
  502. const entPositionListParams = ref({ pageNo: 1, pageSize: 5 })
  503. const openPositionSelectDialog = ref(false)
  504. const entPositionListLastData = computed(() => entPositionListParams.value.pageNo * entPositionListParams.value.pageSize >= entPositionTotal.value)
  505. // 职位列表
  506. const getRecruitPositionList = async () => {
  507. const enterpriseId = info.value?.enterpriseId || null
  508. if (!enterpriseId) return Snackbar.warning('访问企业错误!')
  509. pageLoading.value = true
  510. const { list, total: number } = await getJobAdvertisedSearch({ ...entPositionListParams.value, enterpriseId })
  511. if (!list.length) return Snackbar.warning('企业暂无招聘中的职位,无法进行投递!')
  512. entPositionTotal.value = number
  513. entPositionList.value = list.map(j => {
  514. const e = j?.job || null
  515. if (!e) return e
  516. const salary = e.payFrom && e.payTo ? `${e.payFrom ? e.payFrom + '-' : ''}${e.payTo}${e.payName ? '/' + e.payName : ''}` : '面议'
  517. return {
  518. label: `${formatName(e.name)}_${e.areaName ? e.area?.str : '全国'} ${salary}`,
  519. value: e.id,
  520. data: e
  521. }
  522. }).filter(Boolean)
  523. setTimeout(() => { pageLoading.value = false }, 300)
  524. }
  525. // 获取简历
  526. const showUploadDialog = ref(false)
  527. const enRequestPositionInfo = ref({}) // 企业求简历时选中的职位信息
  528. let handleSendResumeItem = null
  529. async function handleSendResume (item) {
  530. const jobId = enRequestPositionInfo.value && enRequestPositionInfo.value?.id ? enRequestPositionInfo.value?.id : positionInfo.value.id
  531. if (!jobId && !selectJobId.value) {
  532. // 没有基于职位接收到的沟通,弹出职位列表让求职者选择。否则无法投递简历。
  533. handleSendResumeItem = item
  534. await getRecruitPositionList()
  535. if (entPositionTotal.value) openPositionSelectDialog.value = true
  536. return
  537. }
  538. try {
  539. item.loading = true
  540. // 获取简历列表
  541. const result = await getPersonResumeCv()
  542. if (result.length === 0) {
  543. Snackbar.warning(t('resume.resumeYetSubmit'))
  544. showUploadDialog.value = true
  545. return
  546. }
  547. resumeList.value = result
  548. if (item?.content?.query?.positionInfo?.data && Object.keys(item?.content?.query?.positionInfo?.data).length) enRequestPositionInfo.value = item?.content?.query?.positionInfo?.data
  549. showResume.value = true
  550. } finally {
  551. item.loading = false
  552. }
  553. }
  554. // 撤回简历
  555. async function handleBack (val) {
  556. console.log(val)
  557. try {
  558. // 撤回简历
  559. // 撤回聊天
  560. // await messageBack({
  561. // channelId: val.channel_id,
  562. // messageId: val.message_id,
  563. // nickName: baseInfo.name
  564. // // enterpriseId: ''
  565. // })
  566. } catch (error) {
  567. console.log(error)
  568. }
  569. }
  570. /**
  571. * 发送简历
  572. * text param
  573. * {
  574. * remark: 备注
  575. * query: {} 自定义参数 access -1 未确定 0 拒绝 1 同意
  576. * type: 1 => 发送简历
  577. * 2 => 索要简历
  578. * 3 => 信息描述
  579. * }
  580. */
  581. // 没有上传过简历的弹窗上传并发送给对方
  582. const handleSubmitAttachment = async () => {
  583. const { valid } = await CtFormRef.value.formRef.validate()
  584. if (!valid) return
  585. const obj = {}
  586. formItems.value.options.forEach(e => {
  587. obj[e.key] = e.truthValue || e.value
  588. })
  589. if (!obj.title || !obj.url) return
  590. // 学生实习到岗信息
  591. let practice = {}
  592. if (isStudent.value) {
  593. practice = studentDeliveryFormRef.value.getQueryParams()
  594. console.log(practice, '上传简历-到岗信息')
  595. }
  596. if (isStudent.value && (!practice?.practiceStartTime || !practice?.practiceEndTime)) return Snackbar.warning('请完善实习到岗信息')
  597. // 保存附件
  598. await savePersonResumeCv(obj)
  599. // 简历投递至简历库
  600. if (isEmployment.value !== '-1') {
  601. let params = {
  602. jobId: positionInfo.value.id || selectJobId.value,
  603. url: obj.url,
  604. recommendUserId: isEmployment.value
  605. }
  606. // 如果是学生则需要带上实习信息
  607. if (practice && Object.keys(practice).length > 0) params = Object.assign(params, practice)
  608. await jobCvRelHireSend(params)
  609. } else {
  610. const jobId = enRequestPositionInfo.value && enRequestPositionInfo.value?.id ? enRequestPositionInfo.value?.id : positionInfo.value.id || selectJobId.value
  611. const type = (enRequestPositionInfo.value && Object.keys(enRequestPositionInfo.value).length ? enRequestPositionInfo.value.hire : positionInfo.value.hire) ? 1 : 0
  612. let params = {
  613. jobId,
  614. title: obj.title,
  615. url: obj.url,
  616. type
  617. }
  618. // 如果是学生则需要带上实习信息
  619. if (practice && Object.keys(practice).length > 0) params = Object.assign(params, practice)
  620. await jobCvRelSend(params)
  621. }
  622. handleChangeSendResumeStatus(true)
  623. showUploadDialog.value = false
  624. const text = {
  625. remark: '发送简历',
  626. query: {
  627. src: obj.url,
  628. title: obj.title
  629. },
  630. type: 1
  631. }
  632. if (enRequestPositionInfo.value) text.query.positionInfo = enRequestPositionInfo.value
  633. send (JSON.stringify(text), channelItem.value, 105)
  634. enRequestPositionInfo.value = {}
  635. }
  636. async function handleSubmitResume () {
  637. if (!selectResume.value) {
  638. Snackbar.warning(t('resume.selectResumeToSubmit'))
  639. return
  640. }
  641. const _info = resumeList.value.find((item) => item.id === selectResume.value)
  642. // 学生实习到岗信息
  643. let practice = {}
  644. if (isStudent.value) {
  645. practice = studentDeliveryFormRef.value.getQueryParams()
  646. console.log(practice, '选择简历-到岗信息')
  647. }
  648. if (isStudent.value && (!practice?.practiceStartTime || !practice?.practiceEndTime)) return Snackbar.warning('请完善实习到岗信息')
  649. // 简历投递至简历库
  650. if (isEmployment.value !== '-1') {
  651. let params = {
  652. jobId: positionInfo.value.id || selectJobId.value,
  653. url: _info.url,
  654. recommendUserId: isEmployment.value
  655. }
  656. // 如果是学生则需要带上实习信息
  657. if (practice && Object.keys(practice).length > 0) params = Object.assign(params, practice)
  658. await jobCvRelHireSend(params)
  659. } else {
  660. const jobId = enRequestPositionInfo.value && enRequestPositionInfo.value?.id ? enRequestPositionInfo.value?.id : positionInfo.value.id || selectJobId.value
  661. const type = (enRequestPositionInfo.value && Object.keys(enRequestPositionInfo.value).length ? enRequestPositionInfo.value.hire : positionInfo.value.hire) ? 1 : 0
  662. let params = {
  663. jobId,
  664. title: _info.title,
  665. url: _info.url,
  666. type
  667. }
  668. // 如果是学生则需要带上实习信息
  669. if (practice && Object.keys(practice).length > 0) params = Object.assign(params, practice)
  670. await jobCvRelSend(params)
  671. }
  672. handleChangeSendResumeStatus(true)
  673. showResume.value = false
  674. const text = {
  675. remark: '发送简历',
  676. query: {
  677. src: _info.url,
  678. title: _info.title,
  679. id: _info.id,
  680. },
  681. type: 1
  682. }
  683. if (enRequestPositionInfo.value) text.query.positionInfo = enRequestPositionInfo.value
  684. send (JSON.stringify(text), channelItem.value, 105)
  685. enRequestPositionInfo.value = {}
  686. }
  687. // 简历预览
  688. const handlePreview = (val) => {
  689. previewFile(val.content.query.src)
  690. }
  691. const handleGetMore = async () => {
  692. // 当前滚动条高度
  693. const scrollHeight = chatRef.value.chatRef.scrollHeight
  694. // 当前滚动条距离
  695. const scrollTop = chatRef.value.chatRef.scrollTop
  696. try {
  697. chatRef.value.changeLoading(true)
  698. pageSize.value++
  699. const { list, more } = await getMoreMessages(pageSize.value, channelItem.value)
  700. messageItems.value.unshift(...list.value)
  701. hasMore.value = more
  702. nextTick(() => {
  703. // 渲染后高度
  704. const _scrollHeight = chatRef.value.chatRef.scrollHeight
  705. chatRef.value.chatRef.scrollTop = _scrollHeight - scrollHeight - scrollTop
  706. })
  707. } finally {
  708. chatRef.value.changeLoading(false)
  709. }
  710. }
  711. const handleDelete = async ({ channel }) => {
  712. await deleteConversations(channel, entBaseInfo?.enterpriseId)
  713. await updateConversation()
  714. updateUnreadCount()
  715. }
  716. // 没有企业ID则enterpriseId为undefined
  717. // 发送消息体 { text, type: 2 }
  718. // 面试邀约
  719. const getPositionList = async () => {
  720. const data = await getJobAdvertised({ status: 0, exTime: 0 }) // 0开启 1关闭 不带则全部
  721. if (!data.length) return
  722. const list = dealDictArrayData([], data)
  723. positionList.value = list.map(e => {
  724. const salary = e.payFrom && e.payTo ? `${e.payFrom ? e.payFrom + '-' : ''}${e.payTo}${e.payName ? '/' + e.payName : ''}` : '面议'
  725. return {
  726. label: `${formatName(e.name)}_${e.areaName ? e.area?.str : '全国'} ${salary}`,
  727. value: e.id,
  728. data: e
  729. }
  730. })
  731. }
  732. async function handleInvite (item) {
  733. item.loading = true
  734. positionList.value = []
  735. try {
  736. await getPositionList()
  737. if (!positionList.value.length) return Snackbar.warning('请先发布职位')
  738. if (item.key === 'requestResume') return showSelectPosition.value = true
  739. showInvite.value = true
  740. } catch (error) {
  741. console.log(error)
  742. } finally {
  743. item.loading = false
  744. }
  745. }
  746. // 企业-发送面试邀请
  747. const handleSubmit = async () => {
  748. const { valid } = await inviteRef.value.CtFormRef.formRef.validate()
  749. if (!valid) {
  750. return
  751. }
  752. const query = inviteRef.value.getQuery()
  753. if (!query.time) {
  754. Snackbar.warning('时间不能为空')
  755. return
  756. }
  757. query.userId = info.value.userId
  758. query.positionInfo = positionList.value.find(e => e.value === query.jobId)
  759. // 需要id
  760. const data = await saveInterviewInvite(query)
  761. // 保留邀请id
  762. query.id = data.id
  763. showTip.value = true
  764. send(JSON.stringify(query), channelItem.value, 101)
  765. showInvite.value = false
  766. }
  767. const router = useRouter()
  768. const handleToInterviewManagement = () => {
  769. router.push('/recruit/enterprise/interviewManagement')
  770. }
  771. // 企业-求简历
  772. const handleRequestResumeSubmit = async () => {
  773. const { valid } = await requestFromRef.value.formRef.validate()
  774. if (!valid) return
  775. const jobId = requestFormItems.value.options.find(e => e.key === 'jobId').value
  776. const positionInfo = positionList.value.find(e => e.value === jobId)
  777. const text = {
  778. remark: '求简历',
  779. query: {
  780. src: '',
  781. title: '',
  782. id: '',
  783. positionInfo
  784. },
  785. type: 2
  786. }
  787. send (JSON.stringify(text), channelItem.value, 105)
  788. showSelectPosition.value = false
  789. }
  790. const handleAgree = (val) => {
  791. if (!val.id) return
  792. const query = {
  793. id: val.id
  794. }
  795. const type = getIsEnterprise() ? 'entBaseInfo' : 'baseInfo'
  796. const baseInfo = localStorage.getItem(type)
  797. if (baseInfo) {
  798. const { phone } = JSON.parse(baseInfo)
  799. query.phone = phone
  800. }
  801. Confirm(t('common.confirmTitle'), '是否确定接收此面试邀请?').then(async () => {
  802. await userInterviewInviteConsent(query)
  803. Snackbar.success(t('common.operationSuccessful'))
  804. getInterviewInviteList()
  805. send(JSON.stringify({ id: val.id }), channelItem.value, 104)
  806. })
  807. }
  808. // 拒绝面试邀请
  809. const handleRefuse = (val) => {
  810. if (!val.id) return
  811. Confirm(t('common.confirmTitle'), '您是否确定要拒绝此面试邀请?').then(async () => {
  812. await userInterviewInviteReject(val.id)
  813. Snackbar.success(t('common.operationSuccessful'))
  814. getInterviewInviteList()
  815. send(JSON.stringify({ id: val.id }), channelItem.value, 103)
  816. })
  817. }
  818. </script>
  819. <style scoped lang="scss">
  820. .message {
  821. display: flex;
  822. &-left {
  823. position: relative;
  824. flex-shrink: 0;
  825. height: 100%;;
  826. width: 360px;
  827. background-color: #fff;
  828. border-radius: 8px;
  829. margin-right: 12px;
  830. .message-left-search {
  831. width: 100%;
  832. height: 60px;
  833. background: linear-gradient(90deg, #f5fcfc, #fcfbfa);
  834. border-radius: 8px 8px 0 0;
  835. }
  836. .message-chat-box {
  837. height: 0;
  838. flex: 1;
  839. overflow: auto;
  840. padding-bottom: 20px;
  841. .chat-item {
  842. position: relative;
  843. width: 100%;
  844. height: 78px;
  845. padding: 14px 12px;
  846. cursor: pointer;
  847. &:hover {
  848. background-color: #f8f8f8;
  849. }
  850. .chat-item-time {
  851. position: absolute;
  852. right: 12px;
  853. top: 50%;
  854. transform: translateY(-50%);
  855. }
  856. .title-box {
  857. max-width: 114px;
  858. overflow: hidden;
  859. white-space: nowrap;
  860. text-overflow: ellipsis;
  861. display: inline-block;
  862. }
  863. }
  864. }
  865. .message-no-more-text {
  866. color: var(--color-999);
  867. font-size: 14px;
  868. text-align: center
  869. }
  870. .left-noData {
  871. position: absolute;
  872. top: 50%;
  873. left: 50%;
  874. transform: translate(-50%, -50%);
  875. }
  876. }
  877. &-right {
  878. height: 100%;
  879. flex: 1;
  880. width: 0;
  881. position: relative;
  882. background-color: #fff;
  883. border-radius: 8px;
  884. .right-noData {
  885. position: absolute;
  886. top: 50%;
  887. left: 50%;
  888. transform: translate(-50%, -50%);
  889. }
  890. }
  891. }
  892. .enterprise-name {
  893. max-width: 150px;
  894. .line {
  895. display: inline-block;
  896. width: 1px;
  897. height: 12px;
  898. vertical-align: middle;
  899. background-color: #e0e0e0;
  900. margin: 0 3px;
  901. }
  902. }
  903. .radioBox {
  904. &:hover {
  905. border-radius: 2px;
  906. background-color: var(--color-f8);
  907. }
  908. }
  909. </style>