index.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949
  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 > 0) {
  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. if (props.jobInfoData.recruitResumeUrl) {
  497. obj = {
  498. msgContent: {},
  499. msgType: "TIMFileElem",
  500. sendResume: 2,
  501. };
  502. } else {
  503. uni.$u.toast("请先上传附件简历~");
  504. return false;
  505. }
  506. } else if (title == "交换名片") {
  507. if (loginType.value == 1) {
  508. obj = {
  509. msgContent: {},
  510. msgType: "TIMTextElem",
  511. sendCard: 1,
  512. };
  513. } else {
  514. obj = {
  515. msgContent: {},
  516. msgType: "TIMTextElem",
  517. sendCard: 2,
  518. };
  519. }
  520. }
  521. console.log("props.jobInfoData", props.jobInfoData);
  522. // 发送消息后本地存储文本
  523. let url = config.baseUrl + "/recruit/recruitCommunicate";
  524. let data = {
  525. companyUserId: props.jobInfoData.companyUserId,
  526. postId: props.jobInfoData.postId,
  527. recruitUserId: props.jobInfoData.recruitUserId,
  528. sendType: 0,
  529. status: props.jobInfoData.status,
  530. ...obj,
  531. };
  532. console.log("data", data);
  533. let id = uni.getStorageSync("id");
  534. if (loginType.value == 1) {
  535. data.sendType = 1;
  536. // data.companyUserId = id;
  537. }
  538. uni.request({
  539. url: url,
  540. method: "POST",
  541. header: header,
  542. data: data,
  543. success: function (res) {
  544. if (res.data.code === 200) {
  545. // 发送简历后才能发送名片,打电话等高亮显示
  546. if (title == "发送简历" && props.jobInfoData.sendResume != 2) {
  547. emits("emitStatus", true);
  548. }
  549. } else {
  550. uni.$u.toast(res.data.msg);
  551. }
  552. },
  553. });
  554. } else if (title == "打电话") {
  555. if (loginType.value == 1) {
  556. uni.makePhoneCall({
  557. phoneNumber: props.jobInfoData.recruitUserPhone,
  558. });
  559. } else {
  560. uni.makePhoneCall({
  561. phoneNumber: props.jobInfoData.companyUserPhone,
  562. });
  563. }
  564. } else if (title == "面试邀请") {
  565. showDialog.value = true;
  566. }
  567. };
  568. // 跳转详情
  569. const toDetail = (postId, postStatus) => {
  570. if (postStatus == -1) {
  571. uni.$u.toast("该岗位已删除");
  572. } else {
  573. webUni.webView.navigateTo({
  574. url: "/packageJob/home/job-details?id=" + postId + "&type=1",
  575. });
  576. }
  577. };
  578. function getGlobalAudioContext(
  579. audioMap: Map<string, IAudioContext>,
  580. options?: { newAudioSrc: string }
  581. ) {
  582. if (options?.newAudioSrc) {
  583. broadcastNewAudioSrc.value = options.newAudioSrc;
  584. }
  585. }
  586. async function onMessageListUpdated(list: IMessageModel[]) {
  587. observer?.disconnect();
  588. // 是否已发送更新请求标志
  589. let isRequest = false;
  590. console.log(111111111111111111, list);
  591. messageList.value = list
  592. .filter((message) => !message.isDeleted)
  593. .map((message) => {
  594. message.vueForRenderKey = `${message.ID}`;
  595. console.log(isRequest);
  596. console.log(message);
  597. if (
  598. !isRequest &&
  599. message.type == "TIMCustomElem" &&
  600. (JSON.parse(JSON.stringify(message.cloudCustomData)).status == 0) !=
  601. props.jobInfoData
  602. ) {
  603. emits("emitStatus", true);
  604. // 仅执行一次更新请求
  605. isRequest = true;
  606. }
  607. return message;
  608. });
  609. const newLastMessage = messageList.value?.[messageList.value?.length - 1];
  610. if (messageTarget.value) {
  611. // scroll to target message
  612. scrollAndBlinkMessage(messageTarget.value);
  613. } else if (
  614. !isLoadingMessage.value &&
  615. !(
  616. scrollButtonInstanceRef.value?.isScrollButtonVisible &&
  617. newLastMessage?.flow === "in"
  618. )
  619. ) {
  620. // scroll to bottom
  621. nextTick(() => {
  622. scrollToBottom();
  623. });
  624. }
  625. if (isEnabledMessageReadReceiptGlobal()) {
  626. nextTick(() => bindIntersectionObserver());
  627. }
  628. }
  629. // 滚动到最新消息
  630. async function scrollToLatestMessage() {
  631. try {
  632. const { scrollHeight } = await getScrollInfo(
  633. "#messageScrollList",
  634. "messageList"
  635. );
  636. if (scrollHeight) {
  637. scrollTop.value === scrollHeight
  638. ? (scrollTop.value = scrollHeight + 1)
  639. : (scrollTop.value = scrollHeight);
  640. } else {
  641. scrollToBottom();
  642. }
  643. } catch (error) {
  644. scrollToBottom();
  645. }
  646. }
  647. async function onMessageSourceUpdated(message: IMessageModel) {
  648. messageTarget.value = message;
  649. scrollAndBlinkMessage(messageTarget.value);
  650. }
  651. function scrollAndBlinkMessage(message: IMessageModel) {
  652. if (
  653. messageList.value?.some(
  654. (messageListItem) => messageListItem?.ID === message?.ID
  655. )
  656. ) {
  657. nextTick(async () => {
  658. await scrollToTargetMessage(message);
  659. await blinkMessage(message?.ID);
  660. messageTarget.value = undefined;
  661. });
  662. }
  663. }
  664. function onChatCompletedUpdated(flag: boolean) {
  665. isCompleted.value = flag;
  666. }
  667. // 获取历史消息
  668. const getHistoryMessageList = () => {
  669. isLoadingMessage.value = true;
  670. const currentFirstMessageID = messageList.value?.[0]?.ID || "";
  671. TUIChatService.getMessageList().then(() => {
  672. nextTick(() => {
  673. historyFirstMessageID.value = currentFirstMessageID;
  674. const timer = setTimeout(() => {
  675. historyFirstMessageID.value = "";
  676. isLoadingMessage.value = false;
  677. clearTimeout(timer);
  678. }, 500);
  679. });
  680. });
  681. };
  682. // todo: webview 跳转
  683. const openComplaintLink = () => {};
  684. // 消息操作
  685. const handleToggleMessageItem = (
  686. e: any,
  687. message: IMessageModel,
  688. index: number,
  689. isLongpress = false
  690. ) => {
  691. if (isLongpress) {
  692. isLongpressing.value = true;
  693. }
  694. toggleID.value = message.ID;
  695. };
  696. // h5 long press
  697. let timer: number;
  698. const handleH5LongPress = (
  699. e: any,
  700. message: IMessageModel,
  701. index: number,
  702. type: string
  703. ) => {
  704. if (!isH5) return;
  705. function longPressHandler() {
  706. clearTimeout(timer);
  707. handleToggleMessageItem(e, message, index, true);
  708. }
  709. function touchStartHandler() {
  710. timer = setTimeout(longPressHandler, 500);
  711. }
  712. function touchEndHandler() {
  713. clearTimeout(timer);
  714. }
  715. switch (type) {
  716. case "touchstart":
  717. touchStartHandler();
  718. break;
  719. case "touchend":
  720. touchEndHandler();
  721. setTimeout(() => {
  722. isLongpressing.value = false;
  723. }, 200);
  724. break;
  725. }
  726. };
  727. // 消息撤回后,编辑消息
  728. const handleEdit = (message: IMessageModel) => {
  729. emits("handleEditor", message, "reedit");
  730. };
  731. // 重发消息
  732. const resendMessage = (message: IMessageModel) => {
  733. reSendDialogShow.value = true;
  734. resendMessageData.value = message;
  735. };
  736. // 图片预览
  737. // 开启图片预览
  738. const handleImagePreview = (index: number) => {
  739. if (!messageList.value) {
  740. return;
  741. }
  742. const imageMessageIndex: number[] = [];
  743. const imageMessageList: IMessageModel[] = messageList.value.filter(
  744. (item, index) => {
  745. if (
  746. !item.isRevoked &&
  747. !item.hasRiskContent &&
  748. item.type === TYPES.value.MSG_IMAGE
  749. ) {
  750. imageMessageIndex.push(index);
  751. return true;
  752. }
  753. return false;
  754. }
  755. );
  756. uni.previewImage({
  757. current: imageMessageIndex.indexOf(index),
  758. urls: imageMessageList.map(
  759. (message) => message.payload.imageInfoArray?.[2].url
  760. ),
  761. // #ifdef APP-PLUS
  762. indicator: "number",
  763. // #endif
  764. });
  765. };
  766. const resendMessageConfirm = () => {
  767. reSendDialogShow.value = !reSendDialogShow.value;
  768. const messageModel = resendMessageData.value;
  769. messageModel.resendMessage();
  770. };
  771. function blinkMessage(messageID: string): Promise<void> {
  772. return new Promise((resolve) => {
  773. const index = blinkMessageIDList.value.indexOf(messageID);
  774. if (index < 0) {
  775. blinkMessageIDList.value.push(messageID);
  776. const timer = setTimeout(() => {
  777. blinkMessageIDList.value.splice(
  778. blinkMessageIDList.value.indexOf(messageID),
  779. 1
  780. );
  781. clearTimeout(timer);
  782. resolve();
  783. }, 3000);
  784. }
  785. });
  786. }
  787. function scrollTo(scrollHeight: number) {
  788. scrollTop.value = scrollHeight;
  789. }
  790. async function bindIntersectionObserver() {
  791. if (!messageList.value || messageList.value.length === 0) {
  792. return;
  793. }
  794. if (
  795. groupType === TYPES.value.GRP_AVCHATROOM ||
  796. groupType === TYPES.value.GRP_COMMUNITY
  797. ) {
  798. // 直播群以及社群不进行消息的已读回执监听
  799. return;
  800. }
  801. observer?.disconnect();
  802. observer = uni
  803. .createIntersectionObserver(thisInstance, {
  804. threshold: [0.7],
  805. observeAll: true,
  806. // uni 下会把 safetip 也算进去 需要负 margin 来排除
  807. })
  808. .relativeTo("#messageScrollList", { top: -70 });
  809. observer?.observe(".message-li.in .message-bubble-container", (res: any) => {
  810. if (sentReceiptMessageID.has(res.id)) {
  811. return;
  812. }
  813. const matchingMessage = messageList.value.find((message: IMessageModel) => {
  814. return res.id.indexOf(message.ID) > -1;
  815. });
  816. if (
  817. matchingMessage &&
  818. matchingMessage.needReadReceipt &&
  819. matchingMessage.flow === "in" &&
  820. !matchingMessage.readReceiptInfo?.isPeerRead
  821. ) {
  822. TUIChatService.sendMessageReadReceipt([matchingMessage]);
  823. sentReceiptMessageID.add(res.id);
  824. }
  825. });
  826. }
  827. function setReadReceiptPanelVisible(visible: boolean, message?: IMessageModel) {
  828. if (!visible) {
  829. readStatusMessage.value = undefined;
  830. } else {
  831. readStatusMessage.value = message;
  832. }
  833. isShowReadUserStatusPanel.value = visible;
  834. }
  835. async function scrollToTargetMessage(message: IMessageModel) {
  836. const targetMessageID = message.ID;
  837. const isTargetMessageInScreen =
  838. messageList.value &&
  839. messageList.value.some((msg) => msg.ID === targetMessageID);
  840. if (targetMessageID && isTargetMessageInScreen) {
  841. const timer = setTimeout(async () => {
  842. try {
  843. const scrollViewRect = await getBoundingClientRect(
  844. "#messageScrollList",
  845. "messageList"
  846. );
  847. const originalMessageRect = await getBoundingClientRect(
  848. "#tui-" + targetMessageID,
  849. "messageList"
  850. );
  851. const { scrollTop } = await getScrollInfo(
  852. "#messageScrollList",
  853. "messageList"
  854. );
  855. const finalScrollTop =
  856. originalMessageRect.top +
  857. scrollTop -
  858. scrollViewRect.top -
  859. (selfAddValue++ % 2);
  860. scrollTo(finalScrollTop);
  861. clearTimeout(timer);
  862. } catch (error) {
  863. // todo
  864. }
  865. }, 500);
  866. } else {
  867. Toast({
  868. message: TUITranslateService.t("TUIChat.无法定位到原消息"),
  869. type: TOAST_TYPE.WARNING,
  870. });
  871. }
  872. }
  873. function onMessageListBackgroundClick() {
  874. emits("closeInputToolBar");
  875. }
  876. </script>
  877. <style lang="scss" scoped src="./style/index.scss"></style>
  878. <style lang="scss" scoped>
  879. .function-box {
  880. padding: 20px 0px 12px;
  881. > div {
  882. flex: 1;
  883. text-align: center;
  884. }
  885. img {
  886. width: 20px;
  887. height: 20px;
  888. margin-bottom: 12px;
  889. }
  890. }
  891. .job-info {
  892. padding: 8px 25px;
  893. background: #f1fffd;
  894. .company-name {
  895. margin-top: 5px;
  896. }
  897. }
  898. </style>