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

479 lines
14 KiB

1 month 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.loadingMap[firstTab.status] = true;
  79. this.getListData(firstTab.status);
  80. }
  81. },
  82. onShow() {
  83. // 读取要切换的 tabIndex
  84. const tabIndex = uni.getStorageSync('switch_tab_index');
  85. if (tabIndex !== undefined && tabIndex !== '') {
  86. this.currentIndex = Number(tabIndex);
  87. // 加载对应 tab 数据
  88. const currentTab = this.getCurrentTab();
  89. if (currentTab) {
  90. this.loadingMap[currentTab.status] = true;
  91. this.getListData(currentTab.status);
  92. }
  93. // 用完清除,防止下次进来重复触发
  94. uni.removeStorageSync('switch_tab_index');
  95. }
  96. },
  97. // 下拉刷新
  98. onPullDownRefresh() {
  99. this.isRefreshing = true;
  100. const currentTab = this.getCurrentTab();
  101. this.refreshList(currentTab.status);
  102. },
  103. // 上拉加载更多
  104. onReachBottom() {
  105. const currentTab = this.getCurrentTab();
  106. this.loadMoreList(currentTab.status);
  107. },
  108. onPageScroll(res) {
  109. this.currentPageScrollTop = res.scrollTop;
  110. },
  111. methods: {
  112. // 初始化所有状态
  113. initDataStructure() {
  114. this.tabData.forEach(tab => {
  115. const key = tab.status;
  116. this.$set(this.listData, key, []);
  117. this.$set(this.loadingMap, key, false);
  118. this.$set(this.loadMoreStatusMap, key, "");
  119. this.$set(this.pageMap, key, 1);
  120. // 在借中每页4条,其他tab每页10条
  121. this.$set(this.sizeMap, key, key === 'lending' ? 4 : 10);
  122. this.$set(this.hasMoreMap, key, true);
  123. });
  124. },
  125. getCurrentTab() {
  126. return this.tabData[this.currentIndex];
  127. },
  128. // 处理借阅数据
  129. processBorrowData(list) {
  130. return list.map(item => {
  131. // 已归还
  132. if (item.realityTime) {
  133. if (new Date(item.realityTime) <= new Date(item.returnTime)) {
  134. item.returnBook = 2; // 准时归还
  135. } else {
  136. item.returnBook = 3; // 逾期归还
  137. }
  138. } else {
  139. // 未归还
  140. const now = new Date();
  141. const returnDate = new Date(item.returnTime);
  142. const diffDay = Math.ceil((returnDate - now) / (1000 * 3600 * 24));
  143. if (diffDay < 0) {
  144. // 1. 已逾期(应还时间 < 当前时间)
  145. item.returnBook = 3;
  146. }else if (diffDay <= 3) {
  147. // 2. 临期(剩余 0~3 天)
  148. item.returnBook = 1;
  149. } else {
  150. // 3. 正常在借
  151. item.returnBook = 0;
  152. }
  153. }
  154. return item;
  155. });
  156. },
  157. // 获取列表(首次/刷新)
  158. async getListData(statusKey, isRefresh = false) {
  159. const tab = this.tabData.find(item => item.status === statusKey);
  160. if (!tab) return;
  161. if (!isRefresh && this.listData[statusKey]?.length > 0) {
  162. this.loadingMap[statusKey] = false;
  163. return;
  164. }
  165. this.loadingMap[statusKey] = true;
  166. this.pageMap[statusKey] = 1;
  167. try {
  168. const data = await this.fetchBorrowList(tab.apiStatus, this.pageMap[statusKey], this.sizeMap[statusKey]);
  169. console.log('data',data)
  170. let list = data.records || [];
  171. list = this.processBorrowData(list);
  172. this.listData[statusKey] = list;
  173. this.hasMoreMap[statusKey] = list.length === this.sizeMap[statusKey];
  174. this.loadMoreStatusMap[statusKey] = this.hasMoreMap[statusKey] ? "" : "no-more";
  175. } catch (err) {
  176. this.listData[statusKey] = [];
  177. } finally {
  178. this.loadingMap[statusKey] = false;
  179. this.isRefreshing = false;
  180. uni.stopPullDownRefresh();
  181. this.$nextTick(() => {
  182. this.calcSwiperHeight(statusKey);
  183. });
  184. }
  185. },
  186. // 加载更多
  187. async loadMoreList(statusKey) {
  188. if (this.loadingMap[statusKey] || !this.hasMoreMap[statusKey] || this.isRefreshing) return;
  189. this.loadMoreStatusMap[statusKey] = "loading";
  190. this.pageMap[statusKey] += 1;
  191. const tab = this.tabData.find(item => item.status === statusKey);
  192. try {
  193. const data = await this.fetchBorrowList(tab.apiStatus, this.pageMap[statusKey], this.sizeMap[statusKey]);
  194. let newList = data.records || [];
  195. newList = this.processBorrowData(newList);
  196. if (newList.length > 0) {
  197. this.listData[statusKey] = [...this.listData[statusKey], ...newList];
  198. this.hasMoreMap[statusKey] = newList.length === this.sizeMap[statusKey];
  199. this.loadMoreStatusMap[statusKey] = this.hasMoreMap[statusKey] ? "" : "no-more";
  200. } else {
  201. this.hasMoreMap[statusKey] = false;
  202. this.loadMoreStatusMap[statusKey] = "no-more";
  203. }
  204. } catch (err) {
  205. this.loadMoreStatusMap[statusKey] = "";
  206. } finally {
  207. this.$nextTick(() => {
  208. this.calcSwiperHeight(statusKey);
  209. });
  210. }
  211. },
  212. // 下拉刷新
  213. refreshList(statusKey) {
  214. this.getListData(statusKey, true);
  215. },
  216. // 模拟接口(带分页)
  217. async fetchBorrowList(apiStatus, pageNum, pageSize) {
  218. return new Promise((resolve) => {
  219. setTimeout(() => {
  220. const mock = this.getMockData(apiStatus, pageNum, pageSize);
  221. resolve({
  222. records: mock,
  223. total: mock.length * 3
  224. });
  225. }, 500);
  226. });
  227. },
  228. // 模拟分页数据
  229. getMockData(apiStatus, pageNum, pageSize) {
  230. const base = [
  231. {
  232. imgCover: "https://qiniu.aiyxlib.com/1606124577077",
  233. title: "名侦探柯南",
  234. nickname: "青山刚昌",
  235. publish: "长春出版社",
  236. isbn: "1001",
  237. desc: '精心提炼20种GPT提问方法及指令,从入门到进阶再到精通,100个。',
  238. startTime: "2026-03-01",
  239. returnTime: "2026-04-30", // 应还时间:还有10天左右 → 在借中
  240. realityTime: ""
  241. },
  242. {
  243. imgCover: "https://qiniu.aiyxlib.com/1606124577077",
  244. title: "三体",
  245. nickname: "刘慈慈",
  246. publish: "重庆出版社",
  247. isbn: "1002",
  248. desc: '精心提炼20种GPT提问方法及指令,从入门到进阶再到精通,100个。',
  249. startTime: "2026-03-10",
  250. returnTime: "2026-04-22", // 应还时间:剩2天 → 临期
  251. realityTime: ""
  252. },
  253. {
  254. imgCover: "https://qiniu.aiyxlib.com/1606124577077",
  255. title: "红楼梦",
  256. nickname: "曹雪芹",
  257. publish: "人民文学",
  258. isbn: "1003",
  259. desc: '精心提炼20种GPT提问方法及指令,从入门到进阶再到精通,100个。',
  260. startTime: "2026-02-01",
  261. returnTime: "2026-03-25", // 已过期 → 逾期
  262. realityTime: ""
  263. },
  264. {
  265. imgCover: "https://qiniu.aiyxlib.com/1606124577077",
  266. title: "西游记",
  267. nickname: "吴承恩",
  268. publish: "中华书局",
  269. isbn: "1004",
  270. desc: '精心提炼20种GPT提问方法及指令,从入门到进阶再到精通,100个。',
  271. startTime: "2026-02-10",
  272. returnTime: "2026-04-01",
  273. realityTime: "2026-03-31" // 提前归还 → 准时
  274. },
  275. {
  276. imgCover: "https://qiniu.aiyxlib.com/1606124577077",
  277. title: "水浒传",
  278. nickname: "施耐庵",
  279. publish: "人民文学出版社",
  280. isbn: "1005",
  281. desc: '精心提炼20种GPT提问方法及指令,从入门到进阶再到精通,100个。',
  282. startTime: "2026-03-05",
  283. returnTime: "2026-05-05",
  284. realityTime: ""
  285. },
  286. {
  287. imgCover: "https://qiniu.aiyxlib.com/1606124577077",
  288. title: "三国演义",
  289. nickname: "罗贯中",
  290. publish: "人民文学出版社",
  291. isbn: "1006",
  292. desc: '精心提炼20种GPT提问方法及指令,从入门到进阶再到精通,100个。',
  293. startTime: "2026-03-15",
  294. returnTime: "2026-05-15",
  295. realityTime: ""
  296. },
  297. {
  298. imgCover: "https://qiniu.aiyxlib.com/1606124577077",
  299. title: "活着",
  300. nickname: "余华",
  301. publish: "作家出版社",
  302. isbn: "1007",
  303. desc: '精心提炼20种GPT提问方法及指令,从入门到进阶再到精通,100个。',
  304. startTime: "2026-03-20",
  305. returnTime: "2026-05-20",
  306. realityTime: ""
  307. },
  308. {
  309. imgCover: "https://qiniu.aiyxlib.com/1606124577077",
  310. title: "围城",
  311. nickname: "钱钟书",
  312. publish: "人民文学出版社",
  313. isbn: "1008",
  314. desc: '精心提炼20种GPT提问方法及指令,从入门到进阶再到精通,100个。',
  315. startTime: "2026-03-25",
  316. returnTime: "2026-05-25",
  317. realityTime: ""
  318. }
  319. ];
  320. if (pageNum >= 4) { // 第4页开始返回空
  321. return [];
  322. }
  323. // 为在借中tab生成更多数据
  324. let list = [];
  325. if (apiStatus === 0) { // 在借中
  326. // 每页返回指定数量的数据
  327. const start = (pageNum - 1) * pageSize;
  328. const end = start + pageSize;
  329. for (let i = start; i < end; i++) {
  330. const item = base[i % base.length];
  331. list.push({ ...item, isbn: item.isbn + pageNum + i });
  332. }
  333. } else {
  334. list = [...base];
  335. if (pageNum > 1) {
  336. list.push(...base.map(item => ({ ...item, isbn: item.isbn + pageNum })));
  337. }
  338. }
  339. switch (apiStatus) {
  340. case -1: return list;
  341. case 0: return list;
  342. case 1: return [list[0], list[1]];
  343. case 2: return [];
  344. default: return [];
  345. }
  346. },
  347. // 计算swiper高度
  348. calcSwiperHeight(statusKey) {
  349. const selector = `.list-item-${statusKey}`;
  350. const query = uni.createSelectorQuery().in(this);
  351. query.selectAll(selector).boundingClientRect((res) => {
  352. let total = 200;
  353. if (res?.length) {
  354. total = res.reduce((t, h) => t + h.height + 8, 0);
  355. }
  356. // 加上uni-load-more组件的高度
  357. total += 80; // 估算uni-load-more组件的高度
  358. this.swiperHeightData[statusKey] = total;
  359. this.currentSwiperHeight = total;
  360. }).exec();
  361. },
  362. // tab切换
  363. tabClick(index) {
  364. this.currentIndex = index;
  365. const tab = this.getCurrentTab();
  366. if (this.currentPageScrollTop > 100) {
  367. uni.pageScrollTo({ scrollTop: 0, duration: 100 });
  368. }
  369. if (!this.listData[tab.status]?.length) {
  370. this.loadingMap[tab.status] = true;
  371. this.getListData(tab.status);
  372. } else {
  373. this.currentSwiperHeight = this.swiperHeightData[tab.status] || 400;
  374. }
  375. },
  376. onSwiperChange(e) {
  377. if (e.detail.source === "touch") {
  378. this.currentIndex = e.detail.current;
  379. }
  380. },
  381. onSwiperEnd() {
  382. const tab = this.getCurrentTab();
  383. if (!this.listData[tab.status]?.length) {
  384. this.loadingMap[tab.status] = true;
  385. this.getListData(tab.status);
  386. } else {
  387. this.currentSwiperHeight = this.swiperHeightData[tab.status] || 400;
  388. }
  389. },
  390. goToDetail(item) {
  391. uni.navigateTo({
  392. url: `/subpkg/pages/book-detail/book-detail?isbn=${item.isbn}`
  393. });
  394. }
  395. }
  396. };
  397. </script>
  398. <style lang="scss" scoped>
  399. .lending-container {
  400. background-color: #f5f5f5;
  401. min-height: 100vh;
  402. .tab-sticky {
  403. position: sticky;
  404. top: 0;
  405. z-index: 99;
  406. background: #fff;
  407. }
  408. .swiper {
  409. width: 100%;
  410. min-height: 300px;
  411. }
  412. .swiper-item {
  413. width: 100%;
  414. height: 100%;
  415. }
  416. .list-wrapper {
  417. padding: 10px;
  418. box-sizing: border-box;
  419. }
  420. .empty {
  421. text-align: center;
  422. padding: 100px 0;
  423. color: #999;
  424. font-size: 14px;
  425. }
  426. }
  427. ::v-deep .uni-load-more{
  428. height: auto !important;
  429. }
  430. </style>