├── php-api/ # 改造后的PHP接口层 ├── java-ad-service/ # 若依框架微服务(广告+VIP+分账) ├── uniapp-reader/ # UniApp前端项目 │ ├── pages/ # 各端页面 │ └──
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

index.vue 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. <template>
  2. <view class="novel-home">
  3. <!-- 顶部导航栏 -->
  4. <view class="header">
  5. <image src="/static/logo.png" class="logo" />
  6. <view class="search-box" @click="goToSearch">
  7. <uni-icons type="search" size="18" color="#999" />
  8. <text class="placeholder">搜索书名或作者</text>
  9. </view>
  10. <view class="user-icon" @click="goToUserCenter">
  11. <uni-icons type="person" size="24" color="#333" />
  12. </view>
  13. </view>
  14. <!-- 轮播图 -->
  15. <swiper class="banner" :autoplay="true" :interval="3000" circular>
  16. <swiper-item v-for="(item, index) in banners" :key="index">
  17. <image :src="item.image" mode="aspectFill" @click="readNovel(item.novelId)" />
  18. </swiper-item>
  19. </swiper>
  20. <!-- 分类导航 -->
  21. <view class="category-nav">
  22. <view v-for="category in categories" :key="category.id" class="nav-item">
  23. <image :src="category.icon" class="nav-icon" />
  24. <text>{{ category.name }}</text>
  25. </view>
  26. </view>
  27. <!-- 推荐书单 -->
  28. <view class="section">
  29. <view class="section-header">
  30. <text class="section-title">编辑推荐</text>
  31. <text class="more" @click="goToBookList">更多 ></text>
  32. </view>
  33. <scroll-view scroll-x class="book-list">
  34. <view v-for="book in recommendedBooks" :key="book.id" class="book-item" @click="readNovel(book.id, 1)">
  35. <image :src="book.cover" class="book-cover" />
  36. <text class="book-title">{{ book.title }}</text>
  37. <text class="book-author">{{ book.author }}</text>
  38. </view>
  39. </scroll-view>
  40. </view>
  41. <!-- 热门连载 -->
  42. <view class="section">
  43. <view class="section-header">
  44. <text class="section-title">热门连载</text>
  45. </view>
  46. <view v-for="book in serialBooks" :key="book.id" class="book-row" @click="readNovel(book.id, 1)">
  47. <image :src="book.cover" class="row-cover" />
  48. <view class="book-info">
  49. <text class="row-title">{{ book.title }}</text>
  50. <text class="row-author">{{ book.author }}</text>
  51. <text class="row-desc">{{ book.description }}</text>
  52. <view class="row-tags">
  53. <text v-for="tag in book.tags" :key="tag" class="tag">{{ tag }}</text>
  54. </view>
  55. </view>
  56. </view>
  57. </view>
  58. <!-- 阅读登录提示组件 -->
  59. <login-prompt v-if="showLoginPrompt" @login="goToLogin" @continue="continueReading" />
  60. </view>
  61. </template>
  62. <script>
  63. import novelService from '@/services/novelService'
  64. export default {
  65. data() {
  66. return {
  67. banners: [],
  68. categories: [],
  69. recommendedBooks: [],
  70. serialBooks: [],
  71. currentReading: null,
  72. //showLoginPrompt: false,
  73. maxFreeChapters: 5,
  74. freeChaptersRead: 0,
  75. currentChapter: 1, // 当前阅读章节
  76. maxFreeChapters: 5, // 最大免费章节数
  77. readingTime: 0, // 阅读时长(秒)
  78. timer: null
  79. }
  80. },
  81. async onLoad() {
  82. await this.loadHomeData()
  83. },
  84. mounted() {
  85. // 开始阅读计时
  86. this.timer = setInterval(() => {
  87. this.readingTime++
  88. }, 1000)
  89. },
  90. beforeDestroy() {
  91. // 清除计时器
  92. clearInterval(this.timer)
  93. },
  94. computed: {
  95. // 计算显示哪些章节
  96. visibleChapters() {
  97. return this.chapters.filter(chap => chap.id <= this.currentChapter)
  98. },
  99. // 是否显示登录提示
  100. showLoginPrompt() {
  101. return this.currentChapter > this.maxFreeChapters && !this.$store.getters.token
  102. },
  103. // 剩余免费章节数
  104. remainingFreeChapters() {
  105. return this.maxFreeChapters - this.currentChapter
  106. },
  107. // 总章节数
  108. totalChapters() {
  109. return this.chapters.length
  110. }
  111. },
  112. onLoad() {
  113. // 从本地存储获取阅读进度
  114. const progress = uni.getStorageSync('readingProgress') || {}
  115. this.currentReading = progress.currentReading || null
  116. this.freeChaptersRead = progress.freeChaptersRead || 0
  117. // 如果用户正在阅读小说,显示继续阅读提示
  118. if (this.currentReading) {
  119. setTimeout(() => {
  120. uni.showToast({
  121. title: `继续阅读《${this.currentReading.title}》`,
  122. icon: 'none',
  123. duration: 3000
  124. })
  125. }, 1000)
  126. }
  127. },
  128. methods: {
  129. async loadHomeData() {
  130. uni.showLoading({ title: '加载中...' })
  131. try {
  132. const homeData = await novelService.getHomeRecommend()
  133. this.banners = homeData.banners || []
  134. this.categories = homeData.categories || []
  135. this.recommendedBooks = homeData.recommended || []
  136. this.serialBooks = homeData.serializing || []
  137. } catch (error) {
  138. uni.showToast({
  139. title: '加载失败',
  140. icon: 'none'
  141. })
  142. } finally {
  143. uni.hideLoading()
  144. }
  145. },
  146. // 开始/继续阅读小说
  147. readNovel(novelId, chapterId = 1) {
  148. // 查找小说信息
  149. const novel = this.findNovelById(novelId)
  150. // 检查是否需要登录提示
  151. if (this.freeChaptersRead >= this.maxFreeChapters && !this.$store.getters.token) {
  152. this.showLoginPrompt = true
  153. this.currentReading = {
  154. id: novelId,
  155. title: novel.title,
  156. chapterId
  157. }
  158. return
  159. }
  160. // 记录阅读进度
  161. this.recordReadingProgress(novelId, chapterId)
  162. // 跳转到阅读页
  163. uni.navigateTo({
  164. url: `/pages/novel/reader?novelId=${novelId}&chapterId=${chapterId}`
  165. })
  166. },
  167. // 继续阅读(临时允许)
  168. continueReading() {
  169. if (this.currentReading) {
  170. // 增加已读章节计数
  171. this.freeChaptersRead += 1
  172. // 保存进度
  173. this.recordReadingProgress(this.currentReading.id, this.currentReading.chapterId)
  174. // 跳转到阅读页
  175. uni.navigateTo({
  176. url: `/pages/novel/reader?novelId=${this.currentReading.id}&chapterId=${this.currentReading.chapterId}`
  177. })
  178. this.showLoginPrompt = false
  179. }
  180. },
  181. // 记录阅读进度
  182. recordReadingProgress(novelId, chapterId) {
  183. const novel = this.findNovelById(novelId)
  184. // 更新当前阅读
  185. this.currentReading = {
  186. id: novelId,
  187. title: novel.title,
  188. chapterId
  189. }
  190. // 更新已读免费章节数
  191. if (!this.$store.getters.token) {
  192. this.freeChaptersRead += 1
  193. }
  194. // 保存到本地存储
  195. uni.setStorageSync('readingProgress', {
  196. currentReading: this.currentReading,
  197. freeChaptersRead: this.freeChaptersRead
  198. })
  199. },
  200. // 根据ID查找小说
  201. findNovelById(id) {
  202. // 在实际应用中,这里应该调用API获取小说详情
  203. // 这里简化为在所有书籍中查找
  204. const allBooks = [...this.recommendedBooks, ...this.serialBooks]
  205. return allBooks.find(book => book.id === id) || { title: '未知小说' }
  206. },
  207. goToSearch() {
  208. uni.navigateTo({ url: '/pages/search/index' })
  209. },
  210. goToUserCenter() {
  211. if (this.$store.getters.token) {
  212. uni.navigateTo({ url: '/pages/user/index' })
  213. } else {
  214. uni.navigateTo({ url: '/pages/login/index' })
  215. }
  216. },
  217. goToBookList() {
  218. uni.navigateTo({ url: '/pages/book/list' })
  219. },
  220. goToLogin() {
  221. uni.navigateTo({ url: '/pages/login/index' })
  222. },
  223. // 下一章
  224. nextChapter() {
  225. if (this.currentChapter < this.totalChapters) {
  226. this.currentChapter++
  227. // 阅读到第五章时提示
  228. if (this.currentChapter === this.maxFreeChapters) {
  229. uni.showToast({
  230. title: '免费章节已读完,登录后继续',
  231. icon: 'none',
  232. duration: 3000
  233. })
  234. }
  235. }
  236. },
  237. // 根据阅读时长奖励金币
  238. rewardReadingTime() {
  239. const minutes = Math.floor(this.readingTime / 60)
  240. if (minutes > 0 && minutes % 5 === 0) {
  241. const coins = minutes / 5
  242. this.$store.commit('addCoins', coins)
  243. uni.showToast({ title: `阅读奖励: ${coins}金币` })
  244. }
  245. },
  246. // 跳转登录页
  247. goToLogin() {
  248. uni.navigateTo({
  249. url: '/pages/login/index'
  250. })
  251. },
  252. // 继续阅读(临时允许阅读剩余免费章节)
  253. continueReading() {
  254. if (this.remainingFreeChapters > 0) {
  255. this.nextChapter()
  256. }
  257. }
  258. },
  259. onUnload() {
  260. // 保存阅读进度
  261. uni.setStorage({
  262. key: `readingProgress_${this.novelId}`,
  263. data: {
  264. chapterId: this.chapterId,
  265. progress: this.progress,
  266. lastReadTime: new Date().getTime()
  267. }
  268. });
  269. // 更新已读章节数
  270. const readChapters = uni.getStorageSync('readChapters') || 0;
  271. uni.setStorageSync('readChapters', readChapters + 1);
  272. },
  273. unlockChapter() {
  274. if (this.currentChapter > this.maxFreeChapters) {
  275. if (this.$store.getters.vipLevel > 0) {
  276. // VIP用户直接解锁
  277. return true
  278. } else if (this.$store.getters.coins > 10) {
  279. // 消耗金币解锁
  280. this.$store.commit('deductCoins', 10)
  281. return true
  282. } else {
  283. // 提示获取金币方式
  284. uni.showModal({
  285. title: '解锁章节',
  286. content: '观看广告可获取金币解锁本章节',
  287. confirmText: '观看广告',
  288. success: () => {
  289. this.watchAdToUnlock()
  290. }
  291. })
  292. return false
  293. }
  294. }
  295. return true
  296. },
  297. watchAdToUnlock() {
  298. // 实现广告观看逻辑
  299. // 观看成功后增加金币
  300. this.$store.commit('addCoins', 5)
  301. uni.showToast({ title: '获得5金币' })
  302. }
  303. }
  304. </script>
  305. </script>
  306. <style lang="scss">
  307. .novel-home {
  308. padding: 20rpx;
  309. background-color: #f5f5f5;
  310. min-height: 100vh;
  311. padding-bottom: 100rpx;
  312. }
  313. .header {
  314. display: flex;
  315. align-items: center;
  316. padding: 20rpx;
  317. background: white;
  318. .logo {
  319. width: 120rpx;
  320. height: 60rpx;
  321. margin-right: 20rpx;
  322. }
  323. .search-box {
  324. flex: 1;
  325. background: #f0f0f0;
  326. border-radius: 30rpx;
  327. padding: 15rpx 25rpx;
  328. display: flex;
  329. align-items: center;
  330. .placeholder {
  331. color: #999;
  332. font-size: 28rpx;
  333. margin-left: 10rpx;
  334. }
  335. }
  336. .user-icon {
  337. width: 60rpx;
  338. height: 60rpx;
  339. display: flex;
  340. align-items: center;
  341. justify-content: center;
  342. margin-left: 20rpx;
  343. }
  344. }
  345. .banner {
  346. height: 300rpx;
  347. margin: 20rpx 0;
  348. border-radius: 16rpx;
  349. overflow: hidden;
  350. image {
  351. width: 100%;
  352. height: 100%;
  353. }
  354. }
  355. .category-nav {
  356. display: flex;
  357. justify-content: space-around;
  358. background: white;
  359. border-radius: 16rpx;
  360. padding: 30rpx 0;
  361. margin-bottom: 30rpx;
  362. .nav-item {
  363. display: flex;
  364. flex-direction: column;
  365. align-items: center;
  366. .nav-icon {
  367. width: 80rpx;
  368. height: 80rpx;
  369. margin-bottom: 15rpx;
  370. }
  371. text {
  372. font-size: 24rpx;
  373. color: #666;
  374. }
  375. }
  376. }
  377. .section {
  378. background: white;
  379. border-radius: 16rpx;
  380. padding: 25rpx;
  381. margin-bottom: 30rpx;
  382. .section-header {
  383. display: flex;
  384. justify-content: space-between;
  385. align-items: center;
  386. margin-bottom: 25rpx;
  387. .section-title {
  388. font-size: 32rpx;
  389. font-weight: bold;
  390. color: #333;
  391. }
  392. .more {
  393. font-size: 26rpx;
  394. color: #999;
  395. }
  396. }
  397. }
  398. .book-list {
  399. white-space: nowrap;
  400. .book-item {
  401. display: inline-block;
  402. width: 180rpx;
  403. margin-right: 25rpx;
  404. vertical-align: top;
  405. .book-cover {
  406. width: 180rpx;
  407. height: 240rpx;
  408. border-radius: 8rpx;
  409. }
  410. .book-title {
  411. display: block;
  412. font-size: 26rpx;
  413. font-weight: bold;
  414. margin-top: 15rpx;
  415. white-space: nowrap;
  416. overflow: hidden;
  417. text-overflow: ellipsis;
  418. }
  419. .book-author {
  420. display: block;
  421. font-size: 24rpx;
  422. color: #999;
  423. white-space: nowrap;
  424. overflow: hidden;
  425. text-overflow: ellipsis;
  426. }
  427. }
  428. }
  429. .book-row {
  430. display: flex;
  431. padding: 25rpx 0;
  432. border-bottom: 1rpx solid #eee;
  433. &:last-child {
  434. border-bottom: none;
  435. }
  436. .row-cover {
  437. width: 160rpx;
  438. height: 210rpx;
  439. border-radius: 8rpx;
  440. margin-right: 25rpx;
  441. }
  442. .book-info {
  443. flex: 1;
  444. display: flex;
  445. flex-direction: column;
  446. .row-title {
  447. font-size: 30rpx;
  448. font-weight: bold;
  449. margin-bottom: 10rpx;
  450. }
  451. .row-author {
  452. font-size: 26rpx;
  453. color: #666;
  454. margin-bottom: 15rpx;
  455. }
  456. .row-desc {
  457. font-size: 26rpx;
  458. color: #666;
  459. display: -webkit-box;
  460. -webkit-box-orient: vertical;
  461. -webkit-line-clamp: 2;
  462. overflow: hidden;
  463. margin-bottom: 15rpx;
  464. }
  465. }
  466. .row-tags {
  467. display: flex;
  468. flex-wrap: wrap;
  469. .tag {
  470. font-size: 22rpx;
  471. color: #e74c3c;
  472. border: 1rpx solid #e74c3c;
  473. border-radius: 20rpx;
  474. padding: 5rpx 15rpx;
  475. margin-right: 15rpx;
  476. margin-bottom: 10rpx;
  477. }
  478. }
  479. }
  480. </style>