小说小程序前端代码仓库(小程序)
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.

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