From 515e02f3afef46b8916796f498d8e713f56b07b1 Mon Sep 17 00:00:00 2001 From: zoujing Date: Tue, 14 Jul 2026 10:09:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=20=E9=A6=96=E9=A1=B5=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/test-discovery-panel-scroll.mjs | 114 ++++++++++++++++++++++ src/pages/ChatMain/ChatMainList/index.vue | 52 +++++++++- src/utils/discoveryPanelScroll.js | 42 ++++++++ 3 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 scripts/test-discovery-panel-scroll.mjs create mode 100644 src/utils/discoveryPanelScroll.js diff --git a/scripts/test-discovery-panel-scroll.mjs b/scripts/test-discovery-panel-scroll.mjs new file mode 100644 index 0000000..06939e8 --- /dev/null +++ b/scripts/test-discovery-panel-scroll.mjs @@ -0,0 +1,114 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; + +const helperUrl = new URL("../src/utils/discoveryPanelScroll.js", import.meta.url); +const chatMainListUrl = new URL("../src/pages/ChatMain/ChatMainList/index.vue", import.meta.url); +const helperSource = await readFile(helperUrl, "utf8"); +const chatMainListSource = await readFile(chatMainListUrl, "utf8"); +const helperModule = await import( + `data:text/javascript;charset=utf-8,${encodeURIComponent(helperSource)}` +); + +const { + canExpandDiscoveryPanelFromScroll, + getDiscoveryPanelCollapseState, +} = helperModule; + +assert.equal( + getDiscoveryPanelCollapseState({ + currentScrollTop: 24, + lastScrollTop: 0, + isCollapsed: false, + threshold: 16, + }), + true, + "scrolling up into discovery content should collapse the welcome panel" +); + +assert.equal( + getDiscoveryPanelCollapseState({ + currentScrollTop: 18, + lastScrollTop: 15, + isCollapsed: false, + threshold: 16, + }), + true, + "slow upward scrolling past the threshold should collapse the welcome panel" +); + +assert.equal( + getDiscoveryPanelCollapseState({ + currentScrollTop: 40, + lastScrollTop: 64, + isCollapsed: true, + threshold: 16, + allowExpand: true, + }), + false, + "scrolling down should reveal the welcome panel" +); + +assert.equal( + getDiscoveryPanelCollapseState({ + currentScrollTop: 40, + lastScrollTop: 64, + isCollapsed: true, + threshold: 16, + allowExpand: false, + }), + true, + "layout-driven reverse scroll after release should not reveal the welcome panel" +); + +assert.equal( + getDiscoveryPanelCollapseState({ + currentScrollTop: 63, + lastScrollTop: 64, + isCollapsed: true, + threshold: 16, + }), + true, + "small scroll jitter should keep the current panel state" +); + +assert.match( + chatMainListSource, + /@touchmove\.capture="handleDiscoveryPanelTouchMove"/, + "discovery shell should capture touchmove before nested scroll-view consumes the gesture" +); + +assert.equal( + canExpandDiscoveryPanelFromScroll({ + isTouching: false, + touchDirection: "", + lastDownGestureAt: 1000, + now: 1400, + gestureRetainMs: 800, + }), + true, + "momentum scroll after a down gesture should still be allowed to reveal the welcome panel" +); + +assert.equal( + canExpandDiscoveryPanelFromScroll({ + isTouching: false, + touchDirection: "", + lastDownGestureAt: 1000, + now: 1901, + gestureRetainMs: 800, + }), + false, + "old reverse scroll should not be treated as an active down gesture" +); + +assert.equal( + canExpandDiscoveryPanelFromScroll({ + isTouching: true, + touchDirection: "up", + lastDownGestureAt: 1000, + now: 1200, + gestureRetainMs: 800, + }), + false, + "an active upward gesture should block reveal even if a down gesture happened earlier" +); diff --git a/src/pages/ChatMain/ChatMainList/index.vue b/src/pages/ChatMain/ChatMainList/index.vue index fd1387a..982439e 100644 --- a/src/pages/ChatMain/ChatMainList/index.vue +++ b/src/pages/ChatMain/ChatMainList/index.vue @@ -21,7 +21,9 @@ + @touchstart="handleDiscoveryPanelTouchStart" @touchmove.capture="handleDiscoveryPanelTouchMove" + @touchmove="handleDiscoveryPanelTouchMove" + @touchend="handleDiscoveryPanelTouchEnd" @touchcancel="handleDiscoveryPanelTouchEnd"> @@ -196,6 +198,10 @@ import { recentConversation, } from "@/request/api/ConversationApi"; import WebSocketManager from "@/utils/WebSocketManager"; +import { + canExpandDiscoveryPanelFromScroll, + getDiscoveryPanelCollapseState, +} from "@/utils/discoveryPanelScroll"; import { IdUtils } from "@/utils"; import { appendLongTextChunk, @@ -228,8 +234,14 @@ const scrollTop = ref(99999); const discoveryInnerScrollTop = ref(0); const discoveryPanelTouchStartY = ref(0); const isDiscoveryPanelCollapsed = ref(false); +let lastDiscoveryInnerScrollTop = 0; +let isDiscoveryPanelTouching = false; +let discoveryPanelTouchDirection = ""; +let lastDiscoveryDownGestureAt = 0; const DISCOVERY_PANEL_EXPAND_TOP = 4; const DISCOVERY_PANEL_TOUCH_DELTA = 16; +const DISCOVERY_PANEL_DIRECTION_DELTA = 2; +const DISCOVERY_PANEL_GESTURE_RETAIN_MS = 800; /// 会话列表 const chatMsgList = ref([]); @@ -389,6 +401,10 @@ const handleScrollAreaTouchMove = () => { const resetDiscoveryScrollState = () => { discoveryInnerScrollTop.value = 0; discoveryPanelTouchStartY.value = 0; + lastDiscoveryInnerScrollTop = 0; + isDiscoveryPanelTouching = false; + discoveryPanelTouchDirection = ""; + lastDiscoveryDownGestureAt = 0; isDiscoveryPanelCollapsed.value = false; }; @@ -400,6 +416,8 @@ const getTouchPointY = (e) => { const handleDiscoveryPanelTouchStart = (e) => { handleScrollAreaTouchStart(); + isDiscoveryPanelTouching = true; + discoveryPanelTouchDirection = ""; discoveryPanelTouchStartY.value = getTouchPointY(e); }; @@ -410,6 +428,14 @@ const handleDiscoveryPanelTouchMove = (e) => { if (!currentY || !discoveryPanelTouchStartY.value) return; const touchDelta = currentY - discoveryPanelTouchStartY.value; + if (touchDelta < -DISCOVERY_PANEL_DIRECTION_DELTA) { + discoveryPanelTouchDirection = "up"; + lastDiscoveryDownGestureAt = 0; + } else if (touchDelta > DISCOVERY_PANEL_DIRECTION_DELTA) { + discoveryPanelTouchDirection = "down"; + lastDiscoveryDownGestureAt = Date.now(); + } + if (touchDelta < -DISCOVERY_PANEL_TOUCH_DELTA) { isDiscoveryPanelCollapsed.value = true; } else if ( @@ -420,11 +446,27 @@ const handleDiscoveryPanelTouchMove = (e) => { } }; +const handleDiscoveryPanelTouchEnd = () => { + isDiscoveryPanelTouching = false; + discoveryPanelTouchDirection = ""; +}; + const handleDiscoveryContentScroll = ({ scrollTop }) => { - discoveryInnerScrollTop.value = Number(scrollTop || 0); - if (discoveryInnerScrollTop.value > DISCOVERY_PANEL_TOUCH_DELTA) { - isDiscoveryPanelCollapsed.value = true; - } + const currentScrollTop = Number(scrollTop || 0); + isDiscoveryPanelCollapsed.value = getDiscoveryPanelCollapseState({ + currentScrollTop, + lastScrollTop: lastDiscoveryInnerScrollTop, + isCollapsed: isDiscoveryPanelCollapsed.value, + threshold: DISCOVERY_PANEL_TOUCH_DELTA, + allowExpand: canExpandDiscoveryPanelFromScroll({ + isTouching: isDiscoveryPanelTouching, + touchDirection: discoveryPanelTouchDirection, + lastDownGestureAt: lastDiscoveryDownGestureAt, + gestureRetainMs: DISCOVERY_PANEL_GESTURE_RETAIN_MS, + }), + }); + discoveryInnerScrollTop.value = currentScrollTop; + lastDiscoveryInnerScrollTop = currentScrollTop; }; const welcomeHeight = ref(0); diff --git a/src/utils/discoveryPanelScroll.js b/src/utils/discoveryPanelScroll.js new file mode 100644 index 0000000..7146c0f --- /dev/null +++ b/src/utils/discoveryPanelScroll.js @@ -0,0 +1,42 @@ +export const getDiscoveryPanelCollapseState = ({ + currentScrollTop = 0, + lastScrollTop = 0, + isCollapsed = false, + threshold = 16, + directionThreshold = 2, + allowExpand = true, +}) => { + const current = Math.max(Number(currentScrollTop) || 0, 0); + const last = Math.max(Number(lastScrollTop) || 0, 0); + const delta = current - last; + + if (delta > directionThreshold && current > threshold) { + return true; + } + + if (allowExpand && delta < -directionThreshold) { + return false; + } + + return isCollapsed; +}; + +export const canExpandDiscoveryPanelFromScroll = ({ + isTouching = false, + touchDirection = "", + lastDownGestureAt = 0, + now = Date.now(), + gestureRetainMs = 800, +}) => { + if (isTouching) { + return touchDirection === "down"; + } + + const lastDownAt = Math.max(Number(lastDownGestureAt) || 0, 0); + if (!lastDownAt) { + return false; + } + + const currentTime = Math.max(Number(now) || 0, 0); + return currentTime - lastDownAt <= gestureRetainMs; +};