finsh
Some checks failed
构建并部署 / build-and-deploy (push) Failing after 1m50s

This commit is contained in:
jeremygan2021
2026-02-13 17:57:25 +08:00
parent ea63e56f96
commit d5bdaf8b0d
4 changed files with 1156 additions and 1324 deletions

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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 {

View File

@@ -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 }}
/> />