chatting.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  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 }}</span>
  18. <template v-if="info.enterpriseId">
  19. <span>{{ info.postNameCn }}</span>
  20. <span class="septal-line"></span>
  21. <span>{{ info.enterpriseName }}</span>
  22. </template>
  23. </p>
  24. </div>
  25. <!-- <div class="position-content" v-if="!isEnterprise">
  26. <div class="d-flex mb-2">
  27. <div class="font-weight-black">{{ info.name }}</div>
  28. <div class="position-content-emolument">{{ info.payFrom }} - {{ info.payFrom }}</div>
  29. </div>
  30. <div class="text-subtitle-2">{{ info?.enterprise?.name }}</div>
  31. <div class="pt-3">
  32. <v-chip
  33. v-for="(item, index) in info?.enterprise?.welfareList"
  34. :key="item + index"
  35. color="green"
  36. label
  37. class="mr-3 mb-3"
  38. >
  39. {{ item }}
  40. </v-chip>
  41. </div>
  42. </div> -->
  43. </div>
  44. <v-divider></v-divider>
  45. <div class="my-3 message-box" @scroll="handleScroll" ref="chatRef">
  46. <div>
  47. <div class="d-flex justify-center" v-if="hasMore">
  48. <v-btn :loading="loading" variant="text" color="primary" size="small" @click="handleMore">查看更多</v-btn>
  49. </div>
  50. <div v-for="(val, i) in items" :key="i" :id="val.id">
  51. <div class="time-box">{{ timesTampChange(+(val.timestamp.padEnd(13, '0'))) }}</div>
  52. <!-- <template v-if="val.payload.type === 102 && val.from_uid !== IM.uid"> -->
  53. <template v-if="val.payload.type === 102">
  54. <v-card
  55. color="teal"
  56. variant="tonal"
  57. class="mx-auto"
  58. width="400"
  59. min-height="150"
  60. :elevation="3"
  61. >
  62. <div class="pa-3">
  63. <div class="text-h6"> {{ val.payload.content.positionInfo.name }}</div>
  64. <div class="text-subtitle-2">薪酬待遇: {{ val.payload.content.positionInfo.payFrom }} - {{ val.payload.content.positionInfo.payTo }}</div>
  65. <div>
  66. <v-chip
  67. color="secondary"
  68. v-for="(v, i) in val.payload.content.positionInfo.enterprise.welfareList"
  69. :key="val.message_id + v + i"
  70. x-small
  71. class="mt-1 mr-1"
  72. >
  73. {{ v }}
  74. </v-chip>
  75. </div>
  76. <v-divider class="my-3"></v-divider>
  77. <div class="text-subtitle-2 text-right">
  78. <v-avatar size="24">
  79. <v-img :src="val.payload.content.positionInfo.contact.avatar"></v-img>
  80. </v-avatar>
  81. {{ val.payload.content.positionInfo.contact.name }}
  82. {{ val.payload.content.positionInfo.contact.postNameCn }}
  83. {{ val.payload.content.positionInfo.enterprise.name }}
  84. </div>
  85. <div class="text-subtitle-2 text-right">
  86. 地址:{{ val.payload.content.positionInfo.address }}
  87. </div>
  88. </div>
  89. </v-card>
  90. </template>
  91. <div :class="['message-view_item', val.from_uid === IM.uid ? 'is-self' : 'is-other']">
  92. <div style="width: 40px; height: 40px;">
  93. <v-avatar>
  94. <v-img
  95. :src="val.from_uid === IM.uid ? mAvatar : info.avatar"
  96. :width="40"
  97. height="40"
  98. rounded
  99. ></v-img>
  100. </v-avatar>
  101. </div>
  102. <!-- 显示沟通职位 -->
  103. <template v-if="val.payload.type === 102">
  104. <div class="message-text" :class="{ active: val.from_uid === IM.uid}">
  105. {{ val.payload.content.text }}
  106. </div>
  107. </template>
  108. <!-- 发起职位 -->
  109. <div v-else-if="val.payload.type === 101">
  110. <v-chip
  111. class="ma-2"
  112. color="teal"
  113. label
  114. >
  115. 发起了面试邀请
  116. <v-icon icon="mdi-email-newsletter" end></v-icon>
  117. </v-chip>
  118. </div>
  119. <div v-else class="message-text" :class="{ active: val.from_uid === IM.uid}">
  120. {{ val.payload.content }}
  121. </div>
  122. </div>
  123. <!-- 插入面试职位邀请 -->
  124. <div v-if="val.payload.type === 101" class="d-flex justify-center">
  125. <v-card
  126. color="teal"
  127. variant="tonal"
  128. class="mx-auto"
  129. min-width="400"
  130. min-height="150"
  131. :elevation="3"
  132. >
  133. <v-card-item>
  134. <div>
  135. <!-- {{val.payload.content}} -->
  136. <div class="text-overline mb-1">
  137. 面试邀请
  138. </div>
  139. <div class=" d-flex justify-space-between">
  140. <div class="text-h6 mb-1">
  141. {{ val.payload.content.positionInfo.data.name }}
  142. </div>
  143. <div>
  144. {{ val.payload.content.positionInfo.data.payFrom }} -
  145. {{ val.payload.content.positionInfo.data.payTo }}
  146. </div>
  147. </div>
  148. <!-- <div class="text-caption"></div> -->
  149. <div class="text-caption">面试时间: {{ timesTampChange(val.payload.content?.time) }}</div>
  150. <div class="text-caption">面试地点: {{ val.payload.content.address }}</div>
  151. <div class="text-caption">联系电话: {{ val.payload.content.invitePhone }}</div>
  152. </div>
  153. </v-card-item>
  154. <v-card-actions class="justify-end" v-if="val.from_uid !== IM.uid">
  155. <v-btn @click="handleAgree(val.payload.content)">
  156. 接受邀请
  157. </v-btn>
  158. </v-card-actions>
  159. </v-card>
  160. </div>
  161. </div>
  162. </div>
  163. </div>
  164. <!-- <v-divider></v-divider> -->
  165. <div class="tools pa-3" v-if="Object.keys(info).length > 0">
  166. <slot name="tools"></slot>
  167. <!-- <v-btn
  168. v-for="tool in tools"
  169. :key="tool.name"
  170. size="small"
  171. :prepend-icon="tool.icon"
  172. class="mr-3"
  173. @click="tool.handle"
  174. >
  175. {{ tool.name }}
  176. </v-btn> -->
  177. </div>
  178. <div class="bottom-info">
  179. <v-divider></v-divider>
  180. <div class="pa-3">
  181. <v-textarea
  182. v-model="inputVal"
  183. label="请输入消息"
  184. placeholder="请输入消息 按Ctrl+Enter换行"
  185. hide-details
  186. no-resize
  187. bg-color="white"
  188. variant="plain"
  189. :disabled="Object.keys(info).length === 0"
  190. @keydown="handleKeyDown"
  191. >
  192. <!-- @keydown.stop.prevent="handleKeyDown" -->
  193. <template #append-inner>
  194. <v-btn color="primary" :disabled="!inputVal" style="align-self: center;" @click="handleSend">发送</v-btn>
  195. </template>
  196. </v-textarea>
  197. <!-- <v-text-field v-model="inputVal" label="请输入消息" color="primary" density="comfortable" variant="plain" hide-details></v-text-field> -->
  198. <!-- <div class="d-flex align-center justify-end bottom-send">
  199. <div class="color-ccc font-size-14 mr-5">按Enter键发送</div>
  200. <v-btn color="primary" size="small" :disabled="!inputVal" @click="handleSend">发送</v-btn>
  201. </div> -->
  202. </div>
  203. </div>
  204. </div>
  205. </template>
  206. <script setup>
  207. defineOptions({ name: 'message-chatting'})
  208. import { ref, nextTick, onMounted } from 'vue'
  209. import { timesTampChange } from '@/utils/date'
  210. import { useIMStore } from '@/store/im'
  211. import { useUserStore } from '@/store/user'
  212. // const isEnterprise = inject('isEnterprise')
  213. const emits = defineEmits(['handleMore', 'handleSend'])
  214. defineProps({
  215. items: {
  216. type: Array,
  217. default: () => []
  218. },
  219. info: {
  220. type: Object,
  221. default: () => ({})
  222. },
  223. // uid: {
  224. // type: String,
  225. // default: ''
  226. // },
  227. hasMore: {
  228. type: Boolean,
  229. default: false
  230. }
  231. })
  232. const overlay = ref(false)
  233. const IM = useIMStore()
  234. const userStore = useUserStore()
  235. const loading = ref(false)
  236. const mAvatar = userStore.baseInfo?.avatar || 'https://minio.citupro.com/dev/menduner/7.png'
  237. const chatRef = ref()
  238. const inputVal = ref('')
  239. const pullDowning = ref(false) // 下拉中
  240. const pulldownFinished = ref(false) // 下拉完成
  241. // const enterpriseTools = [
  242. // { name: '查看面试', icon: 'mdi-email-newsletter', handle: handleInquire },
  243. // { name: '面试邀约', icon: 'mdi-email', handle: handleInvite }
  244. // ]
  245. // const userTools = [
  246. // { name: '查看面试', icon: 'mdi-email-newsletter', handle: handleInquire }
  247. // ]
  248. // const tools = isEnterprise ? enterpriseTools : userTools
  249. // 查看
  250. // function handleInquire () {
  251. // console.log(props.info)
  252. // emits('handleInquire', props.info.userId, props.info.enterpriseId || undefined)
  253. // }
  254. // // 邀请
  255. // function handleInvite () {
  256. // emits('handleInvite', props.info.userId, props.info.enterpriseId || undefined)
  257. // }
  258. // 滚动到底部
  259. const scrollBottom = () => {
  260. const chat = chatRef.value
  261. if (chat) {
  262. nextTick(function () {
  263. chat.scrollTop = chat.scrollHeight
  264. })
  265. }
  266. }
  267. onMounted(() => {
  268. nextTick(() => {
  269. scrollBottom()
  270. })
  271. })
  272. const handleMore = () => {
  273. emits('handleMore')
  274. }
  275. const handleSend = () => {
  276. emits('handleSend', inputVal)
  277. }
  278. // const pullDown = async () => {
  279. // if (messages.value.length == 0) {
  280. // return
  281. // }
  282. // const firstMsg = messages.value[0]
  283. // if (firstMsg.messageSeq == 1) {
  284. // pulldownFinished.value = true
  285. // return
  286. // }
  287. // const limit = 15
  288. // const msgs = await WKSDK.shared().chatManager.syncMessages(to.value, {
  289. // limit: limit,
  290. // startMessageSeq: firstMsg.messageSeq - 1,
  291. // endMessageSeq: 0,
  292. // pullMode: PullMode.Down,
  293. // })
  294. // if (msgs.length < limit) {
  295. // pulldownFinished.value = true;
  296. // }
  297. // if (msgs && msgs.length > 0) {
  298. // msgs.reverse().forEach((m) => {
  299. // messages.value.unshift(m)
  300. // })
  301. // }
  302. // nextTick(function () {
  303. // const chat = chatRef.value
  304. // const firstMsgEl = document.getElementById(firstMsg.clientMsgNo)
  305. // if (firstMsgEl) {
  306. // chat.scrollTop = firstMsgEl.offsetTop
  307. // }
  308. // })
  309. // }
  310. const handleScroll = (e) => {
  311. const targetScrollTop = e.target.scrollTop;
  312. if (targetScrollTop <= 250) {
  313. // 下拉
  314. if (pullDowning.value || pulldownFinished.value) {
  315. return
  316. }
  317. console.log('下拉')
  318. pullDowning.value = true
  319. // pullDown().then(() => {
  320. // pullDowning.value = false
  321. // }).catch(() => {
  322. // pullDowning.value = false
  323. // })
  324. }
  325. }
  326. const changeOverlay = (val) => {
  327. overlay.value = val
  328. }
  329. const changeLoading = (val) => {
  330. loading.value = val
  331. }
  332. const handleKeyDown = (event) => {
  333. if (event.keyCode === 13) {
  334. event.preventDefault()
  335. if(event.ctrlKey) {
  336. // 换行
  337. inputVal.value += '\n'
  338. return
  339. }
  340. // 发送
  341. emits('handleSend', inputVal)
  342. }
  343. }
  344. const reset = () => {
  345. inputVal.value = ''
  346. }
  347. const handleAgree = (val) => {
  348. // const { positionInfo, ...obj } = val
  349. // console.log(positionInfo)
  350. emits('handleAgree', val)
  351. }
  352. defineExpose({
  353. reset,
  354. changeLoading,
  355. scrollBottom,
  356. changeOverlay
  357. })
  358. </script>
  359. <style scoped lang="scss">
  360. .chatting {
  361. position: relative;
  362. height: 100%;
  363. }
  364. .top-info {
  365. // height: 104px;
  366. padding: 0 30px;
  367. .user-info {
  368. position: relative;
  369. height: 48px;
  370. justify-content: space-between;
  371. // padding: 0 30px;
  372. span {
  373. font-size: 14px;
  374. font-weight: 400;
  375. // color: var(--color-666);
  376. line-height: 22px;
  377. }
  378. .name {
  379. line-height: 25px;
  380. margin-right: 20px;
  381. }
  382. }
  383. .position-content {
  384. position: relative;
  385. padding: 16px;
  386. border-radius: 12px;
  387. // margin: auto;
  388. width: 60%;
  389. max-width: 800px;
  390. // width: 760px;
  391. background: linear-gradient(90deg, #d5ffff, #f0fff4);
  392. .salary {
  393. font-size: 20px;
  394. font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
  395. color: var(--v-error-base);
  396. line-height: 24px;
  397. }
  398. &-emolument {
  399. color: #f30;
  400. font-weight: 600;
  401. }
  402. }
  403. }
  404. .message-box {
  405. flex: 1;
  406. padding: 0 30px 30px;
  407. overflow-y: auto;
  408. .time-box {
  409. user-select: none;
  410. position: relative;
  411. top: 8px;
  412. margin: 20px 0;
  413. max-height: 20px;
  414. text-align: center;
  415. font-weight: 400;
  416. font-size: 12px;
  417. color: var(--color-time-divider);
  418. }
  419. .message-view_item {
  420. display: flex;
  421. flex-direction: row;
  422. align-items: flex-start;
  423. margin: 8px 0;
  424. position: relative;
  425. .message-text {
  426. overflow-wrap: break-word;
  427. background-color: #f0f2f5;
  428. border-radius: 6px;
  429. max-width: 85%;
  430. padding: 10px;
  431. &.active {
  432. background: #d5e6e8;
  433. }
  434. }
  435. }
  436. .is-self {
  437. flex-direction: row-reverse;
  438. display: flex;
  439. .message-text {
  440. margin-right: 10px;
  441. }
  442. }
  443. .is-other {
  444. .message-text {
  445. margin-left: 10px;
  446. }
  447. }
  448. }
  449. .bottom-info {
  450. width: 100%;
  451. height: 160px;
  452. background-color: #fff;
  453. }
  454. input {
  455. outline: none;
  456. width: 748px;
  457. height: 60px;
  458. max-width: 748px;
  459. overflow: auto;
  460. white-space: pre-wrap;
  461. &:focus {
  462. border: none;
  463. }
  464. }
  465. /* 滚动条样式 */
  466. ::-webkit-scrollbar {
  467. -webkit-appearance: none;
  468. width: 10px;
  469. height: 0px;
  470. }
  471. /* 滚动条内的轨道 */
  472. ::-webkit-scrollbar-track {
  473. background: rgba(0, 0, 0, 0.1);
  474. border-radius: 0;
  475. }
  476. /* 滚动条内的滑块 */
  477. ::-webkit-scrollbar-thumb {
  478. cursor: pointer;
  479. border-radius: 5px;
  480. background: rgba(0, 0, 0, 0.15);
  481. transition: color 0.2s ease;
  482. }
  483. </style>