├── php-api/ # 改造后的PHP接口层 ├── java-ad-service/ # 若依框架微服务(广告+VIP+分账) ├── uniapp-reader/ # UniApp前端项目 │ ├── pages/ # 各端页面 │ └──
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

list.vue 9.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. <template>
  2. <view class="novel-list-page">
  3. <!-- 加载状态 -->
  4. <view v-if="loading" class="loading-container">
  5. <uni-icons type="spinner-cycle" size="36" color="var(--primary-color)" class="loading-icon" />
  6. <text>加载中...</text>
  7. </view>
  8. <!-- 内容区域 -->
  9. <template v-else>
  10. <!-- 顶部广告 -->
  11. <ad-banner v-if="topAds.length" :ads="topAds" />
  12. <!-- 顶部分类导航 -->
  13. <scroll-view scroll-x class="category-nav">
  14. <view
  15. v-for="category in categories"
  16. :key="category.id"
  17. class="category-item"
  18. :class="{ active: activeCategory === category.id }"
  19. @click="changeCategory(category.id)"
  20. >
  21. {{ category.name }}
  22. </view>
  23. </scroll-view>
  24. <!-- 空状态 -->
  25. <view v-if="novels.length === 0 && !error" class="empty-container">
  26. <image src="/static/empty-book.png" class="empty-img" />
  27. <text class="empty-text">暂无小说数据</text>
  28. <button class="btn-refresh" @click="loadNovels">重新加载</button>
  29. </view>
  30. <!-- 错误状态 -->
  31. <view v-if="error" class="error-container">
  32. <uni-icons type="info" size="48" color="var(--error-color)" />
  33. <text class="error-text">{{ error }}</text>
  34. <button class="btn-refresh" @click="loadNovels">重新加载</button>
  35. </view>
  36. <!-- 热门推荐 -->
  37. <view v-if="hotNovels.length > 0" class="section">
  38. <view class="section-header">
  39. <text class="section-title">热门推荐</text>
  40. <text class="section-more" @click="goMore">更多 ></text>
  41. </view>
  42. <scroll-view scroll-x class="hot-list">
  43. <view
  44. v-for="novel in hotNovels"
  45. :key="novel.id"
  46. class="hot-item"
  47. @click="openNovel(novel)"
  48. >
  49. <image :src="getCoverUrl(novel.cover)" class="hot-cover" />
  50. <text class="hot-title">{{ novel.title }}</text>
  51. </view>
  52. </scroll-view>
  53. </view>
  54. <!-- 分类小说列表 -->
  55. <view v-if="novels.length > 0" class="section">
  56. <view class="section-header">
  57. <text class="section-title">{{ currentCategoryName }}作品</text>
  58. </view>
  59. <view class="novel-grid">
  60. <view
  61. v-for="(novel, index) in novels"
  62. :key="index"
  63. class="novel-item"
  64. @click="openNovel(novel)"
  65. >
  66. <image :src="getCoverUrl(novel.cover)" class="novel-cover" />
  67. <text class="novel-title">{{ novel.title || '未知标题' }}</text>
  68. <text class="novel-author">{{ novel.author || '未知作者' }}</text>
  69. </view>
  70. </view>
  71. </view>
  72. <!-- 成为签约作家按钮 -->
  73. <view class="become-author" @click="applyAuthor">
  74. <text>成为签约作家,发布你的作品</text>
  75. </view>
  76. <!-- 底部广告 -->
  77. <ad-banner v-if="bottomAds.length" :ads="bottomAds" />
  78. </template>
  79. </view>
  80. </template>
  81. <script>
  82. // 确保路径正确
  83. import AdBanner from '@/components/AdBanner';
  84. export default {
  85. components: { AdBanner },
  86. data() {
  87. return {
  88. topAds: [],
  89. bottomAds: [],
  90. categories: [],
  91. activeCategory: 0,
  92. hotNovels: [],
  93. novels: [],
  94. loading: true,
  95. error: null,
  96. currentCategoryName: '全部'
  97. }
  98. },
  99. async mounted() {
  100. await this.initData();
  101. },
  102. methods: {
  103. async initData() {
  104. try {
  105. // 顺序加载数据,避免并行请求冲突
  106. await this.loadCategories();
  107. await this.loadHotNovels();
  108. await this.loadNovels();
  109. await this.loadAds();
  110. } catch (e) {
  111. console.error('初始化失败', e);
  112. this.error = '初始化失败,请检查网络连接';
  113. } finally {
  114. this.loading = false;
  115. }
  116. },
  117. // 加载广告
  118. async loadAds() {
  119. try {
  120. const [topRes, bottomRes] = await Promise.all([
  121. this.$http.get('/java-api/ad/position?code=TOP_BANNER'),
  122. this.$http.get('/java-api/ad/position?code=BOTTOM_BANNER')
  123. ]);
  124. this.topAds = topRes?.data || topRes || [];
  125. this.bottomAds = bottomRes?.data || bottomRes || [];
  126. } catch (e) {
  127. console.error('加载广告失败', e);
  128. }
  129. },
  130. // 加载分类
  131. async loadCategories() {
  132. try {
  133. const res = await this.$http.get('/category/tree');
  134. // 确保有"全部"选项
  135. this.categories = res?.data ? [{ id: 0, name: '全部' }, ...res.data] : [];
  136. } catch (e) {
  137. console.error('加载分类失败', e);
  138. this.categories = [{ id: 0, name: '全部' }];
  139. }
  140. },
  141. // 加载热门小说
  142. async loadHotNovels() {
  143. try {
  144. const res = await this.$http.get('/api/novel/hot');
  145. // 处理不同API响应格式
  146. if (res?.data?.rows) {
  147. this.hotNovels = res.data.rows;
  148. } else if (Array.isArray(res?.data)) {
  149. this.hotNovels = res.data;
  150. } else if (Array.isArray(res)) {
  151. this.hotNovels = res;
  152. } else {
  153. console.warn('热门小说API返回格式未知', res);
  154. this.hotNovels = [];
  155. }
  156. } catch (e) {
  157. console.error('加载热门小说失败', e);
  158. this.hotNovels = [];
  159. }
  160. },
  161. // 加载小说列表
  162. async loadNovels() {
  163. this.loading = true;
  164. this.error = null;
  165. try {
  166. const res = await this.$http.get('/api/novel/list');
  167. console.log('小说列表API响应:', res);
  168. // 处理不同API响应格式
  169. if (res?.data?.rows) {
  170. this.novels = res.data.rows;
  171. } else if (Array.isArray(res?.data)) {
  172. this.novels = res.data;
  173. } else if (res?.data?.list) {
  174. this.novels = res.data.list;
  175. } else if (Array.isArray(res)) {
  176. this.novels = res;
  177. } else {
  178. console.warn('小说列表API返回格式未知', res);
  179. this.novels = [];
  180. this.error = '数据格式错误';
  181. }
  182. } catch (e) {
  183. console.error('加载小说失败', e);
  184. this.error = '加载失败,请重试';
  185. uni.showToast({
  186. title: '加载失败,请重试',
  187. icon: 'none'
  188. });
  189. this.novels = [];
  190. } finally {
  191. this.loading = false;
  192. }
  193. },
  194. // 封面URL处理
  195. getCoverUrl(cover) {
  196. if (!cover) return '/static/default-cover.jpg';
  197. if (cover.startsWith('http')) return cover;
  198. if (cover.startsWith('/')) return cover;
  199. return `${process.env.VUE_APP_BASE_URL || ''}/uploads/${cover}`;
  200. },
  201. // 其他方法...
  202. changeCategory(categoryId) {
  203. this.activeCategory = categoryId;
  204. this.currentCategoryName = this.categories.find(c => c.id === categoryId)?.name || '全部';
  205. this.loadNovels();
  206. },
  207. openNovel(novel) {
  208. if (novel.id) {
  209. uni.navigateTo({
  210. url: `/pages/novel/detail?id=${novel.id}`
  211. });
  212. } else {
  213. uni.showToast({
  214. title: '小说ID不存在',
  215. icon: 'none'
  216. });
  217. }
  218. },
  219. applyAuthor() {
  220. if (!this.$store.getters.token) {
  221. uni.showToast({ title: '请先登录', icon: 'none' });
  222. uni.navigateTo({ url: '/pages/login' });
  223. return;
  224. }
  225. uni.navigateTo({ url: '/pages/author/apply' });
  226. },
  227. goMore() {
  228. uni.navigateTo({
  229. url: '/pages/novel/more'
  230. });
  231. }
  232. }
  233. }
  234. </script>
  235. <style scoped>
  236. /* 添加加载状态样式 */
  237. .loading-container {
  238. display: flex;
  239. flex-direction: column;
  240. align-items: center;
  241. justify-content: center;
  242. padding: 100rpx 0;
  243. }
  244. .loading-icon {
  245. animation: rotate 1s linear infinite;
  246. margin-bottom: 20rpx;
  247. }
  248. @keyframes rotate {
  249. from { transform: rotate(0deg); }
  250. to { transform: rotate(360deg); }
  251. }
  252. /* 添加空状态样式 */
  253. .empty-container, .error-container {
  254. display: flex;
  255. flex-direction: column;
  256. align-items: center;
  257. justify-content: center;
  258. padding: 100rpx 0;
  259. text-align: center;
  260. }
  261. .empty-img {
  262. width: 200rpx;
  263. height: 200rpx;
  264. opacity: 0.6;
  265. margin-bottom: 40rpx;
  266. }
  267. .empty-text, .error-text {
  268. font-size: 32rpx;
  269. color: var(--text-color);
  270. margin-bottom: 40rpx;
  271. }
  272. .error-text {
  273. color: var(--error-color);
  274. }
  275. .btn-refresh {
  276. background-color: var(--primary-color);
  277. color: white;
  278. width: 60%;
  279. border-radius: 50rpx;
  280. }
  281. /* 确保网格布局正确 */
  282. .novel-grid {
  283. display: grid;
  284. grid-template-columns: repeat(3, 1fr);
  285. gap: 20rpx;
  286. padding: 20rpx;
  287. }
  288. .novel-item {
  289. background-color: var(--card-bg);
  290. border-radius: 12rpx;
  291. overflow: hidden;
  292. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
  293. }
  294. .novel-cover {
  295. width: 100%;
  296. height: 300rpx;
  297. background-color: #f5f5f5; /* 添加默认背景 */
  298. }
  299. .novel-title {
  300. display: block;
  301. font-size: 28rpx;
  302. padding: 10rpx 15rpx;
  303. white-space: nowrap;
  304. overflow: hidden;
  305. text-overflow: ellipsis;
  306. }
  307. .novel-author {
  308. display: block;
  309. font-size: 24rpx;
  310. color: #888;
  311. padding: 0 15rpx 15rpx;
  312. }
  313. </style>