From 61b79730aef6e70fad6d98d020d0ddbb45225677 Mon Sep 17 00:00:00 2001 From: n0099 Date: Mon, 19 Feb 2024 20:17:44 +0800 Subject: [PATCH] + ref `queryParam` & `shouldFetch` * rename ref `currentRoute` to `lastFetchingRoute` * inline ref `title` and its mutating in `fetchPosts()` to its usage inside `useHead()` * move the scrolling viewport to `postListItemScrollPosition()` from `parseRouteThenFetch()` to `watchEffect()` - ref `postPages`, `isFetching` & `lastFetchError` in favor of `useApiPosts` - `v-(if|show)` directive on `` @ `` + param `enabled` and wrap param `queryParam` in `ref<>` to enable reactivity @ `api/index.ts.useApi*()` @ fe --- fe/src/api/index.ts | 29 +++++---- fe/src/views/Post.vue | 136 ++++++++++++++++++++---------------------- 2 files changed, 81 insertions(+), 84 deletions(-) diff --git a/fe/src/api/index.ts b/fe/src/api/index.ts index 3cfc175c..dcfc734a 100644 --- a/fe/src/api/index.ts +++ b/fe/src/api/index.ts @@ -1,4 +1,5 @@ import type { Api, ApiError, ApiForums, ApiPosts, ApiStatsForumPostCount, ApiStatus, ApiUsers, Cursor, CursorPagination } from '@/api/index.d'; +import type { MaybeRefOrGetter, Ref } from 'vue'; import type { InfiniteData, QueryFunctionContext, QueryKey } from '@tanstack/vue-query'; import { useInfiniteQuery, useQuery } from '@tanstack/vue-query'; import nprogress from 'nprogress'; @@ -80,22 +81,26 @@ const useApi = < TApi extends Api, TResponse = TApi['response'], TQueryParam = TApi['queryParam']> -(endpoint: string, requesterGetter: ReqesuterGetter) => (queryParam?: TQueryParam) => - useQuery({ - queryKey: [endpoint, queryParam], - queryFn: requesterGetter(`/${endpoint}`, queryParam) - }); +(endpoint: string, requesterGetter: ReqesuterGetter) => + (queryParam?: Ref, enabled?: MaybeRefOrGetter) => + useQuery({ + queryKey: [endpoint, queryParam], + queryFn: requesterGetter(`/${endpoint}`, queryParam?.value), + enabled + }); const useApiWithCursor = < TApi extends Api, TResponse = TApi['response'] & CursorPagination, TQueryParam = TApi['queryParam']> -(endpoint: string, requesterGetter: ReqesuterGetter) => (queryParam?: TQueryParam) => - useInfiniteQuery, QueryKey, Cursor>({ - queryKey: [endpoint, queryParam], - queryFn: requesterGetter(`/${endpoint}`, queryParam), - initialPageParam: '', - getNextPageParam: lastPage => lastPage.pages.nextCursor - }); +(endpoint: string, requesterGetter: ReqesuterGetter) => + (queryParam?: Ref, enabled?: MaybeRefOrGetter) => + useInfiniteQuery, QueryKey, Cursor>({ + queryKey: [endpoint, queryParam], + queryFn: requesterGetter(`/${endpoint}`, queryParam?.value), + initialPageParam: '', + getNextPageParam: lastPage => lastPage.pages.nextCursor, + enabled + }); export const useApiForums = () => useApi('forums', getRequester)(); export const useApiStatus = useApi('status', getRequesterWithReCAPTCHA); diff --git a/fe/src/views/Post.vue b/fe/src/views/Post.vue index ef9d936d..42e760f3 100644 --- a/fe/src/views/Post.vue +++ b/fe/src/views/Post.vue @@ -2,26 +2,26 @@

当前页数:{{ getRouteCursorParam(route) }}

