index.vue 26 KB

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