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

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