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

478 lines
14 KiB

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