├── 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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  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' })
  215. }
  216. },
  217. goToBookList() {
  218. uni.navigateTo({ url: '/pages/book/list' })
  219. },
  220. goToLogin() {
  221. uni.navigateTo({ url: '/pages/login' })
  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'
  250. })
  251. },
  252. // 继续阅读(临时允许阅读剩余免费章节)
  253. continueReading() {
  254. if (this.remainingFreeChapters > 0) {
  255. this.nextChapter()
  256. }
  257. }
  258. },
  259. onUnload() {
  260. // 保存阅读进度
  261. uni.setStorageSync('readingProgress', this.currentChapter)
  262. console.log('首页已加载')
  263. },
  264. unlockChapter() {
  265. if (this.currentChapter > this.maxFreeChapters) {
  266. if (this.$store.getters.vipLevel > 0) {
  267. // VIP用户直接解锁
  268. return true
  269. } else if (this.$store.getters.coins > 10) {
  270. // 消耗金币解锁
  271. this.$store.commit('deductCoins', 10)
  272. return true
  273. } else {
  274. // 提示获取金币方式
  275. uni.showModal({
  276. title: '解锁章节',
  277. content: '观看广告可获取金币解锁本章节',
  278. confirmText: '观看广告',
  279. success: () => {
  280. this.watchAdToUnlock()
  281. }
  282. })
  283. return false
  284. }
  285. }
  286. return true
  287. },
  288. watchAdToUnlock() {
  289. // 实现广告观看逻辑
  290. // 观看成功后增加金币
  291. this.$store.commit('addCoins', 5)
  292. uni.showToast({ title: '获得5金币' })
  293. }
  294. }
  295. </script>
  296. </script>
  297. <style lang="scss">
  298. .novel-home {
  299. padding: 20rpx;
  300. background-color: #f5f5f5;
  301. min-height: 100vh;
  302. padding-bottom: 100rpx;
  303. }
  304. .header {
  305. display: flex;
  306. align-items: center;
  307. padding: 20rpx;
  308. background: white;
  309. .logo {
  310. width: 120rpx;
  311. height: 60rpx;
  312. margin-right: 20rpx;
  313. }
  314. .search-box {
  315. flex: 1;
  316. background: #f0f0f0;
  317. border-radius: 30rpx;
  318. padding: 15rpx 25rpx;
  319. display: flex;
  320. align-items: center;
  321. .placeholder {
  322. color: #999;
  323. font-size: 28rpx;
  324. margin-left: 10rpx;
  325. }
  326. }
  327. .user-icon {
  328. width: 60rpx;
  329. height: 60rpx;
  330. display: flex;
  331. align-items: center;
  332. justify-content: center;
  333. margin-left: 20rpx;
  334. }
  335. }
  336. .banner {
  337. height: 300rpx;
  338. margin: 20rpx 0;
  339. border-radius: 16rpx;
  340. overflow: hidden;
  341. image {
  342. width: 100%;
  343. height: 100%;
  344. }
  345. }
  346. .category-nav {
  347. display: flex;
  348. justify-content: space-around;
  349. background: white;
  350. border-radius: 16rpx;
  351. padding: 30rpx 0;
  352. margin-bottom: 30rpx;
  353. .nav-item {
  354. display: flex;
  355. flex-direction: column;
  356. align-items: center;
  357. .nav-icon {
  358. width: 80rpx;
  359. height: 80rpx;
  360. margin-bottom: 15rpx;
  361. }
  362. text {
  363. font-size: 24rpx;
  364. color: #666;
  365. }
  366. }
  367. }
  368. .section {
  369. background: white;
  370. border-radius: 16rpx;
  371. padding: 25rpx;
  372. margin-bottom: 30rpx;
  373. .section-header {
  374. display: flex;
  375. justify-content: space-between;
  376. align-items: center;
  377. margin-bottom: 25rpx;
  378. .section-title {
  379. font-size: 32rpx;
  380. font-weight: bold;
  381. color: #333;
  382. }
  383. .more {
  384. font-size: 26rpx;
  385. color: #999;
  386. }
  387. }
  388. }
  389. .book-list {
  390. white-space: nowrap;
  391. .book-item {
  392. display: inline-block;
  393. width: 180rpx;
  394. margin-right: 25rpx;
  395. vertical-align: top;
  396. .book-cover {
  397. width: 180rpx;
  398. height: 240rpx;
  399. border-radius: 8rpx;
  400. }
  401. .book-title {
  402. display: block;
  403. font-size: 26rpx;
  404. font-weight: bold;
  405. margin-top: 15rpx;
  406. white-space: nowrap;
  407. overflow: hidden;
  408. text-overflow: ellipsis;
  409. }
  410. .book-author {
  411. display: block;
  412. font-size: 24rpx;
  413. color: #999;
  414. white-space: nowrap;
  415. overflow: hidden;
  416. text-overflow: ellipsis;
  417. }
  418. }
  419. }
  420. .book-row {
  421. display: flex;
  422. padding: 25rpx 0;
  423. border-bottom: 1rpx solid #eee;
  424. &:last-child {
  425. border-bottom: none;
  426. }
  427. .row-cover {
  428. width: 160rpx;
  429. height: 210rpx;
  430. border-radius: 8rpx;
  431. margin-right: 25rpx;
  432. }
  433. .book-info {
  434. flex: 1;
  435. display: flex;
  436. flex-direction: column;
  437. .row-title {
  438. font-size: 30rpx;
  439. font-weight: bold;
  440. margin-bottom: 10rpx;
  441. }
  442. .row-author {
  443. font-size: 26rpx;
  444. color: #666;
  445. margin-bottom: 15rpx;
  446. }
  447. .row-desc {
  448. font-size: 26rpx;
  449. color: #666;
  450. display: -webkit-box;
  451. -webkit-box-orient: vertical;
  452. -webkit-line-clamp: 2;
  453. overflow: hidden;
  454. margin-bottom: 15rpx;
  455. }
  456. }
  457. .row-tags {
  458. display: flex;
  459. flex-wrap: wrap;
  460. .tag {
  461. font-size: 22rpx;
  462. color: #e74c3c;
  463. border: 1rpx solid #e74c3c;
  464. border-radius: 20rpx;
  465. padding: 5rpx 15rpx;
  466. margin-right: 15rpx;
  467. margin-bottom: 10rpx;
  468. }
  469. }
  470. }
  471. </style>