| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791 |
- <template>
- <view class="reader-container" :style="readerStyles">
- <!-- 登录提示弹窗 -->
- <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 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>
- <rich-text :nodes="chapterContent" class="content-text"></rich-text>
-
- <!-- 章节结束提示 -->
- <view v-if="isChapterEnd" class="chapter-end">
- <text>本章结束</text>
- <button v-if="!isLastChapter" @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"></uni-icons>
- <text>上一章</text>
- </button>
- <button class="action-btn" @click="toggleMenu">
- <uni-icons type="list" size="24"></uni-icons>
- <text>目录</text>
- </button>
- <button class="action-btn" @click="toggleSettings">
- <uni-icons type="gear" size="24"></uni-icons>
- <text>设置</text>
- </button>
- <button class="action-btn" @click="nextChapter" :disabled="isLastChapter">
- <uni-icons type="arrow-down" size="24"></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>
- <slider :value="fontSize" min="24" max="40" @change="updateFontSize" />
- </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)"
- ></view>
- </view>
- </view>
- <button class="close-btn" @click="$refs.settingsPopup.close()">关闭</button>
- </view>
- </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章免费
- }
- },
- async onLoad(options) {
- console.log('阅读器页面参数:', options)
-
- // 确保参数正确解析
- this.novelId = options.novelId || null
- 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'
- })
- }
-
- // 监听网络状态
- uni.onNetworkStatusChange(this.handleNetworkChange)
- },
- onUnload() {
- // 保存阅读进度
- this.saveReadingProgress()
- // 移除网络状态监听
- uni.offNetworkStatusChange(this.handleNetworkChange)
- },
- methods: {
- // 修复响应解析方法
- parseResponse(res) {
- console.log('原始响应对象:', res)
-
- // 处理不同的响应格式
- let responseData = {}
-
- // 如果res有arg属性,使用arg作为响应数据
- if (res && res.arg) {
- responseData = res.arg
- console.log('使用res.arg作为响应数据:', responseData)
- }
- // 如果res有data属性,使用data作为响应数据
- else if (res && res.data) {
- responseData = res.data
- console.log('使用res.data作为响应数据:', responseData)
- }
- // 如果res本身就是响应数据
- else {
- responseData = res
- console.log('使用res本身作为响应数据:', responseData)
- }
-
- return responseData
- },
-
- // 加载小说数据
- 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 = this.formatContent(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()
- }
- },
-
- // 加载第一章
- async loadFirstChapter() {
- if (this.chapters.length > 0) {
- await this.loadChapter(this.chapters[0].id)
- }
- },
-
- // 格式化内容
- formatContent(content) {
- if (!content) return ''
-
- // 处理HTML标签和特殊字符
- return content
- .replace(/<br\s*\/?>/gi, '\n')
- .replace(/ /g, ' ')
- .replace(/<p>/g, '\n\n')
- .replace(/<\/p>/g, '')
- .replace(/<[^>]+>/g, '')
- .replace(/\n{3,}/g, '\n\n')
- .trim()
- },
-
- // 标记章节为已读
- markChapterAsRead(chapterId) {
- if (!this.readChapters.includes(chapterId)) {
- this.readChapters.push(chapterId)
- this.saveReadingProgress()
- }
- },
-
- // 检查章节是否已读
- isChapterRead(chapterId) {
- return this.readChapters.includes(chapterId)
- },
-
- // 检查章节是否锁定
- isChapterLocked(chapterIndex) {
- // 已登录用户可以阅读所有章节
- if (this.$store.getters.token) return false
-
- // 未登录用户只能阅读前5章
- return chapterIndex >= 5
- },
-
- // 加载阅读进度
- loadReadingProgress() {
- const progress = uni.getStorageSync(`readingProgress_${this.novelId}`)
- if (progress) {
- this.readChapters = progress.readChapters || []
- this.readerStyles = progress.readerStyles || this.readerStyles
- this.fontSize = progress.fontSize || 32
- }
- },
-
- // 保存阅读进度
- saveReadingProgress() {
- const progress = {
- novelId: this.novelId,
- chapterId: this.chapterId,
- readChapters: this.readChapters,
- readerStyles: this.readerStyles,
- fontSize: this.fontSize,
- timestamp: Date.now()
- }
-
- uni.setStorageSync(`readingProgress_${this.novelId}`, progress)
-
- // 同步到服务器(如果已登录)
- if (this.$store.getters.token) {
- this.$http.post('/reading/progress', progress)
- }
- },
-
- // 获取最后阅读的章节
- getLastReadChapter() {
- if (this.chapterId) {
- return this.chapters.find(ch => ch.id === this.chapterId)
- }
-
- const progress = uni.getStorageSync(`readingProgress_${this.novelId}`)
- if (progress && progress.chapterId) {
- return this.chapters.find(ch => ch.id === progress.chapterId)
- }
-
- return null
- },
-
- // 计算阅读进度
- calculateReadingProgress() {
- if (this.chapters.length === 0) return 0
- this.readingProgress = Math.round((this.readChaptersCount / this.chapters.length) * 100)
- },
-
- // 显示登录提示
- showLoginPrompt() {
- this.$refs.loginPopup.open()
- },
-
- // 关闭登录对话框
- closeLoginDialog() {
- this.$refs.loginPopup.close()
- },
-
- // 跳转到登录页面
- goToLogin() {
- uni.navigateTo({
- url: '/pages/login?redirect=' + encodeURIComponent(this.$route.fullPath)
- })
- },
-
- // 上一章
- prevChapter() {
- if (this.currentChapterIndex > 0) {
- const prevChapter = this.chapters[this.currentChapterIndex - 1]
- this.loadChapter(prevChapter.id)
- }
- },
-
- // 下一章
- nextChapter() {
- if (this.currentChapterIndex < this.chapters.length - 1) {
- const nextChapter = this.chapters[this.currentChapterIndex + 1]
-
- // 检查下一章是否锁定
- if (this.isChapterLocked(this.currentChapterIndex + 1)) {
- this.showLoginPrompt()
- return
- }
-
- this.loadChapter(nextChapter.id)
- }
- },
-
- // 选择章节
- selectChapter(chapter, index) {
- // 检查章节是否锁定
- if (this.isChapterLocked(index)) {
- this.showLoginPrompt()
- return
- }
-
- this.loadChapter(chapter.id)
- this.$refs.drawer.close()
- },
-
- // 显示更多操作
- showMoreActions() {
- uni.showActionSheet({
- itemList: ['添加到书架', '分享', '举报', '设置'],
- success: (res) => {
- switch (res.tapIndex) {
- case 0:
- this.addToBookshelf()
- break
- case 1:
- this.shareNovel()
- break
- case 2:
- this.reportNovel()
- break
- case 3:
- this.toggleSettings()
- break
- }
- }
- })
- },
-
- // 添加到书架
- addToBookshelf() {
- if (!this.$store.getters.token) {
- uni.showToast({
- title: '请先登录',
- icon: 'none'
- })
- return
- }
-
- this.$http.post('/bookshelf/add', {
- novelId: this.novelId
- }).then(res => {
- uni.showToast({
- title: '已添加到书架',
- icon: 'success'
- })
- }).catch(error => {
- uni.showToast({
- title: '添加失败',
- icon: 'none'
- })
- })
- },
-
- // 分享小说
- shareNovel() {
- uni.share({
- title: this.novelTitle,
- path: `/pages/novel/reader?novelId=${this.novelId}`,
- success: () => {
- uni.showToast({
- title: '分享成功',
- icon: 'success'
- })
- }
- })
- },
-
- // 举报小说
- reportNovel() {
- uni.navigateTo({
- url: `/pages/report?novelId=${this.novelId}`
- })
- },
-
- // 打开目录
- toggleMenu() {
- this.$refs.drawer.open()
- },
-
- // 打开设置
- toggleSettings() {
- this.$refs.settingsPopup.open()
- },
-
- // 更新字体大小
- updateFontSize(e) {
- this.fontSize = e.detail.value
- this.readerStyles.fontSize = `${this.fontSize}rpx`
- this.saveReadingProgress()
- },
-
- // 更改背景颜色
- changeBackgroundColor(color) {
- this.readerStyles.backgroundColor = color
- // 根据背景色调整文字颜色
- this.readerStyles.color = color === '#2c2c2c' ? '#f0f0f0' : '#333'
- this.saveReadingProgress()
- },
-
- // 返回上一页
- goBack() {
- uni.navigateBack()
- },
-
- // 滚动事件
- onScroll(e) {
- this.scrollTop = e.detail.scrollTop
-
- // 检测是否滚动到章节末尾
- const scrollHeight = e.detail.scrollHeight
- const scrollTop = e.detail.scrollTop
- const clientHeight = e.detail.clientHeight
-
- this.isChapterEnd = scrollHeight - scrollTop - clientHeight < 50
- },
-
- // 处理网络状态变化
- handleNetworkChange(res) {
- if (!res.isConnected) {
- uni.showToast({
- title: '网络已断开',
- icon: 'none'
- })
- }
- }
- }
- }
- </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;
- }
-
- .chapter-end {
- text-align: center;
- margin-top: 40rpx;
- padding: 20rpx;
- border-top: 1rpx solid #eee;
- }
-
- .reader-footer {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- background: rgba(255, 255, 255, 0.9);
- padding: 20rpx;
- border-top: 1rpx solid #eee;
- }
-
- .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;
- }
-
- .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;
- }
-
- .color-options {
- display: flex;
- justify-content: space-between;
- }
-
- .color-option {
- width: 60rpx;
- height: 60rpx;
- border-radius: 50%;
- border: 2rpx solid #eee;
- }
-
- .color-option.active {
- border-color: #2a5caa;
- }
-
- .close-btn {
- margin-top: 30rpx;
- background: #f0f0f0;
- color: #333;
- }
- </style>
|