index11.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. <template>
  2. <page-meta :root-font-size="$store.getters.storeRootFontSize"></page-meta>
  3. <view :style="{ backgroundColor: '#F5F7FA' }" class="chating_container">
  4. <!-- #ifndef APP-PLUS -->
  5. <chating-header @click="pageClick" ref="chatingHeaderRef" :mutipleCheckVisible="mutipleCheckVisible" @mutipleCheckUpdate="mutipleCheckUpdate" />
  6. <!-- #endif -->
  7. <chating-list
  8. @click="pageClick"
  9. :mutipleCheckVisible="mutipleCheckVisible"
  10. ref="chatingListRef"
  11. @initSuccess="initSuccess"
  12. :menuOutsideFlag="menuOutsideFlag"
  13. @closeMenu="closeMenu" />
  14. <chating-footer v-if="imType == 1 || imType == 2 || $companyUserIsLogin() || canShowBot" v-show="!mutipleCheckVisible" ref="chatingFooterRef" :footerOutsideFlag="footerOutsideFlag" @scrollToBottom="scrollToBottom" />
  15. <view v-show="mutipleCheckVisible" class="mutiple_action_container">
  16. <!-- <view @click="forward('BatchForWard')" class="action_item">
  17. <image src="@/static/images/mutiple_batch.png" />
  18. <text style="margin-top: 12rpx;"> 逐条转发 </text>
  19. </view> -->
  20. <view @click="forward('MergeForWard')" class="action_item">
  21. <image src="../../../static/images/mutiple_merge.png" />
  22. <text style="margin-top: 12rpx">合并转发</text>
  23. </view>
  24. <view @click="mutipleRemove" class="action_item">
  25. <image src="../../../static/images/mutiple_delete.png" />
  26. <text style="margin-top: 12rpx; color: #ff381f">删除</text>
  27. </view>
  28. </view>
  29. <u-loading-page :loading="initLoading"></u-loading-page>
  30. </view>
  31. </template>
  32. <script>
  33. import { mapGetters, mapActions } from 'vuex';
  34. import IMSDK, { SessionType } from 'openim-uniapp-polyfill';
  35. import { ContactChooseTypes, GroupMemberListTypes, PageEvents } from '../../../constant';
  36. import ChatingHeader from './components/ChatingHeader.vue';
  37. import ChatingFooter from './components/ChatingFooter/index.vue';
  38. import ChatingList from './components/ChatingList.vue';
  39. import { markConversationAsRead, parseMessageByType, callingModule } from '../../../util/imCommon';
  40. import MescrollMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js";
  41. export default {
  42. mixins: [ MescrollMixin ], //mescroll-body写在子组件时
  43. components: {
  44. ChatingHeader,
  45. ChatingFooter,
  46. ChatingList
  47. },
  48. data() {
  49. return {
  50. listHeight: 0,
  51. footerOutsideFlag: 0,
  52. menuOutsideFlag: 0,
  53. initLoading: false,
  54. mutipleCheckVisible: false,
  55. back2Tab: false,
  56. timer: null,
  57. fontSize:"40px",
  58. canShowBot:false,
  59. };
  60. },
  61. computed: {
  62. ...mapGetters(["timStore","storeFriendList","storeCurrentConversation","storeCurrentGroup"]),
  63. // 响应式派生数据
  64. imType() {
  65. return this.timStore?.imType || '';
  66. },
  67. orderId() {
  68. return this.timStore?.orderId || '';
  69. },
  70. isSingle() {
  71. return this.storeCurrentConversation.conversationType === SessionType.Single;
  72. },
  73. isNotify() {
  74. return this.storeCurrentConversation.conversationType === SessionType.Notification;
  75. },
  76. groupMemberCount() {
  77. return `(${this.storeCurrentGroup?.memberCount ?? 0})`;
  78. },
  79. getGroupAnnouncementContent() {
  80. if (this.showGroupAnnouncement) {
  81. return this.$store.getters.storeCurrentGroup.notification;
  82. }
  83. return '';
  84. },
  85. },
  86. onLoad(options) {
  87. this.setPageListener();
  88. this.checkCalling();
  89. if (options?.back2Tab) {
  90. this.back2Tab = JSON.parse(options.back2Tab);
  91. }
  92. // 尝试从参数恢复会话信息,解决nvue跳转时store可能未同步的问题
  93. if (options?.conversationID) {
  94. const currentID = this.$store.getters.storeCurrentConversation?.conversationID;
  95. console.log("qxj currentID",currentID);
  96. if (currentID !== options.conversationID) {
  97. const list = this.$store.getters.storeConversationList || [];
  98. const target = list.find(item => item.conversationID === options.conversationID);
  99. if (target) {
  100. this.$store.commit("conversation/SET_CURRENT_CONVERSATION", target);
  101. }
  102. }
  103. }
  104. let userID = this.$store.getters.storeCurrentConversation?.userID || '';
  105. //this.userID = userID.substring(1);
  106. //this.isUser = userID.indexOf('U') != -1;
  107. if (userID) {
  108. this.canShowBot = this.isUserInFriendList(userID, this.storeFriendList);
  109. }
  110. },
  111. onShow() {
  112. },
  113. onReady() {
  114. let title = this.$store.getters.storeCurrentConversation?.showName || '';
  115. if(!this.isSingle && !this.isNotify){
  116. title=title+this.groupMemberCount;
  117. }
  118. uni.setNavigationBarTitle({ title });
  119. },
  120. onUnload() {
  121. this.disposePageListener();
  122. markConversationAsRead(
  123. {...this.$store.getters.storeCurrentConversation},
  124. true
  125. );
  126. this.resetConversationState();
  127. this.resetMessageState();
  128. if (this.timer) {
  129. clearInterval(this.timer);
  130. }
  131. },
  132. methods: {
  133. ...mapActions('message', ['resetMessageState', 'deleteMessages']),
  134. ...mapActions('conversation', ['resetConversationState']),
  135. scrollToBottom(isRecv = false) {
  136. //this.$refs.chatingListRef.scrollToAnchor(`auchor${clientMsgID}`, isRecv);
  137. //this.$refs.chatingListRef.scrollToBottom(false, isRecv);
  138. this.$refs.chatingListRef.scrollToBottom();
  139. },
  140. nativeCallEndHandler() {
  141. if (typeof plus === 'undefined') {
  142. return;
  143. }
  144. if (!this.$store.getters.storeCurrentConversation.groupID) {
  145. return;
  146. }
  147. IMSDK.asyncApi('signalingGetRoomByGroupID', IMSDK.uuid(), this.$store.getters.storeCurrentConversation.groupID).then(({ data }) => {
  148. if (this.$refs.chatingHeaderRef) {
  149. if (data.invitation) {
  150. this.$refs.chatingHeaderRef.updateCallingData(data);
  151. } else {
  152. this.$refs.chatingHeaderRef.updateCallingData(null);
  153. }
  154. }
  155. });
  156. },
  157. checkCalling() {
  158. if (typeof plus === 'undefined') {
  159. return;
  160. }
  161. if (!this.$store.getters.storeCurrentConversation.groupID) {
  162. return;
  163. }
  164. if (this.timer) {
  165. clearInterval(this.timer);
  166. }
  167. this.nativeCallEndHandler();
  168. this.timer = setInterval(() => {
  169. this.nativeCallEndHandler();
  170. }, 10000);
  171. },
  172. mutipleRemove() {
  173. const checkedMessages = this.$store.getters.storeHistoryMessageList.filter((message) => message.checked);
  174. if (checkedMessages.length === 0) {
  175. uni.$u.toast('请先选择要操作的消息');
  176. return;
  177. }
  178. let count = 0;
  179. checkedMessages.map((message) => {
  180. IMSDK.asyncApi(IMSDK.IMMethods.DeleteMessage, IMSDK.uuid(), {
  181. conversationID: this.$store.getters.storeCurrentConversation.conversationID,
  182. clientMsgID: message.clientMsgID
  183. }).then(() => {
  184. count++;
  185. if (count === checkedMessages.length) {
  186. this.deleteMessages(checkedMessages);
  187. this.mutipleCheckUpdateHandler(false);
  188. uni.$u.toast('删除成功');
  189. }
  190. });
  191. });
  192. },
  193. forward(type) {
  194. const checkedMessages = this.$store.getters.storeHistoryMessageList.filter((message) => message.checked);
  195. if (checkedMessages.length === 0) {
  196. uni.$u.toast('请先选择要操作的消息');
  197. return;
  198. }
  199. const currentConversation = this.$store.getters.storeCurrentConversation;
  200. const title = currentConversation.groupID ? `群聊${currentConversation.showName}的聊天记录` : `和${currentConversation.showName}的聊天记录`;
  201. const mergeInfo = {
  202. messageList: [...checkedMessages],
  203. summaryList: checkedMessages.map((message) => `${message.senderNickname}:${parseMessageByType(message)}`),
  204. title
  205. };
  206. uni.navigateTo({
  207. url: `/pages_im/pages/common/contactChoose/index?type=${type}&mergeInfo=${encodeURIComponent(JSON.stringify(mergeInfo))}`
  208. });
  209. },
  210. closeMenu() {
  211. this.getEl('.message_menu_container').then((res) => {
  212. if (res) {
  213. this.menuOutsideFlag += 1;
  214. }
  215. });
  216. },
  217. async pageClick(e) {
  218. this.getEl('.message_menu_container').then((res) => {
  219. if (res) {
  220. this.menuOutsideFlag += 1;
  221. }
  222. });
  223. this.footerOutsideFlag += 1;
  224. },
  225. getEl(el) {
  226. return new Promise((resolve) => {
  227. const query = uni.createSelectorQuery().in(this);
  228. query.select(el)
  229. .boundingClientRect((data) => {
  230. resolve(data);
  231. }).exec();
  232. });
  233. },
  234. initSuccess() {
  235. this.initLoading = false;
  236. },
  237. async getCheckedUsers(data) {
  238. this.$refs.chatingFooterRef.customEditorCtx.undo();
  239. for (const item of data) {
  240. this.$refs.chatingFooterRef.insertAt(item.userID, item.nickname);
  241. await uni.$u.sleep(200);
  242. }
  243. },
  244. sendRtcInvite(mediaType, userIDList) {
  245. const isVideo = mediaType === 'video';
  246. const inviteeUserIDList = userIDList ?? [this.$store.getters.storeCurrentConversation.userID];
  247. callingModule.startLiveChat(isVideo, inviteeUserIDList, this.$store.getters.storeCurrentConversation.groupID, this.$store.getters.storeCurrentUserID);
  248. },
  249. chooseCallMediaType(mediaType) {
  250. return new Promise((resolve, reject) => {
  251. resolve(mediaType);
  252. if (mediaType) {
  253. resolve(mediaType);
  254. return;
  255. }
  256. // uni.showActionSheet({
  257. // itemList: ['语音通话', '视频通话'],
  258. // success: ({ tapIndex }) => {
  259. // resolve(tapIndex ? 'video' : 'audio');
  260. // },
  261. // fail: () => reject()
  262. // });
  263. });
  264. },
  265. // page event
  266. typingHandler() {
  267. if (this.$refs.chatingHeaderRef) {
  268. this.$refs.chatingHeaderRef.updateTyping();
  269. }
  270. },
  271. onlineCheckHandler() {
  272. if (this.$refs.chatingHeaderRef) {
  273. this.$refs.chatingHeaderRef.checkOnline();
  274. }
  275. },
  276. atSomeOneHandler({ userID, nickname }) {
  277. if (typeof plus !== 'undefined') {
  278. if (plus.os.name == 'iOS') {
  279. var UIImpactFeedbackGenerator = plus.ios.importClass('UIImpactFeedbackGenerator');
  280. var impact = new UIImpactFeedbackGenerator();
  281. impact.prepare();
  282. impact.init(1);
  283. impact.impactOccurred();
  284. } else {
  285. uni.vibrateLong();
  286. }
  287. } else {
  288. uni.vibrateLong();
  289. }
  290. if (this.$refs.chatingFooterRef) {
  291. this.$refs.chatingFooterRef.insertAt(userID, nickname);
  292. }
  293. },
  294. mutipleCheckUpdateHandler({ flag, message }) {
  295. this.mutipleCheckVisible = flag;
  296. if (message) {
  297. this.$store.dispatch('message/updateOneMessage', {
  298. message: {
  299. ...message,
  300. checked: true
  301. }
  302. });
  303. }
  304. if (!flag) {
  305. const tmpMessageList = [...this.$store.getters.storeHistoryMessageList];
  306. tmpMessageList.map((message) => (message.checked = false));
  307. this.$store.commit('message/SET_HISTORY_MESSAGE_LIST', tmpMessageList);
  308. }
  309. },
  310. rtcInvitePrepare(mediaType) {
  311. this.chooseCallMediaType(mediaType).then((callMediaType) => {
  312. if (this.$store.getters.storeCurrentConversation.userID) {
  313. this.sendRtcInvite(callMediaType);
  314. } else {
  315. uni.$u.route('/pages_im/pages/conversation/groupMemberList/index', {
  316. type: GroupMemberListTypes.CallInvite,
  317. groupID: this.$store.getters.storeCurrentConversation.groupID,
  318. callMediaType: callMediaType
  319. });
  320. }
  321. });
  322. },
  323. setPageListener() {
  324. IMSDK.subscribe('onRecvC2CReadReceipt', ({ data: receiptList }) => {
  325. console.log('onRecvC2CReadReceipt', receiptList);
  326. });
  327. uni.$on(PageEvents.TypingUpdate, this.typingHandler);
  328. uni.$on(PageEvents.OnlineStateCheck, this.onlineCheckHandler);
  329. uni.$on(PageEvents.ScrollToBottom, this.scrollToBottom);
  330. uni.$on(PageEvents.AtSomeOne, this.atSomeOneHandler);
  331. uni.$on(PageEvents.MutipleCheckUpdate, this.mutipleCheckUpdateHandler);
  332. uni.$on(PageEvents.RtcCall, this.rtcInvitePrepare);
  333. uni.$on(PageEvents.NativeCallEnd, this.nativeCallEndHandler);
  334. },
  335. disposePageListener() {
  336. uni.$off(PageEvents.TypingUpdate, this.typingHandler);
  337. uni.$off(PageEvents.OnlineStateCheck, this.onlineCheckHandler);
  338. uni.$off(PageEvents.ScrollToBottom, this.scrollToBottom);
  339. uni.$off(PageEvents.AtSomeOne, this.atSomeOneHandler);
  340. uni.$off(PageEvents.MutipleCheckUpdate, this.mutipleCheckUpdateHandler);
  341. uni.$off(PageEvents.RtcCall, this.rtcInvitePrepare);
  342. uni.$off(PageEvents.NativeCallEnd, this.nativeCallEndHandler);
  343. },
  344. mutipleCheckUpdate() {
  345. this.mutipleCheckUpdateHandler({ flag: false });
  346. },
  347. isUserInFriendList(sorUserId,storeList) {
  348. if (!storeList || !Array.isArray(storeList)) {
  349. return false;
  350. }
  351. return storeList.some(item => item.userID === sorUserId);
  352. }
  353. },
  354. // 原生导航栏按钮点击事件(titleNView.buttons)
  355. onNavigationBarButtonTap(e) {
  356. if (e?.id === 'more' || e?.index === 0) {
  357. const isSingle = this.$store.getters.storeCurrentConversation?.conversationType === SessionType.Single;
  358. const url = isSingle ? '/pages_im/pages/conversation/singleSettings/index' : '/pages_im/pages/conversation/groupSettings/index';
  359. uni.navigateTo({ url });
  360. }
  361. },
  362. onBackPress() {
  363. if (this.mutipleCheckVisible) {
  364. this.mutipleCheckUpdateHandler({ flag: false });
  365. return true;
  366. } else {
  367. uni.switchTab({
  368. url: '/pages_im/pages/conversation/conversationList/index'
  369. });
  370. return true;
  371. }
  372. },
  373. beforeDestroy() {
  374. uni.switchTab({
  375. url: '/pages_im/pages/conversation/conversationList/index'
  376. });
  377. }
  378. };
  379. </script>
  380. <style lang="scss" scoped>
  381. .chating_container {
  382. @include colBox(false);
  383. height: 100vh;
  384. width: 100%;
  385. overflow-y: hidden;
  386. touch-action: none; // 禁止触摸时的默认滚动/回弹行为
  387. box-sizing: border-box;
  388. background-repeat: no-repeat;
  389. background-size: 100%;
  390. position: relative;
  391. z-index: 2;
  392. background: #f5f7fa;
  393. .watermark {
  394. font-size: 16px; /* 水印文字大小 */
  395. color: rgba(0, 0, 0, 0.2); /* 水印文字颜色,使用透明度控制可见度 */
  396. position: absolute; /* 水印相对定位 */
  397. transform: rotate(-45deg);
  398. pointer-events: none; /* 防止水印文字干扰交互 */
  399. /* 为不同的水印元素设置不同的偏移,以避免重叠 */
  400. // transform-origin: top right;
  401. // margin-top: 20px;
  402. // margin-right: 20px;
  403. }
  404. // ::before {
  405. // content: "Your Watermark Text"; /* 替换为你想要的水印文字 */
  406. // font-size: 16px; /* 水印文字大小 */
  407. // color: rgba(0, 0, 0, 0.2); /* 水印文字颜色,使用透明度控制可见度 */
  408. // position: absolute; /* 水印相对定位 */
  409. // top: 20px; /* 距离顶部的距离 */
  410. // right: 20px; /* 距离右侧的距离 */
  411. // transform: rotate(-45deg); /* 将水印旋转为倾斜状态 */
  412. // pointer-events: none; /* 防止水印文字干扰交互 */
  413. // z-index: -1; /* 将水印置于底层 */
  414. // }
  415. .mutiple_action_container {
  416. display: flex;
  417. border-top: 1px solid #eaebed;
  418. background: #f0f2f6;
  419. justify-content: space-evenly;
  420. padding: 12px 24px;
  421. .action_item {
  422. @include centerBox();
  423. flex-direction: column;
  424. image {
  425. width: 48px;
  426. height: 48px;
  427. }
  428. }
  429. }
  430. }
  431. </style>