diff --git a/resources/assets/js/pages/manage/components/DialogWrapper.vue b/resources/assets/js/pages/manage/components/DialogWrapper.vue index bee63376b..6cfb660b1 100644 --- a/resources/assets/js/pages/manage/components/DialogWrapper.vue +++ b/resources/assets/js/pages/manage/components/DialogWrapper.vue @@ -7,6 +7,9 @@ @dragover.prevent="chatDragOver(true, $event)" @dragleave.prevent="chatDragOver(false, $event)" @touchstart="onTouchStart" + @touchstart.capture="onTouchState" + @touchend.capture="onTouchState" + @touchcancel.capture="onTouchState" @pointerover="onPointerover">
@@ -210,6 +213,7 @@ @totop="onPrevPage" @range="onRange" @visible="onVisible" + @scroll.native.passive="onNativeScroll" @on-view-reply="onViewReply" @on-view-text="onViewText" @@ -2531,6 +2535,35 @@ export default { target.src = imageFailedSrc }, + // 捕获阶段记录是否有手指在屏幕上(子元素如 v-longpress 会阻止 touchend 冒泡) + onTouchState({touches}) { + this.__touching = !!touches && touches.length > 0 + }, + + onNativeScroll() { + this.__scroll_at = Date.now() + }, + + // iOS 手指按住或惯性滚动(含顶部回弹)时写入 scrollTop 不生效,需等手势结束、滚动停止后再插入历史消息并补偿位置 + waitScrollIdle() { + if (!$A.isIos()) { + return Promise.resolve() + } + return new Promise(resolve => { + const start = Date.now() + const check = () => { + const el = this.$refs.scroller?.$el + const idle = !this.__touching && Date.now() - (this.__scroll_at || 0) >= 150 && (!el || el.scrollTop >= 0) + if (idle || this._isDestroyed || Date.now() - start > 10000) { + resolve() + } else { + setTimeout(check, 50) + } + } + check() + }) + }, + onPointerover({pointerType}) { this.pointerMouse = pointerType === 'mouse'; }, @@ -2919,14 +2952,22 @@ export default { if (this.prevId === 0 || this.preventPrevLoad > 0) { return } + // 等待手势空闲期间请求已结束,需自行阻止重复加载 + const dialogId = this.dialogId + this.preventPrevLoad++ this.getMsgs({ dialog_id: this.dialogId, msg_id: this.msgId, msg_type: this.msgType, prev_id: this.prevId, - save_before: _ => this.scrollDisabled = true, + save_before: _ => this.waitScrollIdle().then(_ => { + this.scrollDisabled = true + }), save_after: _ => this.scrollDisabled = false }).then(({data}) => { + if (dialogId !== this.dialogId) { + return + } const ids = data.list.map(item => item.id) this.$nextTick(() => { const scroller = this.$refs.scroller @@ -2936,7 +2977,9 @@ export default { }) this.onToOffset(scroller.getOffset() + reducer.size, true) }); - }).catch(() => {}) + }).catch(() => {}).finally(_ => { + this.preventPrevLoad-- + }) }, onDialogMenu(cmd) { diff --git a/resources/assets/js/store/actions.js b/resources/assets/js/store/actions.js index ced5f022d..f77097b9b 100644 --- a/resources/assets/js/store/actions.js +++ b/resources/assets/js/store/actions.js @@ -4267,6 +4267,7 @@ export default { * @param dispatch * @param getters * @param data {dialog_id, msg_id, ?msg_type, ?position_id, ?prev_id, ?next_id, ?save_before, ?save_after, ?clear_before, ?spinner} + * save_before 可返回 Promise,等其完成后再写入消息 * @returns {Promise} */ getDialogMsgs({commit, state, dispatch, getters}, data) { @@ -4310,8 +4311,8 @@ export default { data, spinner, complete: _ => dispatch("cancelLoad", loadKey) - }).then(result => { - saveBefore() + }).then(async result => { + await saveBefore() const resData = result.data; if ($A.isJson(resData.dialog)) { const ids = resData.list.map(({id}) => id)