├── php-api/ # 改造后的PHP接口层 ├── java-ad-service/ # 若依框架微服务(广告+VIP+分账) ├── uniapp-reader/ # UniApp前端项目 │ ├── pages/ # 各端页面 │ └──
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

NovelReader.vue 3.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. <template>
  2. <view class="reader-container">
  3. <!-- 阅读模式选择器 -->
  4. <view class="mode-selector">
  5. <button @click="readingMode = 'scroll'" :class="{ active: readingMode === 'scroll' }">
  6. 滚动模式
  7. </button>
  8. <button @click="readingMode = 'page'" :class="{ active: readingMode === 'page' }">
  9. 翻页模式
  10. </button>
  11. </view>
  12. <!-- 滚动阅读模式 -->
  13. <scroll-view
  14. v-if="readingMode === 'scroll'"
  15. scroll-y
  16. class="scroll-reader"
  17. @scrolltolower="loadNextChapter"
  18. >
  19. <rich-text :nodes="formatContent(chapterContent)" />
  20. </scroll-view>
  21. <!-- 翻页阅读模式 -->
  22. <swiper
  23. v-else
  24. :current="currentPage"
  25. circular
  26. class="page-reader"
  27. @change="onPageChange"
  28. >
  29. <swiper-item v-for="(page, index) in paginatedContent" :key="index">
  30. <rich-text :nodes="page" />
  31. </swiper-item>
  32. </swiper>
  33. <!-- 底部广告 -->
  34. <view v-if="feedAd" class="bottom-ad">
  35. <ad :unit-id="feedAd.unitId" ad-type="feed" />
  36. </view>
  37. </view>
  38. </template>
  39. <script setup>
  40. import { ref, computed, onMounted, watch } from 'vue'
  41. import { useAdManager } from '@/utils/adManager'
  42. import { useUserStore } from '@/stores/user'
  43. const props = defineProps({
  44. chapterId: Number,
  45. novelId: Number
  46. })
  47. const userStore = useUserStore()
  48. const { showRewardAd, showFeedAd } = useAdManager()
  49. // 阅读状态
  50. const readingMode = ref('scroll') // 'scroll' | 'page'
  51. const currentPage = ref(0)
  52. const chapterContent = ref('')
  53. const paginatedContent = ref([])
  54. const lastReadPosition = ref(0)
  55. const feedAd = ref(null)
  56. // 获取章节内容
  57. const fetchChapterContent = async () => {
  58. const res = await uni.request({
  59. url: `https://php-backend.aiyadianzi.ltd/chapter/${props.chapterId}`,
  60. method: 'GET'
  61. })
  62. // 清洗内容
  63. chapterContent.value = cleanContent(res.data.content)
  64. // 分页处理
  65. paginatedContent.value = paginateContent(chapterContent.value)
  66. // 恢复阅读位置
  67. if (userStore.isLoggedIn) {
  68. const position = await getReadingPosition()
  69. currentPage.value = position.page
  70. lastReadPosition.value = position.scrollTop
  71. }
  72. }
  73. // 内容清洗(移除原始网站信息)
  74. const cleanContent = (content) => {
  75. return content
  76. .replace(/最新网址[::]?\s*[a-z0-9.-]+/gi, '')
  77. .replace(/www\.[a-z0-9]+\.[a-z]{2,}/gi, '')
  78. }
  79. // 内容分页(每页800字符)
  80. const paginateContent = (content) => {
  81. const pages = []
  82. const pageSize = 800
  83. let start = 0
  84. while (start < content.length) {
  85. pages.push(content.substring(start, start + pageSize))
  86. start += pageSize
  87. }
  88. return pages
  89. }
  90. // 翻页事件处理
  91. const onPageChange = (e) => {
  92. currentPage.value = e.detail.current
  93. saveReadingPosition()
  94. showRewardAd(props.chapterId)
  95. }
  96. // 保存阅读位置
  97. const saveReadingPosition = () => {
  98. if (!userStore.isLoggedIn) return
  99. uni.request({
  100. url: 'https://api.aiyadianzi.ltd/reading/progress',
  101. method: 'POST',
  102. data: {
  103. userId: userStore.userId,
  104. novelId: props.novelId,
  105. chapterId: props.chapterId,
  106. page: currentPage.value,
  107. scrollTop: lastReadPosition.value
  108. }
  109. })
  110. }
  111. // 初始化
  112. onMounted(async () => {
  113. await fetchChapterContent()
  114. feedAd.value = showFeedAd()
  115. })
  116. // VIP状态变化时更新广告
  117. watch(() => userStore.isVIP, (isVip) => {
  118. if (isVip) {
  119. feedAd.value = null
  120. } else {
  121. feedAd.value = showFeedAd()
  122. }
  123. })
  124. </script>
  125. <style scoped>
  126. .reader-container {
  127. height: 100vh;
  128. display: flex;
  129. flex-direction: column;
  130. }
  131. .mode-selector {
  132. display: flex;
  133. padding: 10px;
  134. background: var(--header-bg);
  135. button {
  136. flex: 1;
  137. margin: 0 5px;
  138. font-size: 14px;
  139. &.active {
  140. background: var(--primary-color);
  141. color: white;
  142. }
  143. }
  144. }
  145. .scroll-reader {
  146. flex: 1;
  147. padding: 15px;
  148. overflow-y: auto;
  149. }
  150. .page-reader {
  151. flex: 1;
  152. }
  153. .bottom-ad {
  154. height: 100px;
  155. background: #f5f5f5;
  156. }
  157. </style>