Browse Source

feat(首页): 添加设备自适应轮播图和阅读页滚动优化

- 在首页轮播图中添加平板设备适配,根据设备类型显示不同样式和配置
- 在阅读页添加触摸事件检测,优化自动滚动逻辑避免与用户操作冲突
- 为阅读页文本添加高亮状态样式,提升交互体验
hfll
hflllll 2 weeks ago
parent
commit
42c8ce2143
2 changed files with 265 additions and 20 deletions
  1. +118
    -5
      pages/index/home.vue
  2. +147
    -15
      subPages/home/book.vue

+ 118
- 5
pages/index/home.vue View File

@ -41,17 +41,23 @@
</view> </view>
<!-- 轮播图 --> <!-- 轮播图 -->
<view class="swiper-container">
<view class="swiper-container" :class="{ 'tablet-swiper': isTablet }">
<uv-swiper <uv-swiper
:list="bannerList" :list="bannerList"
keyName="image" keyName="image"
height="121"
radius="12"
:height="swiperConfig.height"
:radius="swiperConfig.radius"
:previousMargin="swiperConfig.previousMargin"
:nextMargin="swiperConfig.nextMargin"
:displayMultipleItems="swiperConfig.displayMultipleItems"
indicator indicator
ndicatorInactiveColor="#fff"
indicatorInactiveColor="#fff"
:loading="false" :loading="false"
indicatorMode="dot" indicatorMode="dot"
indicatorActiveColor="#F95A01" indicatorActiveColor="#F95A01"
:autoplay="true"
:interval="4000"
:circular="true"
@click="onBannerClick" @click="onBannerClick"
></uv-swiper> ></uv-swiper>
</view> </view>
@ -232,11 +238,66 @@ export default {
], ],
currentVideo: ''
currentVideo: '',
//
isTablet: false
}
},
computed: {
//
swiperConfig() {
if (this.isTablet) {
// 使
return {
height: "200",
radius: "16",
previousMargin: "60",
nextMargin: "60",
displayMultipleItems: 1.5
}
} else {
// 使
return {
height: "121",
radius: "12",
previousMargin: "0",
nextMargin: "0",
displayMultipleItems: 1
}
}
} }
}, },
methods: { methods: {
//
detectDevice() {
// #ifdef H5
const userAgent = navigator.userAgent
const screenWidth = window.innerWidth || document.documentElement.clientWidth
const screenHeight = window.innerHeight || document.documentElement.clientHeight
//
// 1. 768px
// 2. iPad
this.isTablet = screenWidth >= 768 || /iPad|Android.*(?=.*\b(tablet|pad)\b)/i.test(userAgent)
console.log('设备检测结果:', {
screenWidth,
screenHeight,
userAgent,
isTablet: this.isTablet
})
// #endif
// #ifndef H5
// H5
const systemInfo = uni.getSystemInfoSync()
const screenWidth = systemInfo.screenWidth
this.isTablet = screenWidth >= 768
// #endif
},
// //
onSplashClose() { onSplashClose() {
console.log('開動頁面已關閉') console.log('開動頁面已關閉')
@ -420,11 +481,31 @@ export default {
}, },
async onShow() { async onShow() {
//
this.detectDevice()
// //
await Promise.all([this.getBanner(), this.getSignup(), this.getCategory(), this.getLabel()]) await Promise.all([this.getBanner(), this.getSignup(), this.getCategory(), this.getLabel()])
// label // label
await this.getBooksByLabels() await this.getBooksByLabels()
},
mounted() {
//
this.detectDevice()
// #ifdef H5
//
window.addEventListener('resize', this.detectDevice)
// #endif
},
beforeDestroy() {
// #ifdef H5
//
window.removeEventListener('resize', this.detectDevice)
// #endif
} }
} }
</script> </script>
@ -499,6 +580,38 @@ export default {
margin: 20rpx; margin: 20rpx;
border-radius: 12rpx; border-radius: 12rpx;
overflow: hidden; overflow: hidden;
//
&.tablet-swiper {
margin: 30rpx 0;
border-radius: 16rpx;
//
:deep(.uv-swiper) {
.swiper-slide {
transition: all 0.3s ease;
transform-origin: center;
//
&:not(.swiper-slide-active) {
transform: scale(0.9);
opacity: 0.7;
}
//
&.swiper-slide-active {
transform: scale(1);
opacity: 1;
z-index: 2;
}
}
//
.uv-swiper__indicator {
bottom: -40rpx;
}
}
}
} }
// //


+ 147
- 15
subPages/home/book.vue View File

