index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. <template>
  2. <!-- #ifdef MP-WEIXIN -->
  3. <tuicallkit
  4. ref="TUICallKit"
  5. ></tuicallkit>
  6. <!-- #endif -->
  7. <view class="TUIChat" v-if="conversationType === 'chat'">
  8. <view
  9. class="more-btn"
  10. v-if="conversation?.type === 'GROUP'"
  11. @click="handleGetProfile"
  12. >
  13. 更多</view
  14. >
  15. <view class="TUIChat-container">
  16. <scroll-view
  17. class="TUIChat-main"
  18. scroll-y="true"
  19. :scroll-with-animation="true"
  20. :refresher-triggered="triggered"
  21. :refresher-enabled="true"
  22. @refresherrefresh="handleRefresher"
  23. :scroll-top="scrollTop"
  24. >
  25. <view
  26. class="TUI-message-list"
  27. @touchstart="handleTouchStart"
  28. @click="dialogID = ''"
  29. >
  30. <view class="loading-text" v-if="isCompleted">没有更多</view>
  31. <view
  32. v-for="(item, index) in messages"
  33. :key="item.ID"
  34. :id="'view' + item.ID"
  35. >
  36. <view class="time-container" v-if="item.showTime">{{
  37. caculateTimeago(item.time * 1000)
  38. }}</view>
  39. <MessageTip
  40. v-if="!item.isRevoked && item.type === types.MSG_GRP_TIP"
  41. :data="handleTipMessageShowContext(item)"
  42. />
  43. <!-- <MessageTip v-if="item.type === types.MSG_GRP_SYS_NOTICE" /> -->
  44. <MessageBubble
  45. v-if="!item.isRevoked && item.type !== types.MSG_GRP_TIP"
  46. :data="item"
  47. >
  48. <Message-Operate
  49. v-if="dialogID === item.ID"
  50. :data="item"
  51. class="dialog dialog-item"
  52. :style="{
  53. top: dialogPosition.top + 'px',
  54. right: dialogPosition.right + 'px',
  55. left: dialogPosition.left + 'px',
  56. }"
  57. >
  58. </Message-Operate>
  59. <MessageText
  60. :id="item.flow + '-' + item.ID"
  61. v-if="item.type === types.MSG_TEXT"
  62. :data="handleTextMessageShowContext(item)"
  63. :messageData="item"
  64. @longpress="handleItem($event, item)"
  65. ></MessageText>
  66. <MessageImage
  67. :id="item.flow + '-' + item.ID"
  68. v-if="item.type === types.MSG_IMAGE"
  69. :data="handleImageMessageShowContext(item)"
  70. :messageData="item"
  71. @longpress="handleItem($event, item)"
  72. ></MessageImage>
  73. <MessageVideo
  74. :id="item.flow + '-' + item.ID"
  75. v-if="item.type === types.MSG_VIDEO"
  76. :data="handleVideoMessageShowContext(item)"
  77. :messageData="item"
  78. @longpress="handleItem($event, item)"
  79. />
  80. <MessageAudio
  81. :id="item.flow + '-' + item.ID"
  82. v-if="item.type === types.MSG_AUDIO"
  83. :data="handleAudioMessageShowContext(item)"
  84. :messageData="item"
  85. @longpress="handleItem($event, item)"
  86. />
  87. <MessageFace
  88. :id="item.flow + '-' + item.ID"
  89. v-if="item.type === types.MSG_FACE"
  90. :data="handleFaceMessageShowContext(item)"
  91. :messageData="item"
  92. @longpress="handleItem($event, item)"
  93. />
  94. <MessageCustom
  95. :id="item.flow + '-' + item.ID"
  96. v-if="item.type === types.MSG_CUSTOM"
  97. :data="handleCustomMessageShowContext(item)"
  98. :messageData="item"
  99. @longpress="handleItem($event, item)"
  100. />
  101. </MessageBubble>
  102. <MessageRevoked
  103. v-if="item.isRevoked"
  104. :data="item"
  105. @edit="handleEdit(item)"
  106. />
  107. </view>
  108. </view>
  109. </scroll-view>
  110. </view>
  111. <TUIChatInput :text="text" :conversationData="conversation"></TUIChatInput>
  112. </view>
  113. <view class="TUIChat" v-if="conversationType === 'system'">
  114. <MessageSystem :data="messages" :types="types" />
  115. </view>
  116. </template>
  117. <script lang="ts">
  118. import {
  119. defineComponent,
  120. reactive,
  121. toRefs,
  122. computed,
  123. nextTick,
  124. watch,
  125. onMounted,
  126. shallowRef
  127. } from "vue";
  128. import {
  129. onReady,
  130. onLoad,
  131. onNavigationBarButtonTap,
  132. onUnload,
  133. } from "@dcloudio/uni-app";
  134. // 消息元素组件
  135. import MessageBubble from "./components/message-elements/message-bubble.vue";
  136. import MessageText from "./components/message-elements/message-text.vue";
  137. import MessageImage from "./components/message-elements/message-image.vue";
  138. import MessageOperate from "./components/message-elements/message-operate.vue";
  139. import MessageVideo from "./components/message-elements/message-video.vue";
  140. import MessageAudio from "./components/message-elements/message-audio.vue";
  141. import MessageFace from "./components/message-elements/message-face.vue";
  142. import MessageCustom from "./components/message-elements/message-custom.vue";
  143. import MessageTip from "./components/message-elements/message-tip.vue";
  144. import MessageRevoked from "./components/message-elements/message-revoked.vue";
  145. import MessageSystem from "./components/message-elements/message-system.vue";
  146. // 底部消息发送组件
  147. import TUIChatInput from "./components/message-input";
  148. import store from "../../TUICore/store";
  149. import {
  150. handleAvatar,
  151. handleTextMessageShowContext,
  152. handleImageMessageShowContext,
  153. handleVideoMessageShowContext,
  154. handleAudioMessageShowContext,
  155. handleFileMessageShowContext,
  156. handleFaceMessageShowContext,
  157. handleLocationMessageShowContext,
  158. handleMergerMessageShowContext,
  159. handleTipMessageShowContext,
  160. handleCustomMessageShowContext,
  161. } from "../../utils/untis";
  162. import { caculateTimeago } from "../../utils/date";
  163. import Vuex from "vuex";
  164. import { TUIChatServer } from "../../TUICore/server";
  165. export default defineComponent({
  166. name: "TUIChat",
  167. components: {
  168. MessageText,
  169. MessageImage,
  170. MessageVideo,
  171. MessageAudio,
  172. MessageFace,
  173. MessageCustom,
  174. MessageBubble,
  175. MessageTip,
  176. MessageRevoked,
  177. MessageSystem,
  178. TUIChatInput,
  179. MessageOperate,
  180. },
  181. setup(props) {
  182. const timStore = store.state.timStore;
  183. uni.$TUIKit.TUIChatServer = new TUIChatServer();
  184. const TUICallKit = shallowRef(null);
  185. const TUIServer = uni.$TUIKit.TUIChatServer;
  186. const left: number | null = 0;
  187. const right: number | null = 0;
  188. const defaultDialogPosition = {
  189. top: -70,
  190. left,
  191. right,
  192. };
  193. const data = reactive({
  194. messageList: computed(() => timStore.messageList),
  195. conversation: computed(() => timStore.conversation),
  196. triggered: false,
  197. scrollTop: 999,
  198. text: "",
  199. types: uni.$TIM.TYPES,
  200. currentMessage: {},
  201. dialogID: "",
  202. forwardStatus: false,
  203. imageFlag: false,
  204. isCompleted: false,
  205. oldMessageTime: 0,
  206. dialogPosition: defaultDialogPosition,
  207. });
  208. // 判断当前会话类型:无/系统会话/正常C2C、群聊
  209. const conversationType = computed(() => {
  210. const conversation: any = data.conversation;
  211. if (!conversation?.conversationID) {
  212. return "";
  213. }
  214. if (conversation?.type === uni.$TIM.TYPES.CONV_SYSTEM) {
  215. return "system";
  216. }
  217. return "chat";
  218. });
  219. // 不展示已删除消息
  220. const messages = computed(() => {
  221. if (data.messageList.length > 0) {
  222. data.oldMessageTime = data.messageList[0].time;
  223. return data.messageList.filter((item: any) => {
  224. return !item.isDeleted;
  225. });
  226. }
  227. });
  228. // 获取页面参数
  229. onLoad((options) => {
  230. uni.setNavigationBarTitle({
  231. title: options && options.conversationName,
  232. });
  233. });
  234. onUnload(() => {
  235. // #ifdef MP-WEIXIN
  236. //回收 TUICallKit
  237. uni.$TUICallKit.value !== null && uni.$TUICallKit.value.destroyed();
  238. // #endif
  239. TUIServer.destroyed();
  240. });
  241. // 监听数据渲染,展示最新一条消息
  242. watch(messages, (newVal: any, oldVal: any) => {
  243. // 下拉刷新不滑动 todo 优化
  244. nextTick(() => {
  245. const newLastMessage = newVal[newVal.length - 1];
  246. const oldLastMessage = oldVal ? oldVal[oldVal.length - 1] : {};
  247. data.oldMessageTime = messages.value[0].time;
  248. handleShowTime();
  249. if (oldVal && newLastMessage.ID !== oldLastMessage.ID) {
  250. // handleScrollBottom(); // 非从conversationList 首次进入
  251. }
  252. });
  253. });
  254. // 监听数据初次渲染,展示最新一条消息
  255. // TODO app 中获取不到DOM 元素
  256. onReady(() => {
  257. const options = {
  258. sdkAppID: uni.$chat_SDKAppID, // 开通实时音视频服务创建应用后分配的 SDKAppID
  259. userID: uni.$chat_userID, // 用户 ID,可以由您的帐号系统指定
  260. userSig: uni.$chat_userSig, // 身份签名,相当于登录密码的作用
  261. tim: uni.$TUIKit, // tim 参数适用于业务中已存在 TIM 实例,为保证 TIM 实例唯一性
  262. }
  263. uni.$TUICallKit = TUICallKit;
  264. nextTick(() => {
  265. uni.$TUICallKit.value !== null && uni.$TUICallKit.value.init(options)
  266. });
  267. setTimeout(() => {
  268. data.scrollTop = 998;
  269. }, 500);
  270. });
  271. onMounted(() => {
  272. handleShowTime();
  273. // // 监听回退,已读上报
  274. uni.addInterceptor("navigateBack", {
  275. success() {
  276. // 小程序无效 官网链接:https://uniapp.dcloud.io/api/interceptor.html
  277. uni.$TUIKit.TUIConversationServer.setMessageRead(
  278. data.conversation.conversationID
  279. );
  280. },
  281. });
  282. });
  283. onNavigationBarButtonTap(() => {
  284. if (data.conversation?.type === uni.$TIM.TYPES.CONV_GROUP) {
  285. uni.navigateTo({
  286. url: "../TUIGroup/index",
  287. });
  288. } else {
  289. uni.showToast({
  290. title: "暂无信息",
  291. });
  292. }
  293. });
  294. const handleGetProfile = () => {
  295. uni.navigateTo({
  296. url: "../TUIGroup/index",
  297. });
  298. };
  299. const handleShowTime = () => {
  300. if (messages.value) {
  301. Array.from(messages.value).forEach((item) => {
  302. if (item.time - data.oldMessageTime > 5 * 60) {
  303. data.oldMessageTime = item.time;
  304. item.showTime = true;
  305. } else {
  306. item.showTime = false;
  307. }
  308. });
  309. }
  310. };
  311. const handleScrollBottom = () => {
  312. uni
  313. .createSelectorQuery()
  314. .select(".TUI-message-list")
  315. .boundingClientRect((res) => {
  316. const scrollH = res.height?.height;
  317. data.scrollTop = scrollH;
  318. })
  319. .exec();
  320. };
  321. // 需要自实现下拉加载
  322. const handleScroll = (e: any) => {
  323. data.triggered = "restore"; // 需要重置
  324. };
  325. const handleRefresher = () => {
  326. data.triggered = true;
  327. if (!data.isCompleted) {
  328. TUIServer.getHistoryMessageList().then((res) => {
  329. data.triggered = false;
  330. data.isCompleted = res.isCompleted;
  331. });
  332. }
  333. setTimeout(() => {
  334. data.triggered = false;
  335. }, 500);
  336. };
  337. // 处理需要合并的数据
  338. const handleSend = (emo: any) => {
  339. data.text += emo.name;
  340. // inputEle.value.focus();
  341. };
  342. // 发送消息
  343. const handleSendTextMessage = (e: any) => {
  344. if (data.text.trimEnd()) {
  345. TUIServer.sendTextMessage(JSON.parse(JSON.stringify(data.text)));
  346. }
  347. data.text = " ";
  348. };
  349. // 右键消息,展示处理功能
  350. const handleItem = (event: any, item: any) => {
  351. const { flow } = item;
  352. // const { height, top } = event.target.getBoundingClientRect();
  353. try {
  354. const query = uni.createSelectorQuery(); // .in(this)
  355. query
  356. .select(`#${item.flow + "-" + item.ID}`)
  357. .boundingClientRect((res: any) => {
  358. const { top } = res;
  359. // 弹框在下面显示,60--弹框高度;44--导航栏高度;20--弹框离信息间距
  360. if (top < 60 + 20) {
  361. data.dialogPosition = {
  362. ...data.dialogPosition,
  363. top: res.height?.res + 10, // 在下面展示弹框 + 10px 间隔
  364. };
  365. data.dialogPosition = {
  366. ...data.dialogPosition,
  367. right: flow === "out" ? 0 : null, // 发出去的消息(弹框 right 都是 0)
  368. left: flow === "in" ? 0 : null, // 接收的消息(弹框 left 都是 0)
  369. };
  370. } else {
  371. data.dialogPosition = {
  372. ...defaultDialogPosition,
  373. right: flow === "out" ? 0 : null, // 发出去的消息(弹框 right 都是 0)
  374. left: flow === "in" ? 0 : null, // 接收的消息(弹框 left 都是 0)
  375. };
  376. }
  377. })
  378. .exec((res: any) => {
  379. data.currentMessage = item;
  380. data.dialogID = item.ID;
  381. });
  382. } catch (error) {
  383. data.currentMessage = item;
  384. data.dialogID = item.ID;
  385. }
  386. };
  387. // 滑动触发时,失焦收起键盘
  388. const handleTouchStart = () => {
  389. uni.hideKeyboard();
  390. };
  391. // 重新编辑
  392. const handleEdit = (item: any) => {
  393. data.text = item.payload.text;
  394. };
  395. return {
  396. ...toRefs(data),
  397. TUICallKit,
  398. conversationType,
  399. messages,
  400. handleShowTime,
  401. handleTouchStart,
  402. handleRefresher,
  403. handleScroll,
  404. handleScrollBottom,
  405. handleSendTextMessage,
  406. handleItem,
  407. handleEdit,
  408. handleTextMessageShowContext,
  409. handleImageMessageShowContext,
  410. handleVideoMessageShowContext,
  411. handleAudioMessageShowContext,
  412. handleFileMessageShowContext,
  413. handleFaceMessageShowContext,
  414. handleLocationMessageShowContext,
  415. handleMergerMessageShowContext,
  416. handleTipMessageShowContext,
  417. handleCustomMessageShowContext,
  418. handleSend,
  419. caculateTimeago,
  420. handleGetProfile,
  421. };
  422. },
  423. });
  424. </script>
  425. <style lang="scss" scoped>
  426. @import "../styles/TUIChat.scss";
  427. </style>