<template>
|
|
<!-- 小说文本页面 -->
|
|
<view class="reader-container" :class="{'dark-mode': isDarkMode}">
|
|
<view class="top-controls" :class="{'top-controls-hidden': isFullScreen}">
|
|
<view class="controls-inner">
|
|
<view class="left" @click="$utils.navigateBack">
|
|
<uv-icon name="arrow-left" :color="isDarkMode ? '#ccc' : '#333'" size="46rpx"></uv-icon>
|
|
</view>
|
|
<view class="center">
|
|
<text class="title">{{ novelData.name }}</text>
|
|
<text class="chapter">{{ currentChapter }}</text>
|
|
</view>
|
|
<!-- <view class="right">
|
|
<uv-icon name="more-dot-fill" color="#333" size="46rpx"></uv-icon>
|
|
</view> -->
|
|
</view>
|
|
<!-- <view class="progress-bar">
|
|
<view class="progress-inner" :style="{width: readProgress + '%'}"></view>
|
|
</view> -->
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<view class="chapter-content" :class="{'full-content': isFullScreen}"
|
|
@tap="handleContentClick">
|
|
|
|
<view class="chapter-content-item">
|
|
<view class="chapter-title">{{ currentChapter }}</view>
|
|
<view class="paragraph-content">
|
|
<view class="paragraph" v-for="(paragraph, index) in paragraphs" :key="index">
|
|
{{ paragraph }}
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 加载更多提示区域 -->
|
|
<view class="load-more-area" v-if="autoLoadNext && !isPay">
|
|
<view class="load-more-content" v-if="!isAutoLoading && hasNextChapter && !isAtBottom">
|
|
<view class="load-more-line"></view>
|
|
<text class="load-more-text">滚动到底部停留2秒自动加载下一章</text>
|
|
<view class="load-more-line"></view>
|
|
</view>
|
|
<view class="waiting-content" v-else-if="!isAutoLoading && hasNextChapter && isAtBottom">
|
|
<uv-icon name="time" color="#ff6b6b" size="30rpx"></uv-icon>
|
|
<text class="waiting-text">正在底部停留 {{ bottomStayTime.toFixed(1) }}s / 2.0s</text>
|
|
<text class="tip-text">向上滚动可取消</text>
|
|
</view>
|
|
<view class="loading-content" v-else-if="isAutoLoading">
|
|
<uv-icon name="clock" color="#4a90e2" size="30rpx"></uv-icon>
|
|
<text class="loading-text">{{ countdown }}秒后自动跳转下一章</text>
|
|
<text class="cancel-text" @tap="cancelAutoLoad">点击取消</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
</view>
|
|
|
|
<view class="bottom-bar" :class="{'bottom-bar-hidden': isFullScreen}">
|
|
<view class="bottom-left">
|
|
<view class="bar-item" v-if="!isBooshelf" @click="addToBookshelf">
|
|
<view class="bar-icon"> <uv-icon name="plus"></uv-icon> </view>
|
|
<text class="bar-label">加入书架</text>
|
|
</view>
|
|
<view class="bar-item" @click="toggleThemeMode">
|
|
<view class="bar-icon">
|
|
<uv-icon :name="isDarkMode ? 'eye' : 'eye-fill'"></uv-icon>
|
|
</view>
|
|
<text class="bar-label">{{ isDarkMode ? '白天' : '夜间' }}</text>
|
|
</view>
|
|
<view class="bar-item" @click="toggleAutoLoad">
|
|
<view class="bar-icon">
|
|
<uv-icon :name="autoLoadNext ? 'play-circle-fill' : 'play-circle'"></uv-icon>
|
|
</view>
|
|
<text class="bar-label">{{ autoLoadNext ? '自动' : '手动' }}</text>
|
|
</view>
|
|
<!-- <view class="bar-item" @click="resetReadingPosition">
|
|
<view class="bar-icon">
|
|
<uv-icon name="rewind-left"></uv-icon>
|
|
</view>
|
|
<text class="bar-label">重读</text>
|
|
</view> -->
|
|
</view>
|
|
<view class="bottom-right">
|
|
<button class="outline-btn"
|
|
@click="nextChapter(-1)">
|
|
<text class="btn-text">上一章</text>
|
|
</button>
|
|
<button class="outline-btn" @click="$refs.chapterPopup.open()">
|
|
<text class="btn-text">目录</text>
|
|
</button>
|
|
<button class="outline-btn"
|
|
@click="nextChapter(1)">
|
|
<text class="btn-text">下一章</text>
|
|
</button>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 使用封装的订阅弹窗组件 -->
|
|
<subscriptionPopup ref="subscriptionPopup"
|
|
:chapterList="chapterList"
|
|
:currentChapter="currentChapterInfo"
|
|
:currentIndex="currentIndex"
|
|
:bookId="id"
|
|
@maskClick="toggleFullScreen"
|
|
@subscribe="goToSubscription"
|
|
@batchSubscribe="handleBatchSubscribe"
|
|
@videoUnlock="handleVideoUnlock" />
|
|
|
|
<novelVotePopup ref="novelVotePopup" />
|
|
|
|
<chapterPopup ref="chapterPopup" :chapterList="chapterList" :currentIndex="currentIndex"
|
|
@selectChapter="selectChapter" />
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import chapterPopup from '../components/novel/chapterPopup.vue'
|
|
import novelVotePopup from '../components/novel/novelVotePopup.vue'
|
|
import subscriptionPopup from '../components/novel/subscriptionPopup.vue'
|
|
import themeMixin from '@/mixins/themeMode.js' // 导入主题混合器
|
|
|
|
export default {
|
|
components: {
|
|
chapterPopup,
|
|
novelVotePopup,
|
|
subscriptionPopup,
|
|
},
|
|
mixins: [themeMixin], // 使用主题混合器
|
|
data() {
|
|
return {
|
|
isFullScreen: false,
|
|
popupShown: false, // 只弹一次
|
|
currentChapter: "",
|
|
readProgress: 15, // 阅读进度百分比
|
|
paragraphs: [],
|
|
id: 0,
|
|
cid: 0,
|
|
novelData: {},
|
|
chapterList: [],
|
|
|
|
// 是否需要购买
|
|
isPay : false,
|
|
|
|
isBooshelf : false,
|
|
|
|
// 自动加载下一章相关
|
|
autoLoadNext: true, // 是否启用自动加载下一章
|
|
lastScrollTop: 0, // 记录上次滚动位置
|
|
isAutoLoading: false, // 是否正在自动加载
|
|
autoLoadTimer: null, // 自动加载计时器
|
|
countdown: 3, // 倒计时秒数
|
|
scrollThrottle: null, // 滚动节流计时器
|
|
triggerChecked: false, // 是否已经检查过触发条件
|
|
bottomStayTime: 0, // 在底部停留时间
|
|
bottomCheckTimer: null, // 底部停留检测计时器
|
|
isAtBottom: false, // 是否在底部区域
|
|
|
|
// 阅读位置记录相关
|
|
savePositionThrottle: null, // 保存位置节流计时器
|
|
restorePositionTimer: null, // 恢复位置计时器
|
|
}
|
|
},
|
|
computed: {
|
|
currentIndex() {
|
|
for (var index = 0; index < this.chapterList.length; index++) {
|
|
var element = this.chapterList[index];
|
|
if (element.id == this.cid) return index
|
|
}
|
|
|
|
return -1
|
|
},
|
|
currentChapterInfo() {
|
|
return this.chapterList.find(chapter => chapter.id == this.cid) || {}
|
|
},
|
|
hasNextChapter() {
|
|
return this.currentIndex >= 0 && this.currentIndex < this.chapterList.length - 1
|
|
}
|
|
},
|
|
onLoad({
|
|
id,
|
|
cid
|
|
}) {
|
|
this.id = id
|
|
this.cid = cid
|
|
this.getDateil()
|
|
this.getBookCatalogDetail()
|
|
},
|
|
onShow() {
|
|
this.getBookCatalogList()
|
|
this.isAddBook()
|
|
},
|
|
onPageScroll(e) {
|
|
const scrollTop = e.scrollTop;
|
|
|
|
// 保存阅读位置
|
|
if (scrollTop > 100) { // 滚动超过100rpx才开始记录,避免记录无意义的顶部位置
|
|
this.saveReadingPosition(scrollTop);
|
|
}
|
|
|
|
// 页面滚动监听,实现自动加载下一章
|
|
if (!this.autoLoadNext || this.isAutoLoading || this.isPay || !this.hasNextChapter || this.triggerChecked) {
|
|
return;
|
|
}
|
|
|
|
// 节流处理,避免频繁执行
|
|
if (this.scrollThrottle) {
|
|
clearTimeout(this.scrollThrottle);
|
|
}
|
|
|
|
this.scrollThrottle = setTimeout(() => {
|
|
this.checkAutoLoadTrigger(scrollTop);
|
|
}, 100);
|
|
},
|
|
mounted() {
|
|
// 初始设置为全屏模式
|
|
this.isFullScreen = true;
|
|
},
|
|
methods: {
|
|
getDateil() {
|
|
this.$fetch('getBookDetail', {
|
|
id: this.id
|
|
}).then(res => {
|
|
this.novelData = res
|
|
})
|
|
},
|
|
async isAddBook(){
|
|
this.$fetch('isAddBook', {
|
|
bookId: this.id
|
|
}).then(res => {
|
|
this.isBooshelf = res
|
|
})
|
|
},
|
|
async getBookCatalogDetail() {
|
|
|
|
this.isPay = await this.$fetch('getMyShopNovel', {
|
|
bookId : this.id,
|
|
novelId : this.cid,
|
|
})
|
|
|
|
this.isPay = !this.isPay
|
|
|
|
this.$fetch('getBookCatalogDetail', {
|
|
id: this.cid
|
|
}).then(res => {
|
|
|
|
this.paragraphs = res.details && res.details.split('\n')
|
|
this.currentChapter = res.title
|
|
|
|
if(res.isPay != 'Y'){
|
|
this.isPay = false
|
|
}
|
|
|
|
this.updateSub()
|
|
|
|
// 更新阅读进度到书架
|
|
this.updateReadProgress()
|
|
|
|
// 重置自动加载相关状态
|
|
this.lastScrollTop = 0;
|
|
if (this.autoLoadTimer) {
|
|
clearInterval(this.autoLoadTimer);
|
|
this.autoLoadTimer = null;
|
|
}
|
|
if (this.scrollThrottle) {
|
|
clearTimeout(this.scrollThrottle);
|
|
this.scrollThrottle = null;
|
|
}
|
|
if (this.savePositionThrottle) {
|
|
clearTimeout(this.savePositionThrottle);
|
|
this.savePositionThrottle = null;
|
|
}
|
|
if (this.restorePositionTimer) {
|
|
clearTimeout(this.restorePositionTimer);
|
|
this.restorePositionTimer = null;
|
|
}
|
|
this.clearBottomTimer(); // 清除底部计时器
|
|
this.isAutoLoading = false;
|
|
this.countdown = 3;
|
|
this.triggerChecked = false; // 重置触发检查标记
|
|
this.isAtBottom = false; // 重置底部状态
|
|
this.bottomStayTime = 0; // 重置停留时间
|
|
|
|
// 滚动到顶部或恢复阅读位置
|
|
this.$nextTick(() => {
|
|
// 先尝试恢复阅读位置,如果没有保存的位置则滚动到顶部
|
|
const key = this.getReadingPositionKey();
|
|
let hasSavedPosition = false;
|
|
|
|
try {
|
|
const positionData = uni.getStorageSync(key);
|
|
hasSavedPosition = positionData && positionData.scrollTop > 100;
|
|
} catch (error) {
|
|
console.warn('检查保存位置失败:', error);
|
|
}
|
|
|
|
if (hasSavedPosition) {
|
|
// 有保存的位置,恢复到上次阅读位置
|
|
this.restoreReadingPosition();
|
|
} else {
|
|
// 没有保存的位置,滚动到顶部
|
|
uni.pageScrollTo({
|
|
scrollTop: 0,
|
|
duration: 0
|
|
});
|
|
}
|
|
})
|
|
})
|
|
},
|
|
getBookCatalogList() {
|
|
this.$fetch('getBookCatalogList', {
|
|
bookId: this.id,
|
|
pageNo: 1,
|
|
pageSize: 9999999,
|
|
reverse: 0,
|
|
}).then(res => {
|
|
this.chapterList = res.records
|
|
})
|
|
},
|
|
handleContentClick() {
|
|
this.toggleFullScreen();
|
|
},
|
|
toggleFullScreen() {
|
|
this.isFullScreen = !this.isFullScreen
|
|
},
|
|
// 检查自动加载触发条件
|
|
checkAutoLoadTrigger(scrollTop) {
|
|
// 使用更严格的触发条件:距离底部很近且停留一段时间
|
|
uni.createSelectorQuery().select('.chapter-content').boundingClientRect((rect) => {
|
|
if (rect) {
|
|
const windowHeight = uni.getSystemInfoSync().windowHeight;
|
|
const contentBottom = rect.bottom;
|
|
const distanceToBottom = contentBottom - windowHeight;
|
|
|
|
// 严格条件:距离底部100rpx以内才算到达底部
|
|
const isNearBottom = distanceToBottom < 100;
|
|
|
|
if (isNearBottom) {
|
|
// 如果刚到达底部,开始计时
|
|
if (!this.isAtBottom) {
|
|
this.isAtBottom = true;
|
|
this.bottomStayTime = 0;
|
|
console.log('到达章节底部,开始计时...');
|
|
this.startBottomTimer();
|
|
}
|
|
} else {
|
|
// 如果离开底部区域,重置状态
|
|
if (this.isAtBottom) {
|
|
this.isAtBottom = false;
|
|
this.bottomStayTime = 0;
|
|
this.clearBottomTimer();
|
|
console.log('离开章节底部,重置计时');
|
|
}
|
|
}
|
|
}
|
|
|
|
this.lastScrollTop = scrollTop;
|
|
}).exec();
|
|
},
|
|
|
|
// 开始底部停留计时
|
|
startBottomTimer() {
|
|
this.clearBottomTimer(); // 清除之前的计时器
|
|
|
|
this.bottomCheckTimer = setInterval(() => {
|
|
// 只有在底部状态下才继续计时
|
|
if (this.isAtBottom) {
|
|
this.bottomStayTime += 0.1; // 每100ms增加0.1秒
|
|
console.log('底部停留时间:', this.bottomStayTime.toFixed(1) + 's');
|
|
|
|
// 在底部停留2秒后触发自动加载
|
|
if (this.bottomStayTime >= 2) {
|
|
console.log('底部停留足够时间,触发自动加载');
|
|
this.clearBottomTimer();
|
|
this.autoLoadNextChapter();
|
|
}
|
|
} else {
|
|
// 如果不在底部,停止计时
|
|
this.clearBottomTimer();
|
|
}
|
|
}, 100);
|
|
},
|
|
|
|
// 清除底部计时器
|
|
clearBottomTimer() {
|
|
if (this.bottomCheckTimer) {
|
|
clearInterval(this.bottomCheckTimer);
|
|
this.bottomCheckTimer = null;
|
|
}
|
|
},
|
|
|
|
// 生成阅读位置存储key
|
|
getReadingPositionKey() {
|
|
return `novel_reading_position_${this.id}_${this.cid}`;
|
|
},
|
|
|
|
// 保存阅读位置(带节流)
|
|
saveReadingPosition(scrollTop) {
|
|
// 节流处理,避免频繁保存
|
|
if (this.savePositionThrottle) {
|
|
clearTimeout(this.savePositionThrottle);
|
|
}
|
|
|
|
this.savePositionThrottle = setTimeout(() => {
|
|
const key = this.getReadingPositionKey();
|
|
const positionData = {
|
|
scrollTop: scrollTop,
|
|
timestamp: Date.now(),
|
|
chapterTitle: this.currentChapter
|
|
};
|
|
|
|
try {
|
|
uni.setStorageSync(key, positionData);
|
|
console.log('保存阅读位置:', scrollTop, '章节:', this.currentChapter);
|
|
} catch (error) {
|
|
console.warn('保存阅读位置失败:', error);
|
|
}
|
|
}, 1000); // 1秒节流
|
|
},
|
|
|
|
// 恢复阅读位置
|
|
restoreReadingPosition() {
|
|
const key = this.getReadingPositionKey();
|
|
|
|
try {
|
|
const positionData = uni.getStorageSync(key);
|
|
|
|
if (positionData && positionData.scrollTop > 0) {
|
|
console.log('恢复阅读位置:', positionData.scrollTop, '章节:', positionData.chapterTitle);
|
|
|
|
// 检查是否是最近保存的(5分钟内),如果是则显示提示
|
|
const now = Date.now();
|
|
const saveTime = positionData.timestamp || 0;
|
|
const shouldShowToast = (now - saveTime) > 5 * 60 * 1000; // 5分钟
|
|
|
|
// 等待DOM更新后再恢复位置
|
|
this.$nextTick(() => {
|
|
// 延迟恢复,确保内容已渲染
|
|
this.restorePositionTimer = setTimeout(() => {
|
|
uni.pageScrollTo({
|
|
scrollTop: positionData.scrollTop,
|
|
duration: 0
|
|
});
|
|
|
|
// 只有在距离上次保存超过5分钟时才显示提示
|
|
if (shouldShowToast) {
|
|
uni.showToast({
|
|
title: '已恢复阅读位置',
|
|
icon: 'none',
|
|
duration: 1500
|
|
});
|
|
}
|
|
}, 500);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.warn('恢复阅读位置失败:', error);
|
|
}
|
|
},
|
|
|
|
// 清除当前章节的阅读位置记录
|
|
clearReadingPosition() {
|
|
const key = this.getReadingPositionKey();
|
|
|
|
try {
|
|
uni.removeStorageSync(key);
|
|
console.log('清除阅读位置记录:', this.currentChapter);
|
|
} catch (error) {
|
|
console.warn('清除阅读位置失败:', error);
|
|
}
|
|
},
|
|
|
|
// 重置阅读位置(清除记录并回到顶部)
|
|
resetReadingPosition() {
|
|
uni.showModal({
|
|
title: '重新阅读',
|
|
content: '确定要清除当前章节的阅读记录并回到开头吗?',
|
|
confirmText: '确定',
|
|
cancelText: '取消',
|
|
success: (res) => {
|
|
if (res.confirm) {
|
|
// 清除阅读位置记录
|
|
this.clearReadingPosition();
|
|
|
|
// 回到顶部
|
|
uni.pageScrollTo({
|
|
scrollTop: 0,
|
|
duration: 0
|
|
});
|
|
|
|
uni.showToast({
|
|
title: '已重置到章节开头',
|
|
icon: 'success',
|
|
duration: 1500
|
|
});
|
|
}
|
|
}
|
|
});
|
|
},
|
|
// 自动加载下一章
|
|
autoLoadNextChapter() {
|
|
if (this.isAutoLoading || !this.hasNextChapter || this.triggerChecked) return;
|
|
|
|
this.triggerChecked = true; // 标记已触发,避免重复
|
|
this.isAutoLoading = true;
|
|
this.countdown = 3;
|
|
|
|
// 清除之前的计时器
|
|
if (this.autoLoadTimer) {
|
|
clearInterval(this.autoLoadTimer);
|
|
}
|
|
|
|
console.log('开始自动加载下一章倒计时');
|
|
|
|
// 开始倒计时
|
|
this.startCountdown();
|
|
},
|
|
|
|
// 开始倒计时
|
|
startCountdown() {
|
|
const countdownInterval = setInterval(() => {
|
|
this.countdown--;
|
|
|
|
if (this.countdown <= 0) {
|
|
clearInterval(countdownInterval);
|
|
// 清除当前章节的阅读记录,因为用户已经读完了
|
|
this.clearReadingPosition();
|
|
this.nextChapter(1);
|
|
this.isAutoLoading = false;
|
|
this.countdown = 3;
|
|
}
|
|
}, 1000);
|
|
|
|
// 保存interval引用以便取消
|
|
this.autoLoadTimer = countdownInterval;
|
|
},
|
|
// 取消自动加载
|
|
cancelAutoLoad() {
|
|
if (this.autoLoadTimer) {
|
|
clearInterval(this.autoLoadTimer);
|
|
this.autoLoadTimer = null;
|
|
}
|
|
if (this.savePositionThrottle) {
|
|
clearTimeout(this.savePositionThrottle);
|
|
this.savePositionThrottle = null;
|
|
}
|
|
if (this.restorePositionTimer) {
|
|
clearTimeout(this.restorePositionTimer);
|
|
this.restorePositionTimer = null;
|
|
}
|
|
this.clearBottomTimer(); // 清除底部计时器
|
|
this.isAutoLoading = false;
|
|
this.countdown = 3;
|
|
this.triggerChecked = false; // 重置触发标记,允许重新触发
|
|
this.isAtBottom = false; // 重置底部状态
|
|
this.bottomStayTime = 0; // 重置停留时间
|
|
uni.showToast({
|
|
title: '已取消自动跳转',
|
|
icon: 'none'
|
|
});
|
|
},
|
|
// 切换自动加载功能
|
|
toggleAutoLoad() {
|
|
this.autoLoadNext = !this.autoLoadNext;
|
|
|
|
// 如果关闭自动加载且正在加载中,则取消加载
|
|
if (!this.autoLoadNext && this.isAutoLoading) {
|
|
this.cancelAutoLoad();
|
|
}
|
|
|
|
uni.showToast({
|
|
title: this.autoLoadNext ? '已开启自动加载' : '已关闭自动加载',
|
|
icon: 'none'
|
|
});
|
|
},
|
|
async goToSubscription() {
|
|
await this.$fetch('buyNovel', {
|
|
bookId : this.id,
|
|
novelId : this.cid,
|
|
})
|
|
|
|
this.isPay = false
|
|
this.updateSub()
|
|
},
|
|
selectChapter({
|
|
item,
|
|
index
|
|
}) {
|
|
// 保存当前章节ID用于清除记录
|
|
const previousCid = this.cid;
|
|
|
|
this.cid = item.id
|
|
this.isFullScreen = true
|
|
this.getBookCatalogDetail()
|
|
},
|
|
nextChapter(next) {
|
|
let index = this.currentIndex + next
|
|
if(index < 0 || index >= this.chapterList.length){
|
|
uni.showToast({
|
|
title: '到底了',
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
this.cid = this.chapterList[index].id
|
|
this.isFullScreen = true
|
|
this.getBookCatalogDetail()
|
|
// 跳转后重置自动加载状态
|
|
this.isAutoLoading = false;
|
|
this.triggerChecked = false;
|
|
if (this.autoLoadTimer) {
|
|
clearInterval(this.autoLoadTimer);
|
|
this.autoLoadTimer = null;
|
|
}
|
|
if (this.scrollThrottle) {
|
|
clearTimeout(this.scrollThrottle);
|
|
this.scrollThrottle = null;
|
|
}
|
|
if (this.savePositionThrottle) {
|
|
clearTimeout(this.savePositionThrottle);
|
|
this.savePositionThrottle = null;
|
|
}
|
|
if (this.restorePositionTimer) {
|
|
clearTimeout(this.restorePositionTimer);
|
|
this.restorePositionTimer = null;
|
|
}
|
|
this.clearBottomTimer(); // 清除底部计时器
|
|
this.isAtBottom = false; // 重置底部状态
|
|
this.bottomStayTime = 0; // 重置停留时间
|
|
},
|
|
addToBookshelf() {
|
|
this.$fetch('addReadBook', {
|
|
shopId: this.id,
|
|
name: this.novelData.name,
|
|
image: this.novelData.image,
|
|
novelId : this.fastCatalog && this.fastCatalog.id
|
|
}).then(res => {
|
|
this.isBooshelf = true
|
|
uni.showToast({
|
|
title: '已加入书架',
|
|
icon: 'success'
|
|
})
|
|
})
|
|
},
|
|
// 更新阅读进度到书架
|
|
updateReadProgress() {
|
|
if (!this.id || !this.cid) return;
|
|
|
|
this.$fetch('saveOrUpdateReadBook', {
|
|
shopId: this.id, // 书籍id
|
|
novelId: this.cid, // 章节id
|
|
name: this.novelData.name,
|
|
image: this.novelData.image
|
|
}).then(res => {
|
|
console.log('阅读进度已更新');
|
|
}).catch(err => {
|
|
console.error('更新阅读进度失败:', err);
|
|
});
|
|
},
|
|
updateSub(){
|
|
if(this.isPay){
|
|
this.$refs.subscriptionPopup.open()
|
|
}else{
|
|
this.$refs.subscriptionPopup.close()
|
|
}
|
|
},
|
|
|
|
// 处理批量订阅
|
|
async handleBatchSubscribe(batchCount) {
|
|
try {
|
|
// 获取从当前章节开始的连续章节ID
|
|
const chapterIds = [];
|
|
const startIndex = this.currentIndex;
|
|
|
|
for (let i = 0; i < batchCount && (startIndex + i) < this.chapterList.length; i++) {
|
|
const chapter = this.chapterList[startIndex + i];
|
|
if (chapter.isPay === 'Y' && !chapter.pay) {
|
|
chapterIds.push(chapter.id);
|
|
}
|
|
}
|
|
|
|
if (chapterIds.length === 0) {
|
|
uni.showToast({
|
|
title: '没有需要购买的章节',
|
|
icon: 'none'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 调用批量订阅接口
|
|
await this.$fetch('buyNovel', {
|
|
bookId: this.id,
|
|
novelId: chapterIds.join(',')
|
|
});
|
|
|
|
uni.showToast({
|
|
title: `成功订阅${chapterIds.length}章`,
|
|
icon: 'success'
|
|
});
|
|
|
|
// 刷新章节列表状态
|
|
this.getBookCatalogList();
|
|
this.isPay = false;
|
|
this.updateSub();
|
|
|
|
} catch (error) {
|
|
console.error('批量订阅失败:', error);
|
|
uni.showToast({
|
|
title: '订阅失败,请重试',
|
|
icon: 'none'
|
|
});
|
|
}
|
|
},
|
|
|
|
// 处理视频解锁
|
|
async handleVideoUnlock() {
|
|
try {
|
|
// await this.$fetch('openBookCatalog', {
|
|
// bookId: this.id,
|
|
// catalogId: this.cid
|
|
// });
|
|
|
|
uni.showToast({
|
|
title: '暂未开放',
|
|
icon: 'none'
|
|
});
|
|
|
|
// this.isPay = false;
|
|
// this.updateSub();
|
|
|
|
} catch (error) {
|
|
console.error('视频解锁失败:', error);
|
|
uni.showToast({
|
|
title: '解锁失败,请重试',
|
|
icon: 'none'
|
|
});
|
|
}
|
|
},
|
|
},
|
|
beforeDestroy() {
|
|
// 组件销毁时清除所有计时器
|
|
if (this.autoLoadTimer) {
|
|
clearInterval(this.autoLoadTimer);
|
|
this.autoLoadTimer = null;
|
|
}
|
|
if (this.scrollThrottle) {
|
|
clearTimeout(this.scrollThrottle);
|
|
this.scrollThrottle = null;
|
|
}
|
|
if (this.savePositionThrottle) {
|
|
clearTimeout(this.savePositionThrottle);
|
|
this.savePositionThrottle = null;
|
|
}
|
|
if (this.restorePositionTimer) {
|
|
clearTimeout(this.restorePositionTimer);
|
|
this.restorePositionTimer = null;
|
|
}
|
|
this.clearBottomTimer(); // 清除底部计时器
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.reader-container {
|
|
min-height: 100vh;
|
|
background: #fff;
|
|
display: flex;
|
|
flex-direction: column;
|
|
position: relative;
|
|
overflow: hidden;
|
|
|
|
&.dark-mode {
|
|
background: #1a1a1a;
|
|
|
|
.top-controls {
|
|
background: rgba(34, 34, 34, 0.98);
|
|
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.2);
|
|
|
|
.controls-inner {
|
|
.center {
|
|
.title {
|
|
color: #eee;
|
|
}
|
|
|
|
.chapter {
|
|
color: #bbb;
|
|
}
|
|
}
|
|
}
|
|
|
|
.progress-bar {
|
|
background: #333;
|
|
|
|
.progress-inner {
|
|
background: #4a90e2;
|
|
}
|
|
}
|
|
}
|
|
|
|
.chapter-content {
|
|
color: #ccc;
|
|
|
|
.chapter-content-item {
|
|
.chapter-title {
|
|
color: #eee;
|
|
}
|
|
|
|
.paragraph-content {
|
|
.paragraph {
|
|
color: #bbb;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.bottom-bar {
|
|
background: #222;
|
|
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.2);
|
|
|
|
.bottom-left {
|
|
.bar-item {
|
|
.bar-label {
|
|
color: #999;
|
|
}
|
|
}
|
|
}
|
|
|
|
.bottom-right {
|
|
.outline-btn {
|
|
background: #222;
|
|
color: #999;
|
|
border: 2rpx solid #999;
|
|
|
|
.btn-text {
|
|
color: #999;
|
|
border-bottom: 2rpx solid #999;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.load-more-area {
|
|
.load-more-content {
|
|
.load-more-line {
|
|
background: linear-gradient(to right, transparent, #444, transparent);
|
|
}
|
|
|
|
.load-more-text {
|
|
color: #666;
|
|
}
|
|
}
|
|
|
|
.waiting-content {
|
|
background: #4a3728;
|
|
border-color: #6b5b47;
|
|
|
|
.waiting-text {
|
|
color: #ff8a80;
|
|
}
|
|
|
|
.tip-text {
|
|
color: #999;
|
|
}
|
|
}
|
|
|
|
.loading-content {
|
|
background: #2a2a2a;
|
|
|
|
.loading-text {
|
|
color: #4a90e2;
|
|
}
|
|
|
|
.cancel-text {
|
|
color: #999;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.top-controls {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
background: rgba(255, 255, 255, 0.98);
|
|
padding-top: calc(var(--status-bar-height) + 10rpx);
|
|
z-index: 100000;
|
|
transform: translateY(0);
|
|
transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out, background-color 0.3s ease;
|
|
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
|
|
|
.controls-inner {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 20rpx 32rpx;
|
|
position: relative;
|
|
|
|
.left {
|
|
width: 100rpx;
|
|
display: flex;
|
|
justify-content: flex-start;
|
|
align-items: center;
|
|
}
|
|
|
|
.center {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
|
|
.title {
|
|
font-size: 32rpx;
|
|
font-weight: 500;
|
|
color: #333;
|
|
margin-bottom: 4rpx;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 320rpx;
|
|
}
|
|
|
|
.chapter {
|
|
font-size: 24rpx;
|
|
color: #666;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 320rpx;
|
|
}
|
|
}
|
|
|
|
.right {
|
|
width: 100rpx;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
align-items: center;
|
|
}
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 4rpx;
|
|
background: #f0f0f0;
|
|
width: 100%;
|
|
position: relative;
|
|
|
|
.progress-inner {
|
|
height: 100%;
|
|
background: #4a90e2;
|
|
transition: width 0.3s;
|
|
}
|
|
}
|
|
|
|
&.top-controls-hidden {
|
|
transform: translateY(-100%);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.chapter-content {
|
|
flex: 1;
|
|
padding: 0 32rpx;
|
|
font-size: 28rpx;
|
|
color: #222;
|
|
line-height: 2.2;
|
|
padding-top: 160rpx;
|
|
/* 为导航栏预留空间,不随状态变化 */
|
|
padding-bottom: 180rpx;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
overflow-x: hidden;
|
|
transition: color 0.3s ease, background-color 0.3s ease;
|
|
|
|
.chapter-content-item {
|
|
width: 100%;
|
|
|
|
.chapter-title {
|
|
font-size: 36rpx;
|
|
font-weight: bold;
|
|
margin: 20rpx 0 40rpx 0;
|
|
text-align: center;
|
|
word-break: break-word;
|
|
white-space: normal;
|
|
transition: color 0.3s ease;
|
|
}
|
|
|
|
.paragraph-content {
|
|
width: 100%;
|
|
|
|
.paragraph {
|
|
text-indent: 2em;
|
|
margin-bottom: 30rpx;
|
|
line-height: 1.8;
|
|
font-size: 30rpx;
|
|
color: #333;
|
|
word-wrap: break-word;
|
|
word-break: normal;
|
|
white-space: normal;
|
|
transition: color 0.3s ease;
|
|
}
|
|
}
|
|
}
|
|
|
|
&.full-content {
|
|
/* 不再修改顶部padding,保持内容位置不变 */
|
|
}
|
|
}
|
|
|
|
.bottom-bar {
|
|
position: fixed;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: #fff;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 180rpx;
|
|
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
|
|
z-index: 100000;
|
|
padding: 0 40rpx 10rpx 40rpx;
|
|
transform: translateY(0);
|
|
transition: transform 0.3s ease-in-out, background-color 0.3s ease;
|
|
|
|
&.bottom-bar-hidden {
|
|
transform: translateY(100%);
|
|
}
|
|
|
|
.bottom-left {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
gap: 48rpx;
|
|
|
|
.bar-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
|
|
.bar-icon {
|
|
width: 48rpx;
|
|
height: 48rpx;
|
|
margin-bottom: 4rpx;
|
|
margin-right: 1rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.bar-label {
|
|
font-size: 22rpx;
|
|
color: #b3b3b3;
|
|
margin-top: 2rpx;
|
|
transition: color 0.3s ease;
|
|
}
|
|
}
|
|
}
|
|
|
|
.bottom-right {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
gap: 22rpx;
|
|
margin-left: 40rpx;
|
|
text-overflow: ellipsis;
|
|
|
|
.outline-btn {
|
|
flex-shrink: 0;
|
|
min-width: 110rpx;
|
|
padding: 0 26rpx;
|
|
height: 60rpx;
|
|
line-height: 60rpx;
|
|
background: #fff;
|
|
color: #223a7a;
|
|
border: 2rpx solid #223a7a;
|
|
border-radius: 32rpx;
|
|
font-size: 26rpx;
|
|
font-weight: bold;
|
|
margin: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
|
|
|
.btn-text {
|
|
font-weight: bold;
|
|
color: #223a7a;
|
|
font-size: 26rpx;
|
|
border-bottom: 2rpx solid #223a7a;
|
|
padding-bottom: 2rpx;
|
|
transition: color 0.3s ease, border-color 0.3s ease;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* 加载更多区域样式 */
|
|
.load-more-area {
|
|
margin-top: 60rpx;
|
|
padding: 40rpx 0;
|
|
width: 100%;
|
|
|
|
.load-more-content {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
|
|
.load-more-line {
|
|
flex: 1;
|
|
height: 2rpx;
|
|
background: linear-gradient(to right, transparent, #e0e0e0, transparent);
|
|
}
|
|
|
|
.load-more-text {
|
|
font-size: 24rpx;
|
|
color: #999;
|
|
margin: 0 30rpx;
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
|
|
.waiting-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20rpx;
|
|
background: #fff3cd;
|
|
border-radius: 16rpx;
|
|
margin: 0 60rpx;
|
|
border: 2rpx solid #ffeaa7;
|
|
|
|
.waiting-text {
|
|
font-size: 28rpx;
|
|
color: #ff6b6b;
|
|
margin: 10rpx 0 5rpx 0;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.tip-text {
|
|
font-size: 22rpx;
|
|
color: #666;
|
|
font-style: italic;
|
|
}
|
|
}
|
|
|
|
.loading-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20rpx;
|
|
background: #f8f9fa;
|
|
border-radius: 16rpx;
|
|
margin: 0 60rpx;
|
|
|
|
.loading-text {
|
|
font-size: 28rpx;
|
|
color: #4a90e2;
|
|
margin: 10rpx 0 5rpx 0;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.cancel-text {
|
|
font-size: 22rpx;
|
|
color: #666;
|
|
text-decoration: underline;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|