- + 列表视图 表格视图
-
+
- +
- +
- - + +
@@ -32,17 +32,18 @@ import QueryForm from '@/components/Post/queryForm/QueryForm.vue'; import PlaceholderError from '@/components/placeholders/PlaceholderError.vue'; import PlaceholderPostList from '@/components/placeholders/PlaceholderPostList.vue'; -import { apiPosts, isApiError } from '@/api'; -import type { ApiError, ApiPosts, Cursor } from '@/api/index.d'; +import { useApiPosts } from '@/api'; +import type { ApiPosts, Cursor } from '@/api/index.d'; import { getReplyTitleTopOffset, postListItemScrollPosition } from '@/components/Post/renderers/rendererList'; import { compareRouteIsNewQuery, getRouteCursorParam } from '@/router'; import type { ObjUnknown } from '@/shared'; import { notyShow, scrollBarWidth, titleTemplate } from '@/shared'; import { useTriggerRouteUpdateStore } from '@/stores/triggerRouteUpdate'; -import { computed, nextTick, onMounted, ref } from 'vue'; +import { computed, nextTick, onMounted, ref, watchEffect } from 'vue'; import type { RouteLocationNormalized } from 'vue-router'; import { onBeforeRouteUpdate, useRoute } from 'vue-router'; +import { watchOnce } from '@vueuse/core'; import { Menu, MenuItem } from 'ant-design-vue'; import { useHead } from '@unhead/vue'; import * as _ from 'lodash-es'; @@ -50,57 +51,47 @@ import * as _ from 'lodash-es'; export type PostRenderer = 'list' | 'table'; const route = useRoute(); -const title = ref('帖子查询'); -const postPages = ref([]); -const isFetching = ref(false); -const lastFetchError = ref(null); -const showPlaceholderPostList = ref(true); +const queryParam = ref(); +const shouldFetch = ref(false); +const { data, error, isFetching, isFetchedAfterMount } = useApiPosts(queryParam, shouldFetch); const selectedRenderTypes = ref<[PostRenderer]>(['list']); const renderType = computed(() => selectedRenderTypes.value[0]); const queryFormRef = ref>(); -const currentRoute = ref(route); -useHead({ title: computed(() => titleTemplate(title.value)) }); +const lastFetchingRoute = ref(route); +useHead({ + title: computed(() => titleTemplate((() => { + const firstPostPage = data.value?.pages[0]; + if (firstPostPage === undefined) + return '帖子查询'; + + const forumName = `${firstPostPage.forum.name}吧`; + const threadTitle = firstPostPage.threads[0].title; + // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check + switch (queryFormRef.value?.getCurrentQueryType()) { + case 'fid': + case 'search': + return `${forumName} - 帖子查询`; + case 'postID': + return `${threadTitle} - ${forumName} - 帖子查询`; + } + + return '帖子查询'; + })())) +}); const fetchPosts = async (queryParams: ObjUnknown[], isNewQuery: boolean, cursor: Cursor) => { const startTime = Date.now(); - lastFetchError.value = null; - showPlaceholderPostList.value = true; - if (isNewQuery) - postPages.value = []; - isFetching.value = true; - - const query = await apiPosts({ + queryParam.value = { query: JSON.stringify(queryParams), cursor: isNewQuery ? undefined : cursor - }).finally(() => { - showPlaceholderPostList.value = false; - isFetching.value = false; + }; + shouldFetch.value = true; + watchOnce(isFetchedAfterMount, value => { + if (value) + shouldFetch.value = false; }); - - if (isApiError(query)) { - lastFetchError.value = query; - + if (error.value !== null) return false; - } - if (isNewQuery) - postPages.value = [query]; - else - postPages.value.push(query); // todo: unshift when fetching previous page - - const forumName = `${postPages.value[0].forum.name}吧`; - const threadTitle = postPages.value[0].threads[0].title; - switch (queryFormRef.value?.getCurrentQueryType()) { - case 'fid': - case 'search': - title.value = `${forumName} - 帖子查询`; - break; - case 'postID': - title.value = `${threadTitle} - ${forumName} - 帖子查询`; - break; - case 'empty': - case undefined: - throw new Error(queryFormRef.value?.getCurrentQueryType()); - } const networkTime = Date.now() - startTime; await nextTick(); // wait for child components finish dom update @@ -110,6 +101,7 @@ const fetchPosts = async (queryParams: ObjUnknown[], isNewQuery: boolean, cursor return true; }; + const scrollToPostListItem = (el: Element) => { // simply invoke el.scrollIntoView() for only once will scroll the element to the top of the viewport // and then some other elements above it such as img[loading='lazy'] may change its box size @@ -133,41 +125,41 @@ const scrollToPostListItem = (el: Element) => { tryScroll(); addEventListener('scrollend', tryScroll); }; +watchEffect(() => { + if (isFetchedAfterMount.value && renderType.value === 'list') { + const scrollPosition = postListItemScrollPosition(lastFetchingRoute.value); + if (scrollPosition === false) + return; + const el = document.querySelector(scrollPosition.el); + if (el === null) + return; + requestIdleCallback(function retry(deadline) { + if (deadline.timeRemaining() > 0) + scrollToPostListItem(el); + else + requestIdleCallback(retry); + }); + } +}); + const parseRouteThenFetch = async (newRoute: RouteLocationNormalized, isNewQuery: boolean, cursor: Cursor) => { if (queryFormRef.value === undefined) return false; const flattenParams = await queryFormRef.value.parseRouteToGetFlattenParams(newRoute); if (flattenParams === false) return false; - currentRoute.value = newRoute; + lastFetchingRoute.value = newRoute; const isFetchSuccess = await fetchPosts(flattenParams, isNewQuery, cursor); - if (isFetchSuccess && renderType.value === 'list') { - (() => { - const scrollPosition = postListItemScrollPosition(newRoute); - if (scrollPosition === false) - return; - const el = document.querySelector(scrollPosition.el); - if (el === null) - return; - requestIdleCallback(function retry(deadline) { - if (deadline.timeRemaining() > 0) - scrollToPostListItem(el); - else - requestIdleCallback(retry); - }); - })(); - } return isFetchSuccess; }; - onBeforeRouteUpdate(async (to, from) => { const isNewQuery = useTriggerRouteUpdateStore() .isTriggeredBy('@submit', { ...to, force: true }) || compareRouteIsNewQuery(to, from); const cursor = getRouteCursorParam(to); if (!(isNewQuery || _.isEmpty(_.filter( - postPages.value, + data.value?.pages, i => i.pages.currentCursor === cursor )))) return true;