index.vue 15 KB

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