index.vue 27 KB

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