index.vue 27 KB

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