chatting.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. <template>
  2. <div class="chatting d-flex flex-column">
  3. <v-overlay
  4. :model-value="overlay"
  5. contained
  6. class="align-center justify-center"
  7. >
  8. <v-progress-circular
  9. color="primary"
  10. size="64"
  11. indeterminate
  12. ></v-progress-circular>
  13. </v-overlay>
  14. <div class="top-info">
  15. <div class="user-info d-flex align-center">
  16. <p class="d-flex align-center float-left">
  17. <span class="name">{{ info.name || info?.phone }}</span>
  18. <template v-if="info.enterpriseId">
  19. <span>{{ info.postNameCn }}</span>
  20. <span v-if="info.postNameCn && info.enterpriseAnotherName" class="septal-line"></span>
  21. <span>{{ info.enterpriseAnotherName }}</span>
  22. </template>
  23. </p>
  24. </div>
  25. </div>
  26. <v-divider></v-divider>
  27. <div class="py-3 px-7" v-if="interview.length">
  28. <div v-for="val in interview" :key="val.id" class="color-666">
  29. <div class="d-flex justify-space-between">
  30. <div class="font-weight-bold color-primary">
  31. <span>{{ val.job.name }}</span>
  32. <span v-if="!val.job.payFrom && !val.job.payTo" class="ml-3">面议</span>
  33. <span v-else class="ml-3">{{ val.job.payFrom ? val.job.payFrom + '-' : '' }}{{ val.job.payTo }}</span>
  34. </div>
  35. <div :style="{'color': ['5', '98', '99'].includes(val.status) ? 'var(--v-error-base)' : 'var(--v-primary-base)'}">{{ statusList.find(e => e.value === val.status)?.label }}</div>
  36. </div>
  37. <div class="mt-1 font-size-14 ellipsis" style="max-width: 100%;">
  38. <span>面试时间:{{ timesTampChange(val.time, 'Y-M-D h:m') }}</span>
  39. <span class="septal-line"></span>
  40. <span>面试地点:{{ val.address }}</span>
  41. <span class="septal-line"></span>
  42. <span>联系电话:{{ val.invitePhone }}</span>
  43. </div>
  44. <div class="mt-2 d-flex justify-space-between align-center">
  45. <div class="tipsText" @click="handleToCenter">在“个人中心-求职反馈”中管理我的面试</div>
  46. <div v-if="val.status === '0'">
  47. <v-btn class="mr-3" variant="outlined" color="error" size="small" @click="handleRefuse(val)">拒绝邀请</v-btn>
  48. <v-btn variant="outlined" color="primary" size="small" @click="handleAgree(val)">接受邀请</v-btn>
  49. </div>
  50. </div>
  51. </div>
  52. </div>
  53. <v-divider v-if="interview.length"></v-divider>
  54. <div class="my-3 message-box" @scroll="handleScroll" ref="chatRef">
  55. <div>
  56. <div class="d-flex justify-center" v-if="hasMore">
  57. <v-btn :loading="loading" variant="text" color="primary" size="small" @click="handleMore">查看更多</v-btn>
  58. </div>
  59. <div v-for="(val, i) in items" :key="i" :id="val.id">
  60. <div class="time-box">{{ timesTampChange(+(val.timestamp.padEnd(13, '0'))) }}</div>
  61. <template v-if="val.payload?.type === 102">
  62. <v-card
  63. color="teal"
  64. variant="tonal"
  65. class="mx-auto mb-5"
  66. width="400"
  67. min-height="150"
  68. :elevation="3"
  69. >
  70. <div class="pa-3">
  71. <div class="text-h6"> {{ val.payload?.content?.positionInfo?.name }}</div>
  72. <div v-if="!val.payload?.content?.positionInfo?.payFrom && !val.payload?.content?.positionInfo?.payTo" class="text-subtitle-2">薪酬待遇: 面议</div>
  73. <div v-else class="text-subtitle-2">薪酬待遇: {{ val.payload?.content?.positionInfo?.payFrom ? val.payload?.content?.positionInfo?.payFrom + ' - ' : '' }}{{ val.payload?.content?.positionInfo?.payTo }}</div>
  74. <div>
  75. <v-chip
  76. color="secondary"
  77. v-for="(v, i) in val.payload?.content?.positionInfo?.enterprise?.welfareList"
  78. :key="val.message_id + v + i"
  79. x-small
  80. class="mt-1 mr-1"
  81. >
  82. {{ v }}
  83. </v-chip>
  84. </div>
  85. <v-divider class="my-3"></v-divider>
  86. <div class="text-subtitle-2 text-right">
  87. <v-avatar size="24">
  88. <v-img :src="val.payload?.content?.positionInfo?.contact?.avatar"></v-img>
  89. </v-avatar>
  90. {{ val.payload?.content?.positionInfo?.contact?.name }}
  91. {{ val.payload?.content?.positionInfo?.contact?.postNameCn }}
  92. {{ val.payload?.content?.positionInfo?.enterprise?.name }}
  93. </div>
  94. <div class="text-subtitle-2 text-right">
  95. 地址:{{ val.payload?.content?.positionInfo?.address }}
  96. </div>
  97. </div>
  98. </v-card>
  99. </template>
  100. <template v-if="val.payload?.type === 1006">
  101. <div class="text-subtitle-2 text-center text-grey">{{ val.from_uid === IM.uid ? '' : '对方' }}撤回了一份简历</div>
  102. </template>
  103. <div v-if="val.payload?.type !== 1006" :class="['message-view_item', val.from_uid === IM.uid ? 'is-self' : 'is-other']">
  104. <div style="width: 40px; height: 40px;">
  105. <v-avatar>
  106. <v-img
  107. :src="(val.from_uid === IM.uid ? mAvatar() : getUserAvatar(info.avatar, info.sex)) || 'https://minio.citupro.com/dev/menduner/7.png'"
  108. :width="40"
  109. height="40"
  110. rounded
  111. ></v-img>
  112. </v-avatar>
  113. </div>
  114. <!-- 显示沟通职位 -->
  115. <template v-if="val.payload?.type === 102">
  116. <div class="message-text" :class="{ active: val.from_uid === IM.uid}">
  117. {{ val.payload?.content.text }}
  118. </div>
  119. </template>
  120. <!-- 发起面试邀请 -->
  121. <div v-else-if="val.payload?.type === 101">
  122. <v-chip class="ma-2" color="teal" label>
  123. <v-icon icon="mdi-email-newsletter" start></v-icon>
  124. 发起了面试邀请
  125. </v-chip>
  126. </div>
  127. <div v-else-if="val.payload?.type === 103">
  128. <v-chip class="ma-2" label color="error">
  129. <v-icon icon="mdi-close" start></v-icon>
  130. 拒绝了面试邀请
  131. </v-chip>
  132. </div>
  133. <div v-else-if="val.payload?.type === 104">
  134. <v-chip class="ma-2" label color="primary">
  135. <v-icon icon="mdi-check" start></v-icon>
  136. 接受了面试邀请
  137. </v-chip>
  138. </div>
  139. <div v-else-if="val.payload.type === 105" class="text-end">
  140. <v-chip class="ma-2" label color="primary" v-if="val.from_uid === IM.uid">
  141. <v-icon icon="mdi-check" start></v-icon>
  142. {{ val.payload.content?.type === 1 ? '附件简历已发送' : '简历请求已发送' }}
  143. </v-chip>
  144. <v-card v-if="val.payload.content?.type !== 2 || val.from_uid !== IM.uid" width="300" class="pa-3 ma-2" color="teal" variant="tonal" :elevation="3">
  145. <v-card-text class="d-flex">
  146. <p v-if="val.payload.content?.type === 1" style="width: 100%; text-align: left;">{{ val.payload.content?.query?.title || t('resume.attachmentResume') }}</p>
  147. <p v-if="val.payload.content?.type === 2">{{ t('resume.requestResume') }}</p>
  148. </v-card-text>
  149. <v-card-actions class="justify-center">
  150. <!-- <v-btn variant="tonal" flat size="small" color="error" @click="handleRejectReceive(val.payload)">拒绝</v-btn> -->
  151. <template v-if="val.payload.content?.type === 1">
  152. <v-btn block variant="tonal" flat size="small" color="success" @click="handlePreview(val.payload)">点击预览附件简历</v-btn>
  153. <!-- <v-btn variant="tonal" flat size="small" color="error" @click="handleBack(val)">撤回简历</v-btn> -->
  154. </template>
  155. <v-btn v-if="val.payload.content?.type === 2" block variant="tonal" flat size="small" color="success" @click="handleSendResume(val.payload)">点击发送附件简历</v-btn>
  156. </v-card-actions>
  157. </v-card>
  158. </div>
  159. <div v-else class="message-text" :class="{ active: val.from_uid === IM.uid}">
  160. {{ val.payload?.content }}
  161. </div>
  162. </div>
  163. <!-- 插入个人-面试职位邀请:同意、拒绝 -->
  164. <div v-if="isEnterprise && val.payload?.type === 101" class="d-flex justify-center">
  165. <v-card
  166. color="teal"
  167. variant="tonal"
  168. class="mx-auto"
  169. min-width="400"
  170. min-height="150"
  171. :elevation="3"
  172. >
  173. <v-card-item>
  174. <div>
  175. <div class="text-overline mb-1">
  176. 面试邀请
  177. </div>
  178. <div class=" d-flex justify-space-between">
  179. <div class="text-h6 mb-1">
  180. {{ val.payload?.content?.positionInfo?.data?.name }}
  181. </div>
  182. <div v-if="!val.payload?.content?.positionInfo?.data?.payFrom && !val.payload?.content?.positionInfo?.data?.payTo">面议</div>
  183. <div v-else>
  184. {{ val.payload?.content?.positionInfo?.data?.payFrom ? val.payload?.content?.positionInfo?.data?.payFrom + ' - ' : '' }}
  185. {{ val.payload?.content?.positionInfo?.data?.payTo }}
  186. </div>
  187. </div>
  188. <div class="text-caption">面试时间: {{ timesTampChange(val.payload?.content?.time) }}</div>
  189. <div class="text-caption">面试地点: {{ val.payload?.content?.address }}</div>
  190. <div class="text-caption">联系电话: {{ val.payload?.content?.invitePhone }}</div>
  191. </div>
  192. </v-card-item>
  193. </v-card>
  194. </div>
  195. <!-- <div class="d-flex justify-center" v-if="val.payload.type === 105">
  196. <v-chip>已成功发送简历</v-chip>
  197. </div> -->
  198. </div>
  199. </div>
  200. </div>
  201. <div class="tools pa-3" v-if="Object.keys(info).length > 0">
  202. <slot name="tools"></slot>
  203. </div>
  204. <div class="bottom-info">
  205. <v-divider></v-divider>
  206. <div class="pa-3">
  207. <v-textarea
  208. v-model="inputVal"
  209. label="请输入消息"
  210. placeholder="请输入消息 按Ctrl+Enter换行"
  211. hide-details
  212. no-resize
  213. color="primary"
  214. bg-color="white"
  215. variant="plain"
  216. :disabled="Object.keys(info).length === 0"
  217. @keydown="handleKeyDown"
  218. >
  219. <template #append-inner>
  220. <v-btn color="primary" :disabled="!inputVal" style="align-self: center;" @click="handleSend">发送</v-btn>
  221. </template>
  222. </v-textarea>
  223. </div>
  224. </div>
  225. </div>
  226. </template>
  227. <script setup>
  228. defineOptions({ name: 'message-chatting'})
  229. import { ref, nextTick, onMounted, inject, watch } from 'vue'
  230. import { timesTampChange } from '@/utils/date'
  231. import { useIMStore } from '@/store/im'
  232. import { useI18n } from '@/hooks/web/useI18n'
  233. import { useRouter } from 'vue-router';
  234. import { getDict } from '@/hooks/web/useDictionaries'
  235. import { getUserAvatar } from '@/utils/avatar'
  236. import { useUserStore } from '@/store/user'
  237. const isEnterprise = inject('isEnterprise')
  238. const { t } = useI18n()
  239. const emits = defineEmits(['handleMore', 'handleSend', 'handleAgree', 'handleRefuse'])
  240. const props = defineProps({
  241. items: {
  242. type: Array,
  243. default: () => []
  244. },
  245. info: {
  246. type: Object,
  247. default: () => ({})
  248. },
  249. // uid: {
  250. // type: String,
  251. // default: ''
  252. // },
  253. hasMore: {
  254. type: Boolean,
  255. default: false
  256. },
  257. interview: {
  258. type: Array,
  259. default: () => []
  260. },
  261. updateConversation: {
  262. type: Function,
  263. default: () => {}
  264. },
  265. updateUnreadCount: {
  266. type: Function,
  267. default: () => {}
  268. },
  269. resetUnread: {
  270. type: Function,
  271. default: () => {}
  272. }
  273. })
  274. watch(() => props.info, async (val) => {
  275. if (!Object.keys(val).length || val.unread === 0) return
  276. await props.resetUnread(val.channel, isEnterprise ? val.enterpriseId : null)
  277. await props.updateConversation()
  278. props.updateUnreadCount()
  279. }, { deep: true, immediate: true })
  280. const router = useRouter()
  281. const overlay = ref(false)
  282. const IM = useIMStore()
  283. const userStore = useUserStore()
  284. const loading = ref(false)
  285. const mAvatar = () => {
  286. if (isEnterprise) {
  287. return getUserAvatar(userStore.entBaseInfo?.avatar, userStore.entBaseInfo?.sex)
  288. }
  289. return getUserAvatar(userStore.baseInfo?.avatar, userStore.baseInfo?.sex)
  290. }
  291. const chatRef = ref()
  292. const inputVal = ref('')
  293. const pullDowning = ref(false) // 下拉中
  294. const pulldownFinished = ref(false) // 下拉完成
  295. // 滚动到底部
  296. const scrollBottom = () => {
  297. const chat = chatRef.value
  298. if (chat) {
  299. nextTick(function () {
  300. chat.scrollTop = chat.scrollHeight
  301. })
  302. }
  303. }
  304. // 求职端-获取求职者与当前邀请人的面试记录
  305. const statusList = ref([])
  306. const getStatusList = () => {
  307. getDict('menduner_interview_invite_status').then(({data}) => {
  308. if (data.length) statusList.value = data
  309. })
  310. }
  311. if (!isEnterprise) getStatusList()
  312. onMounted(() => {
  313. nextTick(() => {
  314. scrollBottom()
  315. })
  316. })
  317. const handleMore = () => {
  318. emits('handleMore')
  319. }
  320. const handleSend = () => {
  321. emits('handleSend', inputVal)
  322. }
  323. const handleScroll = (e) => {
  324. const targetScrollTop = e.target.scrollTop;
  325. if (targetScrollTop <= 250) {
  326. // 下拉
  327. if (pullDowning.value || pulldownFinished.value) {
  328. return
  329. }
  330. pullDowning.value = true
  331. }
  332. }
  333. const changeOverlay = (val) => {
  334. overlay.value = val
  335. }
  336. const changeLoading = (val) => {
  337. loading.value = val
  338. }
  339. const handleKeyDown = (event) => {
  340. if (event.keyCode === 13) {
  341. event.preventDefault()
  342. if(event.ctrlKey) {
  343. // 换行
  344. inputVal.value += '\n'
  345. return
  346. }
  347. // 发送
  348. emits('handleSend', inputVal)
  349. }
  350. }
  351. const reset = () => {
  352. inputVal.value = ''
  353. }
  354. // 同意面试邀请
  355. const handleAgree = (val) => {
  356. if (!val.id) return
  357. emits('handleAgree', val)
  358. }
  359. // 拒绝面试邀请
  360. const handleRefuse = (val) => {
  361. if (!val.id) return
  362. emits('handleRefuse', val)
  363. }
  364. // 跳转个人中心-面试
  365. const handleToCenter = () => {
  366. router.push({ path: '/recruit/personal/personalCenter', query: { showInterviewScheduleMore: true } })
  367. }
  368. // 简历预览
  369. const handlePreview = (val) => {
  370. emits('handlePreview', val)
  371. }
  372. // const handleBack = (val) => {
  373. // emits('handleBack', val)
  374. // }
  375. // 发送简历
  376. const handleSendResume = (val) => {
  377. emits('handleSendResume', val)
  378. }
  379. // 拒绝接收简历
  380. // const handleRejectReceive = (val) => {
  381. // emits('handleRejectReceive', val)
  382. // }
  383. // // 同意接收简历
  384. // const handleAccessReceive = (val) => {
  385. // emits('handleAccessReceive', val)
  386. // }
  387. defineExpose({
  388. chatRef,
  389. reset,
  390. changeLoading,
  391. scrollBottom,
  392. changeOverlay
  393. })
  394. </script>
  395. <style scoped lang="scss">
  396. .chatting {
  397. position: relative;
  398. height: 100%;
  399. }
  400. .top-info {
  401. // height: 104px;
  402. padding: 0 30px;
  403. .user-info {
  404. position: relative;
  405. height: 48px;
  406. justify-content: space-between;
  407. // padding: 0 30px;
  408. span {
  409. font-size: 14px;
  410. font-weight: 400;
  411. // color: var(--color-666);
  412. line-height: 22px;
  413. }
  414. .name {
  415. line-height: 25px;
  416. margin-right: 20px;
  417. }
  418. }
  419. .position-content {
  420. position: relative;
  421. padding: 16px;
  422. border-radius: 12px;
  423. // margin: auto;
  424. width: 60%;
  425. max-width: 800px;
  426. // width: 760px;
  427. background: linear-gradient(90deg, #d5ffff, #f0fff4);
  428. .salary {
  429. font-size: 20px;
  430. font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
  431. color: var(--v-error-base);
  432. line-height: 24px;
  433. }
  434. &-emolument {
  435. color: #f30;
  436. font-weight: 600;
  437. }
  438. }
  439. }
  440. .message-box {
  441. flex: 1;
  442. padding: 0 30px 30px;
  443. overflow-y: auto;
  444. .time-box {
  445. user-select: none;
  446. position: relative;
  447. top: 8px;
  448. margin: 20px 0;
  449. max-height: 20px;
  450. text-align: center;
  451. font-weight: 400;
  452. font-size: 12px;
  453. color: var(--color-time-divider);
  454. }
  455. .message-view_item {
  456. display: flex;
  457. flex-direction: row;
  458. align-items: flex-start;
  459. margin: 8px 0;
  460. position: relative;
  461. .message-text {
  462. overflow-wrap: break-word;
  463. background-color: #f0f2f5;
  464. border-radius: 6px;
  465. max-width: 85%;
  466. padding: 10px;
  467. &.active {
  468. background: #d5e6e8;
  469. }
  470. }
  471. }
  472. .is-self {
  473. flex-direction: row-reverse;
  474. display: flex;
  475. .message-text {
  476. margin-right: 10px;
  477. }
  478. }
  479. .is-other {
  480. .message-text {
  481. margin-left: 10px;
  482. }
  483. }
  484. }
  485. .bottom-info {
  486. width: 100%;
  487. height: 160px;
  488. background-color: #fff;
  489. }
  490. input {
  491. outline: none;
  492. width: 748px;
  493. height: 60px;
  494. max-width: 748px;
  495. overflow: auto;
  496. white-space: pre-wrap;
  497. &:focus {
  498. border: none;
  499. }
  500. }
  501. .tipsText {
  502. color: var(--color-999);
  503. font-size: 12px;
  504. cursor: pointer;
  505. &:hover {
  506. color: var(--v-primary-base);
  507. }
  508. }
  509. /* 滚动条样式 */
  510. ::-webkit-scrollbar {
  511. -webkit-appearance: none;
  512. width: 10px;
  513. height: 0px;
  514. }
  515. /* 滚动条内的轨道 */
  516. ::-webkit-scrollbar-track {
  517. background: rgba(0, 0, 0, 0.1);
  518. border-radius: 0;
  519. }
  520. /* 滚动条内的滑块 */
  521. ::-webkit-scrollbar-thumb {
  522. cursor: pointer;
  523. border-radius: 5px;
  524. background: rgba(0, 0, 0, 0.15);
  525. transition: color 0.2s ease;
  526. }
  527. </style>