ranking.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  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="placeholder"></view>
  10. </view>
  11. <!-- 横向标签栏 -->
  12. <view class="tab-bar">
  13. <view
  14. class="tab-item"
  15. v-for="(tab, index) in rankingTabs"
  16. :key="index"
  17. :class="{ 'tab-active': activeTab === index }"
  18. @click="switchTab(index)"
  19. >
  20. <text class="tab-text" :class="{ 'tab-text-active': activeTab === index }">{{ tab }}</text>
  21. </view>
  22. </view>
  23. <!-- 主体内容区 -->
  24. <view class="main-content">
  25. <!-- 左侧分类边栏 -->
  26. <scroll-view class="sidebar" scroll-y>
  27. <view
  28. class="category-item"
  29. v-for="(category, index) in categories"
  30. :key="index"
  31. :class="{ 'category-active': activeCategory === index }"
  32. @click="switchCategory(index)"
  33. >
  34. <view class="category-indicator" v-if="activeCategory === index"></view>
  35. <text class="category-text" :class="{ 'category-text-active': activeCategory === index }">{{ category }}</text>
  36. </view>
  37. </scroll-view>
  38. <!-- 右侧书籍列表 -->
  39. <scroll-view class="book-list-container" scroll-y @scrolltolower="loadMore" :lower-threshold="100">
  40. <view
  41. class="book-item"
  42. v-for="(book, index) in bookList"
  43. :key="book.id || index"
  44. @click="goToBookDetail(book)"
  45. >
  46. <text class="rank-number">{{ index + 1 }}</text>
  47. <image
  48. class="book-cover"
  49. :src="book.cover"
  50. mode="aspectFill"
  51. :lazy-load="true"
  52. @error="handleImageError(index)"
  53. ></image>
  54. <view class="book-info">
  55. <text class="book-title">{{ book.title }}</text>
  56. <text class="book-author">{{ book.author }}</text>
  57. </view>
  58. <view class="divider-line"></view>
  59. </view>
  60. <!-- 加载中提示 -->
  61. <view class="load-more" v-if="isLoading">
  62. <text class="load-more-text">加载中...</text>
  63. </view>
  64. <!-- 空状态 -->
  65. <view class="load-more" v-else-if="!isLoading && bookList.length === 0">
  66. <text class="load-more-text">暂无数据</text>
  67. </view>
  68. </scroll-view>
  69. </view>
  70. </view>
  71. </template>
  72. <script>
  73. import { getAllRankingGroups, getRankingByCode, getAllCategories } from '../../utils/api.js'
  74. export default {
  75. data() {
  76. return {
  77. activeTab: 0, // 当前选中的标签索引
  78. activeCategory: 0, // 当前选中的分类索引
  79. hasMore: true,
  80. page: 1,
  81. isLoading: false,
  82. rankingTabs: [], // 从数据库获取的排行榜类型
  83. rankingGroups: [], // 排行榜组数据(包含id、name、code)
  84. categories: [], // 从数据库获取的分类列表
  85. categoryList: [], // 分类数据(包含id、name)
  86. bookList: [] // 书籍列表(从数据库获取)
  87. }
  88. },
  89. onLoad() {
  90. // 页面加载时初始化数据
  91. this.initData();
  92. },
  93. methods: {
  94. // 初始化数据:加载排行榜类型和分类
  95. async initData() {
  96. try {
  97. this.isLoading = true;
  98. // 并行加载排行榜类型和分类
  99. await Promise.all([
  100. this.loadRankingGroups(),
  101. this.loadCategories()
  102. ]);
  103. // 加载完成后,加载书籍列表
  104. await this.loadBookList();
  105. } catch (err) {
  106. console.error('初始化数据失败:', err);
  107. uni.showToast({
  108. title: '加载失败,请重试',
  109. icon: 'none'
  110. });
  111. } finally {
  112. this.isLoading = false;
  113. }
  114. },
  115. // 加载排行榜类型
  116. async loadRankingGroups() {
  117. try {
  118. const res = await getAllRankingGroups();
  119. if (res && res.code === 200 && res.data) {
  120. this.rankingGroups = res.data;
  121. this.rankingTabs = res.data.map(group => group.name);
  122. console.log('排行榜类型加载成功:', this.rankingTabs);
  123. } else {
  124. console.warn('排行榜类型数据为空,使用默认数据');
  125. // 如果数据库没有数据,使用默认值
  126. this.rankingTabs = ['畅销榜', '热门榜', '口碑榜', '好评榜', '新书榜'];
  127. this.rankingGroups = [
  128. { id: 1, name: '畅销榜', code: 'bestseller' },
  129. { id: 2, name: '热门榜', code: 'popular' },
  130. { id: 3, name: '口碑榜', code: 'reputation' },
  131. { id: 4, name: '好评榜', code: 'good_reviews' },
  132. { id: 5, name: '新书榜', code: 'new_books' }
  133. ];
  134. }
  135. } catch (err) {
  136. console.error('加载排行榜类型失败:', err);
  137. // 使用默认值
  138. this.rankingTabs = ['畅销榜', '热门榜', '口碑榜', '好评榜', '新书榜'];
  139. this.rankingGroups = [
  140. { id: 1, name: '畅销榜', code: 'bestseller' },
  141. { id: 2, name: '热门榜', code: 'popular' },
  142. { id: 3, name: '口碑榜', code: 'reputation' },
  143. { id: 4, name: '好评榜', code: 'good_reviews' },
  144. { id: 5, name: '新书榜', code: 'new_books' }
  145. ];
  146. }
  147. },
  148. // 加载分类列表
  149. async loadCategories() {
  150. try {
  151. const res = await getAllCategories();
  152. if (res && res.code === 200 && res.data) {
  153. this.categoryList = res.data;
  154. // 添加"全部分类"选项
  155. this.categories = ['全部分类', ...res.data.map(cat => cat.name)];
  156. console.log('分类列表加载成功:', this.categories);
  157. } else {
  158. console.warn('分类数据为空,使用默认数据');
  159. // 如果数据库没有数据,使用默认值
  160. this.categories = ['全部分类', '文艺', '历史', '人文', '科学', '教育', '生活', '外语', '商业', '养生', '职场', '少儿'];
  161. this.categoryList = [
  162. { id: 1, name: '文艺' },
  163. { id: 2, name: '历史' },
  164. { id: 3, name: '人文' },
  165. { id: 4, name: '科学' },
  166. { id: 5, name: '教育' },
  167. { id: 6, name: '生活' },
  168. { id: 7, name: '外语' },
  169. { id: 8, name: '商业' },
  170. { id: 9, name: '养生' },
  171. { id: 10, name: '职场' },
  172. { id: 11, name: '少儿' }
  173. ];
  174. }
  175. } catch (err) {
  176. console.error('加载分类失败:', err);
  177. // 使用默认值
  178. this.categories = ['全部分类', '文艺', '历史', '人文', '科学', '教育', '生活', '外语', '商业', '养生', '职场', '少儿'];
  179. this.categoryList = [
  180. { id: 1, name: '文艺' },
  181. { id: 2, name: '历史' },
  182. { id: 3, name: '人文' },
  183. { id: 4, name: '科学' },
  184. { id: 5, name: '教育' },
  185. { id: 6, name: '生活' },
  186. { id: 7, name: '外语' },
  187. { id: 8, name: '商业' },
  188. { id: 9, name: '养生' },
  189. { id: 10, name: '职场' },
  190. { id: 11, name: '少儿' }
  191. ];
  192. }
  193. },
  194. goBack() {
  195. uni.navigateBack({
  196. delta: 1
  197. });
  198. },
  199. switchTab(index) {
  200. if (this.activeTab !== index) {
  201. this.activeTab = index;
  202. this.page = 1;
  203. this.hasMore = true;
  204. this.loadBookList();
  205. }
  206. },
  207. switchCategory(index) {
  208. if (this.activeCategory !== index) {
  209. this.activeCategory = index;
  210. this.page = 1;
  211. this.hasMore = true;
  212. this.loadBookList();
  213. }
  214. },
  215. goToBookDetail(book) {
  216. if (!book || !book.id) {
  217. uni.showToast({
  218. title: '书籍信息不完整',
  219. icon: 'none'
  220. })
  221. return
  222. }
  223. uni.navigateTo({
  224. url: `/pages/book-detail/book-detail?bookId=${book.id}`
  225. });
  226. },
  227. handleImageError(index) {
  228. // 图片加载失败时使用备用图片
  229. if (this.bookList[index]) {
  230. this.bookList[index].cover = `https://picsum.photos/seed/fallback${index}/200/300`;
  231. }
  232. },
  233. // 从后端加载书籍列表
  234. async loadBookList() {
  235. try {
  236. this.isLoading = true;
  237. // 获取当前选中的排行榜类型
  238. if (!this.rankingGroups || this.rankingGroups.length === 0) {
  239. console.warn('排行榜类型未加载');
  240. this.bookList = [];
  241. return;
  242. }
  243. const currentGroup = this.rankingGroups[this.activeTab];
  244. if (!currentGroup || !currentGroup.code) {
  245. console.warn('当前排行榜类型无效');
  246. this.bookList = [];
  247. return;
  248. }
  249. // 获取当前选中的分类ID
  250. let categoryId = null;
  251. if (this.activeCategory > 0 && this.categoryList && this.categoryList.length > 0) {
  252. // activeCategory为0表示"全部分类",从1开始才是实际分类
  253. const categoryIndex = this.activeCategory - 1;
  254. if (categoryIndex >= 0 && categoryIndex < this.categoryList.length) {
  255. categoryId = this.categoryList[categoryIndex].id;
  256. }
  257. }
  258. // 调用API获取排行榜书籍
  259. const res = await getRankingByCode(currentGroup.code, categoryId);
  260. if (res && res.code === 200 && res.data) {
  261. // 转换数据格式
  262. this.bookList = res.data.map((book, index) => ({
  263. id: book.id,
  264. title: book.title || '未知书名',
  265. author: book.author || '未知作者',
  266. cover: book.cover || book.image || `https://picsum.photos/seed/book${book.id}/200/300`,
  267. rank: index + 1
  268. }));
  269. console.log('排行榜书籍加载成功,共', this.bookList.length, '本');
  270. } else {
  271. console.warn('排行榜书籍数据为空');
  272. this.bookList = [];
  273. }
  274. } catch (err) {
  275. console.error('加载排行榜书籍失败:', err);
  276. this.bookList = [];
  277. uni.showToast({
  278. title: '加载失败,请重试',
  279. icon: 'none'
  280. });
  281. } finally {
  282. this.isLoading = false;
  283. this.hasMore = false; // 排行榜数据不分页,一次性加载
  284. }
  285. },
  286. // 加载更多(排行榜数据一次性加载,此方法保留但不使用)
  287. loadMore() {
  288. // 排行榜数据一次性加载,不需要分页
  289. // 此方法保留用于兼容,但不执行任何操作
  290. if (this.hasMore) {
  291. this.hasMore = false;
  292. }
  293. }
  294. }
  295. }
  296. </script>
  297. <style scoped>
  298. .container {
  299. width: 100%;
  300. height: 100vh;
  301. background-color: #FFFFFF;
  302. display: flex;
  303. flex-direction: column;
  304. }
  305. /* 顶部导航栏 */
  306. .header {
  307. display: flex;
  308. align-items: center;
  309. justify-content: space-between;
  310. padding: 20rpx 30rpx;
  311. background-color: #4DB6AC;
  312. position: relative;
  313. }
  314. .back-btn {
  315. width: 60rpx;
  316. height: 60rpx;
  317. display: flex;
  318. align-items: center;
  319. justify-content: center;
  320. }
  321. .back-icon {
  322. font-size: 40rpx;
  323. color: #FFFFFF;
  324. font-weight: bold;
  325. }
  326. .header-title {
  327. font-size: 36rpx;
  328. font-weight: bold;
  329. color: #FFFFFF;
  330. position: absolute;
  331. left: 50%;
  332. transform: translateX(-50%);
  333. }
  334. .placeholder {
  335. width: 60rpx;
  336. }
  337. /* 横向标签栏 */
  338. .tab-bar {
  339. display: flex;
  340. align-items: center;
  341. background-color: #66CCC2;
  342. padding: 0 20rpx;
  343. overflow-x: auto;
  344. white-space: nowrap;
  345. }
  346. .tab-item {
  347. flex: 1;
  348. display: flex;
  349. align-items: center;
  350. justify-content: center;
  351. padding: 24rpx 20rpx;
  352. min-width: 120rpx;
  353. }
  354. .tab-text {
  355. font-size: 28rpx;
  356. color: #FFFFFF;
  357. transition: color 0.3s ease;
  358. }
  359. .tab-text-active {
  360. font-size: 30rpx;
  361. font-weight: bold;
  362. color: #2E7D32;
  363. }
  364. .tab-active {
  365. border-bottom: 4rpx solid #2E7D32;
  366. }
  367. /* 主体内容区 */
  368. .main-content {
  369. flex: 1;
  370. display: flex;
  371. width: 100%;
  372. height: 0;
  373. overflow: hidden;
  374. }
  375. /* 左侧分类边栏 */
  376. .sidebar {
  377. width: 160rpx;
  378. height: 100%;
  379. background-color: #FFFFFF;
  380. border-right: 1rpx solid #E5E5E5;
  381. }
  382. .category-item {
  383. position: relative;
  384. display: flex;
  385. align-items: center;
  386. justify-content: center;
  387. padding: 30rpx 20rpx;
  388. min-height: 80rpx;
  389. box-sizing: border-box;
  390. }
  391. .category-active {
  392. background-color: #F5F5F5;
  393. }
  394. .category-indicator {
  395. position: absolute;
  396. left: 0;
  397. top: 0;
  398. bottom: 0;
  399. width: 6rpx;
  400. background-color: #4DB6AC;
  401. }
  402. .category-text {
  403. font-size: 28rpx;
  404. color: #999999;
  405. transition: color 0.3s ease;
  406. }
  407. .category-text-active {
  408. color: #4DB6AC;
  409. font-weight: bold;
  410. }
  411. /* 右侧书籍列表 */
  412. .book-list-container {
  413. flex: 1;
  414. height: 100%;
  415. background-color: #FFFFFF;
  416. padding: 0 30rpx;
  417. }
  418. .book-item {
  419. display: flex;
  420. align-items: center;
  421. padding: 30rpx 0;
  422. position: relative;
  423. }
  424. .rank-number {
  425. font-size: 48rpx;
  426. font-weight: bold;
  427. color: #CCCCCC;
  428. width: 80rpx;
  429. text-align: center;
  430. flex-shrink: 0;
  431. }
  432. .book-cover {
  433. width: 120rpx;
  434. height: 160rpx;
  435. border-radius: 8rpx;
  436. margin-right: 24rpx;
  437. flex-shrink: 0;
  438. background-color: #F5F5F5;
  439. }
  440. .book-info {
  441. flex: 1;
  442. display: flex;
  443. flex-direction: column;
  444. justify-content: center;
  445. min-width: 0;
  446. }
  447. .book-title {
  448. font-size: 32rpx;
  449. font-weight: bold;
  450. color: #000000;
  451. margin-bottom: 12rpx;
  452. overflow: hidden;
  453. text-overflow: ellipsis;
  454. white-space: nowrap;
  455. }
  456. .book-author {
  457. font-size: 26rpx;
  458. color: #999999;
  459. overflow: hidden;
  460. text-overflow: ellipsis;
  461. white-space: nowrap;
  462. }
  463. .divider-line {
  464. position: absolute;
  465. bottom: 0;
  466. left: 80rpx;
  467. right: 0;
  468. height: 1rpx;
  469. background-color: #E5E5E5;
  470. }
  471. /* 加载更多 */
  472. .load-more {
  473. width: 100%;
  474. height: 80rpx;
  475. display: flex;
  476. align-items: center;
  477. justify-content: center;
  478. margin-top: 20rpx;
  479. margin-bottom: 40rpx;
  480. }
  481. .load-more-text {
  482. font-size: 28rpx;
  483. color: #999999;
  484. }
  485. </style>