index.vue 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. <template>
  2. <div class="collections">
  3. <img
  4. class="collections-banner"
  5. tabindex="0"
  6. aria-description="You've reached the banner area of the Collections section; this section has one image; please use the tab key to go through the content."
  7. :src="bannerUrl"
  8. />
  9. <div class="container">
  10. <Breadcrumb
  11. :parents="[
  12. {
  13. label: 'Collections',
  14. routeParams: {
  15. name: 'Collections',
  16. params: { type: NAV_LIST[0].type },
  17. },
  18. },
  19. ]"
  20. :cur-route="curRoute"
  21. />
  22. <div class="collections-main">
  23. <Menu />
  24. <div
  25. v-loading="loading"
  26. element-loading-background="rgba(122, 122, 122, 0.8)"
  27. class="collections-main__right"
  28. >
  29. <Waterfall :list="list" :gutter="30" :hasAroundGutter="false">
  30. <template #item="{ item, url }">
  31. <div
  32. class="collections-item"
  33. aria-label="Link"
  34. @click="handleItemClick(item)"
  35. >
  36. <img
  37. alt=""
  38. :src="url"
  39. aria-label="Image link"
  40. :style="{ height: item.imgHeight + 'px' }"
  41. />
  42. <div class="collections-item__inner">
  43. <p tabindex="0" :aria-description="item.name">
  44. {{ item.name }}
  45. </p>
  46. <p tabindex="0" :aria-description="item.digest">
  47. {{ item.digest }}
  48. </p>
  49. </div>
  50. </div>
  51. </template>
  52. </Waterfall>
  53. <p v-if="noData" class="no-more">no more</p>
  54. <div v-if="rendering && pageNum > 1" style="text-align: center">
  55. <VanLoading color="var(--van-primary-color)" />
  56. </div>
  57. </div>
  58. </div>
  59. </div>
  60. </div>
  61. <DetailDialog v-model:visible="visible" :id="checkedItemId" />
  62. </template>
  63. <script lang="ts" setup>
  64. import { Waterfall } from "vue-waterfall-plugin-next";
  65. import { useRoute } from "vue-router";
  66. import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
  67. import { getBaseURL } from "@dage/service";
  68. import { useBaseStore } from "@/stores/base";
  69. import Breadcrumb from "@/components/Breadcrumb/index.vue";
  70. import { getCollectionListApi, type CollectionListItem } from "@/api";
  71. import Menu from "./components/Menu.vue";
  72. import DetailDialog from "./components/DetailDialog/index.vue";
  73. import { NAV_LIST } from "./constants";
  74. import "vue-waterfall-plugin-next/dist/style.css";
  75. import { PaginationType, usePagination } from "@/utils/usePagination";
  76. const baseUrl = getBaseURL();
  77. const baseStore = useBaseStore();
  78. const bannerUrl = computed(() => {
  79. return (
  80. baseUrl +
  81. baseStore.bannerList.find((i) => i.name === "Collections")?.thumbPc
  82. );
  83. });
  84. const route = useRoute();
  85. const curRoute = computed(() =>
  86. NAV_LIST.find((i) => i.type === (route.params.type as string))
  87. );
  88. const visible = ref(false);
  89. const checkedItemId = ref<number | null>(null);
  90. // 当前页图片数据是否全部加载完成
  91. const rendering = ref(false);
  92. const list = ref<any[]>([]);
  93. const loading = computed(
  94. () => (rendering.value || fetchLoading.value) && pageNum.value === 1
  95. );
  96. const {
  97. pageNum,
  98. list: sourceList,
  99. noData,
  100. noMore,
  101. loading: fetchLoading,
  102. resetParams,
  103. getList,
  104. } = usePagination<CollectionListItem>(
  105. (params) => {
  106. return getCollectionListApi({
  107. type: curRoute.value?.name,
  108. ...params,
  109. });
  110. },
  111. PaginationType.DEFAULT,
  112. 20
  113. );
  114. const handleItemClick = (item: any) => {
  115. checkedItemId.value = item.id;
  116. visible.value = true;
  117. };
  118. const getImgRatio: (url: string) => Promise<number> = (url: string) => {
  119. return new Promise((res) => {
  120. const img = new Image();
  121. img.src = url;
  122. img.onload = () => {
  123. const ratio = Math.round((img.height / img.width) * 100) / 100;
  124. res(ratio);
  125. };
  126. img.onerror = () => {
  127. res(0);
  128. };
  129. });
  130. };
  131. const handleScroll = () => {
  132. // 检查用户是否滚动到页面底部
  133. if (
  134. window.innerHeight + window.scrollY >= document.body.offsetHeight &&
  135. !rendering.value &&
  136. !loading.value &&
  137. !noMore.value
  138. ) {
  139. console.log("Scrolled to bottom");
  140. pageNum.value += 1;
  141. getList();
  142. }
  143. };
  144. onMounted(() => {
  145. window.addEventListener("scroll", handleScroll);
  146. });
  147. onBeforeUnmount(() => {
  148. window.removeEventListener("scroll", handleScroll);
  149. });
  150. watch(sourceList, async (v) => {
  151. if (!v.length) return;
  152. rendering.value = true;
  153. try {
  154. for (const i of v) {
  155. const url = baseUrl + i.thumb;
  156. const ratio = await getImgRatio(url);
  157. list.value.push({
  158. ...i,
  159. imgHeight: 303 * ratio,
  160. src: url,
  161. });
  162. }
  163. } finally {
  164. rendering.value = false;
  165. }
  166. });
  167. watch(
  168. route,
  169. () => {
  170. if (route.name !== "Collections") return;
  171. list.value = [];
  172. resetParams();
  173. getList();
  174. },
  175. {
  176. immediate: true,
  177. }
  178. );
  179. </script>
  180. <style lang="scss" scoped>
  181. @import "./index.scss";
  182. </style>