├── php-api/ # 改造后的PHP接口层 ├── java-ad-service/ # 若依框架微服务(广告+VIP+分账) ├── uniapp-reader/ # UniApp前端项目 │ ├── pages/ # 各端页面 │ └──
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

reader.vue 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. <template>
  2. <view class="reader-container" :style="readerStyles">
  3. <!-- 顶部导航 -->
  4. <view class="reader-header">
  5. <uni-icons type="arrowleft" size="28" color="#fff" @click="goBack"></uni-icons>
  6. <text class="novel-title">{{ novelTitle }}</text>
  7. <view class="header-actions">
  8. <uni-icons type="more" size="28" color="#fff" @click="showMoreActions"></uni-icons>
  9. </view>
  10. </view>
  11. <!-- 阅读区域 -->
  12. <scroll-view scroll-y class="reader-content" :scroll-top="scrollTop" @scroll="onScroll">
  13. <view class="chapter-title">{{ chapterDetail.title }}</view>
  14. <view class="content-text">{{ formattedContent }}</view>
  15. <!-- 章节结束提示 -->
  16. <view v-if="isChapterEnd" class="chapter-end">
  17. <text>本章结束</text>
  18. <button v-if="!isLastChapter" class="next-chapter-btn" @click="nextChapter">下一章</button>
  19. <text v-else>已是最新章节</text>
  20. </view>
  21. </scroll-view>
  22. <!-- 底部操作栏 -->
  23. <view class="reader-footer">
  24. <view class="progress">
  25. <text>{{ currentChapterIndex + 1 }}/{{ chapters.length }}</text>
  26. <text>{{ readingProgress }}%</text>
  27. </view>
  28. <view class="actions">
  29. <button class="action-btn" @click="prevChapter" :disabled="isFirstChapter">
  30. <uni-icons type="arrow-up" size="24" color="#2a5caa"></uni-icons>
  31. <text>上一章</text>
  32. </button>
  33. <button class="action-btn" @click="toggleMenu">
  34. <uni-icons type="list" size="24" color="#2a5caa"></uni-icons>
  35. <text>目录</text>
  36. </button>
  37. <button class="action-btn" @click="toggleSettings">
  38. <uni-icons type="gear" size="24" color="#2a5caa"></uni-icons>
  39. <text>设置</text>
  40. </button>
  41. <button class="action-btn" @click="nextChapter" :disabled="isLastChapter">
  42. <uni-icons type="arrow-down" size="24" color="#2a5caa"></uni-icons>
  43. <text>下一章</text>
  44. </button>
  45. </view>
  46. </view>
  47. <!-- 目录抽屉 -->
  48. <uni-drawer ref="drawer" mode="right" :width="300">
  49. <view class="drawer-content">
  50. <text class="drawer-title">目录</text>
  51. <view class="chapter-stats">
  52. <text>共{{ chapters.length }}章</text>
  53. <text>已读{{ readChaptersCount }}章</text>
  54. </view>
  55. <scroll-view scroll-y class="chapter-list">
  56. <view
  57. v-for="(chapter, index) in chapters"
  58. :key="chapter.id"
  59. class="chapter-item"
  60. :class="{
  61. active: chapter.id === chapterDetail.id,
  62. read: isChapterRead(chapter.id),
  63. locked: isChapterLocked(index)
  64. }"
  65. @click="selectChapter(chapter, index)"
  66. >
  67. <text>{{ index + 1 }}. {{ chapter.title }}</text>
  68. <uni-icons v-if="isChapterLocked(index)" type="locked" size="16" color="#999"></uni-icons>
  69. </view>
  70. </scroll-view>
  71. </view>
  72. </uni-drawer>
  73. <!-- 设置面板 -->
  74. <uni-popup ref="settingsPopup" type="bottom">
  75. <view class="settings-panel">
  76. <text class="panel-title">阅读设置</text>
  77. <view class="setting-item">
  78. <text>字体大小</text>
  79. <view class="font-size-controls">
  80. <button class="size-btn" @click="decreaseFontSize">A-</button>
  81. <text class="size-value">{{ fontSize }}px</text>
  82. <button class="size-btn" @click="increaseFontSize">A+</button>
  83. </view>
  84. </view>
  85. <view class="setting-item">
  86. <text>背景颜色</text>
  87. <view class="color-options">
  88. <view
  89. v-for="color in backgroundColors"
  90. :key="color.value"
  91. class="color-option"
  92. :class="{ active: readerStyles.backgroundColor === color.value }"
  93. :style="{ backgroundColor: color.value }"
  94. @click="changeBackgroundColor(color.value)"
  95. >
  96. <uni-icons v-if="readerStyles.backgroundColor === color.value" type="checkmark" size="16" color="#fff"></uni-icons>
  97. </view>
  98. </view>
  99. </view>
  100. <view class="setting-item">
  101. <text>亮度调节</text>
  102. <slider value="50" min="0" max="100" @change="adjustBrightness" />
  103. </view>
  104. <button class="close-btn" @click="$refs.settingsPopup.close()">关闭设置</button>
  105. </view>
  106. </uni-popup>
  107. <!-- 登录提示弹窗 -->
  108. <uni-popup ref="loginPopup" type="dialog">
  109. <uni-popup-dialog
  110. type="info"
  111. title="登录提示"
  112. content="您已阅读完免费章节,登录后可继续阅读"
  113. :duration="2000"
  114. :before-close="true"
  115. @close="closeLoginDialog"
  116. @confirm="goToLogin"
  117. ></uni-popup-dialog>
  118. </uni-popup>
  119. </view>
  120. </template>
  121. <script>
  122. export default {
  123. data() {
  124. return {
  125. novelId: null,
  126. chapterId: null,
  127. novelTitle: '',
  128. chapterDetail: {},
  129. chapterContent: '',
  130. chapters: [],
  131. currentChapterIndex: 0,
  132. // 阅读统计
  133. readChapters: [],
  134. readingProgress: 0,
  135. scrollTop: 0,
  136. isChapterEnd: false,
  137. // 阅读设置
  138. fontSize: 32,
  139. backgroundColors: [
  140. { name: '护眼模式', value: '#f8f2e0' },
  141. { name: '白色', value: '#ffffff' },
  142. { name: '夜间', value: '#2c2c2c' },
  143. { name: '淡蓝', value: '#e8f4f8' }
  144. ],
  145. readerStyles: {
  146. fontSize: '32rpx',
  147. lineHeight: '1.8',
  148. backgroundColor: '#f8f2e0',
  149. color: '#333'
  150. }
  151. }
  152. },
  153. computed: {
  154. // 计算属性
  155. isFirstChapter() {
  156. return this.currentChapterIndex === 0
  157. },
  158. isLastChapter() {
  159. return this.currentChapterIndex === this.chapters.length - 1
  160. },
  161. readChaptersCount() {
  162. return this.readChapters.length
  163. },
  164. // 检查是否达到免费章节限制
  165. reachedFreeLimit() {
  166. const isLoggedIn = this.$store.getters.token
  167. return !isLoggedIn && this.readChaptersCount >= 5 // 默认5章免费
  168. },
  169. // 格式化内容
  170. formattedContent() {
  171. if (!this.chapterContent) return ''
  172. // 处理HTML标签和特殊字符
  173. return this.chapterContent
  174. .replace(/<br\s*\/?>/gi, '\n')
  175. .replace(/&nbsp;/g, ' ')
  176. .replace(/<p>/g, '\n\n')
  177. .replace(/<\/p>/g, '')
  178. .replace(/<[^>]+>/g, '')
  179. .replace(/\n{3,}/g, '\n\n')
  180. .trim()
  181. }
  182. },
  183. async onLoad(options) {
  184. console.log('阅读器页面参数:', options)
  185. // 确保参数正确解析
  186. this.novelId = options.novelId;
  187. this.chapterId = options.chapterId || null
  188. if (!this.novelId) {
  189. uni.showToast({
  190. title: '小说ID参数缺失',
  191. icon: 'none'
  192. })
  193. setTimeout(() => {
  194. uni.navigateBack()
  195. }, 1500)
  196. return
  197. }
  198. // 加载阅读进度
  199. this.loadReadingProgress()
  200. try {
  201. await this.loadNovelData()
  202. // 如果有指定章节ID,加载该章节,否则加载第一章
  203. if (this.chapterId) {
  204. await this.loadChapter(this.chapterId)
  205. } else {
  206. // 尝试从阅读记录中恢复
  207. const lastReadChapter = this.getLastReadChapter()
  208. if (lastReadChapter) {
  209. await this.loadChapter(lastReadChapter.id)
  210. } else {
  211. await this.loadFirstChapter()
  212. }
  213. }
  214. } catch (error) {
  215. console.error('页面初始化失败:', error)
  216. uni.showToast({
  217. title: '页面初始化失败',
  218. icon: 'none'
  219. })
  220. }
  221. },
  222. methods: {
  223. // 加载小说数据
  224. async loadNovelData() {
  225. try {
  226. uni.showLoading({ title: '加载中...' })
  227. // 加载小说基本信息
  228. const novelRes = await this.$http.get(`/novel/detail/${this.novelId}`)
  229. // 使用统一的响应解析方法
  230. const novelData = this.parseResponse(novelRes)
  231. console.log('小说详情响应:', novelData)
  232. if (novelData.code === 200 && novelData.data) {
  233. this.novelTitle = novelData.data.title
  234. uni.setNavigationBarTitle({ title: this.novelTitle })
  235. }
  236. // 加载章节列表
  237. const chapterRes = await this.$http.get(`/chapter/list/${this.novelId}`)
  238. // 使用统一的响应解析方法
  239. const chapterData = this.parseResponse(chapterRes)
  240. console.log('章节列表响应:', chapterData)
  241. if (chapterData.code === 200) {
  242. // 处理不同的响应格式
  243. if (Array.isArray(chapterData.rows)) {
  244. this.chapters = chapterData.rows
  245. } else if (Array.isArray(chapterData.data)) {
  246. this.chapters = chapterData.data
  247. }
  248. // 初始化当前章节索引
  249. if (this.chapterId) {
  250. this.currentChapterIndex = this.chapters.findIndex(ch => ch.id === this.chapterId)
  251. }
  252. }
  253. } catch (error) {
  254. console.error('加载小说数据失败:', error)
  255. uni.showToast({
  256. title: '加载失败',
  257. icon: 'none'
  258. })
  259. } finally {
  260. uni.hideLoading()
  261. }
  262. },
  263. // 加载章节内容
  264. async loadChapter(chapterId) {
  265. // 检查章节是否锁定
  266. const chapterIndex = this.chapters.findIndex(ch => ch.id === chapterId)
  267. if (this.isChapterLocked(chapterIndex)) {
  268. this.showLoginPrompt()
  269. return
  270. }
  271. try {
  272. uni.showLoading({ title: '加载中...' })
  273. const res = await this.$http.get(`/chapter/content/${chapterId}`)
  274. // 使用统一的响应解析方法
  275. const responseData = this.parseResponse(res)
  276. console.log('章节内容响应:', responseData)
  277. if (responseData.code === 200 && responseData.data) {
  278. this.chapterDetail = responseData.data
  279. this.chapterContent = responseData.data.content
  280. this.chapterId = chapterId
  281. this.currentChapterIndex = chapterIndex
  282. // 标记为已读
  283. this.markChapterAsRead(chapterId)
  284. // 计算阅读进度
  285. this.calculateReadingProgress()
  286. }
  287. } catch (error) {
  288. console.error('加载章节内容失败:', error)
  289. uni.showToast({
  290. title: '加载失败',
  291. icon: 'none'
  292. })
  293. } finally {
  294. uni.hideLoading()
  295. }
  296. },
  297. // 字体大小控制
  298. increaseFontSize() {
  299. if (this.fontSize < 40) {
  300. this.fontSize += 2
  301. this.readerStyles.fontSize = `${this.fontSize}rpx`
  302. this.saveReadingProgress()
  303. }
  304. },
  305. decreaseFontSize() {
  306. if (this.fontSize > 24) {
  307. this.fontSize -= 2
  308. this.readerStyles.fontSize = `${this.fontSize}rpx`
  309. this.saveReadingProgress()
  310. }
  311. },
  312. // 亮度调节
  313. adjustBrightness(e) {
  314. // 在实际应用中,这里可以调用设备API调节屏幕亮度
  315. console.log('调整亮度:', e.detail.value)
  316. },
  317. // 其他方法保持不变...
  318. }
  319. }
  320. </script>
  321. <style scoped>
  322. .reader-container {
  323. position: relative;
  324. height: 100vh;
  325. padding: 20rpx;
  326. box-sizing: border-box;
  327. transition: all 0.3s ease;
  328. }
  329. .reader-header {
  330. position: absolute;
  331. top: 0;
  332. left: 0;
  333. right: 0;
  334. display: flex;
  335. justify-content: space-between;
  336. align-items: center;
  337. padding: 20rpx 30rpx;
  338. background: rgba(0, 0, 0, 0.7);
  339. color: white;
  340. z-index: 100;
  341. }
  342. .novel-title {
  343. font-size: 32rpx;
  344. max-width: 60%;
  345. overflow: hidden;
  346. text-overflow: ellipsis;
  347. white-space: nowrap;
  348. }
  349. .reader-content {
  350. height: calc(100vh - 200rpx);
  351. padding-top: 80rpx;
  352. padding-bottom: 120rpx;
  353. }
  354. .chapter-title {
  355. font-size: 40rpx;
  356. font-weight: bold;
  357. text-align: center;
  358. margin-bottom: 40rpx;
  359. color: #2a5caa;
  360. }
  361. .content-text {
  362. font-size: 32rpx;
  363. line-height: 1.8;
  364. white-space: pre-line;
  365. }
  366. .chapter-end {
  367. text-align: center;
  368. margin-top: 40rpx;
  369. padding: 20rpx;
  370. border-top: 1rpx solid #eee;
  371. }
  372. .next-chapter-btn {
  373. background-color: #2a5caa;
  374. color: white;
  375. margin-top: 20rpx;
  376. border-radius: 10rpx;
  377. }
  378. .reader-footer {
  379. position: absolute;
  380. bottom: 0;
  381. left: 0;
  382. right: 0;
  383. background: rgba(255, 255, 255, 0.95);
  384. padding: 20rpx;
  385. border-top: 1rpx solid #eee;
  386. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
  387. }
  388. .progress {
  389. display: flex;
  390. justify-content: space-between;
  391. font-size: 28rpx;
  392. color: #666;
  393. margin-bottom: 20rpx;
  394. }
  395. .actions {
  396. display: flex;
  397. justify-content: space-between;
  398. }
  399. .action-btn {
  400. display: flex;
  401. flex-direction: column;
  402. align-items: center;
  403. background: none;
  404. border: none;
  405. font-size: 24rpx;
  406. padding: 10rpx;
  407. color: #2a5caa;
  408. }
  409. .action-btn:disabled {
  410. opacity: 0.5;
  411. }
  412. .drawer-content {
  413. padding: 30rpx;
  414. }
  415. .drawer-title {
  416. font-size: 36rpx;
  417. font-weight: bold;
  418. display: block;
  419. margin-bottom: 20rpx;
  420. border-bottom: 1rpx solid #eee;
  421. padding-bottom: 20rpx;
  422. }
  423. .chapter-stats {
  424. display: flex;
  425. justify-content: space-between;
  426. margin-bottom: 20rpx;
  427. font-size: 24rpx;
  428. color: #666;
  429. }
  430. .chapter-list {
  431. height: calc(100vh - 200rpx);
  432. }
  433. .chapter-item {
  434. display: flex;
  435. justify-content: space-between;
  436. align-items: center;
  437. padding: 20rpx 10rpx;
  438. border-bottom: 1rpx solid #f0f0f0;
  439. font-size: 28rpx;
  440. }
  441. .chapter-item.active {
  442. color: #2a5caa;
  443. font-weight: bold;
  444. }
  445. .chapter-item.read {
  446. color: #666;
  447. }
  448. .chapter-item.locked {
  449. color: #999;
  450. }
  451. .settings-panel {
  452. padding: 30rpx;
  453. background: white;
  454. border-radius: 20rpx 20rpx 0 0;
  455. }
  456. .panel-title {
  457. font-size: 36rpx;
  458. font-weight: bold;
  459. display: block;
  460. margin-bottom: 30rpx;
  461. text-align: center;
  462. }
  463. .setting-item {
  464. margin-bottom: 30rpx;
  465. }
  466. .setting-item text {
  467. display: block;
  468. margin-bottom: 15rpx;
  469. font-size: 28rpx;
  470. font-weight: bold;
  471. }
  472. .font-size-controls {
  473. display: flex;
  474. align-items: center;
  475. justify-content: space-between;
  476. }
  477. .size-btn {
  478. width: 80rpx;
  479. height: 60rpx;
  480. background: #f0f0f0;
  481. border: none;
  482. border-radius: 10rpx;
  483. font-size: 28rpx;
  484. }
  485. .size-value {
  486. font-size: 28rpx;
  487. font-weight: bold;
  488. }
  489. .color-options {
  490. display: flex;
  491. justify-content: space-between;
  492. }
  493. .color-option {
  494. width: 60rpx;
  495. height: 60rpx;
  496. border-radius: 50%;
  497. border: 2rpx solid #eee;
  498. display: flex;
  499. align-items: center;
  500. justify-content: center;
  501. }
  502. .color-option.active {
  503. border-color: #2a5caa;
  504. }
  505. .close-btn {
  506. margin-top: 30rpx;
  507. background: #2a5caa;
  508. color: white;
  509. border-radius: 10rpx;
  510. }
  511. </style>