小说小程序前端代码仓库(小程序)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

572 lines
12 KiB

  1. <template>
  2. <!-- 小说文本页面 -->
  3. <view class="reader-container" :class="{'dark-mode': isDarkMode}">
  4. <view class="top-controls" :class="{'top-controls-hidden': isFullScreen}">
  5. <view class="controls-inner">
  6. <view class="left" @click="$utils.navigateBack">
  7. <uv-icon name="arrow-left" :color="isDarkMode ? '#ccc' : '#333'" size="46rpx"></uv-icon>
  8. </view>
  9. <view class="center">
  10. <text class="title">{{ novelData.name }}</text>
  11. <text class="chapter">{{ currentChapter }}</text>
  12. </view>
  13. <!-- <view class="right">
  14. <uv-icon name="more-dot-fill" color="#333" size="46rpx"></uv-icon>
  15. </view> -->
  16. </view>
  17. <!-- <view class="progress-bar">
  18. <view class="progress-inner" :style="{width: readProgress + '%'}"></view>
  19. </view> -->
  20. </view>
  21. <scroll-view class="chapter-content" :class="{'full-content': isFullScreen}"
  22. @scroll="handleScroll" @tap="handleContentClick">
  23. <view class="chapter-content-item">
  24. <view class="chapter-title">{{ currentChapter }}</view>
  25. <view class="paragraph-content">
  26. <view class="paragraph" v-for="(paragraph, index) in paragraphs" :key="index">
  27. {{ paragraph }}
  28. </view>
  29. </view>
  30. </view>
  31. </scroll-view>
  32. <view class="bottom-bar" :class="{'bottom-bar-hidden': isFullScreen}">
  33. <view class="bottom-left">
  34. <view class="bar-item" v-if="!isBooshelf" @click="addToBookshelf">
  35. <view class="bar-icon"> <uv-icon name="plus"></uv-icon> </view>
  36. <text class="bar-label">加入书架</text>
  37. </view>
  38. <view class="bar-item" @click="toggleThemeMode">
  39. <view class="bar-icon">
  40. <uv-icon :name="isDarkMode ? 'eye' : 'eye-fill'"></uv-icon>
  41. </view>
  42. <text class="bar-label">{{ isDarkMode ? '白天' : '夜间' }}</text>
  43. </view>
  44. </view>
  45. <view class="bottom-right">
  46. <button class="outline-btn"
  47. @click="nextChapter(-1)">
  48. <text class="btn-text">上一章</text>
  49. </button>
  50. <button class="outline-btn" @click="$refs.chapterPopup.open()">
  51. <text class="btn-text">目录</text>
  52. </button>
  53. <button class="outline-btn"
  54. @click="nextChapter(1)">
  55. <text class="btn-text">下一章</text>
  56. </button>
  57. </view>
  58. </view>
  59. <!-- 使用封装的订阅弹窗组件 -->
  60. <subscriptionPopup ref="subscriptionPopup"
  61. @maskClick="toggleFullScreen"
  62. @subscribe="goToSubscription" />
  63. <novelVotePopup ref="novelVotePopup" />
  64. <chapterPopup ref="chapterPopup" :chapterList="chapterList" :currentIndex="currentIndex"
  65. @selectChapter="selectChapter" />
  66. </view>
  67. </template>
  68. <script>
  69. import chapterPopup from '../components/novel/chapterPopup.vue'
  70. import novelVotePopup from '../components/novel/novelVotePopup.vue'
  71. import subscriptionPopup from '../components/novel/subscriptionPopup.vue'
  72. import themeMixin from '@/mixins/themeMode.js' // 导入主题混合器
  73. export default {
  74. components: {
  75. chapterPopup,
  76. novelVotePopup,
  77. subscriptionPopup,
  78. },
  79. mixins: [themeMixin], // 使用主题混合器
  80. data() {
  81. return {
  82. isFullScreen: false,
  83. popupShown: false, // 只弹一次
  84. currentChapter: "",
  85. readProgress: 15, // 阅读进度百分比
  86. paragraphs: [],
  87. id: 0,
  88. cid: 0,
  89. novelData: {},
  90. chapterList: [],
  91. // 是否需要购买
  92. isPay : false,
  93. isBooshelf : false,
  94. }
  95. },
  96. computed: {
  97. currentIndex() {
  98. for (var index = 0; index < this.chapterList.length; index++) {
  99. var element = this.chapterList[index];
  100. if (element.id == this.cid) return index
  101. }
  102. return -1
  103. },
  104. },
  105. onLoad({
  106. id,
  107. cid
  108. }) {
  109. this.id = id
  110. this.cid = cid
  111. this.getDateil()
  112. this.getBookCatalogDetail()
  113. },
  114. onShow() {
  115. this.getBookCatalogList()
  116. this.isAddBook()
  117. },
  118. mounted() {
  119. // 初始设置为全屏模式
  120. this.isFullScreen = true;
  121. },
  122. methods: {
  123. getDateil() {
  124. this.$fetch('getBookDetail', {
  125. id: this.id
  126. }).then(res => {
  127. this.novelData = res
  128. })
  129. },
  130. async isAddBook(){
  131. this.$fetch('isAddBook', {
  132. bookId: this.id
  133. }).then(res => {
  134. this.isBooshelf = res
  135. })
  136. },
  137. async getBookCatalogDetail() {
  138. this.isPay = await this.$fetch('getMyShopNovel', {
  139. bookId : this.id,
  140. novelId : this.cid,
  141. })
  142. this.isPay = !this.isPay
  143. this.$fetch('getBookCatalogDetail', {
  144. id: this.cid
  145. }).then(res => {
  146. this.paragraphs = res.details && res.details.split('\n')
  147. this.currentChapter = res.title
  148. if(res.isPay != 'Y'){
  149. this.isPay = false
  150. }
  151. this.updateSub()
  152. // 更新阅读进度到书架
  153. this.updateReadProgress()
  154. // 滚动到顶部
  155. this.$nextTick(() => {
  156. uni.pageScrollTo({
  157. scrollTop: 0,
  158. duration: 0
  159. });
  160. })
  161. })
  162. },
  163. getBookCatalogList() {
  164. this.$fetch('getBookCatalogList', {
  165. bookId: this.id,
  166. pageNo: 1,
  167. pageSize: 9999999,
  168. reverse: 0,
  169. }).then(res => {
  170. this.chapterList = res.records
  171. })
  172. },
  173. handleContentClick() {
  174. this.toggleFullScreen();
  175. },
  176. handleScroll(e) {
  177. // scroll-view的滚动事件(如果需要处理scroll-view内部的滚动逻辑)
  178. // 目前主要的滚动逻辑已移至onPageScroll处理
  179. },
  180. toggleFullScreen() {
  181. this.isFullScreen = !this.isFullScreen
  182. },
  183. async goToSubscription() {
  184. await this.$fetch('buyNovel', {
  185. bookId : this.id,
  186. novelId : this.cid,
  187. })
  188. this.isPay = false
  189. this.updateSub()
  190. },
  191. selectChapter({
  192. item,
  193. index
  194. }) {
  195. this.cid = item.id
  196. this.isFullScreen = true
  197. this.getBookCatalogDetail()
  198. },
  199. nextChapter(next) {
  200. let index = this.currentIndex + next
  201. if(index < 0 || index >= this.chapterList.length){
  202. uni.showToast({
  203. title: '到底了',
  204. icon: 'none'
  205. })
  206. return
  207. }
  208. this.cid = this.chapterList[index].id
  209. this.isFullScreen = true
  210. this.getBookCatalogDetail()
  211. },
  212. addToBookshelf() {
  213. this.$fetch('addReadBook', {
  214. shopId: this.id,
  215. name: this.novelData.name,
  216. image: this.novelData.image,
  217. novelId : this.fastCatalog && this.fastCatalog.id
  218. }).then(res => {
  219. this.isBooshelf = true
  220. uni.showToast({
  221. title: '已加入书架',
  222. icon: 'success'
  223. })
  224. })
  225. },
  226. // 更新阅读进度到书架
  227. updateReadProgress() {
  228. if (!this.id || !this.cid) return;
  229. this.$fetch('saveOrUpdateReadBook', {
  230. shopId: this.id, // 书籍id
  231. novelId: this.cid, // 章节id
  232. name: this.novelData.name,
  233. image: this.novelData.image
  234. }).then(res => {
  235. console.log('阅读进度已更新');
  236. }).catch(err => {
  237. console.error('更新阅读进度失败:', err);
  238. });
  239. },
  240. updateSub(){
  241. if(this.isPay){
  242. this.$refs.subscriptionPopup.open()
  243. }else{
  244. this.$refs.subscriptionPopup.close()
  245. }
  246. },
  247. },
  248. }
  249. </script>
  250. <style lang="scss" scoped>
  251. .reader-container {
  252. min-height: 100vh;
  253. background: #fff;
  254. display: flex;
  255. flex-direction: column;
  256. position: relative;
  257. overflow: hidden;
  258. &.dark-mode {
  259. background: #1a1a1a;
  260. .top-controls {
  261. background: rgba(34, 34, 34, 0.98);
  262. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.2);
  263. .controls-inner {
  264. .center {
  265. .title {
  266. color: #eee;
  267. }
  268. .chapter {
  269. color: #bbb;
  270. }
  271. }
  272. }
  273. .progress-bar {
  274. background: #333;
  275. .progress-inner {
  276. background: #4a90e2;
  277. }
  278. }
  279. }
  280. .chapter-content {
  281. color: #ccc;
  282. .chapter-content-item {
  283. .chapter-title {
  284. color: #eee;
  285. }
  286. .paragraph-content {
  287. .paragraph {
  288. color: #bbb;
  289. }
  290. }
  291. }
  292. }
  293. .bottom-bar {
  294. background: #222;
  295. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.2);
  296. .bottom-left {
  297. .bar-item {
  298. .bar-label {
  299. color: #999;
  300. }
  301. }
  302. }
  303. .bottom-right {
  304. .outline-btn {
  305. background: #222;
  306. color: #999;
  307. border: 2rpx solid #999;
  308. .btn-text {
  309. color: #999;
  310. border-bottom: 2rpx solid #999;
  311. }
  312. }
  313. }
  314. }
  315. }
  316. .top-controls {
  317. position: fixed;
  318. top: 0;
  319. left: 0;
  320. right: 0;
  321. background: rgba(255, 255, 255, 0.98);
  322. padding-top: calc(var(--status-bar-height) + 10rpx);
  323. z-index: 100000;
  324. transform: translateY(0);
  325. transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out, background-color 0.3s ease;
  326. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  327. .controls-inner {
  328. display: flex;
  329. align-items: center;
  330. justify-content: space-between;
  331. padding: 20rpx 32rpx;
  332. position: relative;
  333. .left {
  334. width: 100rpx;
  335. display: flex;
  336. justify-content: flex-start;
  337. align-items: center;
  338. }
  339. .center {
  340. flex: 1;
  341. display: flex;
  342. flex-direction: column;
  343. align-items: center;
  344. justify-content: center;
  345. .title {
  346. font-size: 32rpx;
  347. font-weight: 500;
  348. color: #333;
  349. margin-bottom: 4rpx;
  350. white-space: nowrap;
  351. overflow: hidden;
  352. text-overflow: ellipsis;
  353. max-width: 320rpx;
  354. }
  355. .chapter {
  356. font-size: 24rpx;
  357. color: #666;
  358. white-space: nowrap;
  359. overflow: hidden;
  360. text-overflow: ellipsis;
  361. max-width: 320rpx;
  362. }
  363. }
  364. .right {
  365. width: 100rpx;
  366. display: flex;
  367. justify-content: flex-end;
  368. align-items: center;
  369. }
  370. }
  371. .progress-bar {
  372. height: 4rpx;
  373. background: #f0f0f0;
  374. width: 100%;
  375. position: relative;
  376. .progress-inner {
  377. height: 100%;
  378. background: #4a90e2;
  379. transition: width 0.3s;
  380. }
  381. }
  382. &.top-controls-hidden {
  383. transform: translateY(-100%);
  384. opacity: 0;
  385. }
  386. }
  387. .chapter-content {
  388. flex: 1;
  389. padding: 0 32rpx;
  390. font-size: 28rpx;
  391. color: #222;
  392. line-height: 2.2;
  393. padding-top: 160rpx;
  394. /* 为导航栏预留空间,不随状态变化 */
  395. padding-bottom: 180rpx;
  396. width: 100%;
  397. box-sizing: border-box;
  398. overflow-x: hidden;
  399. transition: color 0.3s ease, background-color 0.3s ease;
  400. .chapter-content-item {
  401. width: 100%;
  402. .chapter-title {
  403. font-size: 36rpx;
  404. font-weight: bold;
  405. margin: 20rpx 0 40rpx 0;
  406. text-align: center;
  407. word-break: break-word;
  408. white-space: normal;
  409. transition: color 0.3s ease;
  410. }
  411. .paragraph-content {
  412. width: 100%;
  413. .paragraph {
  414. text-indent: 2em;
  415. margin-bottom: 30rpx;
  416. line-height: 1.8;
  417. font-size: 30rpx;
  418. color: #333;
  419. word-wrap: break-word;
  420. word-break: normal;
  421. white-space: normal;
  422. transition: color 0.3s ease;
  423. }
  424. }
  425. }
  426. &.full-content {
  427. /* 不再修改顶部padding,保持内容位置不变 */
  428. }
  429. }
  430. .bottom-bar {
  431. position: fixed;
  432. left: 0;
  433. right: 0;
  434. bottom: 0;
  435. background: #fff;
  436. display: flex;
  437. justify-content: center;
  438. align-items: center;
  439. height: 180rpx;
  440. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
  441. z-index: 100000;
  442. padding: 0 40rpx 10rpx 40rpx;
  443. transform: translateY(0);
  444. transition: transform 0.3s ease-in-out, background-color 0.3s ease;
  445. &.bottom-bar-hidden {
  446. transform: translateY(100%);
  447. }
  448. .bottom-left {
  449. display: flex;
  450. align-items: flex-end;
  451. gap: 48rpx;
  452. .bar-item {
  453. display: flex;
  454. flex-direction: column;
  455. align-items: center;
  456. justify-content: flex-end;
  457. .bar-icon {
  458. width: 48rpx;
  459. height: 48rpx;
  460. margin-bottom: 4rpx;
  461. margin-right: 1rpx;
  462. display: flex;
  463. align-items: center;
  464. justify-content: center;
  465. }
  466. .bar-label {
  467. font-size: 22rpx;
  468. color: #b3b3b3;
  469. margin-top: 2rpx;
  470. transition: color 0.3s ease;
  471. }
  472. }
  473. }
  474. .bottom-right {
  475. display: flex;
  476. align-items: flex-end;
  477. gap: 22rpx;
  478. margin-left: 40rpx;
  479. text-overflow: ellipsis;
  480. .outline-btn {
  481. flex-shrink: 0;
  482. min-width: 110rpx;
  483. padding: 0 26rpx;
  484. height: 60rpx;
  485. line-height: 60rpx;
  486. background: #fff;
  487. color: #223a7a;
  488. border: 2rpx solid #223a7a;
  489. border-radius: 32rpx;
  490. font-size: 26rpx;
  491. font-weight: bold;
  492. margin: 0;
  493. display: flex;
  494. align-items: center;
  495. justify-content: center;
  496. transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
  497. .btn-text {
  498. font-weight: bold;
  499. color: #223a7a;
  500. font-size: 26rpx;
  501. border-bottom: 2rpx solid #223a7a;
  502. padding-bottom: 2rpx;
  503. transition: color 0.3s ease, border-color 0.3s ease;
  504. }
  505. }
  506. }
  507. }
  508. }
  509. </style>