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

876 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. }
  205. },
  206. computed: {},
  207. onLoad({
  208. id
  209. }) {
  210. this.id = id
  211. this.queryParams.bookId = id
  212. },
  213. onShow() {
  214. this.getDateil()
  215. this.getBookCatalogList()
  216. this.isAddBook()
  217. if(this.isLogin){
  218. this.getAchievement()
  219. }
  220. },
  221. methods: {
  222. updateVote() {
  223. this.getDateil()
  224. this.getAchievement()
  225. },
  226. getDateil() {
  227. let data = {
  228. id: this.id
  229. }
  230. if(uni.getStorageSync('data')){
  231. data.token = uni.getStorageSync('token')
  232. }
  233. this.$fetch('getBookDetail', data).then(res => {
  234. this.novelData = res
  235. })
  236. },
  237. getAchievement() {
  238. this.$fetch('getAchievementByBookId', {
  239. bookId: this.id
  240. }).then(res => {
  241. this.bookLevel = res
  242. })
  243. },
  244. getBookCatalogList() {
  245. this.$fetch('getBookCatalogList', {
  246. bookId : this.id,
  247. pageNo : 1,
  248. pageSize : 9999999,
  249. reverse : 0,
  250. }).then(res => {
  251. this.chapterList = res.records
  252. this.catalog = res.records[res.records.length - 1]
  253. this.fastCatalog = res.records[0]
  254. })
  255. // 获取最后章节
  256. // this.$fetch('getBookCatalogList', {
  257. // bookId: this.id,
  258. // pageNo: 1,
  259. // pageSize: 1,
  260. // reverse: 1,
  261. // }).then(res => {
  262. // this.catalog = res.records[0]
  263. // })
  264. // // 获取第一章节
  265. // this.$fetch('getBookCatalogList', {
  266. // bookId: this.id,
  267. // pageNo: 1,
  268. // pageSize: 1,
  269. // reverse: 0,
  270. // }).then(res => {
  271. // this.fastCatalog = res.records[0]
  272. // })
  273. },
  274. toggleCollect() {
  275. this.isCollected = !this.isCollected
  276. },
  277. goToWriteReview() {
  278. uni.navigateTo({
  279. url: `/pages_order/comment/review?id=${this.id}`
  280. })
  281. },
  282. addToBookshelf() {
  283. this.$fetch('addReadBook', {
  284. shopId: this.id,
  285. name: this.novelData.name,
  286. image: this.novelData.image,
  287. novelId : this.fastCatalog && this.fastCatalog.id
  288. }).then(res => {
  289. this.isBooshelf = true
  290. uni.showToast({
  291. title: '已加入书架',
  292. icon: 'success'
  293. })
  294. })
  295. },
  296. toggleInteractive() {
  297. uni.navigateTo({
  298. url: `/pages_order/novel/Tipping?id=${this.id}`
  299. })
  300. },
  301. goToGiftbox() {
  302. uni.navigateTo({
  303. url: `/pages_order/novel/Giftbox?id=${this.id}`
  304. })
  305. },
  306. async isAddBook(){
  307. this.isBooshelf = await this.$fetch('isAddBook', {
  308. bookId: this.id
  309. })
  310. },
  311. toRead() {
  312. if (!this.fastCatalog) {
  313. uni.showToast({
  314. title: '暂无章节',
  315. icon: 'none'
  316. })
  317. return
  318. }
  319. uni.navigateTo({
  320. url: `/pages_order/novel/readnovels?cid=${this.fastCatalog.id}&id=${this.id}`
  321. })
  322. },
  323. selectChapter({item, index}){
  324. uni.navigateTo({
  325. url: `/pages_order/novel/readnovels?cid=${item.id}&id=${this.id}`
  326. })
  327. },
  328. handleGiftSent(giftData) {
  329. // 处理礼物发送后的逻辑
  330. console.log('礼物发送成功:', giftData);
  331. uni.showToast({
  332. title: `成功赠送${giftData.gift.title} x${giftData.count}`,
  333. icon: 'success'
  334. });
  335. // 重新获取小说详情,更新打赏相关数据
  336. this.getDateil();
  337. // 如果用户已登录,更新成就数据
  338. if(this.isLogin){
  339. this.getAchievement();
  340. }
  341. },
  342. // 删除书架
  343. deleteBookshelfByBookId(){
  344. this.$fetch('deleteBookshelfByBookId', {
  345. bookId : this.id
  346. }).then(res => {
  347. this.isBooshelf = false
  348. })
  349. },
  350. }
  351. }
  352. </script>
  353. <style lang="scss" scoped>
  354. .novel-detail {
  355. min-height: 100vh;
  356. background-color: #f5f5f5;
  357. padding-bottom: calc(env(safe-area-inset-bottom) + 100rpx);
  358. .nav-header {
  359. display: flex;
  360. justify-content: space-between;
  361. align-items: center;
  362. padding: 20rpx 30rpx;
  363. background-color: transparent;
  364. position: fixed;
  365. top: 0;
  366. left: 0;
  367. right: 0;
  368. z-index: 100;
  369. }
  370. .novel-info {
  371. padding: 40rpx;
  372. display: flex;
  373. background: #fff;
  374. .novel-cover {
  375. width: 160rpx;
  376. height: 200rpx;
  377. margin-right: 20rpx;
  378. image {
  379. width: 100%;
  380. height: 100%;
  381. border-radius: 20rpx;
  382. }
  383. }
  384. .novel-basic {
  385. flex: 1;
  386. display: flex;
  387. flex-direction: column;
  388. justify-content: space-between;
  389. .title {
  390. font-size: 36rpx;
  391. font-weight: bold;
  392. margin-bottom: 16rpx;
  393. }
  394. .author-line,
  395. .status-line {
  396. display: flex;
  397. align-items: center;
  398. margin-bottom: 12rpx;
  399. font-size: 26rpx;
  400. color: #666;
  401. }
  402. .content-row {
  403. display: flex;
  404. align-items: center;
  405. margin-bottom: 10rpx;
  406. .book-status {
  407. flex-shrink: 0;
  408. text {
  409. font-size: 20rpx;
  410. color: #67C23A;
  411. background-color: rgba(103, 194, 58, 0.1);
  412. border-radius: 20rpx;
  413. padding: 4rpx 12rpx;
  414. }
  415. }
  416. .book-text {
  417. font-size: 20rpx;
  418. }
  419. }
  420. .label {
  421. color: #999;
  422. margin-right: 8rpx;
  423. }
  424. .score-line {
  425. margin-top: 16rpx;
  426. .score {
  427. font-size: 32rpx;
  428. color: #333;
  429. font-weight: bold;
  430. }
  431. .score-label {
  432. font-size: 24rpx;
  433. color: #999;
  434. margin-left: 8rpx;
  435. }
  436. }
  437. }
  438. }
  439. .recommendation-section {
  440. padding: 24rpx 32rpx;
  441. background: #fff;
  442. display: flex;
  443. justify-content: space-between;
  444. align-items: center;
  445. position: relative;
  446. .rec-left {
  447. display: flex;
  448. flex-direction: column;
  449. align-items: center;
  450. margin-left: 70rpx;
  451. .rec-count {
  452. font-size: 34rpx;
  453. font-weight: 500;
  454. color: #333;
  455. line-height: 1.2;
  456. }
  457. .rec-label {
  458. font-size: 26rpx;
  459. color: #999;
  460. margin-top: 4rpx;
  461. }
  462. }
  463. .rec-divider {
  464. position: absolute;
  465. right: 160rpx;
  466. top: 20rpx;
  467. bottom: 20rpx;
  468. width: 2rpx;
  469. background: #eee;
  470. }
  471. .rec-right {
  472. flex-shrink: 0;
  473. .recommend-btn {
  474. background: #fff;
  475. color: #4a90e2;
  476. border: 2rpx solid #4a90e2;
  477. border-radius: 40rpx;
  478. padding: 12rpx 32rpx;
  479. font-size: 28rpx;
  480. display: flex;
  481. align-items: center;
  482. line-height: 1;
  483. height: 64rpx;
  484. .btn-icon {
  485. margin-right: 8rpx;
  486. font-size: 32rpx;
  487. }
  488. }
  489. }
  490. }
  491. .action-buttons {
  492. display: flex;
  493. padding: 30rpx;
  494. gap: 20rpx;
  495. button {
  496. flex: 1;
  497. height: 80rpx;
  498. border-radius: 40rpx;
  499. font-size: 32rpx;
  500. display: flex;
  501. align-items: center;
  502. justify-content: center;
  503. }
  504. .read-btn {
  505. background-color: #4a90e2;
  506. color: #fff;
  507. }
  508. .collect-btn {
  509. background-color: #f0f0f0;
  510. color: #666;
  511. }
  512. }
  513. .user-level {
  514. margin: 20rpx 30rpx;
  515. background-color: #fff;
  516. border-radius: 12rpx;
  517. padding: 24rpx 32rpx;
  518. display: flex;
  519. justify-content: space-between;
  520. align-items: stretch;
  521. .level-left {
  522. flex: 1;
  523. .level-title {
  524. display: flex;
  525. align-items: center;
  526. gap: 8rpx;
  527. margin-bottom: 20rpx;
  528. margin-left: 20rpx;
  529. .title-icon {
  530. font-size: 36rpx;
  531. color: #FFB800;
  532. }
  533. text {
  534. font-size: 32rpx;
  535. font-weight: 500;
  536. color: #333;
  537. }
  538. }
  539. .level-info {
  540. display: flex;
  541. align-items: flex-start;
  542. gap: 20rpx;
  543. .user-avatar {
  544. width: 80rpx;
  545. height: 80rpx;
  546. border-radius: 50%;
  547. border: 2rpx solid #f0f0f0;
  548. }
  549. .user-details {
  550. display: flex;
  551. flex-direction: column;
  552. gap: 8rpx;
  553. .username {
  554. display: flex;
  555. font-size: 28rpx;
  556. color: #333;
  557. font-weight: 500;
  558. image {
  559. width: 60rpx;
  560. height: 60rpx;
  561. }
  562. }
  563. .user-score {
  564. display: flex;
  565. align-items: center;
  566. gap: 8rpx;
  567. .score-value {
  568. font-size: 28rpx;
  569. color: #333;
  570. }
  571. .score-label {
  572. font-size: 24rpx;
  573. color: #999;
  574. }
  575. }
  576. .user-role {
  577. font-size: 24rpx;
  578. color: #666;
  579. background: #f5f5f5;
  580. padding: 4rpx 12rpx;
  581. border-radius: 4rpx;
  582. display: inline-block;
  583. }
  584. }
  585. }
  586. }
  587. .level-right {
  588. display: flex;
  589. align-items: center;
  590. .rank-btn {
  591. display: flex;
  592. flex-direction: column;
  593. align-items: center;
  594. justify-content: center;
  595. padding: 0 20rpx;
  596. .rank-icon {
  597. width: 200rpx;
  598. height: 60rpx;
  599. margin-bottom: 8rpx;
  600. }
  601. text {
  602. font-size: 26rpx;
  603. color: #333;
  604. line-height: 1.4;
  605. }
  606. .check-text {
  607. font-size: 22rpx;
  608. color: #33e;
  609. }
  610. }
  611. }
  612. }
  613. .novel-intro {
  614. margin: 20rpx 30rpx;
  615. background-color: #fff;
  616. border-radius: 12rpx;
  617. padding: 24rpx;
  618. .intro-title {
  619. font-size: 32rpx;
  620. font-weight: 500;
  621. color: #333;
  622. margin-bottom: 16rpx;
  623. }
  624. .intro-content {
  625. font-size: 28rpx;
  626. color: #666;
  627. line-height: 1.6;
  628. display: flex;
  629. flex-direction: column;
  630. gap: 16rpx;
  631. text {
  632. display: block;
  633. }
  634. }
  635. }
  636. .comments-section {
  637. margin: 20rpx 30rpx;
  638. background-color: #fff;
  639. border-radius: 12rpx;
  640. padding: 24rpx;
  641. .comments-header {
  642. display: flex;
  643. align-items: center;
  644. margin-bottom: 24rpx;
  645. border-bottom: 2rpx solid #f5f5f5;
  646. padding-bottom: 24rpx;
  647. justify-content: flex-start;
  648. .header-left {
  649. display: flex;
  650. align-items: center;
  651. gap: 8rpx;
  652. .title-icon {
  653. font-size: 32rpx;
  654. }
  655. text {
  656. display: flex;
  657. align-items: center;
  658. font-size: 32rpx;
  659. font-weight: 500;
  660. color: #333;
  661. white-space: nowrap;
  662. }
  663. }
  664. .header-right {
  665. margin-left: auto;
  666. }
  667. }
  668. .comment-list {
  669. display: flex;
  670. flex-direction: column;
  671. gap: 32rpx;
  672. }
  673. .like-icon {
  674. font-size: 24rpx;
  675. color: #999;
  676. }
  677. .like-count {
  678. font-size: 24rpx;
  679. color: #999;
  680. }
  681. }
  682. .novel-catalog {
  683. margin: 20rpx 30rpx;
  684. background-color: #fff;
  685. border-radius: 12rpx;
  686. padding: 24rpx;
  687. .catalog-header {
  688. display: flex;
  689. justify-content: space-between;
  690. align-items: center;
  691. border-bottom: 2rpx solid #f5f5f5;
  692. .catalog-title {
  693. display: flex;
  694. align-items: center;
  695. gap: 8rpx;
  696. .title-icon {
  697. font-size: 32rpx;
  698. }
  699. text {
  700. font-size: 32rpx;
  701. font-weight: 500;
  702. color: #333;
  703. }
  704. }
  705. .chapter-nav {
  706. display: flex;
  707. align-items: center;
  708. gap: 8rpx;
  709. .current-chapter {
  710. font-size: 28rpx;
  711. color: #666;
  712. }
  713. .nav-arrow {
  714. font-size: 28rpx;
  715. color: #999;
  716. }
  717. }
  718. }
  719. }
  720. .novel-bottom {
  721. position: fixed;
  722. bottom: 0;
  723. left: 0;
  724. right: 0;
  725. height: 100rpx;
  726. background: #fff;
  727. display: flex;
  728. align-items: center;
  729. padding: 0 30rpx;
  730. padding-top: 15rpx;
  731. padding-bottom: env(safe-area-inset-bottom);
  732. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
  733. gap: 40rpx;
  734. .bottom-left {
  735. display: flex;
  736. gap: 40rpx;
  737. .action-btn {
  738. display: flex;
  739. flex-direction: column;
  740. align-items: center;
  741. gap: 4rpx;
  742. .btn-icon {
  743. font-size: 40rpx;
  744. line-height: 1;
  745. }
  746. text {
  747. font-size: 24rpx;
  748. color: #666;
  749. }
  750. }
  751. }
  752. .bottom-right {
  753. flex: 1;
  754. display: flex;
  755. .read-now-btn {
  756. flex: 1;
  757. background: #1a237e;
  758. color: #fff;
  759. font-size: 32rpx;
  760. height: 80rpx;
  761. line-height: 80rpx;
  762. padding: 0 60rpx;
  763. border-radius: 40rpx;
  764. border: none;
  765. }
  766. }
  767. }
  768. }
  769. </style>