reader-component.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. <template>
  2. <view class="reader-container" :style="containerStyle">
  3. <!-- 顶部导航栏 -->
  4. <reader-header :show="showControls" :capsule-info="capsuleInfo" :header-height="headerHeight" @back="goBack"
  5. @search="handleSearch" @add-to-shelf="handleAddToShelf" @add-bookmark="handleAddBookmark" />
  6. <!-- 内容区域 -->
  7. <view class="content-wrapper" @tap="toggleControls">
  8. <sliding-container :disabled="showControls || showSettingsPanel || showCatalogPanel"
  9. @slide-prev="handleSlidePrev" @slide-next="handleSlideNext">
  10. <!-- 上一章内容 -->
  11. <template #prev>
  12. <view class="content-area" :style="contentAreaStyle" v-if="prevChapter">
  13. <view class="chapter-title">{{ prevChapter.title }}</view>
  14. <view class="chapter-content" :style="contentStyle">{{ prevChapter.content }}</view>
  15. </view>
  16. </template>
  17. <!-- 当前章节内容 -->
  18. <template #current>
  19. <view class="content-area" :style="contentAreaStyle">
  20. <view class="chapter-title" v-if="currentChapter.title">{{ currentChapter.title }}</view>
  21. <view class="chapter-content" :style="contentStyle">{{ currentChapter.content }}</view>
  22. </view>
  23. </template>
  24. <!-- 下一章内容 -->
  25. <template #next>
  26. <view class="content-area" :style="contentAreaStyle" v-if="nextChapter">
  27. <view class="chapter-title">{{ nextChapter.title }}</view>
  28. <view class="chapter-content" :style="contentStyle">{{ nextChapter.content }}</view>
  29. </view>
  30. </template>
  31. </sliding-container>
  32. </view>
  33. <!-- 底部控制栏 -->
  34. <reader-footer :show="showControls" @show-catalog="toggleCatalog" @show-notes="toggleNotes"
  35. @show-progress="toggleProgress" @show-settings="toggleSettings" />
  36. <!-- 设置面板 -->
  37. <reader-settings :show="showSettingsPanel" :theme-index="themeIndex" :font-size="fontSize" :margin="margin"
  38. :line-height="lineHeight" :bg-colors="bgColors" @close="toggleSettings" @theme-change="changeTheme"
  39. @font-size-change="changeFontSize" @margin-change="changeMargin" @line-height-change="changeLineHeight" />
  40. <!-- 目录面板 -->
  41. <reader-catalog :show="showCatalogPanel" :chapters="chapters" :current-chapter-id="currentChapterId"
  42. @select="selectChapter" @close="toggleCatalog" />
  43. <!-- 遮罩层 -->
  44. <view class="mask" v-if="showCatalogPanel || showNotesPanel || showProgressPanel" @tap="closeAllPanels"></view>
  45. <!-- 翻页提示 -->
  46. <view class="page-tip" v-if="showPageTip">
  47. <text>{{ pageTipText }}</text>
  48. </view>
  49. </view>
  50. </template>
  51. <script>
  52. import ReaderHeader from './reader-header.vue'
  53. import ReaderFooter from './reader-footer.vue'
  54. import ReaderSettings from './reader-settings.vue'
  55. import ReaderCatalog from './reader-catalog.vue'
  56. import SlidingContainer from './sliding-container.vue'
  57. import novelService from './novel-service.js'
  58. export default {
  59. name: 'ReaderComponent',
  60. components: {
  61. ReaderHeader,
  62. ReaderFooter,
  63. ReaderSettings,
  64. ReaderCatalog,
  65. SlidingContainer
  66. },
  67. props: {
  68. novelId: {
  69. type: Number,
  70. required: true
  71. }
  72. },
  73. data() {
  74. return {
  75. currentChapterId: 1,
  76. chapters: [],
  77. currentChapter: {
  78. title: '',
  79. content: '加载中...'
  80. },
  81. showControls: false,
  82. showSettingsPanel: false,
  83. showCatalogPanel: false,
  84. showNotesPanel: false,
  85. showProgressPanel: false,
  86. fontSize: 18,
  87. margin: 20,
  88. lineHeight: 1.8,
  89. themeIndex: 0,
  90. bgColors: [{
  91. bg: '#f8f4e9',
  92. text: '#3e3d3b'
  93. },
  94. {
  95. bg: '#ffffff',
  96. text: '#333333'
  97. },
  98. {
  99. bg: '#e9e9e9',
  100. text: '#333333'
  101. },
  102. {
  103. bg: '#cce8cf',
  104. text: '#333333'
  105. },
  106. {
  107. bg: '#333333',
  108. text: '#c4c4c4'
  109. },
  110. ],
  111. touchStartX: 0,
  112. touchEndX: 0,
  113. showPageTip: false,
  114. pageTipText: '',
  115. capsuleInfo: {
  116. height: 0,
  117. top: 0,
  118. right: 0,
  119. statusBarHeight: 0
  120. },
  121. headerHeight: 90,
  122. prevChapter: null,
  123. nextChapter: null,
  124. }
  125. },
  126. computed: {
  127. containerStyle() {
  128. const theme = this.bgColors[this.themeIndex];
  129. return {
  130. backgroundColor: theme.bg,
  131. color: theme.text,
  132. // padding: `0 ${this.margin}px`,
  133. minHeight: '100vh'
  134. }
  135. },
  136. contentStyle() {
  137. return {
  138. fontSize: `${this.fontSize}px`,
  139. lineHeight: String(this.lineHeight), // 确保 lineHeight 是字符串
  140. }
  141. },
  142. contentAreaStyle() {
  143. // #ifdef MP-WEIXIN
  144. const topPadding = this.capsuleInfo.statusBarHeight + this.headerHeight / 2;
  145. return {
  146. paddingTop: `${topPadding}px`,
  147. paddingBottom: '120rpx',
  148. minHeight: '100vh',
  149. boxSizing: 'border-box'
  150. }
  151. // #endif
  152. // #ifndef MP-WEIXIN
  153. return {
  154. paddingTop: '100rpx',
  155. paddingBottom: '120rpx',
  156. minHeight: '100vh',
  157. boxSizing: 'border-box'
  158. }
  159. // #endif
  160. }
  161. },
  162. methods: {
  163. initPage() {
  164. this.getCapsuleInfo();
  165. this.loadSettings();
  166. this.loadChapterList();
  167. this.loadReadingProgress();
  168. },
  169. toggleControls() {
  170. this.showControls = !this.showControls
  171. if (!this.showControls) {
  172. this.closeAllPanels()
  173. }
  174. },
  175. closeAllPanels() {
  176. this.showSettingsPanel = false
  177. this.showCatalogPanel = false
  178. this.showNotesPanel = false
  179. this.showProgressPanel = false
  180. },
  181. goBack() {
  182. this.$emit('back')
  183. },
  184. handleSearch() {
  185. this.$emit('search')
  186. },
  187. handleAddToShelf() {
  188. this.$emit('add-to-shelf')
  189. },
  190. handleAddBookmark() {
  191. novelService.addBookmark(
  192. this.novelId,
  193. this.currentChapterId,
  194. 0,
  195. this.currentChapter.content.substring(0, 50)
  196. ).then(() => {
  197. uni.showToast({
  198. title: '已添加书签',
  199. icon: 'success'
  200. })
  201. this.$emit('add-bookmark')
  202. })
  203. },
  204. toggleCatalog() {
  205. this.showCatalogPanel = !this.showCatalogPanel
  206. this.showSettingsPanel = false
  207. this.showNotesPanel = false
  208. this.showProgressPanel = false
  209. },
  210. toggleNotes() {
  211. this.showNotesPanel = !this.showNotesPanel
  212. this.showSettingsPanel = false
  213. this.showCatalogPanel = false
  214. this.showProgressPanel = false
  215. this.$emit('show-notes')
  216. },
  217. toggleProgress() {
  218. this.showProgressPanel = !this.showProgressPanel
  219. this.showSettingsPanel = false
  220. this.showCatalogPanel = false
  221. this.showNotesPanel = false
  222. this.$emit('show-progress')
  223. },
  224. toggleSettings() {
  225. this.showSettingsPanel = !this.showSettingsPanel
  226. },
  227. changeTheme(index) {
  228. this.themeIndex = index;
  229. // 强制更新视图
  230. this.$forceUpdate();
  231. // 保存设置
  232. uni.setStorageSync('reader_theme', index);
  233. this.$emit('theme-change', index);
  234. },
  235. changeFontSize(value) {
  236. this.fontSize = Number(value)
  237. uni.setStorageSync('reader_font_size', this.fontSize)
  238. this.$emit('font-size-change', this.fontSize)
  239. },
  240. changeMargin(value) {
  241. this.margin = Number(value)
  242. uni.setStorageSync('reader_margin', this.margin)
  243. this.$emit('margin-change', this.margin)
  244. },
  245. changeLineHeight(value) {
  246. this.lineHeight = Number(value)
  247. uni.setStorageSync('reader_line_height', this.lineHeight)
  248. this.$emit('line-height-change', this.lineHeight)
  249. },
  250. touchStart(e) {
  251. this.touchStartX = e.changedTouches[0].clientX
  252. },
  253. touchEnd(e) {
  254. this.touchEndX = e.changedTouches[0].clientX
  255. const diffX = this.touchEndX - this.touchStartX
  256. if (this.showControls) return
  257. if (diffX > 100) {
  258. this.prevPage()
  259. } else if (diffX < -100) {
  260. this.nextPage()
  261. }
  262. },
  263. prevPage() {
  264. if (this.currentChapterId <= 1) {
  265. this.showPageTip = true
  266. this.pageTipText = '已经是第一章了'
  267. setTimeout(() => {
  268. this.showPageTip = false
  269. }, 1500)
  270. return
  271. }
  272. this.loadChapter(this.currentChapterId - 1)
  273. this.showPageTip = true
  274. this.pageTipText = '上一章'
  275. setTimeout(() => {
  276. this.showPageTip = false
  277. }, 1500)
  278. this.$emit('prev-page')
  279. },
  280. nextPage() {
  281. if (this.currentChapterId >= this.chapters.length) {
  282. this.showPageTip = true
  283. this.pageTipText = '已经是最后一章了'
  284. setTimeout(() => {
  285. this.showPageTip = false
  286. }, 1500)
  287. return
  288. }
  289. this.loadChapter(this.currentChapterId + 1)
  290. this.showPageTip = true
  291. this.pageTipText = '下一章'
  292. setTimeout(() => {
  293. this.showPageTip = false
  294. }, 1500)
  295. this.$emit('next-page')
  296. },
  297. selectChapter(chapter) {
  298. this.loadChapter(chapter.id)
  299. this.showCatalogPanel = false
  300. this.$emit('chapter-change', chapter)
  301. },
  302. loadChapter(chapterId) {
  303. this.currentChapter = {
  304. title: '',
  305. content: '加载中...'
  306. }
  307. novelService.getChapterContent(chapterId).then(chapter => {
  308. this.currentChapter = chapter
  309. this.currentChapterId = chapterId
  310. // 预加载相邻章节
  311. this.preloadAdjacentChapters()
  312. novelService.saveReadingProgress(this.novelId, chapterId, 0)
  313. this.$emit('chapter-loaded', chapter)
  314. })
  315. },
  316. loadChapterList() {
  317. novelService.getChapterList(this.novelId).then(chapters => {
  318. this.chapters = chapters
  319. this.$emit('chapters-loaded', chapters)
  320. })
  321. },
  322. loadReadingProgress() {
  323. novelService.getReadingProgress(this.novelId).then(progress => {
  324. if (progress && progress.chapterId) {
  325. this.loadChapter(progress.chapterId)
  326. } else {
  327. this.loadChapter(1)
  328. }
  329. })
  330. },
  331. loadSettings() {
  332. const theme = uni.getStorageSync('reader_theme')
  333. const fontSize = uni.getStorageSync('reader_font_size')
  334. const margin = uni.getStorageSync('reader_margin')
  335. const lineHeight = uni.getStorageSync('reader_line_height')
  336. this.themeIndex = theme !== '' ? Number(theme) : 0
  337. this.fontSize = fontSize !== '' ? Number(fontSize) : 18
  338. this.margin = margin !== '' ? Number(margin) : 20
  339. this.lineHeight = lineHeight !== '' ? Number(lineHeight) : 1.8
  340. },
  341. getCapsuleInfo() {
  342. // #ifdef MP-WEIXIN
  343. const systemInfo = uni.getSystemInfoSync();
  344. const menuButtonInfo = uni.getMenuButtonBoundingClientRect();
  345. this.capsuleInfo = {
  346. height: menuButtonInfo.height,
  347. top: menuButtonInfo.top,
  348. right: systemInfo.windowWidth - menuButtonInfo.right,
  349. statusBarHeight: systemInfo.statusBarHeight
  350. };
  351. this.headerHeight = (menuButtonInfo.top - systemInfo.statusBarHeight) * 2 + menuButtonInfo.height + 10;
  352. // #endif
  353. // #ifndef MP-WEIXIN
  354. const systemInfo = uni.getSystemInfoSync();
  355. this.capsuleInfo.statusBarHeight = systemInfo.statusBarHeight;
  356. this.headerHeight = 90;
  357. // #endif
  358. },
  359. // 处理滑动到上一章
  360. handleSlidePrev() {
  361. if (this.currentChapterId <= 1) {
  362. this.showPageTip = true
  363. this.pageTipText = '已经是第一章了'
  364. setTimeout(() => {
  365. this.showPageTip = false
  366. }, 1500)
  367. return
  368. }
  369. const targetChapterId = this.currentChapterId - 1
  370. this.loadChapter(targetChapterId)
  371. this.$emit('prev-page')
  372. },
  373. // 处理滑动到下一章
  374. handleSlideNext() {
  375. if (this.currentChapterId >= this.chapters.length) {
  376. this.showPageTip = true
  377. this.pageTipText = '已经是最后一章了'
  378. setTimeout(() => {
  379. this.showPageTip = false
  380. }, 1500)
  381. return
  382. }
  383. const targetChapterId = this.currentChapterId + 1
  384. this.loadChapter(targetChapterId)
  385. this.$emit('next-page')
  386. },
  387. // 预加载相邻章节
  388. preloadAdjacentChapters() {
  389. // 预加载上一章
  390. if (this.currentChapterId > 1) {
  391. novelService.getChapterContent(this.currentChapterId - 1).then(chapter => {
  392. this.prevChapter = chapter
  393. })
  394. } else {
  395. this.prevChapter = null
  396. }
  397. // 预加载下一章
  398. if (this.currentChapterId < this.chapters.length) {
  399. novelService.getChapterContent(this.currentChapterId + 1).then(chapter => {
  400. this.nextChapter = chapter
  401. })
  402. } else {
  403. this.nextChapter = null
  404. }
  405. },
  406. },
  407. created() {
  408. this.initPage()
  409. },
  410. onShow() {
  411. this.getCapsuleInfo()
  412. },
  413. onResize() {
  414. this.getCapsuleInfo()
  415. }
  416. }
  417. </script>
  418. <style scoped>
  419. .reader-container {
  420. position: relative;
  421. width: 100%;
  422. min-height: 100vh;
  423. display: flex;
  424. flex-direction: column;
  425. transition: all 0.3s ease;
  426. overflow: hidden;
  427. }
  428. .content-area {
  429. flex: 1;
  430. min-height: 100vh;
  431. padding: 20rpx;
  432. }
  433. .chapter-title {
  434. font-size: 36rpx;
  435. font-weight: bold;
  436. text-align: center;
  437. margin: 30rpx 0;
  438. }
  439. .chapter-content {
  440. text-align: justify;
  441. }
  442. .mask {
  443. position: fixed;
  444. top: 0;
  445. right: 0;
  446. bottom: 0;
  447. left: 0;
  448. background-color: rgba(0, 0, 0, 0.5);
  449. z-index: 250;
  450. }
  451. .page-tip {
  452. position: fixed;
  453. top: 50%;
  454. left: 50%;
  455. transform: translate(-50%, -50%);
  456. background-color: rgba(0, 0, 0, 0.7);
  457. color: #fff;
  458. padding: 20rpx 40rpx;
  459. border-radius: 10rpx;
  460. font-size: 28rpx;
  461. z-index: 300;
  462. }
  463. /* 添加新的样式 */
  464. .content-wrapper {
  465. flex: 1;
  466. position: relative;
  467. overflow: hidden;
  468. min-height: 100vh;
  469. }
  470. </style>