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

437 lines
13 KiB

2 weeks ago
  1. <template>
  2. <view class="lending-container">
  3. <view class="tab-sticky">
  4. <my-tabs
  5. :tabData="tabData"
  6. :defaultIndex="currentIndex"
  7. :config="{ textColor: '#333' }"
  8. @tabClick="tabClick"
  9. />
  10. </view>
  11. <swiper
  12. class="swiper"
  13. :current="currentIndex"
  14. :style="{ height: currentSwiperHeight + 'px' }"
  15. @animationfinish="onSwiperEnd"
  16. @change="onSwiperChange"
  17. >
  18. <swiper-item v-for="(tabItem, idx) in tabData" :key="idx">
  19. <view class="list-wrapper">
  20. <!-- 首次加载 -->
  21. <uni-load-more status="loading" v-if="loadingMap[tabItem.status]" />
  22. <!-- 空数据 -->
  23. <view class="empty" v-else-if="!listData[tabItem.status] || listData[tabItem.status].length === 0">
  24. 暂无{{ tabItem.label }}记录
  25. </view>
  26. <!-- 列表 -->
  27. <block v-else>
  28. <!-- @click="goToDetail(item)" -->
  29. <lending-list-item
  30. :class="'list-item-' + tabItem.status"
  31. v-for="(item, index) in listData[tabItem.status]"
  32. :key="index"
  33. :data="item"
  34. :ranking="index + 1"
  35. />
  36. </block>
  37. <!-- 上拉加载更多 -->
  38. <uni-load-more
  39. v-if="listData[tabItem.status] && listData[tabItem.status].length > 0 && !loadingMap[tabItem.status]"
  40. :status="loadMoreStatusMap[tabItem.status]"
  41. />
  42. </view>
  43. </swiper-item>
  44. </swiper>
  45. </view>
  46. </template>
  47. <script>
  48. import myTabs from "@/components/my-tabs/my-tabs.vue";
  49. import lendingListItem from "@/components/lending-list-item/lending-list-item.vue";
  50. export default {
  51. components: { myTabs, lendingListItem },
  52. data() {
  53. return {
  54. tabData: [
  55. { label: "全部", status: "all", apiStatus: -1 },
  56. { label: "在借中", status: "lending", apiStatus: 0 },
  57. { label: "将过期", status: "expiring", apiStatus: 1 },
  58. { label: "已过期", status: "expired", apiStatus: 2 },
  59. ],
  60. currentIndex: 0,
  61. listData: {},
  62. loadingMap: {}, // 首次加载/刷新 loading
  63. loadMoreStatusMap: {}, // 加载更多状态:loading / no-more / ""
  64. pageMap: {}, // 分页页码
  65. sizeMap: {}, // 每页条数
  66. hasMoreMap: {}, // 是否还有更多
  67. swiperHeightData: {},
  68. currentSwiperHeight: 400,
  69. currentPageScrollTop: 0,
  70. isRefreshing: false // 是否正在下拉刷新
  71. };
  72. },
  73. onLoad() {
  74. this.initDataStructure();
  75. // 加载第一个tab的数据
  76. const firstTab = this.getCurrentTab();
  77. if (firstTab) {
  78. this.getListData(firstTab.status);
  79. }
  80. },
  81. onShow() {
  82. // 读取要切换的 tabIndex
  83. const tabIndex = uni.getStorageSync('switch_tab_index');
  84. if (tabIndex !== undefined && tabIndex !== '') {
  85. this.currentIndex = Number(tabIndex);
  86. // 加载对应 tab 数据
  87. const currentTab = this.getCurrentTab();
  88. if (currentTab) {
  89. this.getListData(currentTab.status);
  90. }
  91. // 用完清除,防止下次进来重复触发
  92. uni.removeStorageSync('switch_tab_index');
  93. }
  94. },
  95. // 下拉刷新
  96. onPullDownRefresh() {
  97. this.isRefreshing = true;
  98. const currentTab = this.getCurrentTab();
  99. this.refreshList(currentTab.status);
  100. },
  101. // 上拉加载更多
  102. onReachBottom() {
  103. const currentTab = this.getCurrentTab();
  104. this.loadMoreList(currentTab.status);
  105. },
  106. onPageScroll(res) {
  107. this.currentPageScrollTop = res.scrollTop;
  108. },
  109. methods: {
  110. // 初始化所有状态
  111. initDataStructure() {
  112. this.tabData.forEach(tab => {
  113. const key = tab.status;
  114. this.$set(this.listData, key, []);
  115. this.$set(this.loadingMap, key, true);
  116. this.$set(this.loadMoreStatusMap, key, "");
  117. this.$set(this.pageMap, key, 1);
  118. this.$set(this.sizeMap, key, 10);
  119. this.$set(this.hasMoreMap, key, true);
  120. });
  121. },
  122. getCurrentTab() {
  123. return this.tabData[this.currentIndex];
  124. },
  125. // 获取列表(首次/刷新)
  126. async getListData(statusKey, isRefresh = false) {
  127. const tab = this.tabData.find(item => item.status === statusKey);
  128. if (!tab) return;
  129. if (!isRefresh && this.listData[statusKey]?.length > 0) {
  130. this.loadingMap[statusKey] = false;
  131. return;
  132. }
  133. this.loadingMap[statusKey] = true;
  134. this.pageMap[statusKey] = 1;
  135. try {
  136. const data = await this.fetchBorrowList(tab.apiStatus, this.pageMap[statusKey], this.sizeMap[statusKey]);
  137. let list = data.records || [];
  138. list = list.map(item => {
  139. // 已归还
  140. if (item.realityTime) {
  141. if (new Date(item.realityTime) <= new Date(item.returnTime)) {
  142. item.returnBook = 2; // 准时归还
  143. } else {
  144. item.returnBook = 3; // 逾期归还
  145. }
  146. } else {
  147. // 未归还
  148. const now = new Date();
  149. const returnDate = new Date(item.returnTime);
  150. const diffDay = Math.ceil((returnDate - now) / (1000 * 3600 * 24));
  151. if (diffDay < 0) {
  152. // 1. 已逾期(应还时间 < 当前时间)
  153. item.returnBook = 3;
  154. }else if (diffDay <= 3) {
  155. // 2. 临期(剩余 0~3 天)
  156. item.returnBook = 1;
  157. } else {
  158. // 3. 正常在借
  159. item.returnBook = 0;
  160. }
  161. }
  162. return item;
  163. });
  164. this.listData[statusKey] = list;
  165. this.hasMoreMap[statusKey] = list.length === this.sizeMap[statusKey];
  166. this.loadMoreStatusMap[statusKey] = this.hasMoreMap[statusKey] ? "" : "no-more";
  167. } catch (err) {
  168. this.listData[statusKey] = [];
  169. } finally {
  170. this.loadingMap[statusKey] = false;
  171. this.isRefreshing = false;
  172. uni.stopPullDownRefresh();
  173. setTimeout(async () => {
  174. this.calcSwiperHeight(statusKey);
  175. }, 0);
  176. }
  177. },
  178. // 加载更多
  179. async loadMoreList(statusKey) {
  180. if (this.loadingMap[statusKey] || !this.hasMoreMap[statusKey] || this.isRefreshing) return;
  181. this.loadMoreStatusMap[statusKey] = "loading";
  182. this.pageMap[statusKey] += 1;
  183. const tab = this.tabData.find(item => item.status === statusKey);
  184. try {
  185. const data = await this.fetchBorrowList(tab.apiStatus, this.pageMap[statusKey], this.sizeMap[statusKey]);
  186. let newList = data.records || [];
  187. newList = newList.map(item => {
  188. // 已归还
  189. if (item.realityTime) {
  190. if (new Date(item.realityTime) <= new Date(item.returnTime)) {
  191. item.returnBook = 2; // 准时归还
  192. } else {
  193. item.returnBook = 3; // 逾期归还
  194. }
  195. } else {
  196. // 未归还
  197. const now = new Date();
  198. const returnDate = new Date(item.returnTime);
  199. const diffDay = Math.ceil((returnDate - now) / (1000 * 3600 * 24));
  200. if (diffDay < 0) {
  201. // 1. 已逾期(应还时间 < 当前时间)
  202. item.returnBook = 3;
  203. }else if (diffDay <= 3) {
  204. // 2. 临期(剩余 0~3 天)
  205. item.returnBook = 1;
  206. } else {
  207. // 3. 正常在借
  208. item.returnBook = 0;
  209. }
  210. }
  211. return item;
  212. });
  213. if (newList.length > 0) {
  214. this.listData[statusKey] = [...this.listData[statusKey], ...newList];
  215. this.hasMoreMap[statusKey] = newList.length === this.sizeMap[statusKey];
  216. this.loadMoreStatusMap[statusKey] = this.hasMoreMap[statusKey] ? "" : "no-more";
  217. } else {
  218. this.hasMoreMap[statusKey] = false;
  219. this.loadMoreStatusMap[statusKey] = "no-more";
  220. }
  221. } catch (err) {
  222. this.loadMoreStatusMap[statusKey] = "";
  223. }
  224. setTimeout(async () => {
  225. this.calcSwiperHeight(statusKey);
  226. }, 0);
  227. },
  228. // 下拉刷新
  229. refreshList(statusKey) {
  230. this.getListData(statusKey, true);
  231. },
  232. // 模拟接口(带分页)
  233. async fetchBorrowList(apiStatus, pageNum, pageSize) {
  234. return new Promise((resolve) => {
  235. setTimeout(() => {
  236. const mock = this.getMockData(apiStatus, pageNum, pageSize);
  237. resolve({
  238. records: mock,
  239. total: mock.length * 3
  240. });
  241. }, 500);
  242. });
  243. },
  244. // 模拟分页数据
  245. getMockData(apiStatus, pageNum, pageSize) {
  246. const base = [
  247. {
  248. imgCover: "https://qiniu.aiyxlib.com/1606124577077",
  249. title: "名侦探柯南",
  250. nickname: "青山刚昌",
  251. publish: "长春出版社",
  252. isbn: "1001",
  253. desc: '精心提炼20种GPT提问方法及指令,从入门到进阶再到精通,100个。',
  254. startTime: "2026-03-01",
  255. returnTime: "2026-04-30", // 应还时间:还有10天左右 → 在借中
  256. realityTime: ""
  257. },
  258. {
  259. imgCover: "https://qiniu.aiyxlib.com/1606124577077",
  260. title: "三体",
  261. nickname: "刘慈慈",
  262. publish: "重庆出版社",
  263. isbn: "1002",
  264. desc: '精心提炼20种GPT提问方法及指令,从入门到进阶再到精通,100个。',
  265. startTime: "2026-03-10",
  266. returnTime: "2026-04-22", // 应还时间:剩2天 → 临期
  267. realityTime: ""
  268. },
  269. {
  270. imgCover: "https://qiniu.aiyxlib.com/1606124577077",
  271. title: "红楼梦",
  272. nickname: "曹雪芹",
  273. publish: "人民文学",
  274. isbn: "1003",
  275. desc: '精心提炼20种GPT提问方法及指令,从入门到进阶再到精通,100个。',
  276. startTime: "2026-02-01",
  277. returnTime: "2026-03-25", // 已过期 → 逾期
  278. realityTime: ""
  279. },
  280. {
  281. imgCover: "https://qiniu.aiyxlib.com/1606124577077",
  282. title: "西游记",
  283. nickname: "吴承恩",
  284. publish: "中华书局",
  285. isbn: "1004",
  286. desc: '精心提炼20种GPT提问方法及指令,从入门到进阶再到精通,100个。',
  287. startTime: "2026-02-10",
  288. returnTime: "2026-04-01",
  289. realityTime: "2026-03-31" // 提前归还 → 准时
  290. }
  291. ];
  292. if (pageNum >= 3) { // 第3页开始返回空
  293. return [];
  294. }
  295. const list = [...base];
  296. if (pageNum > 1) {
  297. list.push(...base.map(item => ({ ...item, isbn: item.isbn + pageNum })));
  298. }
  299. switch (apiStatus) {
  300. case -1: return list;
  301. case 0: return list;
  302. case 1: return [list[0], list[1]];
  303. case 2: return [];
  304. default: return [];
  305. }
  306. },
  307. // 计算swiper高度
  308. calcSwiperHeight(statusKey) {
  309. const selector = `.list-item-${statusKey}`;
  310. const query = uni.createSelectorQuery().in(this);
  311. query.selectAll(selector).boundingClientRect((res) => {
  312. let total = 200;
  313. if (res?.length) {
  314. total = res.reduce((t, h) => t + h.height + 8, 0);
  315. }
  316. this.swiperHeightData[statusKey] = total;
  317. this.currentSwiperHeight = total;
  318. }).exec();
  319. },
  320. // tab切换
  321. tabClick(index) {
  322. this.currentIndex = index;
  323. const tab = this.getCurrentTab();
  324. if (this.currentPageScrollTop > 100) {
  325. uni.pageScrollTo({ scrollTop: 0, duration: 100 });
  326. }
  327. if (!this.listData[tab.status]?.length) {
  328. this.getListData(tab.status);
  329. } else {
  330. this.currentSwiperHeight = this.swiperHeightData[tab.status] || 400;
  331. }
  332. },
  333. onSwiperChange(e) {
  334. if (e.detail.source === "touch") {
  335. this.currentIndex = e.detail.current;
  336. }
  337. },
  338. onSwiperEnd() {
  339. const tab = this.getCurrentTab();
  340. if (!this.listData[tab.status]?.length) {
  341. this.getListData(tab.status);
  342. } else {
  343. this.currentSwiperHeight = this.swiperHeightData[tab.status] || 400;
  344. }
  345. },
  346. goToDetail(item) {
  347. uni.navigateTo({
  348. url: `/subpkg/pages/book-detail/book-detail?isbn=${item.isbn}`
  349. });
  350. }
  351. }
  352. };
  353. </script>
  354. <style lang="scss" scoped>
  355. .lending-container {
  356. background-color: #f5f5f5;
  357. min-height: 100vh;
  358. .tab-sticky {
  359. position: sticky;
  360. top: 0;
  361. z-index: 99;
  362. background: #fff;
  363. }
  364. .swiper {
  365. width: 100%;
  366. min-height: 300px;
  367. }
  368. .swiper-item {
  369. width: 100%;
  370. height: 100%;
  371. }
  372. .list-wrapper {
  373. padding: 10px;
  374. box-sizing: border-box;
  375. }
  376. .empty {
  377. text-align: center;
  378. padding: 100px 0;
  379. color: #999;
  380. font-size: 14px;
  381. }
  382. }
  383. ::v-deep .uni-load-more{
  384. height: auto !important;
  385. }
  386. </style>