图书馆小程序
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.

316 lines
9.4 KiB

1 week ago
  1. <template>
  2. <view class="collection-container">
  3. <!-- 吸顶 Tab -->
  4. <view class="tab-sticky">
  5. <my-tabs
  6. :tabData="tabData"
  7. :defaultIndex="currentIndex"
  8. :config="{ textColor: '#333' }"
  9. @tabClick="tabClick"
  10. />
  11. </view>
  12. <!-- 滑动内容区 -->
  13. <swiper
  14. class="swiper"
  15. :current="currentIndex"
  16. :style="{ height: currentSwiperHeight + 'px' }"
  17. @animationfinish="onSwiperEnd"
  18. @change="onSwiperChange"
  19. >
  20. <swiper-item v-for="(tabItem, idx) in tabData" :key="idx">
  21. <view class="list-wrapper">
  22. <!-- 加载中 -->
  23. <uni-load-more status="loading" v-if="loadingMap[tabItem.status]" />
  24. <!-- 空数据 -->
  25. <view class="empty" v-else-if="!listData[tabItem.status] || listData[tabItem.status].length === 0">
  26. 暂无{{ tabItem.label }}收藏
  27. </view>
  28. <!-- 列表 -->
  29. <block v-else>
  30. <!-- 图书收藏 -->
  31. <view class="recommendation-list">
  32. <view
  33. class="book-item"
  34. v-for="(item, index) in listData.book"
  35. :key="index"
  36. @click="goToBookDetail(item)"
  37. v-if="tabItem.status === 'book'"
  38. >
  39. <image class="book-cover" :src="item.cover"></image>
  40. <view class="book-title">{{ item.title }}</view>
  41. </view>
  42. </view>
  43. <!-- 活动收藏 -->
  44. <view
  45. class="activity-item"
  46. v-for="(item, index) in listData.activity"
  47. :key="index"
  48. @click="toActivityDetail(item)"
  49. v-if="tabItem.status === 'activity'"
  50. >
  51. <image class="activity-img" :src="item.imgUrl"></image>
  52. <view class="activity-info">
  53. <view class="activity-info-left">
  54. <text class="title">{{ item.title }}</text>
  55. <view class="time">
  56. <uni-icons class="time-icon" custom-prefix="iconfont" type="time" size="15"></uni-icons>
  57. <text>{{ item.time }}</text>
  58. </view>
  59. </view>
  60. <button
  61. class="activity-btn"
  62. :class="item.status === 0 ? 'disabled-btn' : ''"
  63. type="primary"
  64. :disabled="item.status === 0"
  65. >
  66. {{ item.status === 1 ? '立即参加' : '活动结束' }}
  67. </button>
  68. </view>
  69. </view>
  70. </block>
  71. <!-- 加载更多 -->
  72. <uni-load-more
  73. v-if="listData[tabItem.status] && listData[tabItem.status].length > 0 && !loadingMap[tabItem.status]"
  74. :status="loadMoreStatusMap[tabItem.status]"
  75. />
  76. </view>
  77. </swiper-item>
  78. </swiper>
  79. </view>
  80. </template>
  81. <script>
  82. import myTabs from "@/components/my-tabs/my-tabs.vue";
  83. export default {
  84. components: { myTabs },
  85. data() {
  86. return {
  87. tabData: [
  88. { label: "图书收藏", status: "book" },
  89. { label: "活动收藏", status: "activity" },
  90. ],
  91. currentIndex: 0,
  92. // 数据结构完全和借阅页一致
  93. listData: {},
  94. loadingMap: {},
  95. loadMoreStatusMap: {},
  96. pageMap: {},
  97. sizeMap: {},
  98. hasMoreMap: {},
  99. swiperHeightData: {},
  100. currentSwiperHeight: 400,
  101. currentPageScrollTop: 0,
  102. isRefreshing: false,
  103. };
  104. },
  105. onLoad() {
  106. this.initDataStructure();
  107. const firstTab = this.getCurrentTab();
  108. this.getListData(firstTab.status);
  109. },
  110. onShow() {
  111. const tabIndex = uni.getStorageSync("switch_tab_index");
  112. if (tabIndex !== "") {
  113. this.currentIndex = Number(tabIndex);
  114. const currentTab = this.getCurrentTab();
  115. this.getListData(currentTab.status);
  116. uni.removeStorageSync("switch_tab_index");
  117. }
  118. },
  119. onPullDownRefresh() {
  120. this.isRefreshing = true;
  121. const currentTab = this.getCurrentTab();
  122. this.refreshList(currentTab.status);
  123. },
  124. onReachBottom() {
  125. const currentTab = this.getCurrentTab();
  126. this.loadMoreList(currentTab.status);
  127. },
  128. onPageScroll(res) {
  129. this.currentPageScrollTop = res.scrollTop;
  130. },
  131. methods: {
  132. initDataStructure() {
  133. this.tabData.forEach((tab) => {
  134. const key = tab.status;
  135. this.$set(this.listData, key, []);
  136. this.$set(this.loadingMap, key, true);
  137. this.$set(this.loadMoreStatusMap, key, "");
  138. this.$set(this.pageMap, key, 1);
  139. this.$set(this.sizeMap, key, 10);
  140. this.$set(this.hasMoreMap, key, true);
  141. });
  142. },
  143. getCurrentTab() {
  144. return this.tabData[this.currentIndex];
  145. },
  146. // 获取列表
  147. async getListData(statusKey, isRefresh = false) {
  148. if (!isRefresh && this.listData[statusKey]?.length > 0) {
  149. this.loadingMap[statusKey] = false;
  150. return;
  151. }
  152. this.loadingMap[statusKey] = true;
  153. this.pageMap[statusKey] = 1;
  154. try {
  155. const res = await this.getCollectData(statusKey);
  156. this.listData[statusKey] = res;
  157. this.hasMoreMap[statusKey] = res.length === this.sizeMap[statusKey];
  158. this.loadMoreStatusMap[statusKey] = this.hasMoreMap[statusKey] ? "" : "no-more";
  159. } catch (err) {
  160. this.listData[statusKey] = [];
  161. } finally {
  162. this.loadingMap[statusKey] = false;
  163. this.isRefreshing = false;
  164. uni.stopPullDownRefresh();
  165. setTimeout(() => this.calcSwiperHeight(statusKey), 0);
  166. }
  167. },
  168. // 加载更多
  169. async loadMoreList(statusKey) {
  170. if (this.loadingMap[statusKey] || !this.hasMoreMap[statusKey] || this.isRefreshing) return;
  171. this.loadMoreStatusMap[statusKey] = "loading";
  172. this.pageMap[statusKey] += 1;
  173. try {
  174. const newData = await this.getCollectData(statusKey);
  175. if (newData.length > 0) {
  176. this.listData[statusKey] = [...this.listData[statusKey], ...newData];
  177. this.hasMoreMap[statusKey] = newData.length === this.sizeMap[statusKey];
  178. this.loadMoreStatusMap[statusKey] = this.hasMoreMap[statusKey] ? "" : "no-more";
  179. } else {
  180. this.hasMoreMap[statusKey] = false;
  181. this.loadMoreStatusMap[statusKey] = "no-more";
  182. }
  183. } catch (err) {
  184. this.loadMoreStatusMap[statusKey] = "";
  185. }
  186. setTimeout(() => this.calcSwiperHeight(statusKey), 0);
  187. },
  188. // 模拟接口(图书/活动收藏)
  189. getCollectData(type) {
  190. return new Promise((resolve) => {
  191. setTimeout(() => {
  192. if (type === "book") {
  193. resolve([
  194. { isbn: "1", title: "JavaScript高级程序设计", cover: "https://qiniu.aiyxlib.com/1606124577077" },
  195. { isbn: "2", title: "Vue3实战", cover: "https://qiniu.aiyxlib.com/1606124577077" },
  196. { isbn: "3", title: "深入理解计算机系统", cover: "https://qiniu.aiyxlib.com/1606124577077" },
  197. { isbn: "1", title: "JavaScript高级程序设计", cover: "https://qiniu.aiyxlib.com/1606124577077" }
  198. ]);
  199. } else {
  200. resolve([
  201. { imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: "读书分享会", time: "2025-11-03", status: 1 },
  202. { imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: "作者见面会", time: "2025-10-01", status: 0 },
  203. ]);
  204. }
  205. }, 300);
  206. });
  207. },
  208. refreshList(statusKey) {
  209. this.getListData(statusKey, true);
  210. },
  211. // 计算swiper高度
  212. calcSwiperHeight(statusKey) {
  213. const selector = statusKey === "book" ? ".book-item" : ".activity-item";
  214. const query = uni.createSelectorQuery().in(this);
  215. query.selectAll(selector).boundingClientRect((res) => {
  216. let total = 200;
  217. if (res?.length) total = res.reduce((t, h) => t + h.height + 10, 0);
  218. this.swiperHeightData[statusKey] = total;
  219. this.currentSwiperHeight = total;
  220. }).exec();
  221. },
  222. tabClick(index) {
  223. this.currentIndex = index;
  224. const tab = this.getCurrentTab();
  225. if (this.currentPageScrollTop > 100) uni.pageScrollTo({ scrollTop: 0, duration: 100 });
  226. if (!this.listData[tab.status]?.length) this.getListData(tab.status);
  227. else this.currentSwiperHeight = this.swiperHeightData[tab.status] || 400;
  228. },
  229. onSwiperChange(e) {
  230. if (e.detail.source === "touch") this.currentIndex = e.detail.current;
  231. },
  232. onSwiperEnd() {
  233. const tab = this.getCurrentTab();
  234. if (!this.listData[tab.status]?.length) this.getListData(tab.status);
  235. else this.currentSwiperHeight = this.swiperHeightData[tab.status] || 400;
  236. },
  237. goToBookDetail(item) {
  238. uni.navigateTo({ url: `/subpkg/pages/book-detail/book-detail?isbn=${item.isbn}` });
  239. },
  240. toActivityDetail(item) {
  241. uni.navigateTo({ url: `/subpkg/pages/activity-detail/activity-detail?title=${item.title}` });
  242. },
  243. },
  244. };
  245. </script>
  246. <style lang="scss" scoped>
  247. .collection-container {
  248. background-color: #f5f5f5;
  249. min-height: 100vh;
  250. .tab-sticky {
  251. position: sticky;
  252. top: 0;
  253. z-index: 99;
  254. background: #fff;
  255. }
  256. .swiper {
  257. width: 100%;
  258. min-height: 300px;
  259. }
  260. .list-wrapper {
  261. padding: 10px;
  262. }
  263. .empty {
  264. text-align: center;
  265. padding: 100px 0;
  266. color: #999;
  267. font-size: 14px;
  268. }
  269. .recommendation-list{
  270. flex-wrap: wrap;
  271. justify-content: flex-start;
  272. .book-item {
  273. margin-bottom: 16px;
  274. }
  275. }
  276. }
  277. ::v-deep .uni-load-more {
  278. height: auto !important;
  279. }
  280. </style>