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

462 lines
14 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">
  7. <uv-icon name="arrow-left" @click="$utils.navigateBack" :color="isDarkMode ? '#ccc' : '#333'" size="46rpx"></uv-icon>
  8. </view>
  9. <view class="center">
  10. <text class="title">{{ novelTitle }}</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}"
  22. @scroll="handleScroll" @tap="handleContentClick">
  23. <view class="chapter-content-item">
  24. <view class="chapter-title">第1章 重回2004</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"><text class="btn-text">上一章</text></button>
  47. <button class="outline-btn" @click="$refs.chapterPopup.open()"><text class="btn-text">目录</text></button>
  48. <button class="outline-btn"><text class="btn-text">下一章</text></button>
  49. </view>
  50. </view>
  51. <!-- 使用封装的订阅弹窗组件 -->
  52. <subscriptionPopup ref="subscriptionPopup" @subscribe="goToSubscription"/>
  53. <novelVotePopup ref="novelVotePopup"/>
  54. <chapterPopup ref="chapterPopup" />
  55. </view>
  56. </template>
  57. <script>
  58. import chapterPopup from '../components/novel/chapterPopup.vue'
  59. import novelVotePopup from '../components/novel/novelVotePopup.vue'
  60. import subscriptionPopup from '../components/novel/subscriptionPopup.vue'
  61. import themeMixin from '@/mixins/themeMode.js' // 导入主题混合器
  62. export default {
  63. components: {
  64. chapterPopup,
  65. novelVotePopup,
  66. subscriptionPopup,
  67. },
  68. mixins: [themeMixin], // 使用主题混合器
  69. data() {
  70. return {
  71. isFullScreen: false,
  72. popupShown: false, // 只弹一次
  73. novelTitle: "这游戏也太真实了",
  74. currentChapter: "第1章 重回2004",
  75. readProgress: 15, // 阅读进度百分比
  76. paragraphs: [
  77. "华东地区某个不知名街区,2004年冬。",
  78. "天还没有亮,王明就起床了。他要去赶早市,今天是进货的日子,错过了就要等下周。他轻轻地穿好衣服,不想吵醒还在熟睡的妻子。",
  79. "天气比想象中冷,他裹紧了身上那件略显破旧的棉袄。出门前,他看了眼床头那个旧闹钟,四点半,还算准时。",
  80. "街上几乎没有人,只有零星的几辆三轮车和面包车正往市场方向驶去。王明加快了脚步,他知道好位置都是先到先得。",
  81. "这一年,互联网刚刚开始在中国普及,但对于像王明这样的小摊贩来说,生活并没有什么变化。每天起早贪黑,挣扎在温饱线上。",
  82. "然而就在今天,他的生活将迎来一场他从未预料到的变化。",
  83. "市场入口处,一个陌生人递给他一张名片,上面写着:\"电子产品批发,价格优惠\"。王明随手接过,塞进了口袋,继续往里走。",
  84. "他不知道的是,这张小小的名片,将成为改变他命运的第一步。",
  85. "几个小时后,当他收摊准备回家时,他偶然摸到了那张名片。出于好奇,他决定去看看。",
  86. "名片上的地址在城市的另一边,是一个他从未去过的工业区。坐了将近一个小时的公交车,他终于找到了那个地方。",
  87. "那是一个不起眼的仓库,门口停着几辆货车。王明犹豫了一下,还是推门走了进去。",
  88. "里面的景象让他震惊。货架上整齐地摆放着各种电子产品:MP3播放器、数码相机、U盘……这些在当时都是新奇而昂贵的物品。",
  89. "\"您是新顾客吧?\"一个中年男人走过来,热情地招呼道。",
  90. "\"是的,我看到了你的名片。\"王明有些拘谨地回答。",
  91. "\"那您来得正是时候,我们刚收到一批新货,价格特别优惠。\"",
  92. "王明被带到一个展示台前,上面摆着几个小巧的设备。\"这是最新款的MP3,容量大,音质好,在市场上很受欢迎。\"",
  93. "王明拿起一个仔细端详。他虽然没什么文化,但做生意的直觉告诉他,这东西可能有市场。",
  94. "\"多少钱一个?\"他问道。",
  95. "\"批发价150元,零售价可以卖到300元以上。\"",
  96. "王明心里快速计算着。他今天的存款只有3000元,如果全买这个,可以拿20个。要是真能卖出去,就是3000元的利润。",
  97. "但风险也很大。万一卖不出去,这可是他半年的积蓄啊。",
  98. "就在他犹豫的时候,旁边传来一个熟悉的声音:\"老王,你也来这进货啊?\"",
  99. "是他在市场上认识的李东。李东比他年轻,做生意也比他精明。",
  100. "\"你觉得这东西怎么样?\"王明问道。",
  101. "\"那必须相当不错啊。我上周进了一批,三天就卖光了。现在是过节嘛,年轻人喜欢这些新鲜玩意儿。\"李东拍了拍他的肩膀,\"要我说,你应该赶紧进一批,过了这个村可就没这个店了。\"",
  102. "看到李东的信心,王明心里的天平开始倾斜。",
  103. "\"那...好吧,给我来20个。\"他终于下定决心,从口袋里掏出了钱。",
  104. "这一决定,将彻底改变他的人生轨迹...",
  105. ]
  106. }
  107. },
  108. methods: {
  109. handleContentClick() {
  110. this.toggleFullScreen();
  111. },
  112. handleScroll(e) {
  113. // 获取滚动位置
  114. const scrollTop = e.detail.scrollTop;
  115. // 滚动时触发订阅弹窗
  116. if (scrollTop > 50 && !this.popupShown) {
  117. this.$refs.subscriptionPopup.open();
  118. this.popupShown = true;
  119. }
  120. },
  121. toggleFullScreen() {
  122. this.isFullScreen = !this.isFullScreen
  123. },
  124. goToSubscription() {
  125. uni.navigateTo({
  126. url: '/pages_order/novel/SubscriptionInformation'
  127. })
  128. },
  129. },
  130. mounted() {
  131. // 初始设置为全屏模式
  132. this.isFullScreen = true;
  133. // #ifdef H5
  134. if (typeof window !== 'undefined') {
  135. window.onscroll = () => {
  136. const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
  137. if (scrollTop > 50 && !this.popupShown) {
  138. this.$refs.subscriptionPopup.open();
  139. this.popupShown = true;
  140. }
  141. };
  142. }
  143. // #endif
  144. },
  145. beforeDestroy() {
  146. // #ifdef H5
  147. if (typeof window !== 'undefined') {
  148. window.onscroll = null;
  149. }
  150. // #endif
  151. },
  152. onLoad() {
  153. // 可接收小说id、章节id等参数
  154. }
  155. }
  156. </script>
  157. <style lang="scss" scoped>
  158. .reader-container {
  159. min-height: 100vh;
  160. background: #fff;
  161. display: flex;
  162. flex-direction: column;
  163. position: relative;
  164. overflow: hidden;
  165. &.dark-mode {
  166. background: #1a1a1a;
  167. .top-controls {
  168. background: rgba(34, 34, 34, 0.98);
  169. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.2);
  170. .controls-inner {
  171. .center {
  172. .title {
  173. color: #eee;
  174. }
  175. .chapter {
  176. color: #bbb;
  177. }
  178. }
  179. }
  180. .progress-bar {
  181. background: #333;
  182. .progress-inner {
  183. background: #4a90e2;
  184. }
  185. }
  186. }
  187. .chapter-content {
  188. color: #ccc;
  189. .chapter-content-item {
  190. .chapter-title {
  191. color: #eee;
  192. }
  193. .paragraph-content {
  194. .paragraph {
  195. color: #bbb;
  196. }
  197. }
  198. }
  199. }
  200. .bottom-bar {
  201. background: #222;
  202. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.2);
  203. .bottom-left {
  204. .bar-item {
  205. .bar-label {
  206. color: #999;
  207. }
  208. }
  209. }
  210. .bottom-right {
  211. .outline-btn {
  212. background: #222;
  213. color: #4a90e2;
  214. border: 2rpx solid #4a90e2;
  215. .btn-text {
  216. color: #4a90e2;
  217. border-bottom: 2rpx solid #4a90e2;
  218. }
  219. }
  220. }
  221. }
  222. }
  223. .top-controls {
  224. position: fixed;
  225. top: 0;
  226. left: 0;
  227. right: 0;
  228. background: rgba(255, 255, 255, 0.98);
  229. padding-top: calc(var(--status-bar-height) + 10rpx);
  230. z-index: 100;
  231. transform: translateY(0);
  232. transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out, background-color 0.3s ease;
  233. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  234. .controls-inner {
  235. display: flex;
  236. align-items: center;
  237. justify-content: space-between;
  238. padding: 20rpx 32rpx;
  239. position: relative;
  240. .left {
  241. width: 100rpx;
  242. display: flex;
  243. justify-content: flex-start;
  244. align-items: center;
  245. }
  246. .center {
  247. flex: 1;
  248. display: flex;
  249. flex-direction: column;
  250. align-items: center;
  251. justify-content: center;
  252. .title {
  253. font-size: 32rpx;
  254. font-weight: 500;
  255. color: #333;
  256. margin-bottom: 4rpx;
  257. white-space: nowrap;
  258. overflow: hidden;
  259. text-overflow: ellipsis;
  260. max-width: 320rpx;
  261. }
  262. .chapter {
  263. font-size: 24rpx;
  264. color: #666;
  265. white-space: nowrap;
  266. overflow: hidden;
  267. text-overflow: ellipsis;
  268. max-width: 320rpx;
  269. }
  270. }
  271. .right {
  272. width: 100rpx;
  273. display: flex;
  274. justify-content: flex-end;
  275. align-items: center;
  276. }
  277. }
  278. .progress-bar {
  279. height: 4rpx;
  280. background: #f0f0f0;
  281. width: 100%;
  282. position: relative;
  283. .progress-inner {
  284. height: 100%;
  285. background: #4a90e2;
  286. transition: width 0.3s;
  287. }
  288. }
  289. &.top-controls-hidden {
  290. transform: translateY(-100%);
  291. opacity: 0;
  292. }
  293. }
  294. .chapter-content {
  295. flex: 1;
  296. padding: 0 32rpx;
  297. font-size: 28rpx;
  298. color: #222;
  299. line-height: 2.2;
  300. padding-top: 160rpx; /* 为导航栏预留空间,不随状态变化 */
  301. padding-bottom: 180rpx;
  302. width: 100%;
  303. box-sizing: border-box;
  304. overflow-x: hidden;
  305. transition: color 0.3s ease, background-color 0.3s ease;
  306. .chapter-content-item {
  307. width: 100%;
  308. .chapter-title {
  309. font-size: 36rpx;
  310. font-weight: bold;
  311. margin: 20rpx 0 40rpx 0;
  312. text-align: center;
  313. word-break: break-word;
  314. white-space: normal;
  315. transition: color 0.3s ease;
  316. }
  317. .paragraph-content {
  318. width: 100%;
  319. .paragraph {
  320. text-indent: 2em;
  321. margin-bottom: 30rpx;
  322. line-height: 1.8;
  323. font-size: 30rpx;
  324. color: #333;
  325. word-wrap: break-word;
  326. word-break: normal;
  327. white-space: normal;
  328. transition: color 0.3s ease;
  329. }
  330. }
  331. }
  332. &.full-content {
  333. /* 不再修改顶部padding,保持内容位置不变 */
  334. }
  335. }
  336. .bottom-bar {
  337. position: fixed;
  338. left: 0;
  339. right: 0;
  340. bottom: 0;
  341. background: #fff;
  342. display: flex;
  343. justify-content: center;
  344. align-items: center;
  345. height: 180rpx;
  346. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
  347. z-index: 10;
  348. padding: 0 40rpx 10rpx 40rpx;
  349. transform: translateY(0);
  350. transition: transform 0.3s ease-in-out, background-color 0.3s ease;
  351. &.bottom-bar-hidden {
  352. transform: translateY(100%);
  353. }
  354. .bottom-left {
  355. display: flex;
  356. align-items: flex-end;
  357. gap: 48rpx;
  358. .bar-item {
  359. display: flex;
  360. flex-direction: column;
  361. align-items: center;
  362. justify-content: flex-end;
  363. .bar-icon {
  364. width: 48rpx;
  365. height: 48rpx;
  366. margin-bottom: 4rpx;
  367. margin-right: 1rpx;
  368. display: flex;
  369. align-items: center;
  370. justify-content: center;
  371. }
  372. .bar-label {
  373. font-size: 22rpx;
  374. color: #b3b3b3;
  375. margin-top: 2rpx;
  376. transition: color 0.3s ease;
  377. }
  378. }
  379. }
  380. .bottom-right {
  381. display: flex;
  382. align-items: flex-end;
  383. gap: 32rpx;
  384. margin-left: 40rpx;
  385. .outline-btn {
  386. min-width: 110rpx;
  387. padding: 0 28rpx;
  388. height: 60rpx;
  389. line-height: 60rpx;
  390. background: #fff;
  391. color: #223a7a;
  392. border: 2rpx solid #223a7a;
  393. border-radius: 32rpx;
  394. font-size: 28rpx;
  395. font-weight: bold;
  396. margin: 0;
  397. display: flex;
  398. align-items: center;
  399. justify-content: center;
  400. transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
  401. .btn-text {
  402. font-weight: bold;
  403. color: #223a7a;
  404. font-size: 28rpx;
  405. border-bottom: 2rpx solid #223a7a;
  406. padding-bottom: 2rpx;
  407. transition: color 0.3s ease, border-color 0.3s ease;
  408. }
  409. }
  410. }
  411. }
  412. }
  413. </style>