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

494 lines
11 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 class="chapter-content" :class="{'full-content': isFullScreen}" @scroll="handleScroll"
  22. @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" @subscribe="goToSubscription" />
  61. <novelVotePopup ref="novelVotePopup" />
  62. <chapterPopup ref="chapterPopup" :chapterList="chapterList" :currentIndex="currentIndex"
  63. @selectChapter="selectChapter" />
  64. </view>
  65. </template>
  66. <script>
  67. import chapterPopup from '../components/novel/chapterPopup.vue'
  68. import novelVotePopup from '../components/novel/novelVotePopup.vue'
  69. import subscriptionPopup from '../components/novel/subscriptionPopup.vue'
  70. import themeMixin from '@/mixins/themeMode.js' // 导入主题混合器
  71. export default {
  72. components: {
  73. chapterPopup,
  74. novelVotePopup,
  75. subscriptionPopup,
  76. },
  77. mixins: [themeMixin], // 使用主题混合器
  78. data() {
  79. return {
  80. isFullScreen: false,
  81. popupShown: false, // 只弹一次
  82. currentChapter: "",
  83. readProgress: 15, // 阅读进度百分比
  84. paragraphs: [],
  85. id: 0,
  86. cid: 0,
  87. novelData: {},
  88. chapterList: [],
  89. }
  90. },
  91. computed: {
  92. currentIndex() {
  93. for (var index = 0; index < this.chapterList.length; index++) {
  94. var element = this.chapterList[index];
  95. if (element.id == this.cid) return index
  96. }
  97. return -1
  98. },
  99. },
  100. onLoad({
  101. id,
  102. cid
  103. }) {
  104. this.id = id
  105. this.cid = cid
  106. this.getDateil()
  107. this.getBookCatalogDetail()
  108. },
  109. onShow() {
  110. this.getBookCatalogList()
  111. },
  112. mounted() {
  113. // 初始设置为全屏模式
  114. this.isFullScreen = true;
  115. },
  116. methods: {
  117. getDateil() {
  118. this.$fetch('getBookDetail', {
  119. id: this.id
  120. }).then(res => {
  121. this.novelData = res
  122. })
  123. },
  124. getBookCatalogDetail() {
  125. this.$fetch('getBookCatalogDetail', {
  126. id: this.cid
  127. }).then(res => {
  128. this.paragraphs = res.details && res.details.split('\n')
  129. this.currentChapter = res.title
  130. })
  131. },
  132. getBookCatalogList() {
  133. this.$fetch('getBookCatalogList', {
  134. bookId: this.id,
  135. pageNo: 1,
  136. pageSize: 9999999,
  137. reverse: 0,
  138. }).then(res => {
  139. this.chapterList = res.records
  140. this.catalog = res.records[res.records.length - 1]
  141. this.fastCatalog = res.records[0]
  142. })
  143. },
  144. handleContentClick() {
  145. this.toggleFullScreen();
  146. },
  147. handleScroll(e) {
  148. // 获取滚动位置
  149. const scrollTop = e.detail.scrollTop;
  150. // 滚动时触发订阅弹窗
  151. if (scrollTop > 50 && !this.popupShown) {
  152. this.$refs.subscriptionPopup.open();
  153. this.popupShown = true;
  154. }
  155. },
  156. toggleFullScreen() {
  157. this.isFullScreen = !this.isFullScreen
  158. },
  159. goToSubscription() {
  160. uni.navigateTo({
  161. url: '/pages_order/novel/SubscriptionInformation'
  162. })
  163. },
  164. selectChapter({
  165. item,
  166. index
  167. }) {
  168. this.cid = item.id
  169. this.isFullScreen = true
  170. this.getBookCatalogDetail()
  171. },
  172. nextChapter(next) {
  173. let index = this.currentIndex + next
  174. if(index < 0 || index >= this.chapterList.length){
  175. uni.showToast({
  176. title: '到底了',
  177. icon: 'none'
  178. })
  179. return
  180. }
  181. this.cid = this.chapterList[index].id
  182. this.isFullScreen = true
  183. this.getBookCatalogDetail()
  184. },
  185. },
  186. }
  187. </script>
  188. <style lang="scss" scoped>
  189. .reader-container {
  190. min-height: 100vh;
  191. background: #fff;
  192. display: flex;
  193. flex-direction: column;
  194. position: relative;
  195. overflow: hidden;
  196. &.dark-mode {
  197. background: #1a1a1a;
  198. .top-controls {
  199. background: rgba(34, 34, 34, 0.98);
  200. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.2);
  201. .controls-inner {
  202. .center {
  203. .title {
  204. color: #eee;
  205. }
  206. .chapter {
  207. color: #bbb;
  208. }
  209. }
  210. }
  211. .progress-bar {
  212. background: #333;
  213. .progress-inner {
  214. background: #4a90e2;
  215. }
  216. }
  217. }
  218. .chapter-content {
  219. color: #ccc;
  220. .chapter-content-item {
  221. .chapter-title {
  222. color: #eee;
  223. }
  224. .paragraph-content {
  225. .paragraph {
  226. color: #bbb;
  227. }
  228. }
  229. }
  230. }
  231. .bottom-bar {
  232. background: #222;
  233. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.2);
  234. .bottom-left {
  235. .bar-item {
  236. .bar-label {
  237. color: #999;
  238. }
  239. }
  240. }
  241. .bottom-right {
  242. .outline-btn {
  243. background: #222;
  244. color: #4a90e2;
  245. border: 2rpx solid #4a90e2;
  246. .btn-text {
  247. color: #4a90e2;
  248. border-bottom: 2rpx solid #4a90e2;
  249. }
  250. }
  251. }
  252. }
  253. }
  254. .top-controls {
  255. position: fixed;
  256. top: 0;
  257. left: 0;
  258. right: 0;
  259. background: rgba(255, 255, 255, 0.98);
  260. padding-top: calc(var(--status-bar-height) + 10rpx);
  261. z-index: 100;
  262. transform: translateY(0);
  263. transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out, background-color 0.3s ease;
  264. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  265. .controls-inner {
  266. display: flex;
  267. align-items: center;
  268. justify-content: space-between;
  269. padding: 20rpx 32rpx;
  270. position: relative;
  271. .left {
  272. width: 100rpx;
  273. display: flex;
  274. justify-content: flex-start;
  275. align-items: center;
  276. }
  277. .center {
  278. flex: 1;
  279. display: flex;
  280. flex-direction: column;
  281. align-items: center;
  282. justify-content: center;
  283. .title {
  284. font-size: 32rpx;
  285. font-weight: 500;
  286. color: #333;
  287. margin-bottom: 4rpx;
  288. white-space: nowrap;
  289. overflow: hidden;
  290. text-overflow: ellipsis;
  291. max-width: 320rpx;
  292. }
  293. .chapter {
  294. font-size: 24rpx;
  295. color: #666;
  296. white-space: nowrap;
  297. overflow: hidden;
  298. text-overflow: ellipsis;
  299. max-width: 320rpx;
  300. }
  301. }
  302. .right {
  303. width: 100rpx;
  304. display: flex;
  305. justify-content: flex-end;
  306. align-items: center;
  307. }
  308. }
  309. .progress-bar {
  310. height: 4rpx;
  311. background: #f0f0f0;
  312. width: 100%;
  313. position: relative;
  314. .progress-inner {
  315. height: 100%;
  316. background: #4a90e2;
  317. transition: width 0.3s;
  318. }
  319. }
  320. &.top-controls-hidden {
  321. transform: translateY(-100%);
  322. opacity: 0;
  323. }
  324. }
  325. .chapter-content {
  326. flex: 1;
  327. padding: 0 32rpx;
  328. font-size: 28rpx;
  329. color: #222;
  330. line-height: 2.2;
  331. padding-top: 160rpx;
  332. /* 为导航栏预留空间,不随状态变化 */
  333. padding-bottom: 180rpx;
  334. width: 100%;
  335. box-sizing: border-box;
  336. overflow-x: hidden;
  337. transition: color 0.3s ease, background-color 0.3s ease;
  338. .chapter-content-item {
  339. width: 100%;
  340. .chapter-title {
  341. font-size: 36rpx;
  342. font-weight: bold;
  343. margin: 20rpx 0 40rpx 0;
  344. text-align: center;
  345. word-break: break-word;
  346. white-space: normal;
  347. transition: color 0.3s ease;
  348. }
  349. .paragraph-content {
  350. width: 100%;
  351. .paragraph {
  352. text-indent: 2em;
  353. margin-bottom: 30rpx;
  354. line-height: 1.8;
  355. font-size: 30rpx;
  356. color: #333;
  357. word-wrap: break-word;
  358. word-break: normal;
  359. white-space: normal;
  360. transition: color 0.3s ease;
  361. }
  362. }
  363. }
  364. &.full-content {
  365. /* 不再修改顶部padding,保持内容位置不变 */
  366. }
  367. }
  368. .bottom-bar {
  369. position: fixed;
  370. left: 0;
  371. right: 0;
  372. bottom: 0;
  373. background: #fff;
  374. display: flex;
  375. justify-content: center;
  376. align-items: center;
  377. height: 180rpx;
  378. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
  379. z-index: 10;
  380. padding: 0 40rpx 10rpx 40rpx;
  381. transform: translateY(0);
  382. transition: transform 0.3s ease-in-out, background-color 0.3s ease;
  383. &.bottom-bar-hidden {
  384. transform: translateY(100%);
  385. }
  386. .bottom-left {
  387. display: flex;
  388. align-items: flex-end;
  389. gap: 48rpx;
  390. .bar-item {
  391. display: flex;
  392. flex-direction: column;
  393. align-items: center;
  394. justify-content: flex-end;
  395. .bar-icon {
  396. width: 48rpx;
  397. height: 48rpx;
  398. margin-bottom: 4rpx;
  399. margin-right: 1rpx;
  400. display: flex;
  401. align-items: center;
  402. justify-content: center;
  403. }
  404. .bar-label {
  405. font-size: 22rpx;
  406. color: #b3b3b3;
  407. margin-top: 2rpx;
  408. transition: color 0.3s ease;
  409. }
  410. }
  411. }
  412. .bottom-right {
  413. display: flex;
  414. align-items: flex-end;
  415. gap: 32rpx;
  416. margin-left: 40rpx;
  417. .outline-btn {
  418. min-width: 110rpx;
  419. padding: 0 28rpx;
  420. height: 60rpx;
  421. line-height: 60rpx;
  422. background: #fff;
  423. color: #223a7a;
  424. border: 2rpx solid #223a7a;
  425. border-radius: 32rpx;
  426. font-size: 28rpx;
  427. font-weight: bold;
  428. margin: 0;
  429. display: flex;
  430. align-items: center;
  431. justify-content: center;
  432. transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
  433. .btn-text {
  434. font-weight: bold;
  435. color: #223a7a;
  436. font-size: 28rpx;
  437. border-bottom: 2rpx solid #223a7a;
  438. padding-bottom: 2rpx;
  439. transition: color 0.3s ease, border-color 0.3s ease;
  440. }
  441. }
  442. }
  443. }
  444. }
  445. </style>