@ -35,6 +35,9 @@
style="height: 100vh;" style="height: 100vh;"
class="scroll-container" class="scroll-container"
@scroll="onScroll" @scroll="onScroll"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
> >
<view class="content-area" @click="toggleNavbar"> <view class="content-area" @click="toggleNavbar">
<view class="title">{{ currentPageTitle }}</view> <view class="title">{{ currentPageTitle }}</view>
@ -54,7 +57,7 @@
</view> </view>
<view v-for="(item, itemIndex) in page" :key="itemIndex"> <view v-for="(item, itemIndex) in page" :key="itemIndex">
<image class="card-image" v-if="item && item.type === 'image'" :src="item.imageUrl" mode="aspectFill" ></image> <image class="card-image" v-if="item && item.type === 'image'" :src="item.imageUrl" mode="aspectFill" ></image>
<view class="english-text-container clickable-text" v-else-if="item && item.type === 'text' && item.language === 'en' && item.content" @click.stop="handleTextClick(item.content, item, index)" >
<view :class="['english-text-container', 'clickable-text', { 'lead-text': isCardTextHighlighted(page, itemIndex) }]" v-else-if="item && item.type === 'text' && item.language === 'en' && item.content" @click.stop="handleTextClick(item.content, item, index)" >
<text <text
v-for="(token, tokenIndex) in splitEnglishSentence(item.content)" v-for="(token, tokenIndex) in splitEnglishSentence(item.content)"
:key="tokenIndex" :key="tokenIndex"
@ -64,7 +67,7 @@
:style="item.style" :style="item.style"
>{{ token.text }}</text> >{{ token.text }}</text>
</view> </view>
<view v-else-if="item && item.type === 'text' && item.language === 'zh' && item.content" @click.stop="handleTextClick(item.content, item, index)">
<view :class="{ 'lead-text': isCardTextHighlighted(page, itemIndex) }" v-else-if="item && item.type === 'text' && item.language === 'zh' && item.content" @click.stop="handleTextClick(item.content, item, index)">
<text <text
v-for="(segment, segmentIndex) in processChineseText(item.content)" v-for="(segment, segmentIndex) in processChineseText(item.content)"
:key="segmentIndex" :key="segmentIndex"
@ -220,6 +223,12 @@ export default {
scrollTops: [], // scroll-view scrollTops: [], // scroll-view
scrollDebounceTimer: null, // scrollDebounceTimer: null, //
isScrolling: false, // isScrolling: false, //
//
isUserTouching: false, //
touchStartTime: 0, //
touchStartY: 0, // Y
userScrollTimer: null, //
courseIdList: [], courseIdList: [],
bookTitle: '', bookTitle: '',
courseList: [ courseList: [
@ -305,14 +314,82 @@ export default {
// } // }
// }, // },
methods: { methods: {
// -
onTouchStart(e) {
this.isUserTouching = true;
this.touchStartTime = Date.now();
this.touchStartY = e.touches[0].pageY;
//
if (this.userScrollTimer) {
clearTimeout(this.userScrollTimer);
this.userScrollTimer = null;
}
console.log('👆 用户开始触摸屏幕');
},
// -
onTouchMove(e) {
if (!this.isUserTouching) return;
const currentY = e.touches[0].pageY;
const deltaY = Math.abs(currentY - this.touchStartY);
//
if (deltaY > 10) {
//
if (this.isScrolling) {
console.log('🛑 检测到用户手动滚动,停止自动滚动');
this.isScrolling = false;
//
if (this.scrollDebounceTimer) {
clearTimeout(this.scrollDebounceTimer);
this.scrollDebounceTimer = null;
}
}
}
},
// -
onTouchEnd(e) {
this.isUserTouching = false;
//
//
this.userScrollTimer = setTimeout(() => {
console.log('✋ 用户滚动操作结束,允许自动滚动');
this.userScrollTimer = null;
}, 1000); // 1
console.log('👆 用户停止触摸屏幕');
},
//
shouldPreventAutoScroll() {
return this.isUserTouching || this.userScrollTimer !== null;
},
// scroll-view // scroll-view
onScroll(e) { onScroll(e) {
// //
const scrollTop = e.detail.scrollTop; const scrollTop = e.detail.scrollTop;
const currentPageIndex = this.currentPage - 1; const currentPageIndex = this.currentPage - 1;
const previousScrollTop = this.scrollTops[currentPageIndex] || 0;
// //
if (Math.abs((this.scrollTops[currentPageIndex] || 0) - scrollTop) > 5) {
if (Math.abs(previousScrollTop - scrollTop) > 5) {
//
if (this.isScrolling) {
//
const scrollDifference = Math.abs(previousScrollTop - scrollTop);
if (scrollDifference > 100) { //
console.log('🖐️ 检测到手动滚动,中断自动滚动状态');
this.isScrolling = false;
}
}
this.$set(this.scrollTops, currentPageIndex, scrollTop); this.$set(this.scrollTops, currentPageIndex, scrollTop);
} }
}, },
@ -677,20 +754,52 @@ export default {
// //
onScrollToText(scrollData) { onScrollToText(scrollData) {
//
if (this.shouldPreventAutoScroll()) {
console.log('🚫 用户正在手动滚动,跳过自动滚动到文本');
return;
}
// //
if (this.scrollDebounceTimer) { if (this.scrollDebounceTimer) {
clearTimeout(this.scrollDebounceTimer); clearTimeout(this.scrollDebounceTimer);
} }
this.scrollDebounceTimer = setTimeout(() => { this.scrollDebounceTimer = setTimeout(() => {
//
if (this.shouldPreventAutoScroll()) {
console.log('🚫 防抖延迟后检测到用户手动滚动,跳过自动滚动到文本');
return;
}
this.performScrollToText(scrollData); this.performScrollToText(scrollData);
}, 100); // 100ms }, 100); // 100ms
}, },
// //
performScrollToText(scrollData) { performScrollToText(scrollData) {
//
if (this.shouldPreventAutoScroll()) {
console.log('🚫 执行滚动前检测到用户手动操作,取消自动滚动');
return;
}
//
const resetScrollingState = () => {
this.isScrolling = false;
console.log('🔄 滚动状态已重置');
};
//
const safetyTimeout = setTimeout(() => {
if (this.isScrolling) {
console.warn('⚠️ 滚动状态安全超时,强制重置');
resetScrollingState();
}
}, 2000); // 2
if (!scrollData || typeof scrollData.highlightIndex !== 'number' || scrollData.highlightIndex < 0) { if (!scrollData || typeof scrollData.highlightIndex !== 'number' || scrollData.highlightIndex < 0) {
console.warn('滚动数据无效:', scrollData); console.warn('滚动数据无效:', scrollData);
clearTimeout(safetyTimeout);
return; return;
} }
@ -700,12 +809,14 @@ export default {
scrollDataPage: scrollData.currentPage, scrollDataPage: scrollData.currentPage,
currentPage: this.currentPage currentPage: this.currentPage
}); });
clearTimeout(safetyTimeout);
return; return;
} }
// //
if (this.isScrolling) { if (this.isScrolling) {
console.warn('正在滚动中,跳过本次滚动'); console.warn('正在滚动中,跳过本次滚动');
clearTimeout(safetyTimeout);
return; return;
} }
@ -742,6 +853,9 @@ export default {
query.select(selector).boundingClientRect(); query.select(selector).boundingClientRect();
query.exec((res) => { query.exec((res) => {
//
clearTimeout(safetyTimeout);
const scrollViewRect = res[0]; const scrollViewRect = res[0];
const targetRect = res[1]; const targetRect = res[1];
@ -772,7 +886,7 @@ export default {
// //
setTimeout(() => { setTimeout(() => {
this.isScrolling = false;
resetScrollingState();
}, 300); // }, 300); //
console.log('✅ 滚动到高亮文本:', { console.log('✅ 滚动到高亮文本:', {
@ -784,7 +898,7 @@ export default {
}); });
} else { } else {
// //
this.isScrolling = false;
resetScrollingState();
console.log('📍 目标已在视野内,无需滚动'); console.log('📍 目标已在视野内,无需滚动');
} }
} else { } else {
@ -795,7 +909,6 @@ export default {
currentPage: this.currentPage, currentPage: this.currentPage,
highlightIndex: scrollData.highlightIndex highlightIndex: scrollData.highlightIndex
}); });
this.isScrolling = false;
// //
if (!targetRect) { if (!targetRect) {
@ -803,8 +916,11 @@ export default {
const fallbackScrollTop = scrollData.highlightIndex * 100; // const fallbackScrollTop = scrollData.highlightIndex * 100; //
this.$set(this.scrollTops, this.currentPage - 1, fallbackScrollTop); this.$set(this.scrollTops, this.currentPage - 1, fallbackScrollTop);
setTimeout(() => { setTimeout(() => {
this.isScrolling = false;
resetScrollingState();
}, 300); }, 300);
} else {
//
resetScrollingState();
} }
} }
}); });
@ -1404,6 +1520,24 @@ export default {
return false; return false;
}, },
// 线
isCardTextHighlighted(page, index) {
//
if (page !== this.bookPages[this.currentPage - 1]) return false;
// text
let textIndex = 0;
for (let i = 0; i <= index; i++) {
if (page[i].type === 'text') {
if (i === index) {
return textIndex === this.currentHighlightIndex;
}
textIndex++;
}
}
return false;
},
async previousPage() { async previousPage() {
if (this.currentPage > 1) { if (this.currentPage > 1) {
this.currentPage--; this.currentPage--;
@ -2175,6 +2309,9 @@ export default {
border: 1px solid #ffe58f; border: 1px solid #ffe58f;
border-radius: 8px; border-radius: 8px;
padding: 10rpx 20rpx; padding: 10rpx 20rpx;
/* 添加平滑过渡动画 */
transition: all 0.3s ease;
} }
.text-highlight { .text-highlight {
@ -2182,15 +2319,10 @@ export default {
border-left: 4rpx solid #ffd700; /* 左侧金色边框作为朗读指示 */ border-left: 4rpx solid #ffd700; /* 左侧金色边框作为朗读指示 */
padding: 4rpx 8rpx; padding: 4rpx 8rpx;
border-radius: 6rpx; border-radius: 6rpx;
transition: all 0.3s ease;
box-shadow: 0 2rpx 6rpx rgba(255, 215, 0, 0.15); /* 柔和的阴影 */ box-shadow: 0 2rpx 6rpx rgba(255, 215, 0, 0.15); /* 柔和的阴影 */
/* 添加平滑过渡动画 */
transition: all 0.3s ease;
} }
</style> </style>

Loading…
Cancel
Save