index.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947
  1. <template>
  2. <div>
  3. <div
  4. :class="{
  5. 'tui-chat': true,
  6. 'tui-chat-h5': isMobile,
  7. }"
  8. @click="onMessageListBackgroundClick"
  9. >
  10. <!-- <JoinGroupCard /> -->
  11. <div class="tui-chat-main">
  12. <div class="tui-chat-safe-tips">
  13. <div class="qzy-space-between-center function-box">
  14. <div
  15. v-for="(item, index) in chatFunctionList"
  16. :key="index"
  17. @tap="sendMsgHandle(item.title)"
  18. >
  19. <!-- 前3个按钮状态根据是否聊过天判断,接受按钮根据是否发送面试邀请判断 -->
  20. <template v-if="loginType == 1">
  21. <img :src="item.activeIcon" />
  22. </template>
  23. <template v-else>
  24. <img
  25. v-if="index != 3"
  26. :src="
  27. messageList && messageList.length > 0
  28. ? item.activeIcon
  29. : item.icon
  30. "
  31. />
  32. <img
  33. v-if="index == 3"
  34. :src="
  35. props.jobInfoData.status == 0 ? item.activeIcon : item.icon
  36. "
  37. />
  38. </template>
  39. <div
  40. class="item-title"
  41. v-if="index != 3"
  42. :class="
  43. messageList && messageList.length > 0
  44. ? ''
  45. : 'item-item-disabled'
  46. "
  47. >
  48. {{ item.title }}
  49. </div>
  50. <div
  51. class="item-title"
  52. v-if="index == 3"
  53. :class="
  54. props.jobInfoData.status == 0 ? '' : 'item-item-disabled'
  55. "
  56. >
  57. {{ item.title }}
  58. </div>
  59. </div>
  60. </div>
  61. <div
  62. class="job-info"
  63. @tap="toDetail(jobInfoData.postId, jobInfoData.postStatus)"
  64. >
  65. <div class="qzy-space-between-center text-16 text-bold">
  66. <span class="color-333">{{ jobInfoData.postName }}</span>
  67. <span class="text-theme">{{ jobInfoData.salary }}</span>
  68. </div>
  69. <div class="text-500 text-14 color-999 company-name">
  70. {{ jobInfoData.companyName }}
  71. </div>
  72. </div>
  73. </div>
  74. <MessageGroupApplication
  75. v-if="isGroup"
  76. :key="props.groupID"
  77. :groupID="props.groupID"
  78. />
  79. <scroll-view
  80. id="messageScrollList"
  81. class="tui-message-list"
  82. scroll-y="true"
  83. :scroll-top="scrollTop"
  84. :scroll-into-view="`tui-${historyFirstMessageID}`"
  85. @scroll="handelScrollListScroll"
  86. >
  87. <p
  88. v-if="!isCompleted"
  89. class="message-more"
  90. @click="getHistoryMessageList"
  91. >
  92. {{ TUITranslateService.t("TUIChat.查看更多") }}
  93. </p>
  94. <li
  95. v-for="(item, index) in messageList"
  96. :id="`tui-${item.ID}`"
  97. :key="item.vueForRenderKey"
  98. :class="'message-li ' + item.flow"
  99. >
  100. <MessageTimestamp
  101. :currTime="item.time"
  102. :prevTime="index > 0 ? messageList[index - 1].time : 0"
  103. />
  104. <div class="message-item" @click="toggleID = ''">
  105. <MessageTip
  106. v-if="
  107. item.type === TYPES.MSG_GRP_TIP ||
  108. isCreateGroupCustomMessage(item)
  109. "
  110. :content="item.getMessageContent()"
  111. />
  112. <div
  113. v-else-if="!item.isRevoked && !isPluginMessage(item)"
  114. :id="`msg-bubble-${item.ID}`"
  115. class="message-bubble-container"
  116. @longpress="handleToggleMessageItem($event, item, index, true)"
  117. @touchstart="
  118. handleH5LongPress($event, item, index, 'touchstart')
  119. "
  120. @touchend="handleH5LongPress($event, item, index, 'touchend')"
  121. @mouseover="handleH5LongPress($event, item, index, 'touchend')"
  122. >
  123. <MessageBubble
  124. :messageItem="item"
  125. :content="item.getMessageContent()"
  126. :blinkMessageIDList="blinkMessageIDList"
  127. @resendMessage="resendMessage(item)"
  128. @blinkMessage="blinkMessage"
  129. @scrollTo="scrollTo"
  130. @setReadReceiptPanelVisible="setReadReceiptPanelVisible"
  131. >
  132. <MessageText
  133. v-if="item.type === TYPES.MSG_TEXT"
  134. :content="item.getMessageContent()"
  135. />
  136. <ProgressMessage
  137. v-if="item.type === TYPES.MSG_IMAGE"
  138. :content="item.getMessageContent()"
  139. :messageItem="item"
  140. >
  141. <MessageImage
  142. :content="item.getMessageContent()"
  143. :messageItem="item"
  144. @previewImage="handleImagePreview(index)"
  145. />
  146. </ProgressMessage>
  147. <ProgressMessage
  148. v-if="item.type === TYPES.MSG_VIDEO"
  149. :content="item.getMessageContent()"
  150. :messageItem="item"
  151. >
  152. <MessageVideo
  153. :content="item.getMessageContent()"
  154. :messageItem="item"
  155. />
  156. </ProgressMessage>
  157. <MessageAudio
  158. v-if="item.type === TYPES.MSG_AUDIO"
  159. :content="item.getMessageContent()"
  160. :messageItem="item"
  161. :broadcastNewAudioSrc="broadcastNewAudioSrc"
  162. @getGlobalAudioContext="getGlobalAudioContext"
  163. />
  164. <MessageFile
  165. v-if="item.type === TYPES.MSG_FILE"
  166. :content="item.getMessageContent()"
  167. />
  168. <MessageFace
  169. v-if="item.type === TYPES.MSG_FACE"
  170. :content="item.getMessageContent()"
  171. :isPC="isPC"
  172. />
  173. <MessageLocation
  174. v-if="item.type === TYPES.MSG_LOCATION"
  175. :content="item.getMessageContent()"
  176. />
  177. <MessageCustom
  178. v-if="item.type === TYPES.MSG_CUSTOM"
  179. :content="item.getMessageContent()"
  180. :messageItem="item"
  181. />
  182. </MessageBubble>
  183. </div>
  184. <MessagePlugin
  185. v-else-if="!item.isRevoked && isPluginMessage(item)"
  186. :message="item"
  187. @resendMessage="resendMessage"
  188. @handleToggleMessageItem="handleToggleMessageItem"
  189. @handleH5LongPress="handleH5LongPress"
  190. />
  191. <MessageRevoked
  192. v-else
  193. :isEdit="item.type === TYPES.MSG_TEXT"
  194. :messageItem="item"
  195. @messageEdit="handleEdit(item)"
  196. />
  197. <!-- <MessageTool
  198. v-if="item.ID === toggleID"
  199. :class="[
  200. 'message-tool',
  201. item.flow === 'out' ? 'message-tool-out' : 'message-tool-in',
  202. ]"
  203. :messageItem="item"
  204. /> -->
  205. </div>
  206. </li>
  207. </scroll-view>
  208. <!-- 滚动按钮 -->
  209. <ScrollButton
  210. ref="scrollButtonInstanceRef"
  211. @scrollToLatestMessage="scrollToLatestMessage"
  212. />
  213. <Dialog
  214. v-if="reSendDialogShow"
  215. :show="reSendDialogShow"
  216. :isH5="!isPC"
  217. :center="true"
  218. :isHeaderShow="isPC"
  219. @submit="resendMessageConfirm()"
  220. @update:show="(e) => (reSendDialogShow = e)"
  221. >
  222. <p class="delDialog-title">
  223. {{ TUITranslateService.t("TUIChat.确认重发该消息?") }}
  224. </p>
  225. </Dialog>
  226. <!-- 已读回执用户列表面板 -->
  227. <ReadReceiptPanel
  228. v-if="isShowReadUserStatusPanel"
  229. :message="Object.assign({}, readStatusMessage)"
  230. @setReadReceiptPanelVisible="setReadReceiptPanelVisible"
  231. />
  232. </div>
  233. </div>
  234. <InterViewApplication
  235. :mapAddress="mapAddress"
  236. ref="InterViewApplicationRef"
  237. v-if="showDialog"
  238. :showDialog="showDialog"
  239. @close="ShowDialogClose"
  240. :jobInfoData="jobInfoData"
  241. @onload="InterViewApplicationOnload"
  242. ></InterViewApplication>
  243. </div>
  244. </template>
  245. <script lang="ts" setup>
  246. import InterViewApplication from "./interview-application.vue";
  247. import ToolbarItemContainer from "../../TUIChat/message-input-toolbar/evaluate/index.vue";
  248. import {
  249. ref,
  250. nextTick,
  251. onMounted,
  252. onUnmounted,
  253. getCurrentInstance,
  254. watch,
  255. } from "../../../adapter-vue";
  256. import TUIChatEngine, {
  257. IMessageModel,
  258. TUIStore,
  259. StoreName,
  260. TUITranslateService,
  261. TUIChatService,
  262. } from "@tencentcloud/chat-uikit-engine";
  263. import throttle from "lodash/throttle";
  264. import {
  265. setInstanceMapping,
  266. getBoundingClientRect,
  267. getScrollInfo,
  268. } from "@tencentcloud/universal-api";
  269. // import { JoinGroupCard } from '@tencentcloud/call-uikit-wechat';
  270. import Link from "./link";
  271. import MessageGroupApplication from "./message-group-application/index.vue";
  272. import MessageText from "./message-elements/message-text.vue";
  273. import ProgressMessage from "../../common/ProgressMessage/index.vue";
  274. import MessageImage from "./message-elements/message-image.vue";
  275. import MessageAudio from "./message-elements/message-audio.vue";
  276. import MessageFile from "./message-elements/message-file.vue";
  277. import MessageFace from "./message-elements/message-face.vue";
  278. import MessageCustom from "./message-elements/message-custom.vue";
  279. import MessageTip from "./message-elements/message-tip.vue";
  280. import MessageBubble from "./message-elements/message-bubble.vue";
  281. import MessageLocation from "./message-elements/message-location.vue";
  282. import MessageTimestamp from "./message-elements/message-timestamp.vue";
  283. import MessageVideo from "./message-elements/message-video.vue";
  284. import MessageTool from "./message-tool/index.vue";
  285. import MessageRevoked from "./message-tool/message-revoked.vue";
  286. import MessagePlugin from "../../../plugins/plugin-components/message-plugin.vue";
  287. import ReadReceiptPanel from "./read-receipt-panel/index.vue";
  288. import ScrollButton from "./scroll-button/index.vue";
  289. import { isPluginMessage } from "../../../plugins/plugin-components/index";
  290. import Dialog from "../../common/Dialog/index.vue";
  291. import { Toast, TOAST_TYPE } from "../../common/Toast/index";
  292. import { isCreateGroupCustomMessage } from "../utils/utils";
  293. import { isEnabledMessageReadReceiptGlobal } from "../utils/utils";
  294. import { isPC, isH5, isMobile } from "../../../utils/env";
  295. import { IAudioContext } from "../../../interface";
  296. import config from "@/request/config";
  297. import value from "../../../../uni_modules/uview-ui/components/u-text/value";
  298. interface IEmits {
  299. (e: "closeInputToolBar"): void;
  300. (e: "handleEditor", message: IMessageModel, type: string): void;
  301. }
  302. const emits = defineEmits<IEmits>();
  303. const props = defineProps({
  304. groupID: {
  305. type: String,
  306. default: "",
  307. },
  308. isGroup: {
  309. type: Boolean,
  310. default: false,
  311. },
  312. jobInfoData: {
  313. type: Object,
  314. },
  315. address: {
  316. type: Object,
  317. },
  318. });
  319. const mapAddress = ref();
  320. watch(props, (nweProps) => {
  321. console.log("ccc", nweProps);
  322. mapAddress.value = nweProps.address;
  323. });
  324. const showDialog = ref(false);
  325. const chatFunctionList = ref();
  326. let observer: any = null;
  327. let groupType: string | undefined;
  328. const sentReceiptMessageID = new Set<string>();
  329. const thisInstance = getCurrentInstance()?.proxy || getCurrentInstance();
  330. const messageList = ref<IMessageModel[]>();
  331. const isCompleted = ref(false);
  332. const currentConversationID = ref("");
  333. const toggleID = ref("");
  334. const scrollTop = ref(5000); // 首次是 15 条消息,最大消息高度为300
  335. const TYPES = ref(TUIChatEngine.TYPES);
  336. const isLoadingMessage = ref(false);
  337. const isLongpressing = ref(false);
  338. const blinkMessageIDList = ref<string[]>([]);
  339. const messageTarget = ref<IMessageModel>();
  340. const scrollButtonInstanceRef = ref<InstanceType<typeof ScrollButton>>();
  341. const historyFirstMessageID = ref<string>("");
  342. let selfAddValue = 0;
  343. // audio control
  344. const broadcastNewAudioSrc = ref<string>("");
  345. // 阅读回执状态message
  346. const readStatusMessage = ref<IMessageModel>();
  347. const isShowReadUserStatusPanel = ref<boolean>(false);
  348. // 消息重发 Dialog
  349. const reSendDialogShow = ref(false);
  350. const resendMessageData = ref();
  351. // 消息滑动到底部,建议搭配 nextTick 使用
  352. const scrollToBottom = () => {
  353. // 文本消息高度:60, 最大消息高度 280
  354. scrollTop.value += 300;
  355. // 解决 uniapp 打包到 app 首次进入滑动到底部,300 可设置
  356. const timer = setTimeout(() => {
  357. scrollTop.value += 1;
  358. clearTimeout(timer);
  359. }, 300);
  360. };
  361. // 监听回调函数
  362. const onCurrentConversationIDUpdated = (conversationID: string) => {
  363. currentConversationID.value = conversationID;
  364. // 开启已读回执的状态 群聊缓存群类型
  365. if (isEnabledMessageReadReceiptGlobal()) {
  366. const { groupProfile } =
  367. TUIStore.getConversationModel(conversationID) || {};
  368. groupType = groupProfile?.type;
  369. }
  370. };
  371. const ShowDialogClose = () => {
  372. showDialog.value = false;
  373. };
  374. const loginType = ref(uni.getStorageSync("loginType"));
  375. onMounted(() => {
  376. let loginType = uni.getStorageSync("loginType");
  377. if (loginType) {
  378. chatFunctionList.value = [
  379. {
  380. icon: require("@/static/images/icon/chat-icon1.png"),
  381. activeIcon: require("@/static/images/icon/chat-icon1-active.png"),
  382. title: "打电话",
  383. },
  384. {
  385. icon: require("@/static/images/icon/chat-icon2.png"),
  386. activeIcon: require("@/static/images/icon/chat-icon2-active.png"),
  387. title: "交换名片",
  388. },
  389. {
  390. icon: require("@/static/images/icon/chat-icon3-active.png"),
  391. activeIcon: require("@/static/images/icon/chat-icon3-active.png"),
  392. title: "面试邀请",
  393. },
  394. ];
  395. } else {
  396. chatFunctionList.value = [
  397. {
  398. icon: require("@/static/images/icon/chat-icon1.png"),
  399. activeIcon: require("@/static/images/icon/chat-icon1-active.png"),
  400. title: "打电话",
  401. },
  402. {
  403. icon: require("@/static/images/icon/chat-icon2.png"),
  404. activeIcon: require("@/static/images/icon/chat-icon2-active.png"),
  405. title: "交换名片",
  406. },
  407. {
  408. icon: require("@/static/images/icon/chat-icon3.png"),
  409. activeIcon: require("@/static/images/icon/chat-icon3-active.png"),
  410. title: "发送简历",
  411. },
  412. {
  413. icon: require("@/static/images/icon/chat-icon4.png"),
  414. activeIcon: require("@/static/images/icon/chat-icon4-active.png"),
  415. title: "接受预约",
  416. },
  417. ];
  418. }
  419. // 消息列表监听
  420. TUIStore.watch(StoreName.CHAT, {
  421. messageList: onMessageListUpdated,
  422. messageSource: onMessageSourceUpdated,
  423. isCompleted: onChatCompletedUpdated,
  424. });
  425. TUIStore.watch(StoreName.CONV, {
  426. currentConversationID: onCurrentConversationIDUpdated,
  427. });
  428. setInstanceMapping("messageList", thisInstance);
  429. uni.$on("scroll-to-bottom", scrollToLatestMessage);
  430. });
  431. // 取消监听
  432. onUnmounted(() => {
  433. TUIStore.unwatch(StoreName.CHAT, {
  434. messageList: onMessageListUpdated,
  435. isCompleted: onChatCompletedUpdated,
  436. });
  437. TUIStore.unwatch(StoreName.CONV, {
  438. currentConversationID: onCurrentConversationIDUpdated,
  439. });
  440. observer?.disconnect();
  441. observer = null;
  442. uni.$off("scroll-to-bottom");
  443. });
  444. const handelScrollListScroll = throttle(
  445. function (e: Event) {
  446. scrollButtonInstanceRef.value?.judgeScrollOverOneScreen(e);
  447. },
  448. 500,
  449. { leading: true }
  450. );
  451. const InterViewApplicationOnload = () => {
  452. emits("emitStatus", true);
  453. };
  454. // 各种消息处理
  455. const sendMsgHandle = (title) => {
  456. const header = {
  457. "Content-Type": "application/json",
  458. Authorization: uni.getStorageSync("token")
  459. ? uni.getStorageSync("token")
  460. : uni.getStorageSync("unitoken"),
  461. };
  462. console.log("props.jobInfoData", props.jobInfoData);
  463. if (loginType.value == 1) {
  464. } else {
  465. // 未发送过附件简历
  466. if (
  467. messageList.value.length == 0 &&
  468. (title == "打电话" || title == "交换名片" || title == "发送简历")
  469. ) {
  470. uni.$u.toast("与对方沟通后才可" + title);
  471. return false;
  472. }
  473. }
  474. //未收到面试邀请,接受按钮置灰不进行任何操作
  475. if (title == "接受预约") {
  476. if (props.jobInfoData.status == 0) {
  477. uni.request({
  478. url: config.baseUrl + "/recruit/recruitInterview/changeStatus",
  479. method: "POST",
  480. header: header,
  481. data: {
  482. id: props.jobInfoData.interviewId,
  483. status: 1,
  484. },
  485. success: function (res) {
  486. emits("emitStatus", true);
  487. },
  488. });
  489. } else {
  490. return false;
  491. }
  492. }
  493. if (title == "发送简历" || title == "交换名片") {
  494. let obj = {};
  495. if (title == "发送简历") {
  496. obj = {
  497. msgContent: {},
  498. msgType: "TIMFileElem",
  499. sendResume: 2,
  500. };
  501. } else if (title == "交换名片") {
  502. if (loginType.value == 1) {
  503. obj = {
  504. msgContent: {},
  505. msgType: "TIMTextElem",
  506. sendCard: 1,
  507. };
  508. } else {
  509. obj = {
  510. msgContent: {},
  511. msgType: "TIMTextElem",
  512. sendCard: 2,
  513. };
  514. }
  515. }
  516. console.log('props.jobInfoData',props.jobInfoData);
  517. // 发送消息后本地存储文本
  518. let url = config.baseUrl + "/recruit/recruitCommunicate";
  519. let data = {
  520. companyUserId: props.jobInfoData.companyUserId,
  521. postId: props.jobInfoData.postId,
  522. recruitUserId: props.jobInfoData.recruitUserId,
  523. sendType: 0,
  524. status: props.jobInfoData.status,
  525. ...obj,
  526. };
  527. console.log('data',data);
  528. let id = uni.getStorageSync("id");
  529. if (loginType.value == 1) {
  530. data.sendType = 1;
  531. // data.companyUserId = id;
  532. }
  533. uni.request({
  534. url: url,
  535. method: "POST",
  536. header: header,
  537. data: data,
  538. success: function (res) {
  539. if (res.data.code === 200) {
  540. // 发送简历后才能发送名片,打电话等高亮显示
  541. if (title == "发送简历" && props.jobInfoData.sendResume != 2) {
  542. emits("emitStatus", true);
  543. }
  544. } else {
  545. uni.$u.toast(res.data.msg);
  546. }
  547. },
  548. });
  549. } else if (title == "打电话") {
  550. if (loginType.value == 1) {
  551. uni.makePhoneCall({
  552. phoneNumber: props.jobInfoData.recruitUserPhone,
  553. });
  554. } else {
  555. uni.makePhoneCall({
  556. phoneNumber: props.jobInfoData.companyUserPhone,
  557. });
  558. }
  559. } else if (title == "面试邀请") {
  560. showDialog.value = true;
  561. }
  562. };
  563. // 跳转详情
  564. const toDetail = (postId, postStatus) => {
  565. if (postStatus == -1) {
  566. uni.$u.toast("该岗位已删除");
  567. } else {
  568. webUni.webView.navigateTo({
  569. url: "/packageJob/home/job-details?id=" + postId + "&type=1",
  570. });
  571. }
  572. };
  573. function getGlobalAudioContext(
  574. audioMap: Map<string, IAudioContext>,
  575. options?: { newAudioSrc: string }
  576. ) {
  577. if (options?.newAudioSrc) {
  578. broadcastNewAudioSrc.value = options.newAudioSrc;
  579. }
  580. }
  581. async function onMessageListUpdated(list: IMessageModel[]) {
  582. observer?.disconnect();
  583. // 是否已发送更新请求标志
  584. let isRequest = false;
  585. console.log(111111111111111111,list);
  586. messageList.value = list
  587. .filter((message) => !message.isDeleted)
  588. .map((message) => {
  589. message.vueForRenderKey = `${message.ID}`;
  590. console.log(isRequest);
  591. console.log(message);
  592. if (
  593. !isRequest &&
  594. message.type == "TIMCustomElem" &&
  595. (JSON.parse(JSON.stringify(message.cloudCustomData)).status == 0) !=
  596. props.jobInfoData
  597. ) {
  598. emits("emitStatus", true);
  599. // 仅执行一次更新请求
  600. isRequest = true;
  601. }
  602. return message;
  603. });
  604. const newLastMessage = messageList.value?.[messageList.value?.length - 1];
  605. if (messageTarget.value) {
  606. // scroll to target message
  607. scrollAndBlinkMessage(messageTarget.value);
  608. } else if (
  609. !isLoadingMessage.value &&
  610. !(
  611. scrollButtonInstanceRef.value?.isScrollButtonVisible &&
  612. newLastMessage?.flow === "in"
  613. )
  614. ) {
  615. // scroll to bottom
  616. nextTick(() => {
  617. scrollToBottom();
  618. });
  619. }
  620. if (isEnabledMessageReadReceiptGlobal()) {
  621. nextTick(() => bindIntersectionObserver());
  622. }
  623. }
  624. // 滚动到最新消息
  625. async function scrollToLatestMessage() {
  626. try {
  627. const { scrollHeight } = await getScrollInfo(
  628. "#messageScrollList",
  629. "messageList"
  630. );
  631. if (scrollHeight) {
  632. scrollTop.value === scrollHeight
  633. ? (scrollTop.value = scrollHeight + 1)
  634. : (scrollTop.value = scrollHeight);
  635. } else {
  636. scrollToBottom();
  637. }
  638. } catch (error) {
  639. scrollToBottom();
  640. }
  641. }
  642. async function onMessageSourceUpdated(message: IMessageModel) {
  643. messageTarget.value = message;
  644. scrollAndBlinkMessage(messageTarget.value);
  645. }
  646. function scrollAndBlinkMessage(message: IMessageModel) {
  647. if (
  648. messageList.value?.some(
  649. (messageListItem) => messageListItem?.ID === message?.ID
  650. )
  651. ) {
  652. nextTick(async () => {
  653. await scrollToTargetMessage(message);
  654. await blinkMessage(message?.ID);
  655. messageTarget.value = undefined;
  656. });
  657. }
  658. }
  659. function onChatCompletedUpdated(flag: boolean) {
  660. isCompleted.value = flag;
  661. }
  662. // 获取历史消息
  663. const getHistoryMessageList = () => {
  664. isLoadingMessage.value = true;
  665. const currentFirstMessageID = messageList.value?.[0]?.ID || "";
  666. TUIChatService.getMessageList().then(() => {
  667. nextTick(() => {
  668. historyFirstMessageID.value = currentFirstMessageID;
  669. const timer = setTimeout(() => {
  670. historyFirstMessageID.value = "";
  671. isLoadingMessage.value = false;
  672. clearTimeout(timer);
  673. }, 500);
  674. });
  675. });
  676. };
  677. // todo: webview 跳转
  678. const openComplaintLink = () => {};
  679. // 消息操作
  680. const handleToggleMessageItem = (
  681. e: any,
  682. message: IMessageModel,
  683. index: number,
  684. isLongpress = false
  685. ) => {
  686. if (isLongpress) {
  687. isLongpressing.value = true;
  688. }
  689. toggleID.value = message.ID;
  690. };
  691. // h5 long press
  692. let timer: number;
  693. const handleH5LongPress = (
  694. e: any,
  695. message: IMessageModel,
  696. index: number,
  697. type: string
  698. ) => {
  699. if (!isH5) return;
  700. function longPressHandler() {
  701. clearTimeout(timer);
  702. handleToggleMessageItem(e, message, index, true);
  703. }
  704. function touchStartHandler() {
  705. timer = setTimeout(longPressHandler, 500);
  706. }
  707. function touchEndHandler() {
  708. clearTimeout(timer);
  709. }
  710. switch (type) {
  711. case "touchstart":
  712. touchStartHandler();
  713. break;
  714. case "touchend":
  715. touchEndHandler();
  716. setTimeout(() => {
  717. isLongpressing.value = false;
  718. }, 200);
  719. break;
  720. }
  721. };
  722. // 消息撤回后,编辑消息
  723. const handleEdit = (message: IMessageModel) => {
  724. emits("handleEditor", message, "reedit");
  725. };
  726. // 重发消息
  727. const resendMessage = (message: IMessageModel) => {
  728. reSendDialogShow.value = true;
  729. resendMessageData.value = message;
  730. };
  731. // 图片预览
  732. // 开启图片预览
  733. const handleImagePreview = (index: number) => {
  734. if (!messageList.value) {
  735. return;
  736. }
  737. const imageMessageIndex: number[] = [];
  738. const imageMessageList: IMessageModel[] = messageList.value.filter(
  739. (item, index) => {
  740. if (
  741. !item.isRevoked &&
  742. !item.hasRiskContent &&
  743. item.type === TYPES.value.MSG_IMAGE
  744. ) {
  745. imageMessageIndex.push(index);
  746. return true;
  747. }
  748. return false;
  749. }
  750. );
  751. uni.previewImage({
  752. current: imageMessageIndex.indexOf(index),
  753. urls: imageMessageList.map(
  754. (message) => message.payload.imageInfoArray?.[2].url
  755. ),
  756. // #ifdef APP-PLUS
  757. indicator: "number",
  758. // #endif
  759. });
  760. };
  761. const resendMessageConfirm = () => {
  762. reSendDialogShow.value = !reSendDialogShow.value;
  763. const messageModel = resendMessageData.value;
  764. messageModel.resendMessage();
  765. };
  766. function blinkMessage(messageID: string): Promise<void> {
  767. return new Promise((resolve) => {
  768. const index = blinkMessageIDList.value.indexOf(messageID);
  769. if (index < 0) {
  770. blinkMessageIDList.value.push(messageID);
  771. const timer = setTimeout(() => {
  772. blinkMessageIDList.value.splice(
  773. blinkMessageIDList.value.indexOf(messageID),
  774. 1
  775. );
  776. clearTimeout(timer);
  777. resolve();
  778. }, 3000);
  779. }
  780. });
  781. }
  782. function scrollTo(scrollHeight: number) {
  783. scrollTop.value = scrollHeight;
  784. }
  785. async function bindIntersectionObserver() {
  786. if (!messageList.value || messageList.value.length === 0) {
  787. return;
  788. }
  789. if (
  790. groupType === TYPES.value.GRP_AVCHATROOM ||
  791. groupType === TYPES.value.GRP_COMMUNITY
  792. ) {
  793. // 直播群以及社群不进行消息的已读回执监听
  794. return;
  795. }
  796. observer?.disconnect();
  797. observer = uni
  798. .createIntersectionObserver(thisInstance, {
  799. threshold: [0.7],
  800. observeAll: true,
  801. // uni 下会把 safetip 也算进去 需要负 margin 来排除
  802. })
  803. .relativeTo("#messageScrollList", { top: -70 });
  804. observer?.observe(".message-li.in .message-bubble-container", (res: any) => {
  805. if (sentReceiptMessageID.has(res.id)) {
  806. return;
  807. }
  808. const matchingMessage = messageList.value.find((message: IMessageModel) => {
  809. return res.id.indexOf(message.ID) > -1;
  810. });
  811. if (
  812. matchingMessage &&
  813. matchingMessage.needReadReceipt &&
  814. matchingMessage.flow === "in" &&
  815. !matchingMessage.readReceiptInfo?.isPeerRead
  816. ) {
  817. TUIChatService.sendMessageReadReceipt([matchingMessage]);
  818. sentReceiptMessageID.add(res.id);
  819. }
  820. });
  821. }
  822. function setReadReceiptPanelVisible(visible: boolean, message?: IMessageModel) {
  823. if (!visible) {
  824. readStatusMessage.value = undefined;
  825. } else {
  826. readStatusMessage.value = message;
  827. }
  828. isShowReadUserStatusPanel.value = visible;
  829. }
  830. async function scrollToTargetMessage(message: IMessageModel) {
  831. const targetMessageID = message.ID;
  832. const isTargetMessageInScreen =
  833. messageList.value &&
  834. messageList.value.some((msg) => msg.ID === targetMessageID);
  835. if (targetMessageID && isTargetMessageInScreen) {
  836. const timer = setTimeout(async () => {
  837. try {
  838. const scrollViewRect = await getBoundingClientRect(
  839. "#messageScrollList",
  840. "messageList"
  841. );
  842. const originalMessageRect = await getBoundingClientRect(
  843. "#tui-" + targetMessageID,
  844. "messageList"
  845. );
  846. const { scrollTop } = await getScrollInfo(
  847. "#messageScrollList",
  848. "messageList"
  849. );
  850. const finalScrollTop =
  851. originalMessageRect.top +
  852. scrollTop -
  853. scrollViewRect.top -
  854. (selfAddValue++ % 2);
  855. scrollTo(finalScrollTop);
  856. clearTimeout(timer);
  857. } catch (error) {
  858. // todo
  859. }
  860. }, 500);
  861. } else {
  862. Toast({
  863. message: TUITranslateService.t("TUIChat.无法定位到原消息"),
  864. type: TOAST_TYPE.WARNING,
  865. });
  866. }
  867. }
  868. function onMessageListBackgroundClick() {
  869. emits("closeInputToolBar");
  870. }
  871. </script>
  872. <style lang="scss" scoped src="./style/index.scss"></style>
  873. <style lang="scss" scoped>
  874. .function-box {
  875. padding: 20px 0px 12px;
  876. > div {
  877. flex: 1;
  878. text-align: center;
  879. }
  880. img {
  881. width: 20px;
  882. height: 20px;
  883. margin-bottom: 12px;
  884. }
  885. }
  886. .job-info {
  887. padding: 8px 25px;
  888. background: #f1fffd;
  889. .company-name {
  890. margin-top: 5px;
  891. }
  892. }
  893. </style>