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

905 lines
18 KiB

  1. <template>
  2. <!-- 小说详情页面 -->
  3. <view class="novel-detail">
  4. <navbar leftClick @leftClick="$utils.navigateBack" />
  5. <!-- 小说基本信息 -->
  6. <view class="novel-info">
  7. <view class="novel-cover">
  8. <image :src="novelData.image &&
  9. novelData.image.split(',')[0]" mode="aspectFill"></image>
  10. </view>
  11. <view class="novel-basic">
  12. <text class="title">{{ novelData.name }}</text>
  13. <view class="author-line">
  14. <text class="label">作者</text>
  15. <text class="author">{{ novelData.author || '暂无显示' }}</text>
  16. </view>
  17. <!-- <view class="status-line">
  18. <text class="label">完结</text>
  19. <text class="status">{{ novelData.status }}</text>
  20. </view> -->
  21. <view class="content-row">
  22. <!-- <view class="book-status">
  23. <text>{{novelData.status}}</text>
  24. </view> -->
  25. <bookStatus :status="novelData.status"/>
  26. <view class="book-text">
  27. {{ novelData.service }}
  28. </view>
  29. </view>
  30. <view class="score-line">
  31. <text class="score">{{ novelData.qmNum || 0}}</text>
  32. <text class="score-label">作者累计亲密度值</text>
  33. </view>
  34. </view>
  35. </view>
  36. <!-- 推荐票数显示 -->
  37. <view class="recommendation-section">
  38. <view class="rec-left">
  39. <text class="rec-count">{{ novelData.tuiNum || 0 }}</text>
  40. <text class="rec-label">推荐票数</text>
  41. </view>
  42. <view class="rec-divider"></view>
  43. <view class="rec-right">
  44. <button class="recommend-btn" @click="$refs.novelVotePopup.open(id)">
  45. <text class="btn-icon">📑</text>
  46. 投推荐票
  47. </button>
  48. </view>
  49. </view>
  50. <!-- 阅读和收藏按钮 -->
  51. <!-- 我的等级 -->
  52. <view class="user-level">
  53. <view class="level-left">
  54. <view class="level-title">
  55. <!-- <text class="title-icon">👑</text> -->
  56. <image style="width: 30rpx;height: 30rpx;" src="/pages_order/static/book/level.png"
  57. mode="aspectFill"></image>
  58. <text>我的等级</text>
  59. </view>
  60. <view class="level-info">
  61. <!-- <image class="user-avatar" src="/pages_order/static/book/dj.png" mode="aspectFill"></image> -->
  62. <image
  63. :src="bookLevel.icon"
  64. class="user-avatar"
  65. mode="aspectFill"></image>
  66. <view class="user-details">
  67. <view class="username">
  68. <image
  69. :src="bookLevel.hanHaiMember
  70. && bookLevel.hanHaiMember.headImage"
  71. style="width: 40rpx;height: 40rpx;border-radius: 50%;margin-right: 4rpx;"
  72. mode="aspectFill"></image>
  73. <view class="name">
  74. {{ (bookLevel.hanHaiMember
  75. && bookLevel.hanHaiMember.nickName) || '_' }}
  76. </view>
  77. </view>
  78. <view class="user-score">
  79. <text class="score-value">{{ bookLevel.num || 0}}</text>
  80. <text class="score-label">亲密值</text>
  81. </view>
  82. <text class="user-role">{{ bookLevel.commonBookAchievement
  83. && (bookLevel.commonBookAchievement.oldName || bookLevel.commonBookAchievement.title)}}</text>
  84. </view>
  85. </view>
  86. </view>
  87. <view class="level-right">
  88. <view class="rank-btn" @click="toggleInteractive">
  89. <image class="rank-icon" src="/pages_order/static/book/bd.png" mode="aspectFit"></image>
  90. <text class="check-text">点击查看</text>
  91. </view>
  92. </view>
  93. </view>
  94. <!-- 小说简介 -->
  95. <view class="novel-intro">
  96. <view class="intro-title">
  97. <text>简介</text>
  98. </view>
  99. <view class="intro-content">
  100. {{ novelData.details }}
  101. </view>
  102. </view>
  103. <!-- 目录 -->
  104. <view class="novel-catalog" @click="$refs.chapterPopup.open()">
  105. <view class="catalog-header">
  106. <view class="catalog-title">
  107. <text class="title-icon">📖</text>
  108. <text>目录</text>
  109. </view>
  110. <view class="chapter-nav">
  111. <text class="current-chapter">
  112. {{ catalog ? catalog.title || '暂无章节' : '暂无章节' }}
  113. </text>
  114. <text class="nav-arrow">></text>
  115. </view>
  116. </view>
  117. </view>
  118. <!-- 书评区域 -->
  119. <view class="comments-section">
  120. <view class="comments-header">
  121. <view class="header-left">
  122. <text class="title-icon">📝</text>
  123. <text>书评</text>
  124. </view>
  125. <view class="header-right">
  126. <text @click="goToWriteReview">写书评</text>
  127. </view>
  128. </view>
  129. <view class="comment-list">
  130. <commentItem v-for="(item, index) in list" :item="item" :key="index" />
  131. <uv-empty mode="list" v-if="list.length == 0"></uv-empty>
  132. </view>
  133. </view>
  134. <!-- 底部操作栏 -->
  135. <view class="novel-bottom">
  136. <view class="bottom-left">
  137. <view class="action-btn"
  138. v-if="isBooshelf"
  139. @click="deleteBookshelfByBookId">
  140. <view class="btn-icon">
  141. <uv-icon name="grid" color="#f20" size="50rpx"></uv-icon>
  142. </view>
  143. <text style="color: #f20;">删除书架</text>
  144. </view>
  145. <view class="action-btn"
  146. v-else
  147. @click="addToBookshelf">
  148. <view class="btn-icon">
  149. <uv-icon name="grid" color="#999" size="50rpx"></uv-icon>
  150. </view>
  151. <text>加入书架</text>
  152. </view>
  153. <!-- <view class="action-btn" @click="goToGiftbox">
  154. <text class="btn-icon">🎁</text>
  155. <text>礼物盒</text>
  156. </view> -->
  157. <view class="action-btn" @click="$refs.interactiveGiftPopup.open()">
  158. <view class="btn-icon">
  159. <uv-icon name="gift" color="#999" size="50rpx"></uv-icon>
  160. </view>
  161. <text>互动打赏</text>
  162. </view>
  163. </view>
  164. <view class="bottom-right">
  165. <button class="read-now-btn" @click="toRead">立即阅读</button>
  166. </view>
  167. </view>
  168. <novelVotePopup ref="novelVotePopup" @updateVote="updateVote"/>
  169. <chapterPopup ref="chapterPopup" :bookId="id" :chapterList="chapterList" @selectChapter="selectChapter"/>
  170. <interactiveGiftPopup ref="interactiveGiftPopup" :bookId="id" @giftSent="handleGiftSent"/>
  171. </view>
  172. </template>
  173. <script>
  174. import catalogpopup from '@/components/novel/CatalogPopup.vue'
  175. import chapterPopup from '../components/novel/chapterPopup.vue'
  176. import commentItem from '../components/comment/commentItem.vue'
  177. import novelVotePopup from '../components/novel/novelVotePopup.vue'
  178. import interactiveGiftPopup from '../components/novel/interactiveGiftPopup.vue'
  179. import mixinsList from '@/mixins/list.js'
  180. import bookStatus from '@/components/novel/bookStatus.vue'
  181. export default {
  182. mixins: [mixinsList],
  183. components: {
  184. catalogpopup,
  185. chapterPopup,
  186. commentItem,
  187. novelVotePopup,
  188. interactiveGiftPopup,
  189. bookStatus,
  190. },
  191. data() {
  192. return {
  193. novelData: {},
  194. isCollected: false,
  195. comments: [],
  196. currentIndex: 0,
  197. id: 0,
  198. bookLevel: {},
  199. mixinsListApi: 'getBookCommentList',
  200. catalog: {}, //最后一个章节
  201. fastCatalog: {}, //第一章节
  202. chapterList : [],//章节列表
  203. isBooshelf : false,
  204. readingRecord: {}, // 用户的阅读记录
  205. }
  206. },
  207. computed: {},
  208. onLoad({
  209. id
  210. }) {
  211. this.id = id
  212. this.queryParams.bookId = id
  213. },
  214. onShow() {
  215. this.getDateil()
  216. this.getBookCatalogList()
  217. this.isAddBook()
  218. if(this.isLogin){
  219. this.getAchievement()
  220. this.getReadingRecord()
  221. }
  222. },
  223. methods: {
  224. updateVote() {
  225. this.getDateil()
  226. this.getAchievement()
  227. },
  228. getDateil() {
  229. let data = {
  230. id: this.id
  231. }
  232. if(uni.getStorageSync('data')){
  233. data.token = uni.getStorageSync('token')
  234. }
  235. this.$fetch('getBookDetail', data).then(res => {
  236. this.novelData = res
  237. })
  238. },
  239. getAchievement() {
  240. this.$fetch('getAchievementByBookId', {
  241. bookId: this.id
  242. }).then(res => {
  243. this.bookLevel = res
  244. })
  245. },
  246. getBookCatalogList() {
  247. this.$fetch('getBookCatalogList', {
  248. bookId : this.id,
  249. pageNo : 1,
  250. pageSize : 9999999,
  251. reverse : 0,
  252. }).then(res => {
  253. this.chapterList = res.records
  254. this.catalog = res.records[res.records.length - 1]
  255. this.fastCatalog = res.records[0]
  256. })
  257. // 获取最后章节
  258. // this.$fetch('getBookCatalogList', {
  259. // bookId: this.id,
  260. // pageNo: 1,
  261. // pageSize: 1,
  262. // reverse: 1,
  263. // }).then(res => {
  264. // this.catalog = res.records[0]
  265. // })
  266. // // 获取第一章节
  267. // this.$fetch('getBookCatalogList', {
  268. // bookId: this.id,
  269. // pageNo: 1,
  270. // pageSize: 1,
  271. // reverse: 0,
  272. // }).then(res => {
  273. // this.fastCatalog = res.records[0]
  274. // })
  275. },
  276. toggleCollect() {
  277. this.isCollected = !this.isCollected
  278. },
  279. goToWriteReview() {
  280. uni.navigateTo({
  281. url: `/pages_order/comment/review?id=${this.id}`
  282. })
  283. },
  284. addToBookshelf() {
  285. this.$fetch('addReadBook', {
  286. shopId: this.id,
  287. name: this.novelData.name,
  288. image: this.novelData.image,
  289. novelId : this.fastCatalog && this.fastCatalog.id
  290. }).then(res => {
  291. this.isBooshelf = true
  292. uni.showToast({
  293. title: '已加入书架',
  294. icon: 'success'
  295. })
  296. })
  297. },
  298. toggleInteractive() {
  299. uni.navigateTo({
  300. url: `/pages_order/novel/Tipping?id=${this.id}`
  301. })
  302. },
  303. goToGiftbox() {
  304. uni.navigateTo({
  305. url: `/pages_order/novel/Giftbox?id=${this.id}`
  306. })
  307. },
  308. async isAddBook(){
  309. this.isBooshelf = await this.$fetch('isAddBook', {
  310. bookId: this.id
  311. })
  312. },
  313. // 获取用户的阅读记录
  314. async getReadingRecord() {
  315. try {
  316. const res = await this.$fetch('getReadChapterByBookId', {
  317. bookId : this.id
  318. });
  319. this.readingRecord = res || {};
  320. } catch (error) {
  321. console.error('获取阅读记录失败:', error);
  322. this.readingRecord = {};
  323. }
  324. },
  325. toRead() {
  326. // 判断用户是否已登录
  327. if (!uni.getStorageSync('token')) {
  328. this.$utils.toLogin();
  329. return;
  330. }
  331. if (!this.fastCatalog) {
  332. uni.showToast({
  333. title: '暂无章节',
  334. icon: 'none'
  335. })
  336. return
  337. }
  338. // 如果有阅读记录(novelId存在),直接跳转到该章节
  339. if (this.readingRecord && this.readingRecord.id) {
  340. uni.navigateTo({
  341. url: `/pages_order/novel/readnovels?cid=${this.readingRecord.id}&id=${this.id}`
  342. });
  343. } else {
  344. // 没有阅读记录,跳转到第一章
  345. uni.navigateTo({
  346. url: `/pages_order/novel/readnovels?cid=${this.fastCatalog.id}&id=${this.id}`
  347. });
  348. }
  349. },
  350. selectChapter({item, index}){
  351. uni.navigateTo({
  352. url: `/pages_order/novel/readnovels?cid=${item.id}&id=${this.id}`
  353. })
  354. },
  355. handleGiftSent(giftData) {
  356. // 处理礼物发送后的逻辑
  357. console.log('礼物发送成功:', giftData);
  358. uni.showToast({
  359. title: `成功赠送${giftData.gift.title} x${giftData.count}`,
  360. icon: 'success'
  361. });
  362. // 重新获取小说详情,更新打赏相关数据
  363. this.getDateil();
  364. // 如果用户已登录,更新成就数据
  365. if(this.isLogin){
  366. this.getAchievement();
  367. }
  368. },
  369. // 删除书架
  370. deleteBookshelfByBookId(){
  371. this.$fetch('deleteBookshelfByBookId', {
  372. bookId : this.id
  373. }).then(res => {
  374. this.isBooshelf = false
  375. })
  376. },
  377. }
  378. }
  379. </script>
  380. <style lang="scss" scoped>
  381. .novel-detail {
  382. min-height: 100vh;
  383. background-color: #f5f5f5;
  384. padding-bottom: calc(env(safe-area-inset-bottom) + 100rpx);
  385. .nav-header {
  386. display: flex;
  387. justify-content: space-between;
  388. align-items: center;
  389. padding: 20rpx 30rpx;
  390. background-color: transparent;
  391. position: fixed;
  392. top: 0;
  393. left: 0;
  394. right: 0;
  395. z-index: 100;
  396. }
  397. .novel-info {
  398. padding: 40rpx;
  399. display: flex;
  400. background: #fff;
  401. .novel-cover {
  402. width: 160rpx;
  403. height: 200rpx;
  404. margin-right: 20rpx;
  405. image {
  406. width: 100%;
  407. height: 100%;
  408. border-radius: 20rpx;
  409. }
  410. }
  411. .novel-basic {
  412. flex: 1;
  413. display: flex;
  414. flex-direction: column;
  415. justify-content: space-between;
  416. .title {
  417. font-size: 36rpx;
  418. font-weight: bold;
  419. margin-bottom: 16rpx;
  420. }
  421. .author-line,
  422. .status-line {
  423. display: flex;
  424. align-items: center;
  425. margin-bottom: 12rpx;
  426. font-size: 26rpx;
  427. color: #666;
  428. }
  429. .content-row {
  430. display: flex;
  431. align-items: center;
  432. margin-bottom: 10rpx;
  433. .book-status {
  434. flex-shrink: 0;
  435. text {
  436. font-size: 20rpx;
  437. color: #67C23A;
  438. background-color: rgba(103, 194, 58, 0.1);
  439. border-radius: 20rpx;
  440. padding: 4rpx 12rpx;
  441. }
  442. }
  443. .book-text {
  444. font-size: 20rpx;
  445. }
  446. }
  447. .label {
  448. color: #999;
  449. margin-right: 8rpx;
  450. }
  451. .score-line {
  452. margin-top: 16rpx;
  453. .score {
  454. font-size: 32rpx;
  455. color: #333;
  456. font-weight: bold;
  457. }
  458. .score-label {
  459. font-size: 24rpx;
  460. color: #999;
  461. margin-left: 8rpx;
  462. }
  463. }
  464. }
  465. }
  466. .recommendation-section {
  467. padding: 24rpx 32rpx;
  468. background: #fff;
  469. display: flex;
  470. justify-content: space-between;
  471. align-items: center;
  472. position: relative;
  473. .rec-left {
  474. display: flex;
  475. flex-direction: column;
  476. align-items: center;
  477. margin-left: 70rpx;
  478. .rec-count {
  479. font-size: 34rpx;
  480. font-weight: 500;
  481. color: #333;
  482. line-height: 1.2;
  483. }
  484. .rec-label {
  485. font-size: 26rpx;
  486. color: #999;
  487. margin-top: 4rpx;
  488. }
  489. }
  490. .rec-divider {
  491. position: absolute;
  492. right: 160rpx;
  493. top: 20rpx;
  494. bottom: 20rpx;
  495. width: 2rpx;
  496. background: #eee;
  497. }
  498. .rec-right {
  499. flex-shrink: 0;
  500. .recommend-btn {
  501. background: #fff;
  502. color: #4a90e2;
  503. border: 2rpx solid #4a90e2;
  504. border-radius: 40rpx;
  505. padding: 12rpx 32rpx;
  506. font-size: 28rpx;
  507. display: flex;
  508. align-items: center;
  509. line-height: 1;
  510. height: 64rpx;
  511. .btn-icon {
  512. margin-right: 8rpx;
  513. font-size: 32rpx;
  514. }
  515. }
  516. }
  517. }
  518. .action-buttons {
  519. display: flex;
  520. padding: 30rpx;
  521. gap: 20rpx;
  522. button {
  523. flex: 1;
  524. height: 80rpx;
  525. border-radius: 40rpx;
  526. font-size: 32rpx;
  527. display: flex;
  528. align-items: center;
  529. justify-content: center;
  530. }
  531. .read-btn {
  532. background-color: #4a90e2;
  533. color: #fff;
  534. }
  535. .collect-btn {
  536. background-color: #f0f0f0;
  537. color: #666;
  538. }
  539. }
  540. .user-level {
  541. margin: 20rpx 30rpx;
  542. background-color: #fff;
  543. border-radius: 12rpx;
  544. padding: 24rpx 32rpx;
  545. display: flex;
  546. justify-content: space-between;
  547. align-items: stretch;
  548. .level-left {
  549. flex: 1;
  550. .level-title {
  551. display: flex;
  552. align-items: center;
  553. gap: 8rpx;
  554. margin-bottom: 20rpx;
  555. margin-left: 20rpx;
  556. .title-icon {
  557. font-size: 36rpx;
  558. color: #FFB800;
  559. }
  560. text {
  561. font-size: 32rpx;
  562. font-weight: 500;
  563. color: #333;
  564. }
  565. }
  566. .level-info {
  567. display: flex;
  568. align-items: flex-start;
  569. gap: 20rpx;
  570. .user-avatar {
  571. width: 80rpx;
  572. height: 80rpx;
  573. border-radius: 50%;
  574. border: 2rpx solid #f0f0f0;
  575. }
  576. .user-details {
  577. display: flex;
  578. flex-direction: column;
  579. gap: 8rpx;
  580. .username {
  581. display: flex;
  582. font-size: 28rpx;
  583. color: #333;
  584. font-weight: 500;
  585. image {
  586. width: 60rpx;
  587. height: 60rpx;
  588. }
  589. }
  590. .user-score {
  591. display: flex;
  592. align-items: center;
  593. gap: 8rpx;
  594. .score-value {
  595. font-size: 28rpx;
  596. color: #333;
  597. }
  598. .score-label {
  599. font-size: 24rpx;
  600. color: #999;
  601. }
  602. }
  603. .user-role {
  604. font-size: 24rpx;
  605. color: #666;
  606. background: #f5f5f5;
  607. padding: 4rpx 12rpx;
  608. border-radius: 4rpx;
  609. display: inline-block;
  610. }
  611. }
  612. }
  613. }
  614. .level-right {
  615. display: flex;
  616. align-items: center;
  617. .rank-btn {
  618. display: flex;
  619. flex-direction: column;
  620. align-items: center;
  621. justify-content: center;
  622. padding: 0 20rpx;
  623. .rank-icon {
  624. width: 200rpx;
  625. height: 60rpx;
  626. margin-bottom: 8rpx;
  627. }
  628. text {
  629. font-size: 26rpx;
  630. color: #333;
  631. line-height: 1.4;
  632. }
  633. .check-text {
  634. font-size: 22rpx;
  635. color: #33e;
  636. }
  637. }
  638. }
  639. }
  640. .novel-intro {
  641. margin: 20rpx 30rpx;
  642. background-color: #fff;
  643. border-radius: 12rpx;
  644. padding: 24rpx;
  645. .intro-title {
  646. font-size: 32rpx;
  647. font-weight: 500;
  648. color: #333;
  649. margin-bottom: 16rpx;
  650. }
  651. .intro-content {
  652. font-size: 28rpx;
  653. color: #666;
  654. line-height: 1.6;
  655. display: flex;
  656. flex-direction: column;
  657. gap: 16rpx;
  658. text {
  659. display: block;
  660. }
  661. }
  662. }
  663. .comments-section {
  664. margin: 20rpx 30rpx;
  665. background-color: #fff;
  666. border-radius: 12rpx;
  667. padding: 24rpx;
  668. .comments-header {
  669. display: flex;
  670. align-items: center;
  671. margin-bottom: 24rpx;
  672. border-bottom: 2rpx solid #f5f5f5;
  673. padding-bottom: 24rpx;
  674. justify-content: flex-start;
  675. .header-left {
  676. display: flex;
  677. align-items: center;
  678. gap: 8rpx;
  679. .title-icon {
  680. font-size: 32rpx;
  681. }
  682. text {
  683. display: flex;
  684. align-items: center;
  685. font-size: 32rpx;
  686. font-weight: 500;
  687. color: #333;
  688. white-space: nowrap;
  689. }
  690. }
  691. .header-right {
  692. margin-left: auto;
  693. }
  694. }
  695. .comment-list {
  696. display: flex;
  697. flex-direction: column;
  698. gap: 32rpx;
  699. }
  700. .like-icon {
  701. font-size: 24rpx;
  702. color: #999;
  703. }
  704. .like-count {
  705. font-size: 24rpx;
  706. color: #999;
  707. }
  708. }
  709. .novel-catalog {
  710. margin: 20rpx 30rpx;
  711. background-color: #fff;
  712. border-radius: 12rpx;
  713. padding: 24rpx;
  714. .catalog-header {
  715. display: flex;
  716. justify-content: space-between;
  717. align-items: center;
  718. border-bottom: 2rpx solid #f5f5f5;
  719. .catalog-title {
  720. display: flex;
  721. align-items: center;
  722. gap: 8rpx;
  723. .title-icon {
  724. font-size: 32rpx;
  725. }
  726. text {
  727. font-size: 32rpx;
  728. font-weight: 500;
  729. color: #333;
  730. }
  731. }
  732. .chapter-nav {
  733. display: flex;
  734. align-items: center;
  735. gap: 8rpx;
  736. .current-chapter {
  737. font-size: 28rpx;
  738. color: #666;
  739. }
  740. .nav-arrow {
  741. font-size: 28rpx;
  742. color: #999;
  743. }
  744. }
  745. }
  746. }
  747. .novel-bottom {
  748. position: fixed;
  749. bottom: 0;
  750. left: 0;
  751. right: 0;
  752. height: 100rpx;
  753. background: #fff;
  754. display: flex;
  755. align-items: center;
  756. padding: 0 30rpx;
  757. padding-top: 15rpx;
  758. padding-bottom: env(safe-area-inset-bottom);
  759. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
  760. gap: 40rpx;
  761. .bottom-left {
  762. display: flex;
  763. gap: 40rpx;
  764. .action-btn {
  765. display: flex;
  766. flex-direction: column;
  767. align-items: center;
  768. gap: 4rpx;
  769. .btn-icon {
  770. font-size: 40rpx;
  771. line-height: 1;
  772. }
  773. text {
  774. font-size: 24rpx;
  775. color: #666;
  776. }
  777. }
  778. }
  779. .bottom-right {
  780. flex: 1;
  781. display: flex;
  782. .read-now-btn {
  783. flex: 1;
  784. background: #1a237e;
  785. color: #fff;
  786. font-size: 32rpx;
  787. height: 80rpx;
  788. line-height: 80rpx;
  789. padding: 0 60rpx;
  790. border-radius: 40rpx;
  791. border: none;
  792. }
  793. }
  794. }
  795. }
  796. </style>