index.vue 15 KB

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