audio-novel.vue 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. <template>
  2. <view class="container">
  3. <!-- 顶部导航栏 -->
  4. <view class="header">
  5. <view class="back-btn" @click="goBack">
  6. <text class="back-icon">←</text>
  7. </view>
  8. <text class="header-title">有声小说</text>
  9. <view class="search-btn" @click="goToSearch">
  10. <text class="search-icon">🔍</text>
  11. </view>
  12. </view>
  13. <!-- 小说列表 -->
  14. <scroll-view class="audio-list-container" scroll-y>
  15. <view class="loading" v-if="isLoading">
  16. <text>加载中...</text>
  17. </view>
  18. <view class="empty" v-else-if="audioList.length === 0">
  19. <text>暂无有声小说</text>
  20. </view>
  21. <view
  22. class="audio-item"
  23. v-for="(audio, index) in audioList"
  24. :key="audio.id || index"
  25. @click="goToAudioDetail(audio)"
  26. >
  27. <view class="cover-wrapper">
  28. <image
  29. class="audio-cover"
  30. :src="audio.cover"
  31. mode="aspectFill"
  32. :lazy-load="true"
  33. @error="handleImageError(index)"
  34. ></image>
  35. <view class="play-icon-overlay">
  36. <text class="play-icon">▶</text>
  37. </view>
  38. </view>
  39. <view class="audio-info">
  40. <text class="audio-title">{{ audio.title }}</text>
  41. <text class="audio-desc">{{ audio.desc }}</text>
  42. <text class="audio-author">{{ audio.author }}</text>
  43. </view>
  44. <button class="play-btn" @click.stop="handlePlay(audio)">
  45. <text class="play-btn-text">播放</text>
  46. </button>
  47. </view>
  48. </scroll-view>
  49. </view>
  50. </template>
  51. <script>
  52. import { getRecommendAudiobooks } from '../../utils/api.js'
  53. export default {
  54. data() {
  55. return {
  56. audioList: [],
  57. isLoading: false
  58. }
  59. },
  60. onLoad() {
  61. this.loadAudioList()
  62. },
  63. methods: {
  64. async loadAudioList() {
  65. this.isLoading = true
  66. try {
  67. const res = await getRecommendAudiobooks(50)
  68. if (res && res.code === 200 && Array.isArray(res.data)) {
  69. this.audioList = res.data.map(item => ({
  70. id: item.id,
  71. title: item.title,
  72. author: item.author || '',
  73. desc: item.brief || item.desc || '',
  74. cover: item.image || item.cover || 'https://picsum.photos/seed/audio-novel/200/300'
  75. }))
  76. } else {
  77. this.audioList = []
  78. }
  79. } catch (error) {
  80. console.error('加载有声小说失败:', error)
  81. this.audioList = []
  82. uni.showToast({
  83. title: '加载失败',
  84. icon: 'none'
  85. })
  86. } finally {
  87. this.isLoading = false
  88. }
  89. },
  90. goBack() {
  91. uni.navigateBack({
  92. delta: 1
  93. })
  94. },
  95. goToSearch() {
  96. uni.navigateTo({
  97. url: '/pages/search/search'
  98. })
  99. },
  100. goToAudioDetail(audio) {
  101. if (!audio || !audio.id) {
  102. uni.showToast({
  103. title: '听书信息不完整',
  104. icon: 'none'
  105. })
  106. return
  107. }
  108. uni.navigateTo({
  109. url: `/pages/listen-detail/listen-detail?audiobookId=${audio.id}`
  110. })
  111. },
  112. handlePlay(audio) {
  113. if (!audio || !audio.id) {
  114. uni.showToast({
  115. title: '听书信息不完整',
  116. icon: 'none'
  117. })
  118. return
  119. }
  120. this.goToAudioDetail(audio)
  121. },
  122. handleImageError(index) {
  123. if (this.audioList[index]) {
  124. this.audioList[index].cover = `https://picsum.photos/seed/audio-novel-fallback${index}/200/300`
  125. }
  126. }
  127. }
  128. }
  129. </script>
  130. <style scoped>
  131. .container {
  132. width: 100%;
  133. height: 100vh;
  134. background-color: #FFFFFF;
  135. display: flex;
  136. flex-direction: column;
  137. padding-top: 30px;
  138. box-sizing: border-box;
  139. }
  140. .header {
  141. display: flex;
  142. align-items: center;
  143. justify-content: space-between;
  144. padding: 20rpx 30rpx;
  145. background-color: #FFFFFF;
  146. border-bottom: 1rpx solid #F0F0F0;
  147. }
  148. .back-btn, .search-btn {
  149. width: 60rpx;
  150. height: 60rpx;
  151. display: flex;
  152. align-items: center;
  153. justify-content: center;
  154. }
  155. .back-icon {
  156. font-size: 40rpx;
  157. color: #333333;
  158. font-weight: bold;
  159. }
  160. .header-title {
  161. font-size: 34rpx;
  162. font-weight: bold;
  163. color: #333333;
  164. }
  165. .search-icon {
  166. font-size: 36rpx;
  167. color: #333333;
  168. }
  169. .audio-list-container {
  170. flex: 1;
  171. width: 100%;
  172. }
  173. .loading, .empty {
  174. text-align: center;
  175. padding: 60rpx 0;
  176. color: #999999;
  177. font-size: 30rpx;
  178. }
  179. .audio-item {
  180. display: flex;
  181. align-items: center;
  182. padding: 30rpx 30rpx;
  183. border-bottom: 1rpx solid #F0F0F0;
  184. }
  185. .cover-wrapper {
  186. position: relative;
  187. width: 160rpx;
  188. height: 220rpx;
  189. margin-right: 30rpx;
  190. flex-shrink: 0;
  191. }
  192. .audio-cover {
  193. width: 100%;
  194. height: 100%;
  195. border-radius: 12rpx;
  196. object-fit: cover;
  197. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.12);
  198. background-color: #F5F5F5;
  199. }
  200. .play-icon-overlay {
  201. position: absolute;
  202. bottom: 10rpx;
  203. right: 10rpx;
  204. width: 48rpx;
  205. height: 48rpx;
  206. border-radius: 50%;
  207. background: rgba(0,0,0,0.6);
  208. display: flex;
  209. align-items: center;
  210. justify-content: center;
  211. }
  212. .play-icon {
  213. color: #FFFFFF;
  214. font-size: 26rpx;
  215. margin-left: 4rpx;
  216. }
  217. .audio-info {
  218. flex: 1;
  219. display: flex;
  220. flex-direction: column;
  221. gap: 10rpx;
  222. }
  223. .audio-title {
  224. font-size: 32rpx;
  225. font-weight: bold;
  226. color: #333333;
  227. }
  228. .audio-desc {
  229. font-size: 26rpx;
  230. color: #666666;
  231. line-height: 1.6;
  232. display: -webkit-box;
  233. -webkit-box-orient: vertical;
  234. -webkit-line-clamp: 2;
  235. overflow: hidden;
  236. }
  237. .audio-author {
  238. font-size: 24rpx;
  239. color: #999999;
  240. }
  241. .play-btn {
  242. background-color: #4CAF50;
  243. color: #FFFFFF;
  244. border: none;
  245. border-radius: 32rpx;
  246. padding: 12rpx 26rpx;
  247. font-size: 26rpx;
  248. }
  249. .play-btn::after {
  250. border: none;
  251. }
  252. .play-btn-text {
  253. color: #FFFFFF;
  254. }
  255. </style>