This commit is contained in:
20
.trae/documents/plan_20260213_095217.md
Normal file
20
.trae/documents/plan_20260213_095217.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
我已找到导致“无法正确下滑与翻页”的根本原因:代码中调用了一个未定义的函数 `getActiveScroller`,导致滚动事件处理中断。此外,滚动边界的检测逻辑需要进一步放宽容差,以适配不同设备的触控精度。
|
||||||
|
|
||||||
|
### 修复与优化计划
|
||||||
|
|
||||||
|
1. **修复运行时错误**
|
||||||
|
- 移除未定义的 `getActiveScroller` 调用。
|
||||||
|
- 改用更稳健的 DOM 查询方式:为每个页面容器添加 `data-index` 属性,通过当前索引精确获取对应的滚动容器。
|
||||||
|
|
||||||
|
2. **重构滚动边界检测(解决“卡顿”与“无法翻页”)**
|
||||||
|
- **增加容差**:将滚动到底/到顶的判断容差从 2px 增加到 5px,解决高分屏或缩放下的精度问题。
|
||||||
|
- **智能识别内容高度**:明确区分“长内容页面”与“短内容页面”。
|
||||||
|
- **短页面**(无滚动条):直接允许向上/向下翻页。
|
||||||
|
- **长页面**:仅在确实滚动到边缘时才触发翻页。
|
||||||
|
- **精准拦截**:仅在**确认要触发翻页**时才调用 `e.preventDefault()`,确保在页面中间时完全保留原生滚动的流畅度。
|
||||||
|
|
||||||
|
3. **优化翻页性能**
|
||||||
|
- 保持当前的 GPU 加速样式(will-change)。
|
||||||
|
- 确保翻页触发后的“冷却时间”逻辑正确,防止一次滑动跳多页。
|
||||||
|
|
||||||
|
我将直接修改 `App.js` 实现上述逻辑,无需额外创建文件。
|
||||||
2193
package-lock.json
generated
2193
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
97
src/App.css
97
src/App.css
@@ -112,6 +112,46 @@ body, html {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
.section::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.section::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(180deg, rgba(0, 245, 212, 0.8), rgba(0, 212, 170, 0.8));
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(0, 245, 212, 0.7) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶/底部渐隐发光,提高滚动品质感 */
|
||||||
|
.section::before,
|
||||||
|
.section::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 40px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
.section::before {
|
||||||
|
top: 0;
|
||||||
|
background: linear-gradient(to bottom, rgba(0, 245, 212, 0.08), transparent);
|
||||||
|
}
|
||||||
|
.section::after {
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(to top, rgba(0, 245, 212, 0.08), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
@@ -1314,6 +1354,23 @@ body, html {
|
|||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 自定义右侧详情面板滚动条样式(更精致) */
|
||||||
|
.detail-scroller::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
.detail-scroller::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.detail-scroller::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(180deg, rgba(0, 245, 212, 0.8), rgba(0, 212, 170, 0.8));
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.detail-scroller {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(0, 245, 212, 0.7) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-card {
|
.detail-card {
|
||||||
background: rgba(255,255,255,0.04);
|
background: rgba(255,255,255,0.04);
|
||||||
border: 1px solid rgba(255,255,255,0.08);
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
@@ -1846,11 +1903,25 @@ body, html {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
background: rgba(255, 255, 255, 0.02);
|
background: rgba(10, 15, 28, 0.65);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(12px) saturate(160%);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
-webkit-backdrop-filter: blur(12px) saturate(160%);
|
||||||
border-radius: 20px;
|
border: 1px solid rgba(0, 245, 212, 0.18);
|
||||||
padding: 1rem 0.5rem;
|
border-radius: 16px;
|
||||||
|
padding: 1rem 0.6rem;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.section-indicators::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
top: 12px;
|
||||||
|
bottom: 12px;
|
||||||
|
width: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: linear-gradient(180deg, rgba(0, 245, 212, 0.25), rgba(0, 212, 170, 0.15));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移动端页面指示器优化 */
|
/* 移动端页面指示器优化 */
|
||||||
@@ -1864,14 +1935,14 @@ body, html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.indicator {
|
.indicator {
|
||||||
width: 12px;
|
width: 14px;
|
||||||
height: 12px;
|
height: 14px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: rgba(255, 255, 255, 0.3);
|
background: rgba(255, 255, 255, 0.3);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: var(--transition-smooth);
|
transition: var(--transition-smooth);
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 2px solid transparent;
|
border: 2px solid rgba(255, 255, 255, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator::before {
|
.indicator::before {
|
||||||
@@ -1889,9 +1960,9 @@ body, html {
|
|||||||
|
|
||||||
.indicator.active {
|
.indicator.active {
|
||||||
background: var(--primary-color);
|
background: var(--primary-color);
|
||||||
transform: scale(1.3);
|
transform: scale(1.28);
|
||||||
box-shadow: 0 0 20px rgba(0, 245, 212, 0.6);
|
box-shadow: 0 0 18px rgba(0, 245, 212, 0.55);
|
||||||
border-color: rgba(0, 245, 212, 0.4);
|
border-color: rgba(0, 245, 212, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator.active::before {
|
.indicator.active::before {
|
||||||
@@ -1901,8 +1972,8 @@ body, html {
|
|||||||
|
|
||||||
.indicator:hover {
|
.indicator:hover {
|
||||||
background: var(--primary-color);
|
background: var(--primary-color);
|
||||||
transform: scale(1.2);
|
transform: scale(1.18);
|
||||||
box-shadow: 0 0 15px rgba(0, 245, 212, 0.4);
|
box-shadow: 0 0 14px rgba(0, 245, 212, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator:hover::before {
|
.indicator:hover::before {
|
||||||
|
|||||||
170
src/App.js
170
src/App.js
@@ -15,6 +15,20 @@ const App = () => {
|
|||||||
const isScrollingRef = useRef(false);
|
const isScrollingRef = useRef(false);
|
||||||
const currentSectionRef = useRef(0);
|
const currentSectionRef = useRef(0);
|
||||||
const isTouchInDetailPanelRef = useRef(false);
|
const isTouchInDetailPanelRef = useRef(false);
|
||||||
|
const sectionScrollRef = useRef(null);
|
||||||
|
const lastFlipAtRef = useRef(0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前激活的滚动容器
|
||||||
|
*/
|
||||||
|
const getActiveScroller = (evtTarget) => {
|
||||||
|
if (sectionScrollRef.current) return sectionScrollRef.current;
|
||||||
|
if (evtTarget && typeof evtTarget.closest === 'function') {
|
||||||
|
const el = evtTarget.closest('.section');
|
||||||
|
if (el) return el;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const isEventFromDetailPanel = (target) => {
|
const isEventFromDetailPanel = (target) => {
|
||||||
if (!target) return false;
|
if (!target) return false;
|
||||||
@@ -49,59 +63,93 @@ const App = () => {
|
|||||||
let touchStartY = 0;
|
let touchStartY = 0;
|
||||||
let touchEndY = 0;
|
let touchEndY = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 桌面端滚轮:允许各页自然滚动;
|
||||||
|
* 当滚动到顶部/底部继续滚动时,触发上一页/下一页翻页
|
||||||
|
*/
|
||||||
const handleWheel = (e) => {
|
const handleWheel = (e) => {
|
||||||
|
// 允许右侧详情面板自身滚动
|
||||||
if (isEventFromDetailPanel(e.target)) {
|
if (isEventFromDetailPanel(e.target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
|
||||||
|
// 获取当前激活的 section 容器
|
||||||
|
const scroller = containerRef.current?.querySelector(`.section[data-index="${currentSectionRef.current}"]`);
|
||||||
|
if (!scroller) return;
|
||||||
|
|
||||||
|
const tolerance = 5; // 增加容差
|
||||||
|
const isScrollable = scroller.scrollHeight > scroller.clientHeight + 1;
|
||||||
|
const atTop = scroller.scrollTop <= tolerance;
|
||||||
|
const atBottom = Math.abs(scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop) <= tolerance;
|
||||||
|
|
||||||
// 如果正在滚动中,忽略新的滚动事件
|
// 如果正在滚动中,忽略新的滚动事件
|
||||||
if (isScrollingRef.current) {
|
if (isScrollingRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置滚动状态
|
// 计算新的section索引(仅在边界继续滚动时触发)
|
||||||
isScrollingRef.current = true;
|
|
||||||
setIsScrolling(true);
|
|
||||||
|
|
||||||
// 计算新的section索引
|
|
||||||
let newSection = currentSectionRef.current;
|
let newSection = currentSectionRef.current;
|
||||||
if (e.deltaY > 0 && currentSectionRef.current < sections.length - 1) {
|
let shouldFlip = false;
|
||||||
newSection = currentSectionRef.current + 1;
|
|
||||||
} else if (e.deltaY < 0 && currentSectionRef.current > 0) {
|
if (e.deltaY > 0) {
|
||||||
newSection = currentSectionRef.current - 1;
|
// 向下滚动
|
||||||
|
if (currentSectionRef.current < sections.length - 1) {
|
||||||
|
// 如果不可滚动,或者已经到底部,则翻页
|
||||||
|
if (!isScrollable || atBottom) {
|
||||||
|
shouldFlip = true;
|
||||||
|
newSection = currentSectionRef.current + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (e.deltaY < 0) {
|
||||||
|
// 向上滚动
|
||||||
|
if (currentSectionRef.current > 0) {
|
||||||
|
// 如果不可滚动,或者已经到顶部,则翻页
|
||||||
|
if (!isScrollable || atTop) {
|
||||||
|
shouldFlip = true;
|
||||||
|
newSection = currentSectionRef.current - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果section有变化,更新状态
|
if (shouldFlip) {
|
||||||
if (newSection !== currentSectionRef.current) {
|
e.preventDefault();
|
||||||
|
|
||||||
|
// 设置滚动状态
|
||||||
|
isScrollingRef.current = true;
|
||||||
|
setIsScrolling(true);
|
||||||
setCurrentSection(newSection);
|
setCurrentSection(newSection);
|
||||||
|
lastFlipAtRef.current = Date.now();
|
||||||
|
|
||||||
|
// 清除之前的超时
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置超时来重置滚动状态(匹配翻页动画)
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
isScrollingRef.current = false;
|
||||||
|
setIsScrolling(false);
|
||||||
|
}, 900);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除之前的超时
|
|
||||||
if (timeout) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置超时来重置滚动状态
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
isScrollingRef.current = false;
|
|
||||||
setIsScrolling(false);
|
|
||||||
}, 1200); // 增加到1.2秒,与动画时间匹配
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 触摸事件处理
|
// 触摸事件处理
|
||||||
|
/**
|
||||||
|
* 移动端触摸开始:记录起点并标记是否在详情面板内
|
||||||
|
*/
|
||||||
const handleTouchStart = (e) => {
|
const handleTouchStart = (e) => {
|
||||||
isTouchInDetailPanelRef.current = isEventFromDetailPanel(e.target);
|
isTouchInDetailPanelRef.current = isEventFromDetailPanel(e.target);
|
||||||
touchStartY = e.touches[0].clientY;
|
touchStartY = e.touches[0].clientY;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTouchMove = (e) => {
|
/**
|
||||||
if (isTouchInDetailPanelRef.current) {
|
* 移动端触摸移动:允许各页自然滚动,不拦截
|
||||||
return;
|
*/
|
||||||
}
|
const handleTouchMove = () => {};
|
||||||
e.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动端触摸结束:在滚动到顶部/底部时,依据滑动方向触发翻页
|
||||||
|
*/
|
||||||
const handleTouchEnd = (e) => {
|
const handleTouchEnd = (e) => {
|
||||||
if (isTouchInDetailPanelRef.current) {
|
if (isTouchInDetailPanelRef.current) {
|
||||||
isTouchInDetailPanelRef.current = false;
|
isTouchInDetailPanelRef.current = false;
|
||||||
@@ -115,31 +163,51 @@ const App = () => {
|
|||||||
const deltaY = touchStartY - touchEndY;
|
const deltaY = touchStartY - touchEndY;
|
||||||
const minSwipeDistance = 50; // 最小滑动距离
|
const minSwipeDistance = 50; // 最小滑动距离
|
||||||
|
|
||||||
|
// 获取当前激活的 section 容器
|
||||||
|
const scroller = containerRef.current?.querySelector(`.section[data-index="${currentSectionRef.current}"]`);
|
||||||
|
|
||||||
|
const tolerance = 5;
|
||||||
|
const isScrollable = scroller ? scroller.scrollHeight > scroller.clientHeight + 1 : false;
|
||||||
|
const atTop = scroller ? scroller.scrollTop <= tolerance : true;
|
||||||
|
const atBottom = scroller ? Math.abs(scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop) <= tolerance : true;
|
||||||
|
|
||||||
if (Math.abs(deltaY) > minSwipeDistance) {
|
if (Math.abs(deltaY) > minSwipeDistance) {
|
||||||
isScrollingRef.current = true;
|
|
||||||
setIsScrolling(true);
|
|
||||||
|
|
||||||
let newSection = currentSectionRef.current;
|
let newSection = currentSectionRef.current;
|
||||||
if (deltaY > 0 && currentSectionRef.current < sections.length - 1) {
|
let shouldFlip = false;
|
||||||
// 向上滑动,下一页
|
|
||||||
newSection = currentSectionRef.current + 1;
|
if (deltaY > 0) {
|
||||||
} else if (deltaY < 0 && currentSectionRef.current > 0) {
|
// 上滑(手指向上,内容向下),看下一页
|
||||||
// 向下滑动,上一页
|
if (currentSectionRef.current < sections.length - 1) {
|
||||||
newSection = currentSectionRef.current - 1;
|
if (!isScrollable || atBottom) {
|
||||||
|
shouldFlip = true;
|
||||||
|
newSection = currentSectionRef.current + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (deltaY < 0) {
|
||||||
|
// 下滑(手指向下,内容向上),看上一页
|
||||||
|
if (currentSectionRef.current > 0) {
|
||||||
|
if (!isScrollable || atTop) {
|
||||||
|
shouldFlip = true;
|
||||||
|
newSection = currentSectionRef.current - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newSection !== currentSectionRef.current) {
|
if (shouldFlip) {
|
||||||
|
isScrollingRef.current = true;
|
||||||
|
setIsScrolling(true);
|
||||||
setCurrentSection(newSection);
|
setCurrentSection(newSection);
|
||||||
}
|
lastFlipAtRef.current = Date.now();
|
||||||
|
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
isScrollingRef.current = false;
|
isScrollingRef.current = false;
|
||||||
setIsScrolling(false);
|
setIsScrolling(false);
|
||||||
}, 1200);
|
}, 900);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -190,13 +258,14 @@ const App = () => {
|
|||||||
<motion.div
|
<motion.div
|
||||||
key={section.id}
|
key={section.id}
|
||||||
className="section"
|
className="section"
|
||||||
|
data-index={index}
|
||||||
initial={{ opacity: 0, scale: 0.8, rotateX: 15 }}
|
initial={{ opacity: 0, scale: 0.8, rotateX: 15 }}
|
||||||
animate={{
|
animate={{
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
rotateX: 0,
|
rotateX: 0,
|
||||||
transition: {
|
transition: {
|
||||||
duration: 1.2,
|
duration: 0.9,
|
||||||
ease: [0.25, 0.1, 0.25, 1],
|
ease: [0.25, 0.1, 0.25, 1],
|
||||||
staggerChildren: 0.1
|
staggerChildren: 0.1
|
||||||
}
|
}
|
||||||
@@ -205,7 +274,7 @@ const App = () => {
|
|||||||
opacity: 0,
|
opacity: 0,
|
||||||
scale: 1.1,
|
scale: 1.1,
|
||||||
rotateX: -15,
|
rotateX: -15,
|
||||||
transition: { duration: 0.8 }
|
transition: { duration: 0.6 }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -234,6 +303,7 @@ const App = () => {
|
|||||||
key={index}
|
key={index}
|
||||||
className={`indicator ${index === currentSection ? 'active' : ''}`}
|
className={`indicator ${index === currentSection ? 'active' : ''}`}
|
||||||
onClick={() => setCurrentSection(index)}
|
onClick={() => setCurrentSection(index)}
|
||||||
|
title={sections[index].title}
|
||||||
whileHover={{ scale: 1.2 }}
|
whileHover={{ scale: 1.2 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user