index.vue 14 KB

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