小说小程序前端代码仓库(小程序)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

555 lines
12 KiB

<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>
<scroll-view :scroll-y="!isPay" :key="'scroll-' + cid" id="chapter-scroll" class="chapter-content" :class="{'full-content': isFullScreen}"
:scroll-top="scrollTop" @scroll="handleScroll" @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>
</scroll-view>
<view class="bottom-bar" :class="{'bottom-bar-hidden': isFullScreen}">
<view class="bottom-left">
<view class="bar-item">
<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>
<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"
@maskClick="toggleFullScreen"
@subscribe="goToSubscription" />
<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: [],
scrollTop: 0, // 滚动位置
// 是否需要购买
isPay : false,
}
},
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
},
},
onLoad({
id,
cid
}) {
this.id = id
this.cid = cid
this.getDateil()
this.getBookCatalogDetail()
},
onShow() {
this.getBookCatalogList()
},
mounted() {
// 初始设置为全屏模式
this.isFullScreen = true;
},
methods: {
getDateil() {
this.$fetch('getBookDetail', {
id: this.id
}).then(res => {
this.novelData = 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.$nextTick(() => {
this.scrollTop = 0; // 设置scroll-view滚动到顶部
})
})
},
getBookCatalogList() {
this.$fetch('getBookCatalogList', {
bookId: this.id,
pageNo: 1,
pageSize: 9999999,
reverse: 0,
}).then(res => {
this.chapterList = res.records
})
},
handleContentClick() {
this.toggleFullScreen();
},
handleScroll(e) {
// 获取滚动位置
const scrollTop = e.detail.scrollTop;
this.scrollTop = scrollTop; // 更新当前滚动位置
// 滚动时触发订阅弹窗
if (scrollTop > 50 && !this.popupShown) {
this.$refs.subscriptionPopup.open();
this.popupShown = true;
}
},
toggleFullScreen() {
this.isFullScreen = !this.isFullScreen
},
async goToSubscription() {
await this.$fetch('buyNovel', {
bookId : this.id,
novelId : this.cid,
})
this.isPay = false
this.updateSub()
},
selectChapter({
item,
index
}) {
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()
},
// 更新阅读进度到书架
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()
}
},
},
}
</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;
}
}
}
}
}
.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;
}
}
}
}
}
</style>