Prechádzať zdrojové kódy

improvement: Optimizing the experience of the app list page (#3885)

majian 1 rok pred
rodič
commit
9d1cb1bc92

+ 14 - 7
web/app/(commonLayout)/apps/Apps.tsx

@@ -1,11 +1,12 @@
 'use client'
 
-import { useEffect, useRef, useState } from 'react'
+import { useCallback, useEffect, useRef, useState } from 'react'
 import useSWRInfinite from 'swr/infinite'
 import { useTranslation } from 'react-i18next'
 import { useDebounceFn } from 'ahooks'
 import AppCard from './AppCard'
 import NewAppCard from './NewAppCard'
+import useAppsQueryState from './hooks/useAppsQueryState'
 import type { AppListResponse } from '@/models/app'
 import { fetchAppList } from '@/service/apps'
 import { useAppContext } from '@/context/app-context'
@@ -54,10 +55,15 @@ const Apps = () => {
   const [activeTab, setActiveTab] = useTabSearchParams({
     defaultTab: 'all',
   })
-  const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
-  const [tagIDs, setTagIDs] = useState<string[]>([])
-  const [keywords, setKeywords] = useState('')
-  const [searchKeywords, setSearchKeywords] = useState('')
+  const { query: { tagIDs = [], keywords = '' }, setQuery } = useAppsQueryState()
+  const [tagFilterValue, setTagFilterValue] = useState<string[]>(tagIDs)
+  const [searchKeywords, setSearchKeywords] = useState(keywords)
+  const setKeywords = useCallback((keywords: string) => {
+    setQuery(prev => ({ ...prev, keywords }))
+  }, [setQuery])
+  const setTagIDs = useCallback((tagIDs: string[]) => {
+    setQuery(prev => ({ ...prev, tagIDs }))
+  }, [setQuery])
 
   const { data, isLoading, setSize, mutate } = useSWRInfinite(
     (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, tagIDs, searchKeywords),
@@ -81,17 +87,18 @@ const Apps = () => {
     }
   }, [])
 
+  const hasMore = data?.at(-1)?.has_more ?? true
   useEffect(() => {
     let observer: IntersectionObserver | undefined
     if (anchorRef.current) {
       observer = new IntersectionObserver((entries) => {
-        if (entries[0].isIntersecting && !isLoading)
+        if (entries[0].isIntersecting && !isLoading && hasMore)
           setSize((size: number) => size + 1)
       }, { rootMargin: '100px' })
       observer.observe(anchorRef.current)
     }
     return () => observer?.disconnect()
-  }, [isLoading, setSize, anchorRef, mutate])
+  }, [isLoading, setSize, anchorRef, mutate, hasMore])
 
   const { run: handleSearch } = useDebounceFn(() => {
     setSearchKeywords(keywords)

+ 53 - 0
web/app/(commonLayout)/apps/hooks/useAppsQueryState.ts

@@ -0,0 +1,53 @@
+import { type ReadonlyURLSearchParams, usePathname, useRouter, useSearchParams } from 'next/navigation'
+import { useCallback, useEffect, useMemo, useState } from 'react'
+
+type AppsQuery = {
+  tagIDs?: string[]
+  keywords?: string
+}
+
+// Parse the query parameters from the URL search string.
+function parseParams(params: ReadonlyURLSearchParams): AppsQuery {
+  const tagIDs = params.get('tagIDs')?.split(';')
+  const keywords = params.get('keywords') || undefined
+  return { tagIDs, keywords }
+}
+
+// Update the URL search string with the given query parameters.
+function updateSearchParams(query: AppsQuery, current: URLSearchParams) {
+  const { tagIDs, keywords } = query || {}
+
+  if (tagIDs && tagIDs.length > 0)
+    current.set('tagIDs', tagIDs.join(';'))
+  else
+    current.delete('tagIDs')
+
+  if (keywords)
+    current.set('keywords', keywords)
+  else
+    current.delete('keywords')
+}
+
+function useAppsQueryState() {
+  const searchParams = useSearchParams()
+  const [query, setQuery] = useState<AppsQuery>(() => parseParams(searchParams))
+
+  const router = useRouter()
+  const pathname = usePathname()
+  const syncSearchParams = useCallback((params: URLSearchParams) => {
+    const search = params.toString()
+    const query = search ? `?${search}` : ''
+    router.push(`${pathname}${query}`)
+  }, [router, pathname])
+
+  // Update the URL search string whenever the query changes.
+  useEffect(() => {
+    const params = new URLSearchParams(searchParams)
+    updateSearchParams(query, params)
+    syncSearchParams(params)
+  }, [query, searchParams, syncSearchParams])
+
+  return useMemo(() => ({ query, setQuery }), [query])
+}
+
+export default useAppsQueryState