| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574 |
- <template>
- <view class="reader-container" :style="readerStyles">
- <!-- 顶部导航 -->
- <view class="reader-header">
- <uni-icons type="arrowleft" size="28" color="#fff" @click="goBack"></uni-icons>
- <text class="novel-title">{{ novelTitle }}</text>
- <view class="header-actions">
- <uni-icons type="more" size="28" color="#fff" @click="showMoreActions"></uni-icons>
- </view>
- </view>
-
- <!-- 阅读区域 -->
- <scroll-view scroll-y class="reader-content" :scroll-top="scrollTop" @scroll="onScroll">
- <view class="chapter-title">{{ chapterDetail.title }}</view>
- <view class="content-text">{{ formattedContent }}</view>
-
- <!-- 章节结束提示 -->
- <view v-if="isChapterEnd" class="chapter-end">
- <text>本章结束</text>
- <button v-if="!isLastChapter" class="next-chapter-btn" @click="nextChapter">下一章</button>
- <text v-else>已是最新章节</text>
- </view>
- </scroll-view>
-
- <!-- 底部操作栏 -->
- <view class="reader-footer">
- <view class="progress">
- <text>{{ currentChapterIndex + 1 }}/{{ chapters.length }}</text>
- <text>{{ readingProgress }}%</text>
- </view>
- <view class="actions">
- <button class="action-btn" @click="prevChapter" :disabled="isFirstChapter">
- <uni-icons type="arrow-up" size="24" color="#2a5caa"></uni-icons>
- <text>上一章</text>
- </button>
- <button class="action-btn" @click="toggleMenu">
- <uni-icons type="list" size="24" color="#2a5caa"></uni-icons>
- <text>目录</text>
- </button>
- <button class="action-btn" @click="toggleSettings">
- <uni-icons type="gear" size="24" color="#2a5caa"></uni-icons>
- <text>设置</text>
- </button>
- <button class="action-btn" @click="nextChapter" :disabled="isLastChapter">
- <uni-icons type="arrow-down" size="24" color="#2a5caa"></uni-icons>
- <text>下一章</text>
- </button>
- </view>
- </view>
-
- <!-- 目录抽屉 -->
- <uni-drawer ref="drawer" mode="right" :width="300">
- <view class="drawer-content">
- <text class="drawer-title">目录</text>
- <view class="chapter-stats">
- <text>共{{ chapters.length }}章</text>
- <text>已读{{ readChaptersCount }}章</text>
- </view>
- <scroll-view scroll-y class="chapter-list">
- <view
- v-for="(chapter, index) in chapters"
- :key="chapter.id"
- class="chapter-item"
- :class="{
- active: chapter.id === chapterDetail.id,
- read: isChapterRead(chapter.id),
- locked: isChapterLocked(index)
- }"
- @click="selectChapter(chapter, index)"
- >
- <text>{{ index + 1 }}. {{ chapter.title }}</text>
- <uni-icons v-if="isChapterLocked(index)" type="locked" size="16" color="#999"></uni-icons>
- </view>
- </scroll-view>
- </view>
- </uni-drawer>
-
- <!-- 设置面板 -->
- <uni-popup ref="settingsPopup" type="bottom">
- <view class="settings-panel">
- <text class="panel-title">阅读设置</text>
- <view class="setting-item">
- <text>字体大小</text>
- <view class="font-size-controls">
- <button class="size-btn" @click="decreaseFontSize">A-</button>
- <text class="size-value">{{ fontSize }}px</text>
- <button class="size-btn" @click="increaseFontSize">A+</button>
- </view>
- </view>
- <view class="setting-item">
- <text>背景颜色</text>
- <view class="color-options">
- <view
- v-for="color in backgroundColors"
- :key="color.value"
- class="color-option"
- :class="{ active: readerStyles.backgroundColor === color.value }"
- :style="{ backgroundColor: color.value }"
- @click="changeBackgroundColor(color.value)"
- >
- <uni-icons v-if="readerStyles.backgroundColor === color.value" type="checkmark" size="16" color="#fff"></uni-icons>
- </view>
- </view>
- </view>
- <view class="setting-item">
- <text>亮度调节</text>
- <slider value="50" min="0" max="100" @change="adjustBrightness" />
- </view>
- <button class="close-btn" @click="$refs.settingsPopup.close()">关闭设置</button>
- </view>
- </uni-popup>
-
- <!-- 登录提示弹窗 -->
- <uni-popup ref="loginPopup" type="dialog">
- <uni-popup-dialog
- type="info"
- title="登录提示"
- content="您已阅读完免费章节,登录后可继续阅读"
- :duration="2000"
- :before-close="true"
- @close="closeLoginDialog"
- @confirm="goToLogin"
- ></uni-popup-dialog>
- </uni-popup>
- </view>
- </template>
-
- <script>
- export default {
- data() {
- return {
- novelId: null,
- chapterId: null,
- novelTitle: '',
- chapterDetail: {},
- chapterContent: '',
- chapters: [],
- currentChapterIndex: 0,
- // 阅读统计
- readChapters: [],
- readingProgress: 0,
- scrollTop: 0,
- isChapterEnd: false,
- // 阅读设置
- fontSize: 32,
- backgroundColors: [
- { name: '护眼模式', value: '#f8f2e0' },
- { name: '白色', value: '#ffffff' },
- { name: '夜间', value: '#2c2c2c' },
- { name: '淡蓝', value: '#e8f4f8' }
- ],
- readerStyles: {
- fontSize: '32rpx',
- lineHeight: '1.8',
- backgroundColor: '#f8f2e0',
- color: '#333'
- }
- }
- },
- computed: {
- // 计算属性
- isFirstChapter() {
- return this.currentChapterIndex === 0
- },
- isLastChapter() {
- return this.currentChapterIndex === this.chapters.length - 1
- },
- readChaptersCount() {
- return this.readChapters.length
- },
- // 检查是否达到免费章节限制
- reachedFreeLimit() {
- const isLoggedIn = this.$store.getters.token
- return !isLoggedIn && this.readChaptersCount >= 5 // 默认5章免费
- },
- // 格式化内容
- formattedContent() {
- if (!this.chapterContent) return ''
-
- // 处理HTML标签和特殊字符
- return this.chapterContent
- .replace(/<br\s*\/?>/gi, '\n')
- .replace(/ /g, ' ')
- .replace(/<p>/g, '\n\n')
- .replace(/<\/p>/g, '')
- .replace(/<[^>]+>/g, '')
- .replace(/\n{3,}/g, '\n\n')
- .trim()
- }
- },
- async onLoad(options) {
- console.log('阅读器页面参数:', options)
-
- // 确保参数正确解析
- this.novelId = options.novelId;
- this.chapterId = options.chapterId || null
-
- if (!this.novelId) {
- uni.showToast({
- title: '小说ID参数缺失',
- icon: 'none'
- })
- setTimeout(() => {
- uni.navigateBack()
- }, 1500)
- return
- }
-
- // 加载阅读进度
- this.loadReadingProgress()
-
- try {
- await this.loadNovelData()
-
- // 如果有指定章节ID,加载该章节,否则加载第一章
- if (this.chapterId) {
- await this.loadChapter(this.chapterId)
- } else {
- // 尝试从阅读记录中恢复
- const lastReadChapter = this.getLastReadChapter()
- if (lastReadChapter) {
- await this.loadChapter(lastReadChapter.id)
- } else {
- await this.loadFirstChapter()
- }
- }
- } catch (error) {
- console.error('页面初始化失败:', error)
- uni.showToast({
- title: '页面初始化失败',
- icon: 'none'
- })
- }
- },
- methods: {
- // 加载小说数据
- async loadNovelData() {
- try {
- uni.showLoading({ title: '加载中...' })
-
- // 加载小说基本信息
- const novelRes = await this.$http.get(`/novel/detail/${this.novelId}`)
-
- // 使用统一的响应解析方法
- const novelData = this.parseResponse(novelRes)
- console.log('小说详情响应:', novelData)
-
- if (novelData.code === 200 && novelData.data) {
- this.novelTitle = novelData.data.title
- uni.setNavigationBarTitle({ title: this.novelTitle })
- }
-
- // 加载章节列表
- const chapterRes = await this.$http.get(`/chapter/list/${this.novelId}`)
-
- // 使用统一的响应解析方法
- const chapterData = this.parseResponse(chapterRes)
- console.log('章节列表响应:', chapterData)
-
- if (chapterData.code === 200) {
- // 处理不同的响应格式
- if (Array.isArray(chapterData.rows)) {
- this.chapters = chapterData.rows
- } else if (Array.isArray(chapterData.data)) {
- this.chapters = chapterData.data
- }
-
- // 初始化当前章节索引
- if (this.chapterId) {
- this.currentChapterIndex = this.chapters.findIndex(ch => ch.id === this.chapterId)
- }
- }
- } catch (error) {
- console.error('加载小说数据失败:', error)
- uni.showToast({
- title: '加载失败',
- icon: 'none'
- })
- } finally {
- uni.hideLoading()
- }
- },
-
- // 加载章节内容
- async loadChapter(chapterId) {
- // 检查章节是否锁定
- const chapterIndex = this.chapters.findIndex(ch => ch.id === chapterId)
- if (this.isChapterLocked(chapterIndex)) {
- this.showLoginPrompt()
- return
- }
-
- try {
- uni.showLoading({ title: '加载中...' })
-
- const res = await this.$http.get(`/chapter/content/${chapterId}`)
-
- // 使用统一的响应解析方法
- const responseData = this.parseResponse(res)
- console.log('章节内容响应:', responseData)
-
- if (responseData.code === 200 && responseData.data) {
- this.chapterDetail = responseData.data
- this.chapterContent = responseData.data.content
- this.chapterId = chapterId
- this.currentChapterIndex = chapterIndex
-
- // 标记为已读
- this.markChapterAsRead(chapterId)
-
- // 计算阅读进度
- this.calculateReadingProgress()
- }
- } catch (error) {
- console.error('加载章节内容失败:', error)
- uni.showToast({
- title: '加载失败',
- icon: 'none'
- })
- } finally {
- uni.hideLoading()
- }
- },
-
- // 字体大小控制
- increaseFontSize() {
- if (this.fontSize < 40) {
- this.fontSize += 2
- this.readerStyles.fontSize = `${this.fontSize}rpx`
- this.saveReadingProgress()
- }
- },
-
- decreaseFontSize() {
- if (this.fontSize > 24) {
- this.fontSize -= 2
- this.readerStyles.fontSize = `${this.fontSize}rpx`
- this.saveReadingProgress()
- }
- },
-
- // 亮度调节
- adjustBrightness(e) {
- // 在实际应用中,这里可以调用设备API调节屏幕亮度
- console.log('调整亮度:', e.detail.value)
- },
-
- // 其他方法保持不变...
- }
- }
- </script>
-
- <style scoped>
- .reader-container {
- position: relative;
- height: 100vh;
- padding: 20rpx;
- box-sizing: border-box;
- transition: all 0.3s ease;
- }
-
- .reader-header {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 20rpx 30rpx;
- background: rgba(0, 0, 0, 0.7);
- color: white;
- z-index: 100;
- }
-
- .novel-title {
- font-size: 32rpx;
- max-width: 60%;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
-
- .reader-content {
- height: calc(100vh - 200rpx);
- padding-top: 80rpx;
- padding-bottom: 120rpx;
- }
-
- .chapter-title {
- font-size: 40rpx;
- font-weight: bold;
- text-align: center;
- margin-bottom: 40rpx;
- color: #2a5caa;
- }
-
- .content-text {
- font-size: 32rpx;
- line-height: 1.8;
- white-space: pre-line;
- }
-
- .chapter-end {
- text-align: center;
- margin-top: 40rpx;
- padding: 20rpx;
- border-top: 1rpx solid #eee;
- }
-
- .next-chapter-btn {
- background-color: #2a5caa;
- color: white;
- margin-top: 20rpx;
- border-radius: 10rpx;
- }
-
- .reader-footer {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- background: rgba(255, 255, 255, 0.95);
- padding: 20rpx;
- border-top: 1rpx solid #eee;
- box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
- }
-
- .progress {
- display: flex;
- justify-content: space-between;
- font-size: 28rpx;
- color: #666;
- margin-bottom: 20rpx;
- }
-
- .actions {
- display: flex;
- justify-content: space-between;
- }
-
- .action-btn {
- display: flex;
- flex-direction: column;
- align-items: center;
- background: none;
- border: none;
- font-size: 24rpx;
- padding: 10rpx;
- color: #2a5caa;
- }
-
- .action-btn:disabled {
- opacity: 0.5;
- }
-
- .drawer-content {
- padding: 30rpx;
- }
-
- .drawer-title {
- font-size: 36rpx;
- font-weight: bold;
- display: block;
- margin-bottom: 20rpx;
- border-bottom: 1rpx solid #eee;
- padding-bottom: 20rpx;
- }
-
- .chapter-stats {
- display: flex;
- justify-content: space-between;
- margin-bottom: 20rpx;
- font-size: 24rpx;
- color: #666;
- }
-
- .chapter-list {
- height: calc(100vh - 200rpx);
- }
-
- .chapter-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 20rpx 10rpx;
- border-bottom: 1rpx solid #f0f0f0;
- font-size: 28rpx;
- }
-
- .chapter-item.active {
- color: #2a5caa;
- font-weight: bold;
- }
-
- .chapter-item.read {
- color: #666;
- }
-
- .chapter-item.locked {
- color: #999;
- }
-
- .settings-panel {
- padding: 30rpx;
- background: white;
- border-radius: 20rpx 20rpx 0 0;
- }
-
- .panel-title {
- font-size: 36rpx;
- font-weight: bold;
- display: block;
- margin-bottom: 30rpx;
- text-align: center;
- }
-
- .setting-item {
- margin-bottom: 30rpx;
- }
-
- .setting-item text {
- display: block;
- margin-bottom: 15rpx;
- font-size: 28rpx;
- font-weight: bold;
- }
-
- .font-size-controls {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
-
- .size-btn {
- width: 80rpx;
- height: 60rpx;
- background: #f0f0f0;
- border: none;
- border-radius: 10rpx;
- font-size: 28rpx;
- }
-
- .size-value {
- font-size: 28rpx;
- font-weight: bold;
- }
-
- .color-options {
- display: flex;
- justify-content: space-between;
- }
-
- .color-option {
- width: 60rpx;
- height: 60rpx;
- border-radius: 50%;
- border: 2rpx solid #eee;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .color-option.active {
- border-color: #2a5caa;
- }
-
- .close-btn {
- margin-top: 30rpx;
- background: #2a5caa;
- color: white;
- border-radius: 10rpx;
- }
- </style>
|