ChatingList1.vue 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. <template>
  2. <scroll-view
  3. :scroll-with-animation="withAnimation"
  4. @click="click"
  5. id="scroll_view"
  6. :style="{
  7. height: '1px',
  8. backgroundImage: `url(${bgUrl})`,
  9. backgroundSize: `100% 100%`
  10. }"
  11. @scroll="throttleScroll"
  12. :scroll-top="scrollTop"
  13. scroll-y
  14. :scroll-into-view="scrollIntoView"
  15. upper-threshold="250"
  16. @scrolltoupper="scrolltoupper">
  17. <view id="scroll_wrap">
  18. <u-loadmore nomoreText="" v-show="showLoading" :status="loadMoreStatus" />
  19. <view v-for="(item, index) in storeHistoryMessageList" :key="item.clientMsgID">
  20. <view v-if="getTimeLine" :class="bgUrl==''?'time_gap_line':'time_gap_line color'">
  21. {{ getTimeLine(item, storeHistoryMessageList[index - 1])}}
  22. </view>
  23. <message-item-render
  24. :mutipleCheckVisible="mutipleCheckVisible"
  25. :menuOutsideFlag="menuOutsideFlag"
  26. @messageItemRender="messageItemRender"
  27. :source="item"
  28. :isSender="item.sendID === storeCurrentUserID"
  29. @closeMenu="closeMenu"/>
  30. <view v-if="sendFailedDesc" class="time_gap_line send_failed_tip">
  31. {{ sendFailedDesc(item) }}
  32. </view>
  33. </view>
  34. <view style="visibility: hidden; height: 12px" id="auchormessage_bottom_item"></view>
  35. </view>
  36. <!-- <transition name="fade">
  37. <view
  38. @click="scrollToBottom(false)"
  39. v-show="getNewMesageCount && !needScoll"
  40. class="new_message_flag fade"
  41. >
  42. <image
  43. style="height: 10px; width: 11px"
  44. src="@/static/images/common_db_arrow.png"
  45. />
  46. <text>{{ `${getNewMesageCount}条新消息` }}</text>
  47. </view>
  48. </transition> -->
  49. </scroll-view>
  50. </template>
  51. <script>
  52. import { mapGetters, mapActions } from 'vuex';
  53. import dayjs from 'dayjs';
  54. import { SendMessageFailedType } from '../../../../constant';
  55. import MessageItemRender from './MessageItem/index.vue';
  56. import { MessageStatus } from 'openim-uniapp-polyfill';
  57. export default {
  58. name: '',
  59. components: {
  60. MessageItemRender
  61. },
  62. props: {
  63. menuOutsideFlag: Number,
  64. mutipleCheckVisible: Boolean
  65. },
  66. data() {
  67. return {
  68. scrollViewHeight: 0, // 滚动容器高度
  69. scrollIntoView: '',
  70. scrollWithAnimation: false,
  71. scrollTop: 0,
  72. old: {
  73. scrollTop: 0
  74. },
  75. initFlag: true,
  76. isOverflow: false,
  77. needScoll: true,
  78. withAnimation: false,
  79. messageLoadState: {
  80. loading: false
  81. },
  82. bgUrl: '',
  83. bgHeight: '',
  84. showLoading:false
  85. };
  86. },
  87. computed: {
  88. ...mapGetters(['storeCurrentConversation', 'storeHistoryMessageList', 'storeHasMoreMessage', 'storeCurrentUserID', 'storeSelfInfo']),
  89. loadMoreStatus() {
  90. if (!this.storeHasMoreMessage) {
  91. return 'nomore';
  92. }
  93. return this.messageLoadState.loading ? 'loading' : 'loadmore';
  94. },
  95. getNewMesageCount() {
  96. return this.storeHistoryMessageList.filter((message) => message.isAppend).length;
  97. },
  98. sendFailedDesc() {
  99. return (message) => {
  100. if (message.status === MessageStatus.Failed && message.errCode === SendMessageFailedType.Blacked) {
  101. return '消息已发出,但被对方拒收了';
  102. }
  103. if (message.status === MessageStatus.Failed && message.errCode === SendMessageFailedType.NotFriend) {
  104. return '对方开启了好友验证,你还不是他(她)好友。请先发送好友验证,对方验证通过后,才能聊天。';
  105. }
  106. return '';
  107. };
  108. },
  109. getTimeLine() {
  110. return (message, preMessage) => {
  111. const sendTime = message.sendTime;
  112. const preSendTime = preMessage?.sendTime;
  113. // 只在时间间隔大于10分钟时才显示时间线
  114. if (preSendTime && sendTime - preSendTime > 600000) {
  115. const now = dayjs();
  116. const messageDate = dayjs(sendTime);
  117. // 检查是否在今天
  118. if (messageDate.isSame(now, 'day')) {
  119. // 检查是否在几秒内
  120. if (now.diff(messageDate, 'second') < 60) {
  121. return '刚刚';
  122. }
  123. // 今天且大于一分钟,显示小时:分钟
  124. return messageDate.format('HH:mm');
  125. }
  126. // 检查是否是昨天
  127. if (messageDate.isSame(now.subtract(1, 'day'), 'day')) {
  128. return '昨天 ' + messageDate.format('HH:mm');
  129. }
  130. // 检查是否在今年
  131. if (messageDate.isSame(now, 'year')) {
  132. // 今年但不是今天或昨天,显示月日 小时:分钟
  133. return messageDate.format('MM-DD HH:mm');
  134. }
  135. // 今年以前,显示年月日 小时:分钟
  136. return messageDate.format('YYYY-MM-DD HH:mm');
  137. }
  138. return null;
  139. };
  140. }
  141. },
  142. beforeMount() {
  143. this.updateBgUrl();
  144. },
  145. mounted() {
  146. this.loadMessageList();
  147. // 监听窗口大小变化(适配旋转屏幕等场景)
  148. },
  149. methods: {
  150. ...mapActions('message', ['getHistoryMesageList']),
  151. messageItemRender(clientMsgID) {
  152. if (this.initFlag && clientMsgID === this.storeHistoryMessageList[this.storeHistoryMessageList.length - 1].clientMsgID) {
  153. this.initFlag = false;
  154. setTimeout(() => this.scrollToBottom(true), 0);
  155. // setTimeout(() => this.scrollToAnchor(`auchor${clientMsgID}`, false, true), 200)
  156. // this.checkInitHeight();
  157. // this.scrollToAnchor('message_bottom_item',true)
  158. }
  159. },
  160. async loadMessageList(isLoadMore = false) {
  161. this.showLoading=isLoadMore;
  162. this.messageLoadState.loading = true;
  163. const lastMsgID = this.storeHistoryMessageList[0]?.clientMsgID;
  164. const options = {
  165. conversationID: this.storeCurrentConversation.conversationID,
  166. count: 20,
  167. startClientMsgID: this.storeHistoryMessageList[0]?.clientMsgID ?? '',
  168. viewType: 0
  169. };
  170. try {
  171. const { emptyFlag } = await this.getHistoryMesageList(options);
  172. if (emptyFlag) {
  173. this.$emit('initSuccess');
  174. }
  175. } catch (e) {
  176. console.log(e);
  177. //TODO handle the exception
  178. }
  179. this.$nextTick(function () {
  180. if (isLoadMore && lastMsgID) {
  181. this.scrollToAnchor(`auchor${lastMsgID}`);
  182. }
  183. this.messageLoadState.loading = false;
  184. });
  185. },
  186. click(e) {
  187. this.$emit('click', e);
  188. },
  189. onScroll(event) {
  190. const { scrollHeight, scrollTop } = event.target;
  191. this.old.scrollTop = scrollTop;
  192. this.needScoll = scrollHeight - scrollTop < uni.getWindowInfo().windowHeight * 1.2;
  193. this.$emit('closeMenu', event);
  194. },
  195. throttleScroll(event) {
  196. uni.$u.throttle(() => this.onScroll(event), 150);
  197. },
  198. scrolltoupper() {
  199. if (!this.messageLoadState.loading && this.storeHasMoreMessage) {
  200. this.loadMessageList(true);
  201. }
  202. },
  203. scrollToBottom(isInit = false, isRecv = false) {
  204. console.log("qxj scrollToBottom");
  205. if (isRecv && !this.needScoll) {
  206. return;
  207. }
  208. if (!isInit) {
  209. this.withAnimation = true;
  210. setTimeout(() => (this.withAnimation = false), 100);
  211. }
  212. this.$nextTick(() => {
  213. uni.createSelectorQuery()
  214. .in(this)
  215. .select('#scroll_wrap')
  216. .boundingClientRect((res) => {
  217. // let top = res.height - this.scrollViewHeight;
  218. // if (top > 0) {
  219. this.scrollTop = this.old.scrollTop;
  220. this.$nextTick(() => (this.scrollTop = res.height));
  221. if (isInit) {
  222. this.$emit('initSuccess');
  223. }
  224. // }
  225. })
  226. .exec();
  227. });
  228. },
  229. scrollToAnchor(auchor) {
  230. this.$nextTick(function () {
  231. this.scrollIntoView = auchor;
  232. });
  233. },
  234. checkInitHeight() {
  235. this.getEl('#scroll_view').then(({ height }) => {
  236. console.log('height',height)
  237. this.bgHeight = `${height}px`;
  238. });
  239. },
  240. getEl(el) {
  241. return new Promise((resolve) => {
  242. const query = uni.createSelectorQuery().in(this);
  243. query.select(el)
  244. .boundingClientRect((data) => {
  245. resolve(data);
  246. }).exec();
  247. });
  248. },
  249. closeMenu() {
  250. this.$emit('closeMenu');
  251. },
  252. updateBgUrl() {
  253. const bgMap = uni.getStorageSync('IMBgMap') || {};
  254. this.bgUrl = bgMap[this.$store.getters.storeCurrentConversation.conversationID] || '';
  255. }
  256. }
  257. };
  258. </script>
  259. <style lang="scss" scoped>
  260. #scroll_view {
  261. flex:1;
  262. background-repeat: no-repeat;
  263. position: relative;
  264. background-attachment: fixed;
  265. z-index: 10 !important;
  266. }
  267. .watermark-view {
  268. width: 100%;
  269. height: 100%;
  270. position: fixed;
  271. }
  272. .watermark {
  273. font-size: 16px; /* 水印文字大小 */
  274. color: #f0f2f6; /* 水印文字颜色,使用透明度控制可见度 */
  275. position: absolute; /* 水印相对定位 */
  276. transform: rotate(-45deg);
  277. pointer-events: none; /* 防止水印文字干扰交互 */
  278. }
  279. .uni-scroll-view {
  280. position: relative;
  281. }
  282. .new_message_flag {
  283. position: sticky;
  284. background: #ffffff;
  285. box-shadow: 0px 3px 8px 0px rgba(0, 0, 0, 0.1);
  286. border-radius: 14px;
  287. padding: 4px 8px;
  288. display: flex;
  289. justify-content: center;
  290. align-items: center;
  291. bottom: 12px;
  292. left: 50%;
  293. transform: translateX(-50%);
  294. width: fit-content;
  295. font-size: 24rpx;
  296. color: #006aff;
  297. }
  298. .time_gap_line {
  299. position: relative;
  300. padding: 0 10vw 12rpx;
  301. text-align: center;
  302. font-size: 16px;
  303. color: #A5A5A5;
  304. &.color{
  305. color:#fff
  306. }
  307. }
  308. .fade-leave,
  309. .fade-enter-to {
  310. opacity: 1;
  311. }
  312. .fade-leave-active,
  313. .fade-enter-active {
  314. transition: all 0.3s;
  315. }
  316. .fade-leave-to,
  317. .fade-enter {
  318. opacity: 0;
  319. }
  320. </style>