ChatingListMe.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. <template>
  2. <!-- 使用mescroll组件替代原生scroll-view -->
  3. <mescroll-uni
  4. ref="mescrollRef"
  5. @init="mescrollInit"
  6. @down="downCallback"
  7. @up="upCallback"
  8. :height="scrollHeight"
  9. :down="downOption"
  10. :up="upOption"
  11. :fixed="false"
  12. id="scroll_view"
  13. :style="{
  14. backgroundImage: `url(${bgUrl})`,
  15. backgroundSize: `100% 100%`
  16. }"
  17. >
  18. <view id="scroll_wrap">
  19. <!-- 消息列表 -->
  20. <view
  21. v-for="(item, index) in storeHistoryMessageList"
  22. :key="item.clientMsgID"
  23. :id="`msg_${item.clientMsgID}`"
  24. :data-index="index"
  25. class="message-wrapper">
  26. <view v-if="shouldShowTimeLine(item, index)" class="time_gap_line">
  27. {{ formatMessageTime(item, storeHistoryMessageList[index - 1]) }}
  28. </view>
  29. <message-item-render
  30. :mutipleCheckVisible="mutipleCheckVisible"
  31. :menuOutsideFlag="menuOutsideFlag"
  32. @messageItemRender="messageItemRender"
  33. :source="item"
  34. :isSender="item.sendID === storeCurrentUserID"
  35. @closeMenu="closeMenu"/>
  36. <view v-if="getFailedMessage(item)" class="time_gap_line send_failed_tip">
  37. {{ getFailedMessage(item) }}
  38. </view>
  39. </view>
  40. <!-- 底部占位 -->
  41. <view style="height: 20px;" id="bottom_anchor"></view>
  42. </view>
  43. </mescroll-uni>
  44. </template>
  45. <script>
  46. import { mapGetters, mapActions } from 'vuex';
  47. import dayjs from 'dayjs';
  48. import { SendMessageFailedType } from '../../../../constant';
  49. import MessageItemRender from './MessageItem/index.vue';
  50. import { MessageStatus } from 'openim-uniapp-polyfill';
  51. import MescrollMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js";
  52. import MescrollMoreItemMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mixins/mescroll-more-item.js";
  53. export default {
  54. mixins: [ MescrollMixin,MescrollMoreItemMixin ],
  55. name: 'ChatingList',
  56. components: {
  57. MessageItemRender,
  58. },
  59. props: {
  60. menuOutsideFlag: Number,
  61. mutipleCheckVisible: Boolean
  62. },
  63. data() {
  64. return {
  65. scrollHeight: "100vh",
  66. bgUrl: '',
  67. mescroll: null,
  68. downOption: {
  69. auto: false, // 不自动下拉刷新
  70. textInOffset: '下拉刷新',
  71. textOutOffset: '释放更新',
  72. textLoading: '加载中...',
  73. offset: 80
  74. },
  75. upOption: {
  76. auto: false, // 不自动上拉加载
  77. page: {
  78. num: 0,
  79. size: 20
  80. },
  81. noMoreSize: 5,
  82. textNoMore: '-- 没有更多消息了 --',
  83. empty: {
  84. tip: '暂无消息'
  85. }
  86. },
  87. // 滚动位置管理
  88. scrollPosition: {
  89. lastScrollTop: 0,
  90. lastScrollTime: 0
  91. },
  92. // 初始化状态
  93. isInitialized: false
  94. };
  95. },
  96. computed: {
  97. ...mapGetters([
  98. 'storeCurrentConversation',
  99. 'storeHistoryMessageList',
  100. 'storeHasMoreMessage',
  101. 'storeCurrentUserID'
  102. ]),
  103. },
  104. mounted() {
  105. this.initializeList();
  106. this.updateBgUrl();
  107. // 计算滚动区域高度
  108. this.calculateScrollHeight();
  109. // 监听窗口变化
  110. uni.onWindowResize(this.calculateScrollHeight);
  111. },
  112. beforeDestroy() {
  113. if (this.mescroll) {
  114. //this.mescroll.destroy();
  115. }
  116. },
  117. methods: {
  118. ...mapActions('message', ['getHistoryMesageList']),
  119. // 计算滚动区域高度
  120. calculateScrollHeight() {
  121. const systemInfo = uni.getSystemInfoSync();
  122. const windowHeight = systemInfo.windowHeight;
  123. uni.createSelectorQuery().in(this)
  124. .select('#scroll_view')
  125. .boundingClientRect(res => {
  126. if (res) {
  127. const top = res.top;
  128. this.scrollHeight = `${windowHeight - top}px`;
  129. }
  130. })
  131. .exec();
  132. },
  133. // Mescroll初始化
  134. mescrollInit(mescroll) {
  135. this.mescroll = mescroll;
  136. },
  137. // 下拉刷新回调
  138. downCallback() {
  139. // 聊天界面通常不需要下拉刷新
  140. this.mescroll.endSuccess();
  141. },
  142. // 上拉加载回调
  143. async upCallback() {
  144. if (!this.storeHasMoreMessage) {
  145. this.mescroll.endSuccess(0, false);
  146. return;
  147. }
  148. try {
  149. const options = {
  150. conversationID: this.storeCurrentConversation.conversationID,
  151. count: this.upOption.page.size,
  152. startClientMsgID: this.storeHistoryMessageList[0]?.clientMsgID ?? '',
  153. viewType: 0
  154. };
  155. // 记录当前第一条消息ID和位置
  156. const firstMsgId = this.storeHistoryMessageList[0]?.clientMsgID;
  157. let firstMsgTop = 0;
  158. if (firstMsgId) {
  159. const rect = await this.getElementPosition(`#msg_${firstMsgId}`);
  160. if (rect) {
  161. firstMsgTop = rect.top;
  162. }
  163. }
  164. // 加载历史消息
  165. await this.getHistoryMesageList(options);
  166. // 等待DOM更新
  167. this.$nextTick(() => {
  168. if (firstMsgId) {
  169. // 计算新位置
  170. this.getElementPosition(`#msg_${firstMsgId}`).then(newRect => {
  171. if (newRect) {
  172. // 计算滚动偏移量
  173. const offset = newRect.top - firstMsgTop;
  174. // 平滑滚动到原位置
  175. if (offset !== 0) {
  176. this.mescroll.scrollTo(this.mescroll.getScrollTop() + offset, 300);
  177. }
  178. }
  179. // 结束加载
  180. this.mescroll.endSuccess(this.storeHasMoreMessage ? this.upOption.page.size : 0, this.storeHasMoreMessage);
  181. });
  182. } else {
  183. this.mescroll.endSuccess(this.storeHasMoreMessage ? this.upOption.page.size : 0, this.storeHasMoreMessage);
  184. }
  185. });
  186. } catch (error) {
  187. console.error('加载历史消息失败:', error);
  188. this.mescroll.endErr();
  189. }
  190. },
  191. // 获取元素位置
  192. getElementPosition(selector) {
  193. return new Promise(resolve => {
  194. uni.createSelectorQuery().in(this)
  195. .select(selector)
  196. .boundingClientRect(res => resolve(res))
  197. .exec();
  198. });
  199. },
  200. // 初始化列表
  201. async initializeList() {
  202. try {
  203. const options = {
  204. conversationID: this.storeCurrentConversation.conversationID,
  205. count: this.upOption.page.size,
  206. startClientMsgID: '',
  207. viewType: 0
  208. };
  209. await this.getHistoryMesageList(options);
  210. this.$nextTick(() => {
  211. this.scrollToBottom(true);
  212. this.isInitialized = true;
  213. this.$emit('initSuccess');
  214. // 初始化mescroll
  215. if (this.mescroll) {
  216. this.mescroll.endSuccess(this.upOption.page.size, this.storeHasMoreMessage);
  217. }
  218. });
  219. } catch (error) {
  220. console.error('初始化消息列表失败:', error);
  221. }
  222. },
  223. // 消息项渲染完成
  224. messageItemRender(clientMsgID) {
  225. if (!this.isInitialized && clientMsgID === this.storeHistoryMessageList[this.storeHistoryMessageList.length - 1]?.clientMsgID) {
  226. this.$nextTick(() => {
  227. this.scrollToBottom(true);
  228. this.isInitialized = true;
  229. this.$emit('initSuccess');
  230. });
  231. }
  232. },
  233. // 滚动到底部
  234. scrollToBottom(instantly = false) {
  235. if (this.mescroll) {
  236. if (instantly) {
  237. this.mescroll.scrollTo(999999, 0);
  238. } else {
  239. this.mescroll.scrollToBottom(300);
  240. }
  241. }
  242. },
  243. // 时间线相关方法
  244. shouldShowTimeLine(message, index) {
  245. if (index === 0) return false;
  246. const preMessage = this.storeHistoryMessageList[index - 1];
  247. return this.formatMessageTime(message, preMessage) !== null;
  248. },
  249. formatMessageTime(message, preMessage) {
  250. if (!preMessage) return null;
  251. const sendTime = message.sendTime;
  252. const preSendTime = preMessage.sendTime;
  253. // 10分钟内不显示时间
  254. if (sendTime - preSendTime <= 600000) return null;
  255. const messageDate = dayjs(sendTime);
  256. const now = dayjs();
  257. if (messageDate.isSame(now, 'day')) {
  258. return messageDate.format('HH:mm');
  259. } else if (messageDate.isSame(now.subtract(1, 'day'), 'day')) {
  260. return '昨天 ' + messageDate.format('HH:mm');
  261. } else if (messageDate.isSame(now, 'year')) {
  262. return messageDate.format('MM-DD HH:mm');
  263. } else {
  264. return messageDate.format('YYYY-MM-DD HH:mm');
  265. }
  266. },
  267. // 失败消息处理
  268. getFailedMessage(message) {
  269. if (message.status === MessageStatus.Failed) {
  270. if (message.errCode === SendMessageFailedType.Blacked) {
  271. return '消息已发出,但被对方拒收了';
  272. }
  273. if (message.errCode === SendMessageFailedType.NotFriend) {
  274. return '对方开启了好友验证,你还不是他(她)好友。请先发送好友验证,对方验证通过后,才能聊天。';
  275. }
  276. }
  277. return '';
  278. },
  279. // 点击事件
  280. click(e) {
  281. this.$emit('click', e);
  282. },
  283. // 关闭菜单
  284. closeMenu() {
  285. this.$emit('closeMenu');
  286. },
  287. // 更新背景
  288. updateBgUrl() {
  289. const bgMap = uni.getStorageSync('IMBgMap') || {};
  290. this.bgUrl = bgMap[this.$store.getters.storeCurrentConversation.conversationID] || '';
  291. },
  292. // 外部接口:滚动到底部
  293. scrollToBottomExternal(isRecv = false) {
  294. if (this.mescroll) {
  295. // 如果用户已经向上滚动较多,接收新消息时不自动滚动到底部
  296. const scrollTop = this.mescroll.getScrollTop();
  297. const scrollHeight = this.mescroll.getScrollHeight();
  298. if (isRecv && scrollTop < (scrollHeight * 0.8)) {
  299. return;
  300. }
  301. this.scrollToBottom();
  302. }
  303. }
  304. }
  305. };
  306. </script>
  307. <style lang="scss" scoped>
  308. #scroll_view {
  309. flex: 1;
  310. background-repeat: no-repeat;
  311. position: relative;
  312. overflow: hidden;
  313. }
  314. #scroll_wrap {
  315. min-height: 100%;
  316. position: relative;
  317. padding-bottom: 20rpx;
  318. }
  319. .message-wrapper {
  320. position: relative;
  321. }
  322. .time_gap_line {
  323. padding: 16rpx 0;
  324. text-align: center;
  325. font-size: 16px;
  326. color: #999;
  327. background: transparent;
  328. }
  329. .send_failed_tip {
  330. color: #ff6b6b;
  331. font-size: 16px;
  332. padding: 8rpx 0;
  333. text-align: center;
  334. }
  335. /* 自定义mescroll样式 */
  336. ::v-deep .mescroll-downwarp .downwarp-content {
  337. padding: 16rpx 0;
  338. font-size: 26rpx;
  339. color: #888;
  340. }
  341. ::v-deep .mescroll-upwarp {
  342. padding: 20rpx 0;
  343. .upwarp-progress {
  344. width: 40rpx;
  345. height: 40rpx;
  346. border-color: #07C160;
  347. }
  348. .upwarp-nodata {
  349. font-size: 28rpx;
  350. color: #999;
  351. }
  352. }
  353. /* 隐藏原生滚动条 */
  354. ::-webkit-scrollbar {
  355. display: none;
  356. }
  357. </style>