| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- <template>
- <scroll-view
- :bounces="true"
- :scroll-with-animation="false"
- @click="click"
- @touchstart="onTouchStart"
- @touchend="onTouchEnd"
- id="scroll_view"
- :style="{
- height: '100%',
- width: '100%',
- backgroundImage: `url(${bgUrl})`,
- backgroundSize: `100% 100%`,
- opacity: listOpacity
- }"
- :show-scrollbar="true"
- @scroll="throttleScroll"
- :scroll-y="true"
- :scroll-top="scrollTop"
- :scroll-into-view="scrollIntoView"
- lower-threshold="200"
- @scrolltolower="scrolltoupper">
- <view id="scroll_wrap">
- <!-- 增加一个高度为30px的view,作为视觉顶部的缓冲,防止滚动到顶部时卡死边缘 -->
- <!-- <view style="height: 0px; width: 100%; flex-shrink: 0; overflow-anchor: none;"></view> -->
- <view v-for="(item, index) in storeHistoryMessageList" :key="item.clientMsgID" class="msgItem" :style="{ zIndex: activeMenuMsgID === item.clientMsgID ? 100 : 0, position: 'relative' }">
- <u-loadmore v-if="index==0" v-show="showLoading" :font-size="16" nomoreText="" :status="loadMoreStatus" />
- <view v-if="getTimeLine(item,index)" :class="bgUrl==''?'time_gap_line':'time_gap_line color'">
- {{ getTimeLine(item, index)}}
- </view>
- <message-item-render
- :source="item"
- :senderFaceUrl="item.senderFaceUrl"
- :senderNickname="item.senderNickname"
- :isSender="item.sendID === storeCurrentUserID"
- :mutipleCheckVisible="mutipleCheckVisible"
- :menuOutsideFlag="menuOutsideFlag"
- @messageItemRender="messageItemRender"
- @messageItemReady="messageItemReady"
- @closeMenu="closeMenu"
- @menuShow="activeMenuMsgID = $event"
- @menuHide="activeMenuMsgID = ''"/>
- <view v-if="sendFailedDesc(item)" class="time_gap_line send_failed_tip">
- {{ sendFailedDesc(item) }}
- </view>
- </view>
- <!-- <view style="visibility: hidden; height: 12px" id="auchormessage_bottom_item"></view> -->
- <view id="last-item" key="lastMsgItem" style="height:10px;" class="msgItem" ></view>
- </view>
-
- <transition name="fade">
- <view click="scrollToBottom(false)" v-show="getNewMesageCount && !needScoll" class="new_message_flag fade">
- <image style="height: 10px; width: 11px" src="../../../../static/images/common_db_arrow.png"/>
- <text>{{ `${getNewMesageCount}条新消息` }}</text>
- </view>
- </transition>
-
- </scroll-view>
- </template>
- <script>
-
- import { mapGetters, mapActions } from 'vuex';
- import dayjs from 'dayjs';
- import { SendMessageFailedType } from '../../../../constant';
- import MessageItemRender from './MessageItem/index.vue';
- import { MessageStatus } from 'openim-uniapp-polyfill';
- export default {
- name: '',
- components: {
- MessageItemRender
- },
- props: {
- menuOutsideFlag: Number,
- mutipleCheckVisible: Boolean,
- keyboardHeight: {
- type: Number,
- default: 0
- }
- },
- watch: {
- keyboardHeight(val) {
- if (val > 0) {
- this.scrollToBottom();
- }
- },
- storeCurrentConversation: {
- handler(val) {
- if (val && val.conversationID && this.initFlag) {
- //console.log("qxj watch storeCurrentConversation trigger load");
- //this.loadMessageList();
- }
- },
- deep: true
- }
- },
- data() {
- return {
- scrollViewHeight: 0, // 滚动容器高度
- scrollIntoView: '',
- scrollWithAnimation: false,
- scrollTop: 0,
- old: {
- scrollTop: 0
- },
- initFlag: true,
- isOverflow: false,
- needScoll: true,
- withAnimation: false,
- messageLoadState: {
- loading: false
- },
- bgUrl: '',
- bgHeight: '',
- showLoading:false,
- isListReady: false,
- scrollTimeout: null,
- isTouching: false,
- lastScrollTop: 0,
- newMsgCount: 0,
- currentLastMsgId: '',
- activeMenuMsgID: ''
- };
- },
- computed: {
- ...mapGetters(['storeCurrentConversation', 'storeHistoryMessageList', 'storeHasMoreMessage', 'storeCurrentUserID', 'storeSelfInfo']),
- loadMoreStatus() {
- if (!this.storeHasMoreMessage) {
- return 'nomore';
- }
- return this.messageLoadState.loading ? 'loading' : 'loadmore';
- },
- getNewMesageCount() {
- if (this.storeHistoryMessageList.length > 0) {
- const lastMessage = this.storeHistoryMessageList[this.storeHistoryMessageList.length - 1];
- if (lastMessage && lastMessage.clientMsgID !== this.currentLastMsgId) {
- this.currentLastMsgId = lastMessage.clientMsgID;
- // 收到新消息,如果不在底部,增加未读计数
- if (this.needScoll) {
- this.newMsgCount++;
- } else {
- // 在底部,自动滚动到底部
- this.scrollToBottom();
- }
- }
- }
- return this.newMsgCount;
- },
- reversedMessageList() {
- return [...this.storeHistoryMessageList].reverse();
- },
- listOpacity() {
- return this.isListReady ? 1 : 0;
- },
- sendFailedDesc() {
- return (message) => {
- if (message.status === MessageStatus.Failed && message.errCode === SendMessageFailedType.Blacked) {
- return '消息已发出,但被对方拒收了';
- }
- if (message.status === MessageStatus.Failed && message.errCode === SendMessageFailedType.NotFriend) {
- return '对方开启了好友验证,你还不是他(她)好友。请先发送好友验证,对方验证通过后,才能聊天。';
- }
- return '';
- };
- },
- getTimeLine() {
- return (message, index) => {
- const sendTime = message.sendTime;
- const preMessage=this.storeHistoryMessageList[index-1];
- const preSendTime = preMessage?.sendTime;
- if(index==0){
- return this.formatTime(sendTime);
- }
- if(index<=this.storeHistoryMessageList.length-1){
- // 只在时间间隔大于30秒时才显示时间线
- if (preSendTime && sendTime - preSendTime > 30000) {
- return this.formatTime(sendTime);
- }
- }
- return null;
- };
- }
- },
- beforeMount() {
- this.updateBgUrl();
- },
- mounted() {
- this.loadMessageList();
- this.$nextTick(() => {
- this.checkInitHeight();
- });
- },
- methods: {
- formatTime(sendTime) {
- const now = dayjs();
- const messageDate = dayjs(sendTime);
- // 检查是否在今天
- if (messageDate.isSame(now, 'day')) {
- // 检查是否在几秒内
- if (now.diff(messageDate, 'second') < 60) {
- return '刚刚';
- }
- // 今天且大于一分钟,显示小时:分钟
- return messageDate.format('HH:mm');
- }
- // 检查是否是昨天
- if (messageDate.isSame(now.subtract(1, 'day'), 'day')) {
- return '昨天 ' + messageDate.format('HH:mm');
- }
- // 检查是否在今年
- if (messageDate.isSame(now, 'year')) {
- // 今年但不是今天或昨天,显示月日 小时:分钟
- return messageDate.format('MM-DD HH:mm');
- }
- // 今年以前,显示年月日 小时:分钟
- return messageDate.format('YYYY-MM-DD HH:mm');
- },
- ...mapActions('message', ['getHistoryMesageList']),
- messageItemRender(clientMsgID) {
- if (this.initFlag && clientMsgID === this.storeHistoryMessageList[this.storeHistoryMessageList.length - 1].clientMsgID){
- //this.initFlag = false;
- //setTimeout(() => this.scrollToBottom(true), 0);
- // setTimeout(() => this.scrollToAnchor(`auchor${clientMsgID}`, false, true), 200)
- // this.checkInitHeight();
- // this.scrollToAnchor('message_bottom_item',true)
- }
- },
- async loadMessageList(isLoadMore = false) {
- this.showLoading=isLoadMore;
- this.messageLoadState.loading = true;
- // 记录加载前的第一条消息ID,作为锚点
- let anchorMsgID = null;
- if (isLoadMore) {
- this.withAnimation = false; // 禁用动画
- anchorMsgID = this.storeHistoryMessageList[0]?.clientMsgID;
- }
- try {
- if (!this.storeCurrentConversation || !this.storeCurrentConversation.conversationID) {
- this.messageLoadState.loading = false;
- return;
- }
- const options = {
- conversationID: this.storeCurrentConversation.conversationID,
- count: 20,
- startClientMsgID: this.storeHistoryMessageList[0]?.clientMsgID ?? '',
- viewType: 0
- };
- const { emptyFlag } = await this.getHistoryMesageList(options);
- if(emptyFlag){
- this.$emit('initSuccess');
- }
-
- } catch (e) {
- console.log(e);
-
- }
-
- this.$nextTick(async () => {
- if (isLoadMore && anchorMsgID) {
- // 锚点定位法:找到之前的第一条消息现在的位置
- // 注意:scroll-view 的 scroll-into-view 属性在 prepend 数据时可能更稳定
- // this.scrollIntoView = '';
- // this.$nextTick(() => {
- // this.scrollIntoView = `auchor${anchorMsgID}`;
- // });
- } else if (!isLoadMore) {
- this.scrollToBottom(true);
- }
- this.messageLoadState.loading = false;
- });
-
- },
- click(e) {
- this.$emit('click', e);
- },
-
- getReversedTimeLine(item, nextItem) {
- if (!nextItem) return this.getTimeLine(item, null);
- return this.getTimeLine(item, nextItem);
- },
- onScroll(event) {
- //console.log("qxj onScroll event",event);
- // const { scrollHeight, scrollTop } = event.target;
- // this.old.scrollTop = scrollTop;
- // this.needScoll = scrollHeight - scrollTop < uni.getWindowInfo().windowHeight * 1.2;
-
- },
- throttleScroll(event) {
- //console.log("qxj throttleScroll event",event);
- this.$emit('closeMenu', event);
- if(this.isListReady){
- const { scrollTop, scrollHeight } = event.detail;
- this.lastScrollTop = scrollTop;
- this.lastScrollHeight = scrollHeight;
- if (this.isTouching) return;
- this.checkScroll();
- }
- },
- checkScroll() {
- if (this.scrollTimeout) {
- clearTimeout(this.scrollTimeout);
- }
- this.scrollTimeout = setTimeout(() => {
- // 修复顶部锁定 (视觉底部/逻辑顶部)
- if(this.lastScrollTop < 0.5) {
- this.scrollTop = this.lastScrollTop;
- this.$nextTick(() => {
- this.scrollTop = 0.5;
- });
- }
- // 修复视觉顶部(逻辑底部)卡死问题,同时兼容OPPO越界滚动
- if (this.scrollViewHeight > 0 && this.lastScrollHeight > 0) {
- const maxScroll = this.lastScrollHeight - this.scrollViewHeight;
- // 计算当前滚动位置超出最大值的距离
- const diff = this.lastScrollTop - maxScroll;
- //console.log("qxj diff",diff);
- // 只有当超出距离在较小范围内(例如 20px 以内)时,才认为是“到达边界并可能卡死”,进行微调。
- if (maxScroll > 0 && diff >= 0 && diff < 5) {
- this.scrollTop = this.lastScrollTop;
- this.$nextTick(() => {
- this.scrollTop = maxScroll - 2;
- });
- }
- }
-
- }, 100);
- },
- onTouchStart() {
- this.isTouching = true;
- if (this.scrollTimeout) {
- clearTimeout(this.scrollTimeout);
- }
- },
- onTouchEnd() {
- this.isTouching = false;
- this.checkScroll();
- },
- scrolltoupper() {
- if (!this.messageLoadState.loading && this.storeHasMoreMessage) {
- this.loadMessageList(true);
- }
- },
- scrollToBottom(isInit = false, isRecv = false) {
- // if (isRecv && !this.needScoll) {
- // return;
- // }
- // // 首次加载不使用动画,避免视觉上的滑动
- // if (!isInit) {
- // this.withAnimation = true;
- // // 动画结束后重置,防止影响下一次手动调整
- // setTimeout(() => (this.withAnimation = false), 250);
- // } else {
- // this.withAnimation = false;
- // }
- this.scrollIntoView = '';
- this.scrollTop = 1;
- this.$nextTick(() => {
- // 延迟再次设置,确保渲染完成后生效
- setTimeout(() => {
- this.scrollTop = 10;
- if (isInit) {
- this.isListReady = true;
- this.$emit('initSuccess');
- }
- }, 200);
- });
- },
- scrollToAnchor(auchor) {
- this.$nextTick(function () {
- this.scrollIntoView = auchor;
- });
- },
- messageItemReady(clientMsgID) {
- // const last = this.storeHistoryMessageList[this.storeHistoryMessageList.length - 1];
- // if (!last) return;
- // if (clientMsgID === last.clientMsgID) {
- // this.scrollToBottom(true);
- // }
- },
- checkInitHeight() {
- this.getEl('#scroll_view').then(({ height }) => {
- console.log('height',height)
- this.bgHeight = `${height}px`;
- this.scrollViewHeight = height;
- });
- },
- getEl(el) {
- return new Promise((resolve) => {
- const query = uni.createSelectorQuery().in(this);
- query.select(el)
- .boundingClientRect((data) => {
- resolve(data);
- }).exec();
- });
- },
- closeMenu() {
- this.$emit('closeMenu');
- },
- updateBgUrl() {
- const bgMap = uni.getStorageSync('IMBgMap') || {};
- this.bgUrl = bgMap[this.$store.getters.storeCurrentConversation.conversationID] || '';
- console.log('this.bgUrl',bgMap,this.bgUrl)
- }
- }
- };
- </script>
- <style lang="scss">
- #scroll_view {
- display: flex;
- background-repeat: no-repeat;
- position: relative;
- z-index: 10 !important;
- // overflow-anchor: auto !important;
- // -webkit-overflow-scrolling: touch;
- //上下翻转
- transform: scale(1, -1);
-
- /* 强制显示滚动条 */
- /deep/ ::-webkit-scrollbar {
- display: block !important;
- width: 4px !important;
- height: 4px !important;
- background: transparent !important;
- }
- /deep/ ::-webkit-scrollbar-thumb {
- background-color: rgba(51, 51, 51, 0.6) !important;
- border-radius: 3px !important;
- }
- /deep/ ::-webkit-scrollbar-track {
- background: transparent !important;
- }
- }
- #scroll_wrap{
- display: flex;
- min-height: 100%;
- flex: 1;
- flex-direction: column-reverse;
- // justify-content: flex-end;
- }
- .msgItem{
- transform: scale(1, -1);
- display: flex;
- flex-direction: column;
- flex-shrink: 0;
- width: 100%;
- }
- .watermark-view {
- width: 100%;
- height: 100%;
- position: fixed;
- }
- .watermark {
- font-size: 16px; /* 水印文字大小 */
- color: #f0f2f6; /* 水印文字颜色,使用透明度控制可见度 */
- position: absolute; /* 水印相对定位 */
- transform: rotate(-45deg);
- pointer-events: none; /* 防止水印文字干扰交互 */
- }
- .uni-scroll-view {
- position: relative;
- }
- .new_message_flag {
- position: sticky;
- background: #ffffff;
- box-shadow: 0px 3px 8px 0px rgba(0, 0, 0, 0.1);
- border-radius: 14px;
- padding: 4px 8px;
- display: flex;
- justify-content: center;
- align-items: center;
- bottom: 12px;
- left: 50%;
- transform: translateX(-50%);
- width: fit-content;
- font-size: 24rpx;
- color: #006aff;
- }
- .time_gap_line {
- position: relative;
- padding: 10rpx 10vw 0rpx;
- text-align: center;
- font-size: 13px;
- color: #A5A5A5;
- &.color{
- color:#fff
- }
- }
- .fade-leave,
- .fade-enter-to {
- opacity: 1;
- }
- .fade-leave-active,
- .fade-enter-active {
- transition: all 0.3s;
- }
- .fade-leave-to,
- .fade-enter {
- opacity: 0;
- }
- </style>
|